1package com.peachpasswords.codex23import org.junit.Assert.assertArrayEquals4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test7import kotlin.random.Random89class CodexCodecTest {1011 @Test fun rs_recovers_dropped_shards() {12 val data = 613 val parity = computeParityShards(data) 14 val size = 4015 val rng = Random(7)16 val original = Array(data) { ByteArray(size) { rng.nextInt().toByte() } }17 val shards = Array(data + parity) { i -> if (i < data) original[i].copyOf() else ByteArray(size) }18 ReedSolomon.encode(shards, data, parity)1920 21 val present = BooleanArray(data + parity) { true }22 shards[1] = ByteArray(0); present[1] = false23 shards[4] = ByteArray(0); present[4] = false24 ReedSolomon.reconstruct(shards, data, parity, present)2526 for (i in 0 until data) assertArrayEquals("shard $i", original[i], shards[i])27 }2829 @Test fun gf_mul_div_are_inverse() {30 for (a in 1..255) for (b in 1..255) {31 assertEquals(a, Galois.div(Galois.mul(a, b), b))32 }33 }3435 @Test fun full_codec_round_trip() {36 val payload = CodexPayload(37 version = 2, verifyCode = "PX-AB12-CD34",38 vaultBlob = "x".repeat(5000), timestamp = 111, entryCount = 7,39 vaultFingerprint = "DEADBEEF".repeat(4), generatedAt = 222,40 )41 val bytes = CodexCodec.json.encodeToString(CodexPayload.serializer(), payload).toByteArray()42 val compressed = CodexCodec.deflate(bytes)43 assertArrayEquals(bytes, CodexCodec.inflate(compressed))4445 val (envelopes, layout) = CodexCodec.encodeToEnvelopes(compressed, payload.verifyCode)46 assertTrue(envelopes.all { CodexCodec.verifyChunkIntegrity(it) })47 assertEquals(layout.dataChunks + layout.parityChunks, envelopes.size)4849 50 val parity = layout.parityChunks / layout.stripeCount51 val survivors = envelopes.toMutableList()52 repeat(parity) { survivors.removeAt(0) } 53 val recovered = CodexCodec.reconstructCompressed(survivors)54 val parsed = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(recovered)))55 assertEquals(payload, parsed)56 }5758 @Test fun corrupted_chunk_fails_integrity() {59 val (envelopes, _) = CodexCodec.encodeToEnvelopes(CodexCodec.deflate("hello world".toByteArray()), "PX-0000-0000")60 val tampered = envelopes.first().copy(d = CodexCodec.base64Url("tampered".toByteArray()))61 assertTrue(!CodexCodec.verifyChunkIntegrity(tampered))62 }6364 @Test fun inflate_exactly_at_the_cap_round_trips() {65 val data = ByteArray(CodexCodec.MAX_INFLATED_BYTES.toInt()) 66 assertArrayEquals(data, CodexCodec.inflate(CodexCodec.deflate(data)))67 }6869 7071727374 private fun hostileChunk(75 total: Int = 4,76 index: Int = 0,77 stripe: Int = 0,78 stripeCount: Int = 1,79 data: ByteArray = ByteArray(CHUNK_SIZE),80 verifyCode: String = "PX-AB12-CD34",81 ): ChunkEnvelope = ChunkEnvelope(82 v = 3, c = verifyCode, s = stripe, capS = stripeCount, i = index, t = total,83 p = index >= total - 2,84 d = CodexCodec.base64Url(data),85 h = CodexCodec.sha256Hex(data).take(32),86 )8788 @Test fun hostile_shard_total_is_rejected_before_allocation() {89 val hostile = hostileChunk(total = Int.MAX_VALUE)90 assertTrue("geometry is rejected", !CodexCodec.isValidEnvelope(hostile))91 val error = try {92 CodexCodec.reconstructCompressed(listOf(hostile))93 null94 } catch (e: CodexException) {95 e96 }97 assertEquals(CodexErrorCode.INSUFFICIENT_CHUNKS, error?.code)98 }99100 @Test fun out_of_range_shard_and_stripe_indices_are_rejected() {101 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(total = 4, index = 4)))102 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(total = 4, index = -1)))103 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(stripe = 2, stripeCount = 2)))104 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(stripeCount = 0)))105 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(stripeCount = 100_000)))106 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(total = 256)))107 }108109 @Test fun oversized_chunk_data_is_rejected() {110 111 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(data = ByteArray(4097))))112 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(data = ByteArray(64 * 1024))))113 assertTrue(CodexCodec.isValidEnvelope(hostileChunk()))114 }115116 117118119120121122123124 @Test fun a_codex_printed_with_an_older_chunk_size_still_restores() {125 val legacyChunkSize = 800126 val payload = ByteArray(legacyChunkSize * 2) { (it % 251).toByte() }127 128 129 val shards = listOf(130 payload.copyOfRange(0, legacyChunkSize),131 payload.copyOfRange(legacyChunkSize, legacyChunkSize * 2),132 ByteArray(legacyChunkSize),133 ByteArray(legacyChunkSize),134 )135 val envelopes = shards.mapIndexed { i, bytes ->136 ChunkEnvelope(137 v = 3, c = "PX-AB12-CD34", s = 0, capS = 1, i = i, t = 4,138 p = i >= 2, d = CodexCodec.base64Url(bytes),139 h = CodexCodec.sha256Hex(bytes).take(32),140 )141 }142 assertTrue("legacy envelopes are accepted", envelopes.all { CodexCodec.isValidEnvelope(it) })143 assertArrayEquals(payload, CodexCodec.reconstructCompressed(envelopes))144 }145146 147148149150151 @Test fun a_reconstruction_past_the_inflate_cap_is_refused() {152 val chunk = ByteArray(4096) 153 val stripeCount = 20 154 val envelopes = (0 until stripeCount).flatMap { s ->155 (0 until 230).map { i ->156 ChunkEnvelope(157 v = 3, c = "PX-AB12-CD34", s = s, capS = stripeCount,158 i = i, t = 230, p = false,159 d = CodexCodec.base64Url(chunk), h = CodexCodec.sha256Hex(chunk).take(32),160 )161 }162 }163 val error = try {164 CodexCodec.reconstructCompressed(envelopes); null165 } catch (e: CodexException) {166 e167 }168 assertEquals(CodexErrorCode.PAYLOAD_TOO_LARGE, error?.code)169 }170171 @Test fun malformed_verify_code_is_rejected() {172 assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(verifyCode = "PX-AB12-CD3")))173 assertTrue(CodexCodec.isValidEnvelope(hostileChunk()))174 }175176 @Test fun mixed_verify_codes_and_shard_totals_are_rejected() {177 val (envelopes, _) = CodexCodec.encodeToEnvelopes(178 CodexCodec.deflate("hello codex".toByteArray()),179 "PX-AB12-CD34",180 )181 val other = CodexCodec.encodeToEnvelopes(182 CodexCodec.deflate("hello codex".toByteArray()),183 "PX-ZZ99-YY88",184 ).first.first()185 var error = try {186 CodexCodec.reconstructCompressed(envelopes + other); null187 } catch (e: CodexException) {188 e189 }190 assertEquals(CodexErrorCode.INVALID_ENVELOPE, error?.code)191192 val mixedTotal = envelopes.first().copy(t = envelopes.first().t + 1, i = 0)193 error = try {194 CodexCodec.reconstructCompressed(listOf(envelopes.first(), mixedTotal)); null195 } catch (e: CodexException) {196 e197 }198 assertEquals(CodexErrorCode.INVALID_ENVELOPE, error?.code)199 }200201 @Test fun hostile_envelopes_are_skipped_and_real_codes_still_restore() {202 val (envelopes, _) = CodexCodec.encodeToEnvelopes(203 CodexCodec.deflate("survives hostile neighbours".toByteArray()),204 "PX-AB12-CD34",205 )206 val recovered = CodexCodec.reconstructCompressed(207 listOf(hostileChunk(total = Int.MAX_VALUE)) + envelopes +208 listOf(hostileChunk(verifyCode = "nope")),209 )210 assertEquals("survives hostile neighbours", String(CodexCodec.inflate(recovered)))211 }212213 @Test fun inflate_refuses_decompression_bombs() {214 val bomb = CodexCodec.deflate(ByteArray(CodexCodec.MAX_INFLATED_BYTES.toInt() + 1))215 assertTrue("bomb compresses tiny", bomb.size < 64 * 1024)216 val error = try {217 CodexCodec.inflate(bomb)218 null219 } catch (e: CodexException) {220 e221 }222 assertEquals(CodexErrorCode.PAYLOAD_TOO_LARGE, error?.code)223 }224225 226227228229230231 @Test fun inflate_rejects_malformed_no_progress_stream() {232 val valid = CodexCodec.deflate("hello world".toByteArray())233 234 val truncated = valid.copyOf(valid.size / 2)235 val error = try {236 CodexCodec.inflate(truncated)237 null238 } catch (e: CodexException) {239 e240 }241 assertEquals(CodexErrorCode.CORRUPTED, error?.code)242 }243}