CodexSuiteTest.kt

6.6 KB · raw

1package com.peachpasswords.codex23import org.junit.Assert.assertArrayEquals4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertTrue7import org.junit.Test8import kotlin.random.Random910/**11 * Codex suite (brief Suite 11): deflate/inflate fidelity, Reed-Solomon erasure12 * limits (remove 1 / max / max+1), and full envelope round-trips for small and13 * large vault blobs.14 */15class CodexSuiteTest {1617    // ── Deflate / inflate ─────────────────────────────────────────────────────1819    @Test fun deflate_inflate_round_trips_small() {20        val data = "the quick brown duck".toByteArray()21        assertArrayEquals(data, CodexCodec.inflate(CodexCodec.deflate(data)))22    }2324    @Test fun deflate_inflate_round_trips_empty() {25        assertArrayEquals(ByteArray(0), CodexCodec.inflate(CodexCodec.deflate(ByteArray(0))))26    }2728    @Test fun deflate_inflate_round_trips_large_random() {29        val rng = Random(42)30        val data = ByteArray(50_000) { rng.nextInt().toByte() }31        assertArrayEquals(data, CodexCodec.inflate(CodexCodec.deflate(data)))32    }3334    @Test fun deflate_shrinks_compressible_data() {35        val data = "A".repeat(10_000).toByteArray()36        assertTrue(CodexCodec.deflate(data).size < data.size)37    }3839    // ── Parity sizing ─────────────────────────────────────────────────────────4041    @Test fun parity_shards_are_at_least_one() {42        assertTrue(computeParityShards(1) >= 1)43        assertTrue(computeParityShards(6) >= 1)44    }4546    @Test fun parity_shards_scale_with_data() {47        // ~15% ratio: more data shards → at least as many parity shards.48        assertTrue(computeParityShards(100) >= computeParityShards(10))49    }5051    // ── Reed-Solomon erasure limits ───────────────────────────────────────────5253    private fun encodedShards(data: Int, parity: Int, size: Int, seed: Int): Pair<Array<ByteArray>, Array<ByteArray>> {54        val rng = Random(seed)55        val original = Array(data) { ByteArray(size) { rng.nextInt().toByte() } }56        val shards = Array(data + parity) { i -> if (i < data) original[i].copyOf() else ByteArray(size) }57        ReedSolomon.encode(shards, data, parity)58        return original to shards59    }6061    @Test fun rs_recovers_after_removing_one_shard() {62        val data = 6; val parity = computeParityShards(data)63        val (original, shards) = encodedShards(data, parity, 32, 1)64        val present = BooleanArray(data + parity) { true }65        shards[2] = ByteArray(0); present[2] = false66        ReedSolomon.reconstruct(shards, data, parity, present)67        for (i in 0 until data) assertArrayEquals(original[i], shards[i])68    }6970    @Test fun rs_recovers_after_removing_max_erasures() {71        val data = 6; val parity = computeParityShards(data)72        val (original, shards) = encodedShards(data, parity, 32, 2)73        val present = BooleanArray(data + parity) { true }74        // Drop exactly `parity` shards — the maximum tolerable.75        repeat(parity) { shards[it] = ByteArray(0); present[it] = false }76        ReedSolomon.reconstruct(shards, data, parity, present)77        for (i in 0 until data) assertArrayEquals(original[i], shards[i])78    }7980    @Test fun rs_fails_beyond_max_erasures() {81        val data = 6; val parity = computeParityShards(data)82        val (_, shards) = encodedShards(data, parity, 32, 3)83        val present = BooleanArray(data + parity) { true }84        // Drop parity+1 shards — one too many to reconstruct.85        repeat(parity + 1) { shards[it] = ByteArray(0); present[it] = false }86        var thrown: CodexException? = null87        try {88            ReedSolomon.reconstruct(shards, data, parity, present)89        } catch (e: CodexException) {90            thrown = e91        }92        assertEquals(CodexErrorCode.INSUFFICIENT_CHUNKS, thrown?.code)93    }9495    // ── Envelope round-trips ──────────────────────────────────────────────────9697    private fun payload(blobLen: Int) = CodexPayload(98        version = 2, verifyCode = "PX-AB12-CD34",99        vaultBlob = "v".repeat(blobLen), timestamp = 1, entryCount = 3,100        vaultFingerprint = "ABCD".repeat(8), generatedAt = 2,101    )102103    private fun encodePayload(p: CodexPayload): Pair<List<ChunkEnvelope>, CodexLayout> {104        val bytes = CodexCodec.json.encodeToString(CodexPayload.serializer(), p).toByteArray()105        return CodexCodec.encodeToEnvelopes(CodexCodec.deflate(bytes), p.verifyCode)106    }107108    @Test fun every_envelope_passes_integrity() {109        val (envelopes, layout) = encodePayload(payload(3000))110        assertTrue(envelopes.all { CodexCodec.verifyChunkIntegrity(it) })111        assertEquals(layout.dataChunks + layout.parityChunks, envelopes.size)112    }113114    @Test fun full_set_reconstructs_payload() {115        val p = payload(3000)116        val (envelopes, _) = encodePayload(p)117        val recovered = CodexCodec.reconstructCompressed(envelopes)118        val parsed = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(recovered)))119        assertEquals(p, parsed)120    }121122    @Test fun large_blob_round_trips_through_envelopes() {123        val p = payload(40_000)124        val (envelopes, _) = encodePayload(p)125        val recovered = CodexCodec.reconstructCompressed(envelopes)126        val parsed = CodexCodec.json.decodeFromString(CodexPayload.serializer(), String(CodexCodec.inflate(recovered)))127        assertEquals(p, parsed)128    }129130    @Test fun tampered_envelope_fails_integrity() {131        val (envelopes, _) = encodePayload(payload(500))132        val tampered = envelopes.first().copy(d = CodexCodec.base64Url("evil".toByteArray()))133        assertFalse(CodexCodec.verifyChunkIntegrity(tampered))134    }135136    // ── Helpers ───────────────────────────────────────────────────────────────137138    @Test fun base64url_round_trips() {139        val rng = Random(9)140        val bytes = ByteArray(256) { rng.nextInt().toByte() }141        assertArrayEquals(bytes, CodexCodec.fromBase64Url(CodexCodec.base64Url(bytes)))142    }143144    @Test fun sha256_hex_is_stable_and_64_chars() {145        val h = CodexCodec.sha256Hex("peach".toByteArray())146        assertEquals(64, h.length)147        assertEquals(h, CodexCodec.sha256Hex("peach".toByteArray()))148    }149}