Something went wrong. Try again.
STreaming ARchive format: simpler, stricter, smaller than CARs for a subset of use-cases.
star car archive atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202//! 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<u8>) { 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<u8> { 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::<Result<_, _>>().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)));}