znippy_plugin_git/pack_walk.rs
1//! The split: one pass over a pushed packfile's entries, and nothing else.
2//!
3//! This is what [`crate::store`]'s `put_pack` walks before it stores a byte, and
4//! it is the reason the **closure check costs nothing extra** (§13.7): the walk
5//! has to visit every entry anyway to find where the next one starts, so the set
6//! of entry boundaries and the set of delta bases fall out of it for free. No
7//! index is consulted to answer *is this pack self-contained*.
8//!
9//! # What it does NOT do
10//!
11//! **It does not compute oids.** An entry's oid is
12//! `sha1("<type> <size>\0" ‖ inflated content)`, and for a delta that content
13//! only exists after the chain is resolved — that is `git index-pack`'s job, it
14//! is the *indexer's* work by §13.9 ("the index is built after the ack, over a
15//! channel"), and it is deliberately not on the ack path. So a [`PackEntry`]
16//! carries the five facts the pack itself states — extent, type, declared
17//! size and delta base — and no more.
18//!
19//! # Why the bytes are inflated but the output is thrown away
20//!
21//! A pack entry has no length field. The only way to find entry *n+1* is to run
22//! the zlib stream of entry *n* to its end and ask the decompressor how many
23//! **input** bytes it consumed. So the walk pays one inflate over the pack — but
24//! it never materialises an object: the output goes into one reusable
25//! [`SCRATCH`]-sized buffer and is discarded, so peak memory is the scratch
26//! buffer and not the repository. `total_out` is still checked against the
27//! declared size, because a stream that inflates to a different length than its
28//! header claims is a corrupt pack and the honest answer is an error.
29//!
30//! # Whose grammar is this
31//!
32//! Ours, and §18 is the reason it is allowed to be: the plan records the choice
33//! between keeping gix as a grammar library and writing the two parsers
34//! ourselves as **open**, and it prescribes exactly how to settle it — "write the
35//! two parsers, and gate them behind a test that runs both ours and gix's over a
36//! real corpus and requires byte-identical output on every entry". That gate is
37//! [`tests::ours_and_gix_agree_on_every_entry_of_every_pack`], which drives
38//! `gix_pack::data::input::BytesToEntriesIter` (a **dev**-dependency, so no gix
39//! crate enters a shipped archiver) over the same bytes and requires the five
40//! facts to match entry for entry. Writing the parser is therefore executing the
41//! plan's decision procedure, not pre-empting its decision: if the gate ever
42//! fails, gix is the arbiter and this module is wrong.
43//!
44//! The one dependency it adds is `flate2` on its pure-Rust `zlib-rs` backend —
45//! the same crate and the same backend `ldeflate` already pulls in
46//! non-optionally through `znippy-common`, so the build graph is unchanged: no
47//! C, no cmake, no build-script network, and `--no-default-features` still
48//! builds in seconds.
49
50use anyhow::{anyhow, bail, Context as _, Result};
51use flate2::{Decompress, FlushDecompress, Status};
52
53use crate::index_layout::ObjType;
54use crate::object::GitHashKind;
55
56/// Bytes of inflated output the walk keeps around at once. The output is
57/// discarded, so this bounds the walk's memory over any pack of any size.
58const SCRATCH: usize = 64 * 1024;
59
60/// A pack header is `PACK`, a version and an object count.
61const HEADER_LEN: usize = 12;
62
63/// Where a delta entry's base is.
64///
65/// **The stored form is an OFFSET, never an ordinal** (§13, decided): an ordinal
66/// indexes the derived objects table, that table is rebuilt whenever the
67/// projection is rebuilt, and a rebuild re-derives every ordinal as the new
68/// oid-lexicographic rank. An ordinal written down before a rebuild therefore
69/// points at a different row afterwards — silently. An offset addresses the
70/// bytes, which append-only storage never moves.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum DeltaBase {
73 /// A whole object: no base.
74 None,
75 /// `OFS_DELTA` — the base is another entry, at this **absolute** offset.
76 /// Absolute in whatever coordinate space the walk was rebased into: within
77 /// the pack after [`walk`], within the archive after [`PackWalk::rebased`].
78 Offset(u64),
79 /// `REF_DELTA` — the base is named by oid and is usually *outside* the pack
80 /// (that is what a thin pack is). Resolving it is the one part of the check
81 /// that reads `objects.oid`, exactly as §13's table says receive-pack does.
82 Ref(Vec<u8>),
83}
84
85impl DeltaBase {
86 /// The offset form for the `objects.delta_base` column.
87 ///
88 /// `0` means *no offset base* and is a safe sentinel rather than a lie:
89 /// archive offset 0 is the first pack's `PACK` magic, so no object entry can
90 /// ever legitimately start there. A `Ref` base has no offset until the
91 /// indexer resolves its oid, and reports `0` for the same reason.
92 pub fn as_offset(&self) -> u64 {
93 match self {
94 DeltaBase::Offset(o) => *o,
95 DeltaBase::None | DeltaBase::Ref(_) => 0,
96 }
97 }
98}
99
100/// One entry, as the pack itself states it.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct PackEntry {
103 /// Start of the entry — its type/size header, not its zlib stream.
104 pub offset: u64,
105 /// Header **plus** compressed stream. `offset + len` is where the next entry
106 /// starts, which is what makes this the byte extent to store verbatim.
107 pub len: u64,
108 pub obj_type: ObjType,
109 /// The size the entry's header declares, and what its zlib stream really
110 /// inflated to — the walk checks the two agree.
111 ///
112 /// For `OfsDelta` / `RefDelta` this is the size of the **delta instruction
113 /// stream**, not of the object the chain resolves to. The resolved size is
114 /// unknowable without applying the chain and is the indexer's output; the
115 /// type column is what stops a caller mistaking one for the other.
116 pub uncompressed_size: u64,
117 pub delta_base: DeltaBase,
118}
119
120/// What one pass over a pack found.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct PackWalk {
123 /// `2` or `3`.
124 pub version: u32,
125 /// Entries in pack order — which is also ascending offset order.
126 pub entries: Vec<PackEntry>,
127 /// The pack's own trailing checksum, as stored. Not verified here: this walk
128 /// reports what the bytes say, and the checksum of *what was stored* is
129 /// [`crate::indexer`]'s row.
130 pub trailer: Vec<u8>,
131}
132
133/// What the closure check found. **Derived from the walk alone** — no index, no
134/// disk, no oid resolution.
135#[derive(Debug, Clone, PartialEq, Eq, Default)]
136pub struct Closure {
137 /// `OFS_DELTA` bases that do not land on an entry boundary inside this pack.
138 /// A non-empty list is a corrupt pack, not a thin one.
139 pub broken_offsets: Vec<u64>,
140 /// `REF_DELTA` base oids. Each has to exist *somewhere* — in this pack under
141 /// an oid the walk cannot compute, or already in the store. Only these need
142 /// `objects.oid`, and there are usually very few.
143 pub external_refs: Vec<Vec<u8>>,
144}
145
146impl Closure {
147 /// Nothing to ask anybody about: every delta base is an offset inside this
148 /// pack.
149 pub fn is_self_contained(&self) -> bool {
150 self.broken_offsets.is_empty() && self.external_refs.is_empty()
151 }
152}
153
154impl PackWalk {
155 /// The same walk with every offset moved into the archive's coordinate
156 /// space, which is what the `objects` table stores.
157 ///
158 /// A delta base moves with it — that is the whole reason it is an offset and
159 /// not an ordinal: the shift is arithmetic on a known quantity, with no
160 /// table to consult and nothing to keep in step.
161 pub fn rebased(mut self, archive_offset: u64) -> Self {
162 for e in &mut self.entries {
163 e.offset += archive_offset;
164 if let DeltaBase::Offset(o) = &mut e.delta_base {
165 *o += archive_offset;
166 }
167 }
168 self
169 }
170
171 /// The closure check, out of the walk we already did.
172 pub fn closure(&self) -> Closure {
173 let boundaries: std::collections::HashSet<u64> =
174 self.entries.iter().map(|e| e.offset).collect();
175 let mut c = Closure::default();
176 for e in &self.entries {
177 match &e.delta_base {
178 DeltaBase::None => {}
179 DeltaBase::Offset(o) => {
180 if !boundaries.contains(o) {
181 c.broken_offsets.push(*o);
182 }
183 }
184 DeltaBase::Ref(oid) => c.external_refs.push(oid.clone()),
185 }
186 }
187 c
188 }
189
190 /// Total inflated bytes the entries declare. Not the pack's own length.
191 pub fn declared_bytes(&self) -> u64 {
192 self.entries.iter().map(|e| e.uncompressed_size).sum()
193 }
194}
195
196/// Walk a pushed pack's entries.
197///
198/// `oid_len` is 20 for sha1 and 32 for sha256 — a `REF_DELTA`'s base oid is raw
199/// bytes with no length prefix, so the width has to be known from outside the
200/// pack. That is git's own situation.
201///
202/// **P-4: a malformed or hostile pack returns `Err`, never a panic and never a
203/// half-truth.** Every arithmetic step is checked against the buffer's end.
204pub fn walk(pack: &[u8], oid_len: usize) -> Result<PackWalk> {
205 if oid_len != 20 && oid_len != 32 {
206 bail!("oid width {oid_len} is neither sha1 (20) nor sha256 (32)");
207 }
208 if pack.len() < HEADER_LEN + oid_len {
209 bail!(
210 "a pack is at least {} bytes (header + trailer), this one is {}",
211 HEADER_LEN + oid_len,
212 pack.len()
213 );
214 }
215 if &pack[0..4] != b"PACK" {
216 bail!("not a packfile: it does not start with `PACK`");
217 }
218 let version = u32::from_be_bytes([pack[4], pack[5], pack[6], pack[7]]);
219 if version != 2 && version != 3 {
220 bail!("pack version {version} is not 2 or 3");
221 }
222 let count = u32::from_be_bytes([pack[8], pack[9], pack[10], pack[11]]) as usize;
223
224 let body_end = pack.len() - oid_len;
225 let mut pos = HEADER_LEN;
226 let mut entries = Vec::with_capacity(count);
227 let mut scratch = vec![0u8; SCRATCH];
228
229 for i in 0..count {
230 if pos >= body_end {
231 bail!(
232 "the pack header claims {count} objects but the bytes ran out after {i} — {} of \
233 {} bytes consumed",
234 pos,
235 pack.len()
236 );
237 }
238 let start = pos;
239 let (obj_type, size, n) = type_and_size(&pack[pos..body_end])
240 .map_err(|e| anyhow!("entry {i} at offset {start}: {e}"))?;
241 pos += n;
242
243 let delta_base = match obj_type {
244 ObjType::OfsDelta => {
245 let (distance, n) = ofs_distance(&pack[pos..body_end])
246 .map_err(|e| anyhow!("entry {i} at offset {start}: {e}"))?;
247 pos += n;
248 let start_u64 = start as u64;
249 if distance == 0 || distance > start_u64 {
250 bail!(
251 "entry {i} at offset {start} is an ofs-delta whose base is {distance} \
252 bytes back, which is outside the pack"
253 );
254 }
255 DeltaBase::Offset(start_u64 - distance)
256 }
257 ObjType::RefDelta => {
258 if pos + oid_len > body_end {
259 bail!("entry {i} at offset {start}: a ref-delta base oid runs off the pack");
260 }
261 let oid = pack[pos..pos + oid_len].to_vec();
262 pos += oid_len;
263 DeltaBase::Ref(oid)
264 }
265 _ => DeltaBase::None,
266 };
267
268 let consumed = inflate_and_discard(&pack[pos..body_end], size, &mut scratch)
269 .map_err(|e| anyhow!("entry {i} at offset {start}: {e}"))?;
270 pos += consumed;
271
272 entries.push(PackEntry {
273 offset: start as u64,
274 len: (pos - start) as u64,
275 obj_type,
276 uncompressed_size: size,
277 delta_base,
278 });
279 }
280
281 if pos != body_end {
282 bail!(
283 "the pack's {count} entries end at {pos} but its trailer starts at {body_end} — \
284 {} bytes are unaccounted for",
285 body_end - pos.min(body_end)
286 );
287 }
288
289 Ok(PackWalk {
290 version,
291 entries,
292 trailer: pack[body_end..].to_vec(),
293 })
294}
295
296/// The type/size varint. `(type, uncompressed size, bytes consumed)`.
297///
298/// Byte 0 is `[continue:1][type:3][size low nibble:4]`; each continuation byte
299/// adds 7 more size bits, least significant group first.
300fn type_and_size(b: &[u8]) -> Result<(ObjType, u64, usize)> {
301 let first = *b.first().ok_or_else(|| anyhow!("no type/size header"))?;
302 let code = (first >> 4) & 0b111;
303 let obj_type = ObjType::from_code(code).ok_or_else(|| {
304 anyhow!("object type code {code} is not one git writes — refusing to guess it")
305 })?;
306 let mut size = u64::from(first & 0x0f);
307 let mut shift = 4u32;
308 let mut i = 1usize;
309 let mut cont = first & 0x80 != 0;
310 while cont {
311 let byte = *b
312 .get(i)
313 .ok_or_else(|| anyhow!("the type/size varint runs off the pack"))?;
314 if shift >= 64 {
315 bail!("the type/size varint is longer than a u64 can hold");
316 }
317 size |= u64::from(byte & 0x7f) << shift;
318 shift += 7;
319 cont = byte & 0x80 != 0;
320 i += 1;
321 }
322 Ok((obj_type, size, i))
323}
324
325/// The `OFS_DELTA` backwards distance. Git's own encoding, which is **not** the
326/// same varint as above: each continuation adds one before shifting, so the
327/// encoding has no redundant representations.
328fn ofs_distance(b: &[u8]) -> Result<(u64, usize)> {
329 let mut i = 0usize;
330 let mut byte = *b
331 .first()
332 .ok_or_else(|| anyhow!("no ofs-delta distance varint"))?;
333 i += 1;
334 let mut d = u64::from(byte & 0x7f);
335 while byte & 0x80 != 0 {
336 byte = *b
337 .get(i)
338 .ok_or_else(|| anyhow!("the ofs-delta distance varint runs off the pack"))?;
339 i += 1;
340 d = d
341 .checked_add(1)
342 .and_then(|d| d.checked_shl(7))
343 .ok_or_else(|| anyhow!("the ofs-delta distance overflows a u64"))?
344 | u64::from(byte & 0x7f);
345 }
346 Ok((d, i))
347}
348
349/// Run one zlib stream to its end, throwing the output away, and report how many
350/// **input** bytes it took. Checks the inflated length against `declared`.
351fn inflate_and_discard(input: &[u8], declared: u64, scratch: &mut [u8]) -> Result<usize> {
352 let mut d = Decompress::new(true);
353 loop {
354 let before_in = d.total_in();
355 let before_out = d.total_out();
356 let status = d
357 .decompress(&input[before_in as usize..], scratch, FlushDecompress::None)
358 .map_err(|e| anyhow!("zlib: {e}"))?;
359 match status {
360 Status::StreamEnd => break,
361 Status::Ok | Status::BufError => {
362 // No progress on either side and not at the end: the stream is
363 // truncated. Without this the loop would spin forever on a
364 // hostile pack.
365 if d.total_in() == before_in && d.total_out() == before_out {
366 bail!("the zlib stream is truncated after {} bytes", d.total_in());
367 }
368 }
369 }
370 }
371 if d.total_out() != declared {
372 bail!(
373 "the entry header declares {declared} bytes but its stream inflates to {}",
374 d.total_out()
375 );
376 }
377 Ok(d.total_in() as usize)
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use flate2::{write::ZlibEncoder, Compression};
384 use std::io::Write;
385
386 fn deflate(bytes: &[u8]) -> Vec<u8> {
387 let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
388 e.write_all(bytes).unwrap();
389 e.finish().unwrap()
390 }
391
392 /// The type/size header git would write.
393 fn header(code: u8, mut size: u64) -> Vec<u8> {
394 let mut out = vec![(code << 4) | (size as u8 & 0x0f)];
395 size >>= 4;
396 while size > 0 {
397 let last = out.len() - 1;
398 out[last] |= 0x80;
399 out.push((size & 0x7f) as u8);
400 size >>= 7;
401 }
402 out
403 }
404
405 /// git's backwards-distance encoding.
406 fn ofs(mut d: u64) -> Vec<u8> {
407 let mut out = vec![(d & 0x7f) as u8];
408 d >>= 7;
409 while d > 0 {
410 d -= 1;
411 out.insert(0, 0x80 | (d & 0x7f) as u8);
412 d >>= 7;
413 }
414 out
415 }
416
417 /// A pack of three entries: a blob, a second blob, and an ofs-delta whose
418 /// base is the first. Returns `(bytes, expected entry offsets)`.
419 fn three_entry_pack() -> (Vec<u8>, Vec<u64>) {
420 let a = b"the quick brown fox jumps over the lazy dog".to_vec();
421 let b = vec![b'x'; 300];
422 // A delta stream's *content* is never parsed by the walk, so any bytes
423 // will do — but the declared size must be its real inflated length,
424 // because the walk checks that.
425 let delta = b"\x2b\x2b\x90\x01\x00".to_vec();
426
427 let mut pack = b"PACK".to_vec();
428 pack.extend_from_slice(&2u32.to_be_bytes());
429 pack.extend_from_slice(&3u32.to_be_bytes());
430
431 let mut offsets = Vec::new();
432 offsets.push(pack.len() as u64);
433 pack.extend_from_slice(&header(3, a.len() as u64));
434 pack.extend_from_slice(&deflate(&a));
435
436 offsets.push(pack.len() as u64);
437 pack.extend_from_slice(&header(3, b.len() as u64));
438 pack.extend_from_slice(&deflate(&b));
439
440 let third = pack.len() as u64;
441 offsets.push(third);
442 pack.extend_from_slice(&header(6, delta.len() as u64));
443 pack.extend_from_slice(&ofs(third - offsets[0]));
444 pack.extend_from_slice(&deflate(&delta));
445
446 pack.extend_from_slice(&[0u8; 20]); // trailer
447 (pack, offsets)
448 }
449
450 /// A pack with one `REF_DELTA` whose base is not in the pack — a thin pack.
451 fn thin_pack(base_oid: &[u8]) -> Vec<u8> {
452 let delta = b"\x0a\x0a\x91\x00\x0a".to_vec();
453 let mut pack = b"PACK".to_vec();
454 pack.extend_from_slice(&2u32.to_be_bytes());
455 pack.extend_from_slice(&1u32.to_be_bytes());
456 pack.extend_from_slice(&header(7, delta.len() as u64));
457 pack.extend_from_slice(base_oid);
458 pack.extend_from_slice(&deflate(&delta));
459 pack.extend_from_slice(&[0u8; 20]);
460 pack
461 }
462
463 /// The walk's whole output, asserted as **applied bytes**: every entry's
464 /// offset is where the previous one ended, every `len` covers header plus
465 /// stream, and `offset + len` of the last entry is exactly where the trailer
466 /// starts. A walk that got any length wrong cannot satisfy all three.
467 ///
468 /// Seen RED by changing `pos += consumed` to `pos += consumed + 1` in
469 /// [`walk`]: "a well-formed pack walks: entry 1 at offset 66: the entry
470 /// header declares 2 bytes but its stream inflates to 300" — one byte of
471 /// desync and the *next* entry's header is read out of the middle of a
472 /// deflate stream, which is why nothing downstream has to trust the length.
473 #[test]
474 fn every_entry_boundary_is_where_the_bytes_say_it_is() {
475 let (pack, expected) = three_entry_pack();
476 let w = walk(&pack, 20).expect("a well-formed pack walks");
477 assert_eq!(w.version, 2);
478 assert_eq!(w.entries.len(), 3);
479
480 let got: Vec<u64> = w.entries.iter().map(|e| e.offset).collect();
481 assert_eq!(got, expected, "entry offsets");
482
483 // Boundaries chain, and the last one ends at the trailer.
484 for (i, e) in w.entries.iter().enumerate() {
485 let next = w
486 .entries
487 .get(i + 1)
488 .map(|n| n.offset)
489 .unwrap_or((pack.len() - 20) as u64);
490 assert_eq!(
491 e.offset + e.len,
492 next,
493 "entry {i} claims to end at {} but the next starts at {next}",
494 e.offset + e.len
495 );
496 }
497
498 assert_eq!(w.entries[0].obj_type, ObjType::Blob);
499 assert_eq!(w.entries[0].uncompressed_size, 43);
500 assert_eq!(w.entries[1].uncompressed_size, 300);
501 assert_eq!(w.entries[2].obj_type, ObjType::OfsDelta);
502 assert_eq!(
503 w.entries[2].delta_base,
504 DeltaBase::Offset(expected[0]),
505 "the ofs-delta base must resolve to the first entry's offset"
506 );
507 assert_eq!(w.trailer.len(), 20);
508 }
509
510 /// The closure check, and that it is answered without an index: a
511 /// self-contained pack asks nobody anything, a thin one names exactly the
512 /// oid it needs, and a corrupt base offset is reported as corrupt rather
513 /// than as external.
514 ///
515 /// Seen RED by making `closure()` insert `e.offset + 1` into `boundaries`:
516 /// "a pack whose only delta is an ofs-delta into itself needs nobody:
517 /// Closure { broken_offsets: [12], external_refs: [] }".
518 #[test]
519 fn the_closure_check_falls_out_of_the_walk_and_consults_nothing() {
520 let (pack, offsets) = three_entry_pack();
521 let c = walk(&pack, 20).unwrap().closure();
522 assert!(
523 c.is_self_contained(),
524 "a pack whose only delta is an ofs-delta into itself needs nobody: {c:?}"
525 );
526 assert!(c.external_refs.is_empty());
527
528 let base = vec![0xab; 20];
529 let c = walk(&thin_pack(&base), 20).unwrap().closure();
530 assert!(!c.is_self_contained(), "a thin pack is not self-contained");
531 assert_eq!(c.external_refs, vec![base], "the one oid to ask about");
532 assert!(c.broken_offsets.is_empty(), "a thin pack is not corrupt");
533
534 // A base offset that lands mid-entry is corruption, and is named.
535 let mut w = walk(&pack, 20).unwrap();
536 w.entries[2].delta_base = DeltaBase::Offset(offsets[0] + 1);
537 let c = w.closure();
538 assert_eq!(c.broken_offsets, vec![offsets[0] + 1]);
539 assert!(c.external_refs.is_empty());
540 }
541
542 /// Rebasing moves the entry and its base together. This is the property that
543 /// makes `delta_base` an offset rather than an ordinal: one addition, no
544 /// table.
545 ///
546 /// Seen RED by leaving the `DeltaBase::Offset` arm out of
547 /// [`PackWalk::rebased`]: `left: Offset(12)`, `right: Offset(1000012)` — the
548 /// entry moved and its base did not, which is precisely the silent
549 /// mis-addressing the offset form exists to make impossible.
550 #[test]
551 fn rebasing_moves_an_entry_and_its_base_by_the_same_amount() {
552 let (pack, offsets) = three_entry_pack();
553 let base = 1_000_000u64;
554 let w = walk(&pack, 20).unwrap().rebased(base);
555 assert_eq!(w.entries[0].offset, offsets[0] + base);
556 assert_eq!(
557 w.entries[2].delta_base,
558 DeltaBase::Offset(offsets[0] + base)
559 );
560 assert!(
561 w.closure().is_self_contained(),
562 "a rebased pack is still self-contained — that is the point"
563 );
564 assert_eq!(DeltaBase::None.as_offset(), 0);
565 assert_eq!(DeltaBase::Ref(vec![1; 20]).as_offset(), 0);
566 }
567
568 /// P-4, on the shapes a hostile client can actually send. Every one is an
569 /// `Err` with a reason, and none of them panics or loops.
570 ///
571 /// Seen RED by replacing `if d.total_out() != declared` with `if false`:
572 /// "lies about its size must be refused, got Ok(PackWalk { version: 2,
573 /// entries: [PackEntry { offset: 12, len: 15, obj_type: Blob,
574 /// uncompressed_size: 999, delta_base: None }] … })" — a walk that believed
575 /// a header over the bytes.
576 #[test]
577 fn a_hostile_pack_is_an_error_with_a_reason_never_a_panic() {
578 let (good, _) = three_entry_pack();
579
580 let mut wrong_magic = good.clone();
581 wrong_magic[0] = b'N';
582
583 let mut wrong_version = good.clone();
584 wrong_version[7] = 9;
585
586 let mut too_many = good.clone();
587 too_many[11] = 99;
588
589 let truncated = good[..good.len() / 2].to_vec();
590
591 // An entry whose declared size is not what its stream inflates to.
592 let mut liar = b"PACK".to_vec();
593 liar.extend_from_slice(&2u32.to_be_bytes());
594 liar.extend_from_slice(&1u32.to_be_bytes());
595 liar.extend_from_slice(&header(3, 999));
596 liar.extend_from_slice(&deflate(b"short"));
597 liar.extend_from_slice(&[0u8; 20]);
598
599 // Type code 5, which git does not use.
600 let mut bad_type = b"PACK".to_vec();
601 bad_type.extend_from_slice(&2u32.to_be_bytes());
602 bad_type.extend_from_slice(&1u32.to_be_bytes());
603 bad_type.extend_from_slice(&header(5, 5));
604 bad_type.extend_from_slice(&deflate(b"hello"));
605 bad_type.extend_from_slice(&[0u8; 20]);
606
607 // An ofs-delta pointing before the start of the pack.
608 let mut bad_ofs = b"PACK".to_vec();
609 bad_ofs.extend_from_slice(&2u32.to_be_bytes());
610 bad_ofs.extend_from_slice(&1u32.to_be_bytes());
611 bad_ofs.extend_from_slice(&header(6, 5));
612 bad_ofs.extend_from_slice(&ofs(1_000_000));
613 bad_ofs.extend_from_slice(&deflate(b"delta"));
614 bad_ofs.extend_from_slice(&[0u8; 20]);
615
616 for (what, bytes) in [
617 ("wrong magic", wrong_magic),
618 ("wrong version", wrong_version),
619 ("more objects than bytes", too_many),
620 ("truncated", truncated),
621 ("lies about its size", liar),
622 ("unused type code", bad_type),
623 ("base before the pack", bad_ofs),
624 ("empty", Vec::new()),
625 ("header only", b"PACK\0\0\0\x02\0\0\0\x01".to_vec()),
626 ] {
627 let r = walk(&bytes, 20);
628 assert!(r.is_err(), "{what} must be refused, got {r:?}");
629 }
630
631 assert!(walk(&good, 21).is_err(), "an oid width of 21 is nonsense");
632 }
633
634 /// §18's decision procedure, run as a test: **ours against gix's, entry for
635 /// entry, on every pack we can find.**
636 ///
637 /// gix is a dev-dependency, so this costs a shipped archiver nothing. The
638 /// synthetic packs always run; the real ones are whatever `.pack` files this
639 /// machine happens to carry under `/home/rickard/git`, capped so the test
640 /// stays a test. The count of both is printed, because a differential test
641 /// that silently compared nothing is exactly the hollow guard LAW 2 is
642 /// about.
643 ///
644 /// Seen RED by changing `shift += 7` to `shift += 8` in [`type_and_size`]:
645 /// "ours walks it: entry 18 at offset 10265: the entry header declares 4133
646 /// bytes but its stream inflates to 2085".
647 ///
648 /// **And that red proof is the argument for the real corpus.** `shift` is
649 /// first *used* at 4 and only then advanced, so the broken version still
650 /// decodes every two-byte varint correctly — every object under 2048 bytes.
651 /// Both synthetic packs passed it. It took entry 18 of a real repository's
652 /// pack to expose it, which is exactly why §18 says the gate has to run over
653 /// a corpus and not over a fixture.
654 #[test]
655 fn ours_and_gix_agree_on_every_entry_of_every_pack() {
656 let mut compared = 0usize;
657 let mut packs = 0usize;
658
659 let (synthetic, _) = three_entry_pack();
660 for p in [synthetic, thin_pack(&[0x7f; 20])] {
661 compared += agree(&p);
662 packs += 1;
663 }
664 assert!(
665 packs == 2 && compared >= 4,
666 "the synthetic packs must compare"
667 );
668
669 let mut real = 0usize;
670 for pack in real_packs(8) {
671 let bytes = std::fs::read(&pack).expect("reading a real pack");
672 let n = agree(&bytes);
673 eprintln!("{}: {n} entries agree", pack.display());
674 compared += n;
675 real += 1;
676 }
677 eprintln!(
678 "compared {compared} entries over {} packs ({real} real)",
679 packs + real
680 );
681 }
682
683 /// Both parsers over one pack; asserts the five facts and returns the entry
684 /// count.
685 fn agree(pack: &[u8]) -> usize {
686 let ours = walk(pack, 20).expect("ours walks it");
687 let theirs = gix_pack::data::input::BytesToEntriesIter::new_from_header(
688 std::io::BufReader::new(pack),
689 gix_pack::data::input::Mode::AsIs,
690 gix_pack::data::input::EntryDataMode::Ignore,
691 gix_hash::Kind::Sha1,
692 )
693 .expect("gix reads the header");
694
695 let mut n = 0usize;
696 for (i, entry) in theirs.enumerate() {
697 let g = entry.expect("gix walks it");
698 let o = &ours.entries[i];
699 assert_eq!(o.offset, g.pack_offset, "entry {i} offset");
700 assert_eq!(
701 o.len,
702 g.bytes_in_pack(),
703 "entry {i} length: ours {} vs gix {}",
704 o.len,
705 g.bytes_in_pack()
706 );
707 assert_eq!(
708 o.uncompressed_size, g.decompressed_size,
709 "entry {i} decompressed size"
710 );
711 let (gt, gbase) = match g.header {
712 gix_pack::data::entry::Header::Commit => (ObjType::Commit, DeltaBase::None),
713 gix_pack::data::entry::Header::Tree => (ObjType::Tree, DeltaBase::None),
714 gix_pack::data::entry::Header::Blob => (ObjType::Blob, DeltaBase::None),
715 gix_pack::data::entry::Header::Tag => (ObjType::Tag, DeltaBase::None),
716 gix_pack::data::entry::Header::OfsDelta { base_distance } => (
717 ObjType::OfsDelta,
718 DeltaBase::Offset(g.pack_offset - base_distance),
719 ),
720 gix_pack::data::entry::Header::RefDelta { base_id } => (
721 ObjType::RefDelta,
722 DeltaBase::Ref(base_id.as_slice().to_vec()),
723 ),
724 };
725 assert_eq!(o.obj_type, gt, "entry {i} type");
726 assert_eq!(o.delta_base, gbase, "entry {i} delta base");
727 n += 1;
728 }
729 assert_eq!(
730 n,
731 ours.entries.len(),
732 "gix found a different number of entries"
733 );
734 n
735 }
736
737 /// Up to `cap` real `.pack` files from this machine's own repositories.
738 fn real_packs(cap: usize) -> Vec<std::path::PathBuf> {
739 let mut out = Vec::new();
740 let root = std::path::Path::new("/home/rickard/git");
741 let Ok(repos) = std::fs::read_dir(root) else {
742 return out;
743 };
744 for repo in repos.flatten() {
745 let dir = repo.path().join(".git/objects/pack");
746 let Ok(files) = std::fs::read_dir(&dir) else {
747 continue;
748 };
749 for f in files.flatten() {
750 let p = f.path();
751 // Small ones only: this is a correctness gate, not a benchmark.
752 let small = f.metadata().map(|m| m.len() < 64 << 20).unwrap_or(false);
753 if small && p.extension().is_some_and(|e| e == "pack") {
754 out.push(p);
755 if out.len() >= cap {
756 return out;
757 }
758 }
759 }
760 }
761 out
762 }
763}
764
765// ── the inverses, for emitting a pack ────────────────────────────────────────
766//
767// Reading this grammar is what the walk above does. Writing it is what emitting
768// a pack for a selected object set needs, and it needs *only* this: a pack
769// entry's compressed payload is position-independent, so a subset of a pack is
770// the stored payloads byte for byte with their headers re-encoded. Nothing is
771// re-compressed and no delta is recomputed.
772//
773// These are deliberately in the same file as their decoders. The two must agree
774// bit for bit, and the way to keep them agreeing is to make disagreement
775// obvious to whoever edits one.
776
777/// The type/size varint, for a caller outside this module that needs to find
778/// where an entry's header ends — reading the grammar rather than assuming a
779/// width.
780pub fn type_and_size_of(b: &[u8]) -> Result<(ObjType, u64, usize)> {
781 type_and_size(b)
782}
783
784/// An `OFS_DELTA`'s **backwards distance** to its base, and how many bytes it
785/// occupied. `b` starts immediately after the type/size varint.
786///
787/// The pair to [`type_and_size_of`], exported for the same reason: a caller
788/// walking a delta chain by entry header — [`crate::serve`]'s type probe is the
789/// one — must read the grammar rather than re-derive it, or the two spellings
790/// drift and the second one is wrong somewhere nothing checks.
791pub fn ofs_distance_of(b: &[u8]) -> Result<(u64, usize)> {
792 ofs_distance(b)
793}
794
795/// Encode the type/size header — the inverse of [`type_and_size`].
796///
797/// Byte 0 is `[continue:1][type:3][size low nibble:4]`; each continuation byte
798/// carries 7 more size bits, least significant group first.
799pub fn encode_type_and_size(out: &mut Vec<u8>, obj_type: ObjType, size: u64) {
800 let mut byte = (obj_type.code() << 4) | ((size & 0x0f) as u8);
801 let mut rest = size >> 4;
802 while rest > 0 {
803 out.push(byte | 0x80);
804 byte = (rest & 0x7f) as u8;
805 rest >>= 7;
806 }
807 out.push(byte);
808}
809
810/// Encode an `OFS_DELTA` backwards distance — the inverse of [`ofs_distance`].
811///
812/// **Not the same varint as the one above**, and that is the whole reason this
813/// is written out rather than shared: each continuation subtracts one before
814/// shifting, so the encoding has no redundant representations. Getting this
815/// wrong by one produces a pack `git index-pack --strict` rejects — or worse,
816/// one it accepts and mis-reads, because a delta would then be applied against
817/// the wrong base.
818///
819/// The distance is emitted most-significant group first, which is why it is
820/// built backwards into a scratch buffer.
821pub fn encode_ofs_distance(out: &mut Vec<u8>, distance: u64) {
822 let mut buf = [0u8; 10];
823 let mut i = buf.len() - 1;
824 let mut d = distance;
825 buf[i] = (d & 0x7f) as u8;
826 while d >= 0x80 {
827 d >>= 7;
828 d -= 1;
829 i -= 1;
830 buf[i] = 0x80 | (d & 0x7f) as u8;
831 }
832 out.extend_from_slice(&buf[i..]);
833}
834
835#[cfg(test)]
836mod encode_tests {
837 use super::*;
838
839 /// **Every encoder here is checked against the decoder beside it**, over
840 /// the boundaries the varints actually turn on rather than a handful of
841 /// round numbers.
842 ///
843 /// The sizes are the ones where a group boundary falls: the 4-bit nibble in
844 /// byte 0, then every 7 bits after it. The distances are the ones where the
845 /// subtract-one encoding changes length, which is where an off-by-one in
846 /// either direction shows up and nowhere else.
847 #[test]
848 fn the_encoders_are_the_inverses_of_the_decoders() {
849 let sizes = [
850 0u64,
851 1,
852 15,
853 16,
854 17,
855 2047,
856 2048,
857 2049,
858 262_143,
859 262_144,
860 1 << 20,
861 1 << 31,
862 (1u64 << 57) - 1,
863 ];
864 for &size in &sizes {
865 for t in [
866 ObjType::Commit,
867 ObjType::Tree,
868 ObjType::Blob,
869 ObjType::Tag,
870 ObjType::OfsDelta,
871 ObjType::RefDelta,
872 ] {
873 let mut buf = Vec::new();
874 encode_type_and_size(&mut buf, t, size);
875 let (got_t, got_size, n) =
876 type_and_size(&buf).expect("what we wrote must parse back");
877 assert_eq!(got_t, t, "type round trip at size {size}");
878 assert_eq!(got_size, size, "size round trip for {t:?}");
879 assert_eq!(
880 n,
881 buf.len(),
882 "the decoder must consume exactly what was written"
883 );
884 }
885 }
886
887 // The subtract-one encoding: 127/128 and 16511/16512 are where the
888 // length changes, and are exactly where an off-by-one hides.
889 let distances = [
890 0u64,
891 1,
892 126,
893 127,
894 128,
895 129,
896 16_383,
897 16_511,
898 16_512,
899 16_513,
900 1 << 20,
901 1 << 40,
902 u32::MAX as u64,
903 ];
904 for &d in &distances {
905 let mut buf = Vec::new();
906 encode_ofs_distance(&mut buf, d);
907 let (got, n) = ofs_distance(&buf).expect("what we wrote must parse back");
908 assert_eq!(got, d, "distance round trip");
909 assert_eq!(
910 n,
911 buf.len(),
912 "the decoder must consume exactly what was written"
913 );
914 }
915 }
916}
917
918// ── emitting a subset as a packfile ──────────────────────────────────────────
919
920/// **Where an entry's bytes are** — an address into the archive, or bytes that
921/// exist nowhere else.
922///
923/// # Why this is not a `Vec<u8>`
924///
925/// It was one until 2026-08-14, and that single field is what made a clone hold
926/// the whole repository. [`crate::git_ops::GitStore::emit_set`] filled it with a
927/// `pread` into a fresh `Vec::with_capacity(len)` — one syscall, one allocation
928/// and one kernel→user copy **per object** — and every one of those `Vec`s was
929/// live at once, because the entries are all built before
930/// [`emit_pack`] writes a byte. On a `linux.git` clone: ~13.8 M syscalls, ~27.6 M
931/// allocations, 6.4 GB copied and held, and a peak RSS of **2314 MB** against
932/// gitea's 122 MB on the identical clone
933/// (`gunnar/.nornir/forge-bakeoff-benchmarks.md`, `vs_forge_clone`).
934///
935/// An [`Extent`](Self::Extent) is 16 bytes and addresses bytes that are already
936/// in the page cache. Resolving it is [`crate::archive_map::Mapped::get`] —
937/// pointer arithmetic and a bounds check — with a `pread` fallback for the one
938/// case a mapping cannot answer.
939#[derive(Debug, Clone, PartialEq, Eq)]
940pub enum EntryBytes {
941 /// The stored entry **exactly as received**, addressed in the archive's
942 /// coordinate space. Header and all: `offset` is the entry's type/size
943 /// varint and `len` runs to where the next entry starts.
944 ///
945 /// This is what a full clone is made of, end to end. Nothing copies it until
946 /// [`emit_pack`] writes it to the wire.
947 Extent { offset: u64, len: u64 },
948 /// **The exception: bytes that are on no disk.**
949 ///
950 /// Three shapes reach here, and every one of them genuinely computed bytes
951 /// that no extent addresses:
952 ///
953 /// * a delta whose base the request does not carry, rebuilt whole or
954 /// re-deltified against a base it does carry ([`Self::recompressed`] —
955 /// `crate::delta`);
956 /// * an `OFS_DELTA` re-headed as a `REF_DELTA` for a client with no
957 /// `ofs-delta` capability, or for a thin fetch — the *payload* is a copy
958 /// but the header in front of it is new, so the concatenation is new;
959 /// * a fixture built by hand in a test.
960 ///
961 /// It is **zero for a whole-repository clone**, which is the measurement the
962 /// extent form exists to make true rather than a promise this makes.
963 Owned(Vec<u8>),
964}
965
966impl EntryBytes {
967 /// How many bytes this entry contributes, without resolving it.
968 ///
969 /// Free for an [`Extent`](Self::Extent) — the length is the address — which
970 /// is what lets a caller size or account for a pack it has not read.
971 pub fn len(&self) -> u64 {
972 match self {
973 EntryBytes::Extent { len, .. } => *len,
974 EntryBytes::Owned(v) => v.len() as u64,
975 }
976 }
977
978 /// Whether this entry contributes nothing. Only a hand-built fixture can.
979 pub fn is_empty(&self) -> bool {
980 self.len() == 0
981 }
982
983 /// The bytes, when this entry carries them itself. `None` for an extent,
984 /// which has to be resolved against the archive.
985 pub fn owned(&self) -> Option<&[u8]> {
986 match self {
987 EntryBytes::Owned(v) => Some(v),
988 EntryBytes::Extent { .. } => None,
989 }
990 }
991}
992
993/// One entry's bytes, resolved against a buffer that spans the archive's
994/// coordinate space — **the in-memory resolver**, and the one
995/// [`emit_pack`]'s callers use when the archive is a `&[u8]` rather than a
996/// mapping.
997///
998/// The mapped path is [`crate::git_ops::GitStore::resolve_emit_payloads`]; this
999/// is the same two-arm decision over a slice, written once so that a test
1000/// fixture and a hand-built pack cannot drift from what a served clone does
1001/// (LAW 5). An extent that runs off the buffer is an error and never a short
1002/// slice, for the reason [`crate::archive_map::Mapped::get`] gives: a truncated
1003/// entry spliced into a pack is data the client will only reject much later.
1004pub fn resolve_against<'b>(e: &'b EmitEntry, archive: &'b [u8]) -> Result<&'b [u8]> {
1005 match &e.stored {
1006 EntryBytes::Owned(v) => Ok(v),
1007 EntryBytes::Extent { offset, len } => {
1008 let (a, b) = (*offset as usize, (*offset + *len) as usize);
1009 archive.get(a..b).ok_or_else(|| {
1010 anyhow!(
1011 "the entry at archive offset {offset} spans ({offset}, {len}), which runs off \
1012 the end of a {}-byte buffer",
1013 archive.len()
1014 )
1015 })
1016 }
1017 }
1018}
1019
1020/// One object to emit: where its stored entry is, and what it deltas against.
1021#[derive(Debug, Clone)]
1022pub struct EmitEntry {
1023 /// The object id, for `REF_DELTA` rewriting and for reporting.
1024 pub oid: Vec<u8>,
1025 /// The bytes this entry contributes — header and all.
1026 ///
1027 /// Normally an [`EntryBytes::Extent`]: the stored entry **exactly as
1028 /// received**, addressed and not copied. For the entry shapes that cannot be
1029 /// shipped verbatim — chiefly a delta whose base the caller is not sending —
1030 /// [`crate::git_ops::GitStore::emit_set`] rebuilds the bytes from the
1031 /// resolved object, hands them over as [`EntryBytes::Owned`], and says so in
1032 /// [`Self::recompressed`].
1033 pub stored: EntryBytes,
1034 /// The entry type **of `stored`**, so a copied delta is
1035 /// `OfsDelta`/`RefDelta` and a rebuilt one is the resolved type.
1036 pub obj_type: ObjType,
1037 /// Post-resolution size, which is what the entry header carries.
1038 pub uncompressed_size: u64,
1039 /// The base's archive offset, `0` for none.
1040 pub delta_base: u64,
1041 /// This entry's own archive offset — the key other entries name it by.
1042 pub offset: u64,
1043 /// **`stored` was rebuilt rather than copied**, so its payload was inflated
1044 /// and re-deflated.
1045 ///
1046 /// Carried on the entry rather than inferred at emission because nothing
1047 /// downstream can tell one zlib stream from another: a re-deflate and a copy
1048 /// produce packs that both pass `git index-pack --strict` and differ only in
1049 /// the CPU they cost. This flag is what makes [`EmitReport::recompressed`] a
1050 /// count of what happened instead of a hopeful zero.
1051 pub recompressed: bool,
1052 /// **`stored` is a delta this server COMPUTED**, against a base the request
1053 /// does carry — rather than the whole object.
1054 ///
1055 /// A strict refinement of [`Self::recompressed`], never set without it: such
1056 /// an entry really was inflated and re-deflated, so it is not `copied` and
1057 /// must not be counted as one. What it adds is *which* rebuild happened,
1058 /// because the two differ by ~4× on the wire and the receipt is the only
1059 /// place that difference is visible — a whole rebuild and a computed delta
1060 /// both pass `index-pack --strict` and `fsck`.
1061 ///
1062 /// Zero for a full clone, for the same reason `recompressed` is: a
1063 /// whole-repository request contains every base, so nothing is rebuilt at
1064 /// all. See [`crate::delta`].
1065 pub deltified: bool,
1066}
1067
1068/// Order `entries` so that every delta follows the base it names.
1069///
1070/// # Why a topological order and not the input order
1071///
1072/// `OFS_DELTA` names its base by **backwards** distance. A base that has not
1073/// been written yet has no distance to name, so an order that puts a delta first
1074/// is not merely inefficient — it cannot be encoded at all.
1075///
1076/// The edges are already data: `delta_base` is an absolute archive offset, and
1077/// `offset` is what an entry is named by. So this is a sort over facts the index
1078/// holds, not a graph anyone has to build.
1079///
1080/// # An entry whose base is not in the set
1081///
1082/// Left where it is, and reported. The caller decides: add the base (right for a
1083/// clone, where the base is reachable anyway) or rewrite the entry as a
1084/// `REF_DELTA` naming the base by oid (right for a thin fetch, where the client
1085/// consented and already holds it). Deciding here would make one of those two
1086/// impossible.
1087///
1088/// Returns the ordered entries and the offsets that were named but absent.
1089pub fn topological_order(entries: Vec<EmitEntry>) -> (Vec<EmitEntry>, Vec<u64>) {
1090 use std::collections::{HashMap, HashSet};
1091
1092 let present: HashMap<u64, usize> = entries
1093 .iter()
1094 .enumerate()
1095 .map(|(i, e)| (e.offset, i))
1096 .collect();
1097
1098 let mut missing = Vec::new();
1099 let mut done: HashSet<usize> = HashSet::new();
1100 // The ORDER, not the entries: the traversal used to push
1101 // `entries[i].clone()`, and an `EmitEntry` owns its stored bytes — so
1102 // ordering a pack cloned every payload in it, a full second copy of the
1103 // emission. Profiled on oden 2026-08-12 as part of the ~44 % of serve CPU
1104 // spent copying/zeroing at 32 concurrent clones. The entries are moved out
1105 // by slot once the order is known; not one payload byte is copied here.
1106 let mut order: Vec<usize> = Vec::with_capacity(entries.len());
1107
1108 // Iterative rather than recursive: a delta chain is allowed to be 50 deep by
1109 // default and nothing forbids a pathological one, so the depth belongs on
1110 // the heap where it cannot take the thread's stack with it.
1111 for start in 0..entries.len() {
1112 if done.contains(&start) {
1113 continue;
1114 }
1115 let mut stack = vec![start];
1116 let mut on_path: HashSet<usize> = HashSet::new();
1117 while let Some(&i) = stack.last() {
1118 if done.contains(&i) {
1119 stack.pop();
1120 continue;
1121 }
1122 let base = entries[i].delta_base;
1123 let pending = if base == 0 {
1124 None
1125 } else {
1126 match present.get(&base) {
1127 Some(&b) if !done.contains(&b) => {
1128 // A cycle is impossible in a well-formed pack — a base
1129 // always precedes its delta, so the offsets strictly
1130 // decrease — but a corrupt one could claim otherwise,
1131 // and looping for ever is a worse answer than emitting
1132 // in an order the encoder will then refuse.
1133 if on_path.contains(&b) {
1134 None
1135 } else {
1136 Some(b)
1137 }
1138 }
1139 Some(_) => None,
1140 None => {
1141 missing.push(base);
1142 None
1143 }
1144 }
1145 };
1146 match pending {
1147 Some(b) => {
1148 on_path.insert(i);
1149 stack.push(b);
1150 }
1151 None => {
1152 stack.pop();
1153 on_path.remove(&i);
1154 done.insert(i);
1155 order.push(i);
1156 }
1157 }
1158 }
1159 }
1160
1161 missing.sort_unstable();
1162 missing.dedup();
1163 // Materialise by MOVING each entry into its ordered place. Every index is
1164 // in `order` exactly once (`done` gates the push), so every slot is taken
1165 // exactly once.
1166 let mut slots: Vec<Option<EmitEntry>> = entries.into_iter().map(Some).collect();
1167 let out: Vec<EmitEntry> = order
1168 .into_iter()
1169 .map(|i| {
1170 slots[i]
1171 .take()
1172 .expect("topological_order emitted an index twice")
1173 })
1174 .collect();
1175 (out, missing)
1176}
1177
1178#[cfg(test)]
1179mod order_tests {
1180 use super::*;
1181
1182 fn e(offset: u64, delta_base: u64) -> EmitEntry {
1183 EmitEntry {
1184 oid: vec![offset as u8],
1185 stored: EntryBytes::Owned(Vec::new()),
1186 obj_type: if delta_base == 0 {
1187 ObjType::Blob
1188 } else {
1189 ObjType::OfsDelta
1190 },
1191 uncompressed_size: 0,
1192 delta_base,
1193 offset,
1194 recompressed: false,
1195 deltified: false,
1196 }
1197 }
1198
1199 /// **Every base precedes every delta that names it**, asserted as a
1200 /// position comparison rather than by eyeballing the sequence.
1201 ///
1202 /// The input is deliberately worst-case: a chain handed in exactly
1203 /// backwards, so an implementation that returned its input — or that sorted
1204 /// by offset, which looks right and is not — fails. Sorting by offset
1205 /// happens to work here only because the chain is linear, so a fork is in
1206 /// the fixture too.
1207 #[test]
1208 fn a_base_always_precedes_the_delta_that_names_it() {
1209 // 100 <- 200 <- 300, plus 400 forking off 200, handed in reverse.
1210 let entries = vec![e(400, 200), e(300, 200), e(200, 100), e(100, 0)];
1211 let (ordered, missing) = topological_order(entries);
1212
1213 assert!(missing.is_empty(), "nothing was absent: {missing:?}");
1214 assert_eq!(ordered.len(), 4, "every entry must be emitted exactly once");
1215
1216 let at = |off: u64| ordered.iter().position(|x| x.offset == off).unwrap();
1217 for (delta, base) in [(200u64, 100u64), (300, 200), (400, 200)] {
1218 assert!(
1219 at(base) < at(delta),
1220 "base {base} at {} must precede delta {delta} at {}",
1221 at(base),
1222 at(delta)
1223 );
1224 }
1225 }
1226
1227 /// A base outside the set is **reported, not invented**.
1228 ///
1229 /// The caller has two correct answers — add the base, or rewrite as
1230 /// `REF_DELTA` — and both need to know which offset was missing. Silently
1231 /// dropping the entry would produce a pack whose closure does not hold, and
1232 /// `index-pack --strict` would be the first thing to say so, a long way from
1233 /// here.
1234 #[test]
1235 fn a_base_outside_the_set_is_reported_rather_than_dropped() {
1236 let (ordered, missing) = topological_order(vec![e(300, 999), e(100, 0)]);
1237 assert_eq!(missing, vec![999], "the absent base must be named");
1238 assert_eq!(ordered.len(), 2, "the entry stays; the caller decides");
1239 }
1240
1241 /// A cycle cannot happen in a well-formed pack — offsets strictly decrease
1242 /// along a chain — but a corrupt one may claim it, and looping for ever is
1243 /// the worst possible answer.
1244 #[test]
1245 fn a_cycle_terminates_instead_of_hanging() {
1246 let (ordered, _) = topological_order(vec![e(100, 200), e(200, 100)]);
1247 assert_eq!(ordered.len(), 2, "both entries must still be emitted");
1248 }
1249}
1250
1251/// What emitting produced, in facts a caller can assert on.
1252#[derive(Debug, Clone, PartialEq, Eq, Default)]
1253pub struct EmitReport {
1254 /// Entries written.
1255 pub written: u32,
1256 /// Entries whose stored payload was copied unchanged. It is counted rather
1257 /// than assumed because "correct but slow" is the failure `index-pack
1258 /// --strict` and `fsck` both pass.
1259 pub copied: u32,
1260 /// Entries whose payload was **inflated and re-deflated** because the caller
1261 /// could not ship the stored bytes — a delta whose base is outside the
1262 /// request. `copied + recompressed == written`, always.
1263 ///
1264 /// Zero for a whole-repository clone, which is why the copy claim survives:
1265 /// a full clone's selection contains every base, so no entry ever needs
1266 /// rebuilding. It is non-zero exactly at the boundary a narrowed request
1267 /// cuts through, and that is the number worth watching.
1268 pub recompressed: u32,
1269 /// Of the [`Self::recompressed`], how many went out as a **computed delta**
1270 /// against a base the pack carries rather than as a whole object.
1271 /// `deltified <= recompressed`, always.
1272 ///
1273 /// The two rebuilds cost the same receipt and very different bytes, so
1274 /// without this column a boundary-heavy request looks identical whether the
1275 /// re-delta ran or not. Measured on `h2h-linear-sha1-2048c-1024f-16k`'s
1276 /// narrowed clone: **961 recompressed, of which 859 deltified**, and the
1277 /// pack fell from 11 613 666 bytes to 6 424 726 for the identical 31 805
1278 /// objects.
1279 pub deltified: u32,
1280 /// Entries whose `OFS_DELTA` distance had to be re-encoded because the gap
1281 /// to their base changed. Zero only if the subset happened to preserve every
1282 /// gap, which for anything but a whole pack it will not.
1283 pub rebased: u32,
1284 /// Bytes on the wire, trailer included.
1285 pub bytes: u64,
1286}
1287
1288/// Assemble ordered entries into a packfile.
1289///
1290/// # What this does and does not do
1291///
1292/// It copies. Every entry's compressed payload goes out **byte for byte** as
1293/// `payload_of` hands it over; nothing here inflates, deflates or computes a
1294/// delta. The only thing rewritten is the entry header, and only because
1295/// `OFS_DELTA` carries a distance that is relative to a position in the *input*
1296/// pack. An entry whose payload the *caller* rebuilt says so in
1297/// [`EmitEntry::recompressed`] and is counted apart, so the receipt names which
1298/// of the two happened rather than assuming.
1299///
1300/// `entries` must already be in [`topological_order`] and must be closed —
1301/// every `OFS_DELTA` base present. A base that is absent is an error here
1302/// rather than a silently dropped back-reference, because the alternative is a
1303/// pack whose closure does not hold and a client that discovers it.
1304///
1305/// The caller supplies `stored_of`, which **borrows** entry `i`'s whole stored
1306/// bytes — header included. Splitting it that way keeps this function free of
1307/// any opinion about where bytes live: they may be a slice of a mapping of the
1308/// archive, a slice of an owned buffer the caller rebuilt, or anything else that
1309/// outlives the call.
1310///
1311/// # Why the WHOLE entry and not the payload
1312///
1313/// It used to be `payload_of`, handing over `stored[header_len..]`, and the
1314/// header the caller had just stripped was then re-parsed **here** anyway — for
1315/// the stated size, and for a `REF_DELTA`'s base oid. Three readings of one
1316/// varint across two files, and the caller's `header_len` had to agree with this
1317/// function's `type_and_size` or a ref-delta went out with its base named twice
1318/// (which it once did: `git index-pack --strict` answered `inflate returned 1`).
1319/// One reading, in one place, is LAW 5's fix by construction.
1320///
1321/// # It streams, and it borrows — `P-018` and `P-025`
1322///
1323/// Two properties, both deliberate, both previously absent:
1324///
1325/// * **`stored_of` returns `&[u8]`, not `Vec<u8>`.** It used to return an owned
1326/// buffer, which is `P-025` in its exact form: `data.to_owned()`, **one heap
1327/// allocation per object served**, on a path whose entire claim is that it
1328/// copies stored bytes without touching them. It is indexed by entry rather
1329/// than handed an `&EmitEntry` so that the bytes may live *outside* the entry —
1330/// which since 2026-08-14 they do: an [`EntryBytes::Extent`] is an address, and
1331/// the bytes it names are a slice of the mapped archive.
1332/// * **The pack goes to `out` as it is built, and the trailer is hashed
1333/// incrementally.** It used to accumulate the whole pack in a `Vec<u8>` and
1334/// hash it at the end, so serving a 2 GiB clone meant holding 2 GiB. The one
1335/// fact the old shape got for free — the output offset an `OFS_DELTA` distance
1336/// is measured against — is counted here instead, which is cheaper than the
1337/// buffer that was carrying it.
1338///
1339/// What is emphatically **not** here is gix's pack pipeline: no counting pass
1340/// with a serial reduce, no `sort_by` over counts, no `BTreeMap` reorder, no
1341/// hashing of every byte on a consuming thread. Those are `P-018`'s serial tail,
1342/// and this function is the reason none of it is needed — the bytes are already
1343/// deflated and already delta-encoded, so emitting is a copy and an addition.
1344pub fn emit_pack<'p>(
1345 entries: &'p [EmitEntry],
1346 hash: GitHashKind,
1347 out: &mut dyn std::io::Write,
1348 stored_of: &dyn Fn(usize) -> Result<&'p [u8]>,
1349) -> Result<EmitReport> {
1350 use std::collections::HashMap;
1351
1352 let count = u32::try_from(entries.len()).map_err(|_| {
1353 anyhow!(
1354 "a pack holds at most u32::MAX entries, was given {}",
1355 entries.len()
1356 )
1357 })?;
1358
1359 let mut out = Trailing::new(out, hash);
1360 out.put(b"PACK")?;
1361 out.put(&2u32.to_be_bytes())?;
1362 out.put(&count.to_be_bytes())?;
1363 // One scratch buffer for entry headers, reused for every entry. A header is
1364 // a handful of bytes and there is one per object, so allocating it per entry
1365 // would be the same defect `payload_of` above just stopped committing.
1366 let mut hdr: Vec<u8> = Vec::with_capacity(32);
1367
1368 // Where each input offset landed in the output. Built as we go, which is
1369 // exactly why the order has to be topological: a delta's base must already
1370 // be in here when the delta is written.
1371 let mut placed: HashMap<u64, u64> = HashMap::with_capacity(entries.len());
1372 let mut report = EmitReport::default();
1373
1374 for (i, e) in entries.iter().enumerate() {
1375 let here = out.written();
1376
1377 // The entry's whole stored bytes — a slice of the mapped archive for
1378 // everything a clone sends, a slice of a rebuilt buffer for the
1379 // exceptions. Resolved once and read three times below; asking for it
1380 // per use would be three bounds checks or three preads.
1381 let stored = stored_of(i)?;
1382
1383 // **The size in the header is the one the ENTRY states, not the one the
1384 // index holds.** For a delta they are different numbers: the header
1385 // carries the length of the delta stream, and `uncompressed_size` is the
1386 // size after the chain is applied. Re-encoding a delta header with the
1387 // resolved size produces a stream `git index-pack --strict` calls
1388 // `inflate returned 1`, which is what it did. Re-reading the grammar
1389 // rather than trusting a neighbouring column is the same discipline
1390 // `header_len` follows, and for the same reason.
1391 let (_, stated_size, varint_len) = type_and_size(stored)?;
1392
1393 match e.obj_type {
1394 ObjType::OfsDelta => {
1395 let base_at = *placed.get(&e.delta_base).ok_or_else(|| {
1396 anyhow!(
1397 "entry at {} deltas against archive offset {}, which is not in this pack \
1398 — the set is not closed and was not ordered by `topological_order`",
1399 e.offset,
1400 e.delta_base
1401 )
1402 })?;
1403 // Backwards distance in OUTPUT coordinates. `here` is always
1404 // greater, because the base was written first.
1405 let distance = here - base_at;
1406 hdr.clear();
1407 encode_type_and_size(&mut hdr, ObjType::OfsDelta, stated_size);
1408 encode_ofs_distance(&mut hdr, distance);
1409 out.put(&hdr)?;
1410 report.rebased += 1;
1411 }
1412 ObjType::RefDelta => {
1413 hdr.clear();
1414 encode_type_and_size(&mut hdr, ObjType::RefDelta, stated_size);
1415 // The base oid follows the header verbatim; it names an object
1416 // rather than a position, so a subset never invalidates it.
1417 let oid_len = hash.oid_len();
1418 // The oid sits directly after the type/size varint — NOT after
1419 // `header_len`, which now includes the oid itself.
1420 let base = stored.get(varint_len..).and_then(|r| r.get(..oid_len));
1421 let base = base.ok_or_else(|| {
1422 anyhow!(
1423 "a ref-delta entry at {} has no base oid after its header",
1424 e.offset
1425 )
1426 })?;
1427 hdr.extend_from_slice(base);
1428 out.put(&hdr)?;
1429 }
1430 t => {
1431 hdr.clear();
1432 encode_type_and_size(&mut hdr, t, stated_size);
1433 out.put(&hdr)?;
1434 }
1435 }
1436
1437 // The payload is what follows the header the loop above just re-encoded,
1438 // and `header_len` is the one reading of that grammar. **This is the only
1439 // place a stored byte is touched** — a page fault on the mapping, a hash
1440 // update and a write, with no buffer in between.
1441 out.put(&stored[header_len(stored, hash)?..])?;
1442 if e.recompressed {
1443 report.recompressed += 1;
1444 if e.deltified {
1445 report.deltified += 1;
1446 }
1447 } else {
1448 report.copied += 1;
1449 debug_assert!(
1450 !e.deltified,
1451 "a computed delta was inflated and re-deflated to produce it; counting it as a \
1452 copy would make the receipt a claim rather than a measurement"
1453 );
1454 }
1455 report.written += 1;
1456 placed.insert(e.offset, here);
1457 }
1458
1459 // The trailer is over everything written, which is what a reader checks.
1460 report.bytes = out.finish()?;
1461 Ok(report)
1462}
1463
1464/// `out`, plus the running pack checksum and the byte count `OFS_DELTA`
1465/// distances are measured against.
1466///
1467/// The two facts the old `Vec<u8>` accumulator was really being kept for. Held
1468/// as three fields instead, so a clone streams and nothing sizes an allocation
1469/// by the repository.
1470struct Trailing<'a> {
1471 inner: &'a mut dyn std::io::Write,
1472 digest: Digest,
1473 written: u64,
1474}
1475
1476/// The pack trailer's hash, fed as the pack is written.
1477///
1478/// An enum and not a `Box<dyn Digest>`: there are exactly two, both are
1479/// already direct dependencies of this crate, and a vtable per `update` on the
1480/// one path that touches every emitted byte is not a cost this takes for two
1481/// variants.
1482enum Digest {
1483 Sha1(sha1::Sha1),
1484 Sha256(sha2::Sha256),
1485}
1486
1487impl<'a> Trailing<'a> {
1488 fn new(inner: &'a mut dyn std::io::Write, hash: GitHashKind) -> Self {
1489 use sha1::Digest as _;
1490 Trailing {
1491 inner,
1492 digest: match hash {
1493 GitHashKind::Sha1 => Digest::Sha1(sha1::Sha1::new()),
1494 GitHashKind::Sha256 => Digest::Sha256(sha2::Sha256::new()),
1495 },
1496 written: 0,
1497 }
1498 }
1499
1500 /// Bytes written so far — the output coordinate an `OFS_DELTA` distance is
1501 /// relative to.
1502 fn written(&self) -> u64 {
1503 self.written
1504 }
1505
1506 fn put(&mut self, bytes: &[u8]) -> Result<()> {
1507 use sha1::Digest as _;
1508 match &mut self.digest {
1509 Digest::Sha1(d) => d.update(bytes),
1510 Digest::Sha256(d) => d.update(bytes),
1511 }
1512 self.inner
1513 .write_all(bytes)
1514 .context("writing an emitted pack")?;
1515 self.written += bytes.len() as u64;
1516 Ok(())
1517 }
1518
1519 /// Append the trailer and report the total, trailer included.
1520 fn finish(self) -> Result<u64> {
1521 use sha1::Digest as _;
1522 let digest: Vec<u8> = match self.digest {
1523 Digest::Sha1(d) => d.finalize().to_vec(),
1524 Digest::Sha256(d) => d.finalize().to_vec(),
1525 };
1526 self.inner
1527 .write_all(&digest)
1528 .context("writing the pack trailer")?;
1529 self.inner.flush().context("flushing an emitted pack")?;
1530 Ok(self.written + digest.len() as u64)
1531 }
1532}
1533
1534/// How many bytes of a stored entry are header — everything before the
1535/// compressed payload.
1536///
1537/// For a delta that includes the back-reference: the `OFS_DELTA` distance or the
1538/// `REF_DELTA` oid. This is what a caller strips to get the payload, and it is
1539/// derived by re-reading the grammar rather than remembered, so it cannot drift
1540/// from what the walk parses.
1541pub fn header_len(stored: &[u8], hash: GitHashKind) -> Result<usize> {
1542 let (t, _, n) = type_and_size(stored)?;
1543 Ok(match t {
1544 ObjType::OfsDelta => {
1545 let (_, d) = ofs_distance(stored.get(n..).unwrap_or(&[]))?;
1546 n + d
1547 }
1548 // **The base oid is part of the header, not of the payload.** It was
1549 // excluded here once, on the reasoning that its width is the caller's to
1550 // know. That made `stored[header_len..]` still contain the oid, and
1551 // `emit_pack` writes the oid itself — so every ref-delta went out with
1552 // its base named twice and `git index-pack --strict` answered
1553 // `inflate returned 1`. The hash kind is a parameter now, and the
1554 // ambiguity is gone rather than documented.
1555 ObjType::RefDelta => n + hash.oid_len(),
1556 _ => n,
1557 })
1558}
1559
1560#[cfg(test)]
1561mod emit_tests {
1562 use super::*;
1563
1564 /// **A pack this emits is one `git index-pack --strict` accepts.**
1565 ///
1566 /// The arbiter is stock git and nothing else. Our own parser reading back
1567 /// what our own writer produced proves the two agree with each other, which
1568 /// is exactly the circularity that lets a wrong varint through — both halves
1569 /// would be wrong the same way. `index-pack --strict` shares no code with
1570 /// this file.
1571 ///
1572 /// The corpus is a real pack off this machine, re-emitted whole. Whole
1573 /// rather than a subset for the first assertion, because a whole pack is the
1574 /// case where every delta base is present by construction, so a failure here
1575 /// is the encoder and cannot be the closure.
1576 #[test]
1577 fn stock_git_accepts_a_pack_we_emitted() {
1578 if std::process::Command::new("git")
1579 .arg("--version")
1580 .output()
1581 .is_err()
1582 {
1583 eprintln!("skipping: no git on PATH");
1584 return;
1585 }
1586 let (pack, _) = crate::store::tests::real_pack();
1587 let walk = walk(&pack, GitHashKind::Sha1.oid_len()).expect("the corpus pack walks");
1588
1589 let entries: Vec<EmitEntry> = walk
1590 .entries
1591 .iter()
1592 .map(|e| EmitEntry {
1593 oid: Vec::new(),
1594 // The corpus pack is in memory here, so the extent IS the pack's
1595 // own offset and the resolver below slices it. That is exactly
1596 // the shape a store uses, with the mapping in place of `pack`.
1597 stored: EntryBytes::Extent {
1598 offset: e.offset,
1599 len: e.len,
1600 },
1601 obj_type: e.obj_type,
1602 uncompressed_size: e.uncompressed_size,
1603 delta_base: e.delta_base.as_offset(),
1604 offset: e.offset,
1605 recompressed: false,
1606 deltified: false,
1607 })
1608 .collect();
1609 let n = entries.len();
1610 assert!(
1611 n > 0,
1612 "the corpus pack has no entries; nothing is being tested"
1613 );
1614
1615 let (ordered, missing) = topological_order(entries);
1616 assert!(
1617 missing.is_empty(),
1618 "a whole pack must be closed: {missing:?}"
1619 );
1620
1621 let mut bytes = Vec::new();
1622 let report = emit_pack(&ordered, GitHashKind::Sha1, &mut bytes, &|i| {
1623 resolve_against(&ordered[i], &pack)
1624 })
1625 .expect("emitting");
1626 assert_eq!(
1627 report.bytes as usize,
1628 bytes.len(),
1629 "the report's byte count must be what was actually written"
1630 );
1631
1632 assert_eq!(report.written as usize, n, "every entry must be written");
1633 assert_eq!(
1634 report.copied, report.written,
1635 "every payload must be COPIED"
1636 );
1637
1638 // 🔴 Through [`crate::git_oracle`], and that is the 2026-08-11 fix here:
1639 // this ran `index-pack --strict` in a plain `tempfile::tempdir()`, and
1640 // outside a repository that command **segfaults** on any pack it would
1641 // have rejected — silently, with no output. The oracle now runs inside a
1642 // fresh bare repo and refuses to read a signal death as a verdict.
1643 let dir = tempfile::tempdir().expect("tempdir");
1644 crate::git_oracle::assert_git_accepts(
1645 dir.path(),
1646 "emitted.git",
1647 &bytes,
1648 crate::git_oracle::Strictness::Connected,
1649 );
1650 }
1651}