//! Reader/writer round-trip tests and strict ordering checks. use star_lite::error::FormatError; use star_lite::reader::{HEADER_CID_PREFIX, MAGIC_V0}; use star_lite::{Error, Reader, Writer}; use star_repo::Cid; use std::io::Cursor; /// A minimal valid header CID: the standard atproto prefix + 32 zero bytes. /// The digest is not a real hash of anything; tests that don't compute MST /// roots just need *some* well-formed header CID. fn synthetic_cid() -> Cid { let mut bytes = [0u8; 36]; bytes[..4].copy_from_slice(&HEADER_CID_PREFIX); Cid::try_from(&bytes[..]).expect("valid CID prefix") } /// Hand-rolled header: magic + synthetic CID + varint(0) for an absent /// partial commit. fn handcrafted_header(buf: &mut Vec) { buf.extend_from_slice(&MAGIC_V0); buf.extend_from_slice(&HEADER_CID_PREFIX); buf.extend_from_slice(&[0u8; 32]); buf.push(0); } fn build_archive(entries: &[(&str, &[u8])]) -> Vec { let mut buf = Vec::new(); let mut w = Writer::new(&mut buf); w.write_header(synthetic_cid(), None).unwrap(); for (k, v) in entries { w.write_entry(k, v).unwrap(); } w.finish().unwrap(); buf } #[test] fn empty_repo_roundtrips() { let archive = build_archive(&[]); assert_eq!(&archive[..3], &MAGIC_V0); let reader = Reader::new(Cursor::new(&archive)).unwrap(); assert_eq!(reader.header_cid().unwrap(), synthetic_cid()); assert!(reader.partial_commit().unwrap().is_none()); let collected: Vec<_> = reader.collect::>().unwrap(); assert!(collected.is_empty()); } #[test] fn full_roundtrip_preserves_keys_and_records() { let entries: &[(&str, &[u8])] = &[ ("app.bsky.feed.post/3kabcabc", b"\xA1\x64body\x65hello"), ("app.bsky.feed.post/3kabcabd", b"\xA1\x64body\x65world"), ("app.bsky.graph.follow/3kfollow", b"\xA0"), ("app.bsky.graph.follow/3lfollow", b"\xA0"), ]; let archive = build_archive(entries); let reader = Reader::new(Cursor::new(&archive)).unwrap(); let parsed: Vec<_> = reader.map(|r| r.unwrap()).collect(); assert_eq!(parsed.len(), entries.len()); for (out, (k, v)) in parsed.iter().zip(entries) { assert_eq!(out.key, *k); assert_eq!(out.record, *v); } } #[test] fn writer_rejects_out_of_order_keys() { let mut buf = Vec::new(); let mut w = Writer::new(&mut buf); w.write_header(synthetic_cid(), None).unwrap(); w.write_entry("b/2", &[]).unwrap(); let err = w.write_entry("a/1", &[]).unwrap_err(); assert!(matches!(err, Error::Format(FormatError::KeyOrder { .. }))); } #[test] fn writer_rejects_duplicate_keys() { let mut buf = Vec::new(); let mut w = Writer::new(&mut buf); w.write_header(synthetic_cid(), None).unwrap(); w.write_entry("a/1", &[]).unwrap(); let err = w.write_entry("a/1", &[]).unwrap_err(); assert!(matches!(err, Error::Format(FormatError::KeyOrder { .. }))); } #[test] fn writer_requires_header_first() { let mut buf = Vec::new(); let mut w = Writer::new(&mut buf); let err = w.write_entry("a/1", &[]).unwrap_err(); assert!(matches!(err, Error::Format(FormatError::BadWriterState(_)))); } #[test] fn reader_rejects_out_of_order_archive() { // Hand-crafted archive: two keys in descending order. // Format: magic || cid(36) || varint(commit_len) || partial || // varint(klen) || k || varint(rlen) || r || ... let mut buf = Vec::new(); handcrafted_header(&mut buf); // "b" first buf.push(0x01); buf.push(b'b'); buf.push(0x00); // then "a" — out of order buf.push(0x01); buf.push(b'a'); buf.push(0x00); let mut reader = Reader::new(Cursor::new(&buf)).unwrap(); assert_eq!(reader.next_entry().unwrap().unwrap().key, "b"); let err = reader.next_entry().unwrap_err(); assert!(matches!(err, Error::Format(FormatError::KeyOrder { .. }))); } #[test] fn reader_rejects_bad_magic() { let buf = b"XXXextra-bytes"; let err = match Reader::new(Cursor::new(&buf[..])) { Ok(_) => panic!("expected error for bad magic"), Err(e) => e, }; assert!(matches!(err, Error::Format(FormatError::InvalidMagic))); } #[test] fn reader_rejects_empty_input() { let buf: &[u8] = &[]; let err = match Reader::new(Cursor::new(buf)) { Ok(_) => panic!("expected error for empty input"), Err(e) => e, }; assert!(matches!(err, Error::Format(FormatError::InvalidMagic))); } #[test] fn reader_rejects_bad_header_cid_prefix() { let mut buf = Vec::new(); buf.extend_from_slice(&MAGIC_V0); // Wrong prefix: not 0x01711220. buf.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); buf.extend_from_slice(&[0u8; 32]); buf.push(0); let err = match Reader::new(Cursor::new(&buf)) { Ok(_) => panic!("expected error for bad header CID prefix"), Err(e) => e, }; assert!(matches!(err, Error::Format(FormatError::InvalidHeaderCid))); } #[test] fn reader_rejects_truncated_record() { let mut buf = Vec::new(); handcrafted_header(&mut buf); buf.push(0x05); // declares 5-byte key buf.extend_from_slice(b"ab"); // only 2 bytes provided let mut reader = Reader::new(Cursor::new(&buf)).unwrap(); let err = reader.next_entry().unwrap_err(); assert!(matches!(err, Error::Format(FormatError::UnexpectedEof))); } #[test] fn reader_enforces_size_limits() { use star_lite::Limits; let mut buf = Vec::new(); handcrafted_header(&mut buf); // Declare a 1024-byte key (exceeds our test limit of 4). // 1024 = 0x400 → varint: 0x80 0x08 buf.push(0x80); buf.push(0x08); let limits = Limits { max_key_len: 4, ..Limits::default() }; let mut reader = Reader::with_limits(Cursor::new(&buf), limits).unwrap(); let err = reader.next_entry().unwrap_err(); assert!(matches!( err, Error::Format(FormatError::SizeLimit { field: "key", .. }) )); } #[test] fn writer_rejects_empty_key() { let mut buf = Vec::new(); let mut w = Writer::new(&mut buf); w.write_header(synthetic_cid(), None).unwrap(); let err = w.write_entry("", b"x").unwrap_err(); assert!(matches!(err, Error::Format(FormatError::EmptyKey))); }