"""Fuzz tests for RAF and Merkle tree features. Adapted from pyaegis (github.com/jedisct1/pyaegis) tests by Frank Denis. Exercises random operation sequences, edge cases, and consistency invariants to verify reliability of the encrypted random-access file and Merkle tree. """ import hashlib import os import random import pytest from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4, raf from aeg.raf import BytesIOStorage, FileStorage CIPHERS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4] # Subset used for heavier fuzz loops, as in pyaegis FUZZ_CIPHERS = [aegis128l, aegis256] def cipher_id(c): return c.NAME class RefFile: """Reference in-memory plaintext file for oracle comparison.""" def __init__(self): self._data = bytearray() self._pos = 0 @property def size(self): return len(self._data) def write(self, data, offset=None): if offset is None: offset = self._pos end = offset + len(data) if end > len(self._data): self._data.extend(b"\x00" * (end - len(self._data))) self._data[offset : offset + len(data)] = data self._pos = end return len(data) def pwrite(self, data, offset): end = offset + len(data) if end > len(self._data): self._data.extend(b"\x00" * (end - len(self._data))) self._data[offset : offset + len(data)] = data return len(data) def read(self, size=-1, offset=None): if offset is None: offset = self._pos if size < 0: size = max(0, len(self._data) - offset) result = bytes(self._data[offset : offset + size]) self._pos = offset + len(result) return result def pread(self, size, offset): return bytes(self._data[offset : offset + size]) def truncate(self, size=None): if size is None: size = self._pos if size < len(self._data): self._data = self._data[:size] else: self._data.extend(b"\x00" * (size - len(self._data))) return size def seek(self, offset, whence=0): if whence == 0: self._pos = offset elif whence == 1: self._pos += offset elif whence == 2: self._pos = len(self._data) + offset self._pos = max(0, self._pos) return self._pos def tell(self): return self._pos class Blake2bMerkleHasher: """Custom Merkle hasher using BLAKE2b for testing.""" hash_len = 32 def hash_leaf(self, chunk: bytes, chunk_len: int, chunk_idx: int) -> bytes: h = hashlib.blake2b(digest_size=32) h.update(b"\x00") h.update(chunk_idx.to_bytes(8, "little")) h.update(chunk[:chunk_len]) return h.digest() def hash_parent( self, left: bytes, right: bytes, level: int, node_idx: int ) -> bytes: h = hashlib.blake2b(digest_size=32) h.update(b"\x01") h.update(level.to_bytes(4, "little")) h.update(node_idx.to_bytes(8, "little")) h.update(left) h.update(right) return h.digest() def hash_empty(self, level: int, node_idx: int) -> bytes: h = hashlib.blake2b(digest_size=32) h.update(b"\x02") h.update(level.to_bytes(4, "little")) h.update(node_idx.to_bytes(8, "little")) return h.digest() def hash_commitment( self, structural_root: bytes, ctx: bytes, file_size: int ) -> bytes: h = hashlib.blake2b(digest_size=32) h.update(b"\x03") h.update(structural_root) h.update(ctx) h.update(file_size.to_bytes(8, "little")) return h.digest() class TestFuzzRAFRandomOps: """Random operation sequences compared against a reference oracle.""" @pytest.mark.parametrize("seed", range(20)) @pytest.mark.parametrize("c", FUZZ_CIPHERS, ids=cipher_id) def test_random_ops_no_merkle(self, seed, c): """Random write/read/seek/truncate ops match reference.""" rng = random.Random(seed) storage = BytesIOStorage() key = c.random_key() chunk_size = rng.choice([1024, 2048, 4096]) ref = RefFile() with raf.create(storage, key, cipher=c, chunk_size=chunk_size) as f: for _ in range(50): op = rng.choice( [ "write", "write", "pwrite", "read", "pread", "seek", "truncate", "read_all", "size", ] ) self._do_op(rng, f, ref, op) @pytest.mark.parametrize("seed", range(20)) def test_random_ops_with_merkle(self, seed): """Random ops with merkle=True; root stays consistent.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() chunk_size = rng.choice([1024, 2048]) ref = RefFile() with raf.create( storage, key, cipher=aegis128l, chunk_size=chunk_size, merkle=True, merkle_max_chunks=256, ) as f: for _ in range(40): op = rng.choice( [ "write", "write", "pwrite", "read", "pread", "seek", "truncate", "read_all", "size", ] ) self._do_op(rng, f, ref, op, max_data=chunk_size * 2) root = f.root_hash assert root is not None and len(root) == 32 # Reopen and verify with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=256 ) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None # Data still matches reference assert f.pread(ref.size, 0) == bytes(ref._data) def _do_op(self, rng, f, ref, op, max_data=4096): if op == "write": data = os.urandom(rng.randint(0, max_data)) # Sometimes write at current pos, sometimes at explicit offset if rng.random() < 0.3 and ref.size > 0: offset = rng.randint(0, ref.size) f.write(data, offset=offset) ref.write(data, offset=offset) else: f.write(data) ref.write(data) elif op == "pwrite": data = os.urandom(rng.randint(0, max_data)) offset = rng.randint(0, max(1, ref.size)) f.pwrite(data, offset) ref.pwrite(data, offset) elif op == "read": size = rng.randint(0, max_data) got = f.read(size) expected = ref.read(size) assert got == expected, ( f"read({size}): got {len(got)} bytes, expected {len(expected)}" ) elif op == "pread": if ref.size > 0: offset = rng.randint(0, ref.size - 1) size = rng.randint(0, ref.size - offset + 100) else: offset = 0 size = rng.randint(0, 100) got = f.pread(size, offset) expected = ref.pread(size, offset) assert got == expected elif op == "read_all": offset = rng.randint(0, max(1, ref.size)) f.seek(offset) ref.seek(offset) got = f.read(-1) expected = ref.read(-1) assert got == expected elif op == "seek": whence = rng.choice([0, 1, 2]) if whence == 0: offset = rng.randint(0, max(1, ref.size + 100)) elif whence == 1: # Avoid negative resulting position offset = rng.randint(-ref.tell(), ref.size + 50) else: offset = rng.randint(-ref.size, 0) if ref.size > 0 else 0 new_pos = f.seek(offset, whence) ref_pos = ref.seek(offset, whence) assert new_pos == ref_pos, ( f"seek({offset}, {whence}): {new_pos} != {ref_pos}" ) elif op == "truncate": new_size = rng.randint(0, max(1, ref.size + 100)) got = f.truncate(new_size) expected = ref.truncate(new_size) assert got == expected elif op == "size": assert f.size == ref.size class TestFuzzMerkleConsistency: """Merkle tree consistency under random operations.""" @pytest.mark.parametrize("seed", range(10)) @pytest.mark.parametrize("c", FUZZ_CIPHERS, ids=cipher_id) def test_merkle_rebuild_always_matches(self, seed, c): """After random writes, close/reopen/rebuild always reproduces root.""" rng = random.Random(seed) storage = BytesIOStorage() key = c.random_key() chunk_size = 1024 with raf.create( storage, key, cipher=c, chunk_size=chunk_size, merkle=True, merkle_max_chunks=128, ) as f: # Do random writes for _ in range(rng.randint(5, 20)): data = os.urandom(rng.randint(1, chunk_size * 3)) if rng.random() < 0.5 and f.size > 0: offset = rng.randint(0, f.size) f.pwrite(data, offset) else: f.write(data) root = f.root_hash with raf.open(storage, key, cipher=c, merkle=True, merkle_max_chunks=128) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None @pytest.mark.parametrize("seed", range(10)) def test_verify_root_after_random_ops(self, seed): """verify_root succeeds after random ops.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=128, ) as f: for _ in range(rng.randint(3, 15)): data = os.urandom(rng.randint(1, 3000)) f.write(data) # Also some truncations if f.size > 0 and rng.random() < 0.3: f.truncate(rng.randint(0, f.size)) root = f.root_hash with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=128 ) as f: f.verify_root(root) @pytest.mark.parametrize("seed", range(10)) def test_merkle_root_deterministic_across_write_patterns(self, seed): """Different write patterns for same content produce same root after rebuild.""" rng = random.Random(seed) key = aegis128l.random_key() total_size = rng.randint(100, 4000) full_data = os.urandom(total_size) # Pattern 1: single write s1 = BytesIOStorage() with raf.create( s1, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=64, ) as f: f.write(full_data) root1 = f.root_hash # Reopen and rebuild - must match with raf.open( s1, key, cipher=aegis128l, merkle=True, merkle_max_chunks=64 ) as f: f.merkle_rebuild() assert f.root_hash == root1 # Pattern 2: chunked writes to same file (truncate and rewrite) with raf.open( s1, key, cipher=aegis128l, merkle=True, merkle_max_chunks=64 ) as f: f.merkle_rebuild() f.truncate(0) pos = 0 while pos < total_size: chunk = rng.randint(1, min(500, total_size - pos)) f.write(full_data[pos : pos + chunk]) pos += chunk root2 = f.root_hash # Pattern 3: pwrite in random order to same file with raf.open( s1, key, cipher=aegis128l, merkle=True, merkle_max_chunks=64 ) as f: f.merkle_rebuild() f.truncate(0) f.write(b"\x00" * total_size) offsets = list(range(0, total_size, 100)) rng.shuffle(offsets) for off in offsets: end = min(off + 100, total_size) f.pwrite(full_data[off:end], off) root3 = f.root_hash assert root1 == root2 assert root1 == root3 class TestFuzzMerkleTampering: """Detect tampering in ciphertext with Merkle verification.""" @pytest.mark.parametrize("seed", range(15)) def test_tamper_random_byte_detected(self, seed): """Flipping a random byte in the ciphertext is detected by rebuild or verify.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=64, ) as f: data = os.urandom(rng.randint(100, 5000)) f.write(data) original_root = f.root_hash raw = storage._data # Skip the 92-byte header to tamper only chunk data if len(raw) > 100: tamper_pos = rng.randint(92, len(raw) - 1) raw[tamper_pos] ^= rng.randint(1, 255) # Either rebuild fails (auth error) or verify detects mismatch try: with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=64 ) as f: f.merkle_rebuild() result = f.merkle_verify() # If rebuild succeeded, verify should find mismatch # OR the root changed assert result is not None or f.root_hash != original_root except (ValueError, RuntimeError): pass # Expected: tampered chunk fails authentication @pytest.mark.parametrize("seed", range(10)) def test_tamper_multiple_bytes_detected(self, seed): """Flipping multiple random bytes is detected.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=64, ) as f: f.write(os.urandom(rng.randint(2000, 8000))) original_root = f.root_hash raw = storage._data # Tamper 3-10 random bytes n_tampers = rng.randint(3, 10) for _ in range(n_tampers): if len(raw) > 100: pos = rng.randint(92, len(raw) - 1) raw[pos] ^= rng.randint(1, 255) try: with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=64 ) as f: f.merkle_rebuild() result = f.merkle_verify() assert result is not None or f.root_hash != original_root except (ValueError, RuntimeError): pass class TestFuzzChunkBoundaries: """Test operations at exact chunk boundaries and crossing them.""" @pytest.mark.parametrize("chunk_size", [1024, 2048]) @pytest.mark.parametrize("seed", range(5)) def test_write_at_chunk_boundaries(self, chunk_size, seed): """Writes that start/end/cross chunk boundaries.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() ref = RefFile() with raf.create( storage, key, cipher=aegis128l, chunk_size=chunk_size, merkle=True, merkle_max_chunks=64, ) as f: for _i in range(20): # Choose offsets near chunk boundaries chunk_idx = rng.randint(0, 5) boundary = chunk_idx * chunk_size offset = boundary + rng.choice([-2, -1, 0, 1, 2, chunk_size // 2]) offset = max(0, offset) size = rng.choice( [ 1, chunk_size - 1, chunk_size, chunk_size + 1, chunk_size * 2, rng.randint(1, chunk_size * 3), ] ) data = os.urandom(size) f.pwrite(data, offset) ref.pwrite(data, offset) # Verify full content matches assert f.size == ref.size assert f.pread(f.size, 0) == bytes(ref._data) root = f.root_hash # Verify merkle with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=64 ) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None @pytest.mark.parametrize("seed", range(5)) def test_read_across_chunk_boundaries(self, seed): """Reads that span multiple chunks.""" rng = random.Random(seed) chunk_size = 1024 storage = BytesIOStorage() key = aegis128l.random_key() total = chunk_size * 5 data = os.urandom(total) with raf.create(storage, key, cipher=aegis128l, chunk_size=chunk_size) as f: f.write(data) # Random reads crossing boundaries for _ in range(30): offset = rng.randint(0, total - 1) size = rng.randint(1, total - offset) got = f.pread(size, offset) assert got == data[offset : offset + size] class TestFuzzTruncate: """Fuzz truncation combined with writes and merkle.""" @pytest.mark.parametrize("seed", range(10)) def test_truncate_and_rewrite(self, seed): """Truncate to various sizes then rewrite; data and merkle stay consistent.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() ref = RefFile() with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=128, ) as f: # Initial write data = os.urandom(rng.randint(500, 5000)) f.write(data) ref.write(data) for _ in range(15): op = rng.choice(["truncate", "write", "pwrite"]) if op == "truncate": new_size = rng.randint(0, ref.size + 500) f.truncate(new_size) ref.truncate(new_size) elif op == "write": d = os.urandom(rng.randint(0, 2000)) f.write(d) ref.write(d) elif op == "pwrite": d = os.urandom(rng.randint(0, 1000)) off = rng.randint(0, max(1, ref.size)) f.pwrite(d, off) ref.pwrite(d, off) assert f.size == ref.size if ref.size > 0: assert f.pread(ref.size, 0) == bytes(ref._data) root = f.root_hash # Reopen and verify with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=128 ) as f: f.merkle_rebuild() assert f.root_hash == root class TestFuzzEmptyAndTiny: """Edge cases with empty files and tiny writes.""" def test_empty_file_merkle(self): """Create empty file with merkle, root_hash is valid.""" storage = BytesIOStorage() key = aegis128l.random_key() with raf.create(storage, key, cipher=aegis128l, merkle=True) as f: root = f.root_hash assert root is not None assert len(root) == 32 def test_single_byte_write_merkle(self): """Single byte write with merkle.""" storage = BytesIOStorage() key = aegis128l.random_key() with raf.create(storage, key, cipher=aegis128l, merkle=True) as f: f.write(b"\x42") root = f.root_hash with raf.open(storage, key, cipher=aegis128l, merkle=True) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None assert f.pread(1, 0) == b"\x42" def test_write_then_truncate_to_zero(self): """Write data then truncate to zero; merkle should be consistent.""" storage = BytesIOStorage() key = aegis128l.random_key() with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=64, ) as f: f.write(os.urandom(3000)) f.truncate(0) assert f.size == 0 root = f.root_hash with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=64 ) as f: f.merkle_rebuild() assert f.root_hash == root @pytest.mark.parametrize("size", [1, 15, 16, 17, 1023, 1024, 1025]) def test_exact_sizes_roundtrip(self, size): """Files of exact sizes (around alignment) round-trip correctly.""" storage = BytesIOStorage() key = aegis128l.random_key() data = os.urandom(size) with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=True ) as f: f.write(data) root = f.root_hash with raf.open(storage, key, cipher=aegis128l, merkle=True) as f: f.merkle_rebuild() assert f.root_hash == root assert f.pread(size, 0) == data class TestFuzzMultipleReopenCycles: """Test repeated open/write/close/reopen cycles.""" @pytest.mark.parametrize("seed", range(5)) def test_multiple_reopen_append(self, seed): """Open/write/close/reopen cycle many times; data accumulates correctly.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() ref = RefFile() # Initial create with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=256, ) as f: d = os.urandom(rng.randint(100, 500)) f.write(d) ref.write(d) for _cycle in range(8): with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=256 ) as f: f.merkle_rebuild() # Sometimes append, sometimes overwrite if rng.random() < 0.6: d = os.urandom(rng.randint(50, 300)) offset = f.size f.pwrite(d, offset) ref.pwrite(d, offset) else: if f.size > 0: offset = rng.randint(0, f.size - 1) d = os.urandom(rng.randint(1, 200)) f.pwrite(d, offset) ref.pwrite(d, offset) # Final verify with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=256 ) as f: f.merkle_rebuild() assert f.merkle_verify() is None assert f.pread(ref.size, 0) == bytes(ref._data) class TestFuzzAllVariants: """Test all 6 AEGIS variants with random operations.""" @pytest.mark.parametrize("c", CIPHERS, ids=cipher_id) @pytest.mark.parametrize("seed", range(3)) def test_variant_random_ops_merkle(self, c, seed): """Each variant handles random ops + merkle correctly.""" rng = random.Random(seed) storage = BytesIOStorage() key = c.random_key() ref = RefFile() with raf.create( storage, key, cipher=c, chunk_size=1024, merkle=True, merkle_max_chunks=128 ) as f: for _ in range(20): data = os.urandom(rng.randint(1, 2000)) if rng.random() < 0.4 and ref.size > 0: off = rng.randint(0, ref.size) f.pwrite(data, off) ref.pwrite(data, off) else: f.write(data) ref.write(data) root = f.root_hash with raf.open(storage, key, cipher=c, merkle=True, merkle_max_chunks=128) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None if ref.size > 0: assert f.pread(ref.size, 0) == bytes(ref._data) class TestFuzzCustomHasher: """Fuzz with custom Merkle hashers.""" @pytest.mark.parametrize("seed", range(5)) def test_blake2b_random_ops(self, seed): """Custom Blake2b hasher under random operations.""" rng = random.Random(seed) hasher = Blake2bMerkleHasher() storage = BytesIOStorage() key = aegis128l.random_key() ref = RefFile() with raf.create( storage, key, cipher=aegis128l, chunk_size=1024, merkle=hasher, merkle_max_chunks=128, ) as f: for _ in range(25): data = os.urandom(rng.randint(1, 2000)) f.write(data) ref.write(data) root = f.root_hash with raf.open( storage, key, cipher=aegis128l, merkle=hasher, merkle_max_chunks=128 ) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None assert f.pread(ref.size, 0) == bytes(ref._data) def test_sha256_vs_blake2b_differ(self): """SHA256 and Blake2b hashers produce different roots for same data.""" key = aegis128l.random_key() data = os.urandom(2000) sha_storage = BytesIOStorage() with raf.create( sha_storage, key, cipher=aegis128l, chunk_size=1024, merkle=True ) as f: f.write(data) sha_root = f.root_hash b2_storage = BytesIOStorage() with raf.create( b2_storage, key, cipher=aegis128l, chunk_size=1024, merkle=Blake2bMerkleHasher(), ) as f: f.write(data) b2_root = f.root_hash assert sha_root != b2_root class TestFuzzWrongKey: """Fuzz: opening files with wrong keys.""" @pytest.mark.parametrize("seed", range(10)) def test_wrong_key_rejected(self, seed): """Random data written, then reopened with wrong key fails.""" rng = random.Random(seed) c = rng.choice(CIPHERS) storage = BytesIOStorage() key = c.random_key() with raf.create(storage, key, cipher=c, chunk_size=1024) as f: f.write(os.urandom(rng.randint(100, 3000))) wrong_key = c.random_key() assert wrong_key != key # Negligible collision probability with pytest.raises(ValueError, match="authentication failed"): raf.open(storage, wrong_key, cipher=c) class TestFuzzFileStorage: """Fuzz test RAF with real file I/O.""" @pytest.mark.parametrize("seed", range(5)) def test_file_storage_random_ops(self, seed, tmp_path): """Random ops on file-backed storage produce correct results.""" rng = random.Random(seed) path = tmp_path / f"fuzz_{seed}.aegis" key = aegis128l.random_key() ref = RefFile() with FileStorage(path, "w+b") as fs: with raf.create( fs, key, cipher=aegis128l, chunk_size=1024, merkle=True, merkle_max_chunks=128, ) as f: for _ in range(20): data = os.urandom(rng.randint(1, 1500)) if rng.random() < 0.3 and ref.size > 0: off = rng.randint(0, ref.size) f.pwrite(data, off) ref.pwrite(data, off) else: f.write(data) ref.write(data) root = f.root_hash # Reopen from file and verify with FileStorage(path, "r+b") as fs: with raf.open( fs, key, cipher=aegis128l, merkle=True, merkle_max_chunks=128, ) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None if ref.size > 0: assert f.pread(ref.size, 0) == bytes(ref._data) class TestFuzzRafOpen: """Fuzz probe + explicit-cipher open with random data. pyaegis tested raf_open auto-detection; our raf.open() requires the cipher, so we check probe() reports the right alg_id instead. """ @pytest.mark.parametrize("c", CIPHERS, ids=cipher_id) def test_probe_reports_variant(self, c): """probe() correctly reports each variant after random writes.""" storage = BytesIOStorage() key = c.random_key() data = os.urandom(random.randint(100, 3000)) with raf.create(storage, key, cipher=c, chunk_size=1024, merkle=True) as f: f.write(data) root = f.root_hash assert raf.probe(storage).alg_id == c.RAF_ALG_ID with raf.open(storage, key, cipher=c, merkle=True) as f: f.verify_root(root) assert f.pread(len(data), 0) == data class TestFuzzMerkleMaxChunks: """Test merkle_max_chunks boundary.""" def test_write_exactly_max_chunks(self): """Writing exactly max_chunks chunks is OK.""" chunk_size = 1024 max_chunks = 4 storage = BytesIOStorage() key = aegis128l.random_key() with raf.create( storage, key, cipher=aegis128l, chunk_size=chunk_size, merkle=True, merkle_max_chunks=max_chunks, ) as f: # Write exactly max_chunks * chunk_size bytes data = os.urandom(chunk_size * max_chunks) f.write(data) assert f.root_hash is not None def test_write_exceeds_max_chunks(self): """Writing past max_chunks raises ValueError.""" chunk_size = 1024 max_chunks = 4 storage = BytesIOStorage() key = aegis128l.random_key() with raf.create( storage, key, cipher=aegis128l, chunk_size=chunk_size, merkle=True, merkle_max_chunks=max_chunks, ) as f: data = os.urandom(chunk_size * (max_chunks + 1)) with pytest.raises(ValueError, match="merkle_max_chunks"): f.write(data) @pytest.mark.parametrize("max_chunks", [1, 2, 3, 8]) def test_boundary_max_chunks(self, max_chunks): """Boundary test: fill exactly to max, verify merkle still works.""" chunk_size = 1024 storage = BytesIOStorage() key = aegis128l.random_key() with raf.create( storage, key, cipher=aegis128l, chunk_size=chunk_size, merkle=True, merkle_max_chunks=max_chunks, ) as f: data = os.urandom(chunk_size * max_chunks) f.write(data) root = f.root_hash with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=max_chunks ) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None class TestFuzzReadInto: """Fuzz test read_into with random patterns.""" @pytest.mark.parametrize("seed", range(5)) def test_read_into_matches_read(self, seed): """read_into produces same result as read for same offset/size.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() data = os.urandom(rng.randint(500, 5000)) with raf.create(storage, key, cipher=aegis128l, chunk_size=1024) as f: f.write(data) for _ in range(30): offset = rng.randint(0, len(data)) size = rng.randint(0, len(data) - offset + 100) size = min(size, len(data)) f.seek(offset) read_result = f.read(size) buf = bytearray(size) f.seek(offset) n = f.read_into(buf, offset) assert bytes(buf[:n]) == read_result class TestFuzzSeekTell: """Fuzz seek/tell consistency.""" @pytest.mark.parametrize("seed", range(5)) def test_seek_tell_consistency(self, seed): """seek returns same value as subsequent tell.""" rng = random.Random(seed) storage = BytesIOStorage() key = aegis128l.random_key() with raf.create(storage, key, cipher=aegis128l, chunk_size=1024) as f: f.write(os.urandom(rng.randint(100, 3000))) for _ in range(50): whence = rng.choice([0, 1, 2]) if whence == 0: off = rng.randint(0, f.size + 100) elif whence == 1: off = rng.randint(-f.tell(), f.size + 50) else: off = rng.randint(-f.size, 50) pos = f.seek(off, whence) assert f.tell() == pos class TestFuzzLargeFile: """Test with larger data sizes.""" def test_large_sequential_writes_merkle(self): """100KB of data in sequential writes with merkle verification.""" storage = BytesIOStorage() key = aegis128l.random_key() total = 100 * 1024 chunk_size = 4096 ref = bytearray() with raf.create( storage, key, cipher=aegis128l, chunk_size=chunk_size, merkle=True, merkle_max_chunks=256, ) as f: written = 0 while written < total: piece = os.urandom(random.randint(1, 8192)) f.write(piece) ref.extend(piece) written += len(piece) root = f.root_hash with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=256 ) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None assert f.pread(len(ref), 0) == bytes(ref) def test_large_random_access_merkle(self): """Random reads/writes across 100KB file with merkle.""" rng = random.Random(42) storage = BytesIOStorage() key = aegis128l.random_key() chunk_size = 4096 ref = RefFile() with raf.create( storage, key, cipher=aegis128l, chunk_size=chunk_size, merkle=True, merkle_max_chunks=256, ) as f: # Initial fill initial = os.urandom(50 * 1024) f.write(initial) ref.write(initial) # Random overwrites for _ in range(100): off = rng.randint(0, ref.size - 1) d = os.urandom(rng.randint(1, 4096)) f.pwrite(d, off) ref.pwrite(d, off) # Verify content assert f.size == ref.size # Read in chunks to verify for off in range(0, ref.size, chunk_size): size = min(chunk_size, ref.size - off) assert f.pread(size, off) == ref.pread(size, off) root = f.root_hash with raf.open( storage, key, cipher=aegis128l, merkle=True, merkle_max_chunks=256 ) as f: f.merkle_rebuild() assert f.root_hash == root assert f.merkle_verify() is None class TestFuzzConcurrentStorageOps: """Test that storage state is consistent across RAF operations.""" def test_probe_after_random_writes(self): """raf.probe returns consistent info after random writes.""" storage = BytesIOStorage() key = aegis128l.random_key() chunk_size = 2048 with raf.create(storage, key, cipher=aegis128l, chunk_size=chunk_size) as f: for _ in range(10): f.write(os.urandom(random.randint(100, 3000))) expected_size = f.size _alg_id, probed_chunk, probed_size = raf.probe(storage) assert probed_chunk == chunk_size assert probed_size == expected_size