From 468aeeebc706ee22d0a7107e0a1ce840d66d9b7b Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 10 Oct 2026 01:27:34 +0200 Subject: [PATCH] Test Android file wire formats and nonempty directory status --- tests/test_member_files.c | 8 +- .../utun/chat/data/MemberFilesProtocolTest.kt | 79 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberFilesProtocolTest.kt diff --git a/tests/test_member_files.c b/tests/test_member_files.c index 8a23874a..272aad77 100644 --- a/tests/test_member_files.c +++ b/tests/test_member_files.c @@ -133,6 +133,12 @@ static void storage_test(const char* directory) { snprintf(q.path, sizeof(q.path), "renamed"); memset(q.version, 0, 16); run(s, 55, 77, MF_STAT, MF_OK); memcpy(q.version, r.version, 16); run(s, 55, 88, MF_REMOVE_PATH, MF_ACCESS_DENIED); run(s, 55, 77, MF_REMOVE_PATH, MF_OK); snprintf(q.path, sizeof(q.path), "folder"); memset(q.version, 0, 16); run(s, 55, 77, MF_MKDIR, MF_OK); + snprintf(q.path, sizeof(q.path), "folder/child"); run(s, 55, 77, MF_MKDIR, MF_OK); + snprintf(q.path, sizeof(q.path), "folder"); run(s, 55, 77, MF_STAT, MF_OK); memcpy(q.version, r.version, 16); + run(s, 55, 77, MF_REMOVE_PATH, MF_DIRECTORY_NOT_EMPTY); + memset(q.version, 0, 16); snprintf(q.path, sizeof(q.path), "folder/child"); run(s, 55, 77, MF_STAT, MF_OK); + memcpy(q.version, r.version, 16); run(s, 55, 77, MF_REMOVE_PATH, MF_OK); + memset(q.version, 0, 16); q.path[0] = 0; run(s, 55, 88, MF_LIST, MF_OK); EXPECT(r.data_len > 0, "directory listing"); #ifndef _WIN32 char link[2048]; snprintf(link, sizeof(link), "%s/55/77/link", directory); @@ -257,7 +263,7 @@ static void count_test(const char* directory) { check_counts(s, 10, 101, 2, 2); check_counts(s, 10, 202, 1, 0); count_mutation(s, 10, 101, "folder", "moved", MF_RENAME, MF_OK); check_counts(s, 10, 101, 2, 2); - count_mutation(s, 10, 101, "moved", NULL, MF_REMOVE_PATH, MF_IO_ERROR); + count_mutation(s, 10, 101, "moved", NULL, MF_REMOVE_PATH, MF_DIRECTORY_NOT_EMPTY); check_counts(s, 10, 101, 2, 2); count_mutation(s, 10, 101, "moved/zero", NULL, MF_REMOVE_PATH, MF_OK); mf_storage_execute(s, 10, 101, &q, &r); EXPECT(r.status == MF_OK, "file remove replay"); diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberFilesProtocolTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberFilesProtocolTest.kt new file mode 100644 index 00000000..c2fb8bc8 --- /dev/null +++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/MemberFilesProtocolTest.kt @@ -0,0 +1,79 @@ +package com.utun.chat.data + +import org.junit.Assert.* +import org.junit.Test +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class MemberFilesProtocolTest { + private val uid = 0xfedcba9876543210uL.toLong() + private fun buffer(size: Int) = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN) + private fun reply(path: String = "", status: Int = 0): ByteArray { + val wire = MemberFilesProtocol.request(71, uid, MemberFilesProtocol.LIST, path) + wire[1] = 2; wire[3] = status.toByte() + return buffer(16 + wire.size).putLong(uid).putLong(23).put(wire).array() + } + private fun entry(name: String, type: Int = 1, size: Long = 400): ByteArray { + val bytes = name.toByteArray() + return buffer(35 + bytes.size).putShort(bytes.size.toShort()).put(type.toByte()).putLong(size).putLong(123) + .put(ByteArray(16) { 7 }).put(bytes).array() + } + @Test fun unicodeNamesAndUnsignedIdentityRoundTrip() { + val r = MemberFilesProtocol.response(reply("Документы/😀.pdf")) + assertEquals(uid, r.group); assertEquals(uid, r.owner); assertEquals(23L, r.server) + assertEquals(71L, r.id); assertEquals("Документы/😀.pdf", r.path) + val e = MemberFilesProtocol.entries(entry("😀.pdf")).single() + assertEquals("😀.pdf", e.name); assertEquals(400L, e.size); assertEquals(123L, e.mtime) + assertArrayEquals(ByteArray(16) { 7 }, e.version) + } + @Test fun portablePathsAndUtf8ByteLimits() { + listOf("", "photos/夏.jpg", "Документы/😀.pdf", "a".repeat(255)).forEach { assertTrue(it, MemberFilesProtocol.validPath(it)) } + listOf("../file", "a//b", "/a", "a/", ".hidden", "a\\b", "CON.txt", "com1", "a.", "a ", "a\u0000b", + "a".repeat(256), "я".repeat(128), (1..61).joinToString("/") { "a" }, "bad\uD800") + .forEach { assertFalse(it, MemberFilesProtocol.validPath(it)) } + assertFalse(MemberFilesProtocol.validPath("", false)) + assertTrue(MemberFilesProtocol.servicePath("Avatars/file")); assertFalse(MemberFilesProtocol.servicePath("photos/avatars")) + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.child("docs", "a/b") } + } + @Test fun paginationAndMutationKeepTheSnapshotToken() { + val token = ByteArray(16) { it.toByte() } + val wire = MemberFilesProtocol.request(71, uid, MemberFilesProtocol.RENAME, "docs/a", "docs/b", version = token) + assertArrayEquals(token, wire.copyOfRange(45, 61)) + wire[1] = 2 + val r = MemberFilesProtocol.response(buffer(16 + wire.size).putLong(uid).putLong(23).put(wire).array()) + assertEquals("docs/a", r.path); assertArrayEquals(token, r.version) + val page = MemberFilesProtocol.request(72, uid, MemberFilesProtocol.LIST, "docs", cursor = 82, version = token) + assertEquals(82L, ByteBuffer.wrap(page).order(ByteOrder.LITTLE_ENDIAN).getLong(37)) + assertThrows(IllegalArgumentException::class.java) { + MemberFilesProtocol.request(71, uid, MemberFilesProtocol.REMOVE, "") + } + } + @Test fun malformedDirectoryCannotBecomeASuccessfulPartialList() { + val e = entry("file") + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.entries(e + e) } + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.entries(e.copyOf(e.size - 1)) } + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.entries(entry("../x")) } + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.entries(entry("x", 3)) } + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.entries(entry("x", size = -1)) } + assertThrows(Exception::class.java) { MemberFilesProtocol.entries(entry("x").also { it[35] = 0xff.toByte() }) } + } + @Test fun errorsAndMalformedFramingArePreserved() { + assertEquals(14, MemberFilesProtocol.response(reply(status = 14)).status) + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.response(reply(status = 15)) } + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.response(reply().copyOf(140)) } + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.response(reply().also { it[134] = 2 }) } + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.response(reply("abc").also { it[141] = 0 }) } + } + @Test fun quotaAndProgressValidate64BitValues() { + val q = MemberFilesProtocol.quota(buffer(40).putLong(12).putLong(1000).putLong(34).putLong(5).putLong(6).array()) + assertEquals(34L, q.reserved); assertEquals(6L, q.directories) + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.quota(ByteArray(40)) } + val wire = buffer(51).putLong(71).putLong(uid).putLong(23).putLong(uid).putLong(20).putLong(40).putShort(0).put(1).array() + val progress = MemberFilesProtocol.progress(wire) + assertFalse(progress.finished); assertEquals(uid, progress.owner); assertEquals(20L, progress.done) + wire[50] = 2; wire[48] = 12 + assertTrue(MemberFilesProtocol.progress(wire).finished); assertEquals(12, MemberFilesProtocol.progress(wire).status) + ByteBuffer.wrap(wire).order(ByteOrder.LITTLE_ENDIAN).putLong(32, 41) + assertThrows(IllegalArgumentException::class.java) { MemberFilesProtocol.progress(wire) } + } +}