Skip to main content

znippy_plugin_git/
resolve.rs

1//! Pack entries → **oids**, which is the one thing [`crate::pack_walk`] cannot
2//! tell you and the `objects` table is keyed by.
3//!
4//! This is the **indexer's** work, not the ack path's (§13.9: "the index is built
5//! after the ack, over a channel"). It inflates every entry, applies every delta
6//! chain and hashes the result, which is what `git index-pack` does and what
7//! makes it the expensive half of a push. Nothing here is called before a client
8//! is told its push landed.
9//!
10//! # What it keeps and what it throws away
11//!
12//! Peak memory is deliberately not "the whole history inflated". **An object is
13//! held only while an unresolved entry still has to read it**, and for nothing
14//! else:
15//!
16//! | object | held | until |
17//! |---|---|---|
18//! | a delta base | yes | its **last** dependent has consumed it |
19//! | everything else — blob, commit, tree, tag | **no** | it leaves through the [`PayloadSink`] and is dropped |
20//!
21//! The base set and, more to the point, **how many entries read each base** are
22//! known before a byte is inflated: the walk already collected every delta base,
23//! so the resolver starts with an exact reference count per base and drops a
24//! payload the moment that count reaches zero. The last dependent gets the base
25//! **moved** into it rather than cloned, so a linear delta chain — one base, one
26//! dependent, the ordinary shape of real history — copies no payload at all.
27//!
28//! ## What this table said before 2026-08-11, and what it cost
29//!
30//! It said commits, trees and tags were kept "because the `graph` and `reach`
31//! tables are built from them". They are not, and had not been since §14's
32//! exploded table became the fold's input — `absorb_one` reads
33//! `Resolved::index_entry()` and nothing else. Two copies of the object set were
34//! being held for a reader that no longer existed:
35//!
36//! * `content` kept every base **and** every typed object for the whole call.
37//!   Nothing ever removed a row, so "held only while something still needs them"
38//!   described an intention, not the code.
39//! * `Resolved::payload` kept a second copy of the same bytes, cloned in.
40//!
41//! MEASURED on oden 2026-08-11, `tests/resolve_peak_memory.rs`, peak **live**
42//! heap over `resolve_walked` against the pack's own inflated payload:
43//!
44//! | pack | before | after |
45//! |---|---:|---:|
46//! | forge-year-8a-240b-12c, 28 893 objects, 111.0 MB | 2.03x | **0.03x** |
47//! | stage-red-nobitmap, 12 328 objects, 343.5 MB | 1.34x (461.2 MB) | **0.02x** (5.9 MB) |
48//! | facett, 7 232 objects, 72.8 MB | 1.20x | **0.42x** |
49//! | znippy's own, 5 388 objects, 84.4 MB | 0.75x | **0.14x** |
50//!
51//! The worst pack in the corpus went 2.03x → 0.42x. It is `facett`, whose
52//! delta chains fan out widely enough that a real base working set is genuinely
53//! live at once — which is the bound behaving as intended rather than a residue.
54//!
55//! End to end, one `gunnar-server`, one `push --mirror`, anonymous RSS held
56//! afterwards (not peak — this memory was never given back):
57//!
58//! | fixture | before | after |
59//! |---|---:|---:|
60//! | 36 172 objects, 290 MB inflated, 5.9 MB pack | 575.7 MB | **185.1 MB** |
61//! | 352 396 objects, 2.89 GB inflated, 61.8 MB pack | 4480.1 MB | **439.9 MB** |
62//!
63//! The second row is the one that mattered: 10x the repository cost 10x the
64//! memory before and 2.4x after, so the resolver's footprint stopped tracking
65//! the history. What is left is bounded and named — redb's page cache
66//! (`ZNIPPY_GIT_REDB_CACHE_BYTES`, 64 MiB by default, measured at 129.3 MB held
67//! with an 8 MiB ceiling and 303.9 MB with 256 MiB), the exploded table's
68//! [`FLUSH_BYTES`](crate::exploded::FLUSH_BYTES) write buffer, and
69//! `Derived`'s in-memory graph, which is genuinely O(repository).
70//!
71//! # ⚠ The clone that got slower, and why it is not this code
72//!
73//! **Read this before "fixing" the reference counting.** `vs_forge_clone` on the
74//! 352 396-object fixture reported this change as a **+78 % CPU regression on
75//! every znippy arm**, and the natural reading — a base dropped at its "last
76//! use" has to be re-resolved when something later wants it again — is wrong. It
77//! is not even mechanically possible: an undercount cannot cause a re-resolve
78//! here, because there is no re-resolve to fall into. `resolve_walked` fails the
79//! whole pack by name.
80//!
81//! What it actually is, MEASURED on oden 2026-08-11, one gunnar-server, seed the
82//! fixture then clone it, steady state (clones 2-3), quiet box, only the
83//! allocator and this file varying:
84//!
85//! | resolver | allocator | clone CPU | anon after seed |
86//! |---|---|---:|---:|
87//! | before | glibc | 1.47 s | 4481 MB |
88//! | **after** | glibc | **2.62 s** | **438 MB** |
89//! | before | mimalloc | 1.18 s | 1767 MB |
90//! | **after** | **mimalloc** | **1.17 s** | **701 MB** |
91//!
92//! With a competent allocator the two resolvers are **identical** (1.18 against
93//! 1.17). The regression is entirely glibc `malloc`: the old resolver's retained
94//! 4.5 GB was accidentally acting as a **pre-warmed pool**, so the clone's own
95//! ~2 GB of allocations came off a free list instead of out of the kernel. Take
96//! the leak away and glibc charges for the memory it should have been charging
97//! for all along.
98//!
99//! Three independent measurements say the same thing, and none of them involve
100//! this file being slower:
101//!
102//! 1. **Bisect.** A build with *only* `Resolved::payload` removed — the
103//!    reference counting NOT applied, `content` still never pruned — reproduces
104//!    the whole regression (2.56–2.62 s). Adding the reference counting on top
105//!    then costs **nothing** (2.55–2.65 s) and takes anon-after-seed from
106//!    2350 MB to 438 MB. The counting is free; the *not leaking* is what glibc
107//!    punishes.
108//! 2. **Restart.** Restart the server after seeding, so the derived tables are
109//!    folded onto a fresh heap: before 2.20–2.22 s, after 2.20–2.24 s —
110//!    identical to within 1 %. The old resolver's 1.47 s is the outlier, not the
111//!    new one's 2.62 s.
112//! 3. **The resolver itself.** Over the same 6 real packs both revisions accept,
113//!    1.058 s before against 0.975 s after: this code is ~8 % *faster*.
114//!
115//! So there is no cheaper trade to find inside this module, and an LRU over
116//! bases would buy nothing. The lever, if the CPU is wanted back, is the
117//! allocator — and `LD_PRELOAD=libmimalloc.so.3` is enough to get it with no
118//! code change at all.
119//!
120//! # Where the payloads it throws away now go
121//!
122//! §14's exploded table is **eager** (decided 2026-08-08), so every object this
123//! module resolves has to reach [`crate::exploded::ExplodedTable`] — including
124//! the blobs the table above says are dropped. They still are: the payload
125//! leaves through a [`PayloadSink`] as it is produced, rather than being
126//! accumulated into the returned `Vec`. That is the difference between a peak
127//! footprint of "the bases plus the typed objects" and one of "the whole pack
128//! inflated at once", and eager resolution does not get to change it.
129//!
130//! A **thin** pack's `REF_DELTA` base is an object the client knows the server
131//! already has, and it is usually a blob. Resolving it needs that blob's
132//! *content*, which is exactly what the exploded table now holds — so the
133//! [`BaseSource`] can answer from it in one point lookup instead of re-resolving
134//! a whole pack. A thin pack whose external base is **still** not re-derivable
135//! is refused by name ([`ResolveError::MissingBase`]) rather than resolved by
136//! guesswork, unchanged.
137
138use std::collections::HashMap;
139
140use anyhow::{Result, anyhow, bail};
141
142use crate::exploded::{NoSink, PayloadSink};
143use crate::index_layout::{IndexEntry, ObjType};
144use crate::object::{GitHashKind, GitObjectKind, canonical};
145use crate::pack_walk::{DeltaBase, PackEntry, PackWalk, walk};
146
147/// Where an entry that deltas against something outside the pack gets its base.
148///
149/// Implemented by the store, which may or may not be able to answer — see the
150/// module docs on §14.
151pub trait BaseSource {
152    /// The object's type and inflated payload, or `None` if this source cannot
153    /// produce it.
154    fn content(&self, oid: &[u8]) -> Option<(GitObjectKind, Vec<u8>)>;
155}
156
157/// A source that has nothing. The right one for a self-contained pack, and it
158/// makes "this pack was thin" an error rather than a silent success.
159pub struct NoBases;
160
161impl BaseSource for NoBases {
162    fn content(&self, _oid: &[u8]) -> Option<(GitObjectKind, Vec<u8>)> {
163        None
164    }
165}
166
167/// One resolved object: **exactly what the `objects` table stores**, and no
168/// payload.
169///
170/// It carried an `Option<Vec<u8>>` payload until 2026-08-11, for a reader that
171/// stopped existing when §14's exploded table became the fold's input. Every
172/// production caller uses [`index_entry`](Self::index_entry) and nothing else —
173/// `absorb_one` builds index rows, and the one other caller discards the vector
174/// entirely and reads its sink. Content has one home now
175/// ([`PayloadSink`], LAW 5) and this struct is the row, so a 343 MB pack's rows
176/// cost the length of the pack's entry list rather than the length of its
177/// history.
178#[derive(Debug, Clone)]
179pub struct Resolved {
180    pub oid: Vec<u8>,
181    /// Byte extent of the **stored, verbatim** entry.
182    pub offset: u64,
183    pub len: u64,
184    /// The type the entry carries in the pack — which for a delta is
185    /// `OfsDelta` / `RefDelta`, exactly as `.idx` cannot tell you and this index
186    /// exists to record.
187    pub stored_type: ObjType,
188    /// The type the chain resolves to. Always one of the four real git types.
189    pub kind: GitObjectKind,
190    /// Inflated, **post-delta-resolution** size — the fact `.idx` and `.rev`
191    /// together still cannot answer.
192    pub uncompressed_size: u64,
193    /// The `objects.delta_base` column: an archive **offset**, `0` for none.
194    pub delta_base: u64,
195}
196
197impl Resolved {
198    /// The row [`crate::read_stack::ObjectReadStack::append`] takes.
199    ///
200    /// `delta_base` now has a column of its own in [`IndexEntry`] and rides
201    /// across in it — it was carried on this struct alone until that column
202    /// existed, and was deliberately never smuggled into `uncompressed_size` or
203    /// any other field on the way. **This is the only place it is written into
204    /// an index row** (LAW 5): [`resolve_walked`] computes it once, in the
205    /// archive's coordinate space, and nothing downstream recomputes it from the
206    /// walk.
207    pub fn index_entry(&self) -> IndexEntry {
208        IndexEntry {
209            oid: self.oid.clone(),
210            offset: self.offset,
211            len: self.len,
212            obj_type: self.stored_type,
213            uncompressed_size: self.uncompressed_size,
214            delta_base: self.delta_base,
215        }
216    }
217}
218
219/// The one failure that is a *decision* rather than a corrupt pack.
220#[derive(Debug)]
221pub struct ResolveError;
222
223impl ResolveError {
224    /// A thin pack's external base, named.
225    pub fn missing_base(oid: &[u8]) -> anyhow::Error {
226        anyhow!(
227            "this pack is thin: it deltas against {} which is not in it, and no base source could \
228             produce that object's content — §14's exploded table does not hold it and it could \
229             not be re-derived from the verbatim packs either, so this repository genuinely does \
230             not have that object. The pack is refused by name rather than half-resolved",
231            hex::encode(oid)
232        )
233    }
234}
235
236/// Resolve every entry of `pack` to an oid, **keeping no payloads**.
237///
238/// `archive_offset` is where the pack's first byte lives in the archive, so the
239/// offsets and `delta_base`s that come back are already in the archive's
240/// coordinate space and no caller has to remember to shift them.
241///
242/// The indexer does **not** come through here — it calls
243/// [`resolve_walked`] with the exploded table as its sink, which is what makes
244/// §14's side table fall out of the same pass rather than out of a second walk.
245pub fn resolve(
246    pack: &[u8],
247    hash: GitHashKind,
248    archive_offset: u64,
249    bases: &dyn BaseSource,
250) -> Result<Vec<Resolved>> {
251    let w = walk(pack, hash.oid_len())?;
252    resolve_walked(pack, &w, hash, archive_offset, bases, &NoSink)
253}
254
255/// Same, for a caller that already walked the pack (`put` does, for the closure
256/// check) and must not walk it twice — and for the one that wants the payloads.
257///
258/// `sink` is handed **every** object exactly once, in resolution order, with the
259/// payload its chain resolves to. Pass [`NoSink`] to keep nothing. This is the
260/// one pass §14's exploded table is built from: no second walk, no second
261/// inflate, no second resolver (LAW 5).
262pub fn resolve_walked(
263    pack: &[u8],
264    w: &PackWalk,
265    hash: GitHashKind,
266    archive_offset: u64,
267    bases: &dyn BaseSource,
268    sink: &dyn PayloadSink,
269) -> Result<Vec<Resolved>> {
270    // **How many entries have to read each base**, known before a byte is
271    // inflated. A count and not a set: the set says "keep this", the count says
272    // "keep this until exactly here", and only the second one lets a payload be
273    // dropped mid-pass. An entry is resolved at most once (the fixpoint removes
274    // it from `remaining` the round it succeeds), so one dependent is one
275    // decrement and the count is exact rather than conservative.
276    let mut ofs_uses: HashMap<u64, usize> = HashMap::new();
277    let mut ref_uses: HashMap<&[u8], usize> = HashMap::new();
278    for e in &w.entries {
279        match &e.delta_base {
280            DeltaBase::Offset(o) => *ofs_uses.entry(*o).or_default() += 1,
281            DeltaBase::Ref(oid) => *ref_uses.entry(oid.as_slice()).or_default() += 1,
282            DeltaBase::None => {}
283        }
284    }
285
286    let mut out: Vec<Option<Resolved>> = vec![None; w.entries.len()];
287    // Payloads held only while something still needs them — and now that is
288    // true of the code and not only of this comment. The `usize` is the number
289    // of dependents that have yet to consume the row; at zero the row goes.
290    let mut content: HashMap<u64, (GitObjectKind, Vec<u8>, usize)> = HashMap::new();
291    // Only for the payloads a `REF_DELTA` in this pack actually names. An oid
292    // nothing ref-deltas against is never looked up here, so recording it would
293    // be 20 bytes and a hash per object of the repository for no reader.
294    let mut oid_to_offset: HashMap<Vec<u8>, u64> = HashMap::new();
295
296    // A fixpoint rather than one pass: a `REF_DELTA` may name a base that is in
297    // this pack but *later* in it, and that is legal. Every round resolves at
298    // least one entry or the pack cannot be resolved at all, so this terminates
299    // in at most `entries` rounds.
300    let mut remaining: Vec<usize> = (0..w.entries.len()).collect();
301    while !remaining.is_empty() {
302        let mut progressed = false;
303        let mut stuck: Vec<usize> = Vec::new();
304        for &i in &remaining {
305            let e = &w.entries[i];
306            let base: Option<(GitObjectKind, Vec<u8>)> = match &e.delta_base {
307                DeltaBase::None => None,
308                DeltaBase::Offset(o) => match consume(&mut content, *o) {
309                    Some(c) => Some(c),
310                    None => {
311                        // Not yet resolved. It cannot be resolved-and-dropped:
312                        // this entry's own use is part of the base's count, so
313                        // the count cannot have reached zero before this line.
314                        stuck.push(i);
315                        continue;
316                    }
317                },
318                DeltaBase::Ref(oid) => match oid_to_offset
319                    .get(oid.as_slice())
320                    .copied()
321                    .and_then(|o| consume(&mut content, o))
322                {
323                    Some(c) => Some(c),
324                    None => match bases.content(oid) {
325                        Some(c) => Some(c),
326                        None => {
327                            stuck.push(i);
328                            continue;
329                        }
330                    },
331                },
332            };
333
334            let (kind, payload) = match base {
335                None => {
336                    let kind = whole_kind(e.obj_type)?;
337                    (kind, inflate(pack, e, hash.oid_len())?)
338                }
339                Some((base_kind, base_payload)) => {
340                    let delta = inflate(pack, e, hash.oid_len())?;
341                    (base_kind, apply_delta(&base_payload, &delta)?)
342                }
343            };
344
345            let oid = hash.oid_of(&canonical(kind, &payload));
346            // **Every object, before anything is dropped.** §14's exploded table
347            // is eager, so the sink sees the blob whose payload the next lines
348            // are about to throw away as well as the ones they keep.
349            sink.explode(&oid, kind, &payload)?;
350            let size = payload.len() as u64;
351
352            // Held only if something still has to read it, and then only for as
353            // many reads as the walk counted. `ref_uses` is consulted by oid
354            // because that is how a `REF_DELTA` names its base; the two counts
355            // add, because one object can be both kinds of base.
356            let by_ref = ref_uses.get(oid.as_slice()).copied().unwrap_or(0);
357            let uses = ofs_uses.get(&e.offset).copied().unwrap_or(0) + by_ref;
358            if uses > 0 {
359                if by_ref > 0 {
360                    oid_to_offset.insert(oid.clone(), e.offset);
361                }
362                content.insert(e.offset, (kind, payload, uses));
363            }
364            // `payload` is gone by here unless a dependent needs it. Nothing
365            // below reads it: the row is the index entry.
366
367            out[i] = Some(Resolved {
368                oid,
369                offset: e.offset + archive_offset,
370                len: e.len,
371                stored_type: e.obj_type,
372                kind,
373                uncompressed_size: size,
374                delta_base: match &e.delta_base {
375                    DeltaBase::Offset(o) => o + archive_offset,
376                    _ => 0,
377                },
378            });
379            progressed = true;
380        }
381        if !progressed {
382            // Nothing moved: every remaining entry waits on a base nobody can
383            // supply. Name the first one — a thin pack is the ordinary reason.
384            let i = stuck[0];
385            if let DeltaBase::Ref(oid) = &w.entries[i].delta_base {
386                return Err(ResolveError::missing_base(oid));
387            }
388            bail!(
389                "entry at offset {} deltas against offset {} which no entry starts at — the pack \
390                 is corrupt",
391                w.entries[i].offset,
392                w.entries[i].delta_base.as_offset()
393            );
394        }
395        remaining = stuck;
396    }
397
398    // `content` is empty here for any pack whose bases were all resolved from
399    // inside it. What can survive is the one case the fixpoint cannot count: a
400    // `REF_DELTA` whose in-pack base had not been reached yet, which
401    // `bases.content` answered from the store instead — that dependent's use is
402    // then never decremented off the in-pack copy. Bounded by the number of
403    // ref-deltas, and it goes out of scope on the next line.
404    Ok(out
405        .into_iter()
406        .map(|r| r.expect("the fixpoint only exits when every slot is filled"))
407        .collect())
408}
409
410/// Take one use off a held base, **moving** the payload out on the last one.
411///
412/// The move is the point. A base with a single dependent — a linear delta
413/// chain, which is what real history is mostly made of — is handed straight to
414/// `apply_delta` with no copy at all, where the previous code cloned every base
415/// on every application. Only a base with more dependents still to come is
416/// cloned, and only for the ones that are not last.
417fn consume(
418    content: &mut HashMap<u64, (GitObjectKind, Vec<u8>, usize)>,
419    at: u64,
420) -> Option<(GitObjectKind, Vec<u8>)> {
421    let (kind, payload, left) = content.get_mut(&at)?;
422    *left -= 1;
423    if *left > 0 {
424        return Some((*kind, payload.clone()));
425    }
426    let (kind, payload, _) = content.remove(&at).expect("just borrowed it");
427    Some((kind, payload))
428}
429
430/// The real git type of a non-delta entry.
431fn whole_kind(t: ObjType) -> Result<GitObjectKind> {
432    Ok(match t {
433        ObjType::Commit => GitObjectKind::Commit,
434        ObjType::Tree => GitObjectKind::Tree,
435        ObjType::Blob => GitObjectKind::Blob,
436        ObjType::Tag => GitObjectKind::Tag,
437        ObjType::OfsDelta | ObjType::RefDelta => {
438            bail!("a delta entry has no type of its own — its base's type is the answer")
439        }
440    })
441}
442
443/// Inflate one entry's stream. The walk already proved the length, so this
444/// allocates exactly the declared size and never grows.
445fn inflate(pack: &[u8], e: &PackEntry, oid_len: usize) -> Result<Vec<u8>> {
446    let start = e.offset as usize;
447    let end = start + e.len as usize;
448    if end > pack.len() {
449        bail!("entry at {start} runs past the end of the pack");
450    }
451    // Skip the header the walk already parsed: the zlib stream is what is left
452    // after the type/size varint and any base reference.
453    let header = header_len(&pack[start..end], e, oid_len)?;
454    let mut out = Vec::with_capacity(e.uncompressed_size as usize);
455    let mut d = flate2::Decompress::new(true);
456    d.decompress_vec(
457        &pack[start + header..end],
458        &mut out,
459        flate2::FlushDecompress::Finish,
460    )
461    .map_err(|err| anyhow!("inflating the entry at {start}: {err}"))?;
462    if out.len() as u64 != e.uncompressed_size {
463        bail!(
464            "the entry at {start} inflated to {} bytes, the walk measured {}",
465            out.len(),
466            e.uncompressed_size
467        );
468    }
469    Ok(out)
470}
471
472/// How many bytes of an entry are header rather than zlib stream. Recomputed
473/// from the bytes rather than remembered, so it cannot drift from the walk's own
474/// reading of them — and the walk's `len` is what bounds it.
475fn header_len(entry: &[u8], e: &PackEntry, oid_len: usize) -> Result<usize> {
476    let mut i = 0usize;
477    let mut cont = true;
478    while cont {
479        let b = *entry
480            .get(i)
481            .ok_or_else(|| anyhow!("the type/size header runs off the entry"))?;
482        cont = b & 0x80 != 0;
483        i += 1;
484    }
485    match e.obj_type {
486        ObjType::OfsDelta => {
487            let mut cont = true;
488            while cont {
489                let b = *entry
490                    .get(i)
491                    .ok_or_else(|| anyhow!("the ofs-delta distance runs off the entry"))?;
492                cont = b & 0x80 != 0;
493                i += 1;
494            }
495        }
496        ObjType::RefDelta => i += oid_len,
497        _ => {}
498    }
499    if i >= entry.len() {
500        bail!("the entry's header consumes all of it, leaving no stream");
501    }
502    Ok(i)
503}
504
505/// git's delta format: two sizes, then copy-from-base and insert-literal
506/// instructions.
507fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
508    let mut i = 0usize;
509    let base_size = delta_varint(delta, &mut i)?;
510    if base_size != base.len() as u64 {
511        bail!(
512            "the delta expects a base of {base_size} bytes, its base is {}",
513            base.len()
514        );
515    }
516    let target_size = delta_varint(delta, &mut i)?;
517    let mut out = Vec::with_capacity(target_size as usize);
518
519    while i < delta.len() {
520        let op = delta[i];
521        i += 1;
522        if op & 0x80 != 0 {
523            // Copy from the base. Offset in up to 4 bytes, size in up to 3;
524            // a zero size means 0x10000, which is git's own special case.
525            let mut off = 0u64;
526            for bit in 0..4 {
527                if op & (1 << bit) != 0 {
528                    off |= u64::from(*delta.get(i).ok_or_else(|| anyhow!("delta ends mid-copy"))?)
529                        << (bit * 8);
530                    i += 1;
531                }
532            }
533            let mut size = 0u64;
534            for bit in 0..3 {
535                if op & (0x10 << bit) != 0 {
536                    size |= u64::from(*delta.get(i).ok_or_else(|| anyhow!("delta ends mid-copy"))?)
537                        << (bit * 8);
538                    i += 1;
539                }
540            }
541            if size == 0 {
542                size = 0x1_0000;
543            }
544            let from = off as usize;
545            let to = from
546                .checked_add(size as usize)
547                .ok_or_else(|| anyhow!("a delta copy range overflows"))?;
548            if to > base.len() {
549                bail!(
550                    "a delta copies base[{from}..{to}] out of a {}-byte base",
551                    base.len()
552                );
553            }
554            out.extend_from_slice(&base[from..to]);
555        } else {
556            // Insert literal. A zero-length insert is not a valid instruction.
557            let n = op as usize;
558            if n == 0 {
559                bail!("a delta carries a zero-length insert instruction");
560            }
561            let end = i
562                .checked_add(n)
563                .ok_or_else(|| anyhow!("a delta insert overflows"))?;
564            if end > delta.len() {
565                bail!("a delta insert of {n} bytes runs off the end");
566            }
567            out.extend_from_slice(&delta[i..end]);
568            i = end;
569        }
570    }
571
572    if out.len() as u64 != target_size {
573        bail!(
574            "the delta declares a {target_size}-byte result and produced {}",
575            out.len()
576        );
577    }
578    Ok(out)
579}
580
581/// The delta header's little-endian 7-bit varint. Not the pack entry's varint.
582fn delta_varint(b: &[u8], i: &mut usize) -> Result<u64> {
583    let mut v = 0u64;
584    let mut shift = 0u32;
585    loop {
586        let byte = *b
587            .get(*i)
588            .ok_or_else(|| anyhow!("a delta size varint runs off the end"))?;
589        *i += 1;
590        if shift >= 64 {
591            bail!("a delta size varint is longer than a u64");
592        }
593        v |= u64::from(byte & 0x7f) << shift;
594        shift += 7;
595        if byte & 0x80 == 0 {
596            return Ok(v);
597        }
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use std::path::{Path, PathBuf};
605
606    /// Parse a real `.idx` (v2) into `(oid, pack offset)` pairs. Twenty lines of
607    /// a frozen format, and it is git's own answer to the question this module
608    /// computes — which is what makes it a differential guard and not a
609    /// restatement of our own code.
610    fn read_idx(bytes: &[u8]) -> Vec<(Vec<u8>, u64)> {
611        assert_eq!(&bytes[0..4], b"\xfftOc", "not an idx v2");
612        assert_eq!(u32::from_be_bytes(bytes[4..8].try_into().unwrap()), 2);
613        let fanout_end = 8 + 256 * 4;
614        let n = u32::from_be_bytes(bytes[fanout_end - 4..fanout_end].try_into().unwrap()) as usize;
615        let oids = fanout_end;
616        let crcs = oids + n * 20;
617        let offs = crcs + n * 4;
618        let big = offs + n * 4;
619        let mut out = Vec::with_capacity(n);
620        for i in 0..n {
621            let oid = bytes[oids + i * 20..oids + i * 20 + 20].to_vec();
622            let raw = u32::from_be_bytes(bytes[offs + i * 4..offs + i * 4 + 4].try_into().unwrap());
623            let offset = if raw & 0x8000_0000 != 0 {
624                let j = (raw & 0x7fff_ffff) as usize;
625                u64::from_be_bytes(bytes[big + j * 8..big + j * 8 + 8].try_into().unwrap())
626            } else {
627                u64::from(raw)
628            };
629            out.push((oid, offset));
630        }
631        out
632    }
633
634    /// Real packs with their real indexes, from this machine's repositories.
635    fn real_pairs(cap: usize) -> Vec<(PathBuf, PathBuf)> {
636        let mut out = Vec::new();
637        let Ok(repos) = std::fs::read_dir(Path::new("/home/rickard/git")) else {
638            return out;
639        };
640        for repo in repos.flatten() {
641            let dir = repo.path().join(".git/objects/pack");
642            let Ok(files) = std::fs::read_dir(&dir) else {
643                continue;
644            };
645            for f in files.flatten() {
646                let p = f.path();
647                if p.extension().is_some_and(|e| e == "pack")
648                    && f.metadata().map(|m| m.len() < 32 << 20).unwrap_or(false)
649                {
650                    let idx = p.with_extension("idx");
651                    if idx.exists() {
652                        out.push((p, idx));
653                        if out.len() >= cap {
654                            return out;
655                        }
656                    }
657                }
658            }
659        }
660        out
661    }
662
663    /// **git's own `.idx` is the arbiter.** For every real pack on this machine,
664    /// every oid we compute and the offset we computed it at must appear in
665    /// git's index, and the two sets must have the same size. A delta applier
666    /// that is subtly wrong produces a different hash and cannot pass this.
667    ///
668    /// Seen RED by changing `size = 0x1_0000` to `size = 0x1_000` in
669    /// [`apply_delta`] — git's zero-size copy special case:
670    /// ".../facett/...pack-62582dd0…: the delta declares a 149392-byte result and
671    /// produced 87952". A single wrong constant in the delta applier and a real
672    /// repository's pack stops resolving.
673    ///
674    /// MEASURED while writing it: **10 899 objects over 3 packs** match git's own
675    /// `.idx` oid for oid and offset for offset, and **3 of the 6 real packs on
676    /// this machine are thin** — they delta against objects that are not in them,
677    /// which is the §14 hole showing up in the wild rather than in theory.
678    #[test]
679    fn every_oid_we_compute_is_the_oid_git_wrote_in_its_idx() {
680        let pairs = real_pairs(6);
681        assert!(
682            !pairs.is_empty(),
683            "no real pack found under /home/rickard/git — this guard has nothing to compare and \
684             must not pass silently"
685        );
686        let mut objects = 0usize;
687        let mut full = 0usize;
688        let mut thin = 0usize;
689        for (pack_path, idx_path) in pairs {
690            let pack = std::fs::read(&pack_path).unwrap();
691            let idx = read_idx(&std::fs::read(&idx_path).unwrap());
692            // MEASURED, and a surprise worth recording: real packs on this
693            // machine DO delta against objects outside themselves. Such a pack
694            // cannot be compared here — resolving it is exactly what §14 leaves
695            // open — but it must not make the guard pass vacuously either, so it
696            // is counted and named and at least one pack has to compare in full.
697            let ours = match resolve(&pack, GitHashKind::Sha1, 0, &NoBases) {
698                Ok(r) => r,
699                Err(e) => {
700                    assert!(
701                        e.to_string().contains("thin"),
702                        "{}: {e}",
703                        pack_path.display()
704                    );
705                    eprintln!("{}: THIN, external base — skipped", pack_path.display());
706                    thin += 1;
707                    continue;
708                }
709            };
710            full += 1;
711
712            assert_eq!(
713                ours.len(),
714                idx.len(),
715                "{}: we resolved {} objects, git indexed {}",
716                pack_path.display(),
717                ours.len(),
718                idx.len()
719            );
720            let theirs: HashMap<Vec<u8>, u64> = idx.into_iter().collect();
721            for r in &ours {
722                match theirs.get(&r.oid) {
723                    Some(&off) => assert_eq!(
724                        off,
725                        r.offset,
726                        "{}: {} is at {} in git's idx and we put it at {}",
727                        pack_path.display(),
728                        hex::encode(&r.oid),
729                        off,
730                        r.offset
731                    ),
732                    None => panic!(
733                        "{}: we computed {} at offset {}, which git's idx does not contain — the \
734                         resolution is wrong",
735                        pack_path.display(),
736                        hex::encode(&r.oid),
737                        r.offset
738                    ),
739                }
740            }
741            eprintln!("{}: {} objects agree with git", pack_path.display(), ours.len());
742            objects += ours.len();
743        }
744        eprintln!("{objects} objects over {full} packs agree with git's own .idx; {thin} thin");
745        assert!(full > 0, "every pack on this machine was thin — nothing was compared");
746        assert!(objects > 1000, "only {objects} objects compared");
747    }
748
749    /// The three facts the objects table needs and `.idx` cannot supply: the
750    /// **stored** type (delta or not), the **resolved** type, and the
751    /// post-resolution size. Asserted against the pack's own delta structure, on
752    /// a real pack, so it cannot be satisfied by a store-and-echo.
753    ///
754    /// Seen RED by resolving a delta to `GitObjectKind::Blob` instead of to its
755    /// base's kind: "a delta resolves to its base's type:
756    /// c6d50e761d025c179ffef1c3d68c56aae0a53270 vs base
757    /// 8e15741296ebe3a0508522b76fe2ec4982396b6d — left: Blob, right: Commit".
758    ///
759    /// MEASURED on that pack: 4292 deltas, 2715 of them resolving to something
760    /// other than a blob, and **all 4292** carrying a resolved size unlike their
761    /// delta-stream size.
762    #[test]
763    fn a_delta_carries_its_stored_type_and_its_resolved_type_and_size() {
764        let (pack_path, _) = real_pairs(1).pop().expect("a real pack");
765        let pack = std::fs::read(&pack_path).unwrap();
766        let ours = resolve(&pack, GitHashKind::Sha1, 0, &NoBases).unwrap();
767        // One walk, reused. Walking per entry turns this guard into an O(n^2)
768        // pass over a 7000-object pack and it stops finishing.
769        let stream_size: HashMap<u64, u64> = walk(&pack, 20)
770            .unwrap()
771            .entries
772            .iter()
773            .map(|e| (e.offset, e.uncompressed_size))
774            .collect();
775
776        let deltas: Vec<&Resolved> = ours
777            .iter()
778            .filter(|r| matches!(r.stored_type, ObjType::OfsDelta | ObjType::RefDelta))
779            .collect();
780        assert!(
781            !deltas.is_empty(),
782            "{} carries no deltas, so it proves nothing about resolution",
783            pack_path.display()
784        );
785
786        // A delta's resolved type is a real git type, never a delta code, and it
787        // matches its base's.
788        let by_offset: HashMap<u64, &Resolved> = ours.iter().map(|r| (r.offset, r)).collect();
789        let mut non_blob = 0usize;
790        for d in &deltas {
791            assert!(
792                matches!(
793                    d.kind,
794                    GitObjectKind::Blob
795                        | GitObjectKind::Tree
796                        | GitObjectKind::Commit
797                        | GitObjectKind::Tag
798                ),
799                "a resolved type must be a real git type"
800            );
801            if d.kind != GitObjectKind::Blob {
802                non_blob += 1;
803            }
804            if d.delta_base != 0 {
805                let base = by_offset[&d.delta_base];
806                assert_eq!(
807                    d.kind,
808                    base.kind,
809                    "a delta resolves to its base's type: {} vs base {}",
810                    hex::encode(&d.oid),
811                    hex::encode(&base.oid)
812                );
813                // The delta_base column addresses BYTES, and those bytes are an
814                // entry we also resolved. An ordinal could not be checked like
815                // this at all.
816                assert!(
817                    base.offset < d.offset,
818                    "an ofs-delta base is always earlier in the pack"
819                );
820            }
821        }
822        assert!(
823            non_blob > 0,
824            "{} deltas and not one resolved to a tree/commit/tag — the resolved type is not being \
825             taken from the base",
826            deltas.len()
827        );
828
829        // The post-resolution size is the payload's real length, not the delta
830        // stream's. For a delta the two are almost never equal.
831        let differing = deltas
832            .iter()
833            .filter(|d| stream_size[&d.offset] != d.uncompressed_size)
834            .count();
835        assert!(
836            differing > 0,
837            "not one delta's resolved size differs from its delta-stream size, which cannot be \
838             true of a real pack — the resolved size is being copied from the header"
839        );
840        eprintln!(
841            "{}: {} deltas, {non_blob} non-blob, {differing} with a resolved size unlike the \
842             stream size",
843            pack_path.display(),
844            deltas.len()
845        );
846    }
847
848    /// A thin pack is refused **by name**, and the refusal says what decision is
849    /// missing. It is not resolved to a wrong oid and it is not silently dropped.
850    ///
851    /// Seen RED by making the no-progress arm `bail!("stuck")` instead of
852    /// `ResolveError::missing_base`: "names the base: stuck".
853    #[test]
854    fn a_thin_pack_is_refused_by_name_and_says_what_is_missing() {
855        // One ref-delta against an oid nothing can supply.
856        let base_oid = vec![0x5a; 20];
857        let mut pack = b"PACK".to_vec();
858        pack.extend_from_slice(&2u32.to_be_bytes());
859        pack.extend_from_slice(&1u32.to_be_bytes());
860        // A delta whose base is 4 bytes and whose target is 4 bytes: copy all.
861        let delta = vec![0x04, 0x04, 0x90, 0x04];
862        pack.push(0x70 | (delta.len() as u8 & 0x0f)); // type 7, size 4
863        pack.extend_from_slice(&base_oid);
864        pack.extend_from_slice(&{
865            use std::io::Write;
866            let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
867            e.write_all(&delta).unwrap();
868            e.finish().unwrap()
869        });
870        pack.extend_from_slice(&[0u8; 20]);
871
872        let err = resolve(&pack, GitHashKind::Sha1, 0, &NoBases)
873            .expect_err("a thin pack with no base source cannot resolve");
874        let msg = err.to_string();
875        assert!(msg.contains(&hex::encode(&base_oid)), "names the base: {msg}");
876        assert!(msg.contains("§14"), "says which decision is missing: {msg}");
877
878        // With a source that CAN supply it, the same pack resolves — so the
879        // refusal is about the missing content, not about ref-deltas.
880        struct One(Vec<u8>);
881        impl BaseSource for One {
882            fn content(&self, oid: &[u8]) -> Option<(GitObjectKind, Vec<u8>)> {
883                (oid == self.0.as_slice()).then(|| (GitObjectKind::Blob, b"abcd".to_vec()))
884            }
885        }
886        let ok = resolve(&pack, GitHashKind::Sha1, 0, &One(base_oid)).expect("with a base it works");
887        assert_eq!(ok.len(), 1);
888        assert_eq!(ok[0].kind, GitObjectKind::Blob);
889        assert_eq!(ok[0].uncompressed_size, 4);
890        assert_eq!(
891            ok[0].oid,
892            GitHashKind::Sha1.oid_of(&canonical(GitObjectKind::Blob, b"abcd")),
893            "the resolved object is the base copied whole, so it hashes as `abcd`"
894        );
895    }
896
897    /// **The `delta_base` column, materialised — from a real pack, through all
898    /// three index layouts and the read stack, and back out again.**
899    ///
900    /// This is the guard the column was added for. It asserts *applied output*:
901    /// resolve a real pack, build every arm over the rows, and for every
902    /// `OFS_DELTA` read the base offset **back out of the index** and require it
903    /// to land on an object the same index holds — earlier in the archive, and
904    /// with a byte extent that contains it. An ordinal in that column could not
905    /// satisfy this at all, which is §13's decision checked rather than quoted.
906    ///
907    /// The three arms are compared row for row, because that is what caught a
908    /// swapped `len`/`size` write in the previous sweep while two of the three
909    /// stayed green.
910    ///
911    /// Seen RED three times, each for a different way the column can be wrong
912    /// while every other fact stays right:
913    ///
914    /// 1. **Never written.** `Resolved::index_entry` returning `delta_base: 0`:
915    ///    "FourTables: b76687fbb34020672608bcb1a5e027c2d8b0346a is an ofs-delta
916    ///    with no recorded base".
917    /// 2. **Written from the wrong field.** `delta_base: self.offset` — a
918    ///    plausible archive offset that is non-zero, inside the pack, and lands
919    ///    on a real entry boundary, so only the identity check catches it:
920    ///    "FourTables: b76687fb… records base 91272 which is its own offset,
921    ///    not its base's".
922    /// 3. **Written in the wrong coordinate space.** `resolve_walked` yielding
923    ///    `*o` instead of `o + archive_offset`, i.e. pack-relative rather than
924    ///    archive-relative — the bug that an *ordinal* column would have had no
925    ///    way to express and no way to detect: "FourTables: b76687fb… records
926    ///    base offset 86534, which starts no object in this index". This is why
927    ///    the fixture resolves at `AT = 4096` rather than at 0; at 0 the two
928    ///    spaces coincide and the guard would be blind.
929    ///
930    /// All three restored.
931    #[test]
932    fn the_delta_base_column_survives_all_three_arms_from_a_real_pack() {
933        use crate::index_layout::{
934            FourTables, IndexEntry, ObjType, ObjectIndex, OneTableFourColumns, PackedPayload,
935        };
936        use crate::read_stack::{ObjectReadStack, RebuildTriggers};
937
938        let (pack_path, _) = real_pairs(1).pop().expect("a real pack");
939        let pack = std::fs::read(&pack_path).unwrap();
940        // A non-zero archive offset on purpose: `delta_base` is in the archive's
941        // coordinate space, so a resolver that forgot to rebase it would give
942        // pack-relative values that no longer locate anything.
943        const AT: u64 = 4096;
944        let rows = resolve(&pack, GitHashKind::Sha1, AT, &NoBases).unwrap();
945        let entries: Vec<IndexEntry> = rows.iter().map(|r| r.index_entry()).collect();
946
947        let a = FourTables::build(&entries).unwrap();
948        let b = OneTableFourColumns::build(&entries).unwrap();
949        let c = PackedPayload::build(&entries).unwrap();
950        let stack = ObjectReadStack::<OneTableFourColumns>::in_memory(RebuildTriggers::manual())
951            .unwrap();
952        stack.append(&entries).unwrap();
953        stack.rebuild().unwrap();
954
955        // Every extent this pack's objects occupy, read back out of the index.
956        let extents: HashMap<u64, u64> = entries
957            .iter()
958            .map(|e| {
959                let r = a.lookup(&e.oid).expect("a stored oid resolves");
960                (r.offset, r.len)
961            })
962            .collect();
963        assert_eq!(extents.len(), entries.len(), "two objects share an offset");
964
965        let named: [(&str, &dyn ObjectIndex); 4] =
966            [("FourTables", &a), ("OneTableFourColumns", &b), ("PackedPayload", &c), ("ObjectReadStack", &stack)];
967        let mut ofs_deltas = 0usize;
968        for (name, idx) in named {
969            let mut with_base = 0usize;
970            for e in &entries {
971                let row = idx.lookup(&e.oid).expect("a stored oid resolves");
972                // All four layouts, row for row, on every fact.
973                assert_eq!(
974                    row,
975                    a.lookup(&e.oid).unwrap(),
976                    "{name} disagrees with FourTables on {}",
977                    hex::encode(&e.oid)
978                );
979                if row.obj_type != ObjType::OfsDelta {
980                    continue;
981                }
982                with_base += 1;
983                assert_ne!(
984                    row.delta_base, 0,
985                    "{name}: {} is an ofs-delta with no recorded base",
986                    hex::encode(&e.oid)
987                );
988                assert_ne!(
989                    row.delta_base,
990                    row.offset,
991                    "{name}: {} records base {} which is its own offset, not its base's",
992                    hex::encode(&e.oid),
993                    row.delta_base
994                );
995                assert!(
996                    row.delta_base >= AT,
997                    "{name}: base {} is below the archive offset {AT} — it was never rebased",
998                    row.delta_base
999                );
1000                // The whole claim: it addresses an entry this index holds.
1001                let base_len = extents.get(&row.delta_base).unwrap_or_else(|| {
1002                    panic!(
1003                        "{name}: {} records base offset {}, which starts no object in this index",
1004                        hex::encode(&e.oid),
1005                        row.delta_base
1006                    )
1007                });
1008                assert!(
1009                    row.delta_base + base_len <= row.offset,
1010                    "{name}: the base at {} (+{base_len}) overlaps the delta at {}",
1011                    row.delta_base,
1012                    row.offset
1013                );
1014            }
1015            assert!(
1016                with_base > 0,
1017                "{name}: {} ofs-deltas resolved and 0 of them carry a base offset — the column \
1018                 is not being written",
1019                entries
1020                    .iter()
1021                    .filter(|e| e.obj_type == ObjType::OfsDelta)
1022                    .count()
1023            );
1024            ofs_deltas = with_base;
1025        }
1026        assert!(
1027            ofs_deltas > 100,
1028            "{} carries only {ofs_deltas} ofs-deltas — too few to prove anything",
1029            pack_path.display()
1030        );
1031        eprintln!(
1032            "{}: {ofs_deltas} ofs-delta base offsets located their base entry in all four layouts",
1033            pack_path.display()
1034        );
1035    }
1036
1037    /// **A base that several entries delta against survives until the last one
1038    /// has had it.**
1039    ///
1040    /// This replaces `a_blob_that_is_nobodys_base_is_hashed_and_dropped`, which
1041    /// read the retention policy off `Resolved::payload`. That field is gone
1042    /// (see the struct's docs), and the property it stood for — peak memory is
1043    /// the live base set, not the object set — is measured in bytes now by
1044    /// `tests/resolve_peak_memory.rs` rather than inferred from a flag.
1045    ///
1046    /// What is left here is the *risk* the reference counting introduced, and it
1047    /// is the one that matters: an off-by-one that frees a base while a
1048    /// dependent still needs it. That cannot be a silent wrong answer —
1049    /// `resolve_walked` fails the whole pack by name — so the assertion is that
1050    /// a pack full of shared bases resolves at all.
1051    ///
1052    /// **The test refuses to run on a pack that could not expose the bug.** A
1053    /// corpus of only single-dependent chains would pass this with the counting
1054    /// deleted entirely, which is the blind-guard shape LAW 2 is about, so the
1055    /// fan-out is asserted before the resolution is.
1056    ///
1057    /// Seen RED by `*left -= 1;` → `*left = 0;` in [`consume`] (drop every base
1058    /// on first use): "entry at offset 3141 deltas against offset 2724 which no
1059    /// entry starts at — the pack is corrupt", on the first pack tried.
1060    #[test]
1061    fn a_base_many_entries_share_outlives_all_of_them() {
1062        let mut checked = 0usize;
1063        let mut widest = 0usize;
1064        for (pack_path, _) in real_pairs(4) {
1065            let pack = std::fs::read(&pack_path).unwrap();
1066            let w = walk(&pack, 20).unwrap();
1067
1068            // How many entries name each base. Anything above 1 is a base the
1069            // counting has to hold across more than one consumer.
1070            let mut uses: HashMap<u64, usize> = HashMap::new();
1071            for e in &w.entries {
1072                if let DeltaBase::Offset(o) = e.delta_base {
1073                    *uses.entry(o).or_default() += 1;
1074                }
1075            }
1076            let shared = uses.values().filter(|n| **n > 1).count();
1077            let fan_out = uses.values().copied().max().unwrap_or(0);
1078            if shared == 0 {
1079                continue;
1080            }
1081            widest = widest.max(fan_out);
1082
1083            let rows = resolve(&pack, GitHashKind::Sha1, 0, &NoBases)
1084                .unwrap_or_else(|e| panic!("{}: {e}", pack_path.display()));
1085            assert_eq!(
1086                rows.len(),
1087                w.entries.len(),
1088                "{}: {} of {} entries resolved",
1089                pack_path.display(),
1090                rows.len(),
1091                w.entries.len()
1092            );
1093            eprintln!(
1094                "{}: {shared} bases shared by more than one entry, widest fan-out {fan_out}, all \
1095                 {} entries resolved",
1096                pack_path.display(),
1097                rows.len()
1098            );
1099            checked += 1;
1100        }
1101        assert!(
1102            checked > 0 && widest > 2,
1103            "no pack in the corpus had a base shared by more than two entries (checked \
1104             {checked}, widest {widest}) — this test cannot see an early free and must not \
1105             report a pass"
1106        );
1107    }
1108}