mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-09 05:44:33 -05:00
Compare commits
7 Commits
renovate/m
...
b5f9c24631
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5f9c24631 | ||
|
|
5bdb945406 | ||
|
|
3064f51d25 | ||
|
|
edf376e3dd | ||
|
|
dff66547b4 | ||
|
|
6fef27bb56 | ||
|
|
505e966653 |
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(" +
|
||||
|
||||
50
server/src/test/kotlin/suwayomi/tachidesk/SafePathTest.kt
Normal file
50
server/src/test/kotlin/suwayomi/tachidesk/SafePathTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user