Fix WebDAV upload of non-ASCII/special filenames (URL + MOVE header encoding)
Build & Release APK / build (push) Successful in 12m50s

Volume test (100 files) surfaced it: files with non-ASCII names (e.g. 'naïve
café.txt') failed to upload — url() built a raw string, so the MOVE Destination
header carried non-ASCII chars that OkHttp rejects. Now url() percent-encodes
each path segment via HttpUrl.addPathSegments (also covers '&', spaces, CJK).
Regression test specialAndNonAsciiNames_upload added.
This commit is contained in:
2026-06-05 14:38:52 +00:00
parent 39aa2f7dfd
commit 1ecae2c690
2 changed files with 25 additions and 2 deletions
@@ -236,7 +236,24 @@ class FullSyncEngineTest {
assertEquals("newer local must win", "local-newer", remoteText("$remote/n.txt")) assertEquals("newer local must win", "local-newer", remoteText("$remote/n.txt"))
} }
// ── 13. Content integrity: binary-ish bytes round-trip exactly ─────────── // ── 13b. Special & non-ASCII filenames upload (WebDAV URL/header encoding)
@Test fun specialAndNonAsciiNames_upload() = runBlocking {
val (pair, local, remote) = newPair("special", SyncDirection.UPLOAD_ONLY, DeleteBehavior.KEEP)
write(local, "naïve café.txt", "accents") // non-ASCII (broke MOVE Destination header)
write(local, "a&b (1).txt", "ampersand") // & ( ) space
write(local, "日本語.txt", "cjk") // multibyte unicode
write(local, "my photo.txt", "space")
val r = sync(pair)
assertEquals("all special-name files must upload", 4, r.uploaded)
assertEquals(0, r.failedFiles)
val names = remoteNames(remote)
assertTrue("naïve café.txt" in names)
assertTrue("a&b (1).txt" in names)
assertTrue("日本語.txt" in names)
assertTrue("my photo.txt" in names)
}
// ── 14. Content integrity: binary-ish bytes round-trip exactly ────────────
@Test fun contentIntegrity_roundTrip() = runBlocking { @Test fun contentIntegrity_roundTrip() = runBlocking {
val (pair, local, remote) = newPair("integrity", SyncDirection.TWO_WAY) val (pair, local, remote) = newPair("integrity", SyncDirection.TWO_WAY)
val payload = (0..5000).joinToString("") { "Ω$it·" } val payload = (0..5000).joinToString("") { "Ω$it·" }
@@ -181,7 +181,13 @@ open class WebDavProvider(protected val account: CloudAccount) : CloudProvider {
} }
} }
protected fun url(path: String) = "$baseUrl/${path.trimStart('/')}" // Build a properly percent-encoded URL. addPathSegments encodes each segment (spaces,
// ampersands, and — critically — non-ASCII like "café"), which keeps OkHttp from rejecting
// non-ASCII in the WebDAV MOVE "Destination" header and avoids malformed request URLs.
protected fun url(path: String): String {
val base = baseUrl.toHttpUrlOrNull() ?: return "$baseUrl/${path.trimStart('/')}"
return base.newBuilder().addPathSegments(path.trimStart('/')).build().toString()
}
private fun parsePropfind(xml: String, parentPath: String, dropFirst: Boolean = true): List<RemoteFile> { private fun parsePropfind(xml: String, parentPath: String, dropFirst: Boolean = true): List<RemoteFile> {
val results = mutableListOf<RemoteFile>() val results = mutableListOf<RemoteFile>()