mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
test(cordn): pin the on-disk format of the file stores
Drives FileCordnGroupStore, FileCordnKeyPackageStore, FileCordnCoordinatorStore, FileCordnHandoffStore, EncryptedAppendLog and CordnMigrationStores through a representative sequence with a deterministic cipher, and asserts the exact tree of relative paths and the exact bytes of every file, captured from the current java.io implementation. A second case lays those pinned bytes down on a clean disk and reads them back through the stores, which is the direction an upgrade travels. The migration read also pins the base64 strings a handoff carries. This is the guard for moving the stores to okio in commonMain: the files hold encrypted MLS group state that cannot be re-derived, so the move must not change a single byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
+532
@@ -0,0 +1,532 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.cordn
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.storage.EncryptedAppendLog
|
||||
import com.vitorpamplona.quartz.cordn.appMultiDevice.CordnCarriedKeyPackage
|
||||
import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnDeliveredMessage
|
||||
import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnDeliveredMessageCodec
|
||||
import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnEnvelope
|
||||
import com.vitorpamplona.quartz.cordn.sync.EchoState
|
||||
import com.vitorpamplona.quartz.cordn.sync.GroupCursor
|
||||
import com.vitorpamplona.quartz.cordn.sync.PendingEpochOperation
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.util.Base64
|
||||
import kotlin.io.path.createTempDirectory
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
/**
|
||||
* The on-disk format of the cordn stores and of [EncryptedAppendLog], pinned
|
||||
* byte for byte.
|
||||
*
|
||||
* These files hold a user's encrypted MLS group state and message history, and
|
||||
* none of it can be re-derived from anywhere else on the device: a layout or
|
||||
* framing change that the next build cannot read is a group the user has lost.
|
||||
* So this drives every writer through a representative sequence — overwrites,
|
||||
* deletes, dedup, appends, a fold, a legacy blob, a torn tail, a migration —
|
||||
* and compares the resulting tree against constants captured from the
|
||||
* implementation that shipped. It then goes the other way, laying those exact
|
||||
* bytes down on a clean disk and reading them back, which is the direction an
|
||||
* upgrade actually travels.
|
||||
*
|
||||
* The cipher is a deterministic stand-in (a tag byte plus XOR) so the bytes are
|
||||
* stable; it also makes a segment's ciphertext one byte longer than its
|
||||
* plaintext, so a length prefix that recorded the wrong one would show.
|
||||
*
|
||||
* If this fails, the format changed. Do not regenerate the constants to make it
|
||||
* pass unless the change is deliberate and comes with a reader for the old one.
|
||||
*/
|
||||
class CordnStorageFormatGoldenTest {
|
||||
/** Deterministic, reversible, and length-changing. */
|
||||
private class TaggedXorCipher : CordnBlobCipher {
|
||||
override fun encrypt(bytes: ByteArray): ByteArray = byteArrayOf(TAG) + ByteArray(bytes.size) { (bytes[it].toInt() xor MASK).toByte() }
|
||||
|
||||
override fun decrypt(bytes: ByteArray): ByteArray {
|
||||
require(bytes.isNotEmpty() && bytes[0] == TAG) { "not ours" }
|
||||
return ByteArray(bytes.size - 1) { (bytes[it + 1].toInt() xor MASK).toByte() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG: Byte = 0x7C
|
||||
const val MASK = 0x5A
|
||||
}
|
||||
}
|
||||
|
||||
private val rootFile: File = createTempDirectory("cordn-golden").toFile()
|
||||
private val cipher = TaggedXorCipher()
|
||||
|
||||
private val account = "a".repeat(64)
|
||||
private val coordinatorA = "b".repeat(64)
|
||||
private val coordinatorB = "c".repeat(64)
|
||||
private val handedBackAccount = "d".repeat(64)
|
||||
private val migratedAccount = "e".repeat(64)
|
||||
|
||||
// ---- Construction glue: the only part of this file that follows the stores' API. ----
|
||||
|
||||
private fun groupStore(coordinator: HexKey) = FileCordnGroupStore(CordnStorageLayout.directoryFor(rootFile, account, coordinator), cipher)
|
||||
|
||||
private fun keyPackageStore(coordinator: HexKey) = FileCordnKeyPackageStore(CordnStorageLayout.directoryFor(rootFile, account, coordinator), cipher)
|
||||
|
||||
private fun coordinatorStore() = FileCordnCoordinatorStore(CordnStorageLayout.accountDirectoryFor(rootFile, account), cipher)
|
||||
|
||||
private fun handoffStore(owner: HexKey) = FileCordnHandoffStore(CordnStorageLayout.accountDirectoryFor(rootFile, owner))
|
||||
|
||||
private fun appendLog(compactAfterSegments: Int) =
|
||||
EncryptedAppendLog(
|
||||
encrypt = cipher::encrypt,
|
||||
decrypt = { runCatching { cipher.decrypt(it) }.getOrNull() },
|
||||
compactAfterSegments = compactAfterSegments,
|
||||
)
|
||||
|
||||
private fun logFile(name: String) = File(rootFile, "logs/$name")
|
||||
|
||||
private suspend fun migrationWrite(snapshot: CordnMigrationSnapshot) = CordnMigrationStores.write(rootFile, migratedAccount, cipher, snapshot)
|
||||
|
||||
private suspend fun migrationRead(configs: List<CoordinatorConfig>) = CordnMigrationStores.read(rootFile, migratedAccount, cipher, configs)
|
||||
|
||||
// ---- End of construction glue. ----
|
||||
|
||||
/** The test's own byte surgery, beneath the API under test. */
|
||||
private fun raw(relative: String) = File(rootFile, relative)
|
||||
|
||||
@AfterTest
|
||||
fun cleanUp() {
|
||||
rootFile.deleteRecursively()
|
||||
}
|
||||
|
||||
private fun message(
|
||||
content: String,
|
||||
cursor: Long,
|
||||
) = CordnDeliveredMessage(CordnEnvelope.build("aa".repeat(32), 1_757_000_000L + cursor, 9, content = content), cursor = cursor)
|
||||
|
||||
private val m1 = message("first", 1)
|
||||
private val m2 = message("second ✓", 2)
|
||||
private val m3 = message("third", 3)
|
||||
|
||||
private fun relay(url: String) = RelayUrlNormalizer.normalizeOrNull(url)!!
|
||||
|
||||
private val coordinators =
|
||||
listOf(
|
||||
CoordinatorConfig(coordinatorA, listOf(relay("wss://one.example.com")), CoordinatorConfig.Origin.MANUAL, "Work"),
|
||||
CoordinatorConfig(coordinatorB, listOf(relay("wss://two.example.com"), relay("wss://three.example.com")), CoordinatorConfig.Origin.GROUP_REF),
|
||||
)
|
||||
|
||||
private val room = CordnRoomState("héllo draft ✓", 42)
|
||||
private val echoes = EchoState(listOf(PendingEpochOperation("c2VhbGVk", localStateApplied = false)), listOf(3L, 5L))
|
||||
|
||||
/** The pre-segment format: `uint32 count, (uint32 len, bytes)*`, encrypted as one blob. */
|
||||
private fun legacyPlain(vararg entries: String): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
|
||||
fun int(v: Int) = out.write(byteArrayOf((v shr 24).toByte(), (v shr 16).toByte(), (v shr 8).toByte(), v.toByte()))
|
||||
int(entries.size)
|
||||
entries.forEach {
|
||||
val bytes = it.encodeToByteArray()
|
||||
int(bytes.size)
|
||||
out.write(bytes)
|
||||
}
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
private fun b64(bytes: ByteArray) = Base64.getEncoder().encodeToString(bytes)
|
||||
|
||||
private val migrationSnapshot =
|
||||
CordnMigrationSnapshot(
|
||||
accountPubKey = migratedAccount,
|
||||
groups =
|
||||
listOf(
|
||||
CordnMigrationGroup(
|
||||
coordinatorPubKey = coordinatorA,
|
||||
coordinatorRelays = listOf("wss://coord.example"),
|
||||
gid = "mg-1",
|
||||
clientStateBase64 = b64("mls-state-1".encodeToByteArray()),
|
||||
cursor = 77,
|
||||
roomStateBase64 = b64(CordnRoomStateCodec.encode(CordnRoomState("migrated draft", 70))),
|
||||
echoStateBase64 = b64(EchoStateCodec.encode(EchoState(ownMessageCursors = listOf(76L)))),
|
||||
joinedViaRequest = true,
|
||||
messages = listOf(CordnDeliveredMessageCodec.encode(m1), CordnDeliveredMessageCodec.encode(m2)),
|
||||
),
|
||||
CordnMigrationGroup(
|
||||
coordinatorPubKey = coordinatorB,
|
||||
coordinatorRelays = listOf("wss://other.example"),
|
||||
gid = "mg-2",
|
||||
clientStateBase64 = b64(byteArrayOf(0, 1, 2, -1, -2)),
|
||||
cursor = 0,
|
||||
),
|
||||
),
|
||||
keyPackages = listOf(CordnCarriedKeyPackage(coordinatorA, "kp-ref", b64("bundle!".encodeToByteArray()))),
|
||||
)
|
||||
|
||||
/** Every writer, through the paths that shape the tree: overwrite, delete, dedup, fold, repair. */
|
||||
private suspend fun drive() {
|
||||
groupStore(coordinatorA).apply {
|
||||
saveGroup("g1", "state-v1".encodeToByteArray())
|
||||
saveGroup("g1", "state-v2-longer".encodeToByteArray())
|
||||
saveCursor("g1", GroupCursor(fetchCursor = 0x0102030405060708L, lastCursor = 0x1112131415161718L))
|
||||
saveJoinedViaRequest("g1")
|
||||
saveRoomState("g1", room)
|
||||
saveEchoState("g1", echoes)
|
||||
appendMessage("g1", m1)
|
||||
appendMessage("g1", m2)
|
||||
appendMessage("g1", m1)
|
||||
appendMessage("g1", m3)
|
||||
|
||||
saveGroup("../odd/gid ✓", byteArrayOf(9, 8, 7))
|
||||
|
||||
saveGroup("blanked", byteArrayOf(1))
|
||||
saveRoomState("blanked", room)
|
||||
saveRoomState("blanked", CordnRoomState())
|
||||
saveEchoState("blanked", echoes)
|
||||
saveEchoState("blanked", EchoState())
|
||||
|
||||
saveGroup("gone", byteArrayOf(2))
|
||||
saveCursor("gone", GroupCursor(5, 5))
|
||||
appendMessage("gone", m1)
|
||||
deleteGroup("gone")
|
||||
}
|
||||
groupStore(coordinatorB).saveGroup("g1", "other-coordinator".encodeToByteArray())
|
||||
|
||||
keyPackageStore(coordinatorA).apply {
|
||||
save("ref-1", "bundle-1".encodeToByteArray())
|
||||
save("ref/2", "bundle-2".encodeToByteArray())
|
||||
save("ref-3", "bundle-3".encodeToByteArray())
|
||||
delete("ref-3")
|
||||
}
|
||||
|
||||
coordinatorStore().save(coordinators)
|
||||
|
||||
handoffStore(account).save(true)
|
||||
handoffStore(handedBackAccount).save(true)
|
||||
handoffStore(handedBackAccount).save(false)
|
||||
|
||||
// Folding: a rewrite, three loose segments and a fold, then a restart
|
||||
// that has to find the fold boundary in the header, not in memory.
|
||||
appendLog(3).apply { (1..5).forEach { append(logFile("folding"), "e$it") } }
|
||||
appendLog(3).apply { (6..7).forEach { append(logFile("folding"), "e$it") } }
|
||||
|
||||
// Legacy: a headerless blob, rewritten into the segmented format by the next append.
|
||||
raw("logs").mkdirs()
|
||||
raw("logs/legacy").writeBytes(cipher.encrypt(legacyPlain("old-1", "old-2")))
|
||||
appendLog(200).append(logFile("legacy"), "new-1")
|
||||
|
||||
// Torn tail: a half-written segment is cut off the file, and the next
|
||||
// append lands after the last good one.
|
||||
appendLog(200).apply {
|
||||
append(logFile("torn"), "t1")
|
||||
append(logFile("torn"), "t2")
|
||||
}
|
||||
raw("logs/torn").appendBytes(byteArrayOf(0, 0, 0, 0x40, 1, 2, 3))
|
||||
appendLog(200).apply {
|
||||
assertEquals(listOf("t1", "t2"), readAll(logFile("torn")))
|
||||
append(logFile("torn"), "t3")
|
||||
}
|
||||
|
||||
appendLog(200).apply {
|
||||
append(logFile("rewritten"), "r1")
|
||||
append(logFile("rewritten"), "r2")
|
||||
rewrite(logFile("rewritten"), listOf("x", "y"))
|
||||
append(logFile("rewritten"), "z")
|
||||
}
|
||||
|
||||
// Migration: replaces the account's tree wholesale, stale files included.
|
||||
raw("cordn/$migratedAccount/stale").apply {
|
||||
parentFile.mkdirs()
|
||||
writeBytes(byteArrayOf(1, 2, 3))
|
||||
}
|
||||
val started = migrationWrite(migrationSnapshot)
|
||||
assertEquals(
|
||||
listOf(
|
||||
CoordinatorConfig(coordinatorA, listOf(relay("wss://coord.example")), CoordinatorConfig.Origin.MANUAL),
|
||||
CoordinatorConfig(coordinatorB, listOf(relay("wss://other.example")), CoordinatorConfig.Origin.MANUAL),
|
||||
),
|
||||
started,
|
||||
)
|
||||
}
|
||||
|
||||
/** What every store reads back from the tree [drive] leaves — before an upgrade and after it. */
|
||||
private suspend fun assertReadsBack() {
|
||||
groupStore(coordinatorA).apply {
|
||||
assertEquals(listOf("../odd/gid ✓", "blanked", "g1"), listGroups().sorted())
|
||||
assertContentEquals("state-v2-longer".encodeToByteArray(), loadGroup("g1"))
|
||||
assertEquals(GroupCursor(0x0102030405060708L, 0x1112131415161718L), loadCursor("g1"))
|
||||
assertTrue(loadJoinedViaRequest("g1"))
|
||||
assertEquals(room, loadRoomState("g1"))
|
||||
assertEquals(echoes, loadEchoState("g1"))
|
||||
assertEquals(listOf(m1, m2, m3), loadMessages("g1"))
|
||||
assertEquals(CordnMessageSummary(m3, 3), loadMessageSummary("g1"))
|
||||
assertContentEquals(byteArrayOf(9, 8, 7), loadGroup("../odd/gid ✓"))
|
||||
assertFalse(loadJoinedViaRequest("blanked"))
|
||||
assertEquals(CordnRoomState(), loadRoomState("blanked"))
|
||||
assertEquals(EchoState(), loadEchoState("blanked"))
|
||||
assertNull(loadGroup("gone"))
|
||||
assertNull(loadCursor("gone"))
|
||||
assertEquals(emptyList(), loadMessages("gone"))
|
||||
}
|
||||
assertEquals(listOf("g1"), groupStore(coordinatorB).listGroups())
|
||||
assertContentEquals("other-coordinator".encodeToByteArray(), groupStore(coordinatorB).loadGroup("g1"))
|
||||
|
||||
keyPackageStore(coordinatorA).apply {
|
||||
assertEquals(listOf("ref-1", "ref/2"), list().sorted())
|
||||
assertContentEquals("bundle-2".encodeToByteArray(), load("ref/2"))
|
||||
assertNull(load("ref-3"))
|
||||
}
|
||||
|
||||
assertEquals(coordinators, coordinatorStore().load())
|
||||
assertTrue(handoffStore(account).load())
|
||||
assertFalse(handoffStore(handedBackAccount).load())
|
||||
|
||||
assertEquals((1..7).map { "e$it" }, appendLog(3).readAll(logFile("folding")))
|
||||
assertEquals(listOf("old-1", "old-2", "new-1"), appendLog(200).readAll(logFile("legacy")))
|
||||
assertEquals(listOf("t1", "t2", "t3"), appendLog(200).readAll(logFile("torn")))
|
||||
assertEquals(listOf("x", "y", "z"), appendLog(200).readAll(logFile("rewritten")))
|
||||
|
||||
// The migration document carries base64 of what the stores hold; the
|
||||
// strings are pinned too, since another device decodes them.
|
||||
val read = migrationRead(migrationSnapshot.groups.map { CoordinatorConfig(it.coordinatorPubKey, listOf(relay(it.coordinatorRelays.single()))) })
|
||||
assertEquals(migratedAccount, read.accountPubKey)
|
||||
assertEquals(
|
||||
listOf(
|
||||
CordnMigrationGroup(
|
||||
coordinatorPubKey = coordinatorA,
|
||||
coordinatorRelays = listOf("wss://coord.example/"),
|
||||
gid = "mg-1",
|
||||
clientStateBase64 = "bWxzLXN0YXRlLTE=",
|
||||
cursor = 77,
|
||||
roomStateBase64 = "AAEADm1pZ3JhdGVkIGRyYWZ0AAAAAAAAAEY=",
|
||||
echoStateBase64 = "AAEAAAAIAAAAAAAAAEw=",
|
||||
joinedViaRequest = true,
|
||||
messages = listOf(CordnDeliveredMessageCodec.encode(m1), CordnDeliveredMessageCodec.encode(m2)),
|
||||
),
|
||||
CordnMigrationGroup(
|
||||
coordinatorPubKey = coordinatorB,
|
||||
coordinatorRelays = listOf("wss://other.example/"),
|
||||
gid = "mg-2",
|
||||
clientStateBase64 = "AAEC//4=",
|
||||
cursor = 0,
|
||||
roomStateBase64 = "AAEAAAAAAAAAAAAA",
|
||||
echoStateBase64 = "AAEAAAAA",
|
||||
joinedViaRequest = false,
|
||||
messages = emptyList(),
|
||||
),
|
||||
),
|
||||
read.groups.sortedBy { it.gid },
|
||||
)
|
||||
assertEquals(listOf(CordnCarriedKeyPackage(coordinatorA, "kp-ref", "YnVuZGxlIQ==")), read.keyPackages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the stores write exactly the pinned files`() =
|
||||
runTest {
|
||||
drive()
|
||||
val actual = tree(rootFile)
|
||||
if (actual != EXPECTED) fail(mismatch(actual))
|
||||
assertReadsBack()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the pinned files read back as the values that wrote them`() =
|
||||
runTest {
|
||||
// The upgrade direction: files written by the build that shipped,
|
||||
// read by this one.
|
||||
EXPECTED.forEach { (path, hex) ->
|
||||
val target = File(rootFile, path.removeSuffix("/"))
|
||||
if (path.endsWith("/")) {
|
||||
target.mkdirs()
|
||||
} else {
|
||||
target.parentFile.mkdirs()
|
||||
target.writeBytes(hex.hexToByteArray())
|
||||
}
|
||||
}
|
||||
assertReadsBack()
|
||||
}
|
||||
|
||||
private fun tree(root: File): Map<String, String> =
|
||||
root
|
||||
.walkTopDown()
|
||||
.filter { it != root }
|
||||
.associate { file ->
|
||||
val relative = file.relativeTo(root).invariantSeparatorsPath
|
||||
if (file.isDirectory) "$relative/" to "" else relative to file.readBytes().toHexString()
|
||||
}.toSortedMap()
|
||||
|
||||
/** The diff, plus the literal to paste if (and only if) the change is deliberate. */
|
||||
private fun mismatch(actual: Map<String, String>): String =
|
||||
buildString {
|
||||
appendLine("the on-disk format changed")
|
||||
(EXPECTED.keys - actual.keys).forEach { appendLine(" missing: $it") }
|
||||
(actual.keys - EXPECTED.keys).forEach { appendLine(" unexpected: $it") }
|
||||
actual.filter { (k, v) -> k in EXPECTED && EXPECTED[k] != v }.forEach { (k, _) -> appendLine(" bytes differ: $k") }
|
||||
appendLine("---- actual ----")
|
||||
actual.forEach { (path, hex) ->
|
||||
appendLine(" \"$path\" to")
|
||||
val chunks = hex.chunked(120).ifEmpty { listOf("") }
|
||||
appendLine(chunks.joinToString(" +\n ", prefix = " ", postfix = ",") { "\"$it\"" })
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EXPECTED: Map<String, String> =
|
||||
sortedMapOf(
|
||||
"cordn/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/Li4vb2RkL2dpZCDinJM/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/Li4vb2RkL2dpZCDinJM/state" to
|
||||
"7c53525d",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/YmxhbmtlZA/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/YmxhbmtlZA/state" to
|
||||
"7c5b",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/cursor" to
|
||||
"7c5b58595e5f5c5d524b48494e4f4c4d42",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/echoes" to
|
||||
"7c5a5b5a515a5239680c32381d0c315a5a4a5a5a5a5a5a5a5a595a5a5a5a5a5a5a5f",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/messages" to
|
||||
"4d524d544c4f47330000000000000101000000ed7c5a5a5a5b5a5a5abe21782c78606b76783978606b76783f78602178333e7860783b6b6b6d6d6a6c" +
|
||||
"696a6f6b3e3b6e686d6c393c3f6262396e696a6f3f623c3f3e38626c3f623e3f3e6f6863396d6b6f6c3f3b6f3e3b383c626b696a383e6e3e3b787678" +
|
||||
"2a2f38313f237860783b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b" +
|
||||
"3b3b3b3b3b3b3b3b3b3b3b3b3b78767839283f3b2e3f3e053b2e78606b6d6f6d6a6a6a6a6a6b76783133343e78606376782e3b3d2978600107767839" +
|
||||
"35342e3f342e7860783c3328292e782727000000f27c5a5a5a5b5a5a5ab321782c78606b76783978606876783f78602178333e78607838696b6d6f3b" +
|
||||
"636e3c3e6e6e3c6f69696c3f6e3f3e39393868396e3b6b6c6d636938696b3f38626e623f3e69686f69636d6c3b6e3e39683b636239396c6b3c3b7876" +
|
||||
"782a2f38313f237860783b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b" +
|
||||
"3b3b3b3b3b3b3b3b3b3b3b3b3b3b78767839283f3b2e3f3e053b2e78606b6d6f6d6a6a6a6a6a6876783133343e78606376782e3b3d29786001077678" +
|
||||
"3935342e3f342e786078293f3935343e7ab8c6c9782727000000ed7c5a5a5a5b5a5a5abe21782c78606b76783978606976783f78602178333e786078" +
|
||||
"6838636f3c3c3f3f6d633c3f6a6f6b3963396a6f6c6f6363393c396e6d6c393b3b3e3e696d3c6e3c6b6e3b3c396b626a6a636a6c6a38396d3b386e63" +
|
||||
"396e6c697876782a2f38313f237860783b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b" +
|
||||
"3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b78767839283f3b2e3f3e053b2e78606b6d6f6d6a6a6a6a6a6976783133343e78606376782e3b3d29" +
|
||||
"7860010776783935342e3f342e7860782e3233283e782727",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/newest" to
|
||||
"7c5a5b5a5a5a595abe21782c78606b76783978606976783f78602178333e7860786838636f3c3c3f3f6d633c3f6a6f6b3963396a6f6c6f6363393c39" +
|
||||
"6e6d6c393b3b3e3e696d3c6e3c6b6e3b3c396b626a6a636a6c6a38396d3b386e63396e6c697876782a2f38313f237860783b3b3b3b3b3b3b3b3b3b3b" +
|
||||
"3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b78767839283f3b" +
|
||||
"2e3f3e053b2e78606b6d6f6d6a6a6a6a6a6976783133343e78606376782e3b3d297860010776783935342e3f342e7860782e3233283e782727",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/room" to
|
||||
"7c5a5b5a4a3299f33636357a3e283b3c2e7ab8c6c95a5a5a5a5a5a5a70",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/state" to
|
||||
"7c292e3b2e3f772c68773635343d3f28",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/ZzE/via-request" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/keypackages/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/keypackages/cmVmLTE" to
|
||||
"7c382f343e363f776b",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/keypackages/cmVmLzI" to
|
||||
"7c382f343e363f7768",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/groups/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/groups/ZzE/" to
|
||||
"",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/groups/ZzE/state" to
|
||||
"7c352e323f2877393535283e33343b2e3528",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/coordinators" to
|
||||
"7c5a5b5a585a1a3838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838383838" +
|
||||
"38383838383838383838385a5c171b140f1b165a5e0d3528315a5b5a4c2d292960757535343f743f223b372a363f74393537755a1a39393939393939" +
|
||||
"3939393939393939393939393939393939393939393939393939393939393939393939393939393939393939393939393939393939393939395a531d" +
|
||||
"08150f0a05081f1c5a5a5a585a4c2d29296075752e2d35743f223b372a363f74393537755a422d29296075752e32283f3f743f223b372a363f743935" +
|
||||
"3775",
|
||||
"cordn/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/handed-off" to
|
||||
"",
|
||||
"cordn/dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/cursor" to
|
||||
"7c5a5a5a5a5a5a5a175a5a5a5a5a5a5a17",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/echoes" to
|
||||
"7c5a5b5a5a5a525a5a5a5a5a5a5a16",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/messages" to
|
||||
"4d524d544c4f47330000000000000101000000ed7c5a5a5a5b5a5a5abe21782c78606b76783978606b76783f78602178333e7860783b6b6b6d6d6a6c" +
|
||||
"696a6f6b3e3b6e686d6c393c3f6262396e696a6f3f623c3f3e38626c3f623e3f3e6f6863396d6b6f6c3f3b6f3e3b383c626b696a383e6e3e3b787678" +
|
||||
"2a2f38313f237860783b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b" +
|
||||
"3b3b3b3b3b3b3b3b3b3b3b3b3b78767839283f3b2e3f3e053b2e78606b6d6f6d6a6a6a6a6a6b76783133343e78606376782e3b3d2978600107767839" +
|
||||
"35342e3f342e7860783c3328292e782727000000f27c5a5a5a5b5a5a5ab321782c78606b76783978606876783f78602178333e78607838696b6d6f3b" +
|
||||
"636e3c3e6e6e3c6f69696c3f6e3f3e39393868396e3b6b6c6d636938696b3f38626e623f3e69686f69636d6c3b6e3e39683b636239396c6b3c3b7876" +
|
||||
"782a2f38313f237860783b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b" +
|
||||
"3b3b3b3b3b3b3b3b3b3b3b3b3b3b78767839283f3b2e3f3e053b2e78606b6d6f6d6a6a6a6a6a6876783133343e78606376782e3b3d29786001077678" +
|
||||
"3935342e3f342e786078293f3935343e7ab8c6c9782727",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/newest" to
|
||||
"7c5a5b5a5a5a585ab321782c78606b76783978606876783f78602178333e78607838696b6d6f3b636e3c3e6e6e3c6f69696c3f6e3f3e39393868396e" +
|
||||
"3b6b6c6d636938696b3f38626e623f3e69686f69636d6c3b6e3e39683b636239396c6b3c3b7876782a2f38313f237860783b3b3b3b3b3b3b3b3b3b3b" +
|
||||
"3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b78767839283f3b" +
|
||||
"2e3f3e053b2e78606b6d6f6d6a6a6a6a6a6876783133343e78606376782e3b3d297860010776783935342e3f342e786078293f3935343e7ab8c6c978" +
|
||||
"2727",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/room" to
|
||||
"7c5a5b5a5437333d283b2e3f3e7a3e283b3c2e5a5a5a5a5a5a5a1c",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/state" to
|
||||
"7c37362977292e3b2e3f776b",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/groups/bWctMQ/via-request" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/keypackages/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/keypackages/a3AtcmVm" to
|
||||
"7c382f343e363f7b",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/groups/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/groups/bWctMg/" to
|
||||
"",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/groups/bWctMg/cursor" to
|
||||
"7c5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
|
||||
"cordn/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc/groups/bWctMg/state" to
|
||||
"7c5a5b58a5a4",
|
||||
"logs/" to
|
||||
"",
|
||||
"logs/folding" to
|
||||
"4d524d544c4f473300000000000000550000000b7c5a5a5a5b5a5a5a583f6b000000177c5a5a5a595a5a5a583f685a5a5a583f695a5a5a583f6e0000" +
|
||||
"00177c5a5a5a595a5a5a583f6f5a5a5a583f6c5a5a5a583f6d",
|
||||
"logs/legacy" to
|
||||
"4d524d544c4f47330000000000000034000000207c5a5a5a595a5a5a5f35363e776b5a5a5a5f35363e77685a5a5a5f343f2d776b",
|
||||
"logs/rewritten" to
|
||||
"4d524d544c4f473300000000000000230000000f7c5a5a5a585a5a5a5b225a5a5a5b230000000a7c5a5a5a5b5a5a5a5b20",
|
||||
"logs/torn" to
|
||||
"4d524d544c4f4733000000000000001f0000000b7c5a5a5a5b5a5a5a582e6b0000000b7c5a5a5a5b5a5a5a582e680000000b7c5a5a5a5b5a5a5a582e" +
|
||||
"69",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user