CodexCodecTest.kt

7.1 KB · raw

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) // 214        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        // Drop `parity` shards (the max tolerable) — here drop 2 data shards.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        // Lose the maximum tolerable codes in stripe 0, then rebuild.50        val parity = layout.parityChunks / layout.stripeCount51        val survivors = envelopes.toMutableList()52        repeat(parity) { survivors.removeAt(0) } // drop first `parity` data shards53        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()) // zeros compress to ~17 KB66        assertArrayEquals(data, CodexCodec.inflate(CodexCodec.deflate(data)))67    }6869    /**70     * PAI-2026-002: the per-chunk hash covers `d` only, so a hostile code71     * passes verifyChunkIntegrity with a tiny payload and any `t`/`S`/`i` it72     * likes. Every case below used to reach Array(t) before being rejected.73     */74    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 short_and_oversized_chunk_data_are_rejected() {110        val short = hostileChunk(data = ByteArray(CHUNK_SIZE - 1))111        val oversized = hostileChunk(data = ByteArray(CHUNK_SIZE + 1))112        assertTrue(!CodexCodec.isValidEnvelope(short))113        assertTrue(!CodexCodec.isValidEnvelope(oversized))114    }115116    @Test fun malformed_verify_code_is_rejected() {117        assertTrue(!CodexCodec.isValidEnvelope(hostileChunk(verifyCode = "PX-AB12-CD3")))118        assertTrue(CodexCodec.isValidEnvelope(hostileChunk()))119    }120121    @Test fun mixed_verify_codes_and_shard_totals_are_rejected() {122        val (envelopes, _) = CodexCodec.encodeToEnvelopes(123            CodexCodec.deflate("hello codex".toByteArray()),124            "PX-AB12-CD34",125        )126        val other = CodexCodec.encodeToEnvelopes(127            CodexCodec.deflate("hello codex".toByteArray()),128            "PX-ZZ99-YY88",129        ).first.first()130        var error = try {131            CodexCodec.reconstructCompressed(envelopes + other); null132        } catch (e: CodexException) {133            e134        }135        assertEquals(CodexErrorCode.INVALID_ENVELOPE, error?.code)136137        val mixedTotal = envelopes.first().copy(t = envelopes.first().t + 1, i = 0)138        error = try {139            CodexCodec.reconstructCompressed(listOf(envelopes.first(), mixedTotal)); null140        } catch (e: CodexException) {141            e142        }143        assertEquals(CodexErrorCode.INVALID_ENVELOPE, error?.code)144    }145146    @Test fun hostile_envelopes_are_skipped_and_real_codes_still_restore() {147        val (envelopes, _) = CodexCodec.encodeToEnvelopes(148            CodexCodec.deflate("survives hostile neighbours".toByteArray()),149            "PX-AB12-CD34",150        )151        val recovered = CodexCodec.reconstructCompressed(152            listOf(hostileChunk(total = Int.MAX_VALUE)) + envelopes +153                listOf(hostileChunk(verifyCode = "nope")),154        )155        assertEquals("survives hostile neighbours", String(CodexCodec.inflate(recovered)))156    }157158    @Test fun inflate_refuses_decompression_bombs() {159        val bomb = CodexCodec.deflate(ByteArray(CodexCodec.MAX_INFLATED_BYTES.toInt() + 1))160        assertTrue("bomb compresses tiny", bomb.size < 64 * 1024)161        val error = try {162            CodexCodec.inflate(bomb)163            null164        } catch (e: CodexException) {165            e166        }167        assertEquals(CodexErrorCode.PAYLOAD_TOO_LARGE, error?.code)168    }169}