use tempfile::TempDir;
use super::{ObjectType, PackBuilder, PackObjectId, PackReader, pack_index::PackIndex};
use crate::{
delta::MAX_DELTA_OUTPUT_SIZE,
object::{ChangeId, ContentHash},
store::{StoreError, compression::CompressionConfig, pack::pack_container_spec},
};
fn create_test_hash(n: u8) -> ContentHash {
let bytes: [u8; 32] = [n; 32];
ContentHash::from_bytes(bytes)
}
fn single_record_pack(
hash: ContentHash,
write_record_tail: impl FnOnce(&mut Vec<u8>),
) -> (Vec<u8>, Vec<u8>) {
let mut pack_data = Vec::new();
pack_data.extend_from_slice(pack_container_spec().magic);
pack_data.extend_from_slice(&pack_container_spec().version.to_be_bytes());
pack_data.extend_from_slice(&1u64.to_be_bytes());
let entry_offset = u64::try_from(pack_data.len()).expect("test pack offset fits in u64");
PackObjectId::Hash(hash).encode_tagged(&mut pack_data);
write_record_tail(&mut pack_data);
super::append_container_checksum(&mut pack_data);
let mut index = PackIndex::new();
index.add(PackObjectId::Hash(hash), entry_offset);
index.sort();
(pack_data, index.to_bytes())
}
fn assert_invalid_object_message_contains(error: StoreError, expected: &str) {
assert!(
matches!(error, StoreError::InvalidObject(ref message) if message.contains(expected)),
"expected InvalidObject containing '{expected}', got: {error:?}"
);
}
#[test]
fn test_pack_container_header_codec_matches_legacy_bytes() {
let mut legacy = Vec::new();
legacy.extend_from_slice(b"LMPK");
legacy.extend_from_slice(&2_u32.to_be_bytes());
legacy.extend_from_slice(&0_u64.to_be_bytes());
let checksum = blake3::hash(&legacy);
legacy.extend_from_slice(checksum.as_bytes());
let mut encoded = Vec::new();
super::write_container_header(&mut encoded, pack_container_spec(), 0);
super::append_container_checksum(&mut encoded);
assert_eq!(encoded, legacy);
assert_eq!(
super::verify_container(&legacy, pack_container_spec()).unwrap(),
(0, 16, 16)
);
}
#[test]
fn test_pack_index_header_codec_matches_legacy_bytes() {
let legacy = b"LMI\0\
\0\0\0\x02\
\0\0\0\0\0\0\0\0"
.to_vec();
let encoded = PackIndex::new().to_bytes();
assert_eq!(encoded, legacy);
assert!(PackIndex::from_bytes(&legacy).unwrap().ids().is_empty());
}
#[test]
fn test_pack_index_roundtrip() {
let mut index = PackIndex::new();
index.add(PackObjectId::Hash(create_test_hash(1)), 100);
index.add(PackObjectId::Hash(create_test_hash(2)), 200);
index.add(PackObjectId::ChangeId(ChangeId::from_bytes([3; 16])), 300);
index.sort();
let bytes = index.to_bytes();
let restored = PackIndex::from_bytes(&bytes).expect("Failed to deserialize index");
assert_eq!(
restored.find(&PackObjectId::Hash(create_test_hash(1))),
Some(100)
);
assert_eq!(
restored.find(&PackObjectId::Hash(create_test_hash(2))),
Some(200)
);
assert_eq!(
restored.find(&PackObjectId::ChangeId(ChangeId::from_bytes([3; 16]))),
Some(300)
);
assert_eq!(
restored.find(&PackObjectId::Hash(create_test_hash(4))),
None
);
}
#[test]
fn test_pack_builder_basic() {
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
let hash1 = create_test_hash(1);
let data1 = b"Hello, World!".to_vec();
builder.add(hash1, ObjectType::Blob, data1.clone());
let hash2 = create_test_hash(2);
let data2 = b"Goodbye, World!".to_vec();
builder.add(hash2, ObjectType::Blob, data2.clone());
let (pack_data, index_data, stats) = builder.build().expect("Failed to build pack");
assert!(!pack_data.is_empty());
assert!(!index_data.is_empty());
assert_eq!(stats.object_count, 2);
assert!(stats.compression_ratio > 0.0 && stats.compression_ratio <= 1.0);
}
#[test]
fn test_pack_reader() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
let hash1 = create_test_hash(1);
let data1 = b"Test data 1".repeat(100);
builder.add(hash1, ObjectType::Blob, data1.clone());
let (pack_data, index_data, _) = builder.build().expect("Failed to build pack");
std::fs::write(&pack_path, &pack_data).expect("Failed to write pack file");
std::fs::write(&index_path, &index_data).expect("Failed to write index file");
let reader = PackReader::open(&pack_path, &index_path).expect("Failed to open pack");
let (obj_type, retrieved) = reader
.get_hashed_object(&hash1)
.expect("Failed to get object")
.expect("Object not found");
assert_eq!(obj_type, ObjectType::Blob);
assert_eq!(retrieved, data1);
}
#[test]
fn test_delta_compression() {
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
let base_hash = create_test_hash(1);
let base_data = b"This is the base content. ".repeat(100).to_vec();
builder.add(base_hash, ObjectType::Blob, base_data.clone());
let target_hash = create_test_hash(2);
let target_data = b"This is modified content. ".repeat(100).to_vec();
builder.add(target_hash, ObjectType::Blob, target_data.clone());
let (_pack_data, _index_data, stats) = builder.build().expect("Failed to build pack");
assert!(stats.delta_count > 0);
assert!(stats.compression_ratio < 1.0);
}
#[test]
fn test_pack_reader_rejects_compressed_size_that_overflows_record_end() {
let hash = create_test_hash(42);
let (pack_data, index_data) = single_record_pack(hash, |record| {
super::varint::encode_type_and_size(ObjectType::Blob, 1, record);
super::varint::encode_varint(u64::MAX, record);
});
let reader = PackReader::from_bytes(pack_data, index_data).expect("container is well-formed");
let error = reader
.get_hashed_object(&hash)
.expect_err("oversized compressed_size must fail before slicing");
assert!(
matches!(
error,
StoreError::InvalidObject(ref message)
if message.contains("overflows") || message.contains("platform limits")
),
"expected overflow/platform-limit error, got: {error:?}",
);
let bytes_error = reader
.get_hashed_object_bytes(&hash)
.expect_err("zero-copy path must reject oversized compressed_size too");
assert!(
matches!(
bytes_error,
StoreError::InvalidObject(ref message)
if message.contains("overflows") || message.contains("platform limits")
),
"expected overflow/platform-limit error, got: {bytes_error:?}",
);
}
#[test]
fn test_pack_reader_rejects_uncompressed_size_above_pack_object_limit() {
let hash = create_test_hash(48);
let (pack_data, index_data) = single_record_pack(hash, |record| {
super::varint::encode_type_and_size(ObjectType::Blob, u64::from(u32::MAX) + 1, record);
super::varint::encode_varint(1, record);
record.push(0);
});
let reader = PackReader::from_bytes(pack_data, index_data).expect("container is well-formed");
let error = reader
.get_hashed_object(&hash)
.expect_err("absurd uncompressed_size must fail before allocation");
assert_invalid_object_message_contains(error, "Pack object output size");
let bytes_error = reader
.get_hashed_object_bytes(&hash)
.expect_err("zero-copy path must reject absurd uncompressed_size too");
assert_invalid_object_message_contains(bytes_error, "Pack object output size");
}
#[test]
fn test_pack_reader_rejects_truncated_compressed_size_varint() {
let hash = create_test_hash(43);
let (pack_data, index_data) = single_record_pack(hash, |record| {
super::varint::encode_type_and_size(ObjectType::Blob, 4, record);
record.push(0x80);
});
let reader = PackReader::from_bytes(pack_data, index_data).expect("container is well-formed");
let error = reader
.get_hashed_object(&hash)
.expect_err("truncated compressed_size must not read into checksum bytes");
assert_invalid_object_message_contains(error, "Truncated compressed_size varint");
let bytes_error = reader
.get_hashed_object_bytes(&hash)
.expect_err("zero-copy path must reject truncated compressed_size too");
assert_invalid_object_message_contains(bytes_error, "Truncated compressed_size varint");
}
#[test]
fn test_pack_reader_rejects_compressed_size_past_content_end() {
let hash = create_test_hash(44);
let (pack_data, index_data) = single_record_pack(hash, |record| {
super::varint::encode_type_and_size(ObjectType::Blob, 10, record);
super::varint::encode_varint(10, record);
record.extend_from_slice(b"abc");
});
let reader = PackReader::from_bytes(pack_data, index_data).expect("container is well-formed");
let error = reader
.get_hashed_object(&hash)
.expect_err("record payload shorter than compressed_size must fail");
assert_invalid_object_message_contains(error, "Entry data out of bounds");
let bytes_error = reader
.get_hashed_object_bytes(&hash)
.expect_err("zero-copy path must reject payload shorter than compressed_size too");
assert_invalid_object_message_contains(bytes_error, "Entry data out of bounds");
}
#[test]
fn test_pack_reader_decodes_well_formed_manual_record() {
let hash = create_test_hash(45);
let payload = b"manual-pack-record".to_vec();
let (pack_data, index_data) = single_record_pack(hash, |record| {
super::varint::encode_type_and_size(ObjectType::Blob, payload.len() as u64, record);
super::varint::encode_varint(payload.len() as u64, record);
record.extend_from_slice(&payload);
});
let reader = PackReader::from_bytes(pack_data, index_data).expect("container is well-formed");
let (obj_type, data) = reader
.get_hashed_object(&hash)
.expect("well-formed record should decode")
.expect("record should exist");
assert_eq!(obj_type, ObjectType::Blob);
assert_eq!(data, payload);
let (bytes_type, bytes) = reader
.get_hashed_object_bytes(&hash)
.expect("well-formed zero-copy record should decode")
.expect("record should exist");
assert_eq!(bytes_type, ObjectType::Blob);
assert_eq!(bytes.as_ref(), payload.as_slice());
}
#[test]
fn test_pack_reader_rejects_delta_output_above_limit() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
let target_hash = create_test_hash(9);
let oversized =
u64::try_from(MAX_DELTA_OUTPUT_SIZE).expect("MAX_DELTA_OUTPUT_SIZE fits in u64") + 1;
let mut pack_data = Vec::new();
pack_data.extend_from_slice(pack_container_spec().magic);
pack_data.extend_from_slice(&pack_container_spec().version.to_be_bytes());
pack_data.extend_from_slice(&1u64.to_be_bytes());
let entry_offset = pack_data.len() as u64;
PackObjectId::Hash(target_hash).encode_tagged(&mut pack_data);
super::varint::encode_type_and_size(ObjectType::Delta, oversized, &mut pack_data);
super::varint::encode_varint(2, &mut pack_data);
PackObjectId::Hash(create_test_hash(1)).encode_tagged(&mut pack_data);
pack_data.extend_from_slice(&[0, b'x']);
let checksum = blake3::hash(&pack_data);
pack_data.extend_from_slice(checksum.as_bytes());
let mut index = PackIndex::new();
index.add(PackObjectId::Hash(target_hash), entry_offset);
index.sort();
std::fs::write(&pack_path, &pack_data).expect("Failed to write pack file");
std::fs::write(&index_path, index.to_bytes()).expect("Failed to write index file");
let reader = PackReader::open(&pack_path, &index_path).expect("Failed to open pack");
let error = reader
.get_hashed_object(&target_hash)
.expect_err("oversized delta output should fail");
assert!(
matches!(error, crate::store::StoreError::InvalidObject(message) if message.contains("Delta output size"))
);
}
#[cfg(feature = "zstd")]
#[test]
fn test_pack_reader_rejects_compressed_record_claiming_huge_uncompressed_size() {
let hash = create_test_hash(46);
let payload = b"small compressed payload".repeat(32);
let compressed = zstd::encode_all(payload.as_slice(), 3).expect("test zstd compression works");
let (pack_data, index_data) = single_record_pack(hash, |record| {
super::varint::encode_type_and_size(ObjectType::Blob, 0xFFFF_FFFF, record);
super::varint::encode_varint(compressed.len() as u64, record);
record.extend_from_slice(&compressed);
});
let reader = PackReader::from_bytes(pack_data, index_data).expect("container is well-formed");
let error = reader
.get_hashed_object(&hash)
.expect_err("hostile uncompressed_size must be rejected without eager allocation");
assert_invalid_object_message_contains(error, "Pack object output size");
let bytes_error = reader
.get_hashed_object_bytes(&hash)
.expect_err("zero-copy fallback must reject hostile uncompressed_size too");
assert_invalid_object_message_contains(bytes_error, "Pack object output size");
}
#[cfg(feature = "zstd")]
#[test]
fn test_pack_decompression_rejects_streaming_output_past_limit() {
let payload = vec![0xA5; 64 * 1024];
let compressed = zstd::encode_all(payload.as_slice(), 3).expect("test zstd compression works");
assert!(
compressed.len() < 1024,
"test payload should exercise compressed-small/decompressed-large shape"
);
let error = super::shared::decompress_pack_payload_with_limit(&compressed, 0, 32 * 1024)
.expect_err("streaming output above the cap must fail cleanly");
assert_invalid_object_message_contains(error, "Pack object output size");
}
#[cfg(feature = "zstd")]
#[test]
fn test_pack_reader_decodes_compressed_object_larger_than_initial_hint() {
let hash = create_test_hash(47);
let payload = vec![0x5C; super::shared::PACK_DECOMPRESSION_INITIAL_CAP + 64 * 1024];
assert!(payload.len() < super::shared::MAX_PACK_OBJECT_OUTPUT_SIZE);
let compressed = zstd::encode_all(payload.as_slice(), 3).expect("test zstd compression works");
assert!(
compressed.len() < payload.len(),
"manual pack must use the compressed reader path"
);
let (pack_data, index_data) = single_record_pack(hash, |record| {
super::varint::encode_type_and_size(ObjectType::Blob, payload.len() as u64, record);
super::varint::encode_varint(compressed.len() as u64, record);
record.extend_from_slice(&compressed);
});
let reader = PackReader::from_bytes(pack_data, index_data).expect("container is well-formed");
let (obj_type, data) = reader
.get_hashed_object(&hash)
.expect("large compressed object should decode")
.expect("record should exist");
assert_eq!(obj_type, ObjectType::Blob);
assert_eq!(data, payload);
let (bytes_type, bytes) = reader
.get_hashed_object_bytes(&hash)
.expect("large compressed object should decode through bytes path")
.expect("record should exist");
assert_eq!(bytes_type, ObjectType::Blob);
assert_eq!(bytes.as_ref(), payload.as_slice());
}
#[test]
fn test_pack_index_rejects_impossible_entry_count() {
let mut bytes = Vec::new();
bytes.extend_from_slice(super::pack_index::INDEX_MAGIC);
bytes.extend_from_slice(&super::pack_index::INDEX_VERSION.to_be_bytes());
bytes.extend_from_slice(&(2_u64).to_be_bytes());
bytes.extend_from_slice(create_test_hash(1).as_bytes());
bytes.extend_from_slice(&(123_u64).to_be_bytes());
let error = match PackIndex::from_bytes(&bytes) {
Ok(_) => panic!("impossible count should fail"),
Err(error) => error,
};
assert!(
matches!(error, crate::store::StoreError::InvalidObject(message) if message.contains("count"))
);
}
#[test]
fn test_pack_reader_rejects_truncated_pack() {
let temp_dir = TempDir::new().unwrap();
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
std::fs::write(&pack_path, b"short").unwrap();
std::fs::write(&index_path, b"").unwrap();
match PackReader::open(&pack_path, &index_path) {
Err(crate::store::StoreError::InvalidObject(msg)) => {
assert!(
msg.contains("too short") || msg.contains("Pack"),
"expected 'too short' error, got: {msg}"
);
}
Err(e) => panic!("expected InvalidObject, got: {e:?}"),
Ok(_) => panic!("expected error for truncated pack"),
}
}
#[test]
fn test_pack_reader_rejects_corrupt_checksum() {
let temp_dir = TempDir::new().unwrap();
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
builder.add(create_test_hash(1), ObjectType::Blob, b"data".to_vec());
let (mut pack_data, index_data, _) = builder.build().unwrap();
let last = pack_data.len() - 1;
pack_data[last] ^= 0xFF;
std::fs::write(&pack_path, &pack_data).unwrap();
std::fs::write(&index_path, &index_data).unwrap();
match PackReader::open(&pack_path, &index_path) {
Err(crate::store::StoreError::InvalidObject(msg)) => {
assert!(
msg.contains("checksum"),
"expected checksum error, got: {msg}"
);
}
Err(e) => panic!("expected InvalidObject, got: {e:?}"),
Ok(_) => panic!("expected error for corrupt checksum"),
}
}
#[test]
fn test_pack_index_rejects_bad_magic() {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"BAAD"); bytes.extend_from_slice(&super::pack_index::INDEX_VERSION.to_be_bytes());
bytes.extend_from_slice(&0u64.to_be_bytes());
let err = PackIndex::from_bytes(&bytes).unwrap_err();
assert!(
matches!(err, crate::store::StoreError::InvalidObject(ref msg) if msg.contains("magic")),
"expected magic error, got: {err:?}"
);
}
#[test]
fn test_pack_index_rejects_bad_version() {
let mut bytes = Vec::new();
bytes.extend_from_slice(super::pack_index::INDEX_MAGIC);
bytes.extend_from_slice(&999u32.to_be_bytes()); bytes.extend_from_slice(&0u64.to_be_bytes());
let err = PackIndex::from_bytes(&bytes).unwrap_err();
assert!(
matches!(err, crate::store::StoreError::InvalidObject(ref msg) if msg.contains("version")),
"expected version error, got: {err:?}"
);
}
#[test]
fn test_pack_reader_missing_object_returns_none() {
let temp_dir = TempDir::new().unwrap();
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
builder.add(create_test_hash(1), ObjectType::Blob, b"data".to_vec());
let (pack_data, index_data, _) = builder.build().unwrap();
std::fs::write(&pack_path, &pack_data).unwrap();
std::fs::write(&index_path, &index_data).unwrap();
let reader = PackReader::open(&pack_path, &index_path).unwrap();
let result = reader.get_hashed_object(&create_test_hash(99)).unwrap();
assert!(result.is_none(), "non-existent hash should return None");
}
#[test]
fn stale_index_swapped_offsets_surfaces_as_invalid_object() {
use crate::store::{StoreError, pack::pack_index::PackIndex};
let blob_a = b"alpha-payload alpha-payload alpha-payload alpha".to_vec();
let blob_b = b"bravo-payload bravo-payload bravo-payload bravo".to_vec();
let hash_a = ContentHash::compute(&blob_a);
let hash_b = ContentHash::compute(&blob_b);
let temp_dir = TempDir::new().unwrap();
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
let mut builder = PackBuilder::new(CompressionConfig::default());
builder.add_with_path(hash_a, ObjectType::Blob, blob_a.clone(), None);
builder.add_with_path(hash_b, ObjectType::Blob, blob_b.clone(), None);
let (pack_data, index_data, _) = builder.build().unwrap();
std::fs::write(&pack_path, &pack_data).unwrap();
std::fs::write(&index_path, &index_data).unwrap();
{
let reader = PackReader::open(&pack_path, &index_path).unwrap();
let (_, got_a) = reader.get_hashed_object(&hash_a).unwrap().expect("A");
assert_eq!(got_a, blob_a);
let (_, got_b) = reader.get_hashed_object(&hash_b).unwrap().expect("B");
assert_eq!(got_b, blob_b);
}
let original_index = PackIndex::from_bytes(&index_data).unwrap();
let offset_a = original_index
.find(&PackObjectId::Hash(hash_a))
.expect("index has A");
let offset_b = original_index
.find(&PackObjectId::Hash(hash_b))
.expect("index has B");
assert_ne!(offset_a, offset_b);
let mut stale = PackIndex::new();
stale.add(PackObjectId::Hash(hash_a), offset_b); stale.add(PackObjectId::Hash(hash_b), offset_a); stale.sort();
std::fs::write(&index_path, stale.to_bytes()).unwrap();
let reader = PackReader::open(&pack_path, &index_path).unwrap();
let err = reader
.get_hashed_object(&hash_a)
.expect_err("stale index must surface as an error, not silent wrong bytes");
assert!(
matches!(&err, StoreError::InvalidObject(msg) if msg.contains("stale or corrupt")),
"expected InvalidObject('… stale or corrupt …'), got: {err:?}",
);
let err_bytes = reader
.get_hashed_object_bytes(&hash_a)
.expect_err("zero-copy path must reject too");
assert!(matches!(err_bytes, StoreError::InvalidObject(_)));
}
fn build_and_open_pack(
objects: Vec<(ContentHash, ObjectType, Vec<u8>, Option<String>)>,
) -> PackReader<'static> {
let temp_dir = TempDir::new().unwrap();
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
for (hash, obj_type, data, path) in objects {
builder.add_with_path(hash, obj_type, data, path);
}
let (pack_data, index_data, _) = builder.build().unwrap();
std::fs::write(&pack_path, &pack_data).unwrap();
std::fs::write(&index_path, &index_data).unwrap();
std::mem::forget(temp_dir);
PackReader::open(&pack_path, &index_path).unwrap()
}
#[test]
fn test_delta_chain_roundtrip() {
let shared = b"This is shared content that remains constant across all versions. ".repeat(10);
let mut objects = Vec::new();
for i in 0..5u8 {
let mut data = shared.clone();
data.extend_from_slice(format!("version {i} unique suffix data here").as_bytes());
let hash = ContentHash::compute(&data);
objects.push((
hash,
ObjectType::Blob,
data,
Some("test/file.txt".to_string()),
));
}
let hashes: Vec<ContentHash> = objects.iter().map(|(h, _, _, _)| *h).collect();
let originals: Vec<Vec<u8>> = objects.iter().map(|(_, _, d, _)| d.clone()).collect();
let reader = build_and_open_pack(objects);
for (i, (hash, expected)) in hashes.iter().zip(originals.iter()).enumerate() {
let (obj_type, data) = reader
.get_hashed_object(hash)
.unwrap_or_else(|e| panic!("Failed to get object {i}: {e}"))
.unwrap_or_else(|| panic!("Object {i} not found"));
assert_eq!(obj_type, ObjectType::Blob, "object {i} type mismatch");
assert_eq!(&data, expected, "object {i} data mismatch");
}
}
#[test]
fn test_delta_chain_produces_deltas() {
let shared = b"Shared base content for delta testing. ".repeat(20);
let mut objects = Vec::new();
for i in 0..4u8 {
let mut data = shared.clone();
data.extend_from_slice(&[i; 32]);
let hash = ContentHash::compute(&data);
objects.push((
hash,
ObjectType::Blob,
data,
Some("src/main.rs".to_string()),
));
}
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
for (hash, obj_type, data, path) in objects {
builder.add_with_path(hash, obj_type, data, path);
}
let (_, _, stats) = builder.build().unwrap();
assert!(
stats.delta_count >= 1,
"expected deltas, got {}",
stats.delta_count
);
}
#[test]
fn test_delta_window_pack_bytes_are_stable() {
let shared = b"Stable pack bytes fixture. ".repeat(16);
let mut builder = PackBuilder::new(CompressionConfig {
enabled: false,
level: 0,
min_size: usize::MAX,
max_delta_size: 10_000_000,
});
for i in 0..4u8 {
let mut data = shared.clone();
data.extend_from_slice(format!("version {i} suffix").as_bytes());
builder.add_with_path(
ContentHash::compute(&data),
ObjectType::Blob,
data,
Some("src/stable.rs".to_string()),
);
}
let (pack_data, index_data, stats) = builder.build().unwrap();
assert!(stats.delta_count > 0);
assert_eq!(
blake3::hash(&pack_data).to_string(),
"4dc15365d17fbdf22ee4c1a8a44ee3414dda6dbdcb93c583536a4629c575e8ca"
);
assert_eq!(
blake3::hash(&index_data).to_string(),
"60f164bdc2dbb696c68f1c07a0b84e18f678fe915d7d446e43b1966f365b74f9"
);
}
#[test]
fn test_single_object_no_delta() {
let data = b"solo object content".repeat(50);
let hash = ContentHash::compute(&data);
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
builder.add(hash, ObjectType::Blob, data.clone());
let (_, _, stats) = builder.build().unwrap();
assert_eq!(stats.delta_count, 0);
assert_eq!(stats.object_count, 1);
}
#[test]
fn test_small_objects_skip_delta() {
let data1 = b"short object A".to_vec();
let data2 = b"short object B".to_vec();
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
builder.add_with_path(
ContentHash::compute(&data1),
ObjectType::Blob,
data1,
Some("tiny.txt".to_string()),
);
builder.add_with_path(
ContentHash::compute(&data2),
ObjectType::Blob,
data2,
Some("tiny.txt".to_string()),
);
let (_, _, stats) = builder.build().unwrap();
assert_eq!(
stats.delta_count, 0,
"small objects should not be delta-encoded"
);
}
#[test]
fn test_chain_resets_on_bad_delta() {
let data1: Vec<u8> = (0..1024).map(|i| ((i * 131 + 17) % 256) as u8).collect();
let data2: Vec<u8> = (0..1024).map(|i| ((i * 197 + 53) % 256) as u8).collect();
let _data3: Vec<u8> = (0..1024).map(|i| ((i * 251 + 89) % 256) as u8).collect();
let temp_dir = TempDir::new().unwrap();
let pack_path = temp_dir.path().join("test.pack");
let index_path = temp_dir.path().join("test.idx");
let mut builder2 = PackBuilder::new(CompressionConfig::default());
builder2.add_with_path(
ContentHash::compute(&data1),
ObjectType::Blob,
data1.clone(),
Some("file.bin".to_string()),
);
builder2.add_with_path(
ContentHash::compute(&data2),
ObjectType::Blob,
data2.clone(),
Some("file.bin".to_string()),
);
let (pd, id, _) = builder2.build().unwrap();
std::fs::write(&pack_path, &pd).unwrap();
std::fs::write(&index_path, &id).unwrap();
let reader = PackReader::open(&pack_path, &index_path).unwrap();
let (_, got) = reader
.get_hashed_object(&ContentHash::compute(&data1))
.unwrap()
.unwrap();
assert_eq!(got, data1);
let (_, got) = reader
.get_hashed_object(&ContentHash::compute(&data2))
.unwrap()
.unwrap();
assert_eq!(got, data2);
}
#[test]
fn test_different_paths_with_different_content_roundtrip() {
let base_a = vec![0xAA; 1024]; let base_b = vec![0xBB; 1024];
let hash_a = ContentHash::compute(&base_a);
let hash_b = ContentHash::compute(&base_b);
let reader = build_and_open_pack(vec![
(
hash_a,
ObjectType::Blob,
base_a.clone(),
Some("a.bin".to_string()),
),
(
hash_b,
ObjectType::Blob,
base_b.clone(),
Some("b.bin".to_string()),
),
]);
let (_, got_a) = reader.get_hashed_object(&hash_a).unwrap().unwrap();
assert_eq!(got_a, base_a);
let (_, got_b) = reader.get_hashed_object(&hash_b).unwrap().unwrap();
assert_eq!(got_b, base_b);
}
#[test]
fn test_objects_without_path_use_size_bucketing() {
let shared = b"Shared prefix for size bucketing test with enough content. ".repeat(10);
let mut objects = Vec::new();
for i in 0..3u8 {
let mut data = shared.clone();
data.extend_from_slice(&[i; 16]);
objects.push((ContentHash::compute(&data), ObjectType::Blob, data, None));
}
let originals: Vec<(ContentHash, Vec<u8>)> =
objects.iter().map(|(h, _, d, _)| (*h, d.clone())).collect();
let reader = build_and_open_pack(objects);
for (hash, expected) in &originals {
let (_, data) = reader.get_hashed_object(hash).unwrap().unwrap();
assert_eq!(&data, expected);
}
}
#[test]
fn test_tree_objects_can_be_delta_encoded() {
let shared = b"tree serialization data that is shared ".repeat(15);
let mut objects = Vec::new();
for i in 0..3u8 {
let mut data = shared.clone();
data.extend_from_slice(format!("tree version {i}").as_bytes());
objects.push((
ContentHash::compute(&data),
ObjectType::Tree,
data,
Some("src/".to_string()),
));
}
let originals: Vec<(ContentHash, Vec<u8>)> =
objects.iter().map(|(h, _, d, _)| (*h, d.clone())).collect();
let reader = build_and_open_pack(objects);
for (hash, expected) in &originals {
let (obj_type, data) = reader.get_hashed_object(hash).unwrap().unwrap();
assert_eq!(obj_type, ObjectType::Tree);
assert_eq!(&data, expected);
}
}
#[test]
fn test_state_objects_not_delta_encoded() {
let data1 = b"state data 1".repeat(50);
let data2 = b"state data 2".repeat(50);
let compression = CompressionConfig::default();
let mut builder = PackBuilder::new(compression);
builder.add(ContentHash::compute(&data1), ObjectType::State, data1);
builder.add(ContentHash::compute(&data2), ObjectType::State, data2);
let (_, _, stats) = builder.build().unwrap();
assert_eq!(stats.delta_count, 0, "states should never be delta-encoded");
}
#[test]
fn test_empty_bucket_is_noop() {
let compression = CompressionConfig::default();
let builder = PackBuilder::new(compression);
let (pack_data, _, stats) = builder.build().unwrap();
assert_eq!(stats.object_count, 0);
assert_eq!(stats.delta_count, 0);
assert!(pack_data.len() >= 16 + 32); }