Skip to main content

sley_pack/
lib.rs

1// sley#7: untrusted-input parsing crate — fallible ops propagate errors;
2// the only retained `expect`s would be documented compile-time invariants.
3#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
4
5use flate2::{Compress, Compression, FlushCompress, Status};
6use sley_core::{
7    primitives::{u16_be, u32_be, u64_be},
8    ByteBudget, CancelFlag, GitError, ObjectFormat, ObjectId, ResourceLimitKind, Result,
9    StreamingDigest,
10};
11use sley_formats::Bundle;
12use sley_object::{EncodedObject, ObjectType};
13use std::borrow::Borrow;
14use std::cell::RefCell;
15use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
16use std::fmt;
17use std::io::Write;
18use std::ops::Range;
19use std::sync::Arc;
20
21// --- Mechanical module split (W21) -----------------------------------------
22// The former single ~10k-line lib.rs is partitioned into contiguous
23// submodules along its existing function-cluster seams. Each submodule pulls
24// the crate-root scope in via `use super::*` and is re-exported below so every
25// `sley_pack::X` path (public API and intra-crate) resolves unchanged.
26// This is a pure code move: no function body was altered.
27mod bounded_read;
28mod delta;
29mod fix_thin;
30mod index;
31pub mod inflate;
32mod limits;
33mod parallel_index;
34mod read;
35mod write;
36
37pub use bounded_read::*;
38pub(crate) use delta::*;
39pub use fix_thin::*;
40pub use index::*;
41pub use limits::{MAX_READ_DELTA_CHAIN_DEPTH, PACK_OBJECT_COUNT_PREALLOC_CAP};
42pub(crate) use limits::{checked_pack_object_count, pack_entry_prealloc};
43pub use parallel_index::*;
44pub use read::*;
45pub use write::*;
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct PackEntry {
49    pub oid: ObjectId,
50    pub compressed_size: u64,
51    pub uncompressed_size: u64,
52    pub offset: u64,
53}
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct RepackPolicy {
56    pub write_bitmaps: bool,
57    pub cruft_packs: bool,
58    pub geometric_factor: Option<u8>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PackFile {
63    pub version: u32,
64    pub entries: Vec<PackObject>,
65    pub checksum: ObjectId,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct PackObject {
70    pub entry: PackEntry,
71    pub object: EncodedObject,
72}
73
74/// Per-object statistics for one entry of a verified pack, in the shape
75/// `git verify-pack -v` reports.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct PackVerifyStat {
78    /// Resolved object id.
79    pub oid: ObjectId,
80    /// Resolved object type (the delta's *result* type, not `ofs-delta`).
81    pub object_type: ObjectType,
82    /// Resolved (inflated) object size in bytes.
83    pub size: u64,
84    /// Bytes this object occupies in the pack: the offset delta to the next
85    /// object, or to the trailing checksum for the last object.
86    pub size_in_pack: u64,
87    /// In-pack byte offset where this object's entry begins.
88    pub offset: u64,
89    /// Delta chain depth: `0` for undeltified objects, base-depth + 1 otherwise.
90    pub delta_depth: u32,
91    /// For delta objects, the id of the *immediate* base object (which may
92    /// itself be a delta). `None` for undeltified objects.
93    pub base_oid: Option<ObjectId>,
94}
95
96/// Result of [`PackFile::verify_pack_stats`]: per-object stats in pack offset
97/// order plus the pack's trailing checksum.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct PackVerifyStats {
100    pub objects: Vec<PackVerifyStat>,
101    pub checksum: ObjectId,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct PackWrite {
106    pub pack: Vec<u8>,
107    pub index: Vec<u8>,
108    pub checksum: ObjectId,
109    pub entries: Vec<PackIndexEntry>,
110    pub delta_count: u32,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct PackWriteSummary {
115    pub index: Vec<u8>,
116    pub checksum: ObjectId,
117    pub entries: Vec<PackIndexEntry>,
118    pub delta_count: u32,
119    pub pack_size: u64,
120    /// High-water mark of decoded window bodies plus retained delta bases,
121    /// as charged by [`PackWriteLimits`]. Does not include zlib output buffers
122    /// or allocator slack.
123    pub peak_working_set_bytes: u64,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct PackInput<'a> {
128    pub oid: &'a ObjectId,
129    pub object: &'a EncodedObject,
130}
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132enum PackObjectKind {
133    Commit,
134    Tree,
135    Blob,
136    Tag,
137    OfsDelta,
138    RefDelta,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142enum DeltaBase {
143    Offset(u64),
144    Ref(ObjectId),
145}
146
147/// One pack entry as stored on disk, used by [`PackFile::verify_pack_stats`] to
148/// recover the delta structure and on-disk stream size that resolved
149/// [`PackObject`]s no longer carry.
150struct OnDiskEntry {
151    offset: u64,
152    base: Option<DeltaBase>,
153    stream_size: u64,
154}
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156struct EntryHeader {
157    kind: PackObjectKind,
158    size: u64,
159}
160fn next_byte(bytes: &[u8], offset: &mut usize) -> Result<u8> {
161    let Some(byte) = bytes.get(*offset).copied() else {
162        return Err(GitError::InvalidFormat(
163            "truncated pack entry header".into(),
164        ));
165    };
166    *offset += 1;
167    Ok(byte)
168}
169
170fn checked_range(
171    start: usize,
172    count: usize,
173    width: usize,
174    total: usize,
175) -> Result<std::ops::Range<usize>> {
176    let len = count
177        .checked_mul(width)
178        .ok_or_else(|| GitError::InvalidFormat("pack index table overflow".into()))?;
179    let end = start
180        .checked_add(len)
181        .ok_or_else(|| GitError::InvalidFormat("pack index table overflow".into()))?;
182    if end > total {
183        return Err(GitError::InvalidFormat("truncated pack index table".into()));
184    }
185    Ok(start..end)
186}
187
188fn validate_position_permutation(positions: &[u32]) -> Result<()> {
189    let mut seen = vec![false; positions.len()];
190    for position in positions {
191        let idx = *position as usize;
192        if idx >= positions.len() {
193            return Err(GitError::InvalidFormat(format!(
194                "invalid rev-index position {position}"
195            )));
196        }
197        if seen[idx] {
198            return Err(GitError::InvalidFormat(format!(
199                "invalid rev-index position {position}"
200            )));
201        }
202        seen[idx] = true;
203    }
204    Ok(())
205}
206
207// Reused zlib inflate state. Resetting and reusing one `Decompress` avoids
208// allocating a fresh (~10 KiB) `InflateState` for every object and delta decoded —
209// an allocation that dominated bulk reads. Borrowed only for the duration of a
210// single inflate; the recursive pack reader fully inflates each entry's data before
211// recursing to its base, so the borrow never nests.
212thread_local! {
213    pub(crate) static INFLATE: RefCell<flate2::Decompress> = RefCell::new(flate2::Decompress::new(true));
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use flate2::Compression;
220    use flate2::read::ZlibDecoder;
221    use flate2::write::ZlibEncoder;
222    use sley_core::AtomicCancel;
223    use std::fs;
224    use std::io::{Read, Write};
225    use std::path::{Path, PathBuf};
226    use std::process::Command;
227    use std::time::{SystemTime, UNIX_EPOCH};
228
229    fn multi_blob_pack(count: usize) -> Vec<u8> {
230        let objects = (0..count)
231            .map(|idx| {
232                EncodedObject::new(
233                    ObjectType::Blob,
234                    format!("stream cancel pack object {idx}\n").into_bytes(),
235                )
236            })
237            .collect::<Vec<_>>();
238        PackFile::write_undeltified(&objects, ObjectFormat::Sha1)
239            .expect("test operation should succeed")
240            .pack
241    }
242
243    #[test]
244    fn parallel_index_already_cancelled_returns_cancelled() {
245        let pack = multi_blob_pack(4);
246        let source = AtomicCancel::new();
247        source.cancel();
248        let err = PackIndex::write_v2_for_pack_with_options(
249            &pack,
250            ObjectFormat::Sha1,
251            |_| Ok(None),
252            PackIndexOptions::default(),
253            CancelFlag::new(&source),
254            |_| {},
255        )
256        .expect_err("pre-cancelled index should fail");
257        assert_eq!(err, GitError::Cancelled);
258    }
259
260    #[test]
261    fn parallel_index_is_identical_with_one_or_many_workers() {
262        let pack = multi_blob_pack(128);
263        let serial_schedule = PackIndex::write_v2_for_pack_with_options(
264            &pack,
265            ObjectFormat::Sha1,
266            |_| Ok(None),
267            PackIndexOptions::default().with_threads(1),
268            CancelFlag::never(),
269            |_| {},
270        )
271        .expect("one-worker schedule");
272        let parallel_schedule = PackIndex::write_v2_for_pack_with_options(
273            &pack,
274            ObjectFormat::Sha1,
275            |_| Ok(None),
276            PackIndexOptions::default().with_threads(32),
277            CancelFlag::never(),
278            |_| {},
279        )
280        .expect("many-worker schedule");
281        assert_eq!(parallel_schedule, serial_schedule);
282    }
283
284    #[test]
285    fn write_packed_from_source_respects_cancel_between_windows() {
286        let format = ObjectFormat::Sha1;
287        let count = 4u32;
288        let objects = (0..count)
289            .map(|idx| {
290                EncodedObject::new(
291                    ObjectType::Blob,
292                    format!("write-cancel object {idx}\n").into_bytes(),
293                )
294            })
295            .collect::<Vec<_>>();
296        let object_ids = objects
297            .iter()
298            .map(|object| object.object_id(format).expect("oid"))
299            .collect::<Vec<_>>();
300        let object_map = object_ids
301            .iter()
302            .copied()
303            .zip(objects.into_iter().map(Arc::new))
304            .collect::<HashMap<_, _>>();
305
306        let source = AtomicCancel::new();
307        source.cancel();
308        let mut written = Vec::new();
309        let err = PackFile::write_packed_from_source_to_writer_with_cancel(
310            object_ids.iter().copied(),
311            count,
312            format,
313            &PackWriteOptions::new().with_reorder(false),
314            PackWriteLimits::default(),
315            |oid| {
316                object_map
317                    .get(oid)
318                    .cloned()
319                    .ok_or_else(|| GitError::not_found(format!("missing test object {oid}")))
320            },
321            &mut written,
322            CancelFlag::new(&source),
323        )
324        .expect_err("pre-cancelled pack write should fail");
325        assert_eq!(err, GitError::Cancelled);
326        assert!(
327            written.is_empty() || written.len() < 32,
328            "should not have finished a full pack after cancel"
329        );
330    }
331
332    fn delta_pack_options(prefer_ofs_delta: bool) -> PackWriteOptions {
333        PackWriteOptions::new()
334            .with_prefer_ofs_delta(prefer_ofs_delta)
335            .with_reorder(false)
336    }
337
338    #[test]
339    fn parses_single_blob_pack() {
340        let pack = single_object_pack(ObjectFormat::Sha1, ObjectType::Blob, b"hello\n");
341        let parsed = PackFile::parse_sha1(&pack).expect("test operation should succeed");
342        assert_eq!(parsed.version, 2);
343        assert_eq!(parsed.entries.len(), 1);
344        let object = &parsed.entries[0].object;
345        assert_eq!(object.object_type, ObjectType::Blob);
346        assert_eq!(object.body, b"hello\n");
347        assert_eq!(
348            parsed.entries[0].entry.oid.to_hex(),
349            "ce013625030ba8dba906f756967f9e9ca394464a"
350        );
351    }
352
353    #[test]
354    fn parses_single_blob_pack_sha256() {
355        let pack = single_object_pack(ObjectFormat::Sha256, ObjectType::Blob, b"hello\n");
356        let parsed =
357            PackFile::parse(&pack, ObjectFormat::Sha256).expect("test operation should succeed");
358        assert_eq!(parsed.version, 2);
359        assert_eq!(parsed.entries.len(), 1);
360        let object = &parsed.entries[0].object;
361        assert_eq!(object.object_type, ObjectType::Blob);
362        assert_eq!(object.body, b"hello\n");
363        assert_eq!(
364            parsed.entries[0].entry.oid,
365            object
366                .object_id(ObjectFormat::Sha256)
367                .expect("test operation should succeed")
368        );
369    }
370
371    #[test]
372    fn parses_bundle_pack_payload_with_bundle_format() {
373        let pack = single_object_pack(ObjectFormat::Sha1, ObjectType::Blob, b"bundle\n");
374        let oid = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"bundle\n")
375            .expect("test operation should succeed");
376        let bundle_bytes = format!("# v2 git bundle\n{oid} refs/heads/main\n\n")
377            .into_bytes()
378            .into_iter()
379            .chain(pack)
380            .collect::<Vec<_>>();
381        let bundle = Bundle::parse(&bundle_bytes, ObjectFormat::Sha1)
382            .expect("test operation should succeed");
383
384        let parsed = PackFile::parse_bundle(&bundle).expect("test operation should succeed");
385        assert_eq!(parsed.entries.len(), 1);
386        assert_eq!(parsed.entries[0].object.object_type, ObjectType::Blob);
387        assert_eq!(parsed.entries[0].object.body, b"bundle\n");
388    }
389
390    /// Build a pack whose single blob entry header LIES about its decompressed
391    /// size: it declares `declared_size` while the actual zlib payload only
392    /// inflates to `real_body`. A short `real_body` plus a `declared_size` of
393    /// `u64::MAX` is the decompression-bomb shape — the header claims terabytes
394    /// from a handful of compressed bytes.
395    fn lying_size_blob_pack(format: ObjectFormat, declared_size: u64, real_body: &[u8]) -> Vec<u8> {
396        let mut pack = Vec::new();
397        pack.extend_from_slice(b"PACK");
398        pack.extend_from_slice(&2u32.to_be_bytes());
399        pack.extend_from_slice(&1u32.to_be_bytes());
400        // Object type 3 == blob; size varint encodes the *attacker-declared* size.
401        write_pack_entry_header_kind(&mut pack, 3, declared_size);
402        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
403        encoder
404            .write_all(real_body)
405            .expect("test operation should succeed");
406        pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
407        let checksum =
408            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
409        pack.extend_from_slice(checksum.as_bytes());
410        pack
411    }
412
413    /// Regression: a crafted pack object header declaring a gigantic decompressed
414    /// size with a tiny compressed payload must NOT drive an up-front
415    /// reservation/allocation of that declared size (OOM/abort). sley#2: the
416    /// header `size` is attacker-controlled over the network (install_raw_pack →
417    /// sley-fetch), so it must be validated/bounded before any `Vec::reserve`.
418    ///
419    /// On the unfixed code, `inflate_into` did `out.reserve(header.size as usize)`
420    /// with `header.size == u64::MAX`, which panics with "capacity overflow" (or
421    /// aborts on alloc failure) *before* the size-mismatch check could fire. We
422    /// run parse on a worker thread so that panic surfaces as a `join()` error
423    /// rather than killing the test process; the fix turns this into a clean
424    /// `Err` returned normally.
425    #[test]
426    fn rejects_decompression_bomb_header_without_oom() {
427        for &declared in &[u64::MAX, 100 * 1024 * 1024 * 1024, u64::from(u32::MAX) * 4] {
428            let pack = lying_size_blob_pack(ObjectFormat::Sha1, declared, b"tiny\n");
429            let handle = std::thread::spawn(move || PackFile::parse_sha1(&pack));
430            let result = handle.join();
431            // The parse thread must not have panicked/aborted on a huge reserve.
432            assert!(
433                result.is_ok(),
434                "parsing a bomb header (declared={declared}) panicked instead of erroring cleanly"
435            );
436            // And parsing must reject the lie (decoded len != declared size).
437            let parse_result = result.expect("parse thread should not panic on a bomb header");
438            assert!(
439                parse_result.is_err(),
440                "bomb header (declared={declared}) should be rejected as invalid"
441            );
442        }
443    }
444
445    /// Build a 2-object pack: a real base blob followed by a delta (ref or ofs)
446    /// whose *result-size* varint lies, declaring `declared_result_size`, while
447    /// carrying a tiny real instruction stream. The delta's base-size varint is
448    /// set correctly (so the base-size check at the top of `apply_pack_delta`
449    /// passes and we reach the result reservation). Used to drive the sley#35
450    /// delta-result-size bomb.
451    fn lying_result_size_delta_pack(
452        format: ObjectFormat,
453        declared_result_size: u64,
454        delta_kind: DeltaKind,
455    ) -> Vec<u8> {
456        let base = b"hello";
457        let result = b"hello world"; // real produced length = 11
458
459        // Hand-build a delta with a truthful base-size and a LYING result-size.
460        let mut delta = Vec::new();
461        write_delta_varint(&mut delta, base.len() as u64);
462        write_delta_varint(&mut delta, declared_result_size);
463        // Real instructions: copy `base` then insert " world".
464        let suffix = &result[base.len()..];
465        delta.push(0x90); // copy, 1 size byte present (bit 0x10)
466        delta.push(base.len() as u8);
467        delta.push(suffix.len() as u8);
468        delta.extend_from_slice(suffix);
469
470        let mut pack = Vec::new();
471        pack.extend_from_slice(b"PACK");
472        pack.extend_from_slice(&2u32.to_be_bytes());
473        pack.extend_from_slice(&2u32.to_be_bytes());
474
475        let base_offset = pack.len();
476        write_entry_header(&mut pack, ObjectType::Blob, base.len() as u64);
477        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
478        encoder
479            .write_all(base)
480            .expect("test operation should succeed");
481        pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
482
483        let delta_offset = pack.len();
484        write_pack_entry_header_kind(
485            &mut pack,
486            match delta_kind {
487                DeltaKind::Offset => 6,
488                DeltaKind::Ref => 7,
489            },
490            delta.len() as u64,
491        );
492        match delta_kind {
493            DeltaKind::Offset => write_ofs_delta_offset(&mut pack, delta_offset - base_offset),
494            DeltaKind::Ref => {
495                let base_oid = sley_core::object_id_for_bytes(format, "blob", base)
496                    .expect("test operation should succeed");
497                pack.extend_from_slice(base_oid.as_bytes());
498            }
499        }
500        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
501        encoder
502            .write_all(&delta)
503            .expect("test operation should succeed");
504        pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
505
506        let checksum =
507            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
508        pack.extend_from_slice(checksum.as_bytes());
509        pack
510    }
511
512    /// Regression (sley#35): the 2nd instance of the sley#2 decompression-bomb
513    /// class. `apply_pack_delta` read an attacker-controlled `result_size` varint
514    /// from a network delta and fed it straight to `Vec::with_capacity`. A tiny
515    /// delta declaring `result_size == u64::MAX` (or ~1 TiB) aborts the process
516    /// ("capacity overflow"/alloc failure, SIGABRT) BEFORE the post-decode
517    /// size-mismatch check can reject the lie. Both ref-delta and ofs-delta paths
518    /// reach the same reservation, so both must be safe. We resolve the pack on a
519    /// worker thread so an abort/panic surfaces as a `join()` error rather than
520    /// killing the whole test binary; the fix turns the bomb into a clean `Err`.
521    #[test]
522    fn rejects_delta_result_size_bomb_without_oom() {
523        let bombs: &[u64] = &[u64::MAX, 1024 * 1024 * 1024 * 1024];
524        for &declared in bombs {
525            for delta_kind in [DeltaKind::Ref, DeltaKind::Offset] {
526                let pack = lying_result_size_delta_pack(ObjectFormat::Sha1, declared, delta_kind);
527                let handle = std::thread::spawn(move || PackFile::parse_sha1(&pack));
528                let join_result = handle.join();
529                assert!(
530                    join_result.is_ok(),
531                    "delta bomb (declared={declared}, kind={delta_kind:?}) panicked/aborted \
532                     instead of erroring cleanly"
533                );
534                let parse_result =
535                    join_result.expect("parse thread should not panic on a delta bomb");
536                assert!(
537                    parse_result.is_err(),
538                    "delta bomb (declared={declared}, kind={delta_kind:?}) should be rejected \
539                     as invalid (result.len() != declared)"
540                );
541            }
542        }
543    }
544
545    /// A legitimate (truthful) delta whose result-size varint matches the real
546    /// produced length must still resolve correctly — the bound only caps the
547    /// speculative reservation, it must not break real delta application.
548    #[test]
549    fn applies_legitimate_delta_after_result_size_bound() {
550        for delta_kind in [DeltaKind::Ref, DeltaKind::Offset] {
551            let base = b"hello";
552            let result = b"hello world";
553            let pack = two_object_delta_pack(ObjectFormat::Sha1, base, result, delta_kind);
554            let parsed = PackFile::parse_sha1(&pack).expect("legitimate delta should resolve");
555            assert_eq!(parsed.entries.len(), 2);
556            assert_eq!(parsed.entries[0].object.body, base);
557            assert_eq!(parsed.entries[1].object.body, result);
558        }
559    }
560
561    #[test]
562    fn bounded_delta_application_polls_cancel_between_commands() {
563        let base = vec![b'x'; 4096];
564        let mut result = base.clone();
565        result.extend_from_slice(b"changed");
566        let delta = DeltaIndex::new(&base).delta(&result).expect("delta");
567        let plan = plan_pack_delta(&base, &delta).expect("plan");
568        let mut out = Vec::with_capacity(result.len());
569        let source = AtomicCancel::new();
570        source.cancel();
571
572        assert_eq!(
573            apply_pack_delta_exact(&base, &delta, plan, &mut out, CancelFlag::new(&source)),
574            Err(GitError::Cancelled)
575        );
576        assert!(out.is_empty());
577    }
578
579    #[test]
580    fn legacy_delta_preserves_final_size_mismatch_and_copy_heavy_growth() {
581        let base = b"x";
582        let overproducing = vec![1, 1, 2, b'x', b'y'];
583        assert_eq!(
584            apply_pack_delta(base, &overproducing),
585            Err(GitError::InvalidObject(
586                "delta result size mismatch: expected 1, got 2".into()
587            ))
588        );
589
590        let copies = 4096usize;
591        let mut copy_heavy = Vec::new();
592        write_delta_varint(&mut copy_heavy, base.len() as u64);
593        write_delta_varint(&mut copy_heavy, copies as u64);
594        for _ in 0..copies {
595            // Copy one byte from base offset zero.
596            copy_heavy.extend_from_slice(&[0x91, 0, 1]);
597        }
598        assert_eq!(
599            apply_pack_delta(base, &copy_heavy).expect("copy-heavy legacy delta"),
600            vec![b'x'; copies]
601        );
602    }
603
604    #[test]
605    fn rejects_bundle_pack_payload_with_wrong_object_format() {
606        let pack = single_object_pack(ObjectFormat::Sha1, ObjectType::Blob, b"bundle\n");
607        let oid = sley_core::object_id_for_bytes(ObjectFormat::Sha256, "blob", b"bundle\n")
608            .expect("test operation should succeed");
609        let bundle_bytes =
610            format!("# v3 git bundle\n@object-format=sha256\n{oid} refs/heads/main\n\n")
611                .into_bytes()
612                .into_iter()
613                .chain(pack)
614                .collect::<Vec<_>>();
615        let bundle = Bundle::parse(&bundle_bytes, ObjectFormat::Sha1)
616            .expect("test operation should succeed");
617
618        assert!(PackFile::parse_bundle(&bundle).is_err());
619    }
620
621    fn assert_pack_index_view_matches_owned(index: &[u8], format: ObjectFormat) {
622        let owned = PackIndex::parse(index, format).expect("test operation should succeed");
623        let view = PackIndexView::parse(index, format).expect("test operation should succeed");
624        let owned_view =
625            PackIndexViewData::parse(Arc::from(index.to_vec().into_boxed_slice()), format)
626                .expect("test operation should succeed");
627
628        assert_eq!(view.version, owned.version);
629        assert_eq!(view.count, owned.entries.len());
630        assert_eq!(view.count(), owned.entries.len());
631        assert_eq!(view.fanout(), &owned.fanout);
632        assert_eq!(view.pack_checksum, owned.pack_checksum);
633        assert_eq!(view.index_checksum, owned.index_checksum);
634        assert_eq!(owned_view.version, owned.version);
635        assert_eq!(owned_view.count(), owned.entries.len());
636        assert_eq!(owned_view.fanout(), &owned.fanout);
637        assert_eq!(owned_view.pack_checksum, owned.pack_checksum);
638        assert_eq!(owned_view.index_checksum, owned.index_checksum);
639        for entry in &owned.entries {
640            let owned_found = owned
641                .find(&entry.oid)
642                .expect("test operation should succeed");
643            let expected = Some(PackIndexLookup {
644                crc32: owned_found.crc32,
645                offset: owned_found.offset,
646            });
647            assert_eq!(view.find(&entry.oid), expected);
648            assert_eq!(owned_view.find(&entry.oid), expected);
649        }
650    }
651
652    #[test]
653    fn writes_pack_and_index_that_round_trip() {
654        let object = EncodedObject::new(ObjectType::Blob, b"hello\n".to_vec());
655        let written = PackFile::write_undeltified_sha1(std::slice::from_ref(&object))
656            .expect("test operation should succeed");
657        let pack = PackFile::parse_sha1(&written.pack).expect("test operation should succeed");
658        let index =
659            PackIndex::parse_v2_sha1(&written.index).expect("test operation should succeed");
660        let oid = object
661            .object_id(ObjectFormat::Sha1)
662            .expect("test operation should succeed");
663        assert_eq!(pack.entries[0].object, object);
664        assert_eq!(index.pack_checksum, pack.checksum);
665        assert_eq!(
666            index
667                .find(&oid)
668                .expect("test operation should succeed")
669                .offset,
670            12
671        );
672    }
673
674    #[test]
675    fn pack_index_view_matches_owned_index_for_generated_sha1_pack() {
676        let objects = (0..8)
677            .map(|idx| {
678                EncodedObject::new(
679                    ObjectType::Blob,
680                    format!("borrowed pack index view sha1 object {idx}\n").into_bytes(),
681                )
682            })
683            .collect::<Vec<_>>();
684        let written = PackFile::write_packed(&objects, ObjectFormat::Sha1)
685            .expect("test operation should succeed");
686
687        assert_pack_index_view_matches_owned(&written.index, ObjectFormat::Sha1);
688
689        let view =
690            PackIndexView::parse_v2_sha1(&written.index).expect("test operation should succeed");
691        let missing = sley_core::object_id_for_bytes(
692            ObjectFormat::Sha1,
693            "blob",
694            b"not present in borrowed index\n",
695        )
696        .expect("test operation should succeed");
697        assert_eq!(view.find(&missing), None);
698    }
699
700    #[test]
701    fn writes_sha256_pack_and_index_that_round_trip() {
702        let object = EncodedObject::new(ObjectType::Blob, b"hello sha256\n".to_vec());
703        let written =
704            PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha256)
705                .expect("test operation should succeed");
706        let pack = PackFile::parse(&written.pack, ObjectFormat::Sha256)
707            .expect("test operation should succeed");
708        let index = PackIndex::parse(&written.index, ObjectFormat::Sha256)
709            .expect("test operation should succeed");
710        let oid = object
711            .object_id(ObjectFormat::Sha256)
712            .expect("test operation should succeed");
713        assert_eq!(pack.entries[0].object, object);
714        assert_eq!(index.pack_checksum, pack.checksum);
715        assert_eq!(index.pack_checksum.format(), ObjectFormat::Sha256);
716        assert_eq!(index.index_checksum.format(), ObjectFormat::Sha256);
717        assert_eq!(
718            index
719                .find(&oid)
720                .expect("test operation should succeed")
721                .offset,
722            12
723        );
724    }
725
726    #[test]
727    fn pack_index_view_matches_owned_index_for_generated_sha256_pack() {
728        let objects = (0..4)
729            .map(|idx| {
730                EncodedObject::new(
731                    ObjectType::Blob,
732                    format!("borrowed pack index view sha256 object {idx}\n").into_bytes(),
733                )
734            })
735            .collect::<Vec<_>>();
736        let written = PackFile::write_undeltified(&objects, ObjectFormat::Sha256)
737            .expect("test operation should succeed");
738
739        assert_pack_index_view_matches_owned(&written.index, ObjectFormat::Sha256);
740    }
741
742    #[test]
743    fn indexes_existing_sha256_pack_bytes() {
744        let object = EncodedObject::new(ObjectType::Blob, b"index raw sha256 pack\n".to_vec());
745        let written =
746            PackFile::write_undeltified(std::slice::from_ref(&object), ObjectFormat::Sha256)
747                .expect("test operation should succeed");
748
749        let indexed = PackIndex::write_v2_for_pack(&written.pack, ObjectFormat::Sha256)
750            .expect("test operation should succeed");
751        let index = PackIndex::parse(&indexed.index, ObjectFormat::Sha256)
752            .expect("test operation should succeed");
753
754        assert_eq!(indexed.pack_checksum, written.checksum);
755        assert_eq!(indexed.entries, written.entries);
756        assert_eq!(index.pack_checksum, written.checksum);
757        assert_eq!(index.entries, written.entries);
758    }
759
760    #[test]
761    fn indexes_existing_delta_pack_bytes() {
762        let (base, changed) = similar_blob_objects();
763        let options = delta_pack_options(true);
764        let written = PackFile::write_packed_with_options(
765            &[base, changed.clone()],
766            ObjectFormat::Sha1,
767            &options,
768        )
769        .expect("test operation should succeed");
770
771        let indexed = PackIndex::write_v2_for_pack(&written.pack, ObjectFormat::Sha1)
772            .expect("test operation should succeed");
773        let index =
774            PackIndex::parse_v2_sha1(&indexed.index).expect("test operation should succeed");
775        let changed_oid = changed
776            .object_id(ObjectFormat::Sha1)
777            .expect("test operation should succeed");
778
779        assert_eq!(indexed.pack_checksum, written.checksum);
780        assert_eq!(indexed.entries, written.entries);
781        assert_eq!(
782            index
783                .find(&changed_oid)
784                .expect("test operation should succeed")
785                .offset,
786            written.entries[1].offset
787        );
788        assert_eq!(
789            index
790                .find(&changed_oid)
791                .expect("test operation should succeed")
792                .crc32,
793            written.entries[1].crc32
794        );
795    }
796
797    #[test]
798    fn git_generated_pack_is_identical_with_one_and_many_workers() {
799        let root = unique_temp_dir("parallel-git-pack");
800        let repository = root.join("repo");
801        fs::create_dir_all(&repository).expect("create git fixture directory");
802        run_git_success(&repository, &["init", "-q"]);
803        run_git_success(&repository, &["config", "user.name", "Sley Test"]);
804        run_git_success(
805            &repository,
806            &["config", "user.email", "sley@example.invalid"],
807        );
808        for index in 0..64 {
809            let body = format!("git generated pack object {index}\n").repeat(128);
810            fs::write(repository.join(format!("object-{index:03}.txt")), body)
811                .expect("write git fixture object");
812        }
813        run_git_success(&repository, &["add", "."]);
814        run_git_success(&repository, &["commit", "-q", "-m", "pack fixture"]);
815        for index in 0..64 {
816            let path = repository.join(format!("object-{index:03}.txt"));
817            let mut body = fs::read(&path).expect("read git fixture object");
818            body.extend_from_slice(format!("second version {index}\n").as_bytes());
819            fs::write(path, body).expect("update git fixture object");
820        }
821        run_git_success(&repository, &["add", "."]);
822        run_git_success(&repository, &["commit", "-q", "-m", "deltified fixture"]);
823        run_git_success(&repository, &["gc", "--aggressive", "--prune=now"]);
824
825        let pack_dir = repository.join(".git/objects/pack");
826        let pack_path = largest_path_with_extension(&pack_dir, "pack");
827        let pack = fs::read(&pack_path).expect("read git-generated pack");
828        let git_index = fs::read(pack_path.with_extension("idx")).expect("read git index");
829        let one = PackIndex::write_v2_for_pack_with_options(
830            &pack,
831            ObjectFormat::Sha1,
832            |_| Ok(None),
833            PackIndexOptions::default().with_threads(1),
834            CancelFlag::never(),
835            |_| {},
836        )
837        .expect("index git pack with one worker");
838        let many = PackIndex::write_v2_for_pack_with_options(
839            &pack,
840            ObjectFormat::Sha1,
841            |_| Ok(None),
842            PackIndexOptions::default().with_threads(64),
843            CancelFlag::never(),
844            |_| {},
845        )
846        .expect("index git pack with many workers");
847        assert_eq!(many, one);
848        assert_eq!(many.index, git_index);
849        fs::remove_dir_all(root).expect("remove git fixture");
850    }
851
852    #[test]
853    fn parallel_index_rejects_self_referential_ref_delta_with_valid_checksum() {
854        let format = ObjectFormat::Sha1;
855        let mut pack = ref_delta_chain_pack(format, 1, false);
856        let descriptors = pack_entry_descriptors(&pack, format);
857        let delta_offset = descriptors[1].offset as usize;
858        let mut base_oid_offset = delta_offset;
859        let header = parse_entry_header(&pack, &mut base_oid_offset).expect("delta header");
860        assert_eq!(header.kind, PackObjectKind::RefDelta);
861        let self_oid = sley_core::object_id_for_bytes(format, "blob", &chain_bodies(1)[1])
862            .expect("self object id");
863        let base_oid_end = base_oid_offset + format.raw_len();
864        pack[base_oid_offset..base_oid_end].copy_from_slice(self_oid.as_bytes());
865        let trailer_offset = pack.len() - format.raw_len();
866        let checksum = sley_core::digest_bytes(format, &pack[..trailer_offset])
867            .expect("recompute valid pack checksum");
868        pack[trailer_offset..].copy_from_slice(checksum.as_bytes());
869
870        let error = PackIndex::write_v2_for_pack_with_options(
871            &pack,
872            format,
873            |_| Ok(None),
874            PackIndexOptions::default().with_threads(32),
875            CancelFlag::never(),
876            |_| {},
877        )
878        .expect_err("a self-referential delta chain must be rejected");
879        assert!(
880            error.to_string().contains("cyclic") || error.to_string().contains("unresolved"),
881            "unexpected chain error: {error}"
882        );
883    }
884
885    #[test]
886    fn writes_ref_delta_pack_and_index_that_round_trip() {
887        let (base, changed) = similar_blob_objects();
888        let options = delta_pack_options(false);
889        let written = PackFile::write_packed_with_options(
890            &[base.clone(), changed.clone()],
891            ObjectFormat::Sha1,
892            &options,
893        )
894        .expect("test operation should succeed");
895        let mut second_offset = written.entries[1].offset as usize;
896        let header = parse_entry_header(&written.pack, &mut second_offset)
897            .expect("test operation should succeed");
898        assert_eq!(header.kind, PackObjectKind::RefDelta);
899
900        let pack = PackFile::parse_sha1(&written.pack).expect("test operation should succeed");
901        let index =
902            PackIndex::parse_v2_sha1(&written.index).expect("test operation should succeed");
903        let oid = changed
904            .object_id(ObjectFormat::Sha1)
905            .expect("test operation should succeed");
906        assert_eq!(pack.entries[0].object, base);
907        assert_eq!(pack.entries[1].object, changed);
908        assert_eq!(index.pack_checksum, pack.checksum);
909        assert_eq!(
910            index
911                .find(&oid)
912                .expect("test operation should succeed")
913                .offset,
914            written.entries[1].offset
915        );
916    }
917
918    #[test]
919    fn read_object_at_matches_full_parse_for_ofs_delta_pack() {
920        let (base, changed) = similar_blob_objects();
921        let options = delta_pack_options(true);
922        let written =
923            PackFile::write_packed_with_options(&[base, changed], ObjectFormat::Sha1, &options)
924                .expect("test operation should succeed");
925        // Ensure the pack genuinely contains an ofs-delta (else the test is vacuous).
926        let mut second = written.entries[1].offset as usize;
927        assert_eq!(
928            parse_entry_header(&written.pack, &mut second)
929                .expect("test operation should succeed")
930                .kind,
931            PackObjectKind::OfsDelta
932        );
933        // Ground truth from a full parse; single-object decode must match at every offset.
934        let parsed = PackFile::parse_sha1(&written.pack).expect("test operation should succeed");
935        for po in &parsed.entries {
936            let got =
937                read_object_at_arc(&written.pack, po.entry.offset, ObjectFormat::Sha1, |_| {
938                    Ok(None)
939                })
940                .expect("test operation should succeed");
941            assert_eq!(*got, po.object, "offset {}", po.entry.offset);
942        }
943    }
944
945    /// A [`HeaderTypeCache`] over a plain map, for asserting the cached header
946    /// read is byte-identical to the uncached one cold and warm (sley#26).
947    #[derive(Default)]
948    struct MapHeaderTypeCache(HashMap<u64, PackObjectHeader>);
949
950    impl HeaderTypeCache for MapHeaderTypeCache {
951        fn get(&self, pack_offset: u64) -> Option<PackObjectHeader> {
952            self.0.get(&pack_offset).copied()
953        }
954        fn put(&mut self, pack_offset: u64, header: PackObjectHeader) {
955            self.0.insert(pack_offset, header);
956        }
957    }
958
959    #[test]
960    fn read_object_header_at_cached_matches_uncached_cold_and_warm_for_ofs_delta() {
961        let (base, changed) = similar_blob_objects();
962        let options = delta_pack_options(true);
963        let written =
964            PackFile::write_packed_with_options(&[base, changed], ObjectFormat::Sha1, &options)
965                .expect("test operation should succeed");
966        // Ensure the pack genuinely contains an ofs-delta (else the test is vacuous).
967        let mut second = written.entries[1].offset as usize;
968        assert_eq!(
969            parse_entry_header(&written.pack, &mut second)
970                .expect("test operation should succeed")
971                .kind,
972            PackObjectKind::OfsDelta
973        );
974
975        let parsed = PackFile::parse_sha1(&written.pack).expect("test operation should succeed");
976        let mut cache = MapHeaderTypeCache::default();
977        for po in &parsed.entries {
978            let uncached = read_object_header_at(
979                &written.pack,
980                po.entry.offset,
981                ObjectFormat::Sha1,
982                0,
983                |_, _| Ok(None),
984            )
985            .expect("test operation should succeed");
986            // Type inherited from the chain base; size is the inflated body length.
987            assert_eq!(
988                uncached.type_and_size(),
989                (po.object.object_type, po.object.body.len() as u64),
990                "uncached header at offset {}",
991                po.entry.offset
992            );
993            // Cold cache: must agree with the uncached read and populate the memo.
994            let cold = read_object_header_at_with_cache(
995                &written.pack,
996                po.entry.offset,
997                ObjectFormat::Sha1,
998                0,
999                |_, _| Ok(None),
1000                &mut cache,
1001            )
1002            .expect("test operation should succeed");
1003            assert_eq!(cold, uncached, "cold cache at offset {}", po.entry.offset);
1004        }
1005        // Warm cache: every offset now resolves from the memo and is still correct,
1006        // proving the fast path does not change behavior (sley#26).
1007        for po in &parsed.entries {
1008            let warm = read_object_header_at_with_cache(
1009                &written.pack,
1010                po.entry.offset,
1011                ObjectFormat::Sha1,
1012                0,
1013                |_, _| panic!("warm cache must not re-walk the chain"),
1014                &mut cache,
1015            )
1016            .expect("test operation should succeed");
1017            assert_eq!(
1018                warm.type_and_size(),
1019                (po.object.object_type, po.object.body.len() as u64),
1020                "warm cache at offset {}",
1021                po.entry.offset
1022            );
1023        }
1024    }
1025
1026    #[test]
1027    fn read_object_at_matches_full_parse_for_ref_delta_pack() {
1028        let (base, changed) = similar_blob_objects();
1029        let options = delta_pack_options(false);
1030        let written =
1031            PackFile::write_packed_with_options(&[base, changed], ObjectFormat::Sha1, &options)
1032                .expect("test operation should succeed");
1033        let parsed = PackFile::parse_sha1(&written.pack).expect("test operation should succeed");
1034        let by_oid: HashMap<ObjectId, Arc<EncodedObject>> = parsed
1035            .entries
1036            .iter()
1037            .map(|po| (po.entry.oid, Arc::new(po.object.clone())))
1038            .collect();
1039        for po in &parsed.entries {
1040            let got =
1041                read_object_at_arc(&written.pack, po.entry.offset, ObjectFormat::Sha1, |oid| {
1042                    Ok(by_oid.get(oid).cloned())
1043                })
1044                .expect("test operation should succeed");
1045            assert_eq!(*got, po.object);
1046        }
1047    }
1048
1049    /// A test-only [`PackDeltaCache`] that records every decode and counts hits,
1050    /// used to prove the cached decode path is byte-identical to the uncached
1051    /// one and that bases are reused across reads.
1052    #[derive(Default)]
1053    struct CountingDeltaCache {
1054        map: std::cell::RefCell<HashMap<u64, Arc<EncodedObject>>>,
1055        hits: std::cell::Cell<usize>,
1056        inserts: std::cell::Cell<usize>,
1057    }
1058
1059    impl PackDeltaCache for CountingDeltaCache {
1060        fn get(&self, offset: u64) -> Option<Arc<EncodedObject>> {
1061            let hit = self.map.borrow().get(&offset).cloned();
1062            if hit.is_some() {
1063                self.hits.set(self.hits.get() + 1);
1064            }
1065            hit
1066        }
1067        fn insert(&self, offset: u64, object: Arc<EncodedObject>) {
1068            self.inserts.set(self.inserts.get() + 1);
1069            self.map.borrow_mut().insert(offset, object);
1070        }
1071    }
1072
1073    #[test]
1074    fn read_object_at_with_cache_matches_uncached_and_reuses_bases() {
1075        // A multi-object pack with a real ofs-delta chain so the cache has bases
1076        // to reuse. Build several similar blobs to encourage deltification.
1077        let mut objects = Vec::new();
1078        for idx in 0..8u32 {
1079            let mut body = vec![b'x'; 4096];
1080            body.extend_from_slice(format!("\nvariant {idx}\n").as_bytes());
1081            objects.push(EncodedObject::new(ObjectType::Blob, body));
1082        }
1083        let options = delta_pack_options(true);
1084        let written = PackFile::write_packed_with_options(&objects, ObjectFormat::Sha1, &options)
1085            .expect("test operation should succeed");
1086        let parsed = PackFile::parse_sha1(&written.pack).expect("test operation should succeed");
1087
1088        let cache = CountingDeltaCache::default();
1089        // Read every object twice through the cache; each result must equal the
1090        // ground-truth from the full parse, byte for byte, both times.
1091        for _ in 0..2 {
1092            for po in &parsed.entries {
1093                let got = read_object_at_with_cache_arc(
1094                    &written.pack,
1095                    po.entry.offset,
1096                    ObjectFormat::Sha1,
1097                    |_| Ok(None),
1098                    &cache,
1099                )
1100                .expect("test operation should succeed");
1101                assert_eq!(*got, po.object, "offset {}", po.entry.offset);
1102            }
1103        }
1104        // The second pass reads everything straight from the cache, so there must
1105        // be at least one hit (proving reuse, not just correctness).
1106        assert!(cache.hits.get() > 0, "cache never served a warm object");
1107    }
1108
1109    #[test]
1110    fn writes_ofs_delta_pack_and_index_that_round_trip() {
1111        let (base, changed) = similar_blob_objects();
1112        let options = delta_pack_options(true);
1113        let written = PackFile::write_packed_with_options(
1114            &[base.clone(), changed.clone()],
1115            ObjectFormat::Sha1,
1116            &options,
1117        )
1118        .expect("test operation should succeed");
1119        let mut second_offset = written.entries[1].offset as usize;
1120        let header = parse_entry_header(&written.pack, &mut second_offset)
1121            .expect("test operation should succeed");
1122        assert_eq!(header.kind, PackObjectKind::OfsDelta);
1123
1124        let pack = PackFile::parse_sha1(&written.pack).expect("test operation should succeed");
1125        let index =
1126            PackIndex::parse_v2_sha1(&written.index).expect("test operation should succeed");
1127        let oid = changed
1128            .object_id(ObjectFormat::Sha1)
1129            .expect("test operation should succeed");
1130        assert_eq!(pack.entries[0].object, base);
1131        assert_eq!(pack.entries[1].object, changed);
1132        assert_eq!(index.pack_checksum, pack.checksum);
1133        assert_eq!(
1134            index
1135                .find(&oid)
1136                .expect("test operation should succeed")
1137                .offset,
1138            written.entries[1].offset
1139        );
1140    }
1141
1142    #[test]
1143    fn resolves_ofs_delta_pack_entry() {
1144        let base = b"hello";
1145        let result = b"hello world";
1146        let pack = two_object_delta_pack(ObjectFormat::Sha1, base, result, DeltaKind::Offset);
1147        let parsed = PackFile::parse_sha1(&pack).expect("test operation should succeed");
1148        assert_eq!(parsed.entries.len(), 2);
1149        assert_eq!(parsed.entries[0].object.body, base);
1150        assert_eq!(parsed.entries[1].object.body, result);
1151        assert_eq!(
1152            parsed.entries[1].entry.oid,
1153            sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", result)
1154                .expect("test operation should succeed")
1155        );
1156    }
1157
1158    #[test]
1159    fn resolves_ref_delta_pack_entry() {
1160        let base = b"hello";
1161        let result = b"hello world";
1162        let pack = two_object_delta_pack(ObjectFormat::Sha1, base, result, DeltaKind::Ref);
1163        let parsed = PackFile::parse_sha1(&pack).expect("test operation should succeed");
1164        assert_eq!(parsed.entries.len(), 2);
1165        assert_eq!(parsed.entries[0].object.body, base);
1166        assert_eq!(parsed.entries[1].object.body, result);
1167        assert_eq!(
1168            parsed.entries[1].entry.oid,
1169            sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", result)
1170                .expect("test operation should succeed")
1171        );
1172    }
1173
1174    #[test]
1175    fn resolves_thin_ref_delta_pack_entry_with_external_base() {
1176        let base = b"hello";
1177        let result = b"hello world";
1178        let pack = thin_ref_delta_pack(ObjectFormat::Sha1, base, result);
1179        assert!(PackFile::parse_sha1(&pack).is_err());
1180
1181        let base_oid = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", base)
1182            .expect("test operation should succeed");
1183        let parsed = PackFile::parse_thin(&pack, ObjectFormat::Sha1, |oid| {
1184            if oid == &base_oid {
1185                Ok(Some(EncodedObject::new(ObjectType::Blob, base.to_vec())))
1186            } else {
1187                Ok(None)
1188            }
1189        })
1190        .expect("test operation should succeed");
1191        assert_eq!(parsed.entries.len(), 1);
1192        assert_eq!(parsed.entries[0].object.body, result);
1193        assert_eq!(
1194            parsed.entries[0].entry.oid,
1195            sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", result)
1196                .expect("test operation should succeed")
1197        );
1198    }
1199
1200    #[test]
1201    fn fixes_thin_pack_and_indexes_it_without_external_bases() {
1202        for format in [ObjectFormat::Sha1, ObjectFormat::Sha256] {
1203            let base = b"hello";
1204            let result = b"hello world";
1205            let pack = thin_ref_delta_pack(format, base, result);
1206            let base_object = EncodedObject::new(ObjectType::Blob, base.to_vec());
1207            let base_oid = base_object
1208                .object_id(format)
1209                .expect("test operation should succeed");
1210
1211            let fixed = fix_thin_pack(&pack, format, |oid| {
1212                Ok((oid == &base_oid).then(|| base_object.clone()))
1213            })
1214            .expect("thin pack completion should succeed");
1215
1216            assert_eq!(u32_be(&fixed.pack[8..12]), 2);
1217            assert_eq!(fixed.appended_bases, vec![base_oid]);
1218            assert_eq!(fixed.index.entries.len(), 2);
1219            let parsed =
1220                PackFile::parse(&fixed.pack, format).expect("completed pack should stand alone");
1221            assert_eq!(parsed.entries.len(), 2);
1222            assert_eq!(parsed.entries[0].object.body, result);
1223            assert_eq!(parsed.entries[1].object, base_object);
1224            let rebuilt = PackIndex::write_v2_for_pack(&fixed.pack, format)
1225                .expect("completed pack should index without a resolver");
1226            assert_eq!(rebuilt, fixed.index);
1227        }
1228    }
1229
1230    #[test]
1231    fn fix_thin_is_idempotent_for_a_complete_pack() {
1232        let original = single_object_pack(ObjectFormat::Sha1, ObjectType::Blob, b"complete\n");
1233        let mut resolver_called = false;
1234        let fixed = fix_thin_pack(&original, ObjectFormat::Sha1, |_| {
1235            resolver_called = true;
1236            Ok(None)
1237        })
1238        .expect("complete pack should remain complete");
1239
1240        assert_eq!(fixed.pack, original);
1241        assert!(fixed.appended_bases.is_empty());
1242        assert!(!resolver_called);
1243        assert_eq!(fixed.index.entries.len(), 1);
1244    }
1245
1246    #[test]
1247    fn fix_thin_does_not_duplicate_a_base_already_in_the_body() {
1248        let pack =
1249            two_object_delta_pack(ObjectFormat::Sha1, b"hello", b"hello world", DeltaKind::Ref);
1250        let original_count = u32_be(&pack[8..12]);
1251        let mut resolver_called = false;
1252        let fixed = fix_thin_pack(&pack, ObjectFormat::Sha1, |_| {
1253            resolver_called = true;
1254            Ok(None)
1255        })
1256        .expect("in-pack ref base should remain self-contained");
1257
1258        assert_eq!(fixed.pack, pack);
1259        assert_eq!(u32_be(&fixed.pack[8..12]), original_count);
1260        assert_eq!(fixed.index.entries.len(), original_count as usize);
1261        assert!(fixed.appended_bases.is_empty());
1262        assert!(!resolver_called);
1263    }
1264
1265    #[test]
1266    fn fix_thin_appends_a_repeated_external_base_once() {
1267        let base = b"shared";
1268        let pack =
1269            thin_ref_delta_pack_many(ObjectFormat::Sha1, base, &[b"shared one", b"shared two"]);
1270        let base_object = EncodedObject::new(ObjectType::Blob, base.to_vec());
1271        let base_oid = base_object
1272            .object_id(ObjectFormat::Sha1)
1273            .expect("test operation should succeed");
1274        let mut calls = 0;
1275        let fixed = fix_thin_pack(&pack, ObjectFormat::Sha1, |oid| {
1276            calls += 1;
1277            Ok((oid == &base_oid).then(|| base_object.clone()))
1278        })
1279        .expect("shared base completion should succeed");
1280
1281        assert_eq!(calls, 1);
1282        assert_eq!(fixed.appended_bases, vec![base_oid]);
1283        assert_eq!(u32_be(&fixed.pack[8..12]), 3);
1284        assert_eq!(fixed.index.entries.len(), 3);
1285    }
1286
1287    #[test]
1288    fn fix_thin_rejects_missing_and_wrong_external_bases() {
1289        let base = b"hello";
1290        let pack = thin_ref_delta_pack(ObjectFormat::Sha1, base, b"hello world");
1291        assert!(fix_thin_pack(&pack, ObjectFormat::Sha1, |_| Ok(None)).is_err());
1292
1293        let wrong = EncodedObject::new(ObjectType::Blob, b"wrong".to_vec());
1294        let error = fix_thin_pack(&pack, ObjectFormat::Sha1, |_| Ok(Some(wrong.clone())))
1295            .expect_err("wrong base body must not be accepted for the requested oid");
1296        assert!(error.to_string().contains("resolved to object"));
1297    }
1298
1299    #[test]
1300    fn fix_thin_preserves_the_delta_depth_limit_boundary() {
1301        let base = b"hello";
1302        let pack = thin_ref_delta_pack(ObjectFormat::Sha1, base, b"hello world");
1303        let base_object = EncodedObject::new(ObjectType::Blob, base.to_vec());
1304        let base_oid = base_object
1305            .object_id(ObjectFormat::Sha1)
1306            .expect("test operation should succeed");
1307        let resolve = |oid: &ObjectId| Ok((oid == &base_oid).then(|| base_object.clone()));
1308
1309        fix_thin_pack_with_limits(
1310            &pack,
1311            ObjectFormat::Sha1,
1312            resolve,
1313            PackReadLimits {
1314                max_delta_depth: 1,
1315                ..PackReadLimits::default()
1316            },
1317        )
1318        .expect("one delta must pass at the exact limit");
1319
1320        let error = fix_thin_pack_with_limits(
1321            &pack,
1322            ObjectFormat::Sha1,
1323            resolve,
1324            PackReadLimits {
1325                max_delta_depth: 0,
1326                ..PackReadLimits::default()
1327            },
1328        )
1329        .expect_err("one delta must fail immediately above the configured limit");
1330        assert!(error.to_string().contains("observed depth 1"));
1331    }
1332
1333    #[test]
1334    fn parallel_indexer_resolves_thin_bases_but_rejects_missing_ones() {
1335        let base = b"hello";
1336        let result = b"hello stream";
1337        let pack = thin_ref_delta_pack(ObjectFormat::Sha1, base, result);
1338        let base_object = EncodedObject::new(ObjectType::Blob, base.to_vec());
1339        let base_oid = base_object
1340            .object_id(ObjectFormat::Sha1)
1341            .expect("test operation should succeed");
1342
1343        let built = PackIndex::write_v2_for_pack_with_base(&pack, ObjectFormat::Sha1, |oid| {
1344            Ok((oid == &base_oid).then(|| base_object.clone()))
1345        })
1346        .expect("parallel indexer should resolve the supplied base");
1347        assert_eq!(built.entries.len(), 1);
1348        assert_eq!(
1349            built.entries[0].oid,
1350            sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", result)
1351                .expect("test operation should succeed")
1352        );
1353
1354        let error = PackIndex::write_v2_for_pack_with_base(&pack, ObjectFormat::Sha1, |_| Ok(None))
1355            .expect_err("an unresolved base must remain an error");
1356        assert!(error.to_string().contains("unresolved"));
1357    }
1358
1359    #[test]
1360    fn fixed_pack_passes_upstream_git_and_supports_clone() {
1361        let format = ObjectFormat::Sha1;
1362        let base = EncodedObject::new(
1363            ObjectType::Blob,
1364            b"the original contents are deliberately similar\n".repeat(32),
1365        );
1366        let mut target_body = base.body.clone();
1367        target_body.extend_from_slice(b"completed thin pack\n");
1368        let target = EncodedObject::new(ObjectType::Blob, target_body);
1369        let target_oid = target
1370            .object_id(format)
1371            .expect("test operation should succeed");
1372
1373        let mut tree_body = b"100644 payload\0".to_vec();
1374        tree_body.extend_from_slice(target_oid.as_bytes());
1375        let tree = EncodedObject::new(ObjectType::Tree, tree_body);
1376        let tree_oid = tree
1377            .object_id(format)
1378            .expect("test operation should succeed");
1379        let commit = EncodedObject::new(
1380            ObjectType::Commit,
1381            format!(
1382                "tree {tree_oid}\nauthor Sley <sley@example.com> 1 +0000\n\
1383                 committer Sley <sley@example.com> 1 +0000\n\nthin pack fixture\n"
1384            )
1385            .into_bytes(),
1386        );
1387        let commit_oid = commit
1388            .object_id(format)
1389            .expect("test operation should succeed");
1390        let base_oid = base
1391            .object_id(format)
1392            .expect("test operation should succeed");
1393        let thin = PackFile::write_thin(
1394            &[commit, tree, target],
1395            format,
1396            HashMap::from([(base_oid, base.clone())]),
1397        )
1398        .expect("write thin pack");
1399        assert!(thin.delta_count > 0, "fixture must really be thin");
1400        assert!(PackIndex::write_v2_for_pack(&thin.pack, format).is_err());
1401
1402        let fixed = fix_thin_pack(&thin.pack, format, |oid| {
1403            Ok((oid == &base_oid).then(|| base.clone()))
1404        })
1405        .expect("fix thin pack");
1406        let root = unique_temp_dir("fix-thin-git");
1407        let strict_pack = root.join("strict.pack");
1408        fs::create_dir_all(&root).expect("create test root");
1409        fs::write(&strict_pack, &fixed.pack).expect("write strict pack");
1410        run_git_success(&root, &["index-pack", "--strict", "strict.pack"]);
1411
1412        let complete_repo = root.join("complete.git");
1413        run_git_success(
1414            &root,
1415            &[
1416                "init",
1417                "--bare",
1418                "-q",
1419                complete_repo.to_str().expect("utf8 complete repo path"),
1420            ],
1421        );
1422        let complete_pack_dir = complete_repo.join("objects/pack");
1423        let complete_stem = format!("pack-{}", fixed.index.pack_checksum);
1424        fs::write(
1425            complete_pack_dir.join(format!("{complete_stem}.pack")),
1426            &fixed.pack,
1427        )
1428        .expect("write completed repository pack");
1429        fs::write(
1430            complete_pack_dir.join(format!("{complete_stem}.idx")),
1431            &fixed.index.index,
1432        )
1433        .expect("write completed repository index");
1434        fs::create_dir_all(complete_repo.join("refs/heads")).expect("create refs");
1435        fs::write(
1436            complete_repo.join("refs/heads/main"),
1437            format!("{commit_oid}\n"),
1438        )
1439        .expect("write main ref");
1440        fs::write(complete_repo.join("HEAD"), b"ref: refs/heads/main\n").expect("write HEAD");
1441        run_git_success(&complete_repo, &["fsck", "--full"]);
1442
1443        let clone_path = root.join("clone");
1444        run_git_success(
1445            &root,
1446            &[
1447                "clone",
1448                "--no-local",
1449                "-q",
1450                complete_repo.to_str().expect("utf8 complete repo path"),
1451                clone_path.to_str().expect("utf8 clone path"),
1452            ],
1453        );
1454        run_git_success(&clone_path, &["fsck", "--full"]);
1455
1456        let thin_repo = root.join("thin.git");
1457        run_git_success(
1458            &root,
1459            &[
1460                "init",
1461                "--bare",
1462                "-q",
1463                thin_repo.to_str().expect("utf8 thin repo path"),
1464            ],
1465        );
1466        let thin_index = PackIndex::write_v2_for_pack_with_base(&thin.pack, format, |oid| {
1467            Ok((oid == &base_oid).then(|| base.clone()))
1468        })
1469        .expect("sley can build the deliberately permissive thin index");
1470        let thin_pack_dir = thin_repo.join("objects/pack");
1471        let thin_stem = format!("pack-{}", thin_index.pack_checksum);
1472        fs::write(thin_pack_dir.join(format!("{thin_stem}.pack")), &thin.pack)
1473            .expect("write thin repository pack");
1474        fs::write(
1475            thin_pack_dir.join(format!("{thin_stem}.idx")),
1476            &thin_index.index,
1477        )
1478        .expect("write thin repository index");
1479        fs::create_dir_all(thin_repo.join("refs/heads")).expect("create thin refs");
1480        fs::write(thin_repo.join("refs/heads/main"), format!("{commit_oid}\n"))
1481            .expect("write thin main ref");
1482        fs::write(thin_repo.join("HEAD"), b"ref: refs/heads/main\n").expect("write thin HEAD");
1483        let broken_clone_path = root.join("broken-clone");
1484        let failed_clone = Command::new("git")
1485            .current_dir(&root)
1486            .args([
1487                "clone",
1488                "--no-local",
1489                "-q",
1490                thin_repo.to_str().expect("utf8 thin repo path"),
1491                broken_clone_path.to_str().expect("utf8 broken clone path"),
1492            ])
1493            .output()
1494            .expect("run failing clone");
1495        assert!(
1496            !failed_clone.status.success(),
1497            "clone from uncompleted thin pack unexpectedly succeeded"
1498        );
1499
1500        fs::remove_dir_all(root).expect("remove git interoperability fixture");
1501    }
1502
1503    #[test]
1504    fn rejects_bad_pack_checksum() {
1505        let mut pack = single_object_pack(ObjectFormat::Sha1, ObjectType::Blob, b"hello\n");
1506        let last = pack.len() - 1;
1507        pack[last] ^= 1;
1508        assert!(PackFile::parse_sha1(&pack).is_err());
1509    }
1510
1511    #[test]
1512    fn raw_pack_index_rejects_bad_pack_checksum() {
1513        let mut pack = single_object_pack(ObjectFormat::Sha1, ObjectType::Blob, b"hello\n");
1514        let last = pack.len() - 1;
1515        pack[last] ^= 1;
1516        assert!(PackIndex::write_v2_for_pack(&pack, ObjectFormat::Sha1).is_err());
1517    }
1518
1519    #[test]
1520    fn pack_index_writer_preserves_duplicate_object_ids() {
1521        let oid = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"same\n")
1522            .expect("test operation should succeed");
1523        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1524            .expect("test operation should succeed");
1525        let entries = vec![
1526            PackIndexEntry {
1527                oid,
1528                crc32: 1,
1529                offset: 12,
1530            },
1531            PackIndexEntry {
1532                oid,
1533                crc32: 2,
1534                offset: 24,
1535            },
1536        ];
1537        let index = PackIndex::write_v2(ObjectFormat::Sha1, &entries, &pack_checksum)
1538            .expect("duplicate objects are valid in a pack index");
1539        let parsed = PackIndex::parse(&index, ObjectFormat::Sha1)
1540            .expect("duplicate-object pack index should parse");
1541        assert_eq!(parsed.entries.len(), 2);
1542        assert!(parsed.entries.iter().all(|entry| entry.oid == oid));
1543        assert!(parsed.find(&oid).is_some());
1544    }
1545
1546    #[test]
1547    fn parses_single_entry_pack_index() {
1548        let oid = ObjectId::from_hex(
1549            ObjectFormat::Sha1,
1550            "ce013625030ba8dba906f756967f9e9ca394464a",
1551        )
1552        .expect("test operation should succeed");
1553        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1554            .expect("test operation should succeed");
1555        let index = single_entry_index(
1556            ObjectFormat::Sha1,
1557            oid,
1558            0x1234_5678,
1559            12,
1560            pack_checksum.clone(),
1561        );
1562        let parsed = PackIndex::parse_v2_sha1(&index).expect("test operation should succeed");
1563        assert_eq!(parsed.version, 2);
1564        assert_eq!(parsed.pack_checksum, pack_checksum);
1565        assert_eq!(parsed.entries.len(), 1);
1566        assert_eq!(
1567            parsed
1568                .find(&oid)
1569                .expect("test operation should succeed")
1570                .offset,
1571            12
1572        );
1573        assert_eq!(
1574            parsed
1575                .find(&oid)
1576                .expect("test operation should succeed")
1577                .crc32,
1578            0x1234_5678
1579        );
1580        assert_pack_index_view_matches_owned(&index, ObjectFormat::Sha1);
1581    }
1582
1583    #[test]
1584    fn parses_single_entry_pack_index_v1() {
1585        let oid = ObjectId::from_hex(
1586            ObjectFormat::Sha1,
1587            "ce013625030ba8dba906f756967f9e9ca394464a",
1588        )
1589        .expect("test operation should succeed");
1590        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1591            .expect("test operation should succeed");
1592        let index =
1593            single_entry_index_v1(ObjectFormat::Sha1, oid, 0x1234_5678, pack_checksum.clone());
1594        let parsed =
1595            PackIndex::parse(&index, ObjectFormat::Sha1).expect("test operation should succeed");
1596        assert_eq!(parsed.version, 1);
1597        assert_eq!(parsed.pack_checksum, pack_checksum);
1598        assert_eq!(parsed.entries.len(), 1);
1599        assert_eq!(
1600            parsed
1601                .find(&oid)
1602                .expect("test operation should succeed")
1603                .offset,
1604            0x1234_5678
1605        );
1606        assert_eq!(
1607            parsed
1608                .find(&oid)
1609                .expect("test operation should succeed")
1610                .crc32,
1611            0
1612        );
1613        assert_pack_index_view_matches_owned(&index, ObjectFormat::Sha1);
1614    }
1615
1616    #[test]
1617    fn rejects_bad_pack_index_v1_checksum() {
1618        let oid = ObjectId::from_hex(
1619            ObjectFormat::Sha1,
1620            "ce013625030ba8dba906f756967f9e9ca394464a",
1621        )
1622        .expect("test operation should succeed");
1623        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1624            .expect("test operation should succeed");
1625        let mut index = single_entry_index_v1(ObjectFormat::Sha1, oid, 12, pack_checksum);
1626        let last = index.len() - 1;
1627        index[last] ^= 1;
1628        assert!(PackIndex::parse(&index, ObjectFormat::Sha1).is_err());
1629    }
1630
1631    #[test]
1632    fn pack_index_view_reads_v2_large_offsets() {
1633        let first = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"large offset a\n")
1634            .expect("test operation should succeed");
1635        let second =
1636            sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"large offset b\n")
1637                .expect("test operation should succeed");
1638        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1639            .expect("test operation should succeed");
1640        let entries = vec![
1641            PackIndexEntry {
1642                oid: first,
1643                crc32: 0x1111_2222,
1644                offset: 0x8000_0000,
1645            },
1646            PackIndexEntry {
1647                oid: second,
1648                crc32: 0x3333_4444,
1649                offset: 0x1_0000_0042,
1650            },
1651        ];
1652        let index = PackIndex::write_v2(ObjectFormat::Sha1, &entries, &pack_checksum)
1653            .expect("test operation should succeed");
1654
1655        assert_pack_index_view_matches_owned(&index, ObjectFormat::Sha1);
1656        let view = PackIndexView::parse(&index, ObjectFormat::Sha1)
1657            .expect("test operation should succeed");
1658        for entry in entries {
1659            assert_eq!(
1660                view.find(&entry.oid),
1661                Some(PackIndexLookup {
1662                    crc32: entry.crc32,
1663                    offset: entry.offset,
1664                })
1665            );
1666        }
1667    }
1668
1669    #[test]
1670    fn pack_index_view_default_parse_checks_index_checksum() {
1671        let oid = ObjectId::from_hex(
1672            ObjectFormat::Sha1,
1673            "ce013625030ba8dba906f756967f9e9ca394464a",
1674        )
1675        .expect("test operation should succeed");
1676        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1677            .expect("test operation should succeed");
1678        let mut index = single_entry_index(ObjectFormat::Sha1, oid, 0x1234_5678, 12, pack_checksum);
1679        let last = index.len() - 1;
1680        index[last] ^= 1;
1681
1682        assert!(PackIndexView::parse(&index, ObjectFormat::Sha1).is_err());
1683        let view = PackIndexView::parse_without_checksum(&index, ObjectFormat::Sha1)
1684            .expect("test operation should succeed");
1685        let trusted_view = PackIndexViewData::parse_trusted_without_checksum(
1686            Arc::from(index.clone().into_boxed_slice()),
1687            ObjectFormat::Sha1,
1688        )
1689        .expect("test operation should succeed");
1690        assert_eq!(
1691            view.find(&oid),
1692            Some(PackIndexLookup {
1693                crc32: 0x1234_5678,
1694                offset: 12,
1695            })
1696        );
1697        assert_eq!(
1698            trusted_view.find(&oid),
1699            Some(PackIndexLookup {
1700                crc32: 0x1234_5678,
1701                offset: 12,
1702            })
1703        );
1704    }
1705
1706    #[test]
1707    fn reverse_index_resolves_oid_at_offset() {
1708        let objects = (0..3)
1709            .map(|idx| {
1710                EncodedObject::new(
1711                    ObjectType::Blob,
1712                    format!("reverse index lookup object {idx}\n").into_bytes(),
1713                )
1714            })
1715            .collect::<Vec<_>>();
1716        let written = PackFile::write_packed(&objects, ObjectFormat::Sha1)
1717            .expect("test operation should succeed");
1718        let index = PackIndex::parse(&written.index, ObjectFormat::Sha1)
1719            .expect("test operation should succeed");
1720        let view = PackIndexViewData::parse_trusted_without_checksum(
1721            Arc::from(written.index.into_boxed_slice()),
1722            ObjectFormat::Sha1,
1723        )
1724        .expect("test operation should succeed");
1725        let positions = pack_order_index_positions(&index.entries);
1726        let reverse = PackReverseIndex::parse(
1727            &PackReverseIndex::write(ObjectFormat::Sha1, &positions, &index.pack_checksum)
1728                .expect("test operation should succeed"),
1729            ObjectFormat::Sha1,
1730            index.entries.len(),
1731        )
1732        .expect("test operation should succeed");
1733
1734        for entry in &index.entries {
1735            assert_eq!(
1736                reverse
1737                    .oid_at_offset(&view, entry.offset)
1738                    .expect("test operation should succeed"),
1739                entry.oid
1740            );
1741        }
1742        assert!(reverse.oid_at_offset(&view, 999).is_none());
1743    }
1744
1745    #[test]
1746    fn parses_pack_reverse_index() {
1747        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1748            .expect("test operation should succeed");
1749        let reverse_index = PackReverseIndex::write(ObjectFormat::Sha1, &[2, 0, 1], &pack_checksum)
1750            .expect("test operation should succeed");
1751        let parsed = PackReverseIndex::parse(&reverse_index, ObjectFormat::Sha1, 3)
1752            .expect("test operation should succeed");
1753        assert_eq!(parsed.version, 1);
1754        assert_eq!(parsed.format, ObjectFormat::Sha1);
1755        assert_eq!(parsed.positions, vec![2, 0, 1]);
1756        assert_eq!(parsed.pack_checksum, pack_checksum);
1757        assert_eq!(
1758            PackReverseIndex::write(ObjectFormat::Sha1, &parsed.positions, &parsed.pack_checksum)
1759                .expect("test operation should succeed"),
1760            reverse_index
1761        );
1762    }
1763
1764    #[test]
1765    fn rejects_bad_pack_reverse_index_checksum() {
1766        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1767            .expect("test operation should succeed");
1768        let mut reverse_index = PackReverseIndex::write(ObjectFormat::Sha1, &[0], &pack_checksum)
1769            .expect("test operation should succeed");
1770        let last = reverse_index.len() - 1;
1771        reverse_index[last] ^= 1;
1772        assert!(matches!(
1773            PackReverseIndex::parse(&reverse_index, ObjectFormat::Sha1, 1),
1774            Err(GitError::InvalidFormat(message)) if message == "invalid checksum"
1775        ));
1776    }
1777
1778    #[test]
1779    fn classifies_pack_reverse_index_corruption_for_fsck() {
1780        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1781            .expect("test operation should succeed");
1782        let valid = PackReverseIndex::write(ObjectFormat::Sha1, &[0], &pack_checksum)
1783            .expect("test operation should succeed");
1784        for (offset, value, expected) in [
1785            (1, 7, "unknown signature"),
1786            (7, 2, "unsupported version 2"),
1787            (11, 3, "unsupported hash id 3"),
1788            (14, 7, "invalid rev-index position 1792"),
1789        ] {
1790            let mut corrupt = valid.clone();
1791            corrupt[offset] = value;
1792            assert!(matches!(
1793                PackReverseIndex::parse(&corrupt, ObjectFormat::Sha1, 1),
1794                Err(GitError::InvalidFormat(message)) if message == expected
1795            ));
1796        }
1797    }
1798
1799    #[test]
1800    fn rejects_bad_pack_reverse_index_positions() {
1801        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1802            .expect("test operation should succeed");
1803        let duplicate = pack_reverse_index(ObjectFormat::Sha1, &[0, 0], pack_checksum.clone());
1804        assert!(PackReverseIndex::parse(&duplicate, ObjectFormat::Sha1, 2).is_err());
1805        let out_of_range = pack_reverse_index(ObjectFormat::Sha1, &[0, 2], pack_checksum);
1806        assert!(PackReverseIndex::parse(&out_of_range, ObjectFormat::Sha1, 2).is_err());
1807        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1808            .expect("test operation should succeed");
1809        assert!(PackReverseIndex::write(ObjectFormat::Sha1, &[0, 0], &pack_checksum).is_err());
1810        assert!(PackReverseIndex::write(ObjectFormat::Sha1, &[0, 2], &pack_checksum).is_err());
1811    }
1812
1813    #[test]
1814    fn parses_pack_mtimes() {
1815        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1816            .expect("test operation should succeed");
1817        let mtimes = PackMtimes::write(
1818            ObjectFormat::Sha1,
1819            &[1, 1_700_000_000, u32::MAX],
1820            &pack_checksum,
1821        )
1822        .expect("test operation should succeed");
1823        let parsed = PackMtimes::parse(&mtimes, ObjectFormat::Sha1, 3)
1824            .expect("test operation should succeed");
1825        assert_eq!(parsed.version, 1);
1826        assert_eq!(parsed.format, ObjectFormat::Sha1);
1827        assert_eq!(parsed.mtimes, vec![1, 1_700_000_000, u32::MAX]);
1828        assert_eq!(parsed.pack_checksum, pack_checksum);
1829        assert_eq!(
1830            PackMtimes::write(ObjectFormat::Sha1, &parsed.mtimes, &parsed.pack_checksum)
1831                .expect("test operation should succeed"),
1832            mtimes
1833        );
1834    }
1835
1836    #[test]
1837    fn rejects_bad_pack_mtimes_checksum() {
1838        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1839            .expect("test operation should succeed");
1840        let mut mtimes = PackMtimes::write(ObjectFormat::Sha1, &[1], &pack_checksum)
1841            .expect("test operation should succeed");
1842        let last = mtimes.len() - 1;
1843        mtimes[last] ^= 1;
1844        assert!(PackMtimes::parse(&mtimes, ObjectFormat::Sha1, 1).is_err());
1845    }
1846
1847    #[test]
1848    fn rejects_bad_pack_mtimes_shape() {
1849        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
1850            .expect("test operation should succeed");
1851        let mtimes = pack_mtimes(ObjectFormat::Sha1, &[1, 2], pack_checksum.clone());
1852        assert!(PackMtimes::parse(&mtimes, ObjectFormat::Sha1, 1).is_err());
1853
1854        let mut wrong_hash = pack_mtimes(ObjectFormat::Sha1, &[1], pack_checksum);
1855        wrong_hash[11] = 2;
1856        let checksum_offset = wrong_hash.len() - ObjectFormat::Sha1.raw_len();
1857        let checksum = sley_core::digest_bytes(ObjectFormat::Sha1, &wrong_hash[..checksum_offset])
1858            .expect("test operation should succeed");
1859        wrong_hash[checksum_offset..].copy_from_slice(checksum.as_bytes());
1860        assert!(PackMtimes::parse(&wrong_hash, ObjectFormat::Sha1, 1).is_err());
1861    }
1862
1863    #[test]
1864    fn parses_multi_pack_index_header_and_chunk_lookup() {
1865        let first = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"first object\n")
1866            .expect("test operation should succeed");
1867        let second = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"second object\n")
1868            .expect("test operation should succeed");
1869        let chunks = midx_chunks_with_pack_names(
1870            ObjectFormat::Sha1,
1871            b"pack-a.idx\0pack-b.idx\0\0\0".to_vec(),
1872            &[(first.clone(), 0, 12), (second.clone(), 1, 0x1_0000_0000)],
1873        );
1874        let midx = multi_pack_index(ObjectFormat::Sha1, 2, 2, &chunks);
1875        let parsed = MultiPackIndex::parse(&midx, ObjectFormat::Sha1)
1876            .expect("test operation should succeed");
1877        assert_eq!(parsed.version, 2);
1878        assert_eq!(parsed.format, ObjectFormat::Sha1);
1879        assert_eq!(parsed.pack_count, 2);
1880        assert_eq!(parsed.pack_names, vec!["pack-a.idx", "pack-b.idx"]);
1881        assert_eq!(parsed.object_count, 2);
1882        assert_eq!(parsed.objects.len(), 2);
1883        assert_eq!(
1884            parsed
1885                .find(&first)
1886                .expect("test operation should succeed")
1887                .pack_int_id,
1888            0
1889        );
1890        assert_eq!(
1891            parsed
1892                .find(&first)
1893                .expect("test operation should succeed")
1894                .offset,
1895            12
1896        );
1897        assert_eq!(
1898            parsed
1899                .find(&second)
1900                .expect("test operation should succeed")
1901                .pack_int_id,
1902            1
1903        );
1904        assert_eq!(
1905            parsed
1906                .find(&second)
1907                .expect("test operation should succeed")
1908                .offset,
1909            0x1_0000_0000
1910        );
1911        assert_eq!(parsed.reverse_index, None);
1912        assert_eq!(parsed.bitmapped_packs, None);
1913        assert_eq!(parsed.chunks.len(), 5);
1914        assert_eq!(parsed.chunks[0].id, *b"PNAM");
1915        assert_eq!(parsed.chunks[0].offset, 84);
1916        assert_eq!(parsed.chunks[0].len, 24);
1917        assert_eq!(parsed.chunks[1].id, *b"OIDF");
1918        assert_eq!(parsed.chunks[1].offset, 108);
1919        assert_eq!(parsed.chunks[1].len, 1024);
1920    }
1921
1922    #[test]
1923    fn raw_multi_pack_index_lookup_finds_pack_and_offset() {
1924        let first = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"first object\n")
1925            .expect("test operation should succeed");
1926        let second = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"second object\n")
1927            .expect("test operation should succeed");
1928        let missing = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"missing\n")
1929            .expect("test operation should succeed");
1930        let chunks = midx_chunks_with_pack_names(
1931            ObjectFormat::Sha1,
1932            b"pack-a.idx\0pack-b.idx\0\0\0".to_vec(),
1933            &[(first.clone(), 0, 12), (second.clone(), 1, 0x1_0000_0000)],
1934        );
1935        let midx = Arc::new(multi_pack_index(ObjectFormat::Sha1, 2, 2, &chunks));
1936        let lookup = MultiPackIndexOidLookup::parse(midx, ObjectFormat::Sha1)
1937            .expect("test operation should succeed");
1938
1939        assert!(lookup.contains(&first));
1940        assert!(lookup.contains(&second));
1941        assert!(!lookup.contains(&missing));
1942
1943        let first_entry = lookup
1944            .find(&first)
1945            .expect("test operation should succeed")
1946            .expect("object should be present");
1947        assert_eq!(
1948            lookup.pack_name(first_entry.pack_int_id),
1949            Some("pack-a.idx")
1950        );
1951        assert_eq!(first_entry.offset, 12);
1952
1953        let second_entry = lookup
1954            .find(&second)
1955            .expect("test operation should succeed")
1956            .expect("object should be present");
1957        assert_eq!(
1958            lookup.pack_name(second_entry.pack_int_id),
1959            Some("pack-b.idx")
1960        );
1961        assert_eq!(second_entry.offset, 0x1_0000_0000);
1962        assert!(
1963            lookup
1964                .find(&missing)
1965                .expect("test operation should succeed")
1966                .is_none()
1967        );
1968    }
1969
1970    #[test]
1971    fn rejects_bad_multi_pack_index_checksum() {
1972        let chunks = midx_chunks_with_pack_names(ObjectFormat::Sha1, Vec::new(), &[]);
1973        let mut midx = multi_pack_index(ObjectFormat::Sha1, 1, 0, &chunks);
1974        let last = midx.len() - 1;
1975        midx[last] ^= 1;
1976        assert!(MultiPackIndex::parse(&midx, ObjectFormat::Sha1).is_err());
1977    }
1978
1979    #[test]
1980    fn rejects_bad_multi_pack_index_shape() {
1981        let chunks = midx_chunks_with_pack_names(ObjectFormat::Sha1, Vec::new(), &[]);
1982        let mut wrong_hash = multi_pack_index(ObjectFormat::Sha1, 1, 0, &chunks);
1983        wrong_hash[5] = 2;
1984        let checksum_offset = wrong_hash.len() - ObjectFormat::Sha1.raw_len();
1985        let checksum = sley_core::digest_bytes(ObjectFormat::Sha1, &wrong_hash[..checksum_offset])
1986            .expect("test operation should succeed");
1987        wrong_hash[checksum_offset..].copy_from_slice(checksum.as_bytes());
1988        assert!(MultiPackIndex::parse(&wrong_hash, ObjectFormat::Sha1).is_err());
1989
1990        let mut missing_terminator = multi_pack_index(ObjectFormat::Sha1, 1, 0, &chunks);
1991        missing_terminator[12] = b'B';
1992        let checksum_offset = missing_terminator.len() - ObjectFormat::Sha1.raw_len();
1993        let checksum =
1994            sley_core::digest_bytes(ObjectFormat::Sha1, &missing_terminator[..checksum_offset])
1995                .expect("test operation should succeed");
1996        missing_terminator[checksum_offset..].copy_from_slice(checksum.as_bytes());
1997        assert!(MultiPackIndex::parse(&missing_terminator, ObjectFormat::Sha1).is_err());
1998
1999        let mut bad_offset = multi_pack_index(
2000            ObjectFormat::Sha1,
2001            2,
2002            0,
2003            &midx_chunks_with_pack_names(ObjectFormat::Sha1, Vec::new(), &[]),
2004        );
2005        bad_offset[16..24].copy_from_slice(&0u64.to_be_bytes());
2006        let checksum_offset = bad_offset.len() - ObjectFormat::Sha1.raw_len();
2007        let checksum = sley_core::digest_bytes(ObjectFormat::Sha1, &bad_offset[..checksum_offset])
2008            .expect("test operation should succeed");
2009        bad_offset[checksum_offset..].copy_from_slice(checksum.as_bytes());
2010        assert!(MultiPackIndex::parse(&bad_offset, ObjectFormat::Sha1).is_err());
2011    }
2012
2013    #[test]
2014    fn rejects_bad_multi_pack_index_pack_names() {
2015        let missing = multi_pack_index(ObjectFormat::Sha1, 2, 1, &[]);
2016        assert!(MultiPackIndex::parse(&missing, ObjectFormat::Sha1).is_err());
2017
2018        let too_few = multi_pack_index(
2019            ObjectFormat::Sha1,
2020            2,
2021            2,
2022            &midx_chunks_with_pack_names(ObjectFormat::Sha1, b"pack-a.idx\0".to_vec(), &[]),
2023        );
2024        assert!(MultiPackIndex::parse(&too_few, ObjectFormat::Sha1).is_err());
2025
2026        let bad_padding = multi_pack_index(
2027            ObjectFormat::Sha1,
2028            2,
2029            1,
2030            &midx_chunks_with_pack_names(ObjectFormat::Sha1, b"pack-a.idx\0xxxx".to_vec(), &[]),
2031        );
2032        assert!(MultiPackIndex::parse(&bad_padding, ObjectFormat::Sha1).is_err());
2033
2034        let unsorted_v1 = multi_pack_index(
2035            ObjectFormat::Sha1,
2036            1,
2037            2,
2038            &midx_chunks_with_pack_names(
2039                ObjectFormat::Sha1,
2040                b"pack-b.idx\0pack-a.idx\0".to_vec(),
2041                &[],
2042            ),
2043        );
2044        assert!(MultiPackIndex::parse(&unsorted_v1, ObjectFormat::Sha1).is_err());
2045
2046        let unsorted_v2 = multi_pack_index(
2047            ObjectFormat::Sha1,
2048            2,
2049            2,
2050            &midx_chunks_with_pack_names(
2051                ObjectFormat::Sha1,
2052                b"pack-b.idx\0pack-a.idx\0".to_vec(),
2053                &[],
2054            ),
2055        );
2056        let parsed = MultiPackIndex::parse(&unsorted_v2, ObjectFormat::Sha1)
2057            .expect("test operation should succeed");
2058        assert_eq!(parsed.pack_names, vec!["pack-b.idx", "pack-a.idx"]);
2059    }
2060
2061    #[test]
2062    fn rejects_bad_multi_pack_index_object_tables() {
2063        let oid_a = ObjectId::from_hex(
2064            ObjectFormat::Sha1,
2065            "1111111111111111111111111111111111111111",
2066        )
2067        .expect("test operation should succeed");
2068        let oid_b = ObjectId::from_hex(
2069            ObjectFormat::Sha1,
2070            "2222222222222222222222222222222222222222",
2071        )
2072        .expect("test operation should succeed");
2073
2074        let missing_oidf = multi_pack_index(
2075            ObjectFormat::Sha1,
2076            2,
2077            1,
2078            &[(*b"PNAM", b"pack-a.idx\0\0".to_vec())],
2079        );
2080        assert!(MultiPackIndex::parse(&missing_oidf, ObjectFormat::Sha1).is_err());
2081
2082        let bad_fanout = vec![
2083            (*b"PNAM", b"pack-a.idx\0\0".to_vec()),
2084            (*b"OIDF", vec![0; 256 * 4]),
2085            (*b"OIDL", oid_a.as_bytes().to_vec()),
2086            (*b"OOFF", midx_ooff_entries(&[(0, 12)], &mut Vec::new())),
2087        ];
2088        let bad_fanout = multi_pack_index(ObjectFormat::Sha1, 2, 1, &bad_fanout);
2089        assert!(MultiPackIndex::parse(&bad_fanout, ObjectFormat::Sha1).is_err());
2090
2091        let mut unsorted = Vec::new();
2092        unsorted.push((*b"PNAM", b"pack-a.idx\0\0".to_vec()));
2093        unsorted.push((*b"OIDF", midx_oid_fanout(&[oid_a.clone(), oid_b.clone()])));
2094        let mut oid_lookup = Vec::new();
2095        oid_lookup.extend_from_slice(oid_b.as_bytes());
2096        oid_lookup.extend_from_slice(oid_a.as_bytes());
2097        unsorted.push((*b"OIDL", oid_lookup));
2098        unsorted.push((
2099            *b"OOFF",
2100            midx_ooff_entries(&[(0, 12), (0, 24)], &mut Vec::new()),
2101        ));
2102        let unsorted = multi_pack_index(ObjectFormat::Sha1, 2, 1, &unsorted);
2103        assert!(MultiPackIndex::parse(&unsorted, ObjectFormat::Sha1).is_err());
2104
2105        let bad_pack = multi_pack_index(
2106            ObjectFormat::Sha1,
2107            2,
2108            1,
2109            &midx_chunks_with_pack_names(
2110                ObjectFormat::Sha1,
2111                b"pack-a.idx\0\0".to_vec(),
2112                &[(oid_a.clone(), 1, 12)],
2113            ),
2114        );
2115        assert!(MultiPackIndex::parse(&bad_pack, ObjectFormat::Sha1).is_err());
2116
2117        let mut large_offsets = Vec::new();
2118        let missing_loff = vec![
2119            (*b"PNAM", b"pack-a.idx\0\0".to_vec()),
2120            (*b"OIDF", midx_oid_fanout(std::slice::from_ref(&oid_a))),
2121            (*b"OIDL", oid_a.as_bytes().to_vec()),
2122            (
2123                *b"OOFF",
2124                midx_ooff_entries(&[(0, 0x1_0000_0000)], &mut large_offsets),
2125            ),
2126        ];
2127        let missing_loff = multi_pack_index(ObjectFormat::Sha1, 2, 1, &missing_loff);
2128        assert!(MultiPackIndex::parse(&missing_loff, ObjectFormat::Sha1).is_err());
2129
2130        let mut bad_loff =
2131            midx_chunks_with_pack_names(ObjectFormat::Sha1, b"pack-a.idx\0\0".to_vec(), &[]);
2132        bad_loff.push((*b"LOFF", vec![0]));
2133        let bad_loff = multi_pack_index(ObjectFormat::Sha1, 2, 1, &bad_loff);
2134        assert!(MultiPackIndex::parse(&bad_loff, ObjectFormat::Sha1).is_err());
2135    }
2136
2137    #[test]
2138    fn parses_multi_pack_index_bitmap_chunks() {
2139        let first = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"first object\n")
2140            .expect("test operation should succeed");
2141        let second = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"second object\n")
2142            .expect("test operation should succeed");
2143        let mut chunks = midx_chunks_with_pack_names(
2144            ObjectFormat::Sha1,
2145            b"pack-a.idx\0pack-b.idx\0\0\0".to_vec(),
2146            &[(first, 0, 12), (second, 1, 24)],
2147        );
2148        chunks.push((*b"RIDX", midx_u32_table(&[1, 0])));
2149        chunks.push((*b"BTMP", midx_bitmap_packs(&[(0, 1), (1, 1)])));
2150        let midx = multi_pack_index(ObjectFormat::Sha1, 2, 2, &chunks);
2151
2152        let parsed = MultiPackIndex::parse(&midx, ObjectFormat::Sha1)
2153            .expect("test operation should succeed");
2154        assert_eq!(parsed.reverse_index, Some(vec![1, 0]));
2155        assert_eq!(
2156            parsed.bitmapped_packs,
2157            Some(vec![
2158                MultiPackBitmapPack {
2159                    bitmap_pos: 0,
2160                    bitmap_nr: 1,
2161                },
2162                MultiPackBitmapPack {
2163                    bitmap_pos: 1,
2164                    bitmap_nr: 1,
2165                },
2166            ])
2167        );
2168    }
2169
2170    #[test]
2171    fn writes_multi_pack_index_that_round_trips() {
2172        let first = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"first object\n")
2173            .expect("test operation should succeed");
2174        let second = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"second object\n")
2175            .expect("test operation should succeed");
2176        let bytes = MultiPackIndex::write(
2177            ObjectFormat::Sha1,
2178            2,
2179            &["pack-b.idx".into(), "pack-a.idx".into()],
2180            &[
2181                MultiPackIndexEntry {
2182                    oid: second.clone(),
2183                    pack_int_id: 0,
2184                    offset: 0x1_0000_0000,
2185                    force_large_offset: false,
2186                },
2187                MultiPackIndexEntry {
2188                    oid: first.clone(),
2189                    pack_int_id: 1,
2190                    offset: 12,
2191                    force_large_offset: false,
2192                },
2193            ],
2194        )
2195        .expect("test operation should succeed");
2196
2197        let parsed = MultiPackIndex::parse(&bytes, ObjectFormat::Sha1)
2198            .expect("test operation should succeed");
2199        assert_eq!(parsed.version, 2);
2200        assert_eq!(parsed.pack_names, vec!["pack-b.idx", "pack-a.idx"]);
2201        assert_eq!(parsed.object_count, 2);
2202        assert_eq!(
2203            parsed
2204                .find(&first)
2205                .expect("test operation should succeed")
2206                .pack_int_id,
2207            1
2208        );
2209        assert_eq!(
2210            parsed
2211                .find(&first)
2212                .expect("test operation should succeed")
2213                .offset,
2214            12
2215        );
2216        assert_eq!(
2217            parsed
2218                .find(&second)
2219                .expect("test operation should succeed")
2220                .pack_int_id,
2221            0
2222        );
2223        assert_eq!(
2224            parsed
2225                .find(&second)
2226                .expect("test operation should succeed")
2227                .offset,
2228            0x1_0000_0000
2229        );
2230        assert!(parsed.chunks.iter().any(|chunk| chunk.id == *b"LOFF"));
2231    }
2232
2233    #[test]
2234    fn write_multi_pack_index_rejects_invalid_inputs() {
2235        let oid = sley_core::object_id_for_bytes(ObjectFormat::Sha1, "blob", b"object\n")
2236            .expect("test operation should succeed");
2237        assert!(MultiPackIndex::write(ObjectFormat::Sha1, 3, &["pack-a.idx".into()], &[]).is_err());
2238        assert!(
2239            MultiPackIndex::write(
2240                ObjectFormat::Sha1,
2241                1,
2242                &["pack-b.idx".into(), "pack-a.idx".into()],
2243                &[],
2244            )
2245            .is_err()
2246        );
2247        assert!(MultiPackIndex::write(ObjectFormat::Sha1, 2, &["pack/a.idx".into()], &[]).is_err());
2248        assert!(
2249            MultiPackIndex::write(
2250                ObjectFormat::Sha1,
2251                2,
2252                &["pack-a.idx".into()],
2253                &[MultiPackIndexEntry {
2254                    oid,
2255                    pack_int_id: 1,
2256                    offset: 12,
2257                    force_large_offset: false,
2258                }],
2259            )
2260            .is_err()
2261        );
2262        assert!(
2263            MultiPackIndex::write(
2264                ObjectFormat::Sha1,
2265                2,
2266                &["pack-a.idx".into()],
2267                &[
2268                    MultiPackIndexEntry {
2269                        oid,
2270                        pack_int_id: 0,
2271                        offset: 12,
2272                        force_large_offset: false,
2273                    },
2274                    MultiPackIndexEntry {
2275                        oid,
2276                        pack_int_id: 0,
2277                        offset: 24,
2278                        force_large_offset: false,
2279                    },
2280                ],
2281            )
2282            .is_err()
2283        );
2284    }
2285
2286    #[test]
2287    fn rejects_bad_multi_pack_index_bitmap_chunks() {
2288        let oid_a = ObjectId::from_hex(
2289            ObjectFormat::Sha1,
2290            "1111111111111111111111111111111111111111",
2291        )
2292        .expect("test operation should succeed");
2293        let oid_b = ObjectId::from_hex(
2294            ObjectFormat::Sha1,
2295            "2222222222222222222222222222222222222222",
2296        )
2297        .expect("test operation should succeed");
2298
2299        let mut duplicate_ridx = midx_chunks_with_pack_names(
2300            ObjectFormat::Sha1,
2301            b"pack-a.idx\0\0".to_vec(),
2302            &[(oid_a.clone(), 0, 12), (oid_b.clone(), 0, 24)],
2303        );
2304        duplicate_ridx.push((*b"RIDX", midx_u32_table(&[0, 0])));
2305        let duplicate_ridx = multi_pack_index(ObjectFormat::Sha1, 2, 1, &duplicate_ridx);
2306        assert!(MultiPackIndex::parse(&duplicate_ridx, ObjectFormat::Sha1).is_err());
2307
2308        let mut short_btmp = midx_chunks_with_pack_names(
2309            ObjectFormat::Sha1,
2310            b"pack-a.idx\0pack-b.idx\0\0\0".to_vec(),
2311            &[(oid_a.clone(), 0, 12), (oid_b.clone(), 1, 24)],
2312        );
2313        short_btmp.push((*b"BTMP", midx_bitmap_packs(&[(0, 1)])));
2314        let short_btmp = multi_pack_index(ObjectFormat::Sha1, 2, 2, &short_btmp);
2315        assert!(MultiPackIndex::parse(&short_btmp, ObjectFormat::Sha1).is_err());
2316
2317        let mut out_of_range_btmp = midx_chunks_with_pack_names(
2318            ObjectFormat::Sha1,
2319            b"pack-a.idx\0\0".to_vec(),
2320            &[(oid_a, 0, 12), (oid_b, 0, 24)],
2321        );
2322        out_of_range_btmp.push((*b"BTMP", midx_bitmap_packs(&[(1, 2)])));
2323        let out_of_range_btmp = multi_pack_index(ObjectFormat::Sha1, 2, 1, &out_of_range_btmp);
2324        assert!(MultiPackIndex::parse(&out_of_range_btmp, ObjectFormat::Sha1).is_err());
2325    }
2326
2327    #[test]
2328    fn parses_pack_bitmap_index_with_hash_cache() {
2329        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
2330            .expect("test operation should succeed");
2331        let bitmap = pack_bitmap_index(
2332            ObjectFormat::Sha1,
2333            3,
2334            PackBitmapIndex::OPTION_FULL_DAG | PackBitmapIndex::OPTION_HASH_CACHE,
2335            &pack_checksum,
2336            &[(2, 0, 1, &[0b101])],
2337            Some(&[0x1111_1111, 0x2222_2222, 0x3333_3333]),
2338        );
2339
2340        let parsed = PackBitmapIndex::parse(&bitmap, ObjectFormat::Sha1, 3)
2341            .expect("test operation should succeed");
2342        assert_eq!(parsed.version, 1);
2343        assert_eq!(parsed.format, ObjectFormat::Sha1);
2344        assert_eq!(
2345            parsed.options,
2346            PackBitmapIndex::OPTION_FULL_DAG | PackBitmapIndex::OPTION_HASH_CACHE
2347        );
2348        assert_eq!(parsed.pack_checksum, pack_checksum);
2349        assert_eq!(parsed.type_bitmaps.commits.bit_size, 3);
2350        assert_eq!(parsed.type_bitmaps.trees.bit_size, 3);
2351        assert_eq!(parsed.entries.len(), 1);
2352        let entry = parsed
2353            .entry_for_index_position(2)
2354            .expect("test operation should succeed");
2355        assert_eq!(entry.xor_offset, 0);
2356        assert_eq!(entry.flags, 1);
2357        assert_eq!(entry.bitmap.words, ewah_literal_words(&[0b101]));
2358        assert_eq!(
2359            parsed.name_hash_cache,
2360            Some(vec![0x1111_1111, 0x2222_2222, 0x3333_3333])
2361        );
2362    }
2363
2364    #[test]
2365    fn parses_pack_bitmap_index_sha256() {
2366        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha256, b"pack")
2367            .expect("test operation should succeed");
2368        let bitmap = pack_bitmap_index(
2369            ObjectFormat::Sha256,
2370            2,
2371            PackBitmapIndex::OPTION_FULL_DAG,
2372            &pack_checksum,
2373            &[(0, 0, 0, &[0b11])],
2374            None,
2375        );
2376
2377        let parsed = PackBitmapIndex::parse(&bitmap, ObjectFormat::Sha256, 2)
2378            .expect("test operation should succeed");
2379        assert_eq!(parsed.version, 1);
2380        assert_eq!(parsed.format, ObjectFormat::Sha256);
2381        assert_eq!(parsed.pack_checksum, pack_checksum);
2382        assert_eq!(parsed.index_checksum.format(), ObjectFormat::Sha256);
2383        assert_eq!(parsed.entries[0].object_position, 0);
2384        assert_eq!(parsed.name_hash_cache, None);
2385    }
2386
2387    #[test]
2388    fn parses_upstream_git_written_pack_bitmap_index() {
2389        let root = unique_temp_dir("git-pack-bitmap-upstream");
2390        fs::create_dir_all(&root).expect("test operation should succeed");
2391        {
2392            run_git_success(&root, &["init", "-q", "-b", "main"]);
2393            run_git_success(
2394                &root,
2395                &[
2396                    "-c",
2397                    "user.name=Example User",
2398                    "-c",
2399                    "user.email=example@example.invalid",
2400                    "commit",
2401                    "--allow-empty",
2402                    "-q",
2403                    "-m",
2404                    "one",
2405                ],
2406            );
2407            run_git_success(
2408                &root,
2409                &[
2410                    "-c",
2411                    "user.name=Example User",
2412                    "-c",
2413                    "user.email=example@example.invalid",
2414                    "commit",
2415                    "--allow-empty",
2416                    "-q",
2417                    "-m",
2418                    "two",
2419                ],
2420            );
2421            run_git_success(&root, &["repack", "-adb"]);
2422            let pack_dir = root.join(".git").join("objects").join("pack");
2423            let idx_path = single_path_with_extension(&pack_dir, "idx");
2424            let bitmap_path = single_path_with_extension(&pack_dir, "bitmap");
2425            let index = PackIndex::parse(
2426                &fs::read(idx_path).expect("test operation should succeed"),
2427                ObjectFormat::Sha1,
2428            )
2429            .expect("test operation should succeed");
2430            let bitmap = PackBitmapIndex::parse(
2431                &fs::read(bitmap_path).expect("test operation should succeed"),
2432                ObjectFormat::Sha1,
2433                index.entries.len(),
2434            )
2435            .expect("test operation should succeed");
2436            assert_eq!(bitmap.pack_checksum, index.pack_checksum);
2437            assert!(!bitmap.entries.is_empty());
2438        };
2439        let _ = fs::remove_dir_all(&root);
2440    }
2441
2442    #[test]
2443    fn rejects_bad_pack_bitmap_index_header_and_checksum() {
2444        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
2445            .expect("test operation should succeed");
2446        let bitmap = pack_bitmap_index(
2447            ObjectFormat::Sha1,
2448            1,
2449            PackBitmapIndex::OPTION_FULL_DAG,
2450            &pack_checksum,
2451            &[(0, 0, 0, &[1])],
2452            None,
2453        );
2454
2455        let mut bad_signature = bitmap.clone();
2456        bad_signature[0] = b'X';
2457        assert!(PackBitmapIndex::parse(&bad_signature, ObjectFormat::Sha1, 1).is_err());
2458
2459        let mut bad_version = bitmap.clone();
2460        bad_version[5] = 2;
2461        refresh_trailing_checksum(ObjectFormat::Sha1, &mut bad_version);
2462        assert!(PackBitmapIndex::parse(&bad_version, ObjectFormat::Sha1, 1).is_err());
2463
2464        let mut bad_option = bitmap.clone();
2465        bad_option[7] = 0x20;
2466        refresh_trailing_checksum(ObjectFormat::Sha1, &mut bad_option);
2467        assert!(PackBitmapIndex::parse(&bad_option, ObjectFormat::Sha1, 1).is_err());
2468
2469        let mut bad_checksum = bitmap;
2470        let last = bad_checksum.len() - 1;
2471        bad_checksum[last] ^= 1;
2472        assert!(PackBitmapIndex::parse(&bad_checksum, ObjectFormat::Sha1, 1).is_err());
2473    }
2474
2475    #[test]
2476    fn rejects_bad_pack_bitmap_index_ewah_and_entries() {
2477        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha1, b"pack")
2478            .expect("test operation should succeed");
2479        let bitmap = pack_bitmap_index(
2480            ObjectFormat::Sha1,
2481            2,
2482            PackBitmapIndex::OPTION_FULL_DAG,
2483            &pack_checksum,
2484            &[(0, 0, 0, &[0b01]), (1, 1, 0, &[0b11])],
2485            None,
2486        );
2487
2488        let mut truncated = bitmap;
2489        truncated.truncate(truncated.len() - ObjectFormat::Sha1.raw_len() - 1);
2490        refresh_trailing_checksum(ObjectFormat::Sha1, &mut truncated);
2491        assert!(PackBitmapIndex::parse(&truncated, ObjectFormat::Sha1, 2).is_err());
2492
2493        let mut out_of_range_position = pack_bitmap_index(
2494            ObjectFormat::Sha1,
2495            2,
2496            PackBitmapIndex::OPTION_FULL_DAG,
2497            &pack_checksum,
2498            &[(2, 0, 0, &[0b01])],
2499            None,
2500        );
2501        assert!(PackBitmapIndex::parse(&out_of_range_position, ObjectFormat::Sha1, 2).is_err());
2502        refresh_trailing_checksum(ObjectFormat::Sha1, &mut out_of_range_position);
2503        assert!(PackBitmapIndex::parse(&out_of_range_position, ObjectFormat::Sha1, 2).is_err());
2504
2505        let invalid_xor = pack_bitmap_index(
2506            ObjectFormat::Sha1,
2507            2,
2508            PackBitmapIndex::OPTION_FULL_DAG,
2509            &pack_checksum,
2510            &[(0, 1, 0, &[0b01])],
2511            None,
2512        );
2513        assert!(PackBitmapIndex::parse(&invalid_xor, ObjectFormat::Sha1, 2).is_err());
2514    }
2515
2516    #[test]
2517    fn parses_single_entry_pack_index_sha256() {
2518        let oid = sley_core::object_id_for_bytes(ObjectFormat::Sha256, "blob", b"hello sha256\n")
2519            .expect("test operation should succeed");
2520        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha256, b"pack")
2521            .expect("test operation should succeed");
2522        let index = single_entry_index(
2523            ObjectFormat::Sha256,
2524            oid,
2525            0x1234_5678,
2526            12,
2527            pack_checksum.clone(),
2528        );
2529        let parsed =
2530            PackIndex::parse(&index, ObjectFormat::Sha256).expect("test operation should succeed");
2531        assert_eq!(parsed.version, 2);
2532        assert_eq!(parsed.pack_checksum, pack_checksum);
2533        assert_eq!(parsed.entries.len(), 1);
2534        assert_eq!(
2535            parsed
2536                .find(&oid)
2537                .expect("test operation should succeed")
2538                .offset,
2539            12
2540        );
2541        assert_eq!(
2542            parsed
2543                .find(&oid)
2544                .expect("test operation should succeed")
2545                .crc32,
2546            0x1234_5678
2547        );
2548        assert_eq!(parsed.index_checksum.format(), ObjectFormat::Sha256);
2549        assert_pack_index_view_matches_owned(&index, ObjectFormat::Sha256);
2550    }
2551
2552    #[test]
2553    fn write_packed_deltifies_similar_blobs_and_round_trips_sha1() {
2554        write_packed_deltifies_similar_blobs_and_round_trips(ObjectFormat::Sha1);
2555    }
2556
2557    #[test]
2558    fn write_packed_deltifies_similar_blobs_and_round_trips_sha256() {
2559        write_packed_deltifies_similar_blobs_and_round_trips(ObjectFormat::Sha256);
2560    }
2561
2562    #[test]
2563    fn write_packed_rejects_duplicate_objects() {
2564        let object = EncodedObject::new(ObjectType::Blob, b"same\n".to_vec());
2565        assert!(PackFile::write_packed(&[object.clone(), object], ObjectFormat::Sha1,).is_err());
2566    }
2567
2568    #[test]
2569    fn write_packed_with_known_ids_validates_ids_before_trusting_them() {
2570        let object = EncodedObject::new(ObjectType::Blob, b"same\n".to_vec());
2571        let sha1 = object
2572            .object_id(ObjectFormat::Sha1)
2573            .expect("test operation should succeed");
2574        let sha256 = object
2575            .object_id(ObjectFormat::Sha256)
2576            .expect("test operation should succeed");
2577        let duplicate = [
2578            PackInput {
2579                oid: &sha1,
2580                object: &object,
2581            },
2582            PackInput {
2583                oid: &sha1,
2584                object: &object,
2585            },
2586        ];
2587        assert!(PackFile::write_packed_with_known_ids(&duplicate, ObjectFormat::Sha1).is_err());
2588
2589        let wrong_format = [PackInput {
2590            oid: &sha256,
2591            object: &object,
2592        }];
2593        assert!(PackFile::write_packed_with_known_ids(&wrong_format, ObjectFormat::Sha1).is_err());
2594    }
2595
2596    #[test]
2597    fn write_packed_with_known_ids_to_writer_matches_in_memory_pack() {
2598        let objects = similar_blob_family(6);
2599        let object_ids = objects
2600            .iter()
2601            .map(|object| {
2602                object
2603                    .object_id(ObjectFormat::Sha1)
2604                    .expect("test operation should succeed")
2605            })
2606            .collect::<Vec<_>>();
2607        let inputs = objects
2608            .iter()
2609            .zip(&object_ids)
2610            .map(|(object, oid)| PackInput { oid, object })
2611            .collect::<Vec<_>>();
2612        let options = PackWriteOptions::new();
2613        let in_memory = PackFile::write_packed_with_known_ids_and_options(
2614            &inputs,
2615            ObjectFormat::Sha1,
2616            &options,
2617        )
2618        .expect("test operation should succeed");
2619        let mut written = Vec::new();
2620        let streamed = PackFile::write_packed_with_known_ids_to_writer(
2621            &inputs,
2622            ObjectFormat::Sha1,
2623            &options,
2624            &mut written,
2625        )
2626        .expect("test operation should succeed");
2627
2628        assert_eq!(written, in_memory.pack);
2629        assert_eq!(streamed.index, in_memory.index);
2630        assert_eq!(streamed.checksum, in_memory.checksum);
2631        assert_eq!(streamed.entries, in_memory.entries);
2632        assert_eq!(streamed.delta_count, in_memory.delta_count);
2633        assert_eq!(streamed.pack_size, in_memory.pack.len() as u64);
2634    }
2635
2636    #[test]
2637    fn write_packed_from_source_to_writer_deltifies_across_windows() {
2638        let format = ObjectFormat::Sha1;
2639        // Keep this candidate inside Git's first-delta budget (half the
2640        // target size minus one raw object id), while still straddling two
2641        // byte-budgeted compression windows.
2642        let shared = b"cross-window base payload with enough shared anchors\n".repeat(64);
2643        let mut base_body = shared.clone();
2644        base_body.extend_from_slice(b"base\n");
2645        let mut target_body = shared;
2646        target_body.extend_from_slice(b"target\n");
2647        let objects = vec![
2648            EncodedObject::new(ObjectType::Blob, base_body),
2649            EncodedObject::new(ObjectType::Blob, target_body),
2650        ];
2651
2652        let object_ids = objects
2653            .iter()
2654            .map(|object| {
2655                object
2656                    .object_id(format)
2657                    .expect("test operation should succeed")
2658            })
2659            .collect::<Vec<_>>();
2660        let base_oid = object_ids[0];
2661        let target_oid = object_ids[1];
2662        let object_map = object_ids
2663            .iter()
2664            .copied()
2665            .zip(objects.into_iter().map(Arc::new))
2666            .collect::<HashMap<_, _>>();
2667
2668        let options = PackWriteOptions::new().with_reorder(false).with_window(10);
2669        let one_object = object_map.get(&base_oid).expect("base").body.len() as u64 + 80;
2670        let limits =
2671            PackWriteLimits::new().with_compression_working_set(ByteBudget::new(one_object));
2672        let mut written = Vec::new();
2673        let summary = PackFile::write_packed_from_source_to_writer(
2674            object_ids.iter().copied(),
2675            2,
2676            format,
2677            &options,
2678            limits,
2679            |oid| {
2680                object_map
2681                    .get(oid)
2682                    .cloned()
2683                    .ok_or_else(|| GitError::not_found(format!("missing test object {oid}")))
2684            },
2685            &mut written,
2686        )
2687        .expect("test operation should succeed");
2688
2689        assert!(
2690            summary.delta_count > 0,
2691            "expected source-backed streaming writer to find deltas"
2692        );
2693        let stats =
2694            PackFile::verify_pack_stats(&written, format).expect("test operation should succeed");
2695        let target = stats
2696            .objects
2697            .iter()
2698            .find(|entry| entry.oid == target_oid)
2699            .expect("target object should be present");
2700        assert_eq!(target.base_oid, Some(base_oid));
2701    }
2702
2703    fn write_packed_deltifies_similar_blobs_and_round_trips(format: ObjectFormat) {
2704        let objects = similar_blob_family(8);
2705        let packed =
2706            PackFile::write_packed(&objects, format).expect("test operation should succeed");
2707        let undeltified =
2708            PackFile::write_undeltified(&objects, format).expect("test operation should succeed");
2709
2710        // The whole point of delta selection: the packed output is smaller than
2711        // storing every object undeltified.
2712        assert!(
2713            packed.pack.len() < undeltified.pack.len(),
2714            "expected delta pack ({}) smaller than undeltified pack ({})",
2715            packed.pack.len(),
2716            undeltified.pack.len()
2717        );
2718
2719        // At least one object must actually be stored as a delta.
2720        let kinds = pack_entry_kinds(&packed.pack, format);
2721        let delta_count = kinds
2722            .iter()
2723            .filter(|kind| matches!(kind, PackObjectKind::OfsDelta | PackObjectKind::RefDelta))
2724            .count();
2725        assert!(
2726            delta_count >= 1,
2727            "expected at least one delta entry, found kinds {kinds:?}"
2728        );
2729
2730        // Round-trip: every original object reconstructs byte-for-byte.
2731        let parsed = PackFile::parse(&packed.pack, format).expect("test operation should succeed");
2732        assert_eq!(parsed.entries.len(), objects.len());
2733        for object in &objects {
2734            let oid = object
2735                .object_id(format)
2736                .expect("test operation should succeed");
2737            let found = parsed
2738                .entries
2739                .iter()
2740                .find(|entry| entry.entry.oid == oid)
2741                .unwrap_or_else(|| panic!("object {oid} missing from parsed pack"));
2742            assert_eq!(&found.object, object, "object {oid} did not round-trip");
2743        }
2744
2745        // The index must agree with the pack and locate every object.
2746        let index = PackIndex::parse(&packed.index, format).expect("test operation should succeed");
2747        assert_eq!(index.pack_checksum, packed.checksum);
2748        for object in &objects {
2749            let oid = object
2750                .object_id(format)
2751                .expect("test operation should succeed");
2752            assert!(index.find(&oid).is_some(), "index missing {oid}");
2753        }
2754    }
2755
2756    #[test]
2757    fn write_packed_emits_ofs_delta_by_default() {
2758        let objects = similar_blob_family(6);
2759        let packed = PackFile::write_packed(&objects, ObjectFormat::Sha1)
2760            .expect("test operation should succeed");
2761        let kinds = pack_entry_kinds(&packed.pack, ObjectFormat::Sha1);
2762        assert!(
2763            kinds.contains(&PackObjectKind::OfsDelta),
2764            "expected an ofs-delta entry by default, found {kinds:?}"
2765        );
2766        assert!(
2767            !kinds.contains(&PackObjectKind::RefDelta),
2768            "default self-contained pack must not use ref-delta, found {kinds:?}"
2769        );
2770        // Round-trips.
2771        assert!(PackFile::parse(&packed.pack, ObjectFormat::Sha1).is_ok());
2772    }
2773
2774    #[test]
2775    fn write_packed_can_emit_ref_delta() {
2776        let objects = similar_blob_family(6);
2777        let options = PackWriteOptions::new().with_prefer_ofs_delta(false);
2778        let packed = PackFile::write_packed_with_options(&objects, ObjectFormat::Sha1, &options)
2779            .expect("test operation should succeed");
2780        let kinds = pack_entry_kinds(&packed.pack, ObjectFormat::Sha1);
2781        assert!(
2782            kinds.contains(&PackObjectKind::RefDelta),
2783            "expected a ref-delta entry, found {kinds:?}"
2784        );
2785        assert!(
2786            !kinds.contains(&PackObjectKind::OfsDelta),
2787            "ref-delta mode must not emit ofs-delta, found {kinds:?}"
2788        );
2789
2790        // Ref-delta packs are still self-contained here, so they round-trip
2791        // without any external base lookup.
2792        let parsed = PackFile::parse(&packed.pack, ObjectFormat::Sha1)
2793            .expect("test operation should succeed");
2794        assert_eq!(parsed.entries.len(), objects.len());
2795    }
2796
2797    #[test]
2798    fn write_packed_bounds_delta_chain_depth() {
2799        // A long chain of progressively-modified blobs. With a large window
2800        // every object could otherwise delta against its immediate predecessor,
2801        // forming a chain as long as the input.
2802        let objects = incremental_blob_chain(20);
2803        let format = ObjectFormat::Sha1;
2804
2805        for max_depth in [1usize, 2, 5] {
2806            let options = PackWriteOptions::new()
2807                .with_window(20)
2808                .with_depth(max_depth);
2809            let packed = PackFile::write_packed_with_options(&objects, format, &options)
2810                .expect("test operation should succeed");
2811
2812            let depths = pack_entry_depths(&packed.pack, format);
2813            let observed = depths.iter().copied().max().unwrap_or(0);
2814            assert!(
2815                observed <= max_depth,
2816                "max chain depth {observed} exceeded bound {max_depth}"
2817            );
2818
2819            // Still correct: round-trips byte-for-byte.
2820            let parsed =
2821                PackFile::parse(&packed.pack, format).expect("test operation should succeed");
2822            for object in &objects {
2823                let oid = object
2824                    .object_id(format)
2825                    .expect("test operation should succeed");
2826                let found = parsed
2827                    .entries
2828                    .iter()
2829                    .find(|entry| entry.entry.oid == oid)
2830                    .expect("test operation should succeed");
2831                assert_eq!(&found.object, object);
2832            }
2833        }
2834    }
2835
2836    #[test]
2837    fn write_packed_depth_zero_stores_everything_undeltified() {
2838        let objects = similar_blob_family(5);
2839        let options = PackWriteOptions::new().with_depth(0);
2840        let packed = PackFile::write_packed_with_options(&objects, ObjectFormat::Sha1, &options)
2841            .expect("test operation should succeed");
2842        let kinds = pack_entry_kinds(&packed.pack, ObjectFormat::Sha1);
2843        assert!(
2844            kinds
2845                .iter()
2846                .all(|kind| !matches!(kind, PackObjectKind::OfsDelta | PackObjectKind::RefDelta)),
2847            "depth 0 must disable deltas, found {kinds:?}"
2848        );
2849    }
2850
2851    #[test]
2852    fn write_thin_uses_external_base_and_round_trips_sha1() {
2853        write_thin_uses_external_base_and_round_trips(ObjectFormat::Sha1);
2854    }
2855
2856    #[test]
2857    fn write_thin_uses_external_base_and_round_trips_sha256() {
2858        write_thin_uses_external_base_and_round_trips(ObjectFormat::Sha256);
2859    }
2860
2861    fn write_thin_uses_external_base_and_round_trips(format: ObjectFormat) {
2862        // The base object stays OUT of the pack; only `target` is written, as a
2863        // ref-delta against the external base's object id.
2864        let base = blob_with_marker("EXTERNAL-BASE");
2865        let target = blob_with_marker("EXTERNAL-TARGET");
2866        let base_oid = base
2867            .object_id(format)
2868            .expect("test operation should succeed");
2869
2870        let mut external = HashMap::new();
2871        external.insert(base_oid, base.clone());
2872        let packed = PackFile::write_thin(std::slice::from_ref(&target), format, external)
2873            .expect("test operation should succeed");
2874
2875        // Exactly one entry, encoded as a ref-delta to the external base.
2876        let kinds = pack_entry_kinds(&packed.pack, format);
2877        assert_eq!(kinds, vec![PackObjectKind::RefDelta]);
2878
2879        // The external base reference must be the base oid.
2880        let mut offset = 12usize;
2881        let header =
2882            parse_entry_header(&packed.pack, &mut offset).expect("test operation should succeed");
2883        assert_eq!(header.kind, PackObjectKind::RefDelta);
2884        let referenced =
2885            ObjectId::from_raw(format, &packed.pack[offset..offset + format.raw_len()])
2886                .expect("test operation should succeed");
2887        assert_eq!(referenced, base_oid);
2888
2889        // A plain (non-thin) parse fails: the base is not present.
2890        assert!(PackFile::parse(&packed.pack, format).is_err());
2891
2892        // A thin parse that supplies the external base reconstructs the target.
2893        let parsed = PackFile::parse_thin(&packed.pack, format, |oid| {
2894            if oid == &base_oid {
2895                Ok(Some(base.clone()))
2896            } else {
2897                Ok(None)
2898            }
2899        })
2900        .expect("test operation should succeed");
2901        assert_eq!(parsed.entries.len(), 1);
2902        assert_eq!(parsed.entries[0].object, target);
2903    }
2904
2905    #[test]
2906    fn write_packed_preserves_distinct_objects_with_no_similarity() {
2907        // Unrelated objects: nothing should delta, but the pack must still be
2908        // valid and complete.
2909        let objects = vec![
2910            EncodedObject::new(ObjectType::Blob, b"alpha distinct\n".to_vec()),
2911            EncodedObject::new(ObjectType::Tree, vec![0u8; 0]),
2912            EncodedObject::new(ObjectType::Commit, b"tree 0000\n".to_vec()),
2913        ];
2914        let format = ObjectFormat::Sha1;
2915        let packed =
2916            PackFile::write_packed(&objects, format).expect("test operation should succeed");
2917        let parsed = PackFile::parse(&packed.pack, format).expect("test operation should succeed");
2918        assert_eq!(parsed.entries.len(), objects.len());
2919        for object in &objects {
2920            let oid = object
2921                .object_id(format)
2922                .expect("test operation should succeed");
2923            assert!(parsed.entries.iter().any(|entry| entry.entry.oid == oid));
2924        }
2925    }
2926
2927    /// Build a family of blobs that all share a large common region but differ
2928    /// in a marker placed in the *middle*, so a good delta finds copy regions on
2929    /// both sides of the change.
2930    fn similar_blob_family(count: usize) -> Vec<EncodedObject> {
2931        let mut common_head = Vec::new();
2932        for _ in 0..200 {
2933            common_head.extend_from_slice(b"shared header line for delta testing\n");
2934        }
2935        let mut common_tail = Vec::new();
2936        for _ in 0..200 {
2937            common_tail.extend_from_slice(b"shared trailer line for delta testing\n");
2938        }
2939        (0..count)
2940            .map(|idx| {
2941                let mut body = common_head.clone();
2942                body.extend_from_slice(format!("UNIQUE MIDDLE MARKER NUMBER {idx}\n").as_bytes());
2943                body.extend_from_slice(&common_tail);
2944                EncodedObject::new(ObjectType::Blob, body)
2945            })
2946            .collect()
2947    }
2948
2949    /// Build a chain where each blob is the previous one plus an appended line,
2950    /// so each is highly similar to its predecessor.
2951    fn incremental_blob_chain(count: usize) -> Vec<EncodedObject> {
2952        let mut body = Vec::new();
2953        for _ in 0..100 {
2954            body.extend_from_slice(b"baseline content shared across the whole chain\n");
2955        }
2956        let mut objects = Vec::with_capacity(count);
2957        for idx in 0..count {
2958            body.extend_from_slice(format!("appended unique line {idx}\n").as_bytes());
2959            objects.push(EncodedObject::new(ObjectType::Blob, body.clone()));
2960        }
2961        objects
2962    }
2963
2964    fn blob_with_marker(marker: &str) -> EncodedObject {
2965        let mut body = Vec::new();
2966        for _ in 0..150 {
2967            body.extend_from_slice(b"common body shared between base and target\n");
2968        }
2969        body.extend_from_slice(marker.as_bytes());
2970        body.push(b'\n');
2971        for _ in 0..150 {
2972            body.extend_from_slice(b"more common body shared between objects\n");
2973        }
2974        EncodedObject::new(ObjectType::Blob, body)
2975    }
2976
2977    /// Classify every entry in a pack (in pack order) by its on-disk kind.
2978    fn pack_entry_kinds(pack: &[u8], format: ObjectFormat) -> Vec<PackObjectKind> {
2979        pack_entry_descriptors(pack, format)
2980            .into_iter()
2981            .map(|descriptor| descriptor.kind)
2982            .collect()
2983    }
2984
2985    /// Compute each entry's delta chain depth (0 = undeltified base), in pack
2986    /// order. Entries always appear after their in-pack bases, so a single
2987    /// forward pass suffices.
2988    fn pack_entry_depths(pack: &[u8], format: ObjectFormat) -> Vec<usize> {
2989        let descriptors = pack_entry_descriptors(pack, format);
2990        let mut depth_by_offset: HashMap<u64, usize> = HashMap::new();
2991        let mut depths = Vec::with_capacity(descriptors.len());
2992        for descriptor in &descriptors {
2993            let depth = match &descriptor.base {
2994                EntryBase::None => 0,
2995                EntryBase::Offset(base_offset) => {
2996                    depth_by_offset.get(base_offset).copied().unwrap_or(0) + 1
2997                }
2998                // Ref-delta to an in-pack base: look it up by offset via oid is
2999                // unnecessary for these tests (which only use ofs-delta for the
3000                // chains), so treat as depth 1 if unknown.
3001                EntryBase::Ref => 1,
3002            };
3003            depth_by_offset.insert(descriptor.offset, depth);
3004            depths.push(depth);
3005        }
3006        depths
3007    }
3008
3009    struct EntryDescriptor {
3010        offset: u64,
3011        kind: PackObjectKind,
3012        base: EntryBase,
3013    }
3014
3015    enum EntryBase {
3016        None,
3017        Offset(u64),
3018        Ref,
3019    }
3020
3021    fn pack_entry_descriptors(pack: &[u8], format: ObjectFormat) -> Vec<EntryDescriptor> {
3022        let trailer_offset = pack.len() - format.raw_len();
3023        let count = u32_be(&pack[8..12]) as usize;
3024        let mut offset = 12usize;
3025        let mut descriptors = Vec::with_capacity(count);
3026        for _ in 0..count {
3027            let entry_offset = offset as u64;
3028            let header =
3029                parse_entry_header(pack, &mut offset).expect("test operation should succeed");
3030            let base = match header.kind {
3031                PackObjectKind::OfsDelta => {
3032                    let base_offset = parse_ofs_delta_base_offset(pack, &mut offset, entry_offset)
3033                        .expect("test operation should succeed");
3034                    EntryBase::Offset(base_offset)
3035                }
3036                PackObjectKind::RefDelta => {
3037                    offset += format.raw_len();
3038                    EntryBase::Ref
3039                }
3040                _ => EntryBase::None,
3041            };
3042            let mut decoder = ZlibDecoder::new(&pack[offset..trailer_offset]);
3043            let mut body = Vec::new();
3044            decoder
3045                .read_to_end(&mut body)
3046                .expect("test operation should succeed");
3047            offset += decoder.total_in() as usize;
3048            descriptors.push(EntryDescriptor {
3049                offset: entry_offset,
3050                kind: header.kind,
3051                base,
3052            });
3053        }
3054        descriptors
3055    }
3056
3057    fn similar_blob_objects() -> (EncodedObject, EncodedObject) {
3058        let mut base = Vec::new();
3059        for _ in 0..300 {
3060            base.extend_from_slice(b"common payload\n");
3061        }
3062        base.extend_from_slice(b"base\n");
3063        let mut changed = Vec::new();
3064        for _ in 0..300 {
3065            changed.extend_from_slice(b"common payload\n");
3066        }
3067        changed.extend_from_slice(b"changed\n");
3068        (
3069            EncodedObject::new(ObjectType::Blob, base),
3070            EncodedObject::new(ObjectType::Blob, changed),
3071        )
3072    }
3073
3074    fn single_object_pack(format: ObjectFormat, object_type: ObjectType, body: &[u8]) -> Vec<u8> {
3075        let mut pack = Vec::new();
3076        pack.extend_from_slice(b"PACK");
3077        pack.extend_from_slice(&2u32.to_be_bytes());
3078        pack.extend_from_slice(&1u32.to_be_bytes());
3079        write_entry_header(&mut pack, object_type, body.len() as u64);
3080        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
3081        encoder
3082            .write_all(body)
3083            .expect("test operation should succeed");
3084        pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
3085        let checksum =
3086            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
3087        pack.extend_from_slice(checksum.as_bytes());
3088        pack
3089    }
3090
3091    #[derive(Clone, Copy, Debug)]
3092    enum DeltaKind {
3093        Offset,
3094        Ref,
3095    }
3096
3097    fn two_object_delta_pack(
3098        format: ObjectFormat,
3099        base: &[u8],
3100        result: &[u8],
3101        delta_kind: DeltaKind,
3102    ) -> Vec<u8> {
3103        let mut pack = Vec::new();
3104        pack.extend_from_slice(b"PACK");
3105        pack.extend_from_slice(&2u32.to_be_bytes());
3106        pack.extend_from_slice(&2u32.to_be_bytes());
3107
3108        let base_offset = pack.len();
3109        write_entry_header(&mut pack, ObjectType::Blob, base.len() as u64);
3110        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
3111        encoder
3112            .write_all(base)
3113            .expect("test operation should succeed");
3114        pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
3115
3116        let delta = append_suffix_delta(base, result);
3117        let delta_offset = pack.len();
3118        write_pack_entry_header_kind(
3119            &mut pack,
3120            match delta_kind {
3121                DeltaKind::Offset => 6,
3122                DeltaKind::Ref => 7,
3123            },
3124            delta.len() as u64,
3125        );
3126        match delta_kind {
3127            DeltaKind::Offset => write_ofs_delta_offset(&mut pack, delta_offset - base_offset),
3128            DeltaKind::Ref => {
3129                let base_oid = sley_core::object_id_for_bytes(format, "blob", base)
3130                    .expect("test operation should succeed");
3131                pack.extend_from_slice(base_oid.as_bytes());
3132            }
3133        }
3134        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
3135        encoder
3136            .write_all(&delta)
3137            .expect("test operation should succeed");
3138        pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
3139
3140        let checksum =
3141            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
3142        pack.extend_from_slice(checksum.as_bytes());
3143        pack
3144    }
3145
3146    fn thin_ref_delta_pack(format: ObjectFormat, base: &[u8], result: &[u8]) -> Vec<u8> {
3147        let mut pack = Vec::new();
3148        pack.extend_from_slice(b"PACK");
3149        pack.extend_from_slice(&2u32.to_be_bytes());
3150        pack.extend_from_slice(&1u32.to_be_bytes());
3151
3152        let delta = append_suffix_delta(base, result);
3153        write_pack_entry_header_kind(&mut pack, 7, delta.len() as u64);
3154        let base_oid = sley_core::object_id_for_bytes(format, "blob", base)
3155            .expect("test operation should succeed");
3156        pack.extend_from_slice(base_oid.as_bytes());
3157        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
3158        encoder
3159            .write_all(&delta)
3160            .expect("test operation should succeed");
3161        pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
3162
3163        let checksum =
3164            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
3165        pack.extend_from_slice(checksum.as_bytes());
3166        pack
3167    }
3168
3169    fn thin_ref_delta_pack_many(format: ObjectFormat, base: &[u8], results: &[&[u8]]) -> Vec<u8> {
3170        let mut pack = Vec::new();
3171        pack.extend_from_slice(b"PACK");
3172        pack.extend_from_slice(&2u32.to_be_bytes());
3173        pack.extend_from_slice(&(results.len() as u32).to_be_bytes());
3174        let base_oid = sley_core::object_id_for_bytes(format, "blob", base)
3175            .expect("test operation should succeed");
3176        for result in results {
3177            let delta = append_suffix_delta(base, result);
3178            write_pack_entry_header_kind(&mut pack, 7, delta.len() as u64);
3179            pack.extend_from_slice(base_oid.as_bytes());
3180            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
3181            encoder
3182                .write_all(&delta)
3183                .expect("test operation should succeed");
3184            pack.extend_from_slice(&encoder.finish().expect("test operation should succeed"));
3185        }
3186        let checksum =
3187            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
3188        pack.extend_from_slice(checksum.as_bytes());
3189        pack
3190    }
3191
3192    fn unique_temp_dir(name: &str) -> PathBuf {
3193        let nanos = SystemTime::now()
3194            .duration_since(UNIX_EPOCH)
3195            .expect("test operation should succeed")
3196            .as_nanos();
3197        std::env::temp_dir().join(format!("sley-{name}-{}-{nanos}", std::process::id()))
3198    }
3199
3200    fn run_git_success(cwd: &Path, args: &[&str]) {
3201        let output = Command::new("git")
3202            .current_dir(cwd)
3203            .args(args)
3204            .output()
3205            .unwrap_or_else(|err| panic!("failed to run git {args:?}: {err}"));
3206        assert!(
3207            output.status.success(),
3208            "git {args:?} failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
3209            output.status.code(),
3210            String::from_utf8_lossy(&output.stdout),
3211            String::from_utf8_lossy(&output.stderr)
3212        );
3213    }
3214
3215    fn single_path_with_extension(dir: &Path, extension: &str) -> PathBuf {
3216        let mut paths = fs::read_dir(dir)
3217            .expect("test operation should succeed")
3218            .map(|entry| entry.expect("test operation should succeed").path())
3219            .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some(extension))
3220            .collect::<Vec<_>>();
3221        assert_eq!(paths.len(), 1, "expected one .{extension} file");
3222        paths.remove(0)
3223    }
3224
3225    fn largest_path_with_extension(dir: &Path, extension: &str) -> PathBuf {
3226        fs::read_dir(dir)
3227            .expect("read fixture directory")
3228            .map(|entry| entry.expect("read fixture entry").path())
3229            .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some(extension))
3230            .max_by_key(|path| fs::metadata(path).expect("read fixture metadata").len())
3231            .expect("at least one fixture path")
3232    }
3233
3234    fn pack_bitmap_index(
3235        format: ObjectFormat,
3236        object_count: u32,
3237        options: u16,
3238        pack_checksum: &ObjectId,
3239        entries: &[(u32, u8, u8, &[u64])],
3240        name_hash_cache: Option<&[u32]>,
3241    ) -> Vec<u8> {
3242        let mut out = Vec::new();
3243        out.extend_from_slice(b"BITM");
3244        out.extend_from_slice(&1u16.to_be_bytes());
3245        out.extend_from_slice(&options.to_be_bytes());
3246        out.extend_from_slice(&(entries.len() as u32).to_be_bytes());
3247        out.extend_from_slice(pack_checksum.as_bytes());
3248        write_test_ewah(&mut out, object_count, &[0b001]);
3249        write_test_ewah(&mut out, object_count, &[0b010]);
3250        write_test_ewah(&mut out, object_count, &[0b100]);
3251        write_test_ewah(&mut out, object_count, &[0]);
3252        for (position, xor_offset, flags, words) in entries {
3253            out.extend_from_slice(&position.to_be_bytes());
3254            out.push(*xor_offset);
3255            out.push(*flags);
3256            write_test_ewah(&mut out, object_count, words);
3257        }
3258        if let Some(cache) = name_hash_cache {
3259            for value in cache {
3260                out.extend_from_slice(&value.to_be_bytes());
3261            }
3262        }
3263        let checksum =
3264            sley_core::digest_bytes(format, &out).expect("test operation should succeed");
3265        out.extend_from_slice(checksum.as_bytes());
3266        out
3267    }
3268
3269    fn write_test_ewah(out: &mut Vec<u8>, bit_size: u32, literals: &[u64]) {
3270        out.extend_from_slice(&bit_size.to_be_bytes());
3271        let words = ewah_literal_words(literals);
3272        out.extend_from_slice(&(words.len() as u32).to_be_bytes());
3273        for word in words {
3274            out.extend_from_slice(&word.to_be_bytes());
3275        }
3276        out.extend_from_slice(&0u32.to_be_bytes());
3277    }
3278
3279    fn ewah_literal_words(literals: &[u64]) -> Vec<u64> {
3280        let rlw = (literals.len() as u64) << 33;
3281        let mut words = vec![rlw];
3282        words.extend_from_slice(literals);
3283        words
3284    }
3285
3286    fn refresh_trailing_checksum(format: ObjectFormat, bytes: &mut [u8]) {
3287        let checksum_offset = bytes.len() - format.raw_len();
3288        let checksum = sley_core::digest_bytes(format, &bytes[..checksum_offset])
3289            .expect("test operation should succeed");
3290        bytes[checksum_offset..].copy_from_slice(checksum.as_bytes());
3291    }
3292
3293    fn append_suffix_delta(base: &[u8], result: &[u8]) -> Vec<u8> {
3294        assert!(result.starts_with(base));
3295        let suffix = &result[base.len()..];
3296        assert!(base.len() < 0x10000);
3297        assert!(suffix.len() < 0x80);
3298        let mut delta = Vec::new();
3299        write_delta_varint(&mut delta, base.len() as u64);
3300        write_delta_varint(&mut delta, result.len() as u64);
3301        delta.push(0x90);
3302        delta.push(base.len() as u8);
3303        delta.push(suffix.len() as u8);
3304        delta.extend_from_slice(suffix);
3305        delta
3306    }
3307
3308    fn write_delta_varint(out: &mut Vec<u8>, mut value: u64) {
3309        loop {
3310            let mut byte = (value as u8) & 0x7f;
3311            value >>= 7;
3312            if value != 0 {
3313                byte |= 0x80;
3314            }
3315            out.push(byte);
3316            if value == 0 {
3317                break;
3318            }
3319        }
3320    }
3321
3322    fn write_pack_entry_header_kind(out: &mut Vec<u8>, type_code: u8, mut size: u64) {
3323        let mut byte = (type_code << 4) | ((size as u8) & 0x0f);
3324        size >>= 4;
3325        if size != 0 {
3326            byte |= 0x80;
3327        }
3328        out.push(byte);
3329        while size != 0 {
3330            let mut byte = (size as u8) & 0x7f;
3331            size >>= 7;
3332            if size != 0 {
3333                byte |= 0x80;
3334            }
3335            out.push(byte);
3336        }
3337    }
3338
3339    fn write_ofs_delta_offset(out: &mut Vec<u8>, relative: usize) {
3340        assert!(relative < 0x80);
3341        out.push(relative as u8);
3342    }
3343
3344    fn single_entry_index(
3345        format: ObjectFormat,
3346        oid: ObjectId,
3347        crc32: u32,
3348        offset: u32,
3349        pack_checksum: ObjectId,
3350    ) -> Vec<u8> {
3351        let mut index = Vec::new();
3352        index.extend_from_slice(&[0xff, b't', b'O', b'c']);
3353        index.extend_from_slice(&2u32.to_be_bytes());
3354        for idx in 0..256 {
3355            let count = if idx >= usize::from(oid.as_bytes()[0]) {
3356                1u32
3357            } else {
3358                0u32
3359            };
3360            index.extend_from_slice(&count.to_be_bytes());
3361        }
3362        index.extend_from_slice(oid.as_bytes());
3363        index.extend_from_slice(&crc32.to_be_bytes());
3364        index.extend_from_slice(&offset.to_be_bytes());
3365        index.extend_from_slice(pack_checksum.as_bytes());
3366        let checksum =
3367            sley_core::digest_bytes(format, &index).expect("test operation should succeed");
3368        index.extend_from_slice(checksum.as_bytes());
3369        index
3370    }
3371
3372    fn single_entry_index_v1(
3373        format: ObjectFormat,
3374        oid: ObjectId,
3375        offset: u32,
3376        pack_checksum: ObjectId,
3377    ) -> Vec<u8> {
3378        let mut index = Vec::new();
3379        for idx in 0..256 {
3380            let count = if idx >= usize::from(oid.as_bytes()[0]) {
3381                1u32
3382            } else {
3383                0u32
3384            };
3385            index.extend_from_slice(&count.to_be_bytes());
3386        }
3387        index.extend_from_slice(&offset.to_be_bytes());
3388        index.extend_from_slice(oid.as_bytes());
3389        index.extend_from_slice(pack_checksum.as_bytes());
3390        let checksum =
3391            sley_core::digest_bytes(format, &index).expect("test operation should succeed");
3392        index.extend_from_slice(checksum.as_bytes());
3393        index
3394    }
3395
3396    fn pack_reverse_index(
3397        format: ObjectFormat,
3398        positions: &[u32],
3399        pack_checksum: ObjectId,
3400    ) -> Vec<u8> {
3401        let mut reverse_index = Vec::new();
3402        reverse_index.extend_from_slice(b"RIDX");
3403        reverse_index.extend_from_slice(&1u32.to_be_bytes());
3404        reverse_index.extend_from_slice(&hash_function_id(format).to_be_bytes());
3405        for position in positions {
3406            reverse_index.extend_from_slice(&position.to_be_bytes());
3407        }
3408        reverse_index.extend_from_slice(pack_checksum.as_bytes());
3409        let checksum =
3410            sley_core::digest_bytes(format, &reverse_index).expect("test operation should succeed");
3411        reverse_index.extend_from_slice(checksum.as_bytes());
3412        reverse_index
3413    }
3414
3415    fn pack_mtimes(format: ObjectFormat, mtimes: &[u32], pack_checksum: ObjectId) -> Vec<u8> {
3416        let mut out = Vec::new();
3417        out.extend_from_slice(b"MTME");
3418        out.extend_from_slice(&1u32.to_be_bytes());
3419        out.extend_from_slice(&hash_function_id(format).to_be_bytes());
3420        for mtime in mtimes {
3421            out.extend_from_slice(&mtime.to_be_bytes());
3422        }
3423        out.extend_from_slice(pack_checksum.as_bytes());
3424        let checksum =
3425            sley_core::digest_bytes(format, &out).expect("test operation should succeed");
3426        out.extend_from_slice(checksum.as_bytes());
3427        out
3428    }
3429
3430    fn midx_chunks_with_pack_names(
3431        _format: ObjectFormat,
3432        pack_names: Vec<u8>,
3433        entries: &[(ObjectId, u32, u64)],
3434    ) -> Vec<([u8; 4], Vec<u8>)> {
3435        let mut entries = entries.to_vec();
3436        entries.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
3437        let object_ids: Vec<ObjectId> = entries.iter().map(|entry| entry.0).collect();
3438        let mut large_offsets = Vec::new();
3439        let mut chunks = vec![
3440            (*b"PNAM", pack_names),
3441            (*b"OIDF", midx_oid_fanout(&object_ids)),
3442            (*b"OIDL", midx_oid_lookup(&object_ids)),
3443            (
3444                *b"OOFF",
3445                midx_ooff_entries(
3446                    &entries
3447                        .iter()
3448                        .map(|(_oid, pack_int_id, offset)| (*pack_int_id, *offset))
3449                        .collect::<Vec<_>>(),
3450                    &mut large_offsets,
3451                ),
3452            ),
3453        ];
3454        if !large_offsets.is_empty() {
3455            chunks.push((*b"LOFF", large_offsets));
3456        }
3457        chunks
3458    }
3459
3460    fn midx_oid_fanout(object_ids: &[ObjectId]) -> Vec<u8> {
3461        let mut counts = [0u32; 256];
3462        for oid in object_ids {
3463            counts[oid.as_bytes()[0] as usize] += 1;
3464        }
3465        let mut running = 0u32;
3466        let mut out = Vec::new();
3467        for count in counts {
3468            running += count;
3469            out.extend_from_slice(&running.to_be_bytes());
3470        }
3471        out
3472    }
3473
3474    fn midx_oid_lookup(object_ids: &[ObjectId]) -> Vec<u8> {
3475        let mut out = Vec::new();
3476        for oid in object_ids {
3477            out.extend_from_slice(oid.as_bytes());
3478        }
3479        out
3480    }
3481
3482    fn midx_ooff_entries(entries: &[(u32, u64)], large_offsets: &mut Vec<u8>) -> Vec<u8> {
3483        let mut out = Vec::new();
3484        for (pack_int_id, offset) in entries {
3485            out.extend_from_slice(&pack_int_id.to_be_bytes());
3486            if *offset < 0x8000_0000 {
3487                out.extend_from_slice(&(*offset as u32).to_be_bytes());
3488            } else {
3489                let large_idx = (large_offsets.len() / 8) as u32;
3490                out.extend_from_slice(&(0x8000_0000 | large_idx).to_be_bytes());
3491                large_offsets.extend_from_slice(&offset.to_be_bytes());
3492            }
3493        }
3494        out
3495    }
3496
3497    fn midx_u32_table(values: &[u32]) -> Vec<u8> {
3498        let mut out = Vec::new();
3499        for value in values {
3500            out.extend_from_slice(&value.to_be_bytes());
3501        }
3502        out
3503    }
3504
3505    fn midx_bitmap_packs(entries: &[(u32, u32)]) -> Vec<u8> {
3506        let mut out = Vec::new();
3507        for (bitmap_pos, bitmap_nr) in entries {
3508            out.extend_from_slice(&bitmap_pos.to_be_bytes());
3509            out.extend_from_slice(&bitmap_nr.to_be_bytes());
3510        }
3511        out
3512    }
3513
3514    fn multi_pack_index(
3515        format: ObjectFormat,
3516        version: u8,
3517        pack_count: u32,
3518        chunks: &[([u8; 4], Vec<u8>)],
3519    ) -> Vec<u8> {
3520        let lookup_len = (chunks.len() + 1) * 12;
3521        let mut out = Vec::new();
3522        out.extend_from_slice(b"MIDX");
3523        out.push(version);
3524        out.push(hash_function_id(format) as u8);
3525        out.push(chunks.len() as u8);
3526        out.push(0);
3527        out.extend_from_slice(&pack_count.to_be_bytes());
3528        let mut chunk_offset = (12 + lookup_len) as u64;
3529        for (id, data) in chunks {
3530            out.extend_from_slice(id);
3531            out.extend_from_slice(&chunk_offset.to_be_bytes());
3532            chunk_offset += data.len() as u64;
3533        }
3534        out.extend_from_slice(&[0, 0, 0, 0]);
3535        out.extend_from_slice(&chunk_offset.to_be_bytes());
3536        for (_id, data) in chunks {
3537            out.extend_from_slice(data);
3538        }
3539        let checksum =
3540            sley_core::digest_bytes(format, &out).expect("test operation should succeed");
3541        out.extend_from_slice(checksum.as_bytes());
3542        out
3543    }
3544
3545    // ---- EWAH encoder / bitmap writer tests ------------------------------
3546
3547    fn pack_checksum_sha1() -> ObjectId {
3548        sley_core::digest_bytes(ObjectFormat::Sha1, b"pack").expect("test operation should succeed")
3549    }
3550
3551    fn parse_ewah_bytes(bytes: &[u8]) -> EwahBitmap {
3552        // Wrap the EWAH body with the surrounding offset bookkeeping the parser
3553        // expects: a checksum offset that lies just past the serialised bitmap.
3554        let mut offset = 0usize;
3555        let checksum_offset = bytes.len();
3556        parse_bitmap_ewah(bytes, &mut offset, checksum_offset, 0)
3557            .expect("test operation should succeed")
3558    }
3559
3560    #[test]
3561    fn ewah_encodes_single_literal_word_matching_helper() {
3562        // A bitmap whose only word is a literal must serialise as one RLW with
3563        // literal_len == 1 followed by the literal, identical to the test
3564        // helper used by the existing parser tests.
3565        let ewah = EwahBitmap::from_words(64, &[0b101]).expect("test operation should succeed");
3566        assert_eq!(ewah.words, ewah_literal_words(&[0b101]));
3567        assert_eq!(ewah.rlw_position, 0);
3568        assert_eq!(ewah.bit_size, 64);
3569    }
3570
3571    #[test]
3572    fn ewah_byte_layout_is_big_endian() {
3573        let ewah = EwahBitmap::from_words(64, &[0x0102_0304_0506_0708])
3574            .expect("test operation should succeed");
3575        let bytes = ewah.to_bytes();
3576        let mut expected = Vec::new();
3577        expected.extend_from_slice(&64u32.to_be_bytes()); // bit_size
3578        expected.extend_from_slice(&2u32.to_be_bytes()); // word count: rlw + literal
3579        expected.extend_from_slice(&(1u64 << 33).to_be_bytes()); // rlw: literal_len = 1
3580        expected.extend_from_slice(&0x0102_0304_0506_0708u64.to_be_bytes());
3581        expected.extend_from_slice(&0u32.to_be_bytes()); // rlw_position
3582        assert_eq!(bytes, expected);
3583    }
3584
3585    #[test]
3586    fn ewah_empty_bitmap_serialises_like_git() {
3587        let ewah = EwahBitmap::empty();
3588        let bytes = ewah.to_bytes();
3589        // bit_size = 0, word_count = 0, rlw_position = 0.
3590        assert_eq!(bytes, vec![0u8; 12]);
3591        // It must still parse and decode to nothing.
3592        let parsed = parse_ewah_bytes(&bytes);
3593        assert_eq!(parsed, ewah);
3594        assert!(
3595            parsed
3596                .to_positions()
3597                .expect("test operation should succeed")
3598                .is_empty()
3599        );
3600    }
3601
3602    #[test]
3603    fn ewah_compresses_clean_zero_run() {
3604        // Three all-zero words followed by a literal: the encoder should emit a
3605        // single RLW carrying a run of 3 clean-zero words plus one literal.
3606        let ewah =
3607            EwahBitmap::from_words(256, &[0, 0, 0, 0b1]).expect("test operation should succeed");
3608        assert_eq!(ewah.words.len(), 2, "expected one RLW plus one literal");
3609        let rlw = ewah.words[0];
3610        assert_eq!(rlw & 1, 0, "run bit should be zero");
3611        assert_eq!((rlw >> 1) & 0xffff_ffff, 3, "run length should be 3");
3612        assert_eq!(rlw >> 33, 1, "literal length should be 1");
3613        assert_eq!(ewah.words[1], 0b1);
3614    }
3615
3616    #[test]
3617    fn ewah_compresses_clean_ones_run() {
3618        let ewah = EwahBitmap::from_words(192, &[u64::MAX, u64::MAX, u64::MAX])
3619            .expect("test operation should succeed");
3620        // Pure run of ones, no literals: one RLW only.
3621        assert_eq!(ewah.words.len(), 1);
3622        let rlw = ewah.words[0];
3623        assert_eq!(rlw & 1, 1, "run bit should be one");
3624        assert_eq!((rlw >> 1) & 0xffff_ffff, 3, "run length should be 3");
3625        assert_eq!(rlw >> 33, 0, "no literals");
3626    }
3627
3628    #[test]
3629    fn ewah_run_then_literal_then_run_roundtrips() {
3630        let words = vec![0, 0, 0xdead_beef, u64::MAX, u64::MAX, 0, 0xabc];
3631        let bit_size = (words.len() * 64) as u32;
3632        let ewah = EwahBitmap::from_words(bit_size, &words).expect("test operation should succeed");
3633        assert_eq!(
3634            ewah.to_words().expect("test operation should succeed"),
3635            words
3636        );
3637    }
3638
3639    #[test]
3640    fn ewah_drops_trailing_clean_zero_words() {
3641        // Trailing all-zero words beyond a literal carry no information and git
3642        // does not serialise them, but to_words() restores them up to bit_size.
3643        let words = vec![0b1, 0, 0, 0];
3644        let ewah = EwahBitmap::from_words(1, &words).expect("test operation should succeed");
3645        // bit_size of 1 means a single backing word.
3646        assert_eq!(ewah.bit_size, 1);
3647        assert_eq!(
3648            ewah.to_words().expect("test operation should succeed"),
3649            vec![0b1]
3650        );
3651    }
3652
3653    #[test]
3654    fn ewah_from_positions_roundtrips_via_positions() {
3655        let positions = [0u32, 1, 63, 64, 65, 200, 511];
3656        let ewah =
3657            EwahBitmap::from_positions(512, &positions).expect("test operation should succeed");
3658        let mut decoded = ewah.to_positions().expect("test operation should succeed");
3659        decoded.sort_unstable();
3660        assert_eq!(decoded, positions);
3661    }
3662
3663    #[test]
3664    fn ewah_from_positions_dedupes_and_orders() {
3665        let ewah = EwahBitmap::from_positions(128, &[100, 5, 100, 5, 5])
3666            .expect("test operation should succeed");
3667        assert_eq!(
3668            ewah.to_positions().expect("test operation should succeed"),
3669            vec![5, 100]
3670        );
3671    }
3672
3673    #[test]
3674    fn ewah_huge_zero_run_spans_multiple_rlws() {
3675        // A run longer than the 32-bit running-length field forces the encoder
3676        // to emit more than one RLW. Use one literal bit far out, with a bit
3677        // size large enough to exceed u32::MAX clean words is impractical, so
3678        // assert the field arithmetic via a direct builder run instead.
3679        let mut builder = EwahBuilder::new(0);
3680        builder.add_empty_words(false, 0xffff_ffff);
3681        builder.add_empty_words(false, 5);
3682        let ewah = builder.finish().expect("test operation should succeed");
3683        assert_eq!(ewah.words.len(), 2, "run split across two RLWs");
3684        assert_eq!((ewah.words[0] >> 1) & 0xffff_ffff, 0xffff_ffff);
3685        assert_eq!(ewah.words[1] & 1, 0);
3686        assert_eq!((ewah.words[1] >> 1) & 0xffff_ffff, 5);
3687        assert_eq!(ewah.rlw_position, 1);
3688    }
3689
3690    #[test]
3691    fn ewah_from_words_rejects_oversized_bit_size() {
3692        // bit_size demands two words but only one is supplied.
3693        assert!(EwahBitmap::from_words(65, &[0]).is_err());
3694    }
3695
3696    #[test]
3697    fn ewah_from_positions_rejects_out_of_range() {
3698        assert!(EwahBitmap::from_positions(64, &[64]).is_err());
3699    }
3700
3701    #[test]
3702    fn ewah_serialised_bytes_reparse_to_equal_bitmap() {
3703        // Exercise the full encode -> serialise -> parse loop for a non-trivial
3704        // pattern and assert structural equality against the parser's model.
3705        let words = vec![0, u64::MAX, 0x1234_5678_9abc_def0, 0, 0, 0xff];
3706        let bit_size = (words.len() * 64) as u32;
3707        let ewah = EwahBitmap::from_words(bit_size, &words).expect("test operation should succeed");
3708        let bytes = ewah.to_bytes();
3709        let parsed = parse_ewah_bytes(&bytes);
3710        assert_eq!(parsed, ewah);
3711        assert_eq!(
3712            parsed.to_words().expect("test operation should succeed"),
3713            words
3714        );
3715    }
3716
3717    #[test]
3718    fn pack_bitmap_index_write_parse_roundtrip_sha1() {
3719        // commit, tree, blob in pack order; one selected commit reaching all.
3720        let object_types = [ObjectType::Commit, ObjectType::Tree, ObjectType::Blob];
3721        let bytes = write_bitmap(
3722            ObjectFormat::Sha1,
3723            pack_checksum_sha1(),
3724            &object_types,
3725            &[(0u32, 0u32, vec![1u32, 2u32])],
3726            None,
3727        )
3728        .expect("test operation should succeed");
3729        assert_eq!(&bytes[..4], b"BITM");
3730
3731        let parsed = PackBitmapIndex::parse(&bytes, ObjectFormat::Sha1, 3)
3732            .expect("test operation should succeed");
3733        assert_eq!(parsed.version, 1);
3734        assert_eq!(parsed.options, PackBitmapIndex::OPTION_FULL_DAG);
3735        assert_eq!(parsed.pack_checksum, pack_checksum_sha1());
3736        assert_eq!(
3737            parsed
3738                .type_bitmaps
3739                .commits
3740                .to_positions()
3741                .expect("test operation should succeed"),
3742            vec![0]
3743        );
3744        assert_eq!(
3745            parsed
3746                .type_bitmaps
3747                .trees
3748                .to_positions()
3749                .expect("test operation should succeed"),
3750            vec![1]
3751        );
3752        assert_eq!(
3753            parsed
3754                .type_bitmaps
3755                .blobs
3756                .to_positions()
3757                .expect("test operation should succeed"),
3758            vec![2]
3759        );
3760        assert!(
3761            parsed
3762                .type_bitmaps
3763                .tags
3764                .to_positions()
3765                .expect("test operation should succeed")
3766                .is_empty()
3767        );
3768        assert_eq!(parsed.entries.len(), 1);
3769        let entry = parsed
3770            .entry_for_index_position(0)
3771            .expect("test operation should succeed");
3772        assert_eq!(entry.xor_offset, 0);
3773        assert_eq!(entry.flags, 0);
3774        assert_eq!(
3775            entry
3776                .bitmap
3777                .to_positions()
3778                .expect("test operation should succeed"),
3779            vec![0, 1, 2]
3780        );
3781        assert_eq!(parsed.name_hash_cache, None);
3782    }
3783
3784    #[test]
3785    fn pack_bitmap_index_write_parse_roundtrip_sha256() {
3786        let pack_checksum = sley_core::digest_bytes(ObjectFormat::Sha256, b"pack")
3787            .expect("test operation should succeed");
3788        let object_types = [ObjectType::Commit, ObjectType::Tree];
3789        let bytes = write_bitmap(
3790            ObjectFormat::Sha256,
3791            pack_checksum.clone(),
3792            &object_types,
3793            &[(0u32, 0u32, vec![1u32])],
3794            None,
3795        )
3796        .expect("test operation should succeed");
3797        let parsed = PackBitmapIndex::parse(&bytes, ObjectFormat::Sha256, 2)
3798            .expect("test operation should succeed");
3799        assert_eq!(parsed.format, ObjectFormat::Sha256);
3800        assert_eq!(parsed.pack_checksum, pack_checksum);
3801        assert_eq!(parsed.index_checksum.format(), ObjectFormat::Sha256);
3802        assert_eq!(
3803            parsed.entries[0]
3804                .bitmap
3805                .to_positions()
3806                .expect("test operation should succeed"),
3807            vec![0, 1]
3808        );
3809    }
3810
3811    #[test]
3812    fn pack_bitmap_index_write_includes_name_hash_cache() {
3813        let object_types = [ObjectType::Commit, ObjectType::Tree, ObjectType::Blob];
3814        let cache = vec![0x1111_1111u32, 0x2222_2222, 0x3333_3333];
3815        let bytes = write_bitmap(
3816            ObjectFormat::Sha1,
3817            pack_checksum_sha1(),
3818            &object_types,
3819            &[(0u32, 0u32, vec![1u32, 2u32])],
3820            Some(cache.clone()),
3821        )
3822        .expect("test operation should succeed");
3823        let parsed = PackBitmapIndex::parse(&bytes, ObjectFormat::Sha1, 3)
3824            .expect("test operation should succeed");
3825        assert_eq!(
3826            parsed.options,
3827            PackBitmapIndex::OPTION_FULL_DAG | PackBitmapIndex::OPTION_HASH_CACHE
3828        );
3829        assert_eq!(parsed.name_hash_cache, Some(cache));
3830    }
3831
3832    #[test]
3833    fn pack_bitmap_writer_supports_multiple_commits() {
3834        let object_types = [
3835            ObjectType::Commit,
3836            ObjectType::Commit,
3837            ObjectType::Tree,
3838            ObjectType::Blob,
3839        ];
3840        let mut writer =
3841            PackBitmapWriter::new(ObjectFormat::Sha1, pack_checksum_sha1(), &object_types)
3842                .expect("test operation should succeed");
3843        writer
3844            .add_commit(0, 0, &[2, 3])
3845            .expect("test operation should succeed");
3846        writer
3847            .add_commit(1, 1, &[2])
3848            .expect("test operation should succeed");
3849        let bytes = writer.write().expect("test operation should succeed");
3850        let parsed = PackBitmapIndex::parse(&bytes, ObjectFormat::Sha1, 4)
3851            .expect("test operation should succeed");
3852        assert_eq!(parsed.entries.len(), 2);
3853        assert_eq!(
3854            parsed
3855                .type_bitmaps
3856                .commits
3857                .to_positions()
3858                .expect("test operation should succeed"),
3859            vec![0, 1]
3860        );
3861        let first = parsed
3862            .entry_for_index_position(0)
3863            .expect("test operation should succeed");
3864        assert_eq!(
3865            first
3866                .bitmap
3867                .to_positions()
3868                .expect("test operation should succeed"),
3869            vec![0, 2, 3]
3870        );
3871        let second = parsed
3872            .entry_for_index_position(1)
3873            .expect("test operation should succeed");
3874        assert_eq!(
3875            second
3876                .bitmap
3877                .to_positions()
3878                .expect("test operation should succeed"),
3879            vec![1, 2]
3880        );
3881    }
3882
3883    #[test]
3884    fn pack_bitmap_writer_roundtrips_lookup_table() {
3885        let object_types = [ObjectType::Commit, ObjectType::Commit, ObjectType::Tree];
3886        let mut writer =
3887            PackBitmapWriter::new(ObjectFormat::Sha1, pack_checksum_sha1(), &object_types)
3888                .expect("test operation should succeed")
3889                .with_lookup_table(true);
3890        writer
3891            .add_commit(0, 1, &[2])
3892            .expect("test operation should succeed");
3893        writer
3894            .add_commit(1, 0, &[2])
3895            .expect("test operation should succeed");
3896        let bytes = writer.write().expect("test operation should succeed");
3897        let parsed = PackBitmapIndex::parse(&bytes, ObjectFormat::Sha1, 3)
3898            .expect("test operation should succeed");
3899        assert!(parsed.lookup_table);
3900        assert_ne!(parsed.options & PackBitmapIndex::OPTION_LOOKUP_TABLE, 0);
3901        assert_eq!(parsed.entries.len(), 2);
3902    }
3903
3904    #[test]
3905    fn pack_bitmap_index_recomputes_checksum_on_write() {
3906        // The provided index_checksum field is ignored; write recomputes it so
3907        // a bogus placeholder still produces a valid, parseable file.
3908        let object_types = [ObjectType::Commit, ObjectType::Blob];
3909        let writer = PackBitmapWriter::new(ObjectFormat::Sha1, pack_checksum_sha1(), &object_types)
3910            .expect("test operation should succeed");
3911        let mut index = writer.build().expect("test operation should succeed");
3912        // build() sets an all-zero placeholder checksum.
3913        assert_eq!(index.index_checksum.as_bytes(), [0u8; 20]);
3914        index.entries.clear(); // mutate the model after build
3915        index.entries.push(PackBitmapEntry {
3916            object_position: 0,
3917            xor_offset: 0,
3918            flags: 0,
3919            bitmap: EwahBitmap::from_positions(2, &[0, 1]).expect("test operation should succeed"),
3920        });
3921        let bytes = index.write().expect("test operation should succeed");
3922        // Parsing validates the trailing checksum, so a wrong checksum fails.
3923        let parsed = PackBitmapIndex::parse(&bytes, ObjectFormat::Sha1, 2)
3924            .expect("test operation should succeed");
3925        assert_ne!(parsed.index_checksum.as_bytes(), [0u8; 20]);
3926    }
3927
3928    #[test]
3929    fn pack_bitmap_writer_rejects_non_commit_selection() {
3930        let object_types = [ObjectType::Commit, ObjectType::Blob];
3931        let mut writer =
3932            PackBitmapWriter::new(ObjectFormat::Sha1, pack_checksum_sha1(), &object_types)
3933                .expect("test operation should succeed");
3934        // Position 1 is a blob, not a commit.
3935        assert!(writer.add_commit(1, 1, &[]).is_err());
3936        // Position 5 is out of range entirely.
3937        assert!(writer.add_commit(5, 5, &[]).is_err());
3938        // Index position out of range.
3939        assert!(writer.add_commit(0, 5, &[]).is_err());
3940        // Reachable position out of range.
3941        assert!(writer.add_commit(0, 0, &[9]).is_err());
3942    }
3943
3944    #[test]
3945    fn pack_bitmap_writer_rejects_checksum_format_mismatch() {
3946        let sha256_checksum = sley_core::digest_bytes(ObjectFormat::Sha256, b"pack")
3947            .expect("test operation should succeed");
3948        assert!(
3949            PackBitmapWriter::new(ObjectFormat::Sha1, sha256_checksum, &[ObjectType::Commit])
3950                .is_err()
3951        );
3952    }
3953
3954    #[test]
3955    fn pack_bitmap_writer_rejects_bad_name_hash_cache_len() {
3956        let writer = PackBitmapWriter::new(
3957            ObjectFormat::Sha1,
3958            pack_checksum_sha1(),
3959            &[ObjectType::Commit],
3960        )
3961        .expect("test operation should succeed");
3962        assert!(writer.with_name_hash_cache(vec![1, 2]).is_err());
3963    }
3964
3965    #[test]
3966    fn pack_bitmap_index_write_rejects_inconsistent_cache_flag() {
3967        let mut index = PackBitmapWriter::new(
3968            ObjectFormat::Sha1,
3969            pack_checksum_sha1(),
3970            &[ObjectType::Commit],
3971        )
3972        .expect("test operation should succeed")
3973        .build()
3974        .expect("test operation should succeed");
3975        // Flag set but no cache present.
3976        index.options |= PackBitmapIndex::OPTION_HASH_CACHE;
3977        assert!(index.write().is_err());
3978        // Cache present but flag missing.
3979        index.options = PackBitmapIndex::OPTION_FULL_DAG;
3980        index.name_hash_cache = Some(vec![0]);
3981        assert!(index.write().is_err());
3982    }
3983
3984    #[test]
3985    fn write_bitmap_roundtrips_through_upstream_git_parser() {
3986        // Build a real pack with git, then overwrite reachability with our own
3987        // writer using the real pack checksum and object types, and confirm our
3988        // bytes parse under the same parser that reads upstream bitmaps.
3989        let root = unique_temp_dir("git-pack-bitmap-writer");
3990        fs::create_dir_all(&root).expect("test operation should succeed");
3991        {
3992            run_git_success(&root, &["init", "-q", "-b", "main"]);
3993            run_git_success(
3994                &root,
3995                &[
3996                    "-c",
3997                    "user.name=Example User",
3998                    "-c",
3999                    "user.email=example@example.invalid",
4000                    "commit",
4001                    "--allow-empty",
4002                    "-q",
4003                    "-m",
4004                    "one",
4005                ],
4006            );
4007            run_git_success(&root, &["repack", "-adb"]);
4008            let pack_dir = root.join(".git").join("objects").join("pack");
4009            let idx_path = single_path_with_extension(&pack_dir, "idx");
4010            let index = PackIndex::parse(
4011                &fs::read(idx_path).expect("test operation should succeed"),
4012                ObjectFormat::Sha1,
4013            )
4014            .expect("test operation should succeed");
4015            // Read object types from the pack so the type bitmaps are accurate.
4016            let pack_path = single_path_with_extension(&pack_dir, "pack");
4017            let pack =
4018                PackFile::parse_sha1(&fs::read(pack_path).expect("test operation should succeed"))
4019                    .expect("test operation should succeed");
4020            // Map each index entry (sorted by oid) to its pack offset, then to a
4021            // pack-order position so positions line up with the index ordering.
4022            let mut offsets: Vec<u64> = index.entries.iter().map(|entry| entry.offset).collect();
4023            offsets.sort_unstable();
4024            let position_of = |offset: u64| -> u32 {
4025                offsets
4026                    .iter()
4027                    .position(|value| *value == offset)
4028                    .expect("test operation should succeed") as u32
4029            };
4030            let mut object_types = vec![ObjectType::Blob; index.entries.len()];
4031            for entry in &index.entries {
4032                let position = position_of(entry.offset) as usize;
4033                // Find the parsed object at this pack offset to read its type.
4034                if let Some(parsed) = pack
4035                    .entries
4036                    .iter()
4037                    .find(|po| po.entry.offset == entry.offset)
4038                {
4039                    object_types[position] = parsed.object.object_type;
4040                }
4041            }
4042            // Select the first commit position we find and reach everything.
4043            let commit_position = object_types
4044                .iter()
4045                .position(|ty| *ty == ObjectType::Commit)
4046                .expect("test operation should succeed") as u32;
4047            // The entry records the commit's position in the oid-sorted index.
4048            let commit_index_position = index
4049                .entries
4050                .iter()
4051                .position(|entry| position_of(entry.offset) == commit_position)
4052                .expect("test operation should succeed")
4053                as u32;
4054            let reachable: Vec<u32> = (0..index.entries.len() as u32).collect();
4055            let bytes = write_bitmap(
4056                ObjectFormat::Sha1,
4057                index.pack_checksum.clone(),
4058                &object_types,
4059                &[(commit_position, commit_index_position, reachable)],
4060                None,
4061            )
4062            .expect("test operation should succeed");
4063            let parsed = PackBitmapIndex::parse(&bytes, ObjectFormat::Sha1, index.entries.len())
4064                .expect("test operation should succeed");
4065            assert_eq!(parsed.pack_checksum, index.pack_checksum);
4066            assert_eq!(parsed.entries.len(), 1);
4067            assert_eq!(
4068                parsed.entries[0]
4069                    .bitmap
4070                    .to_positions()
4071                    .expect("test operation should succeed")
4072                    .len(),
4073                index.entries.len()
4074            );
4075        };
4076        let _ = fs::remove_dir_all(&root);
4077    }
4078
4079    // ---- sley#4 / sley#5: bounds on untrusted pack input --------------------
4080
4081    fn zlib_compress(bytes: &[u8]) -> Vec<u8> {
4082        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
4083        encoder
4084            .write_all(bytes)
4085            .expect("test operation should succeed");
4086        encoder.finish().expect("test operation should succeed")
4087    }
4088
4089    /// A structurally valid 32-byte pack — correct signature, version, and
4090    /// trailing checksum — whose object-count field says `declared`.
4091    fn pack_with_declared_object_count(declared: u32) -> Vec<u8> {
4092        let mut pack = Vec::new();
4093        pack.extend_from_slice(b"PACK");
4094        pack.extend_from_slice(&2u32.to_be_bytes());
4095        pack.extend_from_slice(&declared.to_be_bytes());
4096        let checksum = sley_core::digest_bytes(ObjectFormat::Sha1, &pack)
4097            .expect("test operation should succeed");
4098        pack.extend_from_slice(checksum.as_bytes());
4099        pack
4100    }
4101
4102    /// Regression (sley#4): the pack header's 32-bit object count is
4103    /// attacker-controlled and went straight to `Vec::with_capacity`. A 32-byte
4104    /// pack declaring `u32::MAX` objects asks the allocator for ~480 GiB before
4105    /// a single entry is inspected; that allocation fails and Rust's OOM handler
4106    /// calls `abort()`. Hence the child process: `abort()` takes the whole test
4107    /// binary with it, so neither `JoinHandle::join` nor `catch_unwind` can
4108    /// observe it the way they observe the sley#35 delta bomb.
4109    #[test]
4110    fn rejects_absurd_declared_object_count_without_preallocating() {
4111        const CHILD_ENV: &str = "SLEY_PACK_OBJECT_COUNT_BOMB_CHILD";
4112        const TEST_PATH: &str = "tests::rejects_absurd_declared_object_count_without_preallocating";
4113
4114        if std::env::var_os(CHILD_ENV).is_some() {
4115            for declared in [u32::MAX, 1 << 30, 1 << 24] {
4116                let pack = pack_with_declared_object_count(declared);
4117                assert!(
4118                    PackFile::parse_sha1(&pack).is_err(),
4119                    "a 32-byte pack declaring {declared} objects must be rejected"
4120                );
4121                assert!(
4122                    PackIndex::write_v2_for_pack(&pack, ObjectFormat::Sha1).is_err(),
4123                    "index-pack must reject {declared} objects in a 32-byte pack too"
4124                );
4125                assert!(
4126                    PackFile::verify_pack_stats(&pack, ObjectFormat::Sha1).is_err(),
4127                    "verify-pack must reject {declared} objects in a 32-byte pack too"
4128                );
4129            }
4130            return;
4131        }
4132
4133        let exe = std::env::current_exe().expect("test binary path should be available");
4134        let status = Command::new(exe)
4135            .args(["--exact", TEST_PATH, "--nocapture"])
4136            .env(CHILD_ENV, "1")
4137            .status()
4138            .expect("re-running the test binary should succeed");
4139        assert!(
4140            status.success(),
4141            "child process died ({status:?}) parsing packs with oversized declared \
4142             object counts — the declared count reached an allocation instead of a \
4143             bounds check"
4144        );
4145    }
4146
4147    /// A pack that declares a merely implausible count — not one large enough
4148    /// to abort the process — is still rejected by the header check rather than
4149    /// part-way through the entry loop.
4150    #[test]
4151    fn rejects_declared_object_count_larger_than_pack_can_hold() {
4152        let pack = pack_with_declared_object_count(64);
4153        let error = PackFile::parse_sha1(&pack).expect_err("declared count must be rejected");
4154        assert!(
4155            format!("{error}").contains("only has room for"),
4156            "expected a declared-count bound error, got: {error}"
4157        );
4158    }
4159
4160    /// A delta that ignores its base and emits `result` as a single insert
4161    /// instruction. Every link of a chain built from this is the same tiny
4162    /// size, so chain *depth* is the only variable under test.
4163    fn literal_delta(base_len: usize, result: &[u8]) -> Vec<u8> {
4164        assert!(result.len() <= 0x7f, "insert instruction size is one byte");
4165        let mut delta = Vec::new();
4166        write_delta_varint(&mut delta, base_len as u64);
4167        write_delta_varint(&mut delta, result.len() as u64);
4168        delta.push(result.len() as u8);
4169        delta.extend_from_slice(result);
4170        delta
4171    }
4172
4173    /// Distinct 8-byte bodies, so no two links of a chain share an object id.
4174    fn chain_bodies(depth: usize) -> Vec<Vec<u8>> {
4175        (0..=depth)
4176            .map(|idx| format!("{idx:08}").into_bytes())
4177            .collect()
4178    }
4179
4180    /// One `ofs-delta` chain of `depth` links on top of a single blob, laid out
4181    /// front to back the way a real packer emits it.
4182    fn ofs_delta_chain_pack(format: ObjectFormat, depth: usize) -> Vec<u8> {
4183        let bodies = chain_bodies(depth);
4184        let mut pack = Vec::new();
4185        pack.extend_from_slice(b"PACK");
4186        pack.extend_from_slice(&2u32.to_be_bytes());
4187        pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
4188
4189        let mut base_offset = pack.len();
4190        write_entry_header(&mut pack, ObjectType::Blob, bodies[0].len() as u64);
4191        pack.extend_from_slice(&zlib_compress(&bodies[0]));
4192
4193        for idx in 1..=depth {
4194            let delta = literal_delta(bodies[idx - 1].len(), &bodies[idx]);
4195            let entry_offset = pack.len();
4196            write_pack_entry_header_kind(&mut pack, 6, delta.len() as u64);
4197            write_ofs_delta_offset(&mut pack, entry_offset - base_offset);
4198            pack.extend_from_slice(&zlib_compress(&delta));
4199            base_offset = entry_offset;
4200        }
4201
4202        let checksum =
4203            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
4204        pack.extend_from_slice(checksum.as_bytes());
4205        pack
4206    }
4207
4208    /// One `ref-delta` chain of `depth` links on top of a single blob. When
4209    /// `reversed`, the deltas are laid out deepest-first — legal for ref-deltas
4210    /// (unlike ofs-deltas, whose base must precede them) and the adversarial
4211    /// shape from sley#5: a naive resolver advances the chain by only one link
4212    /// per full-list scan, so an unbounded chain costs one scan per link.
4213    fn ref_delta_chain_pack(format: ObjectFormat, depth: usize, reversed: bool) -> Vec<u8> {
4214        let bodies = chain_bodies(depth);
4215
4216        let mut base_entry = Vec::new();
4217        write_entry_header(&mut base_entry, ObjectType::Blob, bodies[0].len() as u64);
4218        base_entry.extend_from_slice(&zlib_compress(&bodies[0]));
4219
4220        let mut delta_entries = Vec::with_capacity(depth);
4221        for idx in 1..=depth {
4222            let delta = literal_delta(bodies[idx - 1].len(), &bodies[idx]);
4223            let base_oid = sley_core::object_id_for_bytes(format, "blob", &bodies[idx - 1])
4224                .expect("test operation should succeed");
4225            let mut entry = Vec::new();
4226            write_pack_entry_header_kind(&mut entry, 7, delta.len() as u64);
4227            entry.extend_from_slice(base_oid.as_bytes());
4228            entry.extend_from_slice(&zlib_compress(&delta));
4229            delta_entries.push(entry);
4230        }
4231        if reversed {
4232            delta_entries.reverse();
4233        }
4234
4235        let mut pack = Vec::new();
4236        pack.extend_from_slice(b"PACK");
4237        pack.extend_from_slice(&2u32.to_be_bytes());
4238        pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
4239        if !reversed {
4240            pack.extend_from_slice(&base_entry);
4241        }
4242        for entry in &delta_entries {
4243            pack.extend_from_slice(entry);
4244        }
4245        if reversed {
4246            pack.extend_from_slice(&base_entry);
4247        }
4248
4249        let checksum =
4250            sley_core::digest_bytes(format, &pack).expect("test operation should succeed");
4251        pack.extend_from_slice(checksum.as_bytes());
4252        pack
4253    }
4254
4255    #[test]
4256    fn header_read_bounds_ofs_delta_recursion_at_the_shared_ceiling() {
4257        let at_ceiling = ofs_delta_chain_pack(ObjectFormat::Sha1, MAX_READ_DELTA_CHAIN_DEPTH);
4258        let at_ceiling_offset = pack_entry_descriptors(&at_ceiling, ObjectFormat::Sha1)
4259            .last()
4260            .expect("the pack has entries")
4261            .offset;
4262        let header = read_object_header_at(
4263            &at_ceiling,
4264            at_ceiling_offset,
4265            ObjectFormat::Sha1,
4266            0,
4267            |_, _| Ok(None),
4268        )
4269        .expect("a header chain at the ceiling must resolve");
4270        assert_eq!(header.type_and_size(), (ObjectType::Blob, 8));
4271
4272        let over_ceiling = ofs_delta_chain_pack(ObjectFormat::Sha1, 5_000);
4273        let over_ceiling_offset = pack_entry_descriptors(&over_ceiling, ObjectFormat::Sha1)
4274            .last()
4275            .expect("the pack has entries")
4276            .offset;
4277        let error = std::thread::Builder::new()
4278            .stack_size(512 * 1024)
4279            .spawn(move || {
4280                read_object_header_at(
4281                    &over_ceiling,
4282                    over_ceiling_offset,
4283                    ObjectFormat::Sha1,
4284                    0,
4285                    |_, _| Ok(None),
4286                )
4287                .expect_err("a header chain past the ceiling must be rejected")
4288            })
4289            .expect("spawn small-stack thread")
4290            .join()
4291            .expect("the bounded header walk must not overflow its stack");
4292        let rejected_depth = MAX_READ_DELTA_CHAIN_DEPTH + 1;
4293        assert!(
4294            error
4295                .to_string()
4296                .contains(&format!("observed depth {rejected_depth}")),
4297            "expected an actionable depth error, got: {error}"
4298        );
4299    }
4300
4301    #[test]
4302    fn header_read_combines_prior_and_local_delta_depth() {
4303        let pack = ofs_delta_chain_pack(ObjectFormat::Sha1, 2);
4304        let offset = pack_entry_descriptors(&pack, ObjectFormat::Sha1)
4305            .last()
4306            .expect("the pack has entries")
4307            .offset;
4308        let error = read_object_header_at(
4309            &pack,
4310            offset,
4311            ObjectFormat::Sha1,
4312            MAX_READ_DELTA_CHAIN_DEPTH - 1,
4313            |_, _| Ok(None),
4314        )
4315        .expect_err("local ofs-deltas must count prior cross-pack delta links");
4316        let rejected_depth = MAX_READ_DELTA_CHAIN_DEPTH + 1;
4317        assert!(
4318            error
4319                .to_string()
4320                .contains(&format!("observed depth {rejected_depth}")),
4321            "expected the combined depth in the error, got: {error}"
4322        );
4323    }
4324
4325    #[test]
4326    fn header_cache_preserves_the_cumulative_depth_limit() {
4327        let pack = ofs_delta_chain_pack(ObjectFormat::Sha1, 3);
4328        let descriptors = pack_entry_descriptors(&pack, ObjectFormat::Sha1);
4329        let target_offset = descriptors[3].offset;
4330        let cached_base_offset = descriptors[2].offset;
4331        let initial_depth = MAX_READ_DELTA_CHAIN_DEPTH - 2;
4332        let rejected_depth = MAX_READ_DELTA_CHAIN_DEPTH + 1;
4333
4334        let mut direct_cache = MapHeaderTypeCache::default();
4335        read_object_header_at_with_cache(
4336            &pack,
4337            target_offset,
4338            ObjectFormat::Sha1,
4339            0,
4340            |_, _| Ok(None),
4341            &mut direct_cache,
4342        )
4343        .expect("the top-level read should warm the target header");
4344        let direct_error = read_object_header_at_with_cache(
4345            &pack,
4346            target_offset,
4347            ObjectFormat::Sha1,
4348            initial_depth,
4349            |_, _| Ok(None),
4350            &mut direct_cache,
4351        )
4352        .expect_err("a direct cache hit must include its represented chain depth");
4353        assert!(
4354            direct_error
4355                .to_string()
4356                .contains(&format!("observed depth {rejected_depth}")),
4357            "expected the direct cached depth in the error, got: {direct_error}"
4358        );
4359
4360        let mut base_cache = MapHeaderTypeCache::default();
4361        read_object_header_at_with_cache(
4362            &pack,
4363            cached_base_offset,
4364            ObjectFormat::Sha1,
4365            0,
4366            |_, _| Ok(None),
4367            &mut base_cache,
4368        )
4369        .expect("the top-level read should warm the target's immediate base");
4370        let base_error = read_object_header_at_with_cache(
4371            &pack,
4372            target_offset,
4373            ObjectFormat::Sha1,
4374            initial_depth,
4375            |_, _| Ok(None),
4376            &mut base_cache,
4377        )
4378        .expect_err("an internal base-cache hit must include its represented chain depth");
4379        assert!(
4380            base_error
4381                .to_string()
4382                .contains(&format!("observed depth {rejected_depth}")),
4383            "expected the cached base depth in the error, got: {base_error}"
4384        );
4385    }
4386
4387    /// A chain exactly at the ceiling still resolves: the bound must not reject
4388    /// anything sley's own writer (or git at its `pack.depth` default) emits.
4389    #[test]
4390    fn resolves_delta_chain_at_the_depth_ceiling() {
4391        for pack in [
4392            ofs_delta_chain_pack(ObjectFormat::Sha1, DEFAULT_PACK_DEPTH),
4393            ref_delta_chain_pack(ObjectFormat::Sha1, DEFAULT_PACK_DEPTH, false),
4394        ] {
4395            let parsed = PackFile::parse_sha1(&pack).expect("a chain at the ceiling must resolve");
4396            assert_eq!(parsed.entries.len(), DEFAULT_PACK_DEPTH + 1);
4397        }
4398    }
4399
4400    #[test]
4401    fn configured_delta_chain_depth_limit_allows_deeper_pack() {
4402        let limits = PackReadLimits {
4403            max_delta_depth: 60,
4404            ..PackReadLimits::default()
4405        };
4406        let pack = ofs_delta_chain_pack(ObjectFormat::Sha1, 60);
4407
4408        let parsed = PackFile::parse_with_limits(&pack, ObjectFormat::Sha1, limits)
4409            .expect("a chain at the configured ceiling must resolve");
4410        assert_eq!(parsed.entries.len(), 61);
4411
4412        let indexed = PackIndex::write_v2_for_pack_with_limits(&pack, ObjectFormat::Sha1, limits)
4413            .expect("the in-memory indexer must use the configured ceiling");
4414        assert_eq!(indexed.entries.len(), 61);
4415    }
4416
4417    #[test]
4418    fn configured_delta_chain_depth_limit_remains_finite_and_actionable() {
4419        let limits = PackReadLimits {
4420            max_delta_depth: 60,
4421            ..PackReadLimits::default()
4422        };
4423        let pack = ofs_delta_chain_pack(ObjectFormat::Sha1, 61);
4424
4425        let error = PackFile::parse_with_limits(&pack, ObjectFormat::Sha1, limits)
4426            .expect_err("a chain past the configured ceiling must be rejected");
4427        let message = format!("{error}");
4428
4429        assert!(
4430            message.contains("observed depth 61"),
4431            "expected the observed depth in the error, got: {error}"
4432        );
4433        assert!(
4434            message.contains("configured limit 60"),
4435            "expected the configured limit in the error, got: {error}"
4436        );
4437        assert!(
4438            message.contains("PackReadLimits::max_delta_depth")
4439                && message.contains("git repack --depth="),
4440            "expected actionable remedies in the error, got: {error}"
4441        );
4442    }
4443
4444    /// Regression (sley#5): the read path enforced no chain-depth limit at all.
4445    /// `DEFAULT_PACK_DEPTH` existed but was consulted only when *writing*.
4446    #[test]
4447    fn rejects_delta_chain_deeper_than_the_ceiling() {
4448        for pack in [
4449            ofs_delta_chain_pack(ObjectFormat::Sha1, DEFAULT_PACK_DEPTH + 1),
4450            ref_delta_chain_pack(ObjectFormat::Sha1, DEFAULT_PACK_DEPTH + 1, false),
4451        ] {
4452            let error =
4453                PackFile::parse_sha1(&pack).expect_err("a chain past the ceiling must be rejected");
4454            assert!(
4455                format!("{error}").contains("exceeds maximum depth"),
4456                "expected a chain-depth error, got: {error}"
4457            );
4458        }
4459    }
4460
4461    /// Regression (sley#5): a long chain laid out back to front made the former
4462    /// resolver run one full pass per link — O(N^2) scans on top of O(N) delta
4463    /// applications — with nothing to stop it. The dependency-level resolver
4464    /// rejects the chain at the configured depth ceiling.
4465    #[test]
4466    fn rejects_adversarially_ordered_long_delta_chain_promptly() {
4467        let pack = ref_delta_chain_pack(ObjectFormat::Sha1, 5_000, true);
4468        let started = std::time::Instant::now();
4469        let error = PackFile::parse_sha1(&pack)
4470            .expect_err("a long adversarial chain must be rejected, not resolved");
4471        let elapsed = started.elapsed();
4472        assert!(
4473            format!("{error}").contains("exceeds maximum depth"),
4474            "expected a chain-depth error, got: {error}"
4475        );
4476        assert!(
4477            elapsed < std::time::Duration::from_secs(20),
4478            "rejection took {elapsed:?}; resolution is still doing work proportional \
4479             to the chain length"
4480        );
4481    }
4482
4483    /// `index-pack` resolves through the same helper, so it inherits the same
4484    /// ceiling — the bound belongs to the shared resolver, not to one caller.
4485    #[test]
4486    fn index_pack_rejects_delta_chain_deeper_than_the_ceiling() {
4487        let pack = ofs_delta_chain_pack(ObjectFormat::Sha1, DEFAULT_PACK_DEPTH + 1);
4488        let error = PackIndex::write_v2_for_pack(&pack, ObjectFormat::Sha1)
4489            .expect_err("index-pack must reject a chain past the ceiling");
4490        assert!(
4491            format!("{error}").contains("exceeds maximum depth"),
4492            "expected a chain-depth error, got: {error}"
4493        );
4494    }
4495}