Compare commits

..

7 Commits

Author SHA1 Message Date
renovate[bot]
b5f9c24631 Update dependency com.github.Suwayomi:exposed-migrations to v3.10.1 2026-05-11 23:21:11 +00:00
Akiaki0324
5bdb945406 fix: truncate filenames by bytes instead of characters to avoid File name too long (#1933)
* fix: truncate filenames by bytes instead of characters to avoid IOException File name too long

* add a CHANGELOG.md entry.
2026-05-10 19:02:11 -04:00
David Brochero
3064f51d25 fix: don't resuse invalidated cf_clearance cookie on CloudFlareInterceptor (#1916)
* fix: let FlareSolverr handle it's own `cf_clearance` cookie

also dedups cookies

* linting

* suggested changes

* my bad

* add to changelog
2026-05-10 19:01:51 -04:00
renovate[bot]
edf376e3dd Update graphqlkotlin to v10 alpha (major) (#1923)
* Update graphqlkotlin to v9

* Update to the v10 alpha due to nullability issues in v9

* Fixes

* Remove asDataFetcherResult

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2026-05-10 19:01:34 -04:00
renovate[bot]
dff66547b4 Update jackson monorepo (#1906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 18:16:05 -04:00
Mitchell Syer
6fef27bb56 Wait until WebUI is ready to open in browser (#2010)
* Wait until WebUI is ready

* Changelog

* Move openInBrowser out of timeout
2026-05-09 18:15:43 -04:00
Mitchell Syer
505e966653 Fix Polyglot (#2011) 2026-05-09 18:15:33 -04:00
8 changed files with 114 additions and 14 deletions

View File

@@ -9,6 +9,9 @@ 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
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,
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
@@ -27,11 +30,41 @@ object SafePath {
sb.append('_')
}
}
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
return sb.toString().take(240)
return truncateFilename(sb.toString())
}
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.
*/

View File

@@ -13,7 +13,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- .
### Fixed
- (CloudFlareInterceptor) Don't send the `cf_clearance` cookie back to Flaresolverr
- (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

View File

@@ -6,7 +6,7 @@ jvmTarget = "21"
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
javalin = "7.2.0"
jte = "3.2.4"
jackson = "3.1.2" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.61.0"
dex2jar = "2.4.36"
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" }
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20"
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.21"
jte = { module = "gg.jte:jte", version.ref = "jte" }
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
@@ -73,7 +73,7 @@ h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v
hikaricp = "com.zaxxer:HikariCP:7.0.2"
# Exposed Migrations
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.8.0"
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.10.1"
# Dependency Injection
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
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)
}
}