1use crate::delta;
64use crate::hash::{self, Hash};
65use crate::object::{MkitError, Object};
66use crate::store::{MAX_RAW_OBJECT_SIZE, ObjectStore};
67use std::borrow::Cow;
68use std::sync::atomic::{AtomicU64, Ordering};
69
70pub const MAGIC: &[u8; 4] = b"MKIT";
72pub const VERSION: u32 = 1;
76pub const VERSION_V2: u32 = 2;
81
82pub const MAX_ENTRIES: u32 = 10_000_000;
84pub const MAX_TOTAL_PAYLOAD: u64 = 4 * 1024 * 1024 * 1024;
86pub const TRAILER_LEN: usize = 32;
88
89pub const HEADER_LEN: usize = 4 + 4 + 4;
91pub const ENTRY_FRAME_LEN: usize = 1 + 4;
93pub const VERSION_OFFSET: usize = 4;
98pub const ENTRY_COUNT_OFFSET: usize = 8;
104
105#[cfg(feature = "pack-zstd")]
110const MIN_COMPRESS_LEN: usize = 64;
111#[cfg(feature = "pack-zstd")]
115const ZSTD_LEVEL: i32 = 3;
116const ZSTD_LEN_PREFIX: usize = 4;
120
121#[derive(Debug, thiserror::Error)]
125pub enum PackError {
126 #[error("packfile is shorter than the {HEADER_LEN}-byte header + {TRAILER_LEN}-byte trailer")]
127 PackfileTooShort,
128 #[error("first 4 bytes are not ASCII \"MKIT\"")]
129 InvalidMagic,
130 #[error("version {0} is not supported (v1 or v2 only)")]
131 UnsupportedVersion(u32),
132 #[error(
133 "entry_type {0:#04x} is not 0x00 (raw), 0x02 (delta), 0x03 (zstd-raw), or 0x04 \
134 (zstd-delta) — or is a v2-only entry type inside a version-1 pack"
135 )]
136 InvalidEntryType(u8),
137 #[error("entry_count {0} exceeds the {MAX_ENTRIES} cap")]
138 TooManyObjects(u32),
139 #[error("sum of payload_len exceeds {MAX_TOTAL_PAYLOAD} bytes")]
140 PackfileTooLarge,
141 #[error("entry payload extends past the trailer offset")]
142 UnexpectedEof,
143 #[error("trailer BLAKE3 mismatch — packfile is corrupt or truncated")]
144 PackfileCorrupted,
145 #[error("delta entry references base hash {0} which is not in this pack or the store")]
146 DeltaBaseMissing(String),
147 #[error("delta entry payload is shorter than the 32-byte base hash prefix")]
148 DeltaEntryTruncated,
149 #[error("delta reconstruction failed: {0}")]
150 DeltaApply(#[from] MkitError),
151 #[error("pack entry is not a canonical storable object: {0}")]
152 InvalidObject(MkitError),
153 #[error("pack entry resolves to pack-only delta object")]
154 NonStorableObject,
155 #[error("pack contains trailing bytes after declared entries")]
156 TrailingData,
157 #[error("store I/O failure: {0}")]
158 Store(#[from] crate::store::StoreError),
159 #[error("zstd entry payload is shorter than its length-prefix header")]
164 ZstdEntryTruncated,
165 #[error(
169 "zstd entry's claimed decompressed size {0} exceeds the {MAX_RAW_OBJECT_SIZE}-byte cap"
170 )]
171 DecompressedSizeOverCap(usize),
172 #[error("zstd entry claims {0} decompressed bytes but produced {1}")]
176 DecompressedSizeMismatch(usize, usize),
177 #[error("zstd decompression failed: {0}")]
179 ZstdDecompress(String),
180}
181
182#[derive(Debug, Clone, Default, PartialEq, Eq)]
185pub struct UnpackReport {
186 pub raw_count: u32,
187 pub delta_count: u32,
188 pub stored: Vec<Hash>,
190}
191
192#[derive(Debug)]
199pub struct PackWriter {
200 buf: Vec<u8>,
207 entry_count: u32,
208 total_payload: u64,
209 has_compressed_entry: bool,
214}
215
216impl Default for PackWriter {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222impl PackWriter {
223 #[must_use]
225 pub fn new() -> Self {
226 let mut buf = Vec::with_capacity(HEADER_LEN);
227 buf.extend_from_slice(MAGIC);
228 buf.extend_from_slice(&VERSION.to_le_bytes());
229 buf.extend_from_slice(&0u32.to_le_bytes()); Self {
231 buf,
232 entry_count: 0,
233 total_payload: 0,
234 has_compressed_entry: false,
235 }
236 }
237
238 pub fn push_raw(&mut self, hash_of_bytes: Hash, bytes: &[u8]) -> Result<Hash, PackError> {
254 if let Some(frame) = maybe_compress(bytes) {
255 let uncompressed_len: u32 = bytes
256 .len()
257 .try_into()
258 .map_err(|_| PackError::PackfileTooLarge)?;
259 let payload_len = ZSTD_LEN_PREFIX + frame.len();
260 self.check_caps_for(payload_len)?;
261 self.total_payload += payload_len as u64;
262 self.append_entry(0x03, &[&uncompressed_len.to_le_bytes(), &frame])?;
263 self.has_compressed_entry = true;
264 } else {
265 self.check_caps_for(bytes.len())?;
266 self.total_payload += bytes.len() as u64;
267 self.append_entry(0x00, &[bytes])?;
268 }
269 self.entry_count += 1;
270 Ok(hash_of_bytes)
271 }
272
273 pub fn push_delta(&mut self, base_hash: &Hash, delta_stream: &[u8]) -> Result<(), PackError> {
285 if let Some(frame) = maybe_compress(delta_stream) {
286 let uncompressed_len: u32 = delta_stream
287 .len()
288 .try_into()
289 .map_err(|_| PackError::PackfileTooLarge)?;
290 let payload_len = hash::HASH_LEN + ZSTD_LEN_PREFIX + frame.len();
291 self.check_caps_for(payload_len)?;
292 self.total_payload += payload_len as u64;
293 self.append_entry(
294 0x04,
295 &[
296 base_hash.as_slice(),
297 &uncompressed_len.to_le_bytes(),
298 &frame,
299 ],
300 )?;
301 self.has_compressed_entry = true;
302 } else {
303 let payload_len = hash::HASH_LEN + delta_stream.len();
304 self.check_caps_for(payload_len)?;
305 self.total_payload += payload_len as u64;
306 self.append_entry(0x02, &[base_hash.as_slice(), delta_stream])?;
307 }
308 self.entry_count += 1;
309 Ok(())
310 }
311
312 fn append_entry(&mut self, etype: u8, parts: &[&[u8]]) -> Result<(), PackError> {
318 let payload_len: usize = parts.iter().map(|p| p.len()).sum();
319 let plen: u32 = payload_len
320 .try_into()
321 .map_err(|_| PackError::PackfileTooLarge)?;
322 self.buf.push(etype);
323 self.buf.extend_from_slice(&plen.to_le_bytes());
324 for p in parts {
325 self.buf.extend_from_slice(p);
326 }
327 Ok(())
328 }
329
330 fn check_caps_for(&self, add_len: usize) -> Result<(), PackError> {
331 let next_count = u64::from(self.entry_count) + 1;
332 if next_count > u64::from(MAX_ENTRIES) {
333 return Err(PackError::TooManyObjects(MAX_ENTRIES + 1));
334 }
335 let next_total = self.total_payload.saturating_add(add_len as u64);
336 if next_total > MAX_TOTAL_PAYLOAD {
337 return Err(PackError::PackfileTooLarge);
338 }
339 Ok(())
340 }
341
342 #[must_use]
344 pub fn entry_count(&self) -> usize {
345 self.entry_count as usize
346 }
347
348 #[must_use]
359 pub fn total_payload(&self) -> u64 {
360 self.total_payload
361 }
362
363 pub fn finish(self) -> Result<Vec<u8>, PackError> {
371 self.finish_inner(None)
372 }
373
374 #[cfg(test)]
383 pub(crate) fn finish_tracking_bytes_copied(
384 self,
385 bytes_copied: &AtomicU64,
386 ) -> Result<Vec<u8>, PackError> {
387 self.finish_inner(Some(bytes_copied))
388 }
389
390 fn finish_inner(mut self, bytes_copied: Option<&AtomicU64>) -> Result<Vec<u8>, PackError> {
391 if self.entry_count > MAX_ENTRIES {
392 return Err(PackError::TooManyObjects(self.entry_count));
393 }
394 let version = if self.has_compressed_entry {
395 VERSION_V2
396 } else {
397 VERSION
398 };
399 self.buf[VERSION_OFFSET..VERSION_OFFSET + 4].copy_from_slice(&version.to_le_bytes());
400 self.buf[ENTRY_COUNT_OFFSET..ENTRY_COUNT_OFFSET + 4]
401 .copy_from_slice(&self.entry_count.to_le_bytes());
402 let trailer = hash::hash(&self.buf);
403 if let Some(c) = bytes_copied {
404 c.fetch_add(trailer.len() as u64, Ordering::Relaxed);
405 }
406 self.buf.extend_from_slice(&trailer);
407 Ok(self.buf)
408 }
409}
410
411#[must_use]
415pub fn pack_key(pack_bytes: &[u8]) -> Hash {
416 hash::hash(pack_bytes)
417}
418
419#[cfg(feature = "pack-zstd")]
430fn maybe_compress(data: &[u8]) -> Option<Vec<u8>> {
431 if data.len() < MIN_COMPRESS_LEN {
432 return None;
433 }
434 let compressed = zstd::bulk::compress(data, ZSTD_LEVEL).ok()?;
435 if ZSTD_LEN_PREFIX + compressed.len() < data.len() {
436 Some(compressed)
437 } else {
438 None
439 }
440}
441
442#[cfg(not(feature = "pack-zstd"))]
443fn maybe_compress(_data: &[u8]) -> Option<Vec<u8>> {
444 None
449}
450
451fn decompress_zstd_entry(payload: &[u8]) -> Result<Vec<u8>, PackError> {
458 if payload.len() < ZSTD_LEN_PREFIX {
459 return Err(PackError::ZstdEntryTruncated);
460 }
461 let uncompressed_len =
462 u32::from_le_bytes(payload[..ZSTD_LEN_PREFIX].try_into().expect("4 bytes")) as usize;
463 if uncompressed_len > MAX_RAW_OBJECT_SIZE {
464 return Err(PackError::DecompressedSizeOverCap(uncompressed_len));
465 }
466 let frame = &payload[ZSTD_LEN_PREFIX..];
467 let decompressed = zstd_decompress_capped(frame, uncompressed_len)?;
468 if decompressed.len() != uncompressed_len {
469 return Err(PackError::DecompressedSizeMismatch(
470 uncompressed_len,
471 decompressed.len(),
472 ));
473 }
474 Ok(decompressed)
475}
476
477#[cfg(feature = "pack-zstd")]
481fn zstd_decompress_capped(frame: &[u8], capacity: usize) -> Result<Vec<u8>, PackError> {
482 zstd::bulk::decompress(frame, capacity).map_err(|e| PackError::ZstdDecompress(e.to_string()))
483}
484
485#[cfg(not(feature = "pack-zstd"))]
486fn zstd_decompress_capped(_frame: &[u8], _capacity: usize) -> Result<Vec<u8>, PackError> {
487 Err(PackError::ZstdDecompress(
488 "this build was compiled without the `pack-zstd` feature".to_string(),
489 ))
490}
491
492pub fn delta_base_hashes(pack_bytes: &[u8]) -> Result<Vec<Hash>, PackError> {
515 if pack_bytes.len() < HEADER_LEN + TRAILER_LEN {
516 return Err(PackError::PackfileTooShort);
517 }
518 if &pack_bytes[..4] != MAGIC.as_slice() {
519 return Err(PackError::InvalidMagic);
520 }
521 let version = u32::from_le_bytes(pack_bytes[4..8].try_into().expect("4 bytes"));
522 if version != VERSION && version != VERSION_V2 {
523 return Err(PackError::UnsupportedVersion(version));
524 }
525 let count = u32::from_le_bytes(
526 pack_bytes[ENTRY_COUNT_OFFSET..ENTRY_COUNT_OFFSET + 4]
527 .try_into()
528 .expect("4 bytes"),
529 );
530 if count > MAX_ENTRIES {
531 return Err(PackError::TooManyObjects(count));
532 }
533 let split = pack_bytes.len() - TRAILER_LEN;
535
536 let mut bases = Vec::new();
537 let mut seen = std::collections::HashSet::new();
538 let mut pos = HEADER_LEN;
539 for _ in 0..count {
540 if pos + ENTRY_FRAME_LEN > split {
541 return Err(PackError::UnexpectedEof);
542 }
543 let etype = pack_bytes[pos];
544 pos += 1;
545 let payload_len =
546 u32::from_le_bytes(pack_bytes[pos..pos + 4].try_into().expect("4 bytes")) as usize;
547 pos += 4;
548 if pos + payload_len > split {
549 return Err(PackError::UnexpectedEof);
550 }
551 if etype == 0x02 || etype == 0x04 {
556 if payload_len < TRAILER_LEN {
557 return Err(PackError::DeltaEntryTruncated);
558 }
559 let mut base = [0u8; 32];
560 base.copy_from_slice(&pack_bytes[pos..pos + TRAILER_LEN]);
561 if seen.insert(base) {
562 bases.push(base);
563 }
564 }
565 pos += payload_len;
566 }
567 Ok(bases)
568}
569
570#[derive(Debug)]
574pub struct PackReader;
575
576impl PackReader {
577 pub fn read(pack_bytes: &[u8], store: &ObjectStore) -> Result<UnpackReport, PackError> {
594 Self::read_with_payload_cap(pack_bytes, store, MAX_TOTAL_PAYLOAD)
595 }
596
597 pub(crate) fn read_with_payload_cap(
604 pack_bytes: &[u8],
605 store: &ObjectStore,
606 payload_cap: u64,
607 ) -> Result<UnpackReport, PackError> {
608 Self::read_inner(pack_bytes, store, payload_cap, None)
609 }
610
611 #[cfg(test)]
619 pub(crate) fn read_tracking_owned_bytes(
620 pack_bytes: &[u8],
621 store: &ObjectStore,
622 owned_bytes: &AtomicU64,
623 ) -> Result<UnpackReport, PackError> {
624 Self::read_inner(pack_bytes, store, MAX_TOTAL_PAYLOAD, Some(owned_bytes))
625 }
626
627 fn read_inner(
628 pack_bytes: &[u8],
629 store: &ObjectStore,
630 payload_cap: u64,
631 owned_bytes: Option<&AtomicU64>,
632 ) -> Result<UnpackReport, PackError> {
633 let (version, split, count) = validate_pack_header(pack_bytes)?;
638
639 let mut report = UnpackReport::default();
640 let mut in_pack: std::collections::HashMap<Hash, Cow<'_, [u8]>> =
652 std::collections::HashMap::new();
653 let mut total_payload: u64 = 0;
654 let mut pos = HEADER_LEN;
655
656 let batch = store.batch();
666
667 for _ in 0..count {
668 if pos + ENTRY_FRAME_LEN > split {
670 return Err(PackError::UnexpectedEof);
671 }
672 let etype = pack_bytes[pos];
673 pos += 1;
674 let payload_len =
675 u32::from_le_bytes(pack_bytes[pos..pos + 4].try_into().expect("4 bytes")) as usize;
676 pos += 4;
677
678 total_payload = total_payload.saturating_add(payload_len as u64);
679 if total_payload > payload_cap {
680 return Err(PackError::PackfileTooLarge);
681 }
682 if pos + payload_len > split {
683 return Err(PackError::UnexpectedEof);
684 }
685 let payload = &pack_bytes[pos..pos + payload_len];
686 pos += payload_len;
687
688 match etype {
689 0x00 => {
690 stage_raw_object(
692 &batch,
693 &mut in_pack,
694 &mut report,
695 owned_bytes,
696 Cow::Borrowed(payload),
697 )?;
698 }
699 0x02 => {
700 if payload.len() < hash::HASH_LEN {
702 return Err(PackError::DeltaEntryTruncated);
703 }
704 let mut base_hash = [0u8; hash::HASH_LEN];
705 base_hash.copy_from_slice(&payload[..hash::HASH_LEN]);
706 let stream = &payload[hash::HASH_LEN..];
707 stage_delta_target(
708 store,
709 &batch,
710 &mut in_pack,
711 &mut report,
712 owned_bytes,
713 base_hash,
714 stream,
715 )?;
716 }
717 0x03 if version == VERSION_V2 => {
718 let obj_bytes = decompress_zstd_entry(payload)?;
722 stage_raw_object(
723 &batch,
724 &mut in_pack,
725 &mut report,
726 owned_bytes,
727 Cow::Owned(obj_bytes),
728 )?;
729 }
730 0x04 if version == VERSION_V2 => {
731 if payload.len() < hash::HASH_LEN {
734 return Err(PackError::DeltaEntryTruncated);
735 }
736 let mut base_hash = [0u8; hash::HASH_LEN];
737 base_hash.copy_from_slice(&payload[..hash::HASH_LEN]);
738 let stream = decompress_zstd_entry(&payload[hash::HASH_LEN..])?;
739 stage_delta_target(
740 store,
741 &batch,
742 &mut in_pack,
743 &mut report,
744 owned_bytes,
745 base_hash,
746 &stream,
747 )?;
748 }
749 0x01 => return Err(PackError::InvalidEntryType(0x01)),
750 other => return Err(PackError::InvalidEntryType(other)),
751 }
752 }
753
754 if pos != split {
755 return Err(PackError::TrailingData);
756 }
757
758 batch.commit()?;
762
763 Ok(report)
764 }
765}
766
767fn validate_pack_header(pack_bytes: &[u8]) -> Result<(u32, usize, u32), PackError> {
776 if pack_bytes.len() < HEADER_LEN + TRAILER_LEN {
778 return Err(PackError::PackfileTooShort);
779 }
780 if &pack_bytes[..4] != MAGIC.as_slice() {
782 return Err(PackError::InvalidMagic);
783 }
784 let version = u32::from_le_bytes(pack_bytes[4..8].try_into().expect("4 bytes"));
787 if version != VERSION && version != VERSION_V2 {
788 return Err(PackError::UnsupportedVersion(version));
789 }
790 let split = pack_bytes.len() - TRAILER_LEN;
800 let body = &pack_bytes[..split];
801 let trailer = &pack_bytes[split..];
802 let computed = hash::hash(body);
803 if computed.as_slice() != trailer {
804 return Err(PackError::PackfileCorrupted);
805 }
806 let count = u32::from_le_bytes(
808 pack_bytes[ENTRY_COUNT_OFFSET..ENTRY_COUNT_OFFSET + 4]
809 .try_into()
810 .expect("4 bytes"),
811 );
812 if count > MAX_ENTRIES {
813 return Err(PackError::TooManyObjects(count));
814 }
815 let body_after_header = body.len() - HEADER_LEN;
817 if u64::from(count) * ENTRY_FRAME_LEN as u64 > body_after_header as u64 {
818 return Err(PackError::TooManyObjects(count));
819 }
820 Ok((version, split, count))
821}
822
823fn stage_raw_object<'p>(
831 batch: &crate::batch::WriteBatch<'_>,
832 in_pack: &mut std::collections::HashMap<Hash, Cow<'p, [u8]>>,
833 report: &mut UnpackReport,
834 owned_bytes: Option<&AtomicU64>,
835 payload: Cow<'p, [u8]>,
836) -> Result<(), PackError> {
837 let obj = validate_storable_object(&payload)?;
838 let stored_hash = crate::object::id_from_object(&obj, &payload);
843 batch.write_prehashed(stored_hash, &[payload.as_ref()])?;
844 if let (Cow::Owned(_), Some(c)) = (&payload, owned_bytes) {
845 c.fetch_add(payload.len() as u64, Ordering::Relaxed);
846 }
847 in_pack.insert(stored_hash, payload);
848 report.raw_count += 1;
849 report.stored.push(stored_hash);
850 Ok(())
851}
852
853#[allow(clippy::too_many_arguments)]
860fn stage_delta_target(
861 store: &ObjectStore,
862 batch: &crate::batch::WriteBatch<'_>,
863 in_pack: &mut std::collections::HashMap<Hash, Cow<'_, [u8]>>,
864 report: &mut UnpackReport,
865 owned_bytes: Option<&AtomicU64>,
866 base_hash: Hash,
867 stream: &[u8],
868) -> Result<(), PackError> {
869 let resolved = resolve_delta_target(store, in_pack, base_hash, stream)?;
870 let obj = validate_storable_object(&resolved)?;
871 let stored_hash = crate::object::id_from_object(&obj, &resolved);
872 batch.write_prehashed(stored_hash, &[&resolved])?;
873 if let Some(c) = owned_bytes {
874 c.fetch_add(resolved.len() as u64, Ordering::Relaxed);
875 }
876 in_pack.insert(stored_hash, Cow::Owned(resolved));
877 report.delta_count += 1;
878 report.stored.push(stored_hash);
879 Ok(())
880}
881
882fn resolve_delta_target(
888 store: &ObjectStore,
889 in_pack: &mut std::collections::HashMap<Hash, Cow<'_, [u8]>>,
890 base_hash: Hash,
891 stream: &[u8],
892) -> Result<Vec<u8>, PackError> {
893 let base_bytes: Cow<'_, [u8]> = if let Some(b) = in_pack.get(&base_hash) {
905 Cow::Borrowed(b.as_ref())
906 } else if store.contains(&base_hash) {
907 let bytes = store.read(&base_hash)?;
908 validate_storable_object(&bytes)?;
909 in_pack.insert(base_hash, Cow::Owned(bytes.clone()));
910 Cow::Owned(bytes)
911 } else {
912 return Err(PackError::DeltaBaseMissing(hash::to_hex(&base_hash)));
913 };
914 validate_delta_result_size(stream)?;
915 let resolved = delta::decode(base_bytes.as_ref(), stream)?;
916 Ok(resolved)
917}
918
919fn validate_storable_object(bytes: &[u8]) -> Result<Object, PackError> {
922 if bytes.len() > MAX_RAW_OBJECT_SIZE {
923 return Err(PackError::Store(crate::store::StoreError::ObjectTooLarge));
924 }
925 match crate::serialize::deserialize(bytes).map_err(PackError::InvalidObject)? {
926 Object::Delta(_) => Err(PackError::NonStorableObject),
927 obj @ (Object::Blob(_)
928 | Object::Tree(_)
929 | Object::Commit(_)
930 | Object::Remix(_)
931 | Object::ChunkedBlob(_)
932 | Object::Tag(_)) => Ok(obj),
933 }
934}
935
936fn validate_delta_result_size(stream: &[u8]) -> Result<(), PackError> {
937 if stream.len() < delta::HEADER_LEN {
938 return Err(PackError::DeltaApply(MkitError::UnexpectedEof));
939 }
940 let result_len = u32::from_le_bytes(stream[5..9].try_into().expect("4 bytes")) as usize;
941 if result_len > MAX_RAW_OBJECT_SIZE {
942 return Err(PackError::Store(crate::store::StoreError::ObjectTooLarge));
943 }
944 Ok(())
945}
946
947#[cfg(test)]
952mod tests {
953 use super::*;
954 use tempfile::TempDir;
955
956 fn fresh_store() -> (TempDir, ObjectStore) {
957 let dir = TempDir::new().unwrap();
958 let store = ObjectStore::init(&crate::layout::RepoLayout::single(dir.path())).unwrap();
959 (dir, store)
960 }
961
962 fn write_blob_via_serialize(payload: &[u8]) -> Vec<u8> {
963 let blob = crate::object::Object::Blob(crate::object::Blob {
967 data: payload.to_vec(),
968 });
969 crate::serialize::serialize(&blob).expect("serialize blob")
970 }
971
972 fn finish_pack_body(mut body: Vec<u8>) -> Vec<u8> {
973 let trailer = hash::hash(&body);
974 body.extend_from_slice(&trailer);
975 body
976 }
977
978 fn incompressible_bytes(seed: u64, len: usize) -> Vec<u8> {
991 let mut buf = vec![0u8; len];
992 let mut state = seed | 1; for chunk in buf.chunks_mut(8) {
994 state = state
995 .wrapping_mul(6_364_136_223_846_793_005)
996 .wrapping_add(1_442_695_040_888_963_407);
997 let bytes = state.to_le_bytes();
998 chunk.copy_from_slice(&bytes[..chunk.len()]);
999 }
1000 buf
1001 }
1002
1003 #[test]
1004 fn empty_pack_is_44_bytes() {
1005 let pack = PackWriter::new().finish().unwrap();
1006 assert_eq!(pack.len(), HEADER_LEN + TRAILER_LEN);
1007 assert_eq!(&pack[..4], MAGIC);
1008 assert_eq!(u32::from_le_bytes(pack[4..8].try_into().unwrap()), VERSION);
1009 assert_eq!(
1010 u32::from_le_bytes(
1011 pack[ENTRY_COUNT_OFFSET..ENTRY_COUNT_OFFSET + 4]
1012 .try_into()
1013 .unwrap()
1014 ),
1015 0
1016 );
1017
1018 let (_dir, store) = fresh_store();
1019 let report = PackReader::read(&pack, &store).unwrap();
1020 assert_eq!(report.raw_count, 0);
1021 assert_eq!(report.delta_count, 0);
1022 assert!(report.stored.is_empty());
1023 }
1024
1025 #[test]
1026 fn unpack_writes_objects_via_single_batch_flush() {
1027 use crate::batch::testing::{Ev, RecordingSyncer};
1030 use std::sync::Arc;
1031
1032 let mut w = PackWriter::new();
1033 let mut blobs = Vec::new();
1034 for i in 0u32..30 {
1035 let blob = write_blob_via_serialize(format!("pack object {i}").as_bytes());
1036 w.push_raw(hash::hash(&blob), &blob).unwrap();
1037 blobs.push(blob);
1038 }
1039 let pack = w.finish().unwrap();
1040
1041 let (_dir, mut store) = fresh_store();
1042 let rec = Arc::new(RecordingSyncer::default());
1043 store.set_syncer(rec.clone());
1044
1045 let report = PackReader::read(&pack, &store).unwrap();
1046 assert_eq!(report.raw_count, 30);
1047
1048 let fulls = rec
1049 .events()
1050 .iter()
1051 .filter(|e| matches!(e, Ev::Full(_)))
1052 .count();
1053 assert_eq!(
1054 fulls, 2,
1055 "unpack flush cost must be constant, not O(objects)"
1056 );
1057 for blob in &blobs {
1058 assert_eq!(store.read(&hash::hash(blob)).unwrap(), *blob);
1059 }
1060 }
1061
1062 #[test]
1063 fn single_raw_roundtrip() {
1064 let blob = write_blob_via_serialize(b"hello packfile");
1065 let h = hash::hash(&blob);
1066
1067 let mut w = PackWriter::new();
1068 w.push_raw(h, &blob).unwrap();
1069 let pack = w.finish().unwrap();
1070
1071 let (_dir, store) = fresh_store();
1072 let report = PackReader::read(&pack, &store).unwrap();
1073 assert_eq!(report.raw_count, 1);
1074 assert_eq!(report.delta_count, 0);
1075 assert_eq!(report.stored, vec![h]);
1076 assert_eq!(store.read(&h).unwrap(), blob);
1077 }
1078
1079 #[test]
1080 fn total_payload_tracks_wire_sum_for_mixed_raw_and_delta() {
1081 let mut w = PackWriter::new();
1088 assert_eq!(w.total_payload(), 0);
1089
1090 let raw = write_blob_via_serialize(&incompressible_bytes(0xA11C_E000, 2048));
1093 let raw_hash = hash::hash(&raw);
1094 w.push_raw(raw_hash, &raw).unwrap();
1095 assert_eq!(w.total_payload(), raw.len() as u64);
1096
1097 let base = write_blob_via_serialize(&incompressible_bytes(0xB0BA_1000, 2048));
1098 let base_hash = hash::hash(&base);
1099 let target = write_blob_via_serialize(&incompressible_bytes(0xC0FF_EE00, 2048));
1100 let stream = delta::encode(&base, &target).unwrap();
1101 let before_delta = w.total_payload();
1102 w.push_delta(&base_hash, &stream).unwrap();
1103 let delta_wire_len = w.total_payload() - before_delta;
1104
1105 assert!(delta_wire_len <= (hash::HASH_LEN + stream.len()) as u64);
1110 assert_eq!(w.total_payload(), before_delta + delta_wire_len);
1111 assert!(w.total_payload() <= raw.len() as u64 + (hash::HASH_LEN + stream.len()) as u64);
1112 }
1113
1114 #[test]
1115 fn raw_then_delta_resolves_in_pack() {
1116 let mut content_base = vec![0u8; 1024];
1118 for (i, b) in content_base.iter_mut().enumerate() {
1119 *b = u8::try_from(i % 251).expect("modulo < 256");
1120 }
1121 let mut content_target = content_base.clone();
1122 content_target[500] = 0xFF;
1123 content_target[501] = 0xFE;
1124
1125 let base_obj = write_blob_via_serialize(&content_base);
1126 let target_obj = write_blob_via_serialize(&content_target);
1127 let base_hash = hash::hash(&base_obj);
1128 let target_hash = hash::hash(&target_obj);
1129
1130 let stream = delta::encode(&base_obj, &target_obj).unwrap();
1131
1132 let mut w = PackWriter::new();
1133 w.push_raw(base_hash, &base_obj).unwrap();
1134 w.push_delta(&base_hash, &stream).unwrap();
1135 let pack = w.finish().unwrap();
1136
1137 let (_dir, store) = fresh_store();
1138 let report = PackReader::read(&pack, &store).unwrap();
1139 assert_eq!(report.raw_count, 1);
1140 assert_eq!(report.delta_count, 1);
1141 assert_eq!(report.stored, vec![base_hash, target_hash]);
1142 assert_eq!(store.read(&target_hash).unwrap(), target_obj);
1143 }
1144
1145 #[test]
1146 fn delta_base_hashes_lists_delta_bases_only() {
1147 let base_a = write_blob_via_serialize(b"base alpha content here padding");
1150 let base_b = write_blob_via_serialize(b"base bravo content here padding");
1151 let ha = hash::hash(&base_a);
1152 let hb = hash::hash(&base_b);
1153 let target_a = write_blob_via_serialize(b"base alpha content here PADDED!");
1154 let target_b = write_blob_via_serialize(b"base bravo content here PADDED!");
1155 let stream_a = delta::encode(&base_a, &target_a).unwrap();
1156 let stream_b = delta::encode(&base_b, &target_b).unwrap();
1157
1158 let mut w = PackWriter::new();
1159 w.push_raw(ha, &base_a).unwrap(); w.push_delta(&ha, &stream_a).unwrap();
1161 w.push_delta(&hb, &stream_b).unwrap();
1162 w.push_delta(&ha, &stream_a).unwrap(); let pack = w.finish().unwrap();
1164
1165 let mut bases = delta_base_hashes(&pack).unwrap();
1166 bases.sort_unstable();
1167 let mut expected = vec![ha, hb];
1168 expected.sort_unstable();
1169 assert_eq!(bases, expected);
1170 }
1171
1172 #[test]
1173 fn delta_base_hashes_rejects_bad_magic() {
1174 let mut pack = PackWriter::new().finish().unwrap();
1175 pack[0] = b'X';
1176 assert!(matches!(
1177 delta_base_hashes(&pack),
1178 Err(PackError::InvalidMagic)
1179 ));
1180 }
1181
1182 #[test]
1183 fn rejects_raw_payload_that_is_not_canonical_object_without_store_write() {
1184 let payload = b"not a serialized mkit object".to_vec();
1185 let payload_hash = hash::hash(&payload);
1186 let mut body = Vec::new();
1187 body.extend_from_slice(MAGIC);
1188 body.extend_from_slice(&VERSION.to_le_bytes());
1189 body.extend_from_slice(&1u32.to_le_bytes());
1190 body.push(0x00);
1191 let payload_len = u32::try_from(payload.len()).unwrap();
1192 body.extend_from_slice(&payload_len.to_le_bytes());
1193 body.extend_from_slice(&payload);
1194 let pack = finish_pack_body(body);
1195
1196 let (_dir, store) = fresh_store();
1197 let err = PackReader::read(&pack, &store).unwrap_err();
1198 assert!(matches!(err, PackError::InvalidObject(_)), "got {err:?}");
1199 assert!(!store.contains(&payload_hash));
1200 }
1201
1202 #[test]
1203 fn rejects_raw_delta_object_without_store_write() {
1204 let delta = crate::object::Object::Delta(crate::object::Delta {
1205 base_hash: [0xAB; 32],
1206 result_size: 0,
1207 instructions: Vec::new(),
1208 });
1209 let payload = crate::serialize::serialize(&delta).unwrap();
1210 let payload_hash = hash::hash(&payload);
1211 let mut w = PackWriter::new();
1212 w.push_raw(payload_hash, &payload).unwrap();
1213 let pack = w.finish().unwrap();
1214
1215 let (_dir, store) = fresh_store();
1216 let err = PackReader::read(&pack, &store).unwrap_err();
1217 assert!(matches!(err, PackError::NonStorableObject), "got {err:?}");
1218 assert!(!store.contains(&payload_hash));
1219 }
1220
1221 #[test]
1222 fn rejects_delta_resolving_to_non_object_without_partial_store_write() {
1223 let base_obj = write_blob_via_serialize(b"base bytes");
1224 let base_hash = hash::hash(&base_obj);
1225 let invalid_target = b"not a serialized object".to_vec();
1226 let invalid_hash = hash::hash(&invalid_target);
1227 let stream = delta::encode(&base_obj, &invalid_target).unwrap();
1228
1229 let mut w = PackWriter::new();
1230 w.push_raw(base_hash, &base_obj).unwrap();
1231 w.push_delta(&base_hash, &stream).unwrap();
1232 let pack = w.finish().unwrap();
1233
1234 let (_dir, store) = fresh_store();
1235 let err = PackReader::read(&pack, &store).unwrap_err();
1236 assert!(matches!(err, PackError::InvalidObject(_)), "got {err:?}");
1237 assert!(!store.contains(&base_hash));
1238 assert!(!store.contains(&invalid_hash));
1239 }
1240
1241 #[test]
1242 fn rejects_delta_result_over_object_cap_without_partial_store_write() {
1243 let base_obj = write_blob_via_serialize(b"base bytes");
1244 let base_hash = hash::hash(&base_obj);
1245 let mut stream = Vec::new();
1246 stream.push(delta::STREAM_VERSION);
1247 stream.extend_from_slice(&u32::try_from(base_obj.len()).unwrap().to_le_bytes());
1248 stream.extend_from_slice(
1249 &u32::try_from(MAX_RAW_OBJECT_SIZE + 1)
1250 .unwrap()
1251 .to_le_bytes(),
1252 );
1253
1254 let mut w = PackWriter::new();
1255 w.push_raw(base_hash, &base_obj).unwrap();
1256 w.push_delta(&base_hash, &stream).unwrap();
1257 let pack = w.finish().unwrap();
1258
1259 let (_dir, store) = fresh_store();
1260 let err = PackReader::read(&pack, &store).unwrap_err();
1261 assert!(
1262 matches!(
1263 err,
1264 PackError::Store(crate::store::StoreError::ObjectTooLarge)
1265 ),
1266 "got {err:?}"
1267 );
1268 assert!(!store.contains(&base_hash));
1269 }
1270
1271 #[test]
1272 fn rejects_trailing_bytes_after_declared_entries_without_store_write() {
1273 let blob = write_blob_via_serialize(b"trailing bytes test");
1274 let blob_hash = hash::hash(&blob);
1275 let mut body = Vec::new();
1276 body.extend_from_slice(MAGIC);
1277 body.extend_from_slice(&VERSION.to_le_bytes());
1278 body.extend_from_slice(&1u32.to_le_bytes());
1279 body.push(0x00);
1280 let blob_len = u32::try_from(blob.len()).unwrap();
1281 body.extend_from_slice(&blob_len.to_le_bytes());
1282 body.extend_from_slice(&blob);
1283 body.extend_from_slice(b"junk");
1284 let pack = finish_pack_body(body);
1285
1286 let (_dir, store) = fresh_store();
1287 let err = PackReader::read(&pack, &store).unwrap_err();
1288 assert!(matches!(err, PackError::TrailingData), "got {err:?}");
1289 assert!(!store.contains(&blob_hash));
1290 }
1291
1292 #[test]
1293 fn rejects_invalid_magic() {
1294 let mut pack = PackWriter::new().finish().unwrap();
1297 pack[0] = b'X';
1298 pack[1] = b'X';
1299 pack[2] = b'X';
1300 pack[3] = b'X';
1301 let (_dir, store) = fresh_store();
1302 let err = PackReader::read(&pack, &store).unwrap_err();
1303 assert!(matches!(err, PackError::InvalidMagic));
1304 }
1305
1306 #[test]
1307 fn rejects_unknown_version() {
1308 let mut pack = PackWriter::new().finish().unwrap();
1309 pack[4] = 99;
1311 let (_dir, store) = fresh_store();
1317 let err = PackReader::read(&pack, &store).unwrap_err();
1318 assert!(matches!(err, PackError::UnsupportedVersion(99)));
1319 }
1320
1321 #[test]
1322 fn rejects_truncated_pack() {
1323 let pack = vec![b'M', b'K']; let (_dir, store) = fresh_store();
1325 let err = PackReader::read(&pack, &store).unwrap_err();
1326 assert!(matches!(err, PackError::PackfileTooShort));
1327 }
1328
1329 #[test]
1330 fn rejects_bit_flipped_trailer() {
1331 let blob = write_blob_via_serialize(b"trailer test");
1332 let h = hash::hash(&blob);
1333 let mut w = PackWriter::new();
1334 w.push_raw(h, &blob).unwrap();
1335 let mut pack = w.finish().unwrap();
1336 let last = pack.len() - 1;
1337 pack[last] ^= 0x01; let (_dir, store) = fresh_store();
1339 let err = PackReader::read(&pack, &store).unwrap_err();
1340 assert!(matches!(err, PackError::PackfileCorrupted));
1341 }
1342
1343 #[test]
1344 fn rejects_reserved_entry_type_0x01() {
1345 let mut buf = Vec::new();
1347 buf.extend_from_slice(MAGIC);
1348 buf.extend_from_slice(&VERSION.to_le_bytes());
1349 buf.extend_from_slice(&1u32.to_le_bytes());
1350 buf.push(0x01); buf.extend_from_slice(&0u32.to_le_bytes()); let trailer = hash::hash(&buf);
1353 buf.extend_from_slice(&trailer);
1354
1355 let (_dir, store) = fresh_store();
1356 let err = PackReader::read(&buf, &store).unwrap_err();
1357 assert!(matches!(err, PackError::InvalidEntryType(0x01)));
1358 }
1359
1360 #[test]
1361 fn rejects_unknown_entry_type() {
1362 let mut buf = Vec::new();
1363 buf.extend_from_slice(MAGIC);
1364 buf.extend_from_slice(&VERSION.to_le_bytes());
1365 buf.extend_from_slice(&1u32.to_le_bytes());
1366 buf.push(0x77); buf.extend_from_slice(&0u32.to_le_bytes());
1368 let trailer = hash::hash(&buf);
1369 buf.extend_from_slice(&trailer);
1370
1371 let (_dir, store) = fresh_store();
1372 let err = PackReader::read(&buf, &store).unwrap_err();
1373 assert!(matches!(err, PackError::InvalidEntryType(0x77)));
1374 }
1375
1376 #[test]
1377 fn delta_base_missing_is_loud() {
1378 let mut fake_base = [0u8; 32];
1379 fake_base[0] = 0xAB;
1380 let mut stream = Vec::new();
1382 stream.push(0x01); stream.extend_from_slice(&0u32.to_le_bytes()); stream.extend_from_slice(&0u32.to_le_bytes()); let mut w = PackWriter::new();
1386 w.push_delta(&fake_base, &stream).unwrap();
1387 let pack = w.finish().unwrap();
1388
1389 let (_dir, store) = fresh_store();
1390 let err = PackReader::read(&pack, &store).unwrap_err();
1391 assert!(matches!(err, PackError::DeltaBaseMissing(_)), "got {err:?}");
1392 }
1393
1394 #[test]
1395 fn entry_payload_past_trailer_rejected() {
1396 let mut buf = Vec::new();
1397 buf.extend_from_slice(MAGIC);
1398 buf.extend_from_slice(&VERSION.to_le_bytes());
1399 buf.extend_from_slice(&1u32.to_le_bytes());
1400 buf.push(0x00);
1401 buf.extend_from_slice(&1_000_000u32.to_le_bytes());
1402 let trailer = hash::hash(&buf);
1404 buf.extend_from_slice(&trailer);
1405
1406 let (_dir, store) = fresh_store();
1407 let err = PackReader::read(&buf, &store).unwrap_err();
1408 assert!(matches!(err, PackError::UnexpectedEof));
1409 }
1410
1411 #[test]
1412 fn entry_count_over_cap_rejected() {
1413 let mut buf = Vec::new();
1414 buf.extend_from_slice(MAGIC);
1415 buf.extend_from_slice(&VERSION.to_le_bytes());
1416 buf.extend_from_slice(&u32::MAX.to_le_bytes());
1417 let trailer = hash::hash(&buf);
1422 buf.extend_from_slice(&trailer);
1423
1424 let (_dir, store) = fresh_store();
1425 let err = PackReader::read(&buf, &store).unwrap_err();
1426 assert!(
1429 matches!(err, PackError::TooManyObjects(_)),
1430 "expected TooManyObjects, got {err:?}"
1431 );
1432 }
1433
1434 #[test]
1435 fn payload_sum_over_cap_is_rejected_before_bounds_or_decode() {
1436 let blob_a = write_blob_via_serialize(&incompressible_bytes(0xA5A5, 64));
1446 let blob_b = write_blob_via_serialize(&incompressible_bytes(0xB6B6, 64));
1447 let mut w = PackWriter::new();
1448 w.push_raw(hash::hash(&blob_a), &blob_a).unwrap();
1449 w.push_raw(hash::hash(&blob_b), &blob_b).unwrap();
1450 let pack = w.finish().unwrap();
1451
1452 let (_dir, store) = fresh_store();
1453
1454 let cap = (blob_a.len() as u64) + 10;
1458 let err = PackReader::read_with_payload_cap(&pack, &store, cap).unwrap_err();
1459 assert!(
1460 matches!(err, PackError::PackfileTooLarge),
1461 "expected PackfileTooLarge, got {err:?}"
1462 );
1463
1464 let report = PackReader::read(&pack, &store).unwrap();
1467 assert_eq!(report.raw_count, 2);
1468 }
1469
1470 #[test]
1471 fn pack_key_is_blake3_of_pack_bytes() {
1472 let blob = write_blob_via_serialize(b"key test");
1473 let h = hash::hash(&blob);
1474 let mut w = PackWriter::new();
1475 w.push_raw(h, &blob).unwrap();
1476 let pack = w.finish().unwrap();
1477 assert_eq!(pack_key(&pack), hash::hash(&pack));
1478 }
1479
1480 #[test]
1481 fn unpack_does_not_recopy_raw_payloads_into_a_second_buffer() {
1482 let mut w = PackWriter::new();
1497 for i in 0u32..64 {
1498 let payload = incompressible_bytes(0x1000_0000 + u64::from(i), 16 * 1024);
1499 let blob = write_blob_via_serialize(&payload);
1500 w.push_raw(hash::hash(&blob), &blob).unwrap();
1501 }
1502 let pack = w.finish().unwrap();
1503 assert!(
1504 pack.len() > 512 * 1024,
1505 "sanity: synthetic pack should be substantial, got {}",
1506 pack.len()
1507 );
1508 assert_eq!(
1509 u32::from_le_bytes(pack[VERSION_OFFSET..VERSION_OFFSET + 4].try_into().unwrap()),
1510 VERSION,
1511 "sanity: incompressible filler must stay an uncompressed v1 pack"
1512 );
1513
1514 let (_dir, store) = fresh_store();
1515 let owned_bytes = AtomicU64::new(0);
1516 let report = PackReader::read_tracking_owned_bytes(&pack, &store, &owned_bytes).unwrap();
1517 assert_eq!(report.raw_count, 64);
1518
1519 assert_eq!(
1520 owned_bytes.load(Ordering::Relaxed),
1521 0,
1522 "an all-raw pack must not allocate a second copy of any entry's payload"
1523 );
1524 }
1525
1526 #[test]
1527 fn unpack_owned_bytes_for_deltas_is_exactly_the_delta_targets_not_the_whole_pack() {
1528 let content_base = incompressible_bytes(0x2BAD_2BAD, 4096);
1536 let base_obj = write_blob_via_serialize(&content_base);
1537 let base_hash = hash::hash(&base_obj);
1538
1539 let mut w = PackWriter::new();
1540 w.push_raw(base_hash, &base_obj).unwrap();
1541 let mut expected_owned = 0u64;
1542 for i in 0u32..10 {
1543 let mut target = content_base.clone();
1544 target[i as usize] ^= 0xFF;
1545 let target_obj = write_blob_via_serialize(&target);
1546 let stream = delta::encode(&base_obj, &target_obj).unwrap();
1547 w.push_delta(&base_hash, &stream).unwrap();
1548 expected_owned += target_obj.len() as u64;
1549 }
1550 let pack = w.finish().unwrap();
1551
1552 let (_dir, store) = fresh_store();
1553 let owned_bytes = AtomicU64::new(0);
1554 let report = PackReader::read_tracking_owned_bytes(&pack, &store, &owned_bytes).unwrap();
1555 assert_eq!(report.raw_count, 1);
1556 assert_eq!(report.delta_count, 10);
1557
1558 assert_eq!(
1559 owned_bytes.load(Ordering::Relaxed),
1560 expected_owned,
1561 "owned bytes must equal exactly the sum of delta target sizes — \
1562 no extra copy of the raw base"
1563 );
1564 }
1565
1566 #[test]
1567 fn pack_writer_finish_does_not_recopy_pushed_payloads() {
1568 let mut w = PackWriter::new();
1583 for i in 0u32..64 {
1584 let payload = incompressible_bytes(0x2000_0000 + u64::from(i), 16 * 1024);
1585 let blob = write_blob_via_serialize(&payload);
1586 w.push_raw(hash::hash(&blob), &blob).unwrap();
1587 }
1588 let bytes_copied = AtomicU64::new(0);
1589 let pack = w.finish_tracking_bytes_copied(&bytes_copied).unwrap();
1590 assert!(pack.len() > 512 * 1024);
1591
1592 assert_eq!(
1593 bytes_copied.load(Ordering::Relaxed),
1594 TRAILER_LEN as u64,
1595 "finish() must only append the trailer, not re-copy every pushed entry"
1596 );
1597 }
1598
1599 #[test]
1600 fn delta_resolves_against_pre_existing_store_object() {
1601 let (_dir, store) = fresh_store();
1602 let mut content_base = vec![0u8; 256];
1604 for (i, b) in content_base.iter_mut().enumerate() {
1605 *b = u8::try_from(i % 251).expect("modulo < 256");
1606 }
1607 let base_obj = write_blob_via_serialize(&content_base);
1608 let base_hash = store.write(&base_obj).unwrap();
1609
1610 let mut content_target = content_base.clone();
1612 content_target[100] = 0xAA;
1613 let target_obj = write_blob_via_serialize(&content_target);
1614 let target_hash = hash::hash(&target_obj);
1615 let stream = delta::encode(&base_obj, &target_obj).unwrap();
1616
1617 let mut w = PackWriter::new();
1618 w.push_delta(&base_hash, &stream).unwrap();
1619 let pack = w.finish().unwrap();
1620
1621 let report = PackReader::read(&pack, &store).unwrap();
1622 assert_eq!(report.delta_count, 1);
1623 assert_eq!(report.raw_count, 0);
1624 assert_eq!(store.read(&target_hash).unwrap(), target_obj);
1625 }
1626
1627 #[test]
1628 fn multiple_deltas_against_shared_external_base_read_store_once() {
1629 const N: usize = 5;
1635
1636 let (_dir, store) = fresh_store();
1637
1638 let mut content_base = vec![0u8; 512];
1639 for (i, b) in content_base.iter_mut().enumerate() {
1640 *b = u8::try_from(i % 251).expect("modulo < 256");
1641 }
1642 let base_obj = write_blob_via_serialize(&content_base);
1643 let base_hash = store.write(&base_obj).unwrap();
1644
1645 let mut w = PackWriter::new();
1647 let mut expected_targets = Vec::new();
1648 for i in 0..N {
1649 let mut content_target = content_base.clone();
1650 content_target[100] = u8::try_from(i).unwrap();
1651 let target_obj = write_blob_via_serialize(&content_target);
1652 let target_hash = hash::hash(&target_obj);
1653 let stream = delta::encode(&base_obj, &target_obj).unwrap();
1654 w.push_delta(&base_hash, &stream).unwrap();
1655 expected_targets.push((target_hash, target_obj));
1656 }
1657 let pack = w.finish().unwrap();
1658
1659 let reads_before = store.read_call_count();
1660 let report = PackReader::read(&pack, &store).unwrap();
1661 let reads_after_for_base = store.read_call_count() - reads_before;
1662
1663 assert_eq!(report.delta_count, u32::try_from(N).unwrap());
1664 assert_eq!(
1665 reads_after_for_base, 1,
1666 "base object must be read from the store exactly once for {N} deltas sharing it, got {reads_after_for_base}"
1667 );
1668
1669 for (target_hash, target_obj) in expected_targets {
1673 assert_eq!(store.read(&target_hash).unwrap(), target_obj);
1674 }
1675 }
1676
1677 fn compressible_bytes(len: usize) -> Vec<u8> {
1685 vec![0x42u8; len]
1686 }
1687
1688 #[test]
1689 #[cfg(feature = "pack-zstd")]
1690 fn compressed_raw_entry_roundtrips() {
1691 let payload = compressible_bytes(4096);
1692 let blob = write_blob_via_serialize(&payload);
1693 let h = hash::hash(&blob);
1694
1695 let mut w = PackWriter::new();
1696 w.push_raw(h, &blob).unwrap();
1697 let pack = w.finish().unwrap();
1698
1699 assert_eq!(
1700 u32::from_le_bytes(pack[VERSION_OFFSET..VERSION_OFFSET + 4].try_into().unwrap()),
1701 VERSION_V2,
1702 "a pack containing a compressed entry must be emitted as version 2"
1703 );
1704 assert_eq!(
1705 pack[HEADER_LEN], 0x03,
1706 "a highly-compressible raw payload must be emitted as 0x03 zstd-raw"
1707 );
1708
1709 let (_dir, store) = fresh_store();
1710 let report = PackReader::read(&pack, &store).unwrap();
1711 assert_eq!(report.raw_count, 1);
1712 assert_eq!(report.delta_count, 0);
1713 assert_eq!(report.stored, vec![h]);
1714 assert_eq!(
1715 store.read(&h).unwrap(),
1716 blob,
1717 "recovered object must be byte-identical to the pre-compression original"
1718 );
1719 }
1720
1721 #[test]
1722 #[cfg(feature = "pack-zstd")]
1723 fn compressed_delta_entry_roundtrips() {
1724 let base_obj =
1729 write_blob_via_serialize(b"delta base filler bytes, not compressible-target-shaped");
1730 let base_hash = hash::hash(&base_obj);
1731 let target_content = compressible_bytes(4096);
1732 let target_obj = write_blob_via_serialize(&target_content);
1733 let target_hash = hash::hash(&target_obj);
1734 let stream = delta::encode(&base_obj, &target_obj).unwrap();
1735 assert!(
1736 stream.len() >= 64,
1737 "sanity: delta stream must clear the writer's compression-candidate floor, got {}",
1738 stream.len()
1739 );
1740
1741 let mut w = PackWriter::new();
1742 w.push_raw(base_hash, &base_obj).unwrap();
1743 w.push_delta(&base_hash, &stream).unwrap();
1744 let pack = w.finish().unwrap();
1745
1746 assert_eq!(
1747 u32::from_le_bytes(pack[VERSION_OFFSET..VERSION_OFFSET + 4].try_into().unwrap()),
1748 VERSION_V2,
1749 "a pack containing a compressed entry must be emitted as version 2"
1750 );
1751 let base_payload_len =
1754 u32::from_le_bytes(pack[HEADER_LEN + 1..HEADER_LEN + 5].try_into().unwrap()) as usize;
1755 let second_entry_type_offset = HEADER_LEN + ENTRY_FRAME_LEN + base_payload_len;
1756 assert_eq!(
1757 pack[second_entry_type_offset], 0x04,
1758 "a highly-compressible delta stream must be emitted as 0x04 zstd-delta"
1759 );
1760
1761 let (_dir, store) = fresh_store();
1762 let report = PackReader::read(&pack, &store).unwrap();
1763 assert_eq!(report.raw_count, 1);
1764 assert_eq!(report.delta_count, 1);
1765 assert_eq!(report.stored, vec![base_hash, target_hash]);
1766 assert_eq!(
1767 store.read(&target_hash).unwrap(),
1768 target_obj,
1769 "recovered delta target must be byte-identical to the pre-compression original"
1770 );
1771 }
1772
1773 #[test]
1774 fn rejects_v2_entry_type_in_v1_pack() {
1775 let mut buf = Vec::new();
1780 buf.extend_from_slice(MAGIC);
1781 buf.extend_from_slice(&VERSION.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.push(0x03);
1784 let inner_payload = 0u32.to_le_bytes(); buf.extend_from_slice(&u32::try_from(inner_payload.len()).unwrap().to_le_bytes());
1786 buf.extend_from_slice(&inner_payload);
1787 let pack = finish_pack_body(buf);
1788
1789 let (_dir, store) = fresh_store();
1790 let err = PackReader::read(&pack, &store).unwrap_err();
1791 assert!(
1792 matches!(err, PackError::InvalidEntryType(0x03)),
1793 "got {err:?}"
1794 );
1795 }
1796
1797 #[test]
1798 #[cfg(feature = "pack-zstd")]
1799 fn rejects_decompressed_len_mismatch() {
1800 let payload = compressible_bytes(4096);
1804 let blob = write_blob_via_serialize(&payload);
1805 let h = hash::hash(&blob);
1806 let mut w = PackWriter::new();
1807 w.push_raw(h, &blob).unwrap();
1808 let mut pack = w.finish().unwrap();
1809
1810 assert_eq!(pack[HEADER_LEN], 0x03, "sanity: must be a zstd-raw entry");
1811 let len_prefix_offset = HEADER_LEN + ENTRY_FRAME_LEN;
1812 let claimed_len = u32::from_le_bytes(
1813 pack[len_prefix_offset..len_prefix_offset + 4]
1814 .try_into()
1815 .unwrap(),
1816 );
1817 pack[len_prefix_offset..len_prefix_offset + 4]
1823 .copy_from_slice(&(claimed_len + 1).to_le_bytes());
1824 let split = pack.len() - TRAILER_LEN;
1825 let new_trailer = hash::hash(&pack[..split]);
1826 pack[split..].copy_from_slice(&new_trailer);
1827
1828 let (_dir, store) = fresh_store();
1829 let err = PackReader::read(&pack, &store).unwrap_err();
1830 assert!(
1831 matches!(err, PackError::DecompressedSizeMismatch(_, _)),
1832 "got {err:?}"
1833 );
1834 assert!(!store.contains(&h));
1835 }
1836
1837 #[test]
1838 #[cfg(feature = "pack-zstd")]
1839 fn rejects_decompressed_len_over_object_cap() {
1840 let claimed_len = u32::try_from(MAX_RAW_OBJECT_SIZE + 1).unwrap();
1845 let mut buf = Vec::new();
1846 buf.extend_from_slice(MAGIC);
1847 buf.extend_from_slice(&VERSION_V2.to_le_bytes());
1848 buf.extend_from_slice(&1u32.to_le_bytes());
1849 buf.push(0x03);
1850 let mut inner = Vec::new();
1854 inner.extend_from_slice(&claimed_len.to_le_bytes());
1855 inner.extend_from_slice(&[0u8; 8]);
1856 buf.extend_from_slice(&u32::try_from(inner.len()).unwrap().to_le_bytes());
1857 buf.extend_from_slice(&inner);
1858 let pack = finish_pack_body(buf);
1859
1860 let (_dir, store) = fresh_store();
1861 let err = PackReader::read(&pack, &store).unwrap_err();
1862 assert!(
1863 matches!(err, PackError::DecompressedSizeOverCap(n) if n == claimed_len as usize),
1864 "got {err:?}"
1865 );
1866 }
1867}