Skip to main content

sley_pack/
write.rs

1//! Pack generation: options, deltified/undeltified writes, compression, and bitmap output.
2//!
3//! Split out of `lib.rs` in the W21 mechanical refactor: a pure code move
4//! (no function body changed); all items are re-exported from `lib.rs`.
5use super::*;
6
7/// Default sliding-window size used by [`PackFile::write_packed`].
8///
9/// Each object is compared against up to this many previously emitted
10/// candidates of the same type when searching for a small delta. Matches git's
11/// default `pack.window`.
12pub const DEFAULT_PACK_WINDOW: usize = 10;
13
14/// Default maximum delta chain depth used by [`PackFile::write_packed`].
15///
16/// A delta may reference a base that is itself a delta; this bounds how long
17/// such chains may grow so that reconstructing any object stays cheap and the
18/// reader's recursion stays shallow. Matches git's default `pack.depth`.
19pub const DEFAULT_PACK_DEPTH: usize = 50;
20
21/// Object-count threshold before pack payload compression is fanned out across
22/// worker threads. Below this, thread setup and extra buffering cost more than
23/// they save.
24pub(crate) const PACK_PARALLEL_COMPRESSION_MIN_OBJECTS: usize = 64;
25
26/// Keep parallel compression bounded. Git gets much of its wall-clock win from
27/// using several cores, but unbounded threads can steal cache from delta
28/// planning and inflate peak memory on large packs.
29pub(crate) const PACK_PARALLEL_COMPRESSION_MAX_THREADS: usize = 4;
30
31/// Per-object metadata charged against [`PackWriteLimits::compression_working_set`]
32/// so a flood of empty blobs cannot form an unbounded compression window.
33const PACK_OBJECT_WINDOW_OVERHEAD: u64 = 64;
34
35/// Options controlling sliding-window delta selection during pack generation.
36///
37/// Construct with [`PackWriteOptions::new`] (sensible defaults) and adjust with
38/// the builder-style setters, or build one directly. Used by
39/// [`PackFile::write_packed_with_options`] and [`PackFile::write_thin`].
40#[derive(Debug, Clone)]
41pub struct PackWriteOptions {
42    /// Number of previous same-type candidates each object is deltified
43    /// against. Larger windows find better deltas at higher cost.
44    pub window: usize,
45    /// Maximum delta chain depth. A value of `0` disables deltification.
46    pub depth: usize,
47    /// When `true`, in-pack deltas are encoded as ofs-deltas (the default and
48    /// git's preference). When `false`, in-pack deltas use ref-deltas. Deltas
49    /// against external thin-pack bases always use ref-deltas regardless.
50    pub prefer_ofs_delta: bool,
51    /// External base objects, keyed by object id, that are *not* written into
52    /// the pack but may be used as delta bases. Supplying any entries here
53    /// produces a thin pack (see [`PackFile::write_thin`]). Empty by default,
54    /// yielding a self-contained pack.
55    pub thin_bases: HashMap<ObjectId, EncodedObject>,
56    /// Preferred external base for a specific target object. Upload-pack uses
57    /// this to preserve an existing on-disk delta when its base belongs to the
58    /// client. Preferred pairs avoid comparing every target with every thin
59    /// base and are used when the recomputed delta remains worthwhile.
60    pub preferred_thin_bases: HashMap<ObjectId, ObjectId>,
61    /// When `true` (the default), objects are reordered by type and size for
62    /// better delta locality. When `false`, the input order is preserved (the
63    /// emitted pack lists objects in the order supplied); deltas then only
64    /// reference earlier input objects. Reordering is always skipped when
65    /// deltification is disabled (`depth == 0`), since it has no effect there.
66    pub reorder: bool,
67    /// Zlib compression level for pack entry payloads.
68    pub compression_level: u32,
69}
70
71impl Default for PackWriteOptions {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl PackWriteOptions {
78    /// Options with git-compatible defaults: window
79    /// [`DEFAULT_PACK_WINDOW`], depth [`DEFAULT_PACK_DEPTH`], ofs-deltas, and
80    /// no external thin bases.
81    pub fn new() -> Self {
82        Self {
83            window: DEFAULT_PACK_WINDOW,
84            depth: DEFAULT_PACK_DEPTH,
85            prefer_ofs_delta: true,
86            thin_bases: HashMap::new(),
87            preferred_thin_bases: HashMap::new(),
88            reorder: true,
89            compression_level: 6,
90        }
91    }
92
93    /// Set the sliding-window size.
94    pub fn with_window(mut self, window: usize) -> Self {
95        self.window = window;
96        self
97    }
98
99    /// Set the maximum delta chain depth (`0` disables deltas).
100    pub fn with_depth(mut self, depth: usize) -> Self {
101        self.depth = depth;
102        self
103    }
104
105    /// Choose whether in-pack deltas use ofs-delta (`true`) or ref-delta
106    /// (`false`) base references.
107    pub fn with_prefer_ofs_delta(mut self, prefer_ofs_delta: bool) -> Self {
108        self.prefer_ofs_delta = prefer_ofs_delta;
109        self
110    }
111
112    /// Provide the set of external base objects permitted for a thin pack.
113    pub fn with_thin_bases(mut self, thin_bases: HashMap<ObjectId, EncodedObject>) -> Self {
114        self.thin_bases = thin_bases;
115        self
116    }
117
118    /// Prefer a particular external base for each target object id.
119    pub fn with_preferred_thin_bases(
120        mut self,
121        preferred_thin_bases: HashMap<ObjectId, ObjectId>,
122    ) -> Self {
123        self.preferred_thin_bases = preferred_thin_bases;
124        self
125    }
126
127    /// Choose whether objects may be reordered for delta locality (`true`) or
128    /// emitted in input order (`false`).
129    pub fn with_reorder(mut self, reorder: bool) -> Self {
130        self.reorder = reorder;
131        self
132    }
133
134    /// Set the zlib compression level used for pack entry payloads.
135    pub fn with_compression_level(mut self, level: u32) -> Self {
136        self.compression_level = level.min(9);
137        self
138    }
139}
140
141/// Memory budgets for streaming pack generation.
142///
143/// Compression windows are admitted by these byte budgets, not by a fixed
144/// object count. A single object larger than [`Self::compression_working_set`]
145/// but within [`Self::decoded_object`] is written as an explicit one-object
146/// quantum. A single retained delta base may likewise exceed
147/// [`Self::delta_base`] and is kept alone. Either case is documented by the
148/// resulting working-set high-water mark; nothing else may silently exceed
149/// the configured budgets.
150///
151/// Peak charged memory is at most `compression_working_set + delta_base` plus
152/// one leftover lookahead object that did not fit the current window, and one
153/// oversized one-object quantum or retained base. The leftover is charged so
154/// decoded RAM cannot silently approach twice the working-set budget.
155/// Zlib output buffers and allocator slack are not included.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct PackWriteLimits {
158    /// Decoded bodies plus per-object overhead admitted into one compression
159    /// quantum.
160    pub compression_working_set: ByteBudget,
161    /// Hard cap on one decoded object body. Exceeding this is a typed limit
162    /// error; the object is not treated as a one-object quantum.
163    pub decoded_object: ByteBudget,
164    /// Retained sliding-window delta-base bodies. Charged separately from the
165    /// compression working set.
166    pub delta_base: ByteBudget,
167}
168
169impl Default for PackWriteLimits {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl PackWriteLimits {
176    /// Fail-closed defaults: 32 MiB compression working set, 512 MiB single
177    /// decoded object, 32 MiB retained delta bases.
178    pub fn new() -> Self {
179        Self {
180            compression_working_set: ByteBudget::new(32 * 1024 * 1024),
181            decoded_object: ByteBudget::new(512 * 1024 * 1024),
182            delta_base: ByteBudget::new(32 * 1024 * 1024),
183        }
184    }
185
186    pub fn with_compression_working_set(mut self, budget: ByteBudget) -> Self {
187        self.compression_working_set = budget;
188        self
189    }
190
191    pub fn with_decoded_object(mut self, budget: ByteBudget) -> Self {
192        self.decoded_object = budget;
193        self
194    }
195
196    pub fn with_delta_base(mut self, budget: ByteBudget) -> Self {
197        self.delta_base = budget;
198        self
199    }
200}
201
202fn pack_object_window_cost(object: &EncodedObject) -> u64 {
203    (object.body.len() as u64).saturating_add(PACK_OBJECT_WINDOW_OVERHEAD)
204}
205
206fn pack_delta_base_cost(object: &EncodedObject) -> u64 {
207    pack_object_window_cost(object)
208}
209
210fn duplicate_pack_object_id(oid: ObjectId) -> GitError {
211    GitError::InvalidFormat(format!("pack contains duplicate object id {oid}"))
212}
213
214fn next_compression_window_end(
215    objects: &[&EncodedObject],
216    order: &[usize],
217    start: usize,
218    budget: ByteBudget,
219) -> usize {
220    if start >= order.len() {
221        return start;
222    }
223    let mut end = start + 1;
224    let mut used = pack_object_window_cost(objects[order[start]]);
225    if used > budget.as_u64() {
226        return end;
227    }
228    while end < order.len() {
229        let additional = pack_object_window_cost(objects[order[end]]);
230        if !budget.allows(used, additional) {
231            break;
232        }
233        used = used.saturating_add(additional);
234        end += 1;
235    }
236    end
237}
238
239struct PendingSourceObject {
240    oid: ObjectId,
241    object: Arc<EncodedObject>,
242}
243
244fn leftover_window_cost(leftover: Option<&PendingSourceObject>) -> u64 {
245    leftover
246        .map(|pending| pack_object_window_cost(&pending.object))
247        .unwrap_or(0)
248}
249
250struct SourcePackStream<I, F> {
251    selected: I,
252    leftover: Option<PendingSourceObject>,
253    seen: HashSet<ObjectId>,
254    yielded: u64,
255    object_count: u32,
256    format: ObjectFormat,
257    limits: PackWriteLimits,
258    read_object: F,
259    index_entries: Vec<PackIndexEntry>,
260    delta_count: u32,
261    peak_working_set_bytes: u64,
262}
263
264impl<I, F> SourcePackStream<I, F>
265where
266    I: Iterator<Item = ObjectId>,
267    F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
268{
269    fn new(
270        selected: I,
271        object_count: u32,
272        format: ObjectFormat,
273        limits: PackWriteLimits,
274        read_object: F,
275    ) -> Self {
276        Self {
277            selected,
278            leftover: None,
279            seen: HashSet::new(),
280            yielded: 0,
281            object_count,
282            format,
283            limits,
284            read_object,
285            index_entries: Vec::new(),
286            delta_count: 0,
287            peak_working_set_bytes: 0,
288        }
289    }
290
291    fn pull(&mut self) -> Result<Option<PendingSourceObject>> {
292        if let Some(pending) = self.leftover.take() {
293            return Ok(Some(pending));
294        }
295        let Some(oid) = self.selected.next() else {
296            return Ok(None);
297        };
298        if self.yielded >= u64::from(self.object_count) {
299            return Err(GitError::count_mismatch(
300                u64::from(self.object_count),
301                self.yielded.saturating_add(1),
302            ));
303        }
304        self.yielded = self.yielded.saturating_add(1);
305        if oid.format() != self.format {
306            return Err(GitError::InvalidObjectId(
307                "pack object id format does not match pack format".into(),
308            ));
309        }
310        if !self.seen.insert(oid) {
311            return Err(duplicate_pack_object_id(oid));
312        }
313        let object = (self.read_object)(&oid)?;
314        let attempted = object.body.len() as u64;
315        let limit = self.limits.decoded_object.as_u64();
316        if attempted > limit {
317            return Err(GitError::resource_limit(
318                ResourceLimitKind::DecodedObject,
319                limit,
320                attempted,
321            ));
322        }
323        Ok(Some(PendingSourceObject { oid, object }))
324    }
325
326    fn fill_window(&mut self, cancel: CancelFlag<'_>) -> Result<(Vec<PendingSourceObject>, u64)> {
327        let mut window = Vec::new();
328        let mut used = 0u64;
329        loop {
330            cancel.check()?;
331            let Some(pending) = self.pull()? else {
332                break;
333            };
334            let cost = pack_object_window_cost(&pending.object);
335            if window.is_empty() {
336                window.push(pending);
337                used = cost;
338                if cost > self.limits.compression_working_set.as_u64() {
339                    break;
340                }
341                continue;
342            }
343            if !self.limits.compression_working_set.allows(used, cost) {
344                self.leftover = Some(pending);
345                break;
346            }
347            used = used.saturating_add(cost);
348            window.push(pending);
349        }
350        Ok((window, used))
351    }
352
353    fn note_peak(&mut self, window_bytes: u64, horizon_bytes: u64) {
354        let now = window_bytes
355            .saturating_add(horizon_bytes)
356            .saturating_add(leftover_window_cost(self.leftover.as_ref()));
357        if now > self.peak_working_set_bytes {
358            self.peak_working_set_bytes = now;
359        }
360    }
361
362    fn finish(self, output: PackDigestWriter<'_, impl Write>) -> Result<PackWriteSummary> {
363        if self.leftover.is_some() || self.yielded != u64::from(self.object_count) {
364            return Err(GitError::count_mismatch(
365                u64::from(self.object_count),
366                self.yielded,
367            ));
368        }
369        let (checksum, pack_size) = output.finish()?;
370        let index = PackIndex::write_v2(self.format, &self.index_entries, &checksum)?;
371        Ok(PackWriteSummary {
372            index,
373            checksum,
374            entries: self.index_entries,
375            delta_count: self.delta_count,
376            pack_size,
377            peak_working_set_bytes: self.peak_working_set_bytes,
378        })
379    }
380}
381
382fn retain_streaming_delta_base(
383    horizon: &mut VecDeque<StreamingDeltaBase>,
384    horizon_bytes: &mut u64,
385    base: StreamingDeltaBase,
386    options: &PackWriteOptions,
387    limits: PackWriteLimits,
388) {
389    if options.depth == 0 || options.window == 0 {
390        return;
391    }
392    let cost = pack_delta_base_cost(&base.object);
393    horizon.push_back(base);
394    *horizon_bytes = horizon_bytes.saturating_add(cost);
395    while horizon.len() > options.window {
396        if let Some(evicted) = horizon.pop_front() {
397            *horizon_bytes = horizon_bytes.saturating_sub(pack_delta_base_cost(&evicted.object));
398        }
399    }
400    while horizon.len() > 1 && *horizon_bytes > limits.delta_base.as_u64() {
401        if let Some(evicted) = horizon.pop_front() {
402            *horizon_bytes = horizon_bytes.saturating_sub(pack_delta_base_cost(&evicted.object));
403        }
404    }
405}
406
407fn validate_thin_base_formats(options: &PackWriteOptions, format: ObjectFormat) -> Result<()> {
408    for oid in options.thin_bases.keys() {
409        if oid.format() != format {
410            return Err(GitError::InvalidObjectId(
411                "thin pack base object id format does not match pack format".into(),
412            ));
413        }
414    }
415    Ok(())
416}
417
418impl PackFile {
419    pub fn write_undeltified_sha1<T>(objects: &[T]) -> Result<PackWrite>
420    where
421        T: Borrow<EncodedObject>,
422    {
423        Self::write_undeltified(objects, ObjectFormat::Sha1)
424    }
425
426    /// Write a pack with every object stored undeltified (no delta entries).
427    ///
428    /// This is the simple, self-contained encoding; objects appear in the given
429    /// order. For smaller output that exploits similarity between objects, use
430    /// [`PackFile::write_packed`].
431    pub fn write_undeltified<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
432    where
433        T: Borrow<EncodedObject>,
434    {
435        let options = PackWriteOptions::new().with_depth(0).with_reorder(false);
436        Self::write_packed_impl(objects, format, &options)
437    }
438
439    /// Write a pack using sliding-window delta selection with git-compatible
440    /// defaults (window [`DEFAULT_PACK_WINDOW`], depth [`DEFAULT_PACK_DEPTH`],
441    /// ofs-deltas, self-contained).
442    ///
443    /// Objects are grouped by type and ordered for good deltas, then each is
444    /// compared against a window of previously emitted candidates; the smallest
445    /// acceptable delta is kept, otherwise the object is stored undeltified. The
446    /// result round-trips through [`PackFile::parse`].
447    pub fn write_packed<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
448    where
449        T: Borrow<EncodedObject>,
450    {
451        Self::write_packed_with_options(objects, format, &PackWriteOptions::new())
452    }
453
454    /// Like [`PackFile::write_packed`] but with caller-supplied
455    /// [`PackWriteOptions`] (window, depth, base-reference style, and optional
456    /// external thin bases).
457    pub fn write_packed_with_options<T>(
458        objects: &[T],
459        format: ObjectFormat,
460        options: &PackWriteOptions,
461    ) -> Result<PackWrite>
462    where
463        T: Borrow<EncodedObject>,
464    {
465        Self::write_packed_impl(objects, format, options)
466    }
467
468    /// Like [`PackFile::write_packed`], but uses caller-supplied object ids
469    /// instead of re-hashing each object before pack planning.
470    ///
471    /// This is intended for object-database paths that reached each object by
472    /// its id and already trust that id/object mapping. The function validates
473    /// id formats and duplicate ids, but it does not re-hash object bodies; use
474    /// [`PackFile::write_packed`] when the ids are not already known to be
475    /// canonical.
476    pub fn write_packed_with_known_ids(
477        inputs: &[PackInput<'_>],
478        format: ObjectFormat,
479    ) -> Result<PackWrite> {
480        Self::write_packed_with_known_ids_and_options(inputs, format, &PackWriteOptions::new())
481    }
482
483    /// Like [`PackFile::write_packed_with_known_ids`] but with caller-supplied
484    /// [`PackWriteOptions`].
485    pub fn write_packed_with_known_ids_and_options(
486        inputs: &[PackInput<'_>],
487        format: ObjectFormat,
488        options: &PackWriteOptions,
489    ) -> Result<PackWrite> {
490        Self::write_packed_with_known_ids_and_options_and_hints(
491            inputs,
492            format,
493            options,
494            PackPlanningHints::new(),
495        )
496    }
497
498    /// Compatibility wrapper for callers that only have Git path-name hashes.
499    pub fn write_packed_with_known_ids_and_options_and_name_hashes(
500        inputs: &[PackInput<'_>],
501        format: ObjectFormat,
502        options: &PackWriteOptions,
503        name_hashes: Option<&HashMap<ObjectId, u32>>,
504    ) -> Result<PackWrite> {
505        let hints = name_hashes.map_or_else(PackPlanningHints::new, |hashes| {
506            PackPlanningHints::new().with_name_hashes(hashes)
507        });
508        Self::write_packed_with_known_ids_and_options_and_hints(inputs, format, options, hints)
509    }
510
511    /// Like [`PackFile::write_packed_with_known_ids_and_options`], with
512    /// repository-derived planning metadata kept separate from encoding
513    /// policy.
514    pub fn write_packed_with_known_ids_and_options_and_hints(
515        inputs: &[PackInput<'_>],
516        format: ObjectFormat,
517        options: &PackWriteOptions,
518        hints: PackPlanningHints<'_>,
519    ) -> Result<PackWrite> {
520        if inputs.len() > u32::MAX as usize {
521            return Err(GitError::InvalidFormat("too many pack objects".into()));
522        }
523        let mut objects = Vec::with_capacity(inputs.len());
524        let mut object_ids = Vec::with_capacity(inputs.len());
525        for input in inputs {
526            if input.oid.format() != format {
527                return Err(GitError::InvalidObjectId(format!(
528                    "pack object id {} uses {}, pack uses {}",
529                    input.oid,
530                    input.oid.format().name(),
531                    format.name()
532                )));
533            }
534            objects.push(input.object);
535            object_ids.push(*input.oid);
536        }
537        Self::write_packed_from_parts(objects, object_ids, format, options, hints)
538    }
539
540    pub fn write_packed_with_known_ids_to_writer<W>(
541        inputs: &[PackInput<'_>],
542        format: ObjectFormat,
543        options: &PackWriteOptions,
544        writer: &mut W,
545    ) -> Result<PackWriteSummary>
546    where
547        W: Write,
548    {
549        Self::write_packed_with_known_ids_and_options_and_hints_to_writer(
550            inputs,
551            format,
552            options,
553            PackPlanningHints::new(),
554            writer,
555        )
556    }
557
558    /// Writer-backed known-id pack generation with repository planning hints.
559    pub fn write_packed_with_known_ids_and_options_and_hints_to_writer<W>(
560        inputs: &[PackInput<'_>],
561        format: ObjectFormat,
562        options: &PackWriteOptions,
563        hints: PackPlanningHints<'_>,
564        writer: &mut W,
565    ) -> Result<PackWriteSummary>
566    where
567        W: Write,
568    {
569        if inputs.len() > u32::MAX as usize {
570            return Err(GitError::InvalidFormat("too many pack objects".into()));
571        }
572        let mut objects = Vec::with_capacity(inputs.len());
573        let mut object_ids = Vec::with_capacity(inputs.len());
574        for input in inputs {
575            if input.oid.format() != format {
576                return Err(GitError::InvalidObjectId(format!(
577                    "pack object id {} uses {}, pack uses {}",
578                    input.oid,
579                    input.oid.format().name(),
580                    format.name()
581                )));
582            }
583            objects.push(input.object);
584            object_ids.push(*input.oid);
585        }
586        Self::write_packed_from_parts_to_writer(objects, object_ids, format, options, hints, writer)
587    }
588
589    /// Write a thin pack: objects may be deltified against `external_bases`
590    /// that are *not* included in the pack, referenced by ref-delta to their
591    /// object id.
592    ///
593    /// The receiver must already have (or otherwise obtain) those base objects
594    /// and resolve the pack with [`PackFile::parse_thin`]. Window and depth use
595    /// the defaults; pass options via [`PackFile::write_packed_with_options`]
596    /// with [`PackWriteOptions::with_thin_bases`] for finer control.
597    pub fn write_thin<T>(
598        objects: &[T],
599        format: ObjectFormat,
600        external_bases: HashMap<ObjectId, EncodedObject>,
601    ) -> Result<PackWrite>
602    where
603        T: Borrow<EncodedObject>,
604    {
605        let options = PackWriteOptions::new().with_thin_bases(external_bases);
606        Self::write_packed_impl(objects, format, &options)
607    }
608
609    pub(crate) fn write_packed_impl<T>(
610        objects: &[T],
611        format: ObjectFormat,
612        options: &PackWriteOptions,
613    ) -> Result<PackWrite>
614    where
615        T: Borrow<EncodedObject>,
616    {
617        if objects.len() > u32::MAX as usize {
618            return Err(GitError::InvalidFormat("too many pack objects".into()));
619        }
620        let objects: Vec<&EncodedObject> = objects.iter().map(Borrow::borrow).collect();
621
622        // Compute object ids up front; they are needed both for the index and,
623        // for ref-deltas, inside the pack entries themselves.
624        let mut object_ids: Vec<ObjectId> = Vec::with_capacity(objects.len());
625        for object in &objects {
626            object_ids.push(object.object_id(format)?);
627        }
628        Self::write_packed_from_parts(
629            objects,
630            object_ids,
631            format,
632            options,
633            PackPlanningHints::new(),
634        )
635    }
636
637    pub(crate) fn write_packed_from_parts(
638        objects: Vec<&EncodedObject>,
639        object_ids: Vec<ObjectId>,
640        format: ObjectFormat,
641        options: &PackWriteOptions,
642        hints: PackPlanningHints<'_>,
643    ) -> Result<PackWrite> {
644        let mut seen = HashSet::with_capacity(object_ids.len());
645        for oid in &object_ids {
646            if !seen.insert(oid) {
647                return Err(duplicate_pack_object_id(*oid));
648            }
649        }
650
651        // Validate external thin bases share the pack's hash format.
652        for oid in options.thin_bases.keys() {
653            if oid.format() != format {
654                return Err(GitError::InvalidObjectId(
655                    "thin pack base object id format does not match pack format".into(),
656                ));
657            }
658        }
659
660        // Decide, for each object, whether it is stored undeltified or as a
661        // delta against another object (in-pack or an external thin base), and
662        // obtain the emit order. In-pack deltas only ever reference candidates
663        // that appear earlier in `order`, so emitting in `order` guarantees a
664        // base is always written before any object that deltas against it.
665        let (plan, order) = plan_pack_deltas(&objects, &object_ids, options, hints)?;
666
667        let mut pack = Vec::new();
668        pack.extend_from_slice(b"PACK");
669        pack.extend_from_slice(&2u32.to_be_bytes());
670        pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
671
672        let mut index_entries = Vec::with_capacity(objects.len());
673        let mut delta_count = 0u32;
674        // Pack offset at which each original object index was written, or
675        // `None` until it has been emitted.
676        let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
677
678        let compressed_payloads =
679            compress_planned_payloads(&objects, &plan, &order, options.compression_level)?;
680
681        for (order_pos, &idx) in order.iter().enumerate() {
682            let offset = pack.len() as u64;
683            let mut entry_bytes = Vec::new();
684            match &plan[idx].base {
685                PlannedBase::None => {
686                    write_entry_header(
687                        &mut entry_bytes,
688                        objects[idx].object_type,
689                        objects[idx].body.len() as u64,
690                    );
691                }
692                PlannedBase::InPack { base_idx, delta } => {
693                    delta_count += 1;
694                    let base_offset = written_offsets[*base_idx].ok_or_else(|| {
695                        GitError::InvalidFormat(
696                            "in-pack delta base emitted after dependent object".into(),
697                        )
698                    })?;
699                    if options.prefer_ofs_delta {
700                        write_pack_entry_header_kind(&mut entry_bytes, 6, delta.len() as u64);
701                        let relative = offset.checked_sub(base_offset).ok_or_else(|| {
702                            GitError::InvalidFormat("ofs-delta base offset is after delta".into())
703                        })?;
704                        write_ofs_delta_offset(&mut entry_bytes, relative)?;
705                    } else {
706                        write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
707                        entry_bytes.extend_from_slice(object_ids[*base_idx].as_bytes());
708                    }
709                }
710                PlannedBase::External { base_oid, delta } => {
711                    delta_count += 1;
712                    write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
713                    entry_bytes.extend_from_slice(base_oid.as_bytes());
714                }
715            }
716            entry_bytes.extend_from_slice(&compressed_payloads[order_pos]);
717            let crc32 = crc32fast::hash(&entry_bytes);
718            pack.extend_from_slice(&entry_bytes);
719            written_offsets[idx] = Some(offset);
720            index_entries.push(PackIndexEntry {
721                oid: object_ids[idx].clone(),
722                crc32,
723                offset,
724            });
725        }
726
727        let checksum = sley_core::digest_bytes(format, &pack)?;
728        pack.extend_from_slice(checksum.as_bytes());
729        let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
730        Ok(PackWrite {
731            pack,
732            index,
733            checksum,
734            entries: index_entries,
735            delta_count,
736        })
737    }
738
739    pub(crate) fn write_packed_from_parts_to_writer<W>(
740        objects: Vec<&EncodedObject>,
741        object_ids: Vec<ObjectId>,
742        format: ObjectFormat,
743        options: &PackWriteOptions,
744        hints: PackPlanningHints<'_>,
745        writer: &mut W,
746    ) -> Result<PackWriteSummary>
747    where
748        W: Write,
749    {
750        let mut seen = HashSet::with_capacity(object_ids.len());
751        for oid in &object_ids {
752            if !seen.insert(oid) {
753                return Err(duplicate_pack_object_id(*oid));
754            }
755        }
756
757        for oid in options.thin_bases.keys() {
758            if oid.format() != format {
759                return Err(GitError::InvalidObjectId(
760                    "thin pack base object id format does not match pack format".into(),
761                ));
762            }
763        }
764
765        let (plan, order) = plan_pack_deltas(&objects, &object_ids, options, hints)?;
766        let mut output = PackDigestWriter::new(writer, format);
767        output.write_pack_bytes(b"PACK")?;
768        output.write_pack_bytes(&2u32.to_be_bytes())?;
769        output.write_pack_bytes(&(objects.len() as u32).to_be_bytes())?;
770
771        let mut index_entries = Vec::with_capacity(objects.len());
772        let mut delta_count = 0u32;
773        let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
774
775        let compression_budget = PackWriteLimits::new().compression_working_set;
776        let mut window_start = 0;
777        while window_start < order.len() {
778            let window_end =
779                next_compression_window_end(&objects, &order, window_start, compression_budget);
780            let order_window = &order[window_start..window_end];
781            let compressed_payloads = compress_planned_payloads(
782                &objects,
783                &plan,
784                order_window,
785                options.compression_level,
786            )?;
787            for (&idx, compressed_payload) in order_window.iter().zip(&compressed_payloads) {
788                let offset = output.position();
789                let mut entry_header = Vec::new();
790                match &plan[idx].base {
791                    PlannedBase::None => {
792                        write_entry_header(
793                            &mut entry_header,
794                            objects[idx].object_type,
795                            objects[idx].body.len() as u64,
796                        );
797                    }
798                    PlannedBase::InPack { base_idx, delta } => {
799                        delta_count += 1;
800                        let base_offset = written_offsets[*base_idx].ok_or_else(|| {
801                            GitError::InvalidFormat(
802                                "in-pack delta base emitted after dependent object".into(),
803                            )
804                        })?;
805                        if options.prefer_ofs_delta {
806                            write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
807                            let relative = offset.checked_sub(base_offset).ok_or_else(|| {
808                                GitError::InvalidFormat(
809                                    "ofs-delta base offset is after delta".into(),
810                                )
811                            })?;
812                            write_ofs_delta_offset(&mut entry_header, relative)?;
813                        } else {
814                            write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
815                            entry_header.extend_from_slice(object_ids[*base_idx].as_bytes());
816                        }
817                    }
818                    PlannedBase::External { base_oid, delta } => {
819                        delta_count += 1;
820                        write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
821                        entry_header.extend_from_slice(base_oid.as_bytes());
822                    }
823                }
824                let mut crc32 = crc32fast::Hasher::new();
825                crc32.update(&entry_header);
826                crc32.update(compressed_payload);
827                output.write_pack_bytes(&entry_header)?;
828                output.write_pack_bytes(compressed_payload)?;
829                written_offsets[idx] = Some(offset);
830                index_entries.push(PackIndexEntry {
831                    oid: object_ids[idx],
832                    crc32: crc32.finalize(),
833                    offset,
834                });
835            }
836            window_start = window_end;
837        }
838
839        let (checksum, pack_size) = output.finish()?;
840        let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
841        Ok(PackWriteSummary {
842            index,
843            checksum,
844            entries: index_entries,
845            delta_count,
846            pack_size,
847            peak_working_set_bytes: 0,
848        })
849    }
850
851    pub fn write_undeltified_from_source_to_writer<W, I, F>(
852        selected_objects: I,
853        object_count: u32,
854        format: ObjectFormat,
855        options: &PackWriteOptions,
856        limits: PackWriteLimits,
857        read_object: F,
858        writer: &mut W,
859    ) -> Result<PackWriteSummary>
860    where
861        W: Write,
862        I: IntoIterator<Item = ObjectId>,
863        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
864    {
865        Self::write_undeltified_from_source_to_writer_with_cancel(
866            selected_objects,
867            object_count,
868            format,
869            options,
870            limits,
871            read_object,
872            writer,
873            CancelFlag::never(),
874        )
875    }
876
877    /// Undeltified pack write from a one-shot object-id iterator.
878    ///
879    /// `object_count` is written into the pack header. Yielding fewer or more
880    /// ids is [`GitError::CountMismatch`] and never a successful pack.
881    /// Repeated ids are rejected as they are consumed. Compression windows are
882    /// admitted by [`PackWriteLimits`]. Polls `cancel` between windows and
883    /// while filling a window.
884    #[allow(clippy::too_many_arguments)]
885    pub fn write_undeltified_from_source_to_writer_with_cancel<W, I, F>(
886        selected_objects: I,
887        object_count: u32,
888        format: ObjectFormat,
889        options: &PackWriteOptions,
890        limits: PackWriteLimits,
891        read_object: F,
892        writer: &mut W,
893        cancel: CancelFlag<'_>,
894    ) -> Result<PackWriteSummary>
895    where
896        W: Write,
897        I: IntoIterator<Item = ObjectId>,
898        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
899    {
900        cancel.check()?;
901        let mut stream = SourcePackStream::new(
902            selected_objects.into_iter(),
903            object_count,
904            format,
905            limits,
906            read_object,
907        );
908        let mut output = PackDigestWriter::new(writer, format);
909        output.write_pack_bytes(b"PACK")?;
910        output.write_pack_bytes(&2u32.to_be_bytes())?;
911        output.write_pack_bytes(&object_count.to_be_bytes())?;
912
913        loop {
914            cancel.check()?;
915            let (window, window_bytes) = stream.fill_window(cancel)?;
916            if window.is_empty() {
917                break;
918            }
919            stream.note_peak(window_bytes, 0);
920            let objects = window
921                .iter()
922                .map(|entry| Arc::clone(&entry.object))
923                .collect::<Vec<_>>();
924            let compressed_payloads =
925                compress_undeltified_payloads(&objects, options.compression_level)?;
926            for (entry, compressed_payload) in window.iter().zip(&compressed_payloads) {
927                let offset = output.position();
928                let mut entry_header = Vec::new();
929                write_entry_header(
930                    &mut entry_header,
931                    entry.object.object_type,
932                    entry.object.body.len() as u64,
933                );
934                let mut crc32 = crc32fast::Hasher::new();
935                crc32.update(&entry_header);
936                crc32.update(compressed_payload);
937                output.write_pack_bytes(&entry_header)?;
938                output.write_pack_bytes(compressed_payload)?;
939                stream.index_entries.push(PackIndexEntry {
940                    oid: entry.oid,
941                    crc32: crc32.finalize(),
942                    offset,
943                });
944            }
945        }
946
947        stream.finish(output)
948    }
949
950    pub fn write_packed_from_source_to_writer<W, I, F>(
951        selected_objects: I,
952        object_count: u32,
953        format: ObjectFormat,
954        options: &PackWriteOptions,
955        limits: PackWriteLimits,
956        read_object: F,
957        writer: &mut W,
958    ) -> Result<PackWriteSummary>
959    where
960        W: Write,
961        I: IntoIterator<Item = ObjectId>,
962        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
963    {
964        Self::write_packed_from_source_to_writer_with_hints_and_cancel(
965            selected_objects,
966            object_count,
967            format,
968            options,
969            PackPlanningHints::new(),
970            limits,
971            read_object,
972            writer,
973            CancelFlag::never(),
974        )
975    }
976
977    /// Streaming pack write with repository-derived planning metadata.
978    #[allow(clippy::too_many_arguments)]
979    pub fn write_packed_from_source_to_writer_with_hints<W, I, F>(
980        selected_objects: I,
981        object_count: u32,
982        format: ObjectFormat,
983        options: &PackWriteOptions,
984        hints: PackPlanningHints<'_>,
985        limits: PackWriteLimits,
986        read_object: F,
987        writer: &mut W,
988    ) -> Result<PackWriteSummary>
989    where
990        W: Write,
991        I: IntoIterator<Item = ObjectId>,
992        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
993    {
994        Self::write_packed_from_source_to_writer_with_hints_and_cancel(
995            selected_objects,
996            object_count,
997            format,
998            options,
999            hints,
1000            limits,
1001            read_object,
1002            writer,
1003            CancelFlag::never(),
1004        )
1005    }
1006
1007    /// Streaming deltified pack write from a one-shot object-id iterator.
1008    ///
1009    /// Separates the pack header's required `object_count` from enumeration:
1010    /// `selected_objects` is consumed once and does not need `ExactSizeIterator`,
1011    /// `Clone`, or collection conversion. Yielding fewer or more ids than
1012    /// `object_count` is [`GitError::CountMismatch`] and never a successful pack.
1013    ///
1014    /// Compression windows are admitted by [`PackWriteLimits`], not a fixed
1015    /// object count. A single object larger than the ordinary working-set
1016    /// budget is written as a one-object quantum when it still fits
1017    /// [`PackWriteLimits::decoded_object`]; otherwise it is a typed
1018    /// [`GitError::ResourceLimit`]. Delta-base retention is charged to
1019    /// [`PackWriteLimits::delta_base`]. A leftover lookahead object that does
1020    /// not fit the current window is charged against the working-set peak.
1021    /// Repeated ids are rejected as they are consumed. Output-writer
1022    /// backpressure and `read_object` failures stop further enumeration. Polls
1023    /// `cancel` between windows and while filling a window, and returns
1024    /// [`GitError::Cancelled`] when the flag trips.
1025    #[allow(clippy::too_many_arguments)]
1026    pub fn write_packed_from_source_to_writer_with_cancel<W, I, F>(
1027        selected_objects: I,
1028        object_count: u32,
1029        format: ObjectFormat,
1030        options: &PackWriteOptions,
1031        limits: PackWriteLimits,
1032        read_object: F,
1033        writer: &mut W,
1034        cancel: CancelFlag<'_>,
1035    ) -> Result<PackWriteSummary>
1036    where
1037        W: Write,
1038        I: IntoIterator<Item = ObjectId>,
1039        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
1040    {
1041        Self::write_packed_from_source_to_writer_with_hints_and_cancel(
1042            selected_objects,
1043            object_count,
1044            format,
1045            options,
1046            PackPlanningHints::new(),
1047            limits,
1048            read_object,
1049            writer,
1050            cancel,
1051        )
1052    }
1053
1054    /// Cancel-aware streaming pack write with repository-derived planning
1055    /// metadata.
1056    #[allow(clippy::too_many_arguments)]
1057    pub fn write_packed_from_source_to_writer_with_hints_and_cancel<W, I, F>(
1058        selected_objects: I,
1059        object_count: u32,
1060        format: ObjectFormat,
1061        options: &PackWriteOptions,
1062        hints: PackPlanningHints<'_>,
1063        limits: PackWriteLimits,
1064        read_object: F,
1065        writer: &mut W,
1066        cancel: CancelFlag<'_>,
1067    ) -> Result<PackWriteSummary>
1068    where
1069        W: Write,
1070        I: IntoIterator<Item = ObjectId>,
1071        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
1072    {
1073        validate_thin_base_formats(options, format)?;
1074        cancel.check()?;
1075        let mut stream = SourcePackStream::new(
1076            selected_objects.into_iter(),
1077            object_count,
1078            format,
1079            limits,
1080            read_object,
1081        );
1082        let mut output = PackDigestWriter::new(writer, format);
1083        output.write_pack_bytes(b"PACK")?;
1084        output.write_pack_bytes(&2u32.to_be_bytes())?;
1085        output.write_pack_bytes(&object_count.to_be_bytes())?;
1086
1087        let mut base_horizon: VecDeque<StreamingDeltaBase> = VecDeque::new();
1088        let mut horizon_bytes = 0u64;
1089
1090        loop {
1091            cancel.check()?;
1092            let (window, window_bytes) = stream.fill_window(cancel)?;
1093            if window.is_empty() {
1094                break;
1095            }
1096            stream.note_peak(window_bytes, horizon_bytes);
1097
1098            let objects = window
1099                .iter()
1100                .map(|entry| Arc::clone(&entry.object))
1101                .collect::<Vec<_>>();
1102            let object_ids = window.iter().map(|entry| entry.oid).collect::<Vec<_>>();
1103            let (plan, order) =
1104                plan_streaming_window_deltas(&objects, &object_ids, &base_horizon, options, hints);
1105            let compressed_payloads = compress_streaming_planned_payloads(
1106                &objects,
1107                &plan,
1108                &order,
1109                options.compression_level,
1110            )?;
1111            let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
1112
1113            for (&idx, compressed_payload) in order.iter().zip(&compressed_payloads) {
1114                let offset = output.position();
1115                let mut entry_header = Vec::new();
1116                match &plan[idx].base {
1117                    StreamingPlannedBase::None => {
1118                        write_entry_header(
1119                            &mut entry_header,
1120                            objects[idx].object_type,
1121                            objects[idx].body.len() as u64,
1122                        );
1123                    }
1124                    StreamingPlannedBase::Current { base_idx, delta } => {
1125                        stream.delta_count += 1;
1126                        let base_offset = written_offsets[*base_idx].ok_or_else(|| {
1127                            GitError::InvalidFormat(
1128                                "in-pack delta base emitted after dependent object".into(),
1129                            )
1130                        })?;
1131                        if options.prefer_ofs_delta {
1132                            write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
1133                            let relative = offset.checked_sub(base_offset).ok_or_else(|| {
1134                                GitError::InvalidFormat(
1135                                    "ofs-delta base offset is after delta".into(),
1136                                )
1137                            })?;
1138                            write_ofs_delta_offset(&mut entry_header, relative)?;
1139                        } else {
1140                            write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
1141                            entry_header.extend_from_slice(object_ids[*base_idx].as_bytes());
1142                        }
1143                    }
1144                    StreamingPlannedBase::Previous {
1145                        base_oid,
1146                        base_offset,
1147                        delta,
1148                    } => {
1149                        stream.delta_count += 1;
1150                        if options.prefer_ofs_delta {
1151                            write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
1152                            let relative = offset.checked_sub(*base_offset).ok_or_else(|| {
1153                                GitError::InvalidFormat(
1154                                    "ofs-delta base offset is after delta".into(),
1155                                )
1156                            })?;
1157                            write_ofs_delta_offset(&mut entry_header, relative)?;
1158                        } else {
1159                            write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
1160                            entry_header.extend_from_slice(base_oid.as_bytes());
1161                        }
1162                    }
1163                    StreamingPlannedBase::External { base_oid, delta } => {
1164                        stream.delta_count += 1;
1165                        write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
1166                        entry_header.extend_from_slice(base_oid.as_bytes());
1167                    }
1168                }
1169
1170                let mut crc32 = crc32fast::Hasher::new();
1171                crc32.update(&entry_header);
1172                crc32.update(compressed_payload);
1173                output.write_pack_bytes(&entry_header)?;
1174                output.write_pack_bytes(compressed_payload)?;
1175                written_offsets[idx] = Some(offset);
1176                stream.index_entries.push(PackIndexEntry {
1177                    oid: object_ids[idx],
1178                    crc32: crc32.finalize(),
1179                    offset,
1180                });
1181
1182                retain_streaming_delta_base(
1183                    &mut base_horizon,
1184                    &mut horizon_bytes,
1185                    StreamingDeltaBase {
1186                        oid: object_ids[idx],
1187                        object: Arc::clone(&objects[idx]),
1188                        offset,
1189                        depth: plan[idx].depth,
1190                    },
1191                    options,
1192                    limits,
1193                );
1194                stream.note_peak(window_bytes, horizon_bytes);
1195            }
1196        }
1197
1198        stream.finish(output)
1199    }
1200}
1201
1202pub(crate) struct PackDigestWriter<'a, W> {
1203    writer: &'a mut W,
1204    digest: StreamingDigest,
1205    position: u64,
1206}
1207
1208impl<'a, W> PackDigestWriter<'a, W>
1209where
1210    W: Write,
1211{
1212    pub(crate) fn new(writer: &'a mut W, format: ObjectFormat) -> Self {
1213        Self {
1214            writer,
1215            digest: StreamingDigest::new(format),
1216            position: 0,
1217        }
1218    }
1219
1220    pub(crate) fn position(&self) -> u64 {
1221        self.position
1222    }
1223
1224    pub(crate) fn write_pack_bytes(&mut self, bytes: &[u8]) -> Result<()> {
1225        self.writer.write_all(bytes)?;
1226        self.digest.update(bytes);
1227        self.position = self
1228            .position
1229            .checked_add(bytes.len() as u64)
1230            .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
1231        Ok(())
1232    }
1233
1234    pub(crate) fn finish(mut self) -> Result<(ObjectId, u64)> {
1235        let checksum = self.digest.finalize()?;
1236        self.writer.write_all(checksum.as_bytes())?;
1237        self.position = self
1238            .position
1239            .checked_add(checksum.as_bytes().len() as u64)
1240            .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
1241        Ok((checksum, self.position))
1242    }
1243}
1244pub(crate) fn compress_planned_payloads(
1245    objects: &[&EncodedObject],
1246    plan: &[PlannedEntry],
1247    order: &[usize],
1248    compression_level: u32,
1249) -> Result<Vec<Vec<u8>>> {
1250    if order.is_empty() {
1251        return Ok(Vec::new());
1252    }
1253
1254    let worker_count = std::thread::available_parallelism()
1255        .map(|threads| threads.get())
1256        .unwrap_or(1)
1257        .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
1258        .min(order.len());
1259    if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
1260        let mut payloads = Vec::with_capacity(order.len());
1261        for &idx in order {
1262            payloads.push(compressed_payload(
1263                planned_payload(objects, plan, idx),
1264                compression_level,
1265            )?);
1266        }
1267        return Ok(payloads);
1268    }
1269
1270    let chunk_len = order.len().div_ceil(worker_count);
1271    let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
1272    std::thread::scope(|scope| {
1273        let mut handles = Vec::new();
1274        for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
1275            let chunk_start = chunk_idx * chunk_len;
1276            handles.push(scope.spawn(sley_core::diagnostics::inherit(
1277                move || -> Result<Vec<(usize, Vec<u8>)>> {
1278                    let mut chunk_payloads = Vec::with_capacity(chunk.len());
1279                    for (offset, &idx) in chunk.iter().enumerate() {
1280                        chunk_payloads.push((
1281                            chunk_start + offset,
1282                            compressed_payload(
1283                                planned_payload(objects, plan, idx),
1284                                compression_level,
1285                            )?,
1286                        ));
1287                    }
1288                    Ok(chunk_payloads)
1289                },
1290            )));
1291        }
1292
1293        let mut first_error = None;
1294        for handle in handles {
1295            match handle.join() {
1296                Ok(Ok(chunk_payloads)) => {
1297                    if first_error.is_none() {
1298                        for (pos, payload) in chunk_payloads {
1299                            payloads[pos] = payload;
1300                        }
1301                    }
1302                }
1303                Ok(Err(err)) => {
1304                    first_error.get_or_insert(err);
1305                }
1306                Err(_) => {
1307                    first_error.get_or_insert_with(|| {
1308                        GitError::InvalidObject("pack compression worker panicked".into())
1309                    });
1310                }
1311            }
1312        }
1313
1314        match first_error {
1315            Some(err) => Err(err),
1316            None => Ok(()),
1317        }
1318    })?;
1319    Ok(payloads)
1320}
1321
1322pub(crate) fn compress_streaming_planned_payloads(
1323    objects: &[Arc<EncodedObject>],
1324    plan: &[StreamingPlannedEntry],
1325    order: &[usize],
1326    compression_level: u32,
1327) -> Result<Vec<Vec<u8>>> {
1328    if order.is_empty() {
1329        return Ok(Vec::new());
1330    }
1331
1332    let worker_count = std::thread::available_parallelism()
1333        .map(|threads| threads.get())
1334        .unwrap_or(1)
1335        .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
1336        .min(order.len());
1337    if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
1338        let mut payloads = Vec::with_capacity(order.len());
1339        for &idx in order {
1340            payloads.push(compressed_payload(
1341                streaming_planned_payload(objects, plan, idx),
1342                compression_level,
1343            )?);
1344        }
1345        return Ok(payloads);
1346    }
1347
1348    let chunk_len = order.len().div_ceil(worker_count);
1349    let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
1350    std::thread::scope(|scope| {
1351        let mut handles = Vec::new();
1352        for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
1353            let chunk_start = chunk_idx * chunk_len;
1354            handles.push(scope.spawn(sley_core::diagnostics::inherit(
1355                move || -> Result<Vec<(usize, Vec<u8>)>> {
1356                    let mut chunk_payloads = Vec::with_capacity(chunk.len());
1357                    for (offset, &idx) in chunk.iter().enumerate() {
1358                        chunk_payloads.push((
1359                            chunk_start + offset,
1360                            compressed_payload(
1361                                streaming_planned_payload(objects, plan, idx),
1362                                compression_level,
1363                            )?,
1364                        ));
1365                    }
1366                    Ok(chunk_payloads)
1367                },
1368            )));
1369        }
1370
1371        let mut first_error = None;
1372        for handle in handles {
1373            match handle.join() {
1374                Ok(Ok(chunk_payloads)) => {
1375                    if first_error.is_none() {
1376                        for (pos, payload) in chunk_payloads {
1377                            payloads[pos] = payload;
1378                        }
1379                    }
1380                }
1381                Ok(Err(err)) => {
1382                    first_error.get_or_insert(err);
1383                }
1384                Err(_) => {
1385                    first_error.get_or_insert_with(|| {
1386                        GitError::InvalidObject("pack compression worker panicked".into())
1387                    });
1388                }
1389            }
1390        }
1391
1392        match first_error {
1393            Some(err) => Err(err),
1394            None => Ok(()),
1395        }
1396    })?;
1397    Ok(payloads)
1398}
1399
1400pub(crate) fn compress_undeltified_payloads(
1401    objects: &[Arc<EncodedObject>],
1402    compression_level: u32,
1403) -> Result<Vec<Vec<u8>>> {
1404    if objects.is_empty() {
1405        return Ok(Vec::new());
1406    }
1407
1408    let worker_count = std::thread::available_parallelism()
1409        .map(|threads| threads.get())
1410        .unwrap_or(1)
1411        .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
1412        .min(objects.len());
1413    if worker_count <= 1 || objects.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
1414        let mut payloads = Vec::with_capacity(objects.len());
1415        for object in objects {
1416            payloads.push(compressed_payload(&object.body, compression_level)?);
1417        }
1418        return Ok(payloads);
1419    }
1420
1421    let chunk_len = objects.len().div_ceil(worker_count);
1422    let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new)
1423        .take(objects.len())
1424        .collect();
1425    std::thread::scope(|scope| {
1426        let mut handles = Vec::new();
1427        for (chunk_idx, chunk) in objects.chunks(chunk_len).enumerate() {
1428            let chunk_start = chunk_idx * chunk_len;
1429            handles.push(scope.spawn(sley_core::diagnostics::inherit(
1430                move || -> Result<Vec<(usize, Vec<u8>)>> {
1431                    let mut chunk_payloads = Vec::with_capacity(chunk.len());
1432                    for (offset, object) in chunk.iter().enumerate() {
1433                        chunk_payloads.push((
1434                            chunk_start + offset,
1435                            compressed_payload(&object.body, compression_level)?,
1436                        ));
1437                    }
1438                    Ok(chunk_payloads)
1439                },
1440            )));
1441        }
1442
1443        let mut first_error = None;
1444        for handle in handles {
1445            match handle.join() {
1446                Ok(Ok(chunk_payloads)) => {
1447                    if first_error.is_none() {
1448                        for (pos, payload) in chunk_payloads {
1449                            payloads[pos] = payload;
1450                        }
1451                    }
1452                }
1453                Ok(Err(err)) => {
1454                    first_error.get_or_insert(err);
1455                }
1456                Err(_) => {
1457                    first_error.get_or_insert_with(|| {
1458                        GitError::InvalidObject("pack compression worker panicked".into())
1459                    });
1460                }
1461            }
1462        }
1463
1464        match first_error {
1465            Some(err) => Err(err),
1466            None => Ok(()),
1467        }
1468    })?;
1469    Ok(payloads)
1470}
1471
1472pub(crate) fn streaming_planned_payload<'a>(
1473    objects: &'a [Arc<EncodedObject>],
1474    plan: &'a [StreamingPlannedEntry],
1475    idx: usize,
1476) -> &'a [u8] {
1477    match &plan[idx].base {
1478        StreamingPlannedBase::None => &objects[idx].body,
1479        StreamingPlannedBase::Current { delta, .. }
1480        | StreamingPlannedBase::Previous { delta, .. }
1481        | StreamingPlannedBase::External { delta, .. } => delta,
1482    }
1483}
1484
1485pub(crate) fn planned_payload<'a>(
1486    objects: &'a [&'a EncodedObject],
1487    plan: &'a [PlannedEntry],
1488    idx: usize,
1489) -> &'a [u8] {
1490    match &plan[idx].base {
1491        PlannedBase::None => &objects[idx].body,
1492        PlannedBase::InPack { delta, .. } | PlannedBase::External { delta, .. } => delta,
1493    }
1494}
1495
1496pub(crate) fn compressed_payload(body: &[u8], compression_level: u32) -> Result<Vec<u8>> {
1497    let mut out = Vec::new();
1498    write_compressed_payload(&mut out, body, compression_level)?;
1499    Ok(out)
1500}
1501pub(crate) fn write_compressed_payload(
1502    out: &mut Vec<u8>,
1503    body: &[u8],
1504    compression_level: u32,
1505) -> Result<()> {
1506    let mut compressor = Compress::new(Compression::new(compression_level.min(9)), true);
1507    out.reserve(zlib_compress_bound(body.len()));
1508    let status = compressor
1509        .compress_vec(body, out, FlushCompress::Finish)
1510        .map_err(|err| GitError::InvalidObject(format!("zlib compression failed: {err}")))?;
1511    if status != Status::StreamEnd || compressor.total_in() != body.len() as u64 {
1512        return Err(GitError::InvalidObject(
1513            "zlib compression did not finish pack entry".into(),
1514        ));
1515    }
1516    Ok(())
1517}
1518
1519pub(crate) fn zlib_compress_bound(len: usize) -> usize {
1520    len.saturating_add(len >> 12)
1521        .saturating_add(len >> 14)
1522        .saturating_add(len >> 25)
1523        .saturating_add(13)
1524}
1525
1526pub(crate) fn write_entry_header(out: &mut Vec<u8>, object_type: ObjectType, size: u64) {
1527    let type_code = match object_type {
1528        ObjectType::Commit => 1,
1529        ObjectType::Tree => 2,
1530        ObjectType::Blob => 3,
1531        ObjectType::Tag => 4,
1532    };
1533    write_pack_entry_header_kind(out, type_code, size);
1534}
1535
1536pub(crate) fn write_pack_entry_header_kind(out: &mut Vec<u8>, type_code: u8, mut size: u64) {
1537    let mut byte = (type_code << 4) | ((size as u8) & 0x0f);
1538    size >>= 4;
1539    if size != 0 {
1540        byte |= 0x80;
1541    }
1542    out.push(byte);
1543    while size != 0 {
1544        let mut byte = (size as u8) & 0x7f;
1545        size >>= 7;
1546        if size != 0 {
1547            byte |= 0x80;
1548        }
1549        out.push(byte);
1550    }
1551}
1552
1553pub(crate) fn write_ofs_delta_offset(out: &mut Vec<u8>, relative: u64) -> Result<()> {
1554    if relative == 0 {
1555        return Err(GitError::InvalidFormat(
1556            "ofs-delta relative offset cannot be zero".into(),
1557        ));
1558    }
1559    let mut value = relative;
1560    let mut bytes = vec![(value & 0x7f) as u8];
1561    value >>= 7;
1562    while value != 0 {
1563        value -= 1;
1564        bytes.push(((value & 0x7f) as u8) | 0x80);
1565        value >>= 7;
1566    }
1567    bytes.reverse();
1568    out.extend_from_slice(&bytes);
1569    Ok(())
1570}
1571/// Builder that assembles a reachability bitmap (`.bitmap`) for a pack.
1572///
1573/// The writer is constructed from the object layout of a pack (one
1574/// [`ObjectType`] per object, in pack order) and the pack's trailing checksum.
1575/// Callers then register one selected commit per [`add_commit`] call, supplying
1576/// the set of pack positions reachable from that commit. [`build`]/[`write`]
1577/// produce a [`PackBitmapIndex`] / serialised `.bitmap` bytes matching git's
1578/// on-disk format (signature `BITM`, version 1).
1579///
1580/// [`add_commit`]: PackBitmapWriter::add_commit
1581/// [`build`]: PackBitmapWriter::build
1582/// [`write`]: PackBitmapWriter::write
1583#[derive(Debug, Clone)]
1584pub struct PackBitmapWriter {
1585    format: ObjectFormat,
1586    pack_checksum: ObjectId,
1587    object_count: u32,
1588    commit_positions: Vec<u32>,
1589    tree_positions: Vec<u32>,
1590    blob_positions: Vec<u32>,
1591    tag_positions: Vec<u32>,
1592    name_hash_cache: Option<Vec<u32>>,
1593    write_lookup_table: bool,
1594    selected: Vec<SelectedCommit>,
1595    pseudo_merges: Vec<PackBitmapPseudoMerge>,
1596}
1597
1598#[derive(Debug, Clone)]
1599pub(crate) struct SelectedCommit {
1600    /// Oid-sorted `.idx` position (what the on-disk entry records). The
1601    /// commit's pack-order position lives in `reachable` with the rest of the
1602    /// bits.
1603    commit_index_position: u32,
1604    flags: u8,
1605    reachable: Vec<u32>,
1606}
1607
1608impl PackBitmapWriter {
1609    /// `OBJ_NONE` selection flag: this commit's bitmap is stored in full (no XOR
1610    /// compression against a previously selected commit). This is the only flag
1611    /// value this writer emits.
1612    pub const FLAG_NONE: u8 = 0;
1613
1614    /// Creates a writer for a pack whose objects (in pack order) have the given
1615    /// [`ObjectType`]s and whose trailing checksum is `pack_checksum`.
1616    ///
1617    /// Returns an error if the pack contains more than `u32::MAX` objects, if
1618    /// `pack_checksum`'s format does not match `format`, or if any object type
1619    /// is not one of the four reachable git object kinds.
1620    pub fn new(
1621        format: ObjectFormat,
1622        pack_checksum: ObjectId,
1623        object_types: &[ObjectType],
1624    ) -> Result<Self> {
1625        if object_types.len() > u32::MAX as usize {
1626            return Err(GitError::InvalidFormat(
1627                "too many objects for a pack bitmap".into(),
1628            ));
1629        }
1630        if pack_checksum.format() != format {
1631            return Err(GitError::InvalidObjectId(
1632                "pack checksum format does not match bitmap format".into(),
1633            ));
1634        }
1635        let object_count = object_types.len() as u32;
1636        let mut commit_positions = Vec::new();
1637        let mut tree_positions = Vec::new();
1638        let mut blob_positions = Vec::new();
1639        let mut tag_positions = Vec::new();
1640        for (index, object_type) in object_types.iter().enumerate() {
1641            let position = index as u32;
1642            match object_type {
1643                ObjectType::Commit => commit_positions.push(position),
1644                ObjectType::Tree => tree_positions.push(position),
1645                ObjectType::Blob => blob_positions.push(position),
1646                ObjectType::Tag => tag_positions.push(position),
1647            }
1648        }
1649        Ok(Self {
1650            format,
1651            pack_checksum,
1652            object_count,
1653            commit_positions,
1654            tree_positions,
1655            blob_positions,
1656            tag_positions,
1657            name_hash_cache: None,
1658            write_lookup_table: false,
1659            selected: Vec::new(),
1660            pseudo_merges: Vec::new(),
1661        })
1662    }
1663
1664    /// Attaches a name-hash cache (one `u32` per object, in pack order). When
1665    /// set, the written bitmap advertises [`PackBitmapIndex::OPTION_HASH_CACHE`]
1666    /// and appends the cache after the bitmap entries, exactly as git does.
1667    ///
1668    /// Returns an error if the cache length does not equal the object count.
1669    pub fn with_name_hash_cache(mut self, cache: Vec<u32>) -> Result<Self> {
1670        if cache.len() != self.object_count as usize {
1671            return Err(GitError::InvalidFormat(format!(
1672                "name hash cache has {} entries but pack has {} objects",
1673                cache.len(),
1674                self.object_count
1675            )));
1676        }
1677        self.name_hash_cache = Some(cache);
1678        Ok(self)
1679    }
1680
1681    /// Enable the commit lookup-table extension. Each row is derived from the
1682    /// selected commit entries when the bitmap is serialised.
1683    pub fn with_lookup_table(mut self, enabled: bool) -> Self {
1684        self.write_lookup_table = enabled;
1685        self
1686    }
1687
1688    /// Registers a selected commit and the pack positions reachable from it.
1689    ///
1690    /// `commit_position` is the *pack-order* position of the commit itself (the
1691    /// bit-number space); it must reference a commit object and is implicitly
1692    /// part of the reachable set. `commit_index_position` is the commit's
1693    /// position in the *oid-sorted* pack index — this is what the on-disk entry
1694    /// records (upstream `oid_pos`); bits and entry positions live in different
1695    /// spaces. `reachable` lists the pack-order positions of every object
1696    /// reachable from the commit (it may include or omit `commit_position`;
1697    /// duplicates are fine). All positions must be in range. The commit's full
1698    /// (non-XORed) bitmap is stored.
1699    pub fn add_commit(
1700        &mut self,
1701        commit_position: u32,
1702        commit_index_position: u32,
1703        reachable: &[u32],
1704    ) -> Result<()> {
1705        if commit_position >= self.object_count {
1706            return Err(GitError::InvalidFormat(format!(
1707                "commit position {commit_position} out of range for {} objects",
1708                self.object_count
1709            )));
1710        }
1711        if commit_index_position >= self.object_count {
1712            return Err(GitError::InvalidFormat(format!(
1713                "commit index position {commit_index_position} out of range for {} objects",
1714                self.object_count
1715            )));
1716        }
1717        if !self.commit_positions.contains(&commit_position) {
1718            return Err(GitError::InvalidFormat(format!(
1719                "bitmap commit position {commit_position} is not a commit object"
1720            )));
1721        }
1722        for &position in reachable {
1723            if position >= self.object_count {
1724                return Err(GitError::InvalidFormat(format!(
1725                    "reachable position {position} out of range for {} objects",
1726                    self.object_count
1727                )));
1728            }
1729        }
1730        let mut reachable = reachable.to_vec();
1731        reachable.push(commit_position);
1732        self.selected.push(SelectedCommit {
1733            commit_index_position,
1734            flags: Self::FLAG_NONE,
1735            reachable,
1736        });
1737        Ok(())
1738    }
1739
1740    /// Registers a pseudo-merge bitmap. Both `commits` and `reachable` are
1741    /// positions in the bitmap's bit-numbering order (pack order for a single
1742    /// pack, pseudo-pack order for a MIDX). Every commit position must refer to
1743    /// a commit object; every reachable position must be in range.
1744    pub fn add_pseudo_merge(&mut self, commits: &[u32], reachable: &[u32]) -> Result<()> {
1745        if commits.is_empty() {
1746            return Err(GitError::InvalidFormat(
1747                "pseudo-merge must contain at least one commit".into(),
1748            ));
1749        }
1750        for &position in commits {
1751            if position >= self.object_count {
1752                return Err(GitError::InvalidFormat(format!(
1753                    "pseudo-merge commit position {position} out of range for {} objects",
1754                    self.object_count
1755                )));
1756            }
1757            if !self.commit_positions.contains(&position) {
1758                return Err(GitError::InvalidFormat(format!(
1759                    "pseudo-merge commit position {position} is not a commit object"
1760                )));
1761            }
1762        }
1763        for &position in reachable {
1764            if position >= self.object_count {
1765                return Err(GitError::InvalidFormat(format!(
1766                    "pseudo-merge reachable position {position} out of range for {} objects",
1767                    self.object_count
1768                )));
1769            }
1770        }
1771        self.pseudo_merges.push(PackBitmapPseudoMerge {
1772            commits: EwahBitmap::from_positions(self.object_count, commits)?,
1773            bitmap: EwahBitmap::from_positions(self.object_count, reachable)?,
1774        });
1775        Ok(())
1776    }
1777
1778    /// Builds the in-memory [`PackBitmapIndex`] without serialising it.
1779    ///
1780    /// The resulting index always advertises
1781    /// [`PackBitmapIndex::OPTION_FULL_DAG`] (the four type bitmaps fully cover
1782    /// the pack) and, when a name-hash cache was attached,
1783    /// [`PackBitmapIndex::OPTION_HASH_CACHE`].
1784    pub fn build(&self) -> Result<PackBitmapIndex> {
1785        let commits = EwahBitmap::from_positions(self.object_count, &self.commit_positions)?;
1786        let trees = EwahBitmap::from_positions(self.object_count, &self.tree_positions)?;
1787        let blobs = EwahBitmap::from_positions(self.object_count, &self.blob_positions)?;
1788        let tags = EwahBitmap::from_positions(self.object_count, &self.tag_positions)?;
1789
1790        let mut entries = Vec::with_capacity(self.selected.len());
1791        for selected in &self.selected {
1792            let bitmap = EwahBitmap::from_positions(self.object_count, &selected.reachable)?;
1793            entries.push(PackBitmapEntry {
1794                object_position: selected.commit_index_position,
1795                xor_offset: 0,
1796                flags: selected.flags,
1797                bitmap,
1798            });
1799        }
1800
1801        let mut options = PackBitmapIndex::OPTION_FULL_DAG;
1802        if self.name_hash_cache.is_some() {
1803            options |= PackBitmapIndex::OPTION_HASH_CACHE;
1804        }
1805        if !self.pseudo_merges.is_empty() {
1806            options |= PackBitmapIndex::OPTION_PSEUDO_MERGES;
1807        }
1808        if self.write_lookup_table {
1809            options |= PackBitmapIndex::OPTION_LOOKUP_TABLE;
1810        }
1811
1812        // The index checksum is only known once the body is serialised; the
1813        // dedicated `write` path fills it in. `build` reports a placeholder of
1814        // the correct format so the struct is self-consistent for callers that
1815        // only need the decoded bitmaps.
1816        let placeholder_checksum = ObjectId::null(self.format);
1817        Ok(PackBitmapIndex {
1818            version: 1,
1819            format: self.format,
1820            options,
1821            pack_checksum: self.pack_checksum.clone(),
1822            index_checksum: placeholder_checksum,
1823            type_bitmaps: PackBitmapTypeBitmaps {
1824                commits,
1825                trees,
1826                blobs,
1827                tags,
1828            },
1829            entries,
1830            pseudo_merges: self.pseudo_merges.clone(),
1831            lookup_table: self.write_lookup_table,
1832            name_hash_cache: self.name_hash_cache.clone(),
1833        })
1834    }
1835
1836    /// Builds and serialises the `.bitmap` file, returning the on-disk bytes
1837    /// (including the trailing index checksum).
1838    pub fn write(&self) -> Result<Vec<u8>> {
1839        self.build()?.write()
1840    }
1841}
1842
1843impl PackBitmapIndex {
1844    /// Serialises this index into git's on-disk `.bitmap` byte layout.
1845    ///
1846    /// This is the exact inverse of [`PackBitmapIndex::parse`]: signature
1847    /// `BITM`, version (u16 BE), options (u16 BE), entry count (u32 BE), the
1848    /// pack checksum, the four type bitmaps (commits, trees, blobs, tags), each
1849    /// commit entry (object position, XOR offset, flags, EWAH bitmap), the
1850    /// optional pseudo-merge extension, the optional name-hash cache, and
1851    /// finally the trailing index checksum over everything written so far.
1852    ///
1853    /// The `index_checksum` field of `self` is ignored and recomputed from the
1854    /// serialised body. Returns an error for unsupported versions, mismatched
1855    /// object-id formats, an oversized entry table, or an inconsistent name-hash
1856    /// cache.
1857    pub fn write(&self) -> Result<Vec<u8>> {
1858        if self.version != 1 {
1859            return Err(GitError::Unsupported(format!(
1860                "bitmap index version {}",
1861                self.version
1862            )));
1863        }
1864        let mut options = self.options;
1865        if !self.pseudo_merges.is_empty() {
1866            options |= Self::OPTION_PSEUDO_MERGES;
1867        }
1868        if self.lookup_table {
1869            options |= Self::OPTION_LOOKUP_TABLE;
1870        }
1871        let known_options = Self::OPTION_FULL_DAG
1872            | Self::OPTION_HASH_CACHE
1873            | Self::OPTION_LOOKUP_TABLE
1874            | Self::OPTION_PSEUDO_MERGES;
1875        if options & !known_options != 0 {
1876            return Err(GitError::Unsupported(format!(
1877                "bitmap index options {:#06x}",
1878                options & !known_options
1879            )));
1880        }
1881        if self.pack_checksum.format() != self.format {
1882            return Err(GitError::InvalidObjectId(
1883                "bitmap pack checksum format does not match index format".into(),
1884            ));
1885        }
1886        if self.entries.len() > u32::MAX as usize {
1887            return Err(GitError::InvalidFormat(
1888                "too many bitmap index entries".into(),
1889            ));
1890        }
1891        if options & Self::OPTION_PSEUDO_MERGES != 0 && self.pseudo_merges.is_empty() {
1892            return Err(GitError::InvalidFormat(
1893                "OPTION_PSEUDO_MERGES set without pseudo-merge records".into(),
1894            ));
1895        }
1896        let want_cache = options & Self::OPTION_HASH_CACHE != 0;
1897        match (&self.name_hash_cache, want_cache) {
1898            (Some(_), false) => {
1899                return Err(GitError::InvalidFormat(
1900                    "name hash cache present without OPTION_HASH_CACHE".into(),
1901                ));
1902            }
1903            (None, true) => {
1904                return Err(GitError::InvalidFormat(
1905                    "OPTION_HASH_CACHE set without a name hash cache".into(),
1906                ));
1907            }
1908            _ => {}
1909        }
1910
1911        let mut out = Vec::new();
1912        out.extend_from_slice(b"BITM");
1913        out.extend_from_slice(&self.version.to_be_bytes());
1914        out.extend_from_slice(&options.to_be_bytes());
1915        out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
1916        out.extend_from_slice(self.pack_checksum.as_bytes());
1917
1918        self.type_bitmaps.commits.append_bytes(&mut out);
1919        self.type_bitmaps.trees.append_bytes(&mut out);
1920        self.type_bitmaps.blobs.append_bytes(&mut out);
1921        self.type_bitmaps.tags.append_bytes(&mut out);
1922
1923        let mut entry_offsets = Vec::with_capacity(self.entries.len());
1924        for (idx, entry) in self.entries.iter().enumerate() {
1925            if entry.xor_offset as usize > idx {
1926                return Err(GitError::InvalidFormat(
1927                    "bitmap index entry has invalid XOR offset".into(),
1928                ));
1929            }
1930            entry_offsets.push(out.len() as u64);
1931            out.extend_from_slice(&entry.object_position.to_be_bytes());
1932            out.push(entry.xor_offset);
1933            out.push(entry.flags);
1934            entry.bitmap.append_bytes(&mut out);
1935        }
1936
1937        if !self.pseudo_merges.is_empty() {
1938            append_bitmap_pseudo_merges(&mut out, &self.pseudo_merges)?;
1939        }
1940
1941        if self.lookup_table {
1942            append_bitmap_lookup_table(&mut out, &self.entries, &entry_offsets)?;
1943        }
1944
1945        if let Some(cache) = &self.name_hash_cache {
1946            for value in cache {
1947                out.extend_from_slice(&value.to_be_bytes());
1948            }
1949        }
1950
1951        let checksum = sley_core::digest_bytes(self.format, &out)?;
1952        out.extend_from_slice(checksum.as_bytes());
1953        Ok(out)
1954    }
1955}
1956
1957fn append_bitmap_lookup_table(
1958    out: &mut Vec<u8>,
1959    entries: &[PackBitmapEntry],
1960    entry_offsets: &[u64],
1961) -> Result<()> {
1962    if entries.len() != entry_offsets.len() {
1963        return Err(GitError::InvalidFormat(
1964            "bitmap lookup table offset count mismatch".into(),
1965        ));
1966    }
1967    let mut table: Vec<usize> = (0..entries.len()).collect();
1968    table.sort_by_key(|&index| entries[index].object_position);
1969    let mut inverse = vec![0u32; entries.len()];
1970    for (row, &entry_index) in table.iter().enumerate() {
1971        inverse[entry_index] = row as u32;
1972    }
1973    for &entry_index in &table {
1974        let entry = &entries[entry_index];
1975        let xor_row = if entry.xor_offset == 0 {
1976            u32::MAX
1977        } else {
1978            let base = entry_index
1979                .checked_sub(entry.xor_offset as usize)
1980                .ok_or_else(|| {
1981                    GitError::InvalidFormat("bitmap lookup table XOR base underflow".into())
1982                })?;
1983            inverse[base]
1984        };
1985        out.extend_from_slice(&entry.object_position.to_be_bytes());
1986        out.extend_from_slice(&entry_offsets[entry_index].to_be_bytes());
1987        out.extend_from_slice(&xor_row.to_be_bytes());
1988    }
1989    Ok(())
1990}
1991
1992pub(crate) fn append_bitmap_pseudo_merges(
1993    out: &mut Vec<u8>,
1994    pseudo_merges: &[PackBitmapPseudoMerge],
1995) -> Result<()> {
1996    if pseudo_merges.len() > u32::MAX as usize {
1997        return Err(GitError::InvalidFormat(
1998            "too many pseudo-merge bitmap records".into(),
1999        ));
2000    }
2001    let start = out.len();
2002    let mut pseudo_offsets = Vec::with_capacity(pseudo_merges.len());
2003    let mut commit_to_offsets: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
2004    for merge in pseudo_merges {
2005        let offset = u64::try_from(out.len())
2006            .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
2007        pseudo_offsets.push(offset);
2008        for commit_pos in merge.commits.to_positions()? {
2009            commit_to_offsets
2010                .entry(commit_pos)
2011                .or_default()
2012                .push(offset);
2013        }
2014        merge.commits.append_bytes(out);
2015        merge.bitmap.append_bytes(out);
2016    }
2017    if commit_to_offsets.len() > u32::MAX as usize {
2018        return Err(GitError::InvalidFormat(
2019            "too many pseudo-merge commits".into(),
2020        ));
2021    }
2022
2023    let lookup_start = out.len();
2024    let lookup_len = commit_to_offsets
2025        .len()
2026        .checked_mul(12)
2027        .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?;
2028    let mut next_extended = u64::try_from(
2029        lookup_start
2030            .checked_add(lookup_len)
2031            .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?,
2032    )
2033    .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
2034    let mut rows = Vec::with_capacity(commit_to_offsets.len());
2035    for (commit_pos, offsets) in commit_to_offsets {
2036        let extended_offset = if offsets.len() > 1 {
2037            if next_extended & (1u64 << 63) != 0 {
2038                return Err(GitError::InvalidFormat(
2039                    "pseudo-merge extended offset overflow".into(),
2040                ));
2041            }
2042            let offset = next_extended;
2043            let ext_len = offsets
2044                .len()
2045                .checked_mul(8)
2046                .and_then(|len| len.checked_add(4))
2047                .ok_or_else(|| {
2048                    GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
2049                })?;
2050            next_extended = next_extended.checked_add(ext_len as u64).ok_or_else(|| {
2051                GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
2052            })?;
2053            Some(offset)
2054        } else {
2055            None
2056        };
2057        rows.push((commit_pos, offsets, extended_offset));
2058    }
2059
2060    for (commit_pos, offsets, extended_offset) in &rows {
2061        out.extend_from_slice(&commit_pos.to_be_bytes());
2062        match extended_offset {
2063            Some(offset) => out.extend_from_slice(&(offset | (1u64 << 63)).to_be_bytes()),
2064            None => out.extend_from_slice(&offsets[0].to_be_bytes()),
2065        }
2066    }
2067
2068    for (_commit_pos, offsets, extended_offset) in &rows {
2069        if extended_offset.is_none() {
2070            continue;
2071        }
2072        let count = u32::try_from(offsets.len())
2073            .map_err(|_| GitError::InvalidFormat("pseudo-merge extended lookup overflow".into()))?;
2074        out.extend_from_slice(&count.to_be_bytes());
2075        for offset in offsets {
2076            out.extend_from_slice(&offset.to_be_bytes());
2077        }
2078    }
2079
2080    for offset in &pseudo_offsets {
2081        out.extend_from_slice(&offset.to_be_bytes());
2082    }
2083    out.extend_from_slice(&(pseudo_merges.len() as u32).to_be_bytes());
2084    out.extend_from_slice(&(rows.len() as u32).to_be_bytes());
2085    let lookup_relative = lookup_start
2086        .checked_sub(start)
2087        .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup underflow".into()))?;
2088    out.extend_from_slice(&(lookup_relative as u64).to_be_bytes());
2089    let extension_size = out
2090        .len()
2091        .checked_sub(start)
2092        .and_then(|len| len.checked_add(8))
2093        .ok_or_else(|| GitError::InvalidFormat("pseudo-merge extension overflow".into()))?;
2094    out.extend_from_slice(&(extension_size as u64).to_be_bytes());
2095    Ok(())
2096}
2097
2098/// Convenience wrapper that builds a `.bitmap` file in one call.
2099///
2100/// `object_types` lists the [`ObjectType`] of every pack object in pack order,
2101/// `pack_checksum` is the pack's trailing checksum, and `commits` carries, per
2102/// selected commit, `(pack_position, index_position, reachable_pack_positions)`
2103/// (see [`PackBitmapWriter::add_commit`] for the two position spaces). An
2104/// optional `name_hash_cache` (one entry per object) may be supplied to emit
2105/// the hash-cache extension.
2106pub fn write_bitmap(
2107    format: ObjectFormat,
2108    pack_checksum: ObjectId,
2109    object_types: &[ObjectType],
2110    commits: &[(u32, u32, Vec<u32>)],
2111    name_hash_cache: Option<Vec<u32>>,
2112) -> Result<Vec<u8>> {
2113    let mut writer = PackBitmapWriter::new(format, pack_checksum, object_types)?;
2114    if let Some(cache) = name_hash_cache {
2115        writer = writer.with_name_hash_cache(cache)?;
2116    }
2117    for (commit_position, commit_index_position, reachable) in commits {
2118        writer.add_commit(*commit_position, *commit_index_position, reachable)?;
2119    }
2120    writer.write()
2121}