From e8c6c4ebb3d76ee280043bb66cf499584c591310 Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 23 Aug 2026 00:00:00 +0000 Subject: app: report config limits instead of crashing The remote and folder counts and the serialized config size are enforced only inside save, which throws. Every other rule an editor can break is reported in the form, so a config that outgrew a limit crashed the activity instead of saying so. overLimit answers before the entry is added, and the editors report the answer. hostKeyAllowed reconstructs the known_hosts address that rsh writes with knownhosts.Normalize, and nothing recorded or tested that coupling. Normalize leaves the host bare on port 22 for IPv6 literals too, so bracketing them here looks right and would reject every IPv6 pin. --- app/src/test/java/invalid/lena/rsend/ConfigTest.kt | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) (limited to 'app/src/test/java') diff --git a/app/src/test/java/invalid/lena/rsend/ConfigTest.kt b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt index 6b6dfdc..e289afd 100644 --- a/app/src/test/java/invalid/lena/rsend/ConfigTest.kt +++ b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt @@ -308,6 +308,30 @@ class ConfigTest { assertFalse(RemoteRules.hostKeyAllowed(Remote("nas", "home", 22, "backup", "home ssh-ed25519 not-base64"))) } + // The address field of a pin must be exactly what rsh writes, which is + // knownhosts.Normalize: the bare host on port 22 (an IPv6 literal included), + // "[host]:port" otherwise. Bracketing IPv6 on port 22 looks right and would + // make every IPv6 pin fail to validate. + @Test + fun hostPinAddressMatchesTheKnownHostsForm() { + val key = "ssh-ed25519 AAAA" + val cases = listOf( + "home.example.net" to 22, + "home.example.net" to 2222, + "2001:db8::1" to 22, + "2001:db8::1" to 2222, + "fe80::1%wlan0" to 22, + ) + for ((host, port) in cases) { + val address = if (port == 22) host else "[$host]:$port" + assertTrue( + "$host:$port", + RemoteRules.hostKeyAllowed(Remote("nas", host, port, "backup", "$address $key")), + ) + } + assertFalse(RemoteRules.hostKeyAllowed(Remote("nas", "2001:db8::1", 22, "backup", "[2001:db8::1] $key"))) + } + @Test fun configRejectsCoercedAndUnknownFields() { val badRemotes = Config().toJson().put("remotes", "not an array") @@ -370,4 +394,37 @@ class ConfigTest { } catch (_: IllegalArgumentException) { } } + + // The editors ask before adding an entry, because these three limits are + // the ones a form cannot see. Reporting them is what keeps save from + // throwing at a user who has done nothing wrong. + @Test + fun limitsAreReportedRatherThanThrown() { + val remote = Remote("nas", "nas", 22, "backup", "nas ssh-ed25519 AAAA") + val folder = Folder("f", "/a", "nas", "backup") + assertEquals(null, Config.overLimit(Config(remotes = listOf(remote), folders = listOf(folder)))) + + val remotes = (1..65).map { remote.copy(name = "nas$it", hostKey = "") } + assertTrue(Config.overLimit(Config(remotes = remotes))!!.contains("Too many remotes")) + + val folders = (1..257).map { folder.copy(name = "f$it", remotePath = "backup/$it") } + assertTrue( + Config.overLimit(Config(remotes = listOf(remote), folders = folders))!! + .contains("Too many folders") + ) + + // Field limits alone allow far more than the file limit: 40 folders of + // maximum-length excludes already exceed it. + val fat = (1..40).map { + folder.copy( + name = "f$it", + remotePath = "backup/$it", + excludes = List(FolderRules.MAX_EXCLUDES) { i -> "x".repeat(512 - "$i".length) + i }, + ) + } + assertTrue( + Config.overLimit(Config(remotes = listOf(remote), folders = fat))!! + .contains("too large") + ) + } } -- cgit v1.2.3