Compare commits

..

4 Commits

Author SHA1 Message Date
Syer10
5561761020 Remove asDataFetcherResult 2026-05-10 12:32:24 -04:00
Syer10
a210153ed1 Fixes 2026-05-09 15:41:21 -04:00
Syer10
b40447c4f9 Update to the v10 alpha due to nullability issues in v9 2026-05-09 15:29:34 -04:00
renovate[bot]
193dd1ee84 Update graphqlkotlin to v9 2026-05-09 18:54:15 +00:00
8 changed files with 13 additions and 113 deletions

View File

@@ -9,9 +9,6 @@ package xyz.nulldev.androidcompat.util
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt // adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
object SafePath { object SafePath {
private const val MAX_FILENAME_CHARS = 240
private const val MAX_FILENAME_UTF8_BYTES = 240
/** /**
* Mutate the given filename to make it valid for a FAT filesystem, * Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting * replacing any invalid characters with "_". This method doesn't allow hidden files (starting
@@ -30,41 +27,11 @@ object SafePath {
sb.append('_') sb.append('_')
} }
} }
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
return truncateFilename(sb.toString()) // ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
return sb.toString().take(240)
} }
private fun truncateFilename(filename: String): String {
// Keep a safety margin under common filesystem limits and satisfy both
// character count and UTF-8 byte-length constraints.
val output = StringBuilder(minOf(filename.length, MAX_FILENAME_CHARS))
var usedBytes = 0
var index = 0
while (index < filename.length && output.length < MAX_FILENAME_CHARS) {
val codePoint = Character.codePointAt(filename, index)
val codePointBytes = utf8ByteCount(codePoint)
if (usedBytes + codePointBytes > MAX_FILENAME_UTF8_BYTES) {
break
}
output.appendCodePoint(codePoint)
usedBytes += codePointBytes
index += Character.charCount(codePoint)
}
return output.toString()
}
private fun utf8ByteCount(codePoint: Int): Int =
when {
codePoint <= 0x7f -> 1
codePoint <= 0x7ff -> 2
codePoint <= 0xffff -> 3
else -> 4
}
/** /**
* Returns true if the given character is a valid filename character, false otherwise. * Returns true if the given character is a valid filename character, false otherwise.
*/ */

View File

@@ -13,10 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- . - .
### Fixed ### Fixed
- (CloudFlareInterceptor) Don't send the `cf_clearance` cookie back to Flaresolverr
- (WebUI) Handle serving non-default webui with "bundled" - (WebUI) Handle serving non-default webui with "bundled"
- (WebUI) Wait until WebUI is ready to open in browser
- (Downloads) Truncate filenames by byte length to prevent "File name too long" IO errors
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08 ## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08

View File

@@ -6,7 +6,7 @@ jvmTarget = "21"
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
javalin = "7.2.0" javalin = "7.2.0"
jte = "3.2.4" jte = "3.2.4"
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` jackson = "3.1.2" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.61.0" exposed = "0.61.0"
dex2jar = "2.4.36" dex2jar = "2.4.36"
polyglot = "25.0.3" polyglot = "25.0.3"
@@ -54,7 +54,7 @@ javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javali
javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" } javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" } jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.21" jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20"
jte = { module = "gg.jte:jte", version.ref = "jte" } jte = { module = "gg.jte:jte", version.ref = "jte" }
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" } kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }

View File

@@ -171,7 +171,6 @@ tasks {
"Implementation-Vendor" to "The Suwayomi Project", "Implementation-Vendor" to "The Suwayomi Project",
"Specification-Version" to getTachideskVersion(), "Specification-Version" to getTachideskVersion(),
"Implementation-Version" to getTachideskRevision(), "Implementation-Version" to getTachideskRevision(),
"Multi-Release" to true, // needed for polyglot
) )
} }
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)

View File

@@ -103,7 +103,7 @@ class CloudflareInterceptor(
companion object { companion object {
private val ERROR_CODES = listOf(403, 503) private val ERROR_CODES = listOf(403, 503)
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
val COOKIE_NAMES = listOf("cf_clearance") private val COOKIE_NAMES = listOf("cf_clearance")
private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""<title>(.*?) \(\d+×\d+\)</title>""") private val CHROME_IMAGE_TEMPLATE_REGEX = Regex("""<title>(.*?) \(\d+×\d+\)</title>""")
} }
} }
@@ -205,12 +205,9 @@ object CFClearance {
session = serverConfig.flareSolverrSessionName.value, session = serverConfig.flareSolverrSessionName.value,
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value, sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
cookies = cookies =
network.cookieStore network.cookieStore.get(originalRequest.url).map {
.get(originalRequest.url) FlareSolverCookie(it.name, it.value)
.filter { it.name !in CloudflareInterceptor.COOKIE_NAMES } },
.map { cookie ->
FlareSolverCookie(cookie.name, cookie.value)
},
returnOnlyCookies = onlyCookies, returnOnlyCookies = onlyCookies,
maxTimeout = timeout.inWholeMilliseconds.toInt(), maxTimeout = timeout.inWholeMilliseconds.toInt(),
postData = postData =

View File

@@ -27,10 +27,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.future.future import kotlinx.coroutines.future.future
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.global.GlobalAPI
import suwayomi.tachidesk.graphql.GraphQL import suwayomi.tachidesk.graphql.GraphQL
@@ -54,7 +51,6 @@ import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.text.get import kotlin.text.get
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
object JavalinSetup { object JavalinSetup {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -136,12 +132,7 @@ object JavalinSetup {
config.events.serverStarted { config.events.serverStarted {
if (serverConfig.initialOpenInBrowserEnabled.value) { if (serverConfig.initialOpenInBrowserEnabled.value) {
scope.launch { Browser.openInBrowser()
withTimeoutOrNull(10.seconds) {
WebInterfaceManager.isSetupComplete.first { it }
}
Browser.openInBrowser()
}
} }
} }
} }

View File

@@ -27,7 +27,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
@@ -91,7 +90,7 @@ object WebInterfaceManager {
private val preferences = Injekt.get<Application>().getSharedPreferences("server_util", Context.MODE_PRIVATE) private val preferences = Injekt.get<Application>().getSharedPreferences("server_util", Context.MODE_PRIVATE)
private var currentUpdateTaskId: String = "" private var currentUpdateTaskId: String = ""
val isSetupComplete = MutableStateFlow(false) private var isSetupComplete = false
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
@@ -197,7 +196,7 @@ object WebInterfaceManager {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
GlobalScope.launchIO { GlobalScope.launchIO {
setupWebUI() setupWebUI()
isSetupComplete.value = true isSetupComplete = true
} }
} }
@@ -261,7 +260,7 @@ object WebInterfaceManager {
val lastAutomatedUpdate = preferences.getLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis()) val lastAutomatedUpdate = preferences.getLong(LAST_WEBUI_UPDATE_CHECK_KEY, System.currentTimeMillis())
val task = { val task = {
if (isSetupComplete.value) { if (isSetupComplete) {
val log = val log =
KotlinLogging.logger( KotlinLogging.logger(
"${logger.name}::scheduleWebUIUpdateCheck(" + "${logger.name}::scheduleWebUIUpdateCheck(" +

View File

@@ -1,50 +0,0 @@
package suwayomi.tachidesk
import xyz.nulldev.androidcompat.util.SafePath
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SafePathTest {
@Test
fun invalidCharactersAreReplacedAndEdgesAreTrimmed() {
val input = " .a:b*c?d<e>f|g\\h/i. "
val result = SafePath.buildValidFilename(input)
assertEquals("a_b_c_d_e_f_g_h_i", result)
}
@Test
fun emptyAfterTrimReturnsInvalidMarker() {
assertEquals("(invalid)", SafePath.buildValidFilename(" ... . "))
}
@Test
fun resultIsTruncatedTo240Characters() {
val input = "a".repeat(300)
val result = SafePath.buildValidFilename(input)
assertEquals(240, result.length)
assertEquals("a".repeat(240), result)
}
@Test
fun mixed256CharactersCanExceed255Utf8Bytes() {
val mixed256 =
buildString {
repeat(128) {
append('a')
append('你')
}
}
val result = SafePath.buildValidFilename(mixed256)
assertEquals(120, result.length)
assertEquals(mixed256.take(120), result)
assertEquals(240, result.toByteArray(Charsets.UTF_8).size)
assertTrue(result.toByteArray(Charsets.UTF_8).size == 240)
}
}