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/// Streaming pack writes pre-compress only this many ordered entries at a time.
32/// This restores CPU parallelism without holding every compressed payload for a
33/// large pack in memory at once.
34pub(crate) const PACK_STREAM_COMPRESSION_WINDOW_OBJECTS: usize = 256;
35
36/// Options controlling sliding-window delta selection during pack generation.
37///
38/// Construct with [`PackWriteOptions::new`] (sensible defaults) and adjust with
39/// the builder-style setters, or build one directly. Used by
40/// [`PackFile::write_packed_with_options`] and [`PackFile::write_thin`].
41#[derive(Debug, Clone)]
42pub struct PackWriteOptions {
43    /// Number of previous same-type candidates each object is deltified
44    /// against. Larger windows find better deltas at higher cost.
45    pub window: usize,
46    /// Maximum delta chain depth. A value of `0` disables deltification.
47    pub depth: usize,
48    /// When `true`, in-pack deltas are encoded as ofs-deltas (the default and
49    /// git's preference). When `false`, in-pack deltas use ref-deltas. Deltas
50    /// against external thin-pack bases always use ref-deltas regardless.
51    pub prefer_ofs_delta: bool,
52    /// External base objects, keyed by object id, that are *not* written into
53    /// the pack but may be used as delta bases. Supplying any entries here
54    /// produces a thin pack (see [`PackFile::write_thin`]). Empty by default,
55    /// yielding a self-contained pack.
56    pub thin_bases: HashMap<ObjectId, EncodedObject>,
57    /// When `true` (the default), objects are reordered by type and size for
58    /// better delta locality. When `false`, the input order is preserved (the
59    /// emitted pack lists objects in the order supplied); deltas then only
60    /// reference earlier input objects. Reordering is always skipped when
61    /// deltification is disabled (`depth == 0`), since it has no effect there.
62    pub reorder: bool,
63    /// Zlib compression level for pack entry payloads.
64    pub compression_level: u32,
65}
66
67impl Default for PackWriteOptions {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl PackWriteOptions {
74    /// Options with git-compatible defaults: window
75    /// [`DEFAULT_PACK_WINDOW`], depth [`DEFAULT_PACK_DEPTH`], ofs-deltas, and
76    /// no external thin bases.
77    pub fn new() -> Self {
78        Self {
79            window: DEFAULT_PACK_WINDOW,
80            depth: DEFAULT_PACK_DEPTH,
81            prefer_ofs_delta: true,
82            thin_bases: HashMap::new(),
83            reorder: true,
84            compression_level: 6,
85        }
86    }
87
88    /// Set the sliding-window size.
89    pub fn with_window(mut self, window: usize) -> Self {
90        self.window = window;
91        self
92    }
93
94    /// Set the maximum delta chain depth (`0` disables deltas).
95    pub fn with_depth(mut self, depth: usize) -> Self {
96        self.depth = depth;
97        self
98    }
99
100    /// Choose whether in-pack deltas use ofs-delta (`true`) or ref-delta
101    /// (`false`) base references.
102    pub fn with_prefer_ofs_delta(mut self, prefer_ofs_delta: bool) -> Self {
103        self.prefer_ofs_delta = prefer_ofs_delta;
104        self
105    }
106
107    /// Provide the set of external base objects permitted for a thin pack.
108    pub fn with_thin_bases(mut self, thin_bases: HashMap<ObjectId, EncodedObject>) -> Self {
109        self.thin_bases = thin_bases;
110        self
111    }
112
113    /// Choose whether objects may be reordered for delta locality (`true`) or
114    /// emitted in input order (`false`).
115    pub fn with_reorder(mut self, reorder: bool) -> Self {
116        self.reorder = reorder;
117        self
118    }
119
120    /// Set the zlib compression level used for pack entry payloads.
121    pub fn with_compression_level(mut self, level: u32) -> Self {
122        self.compression_level = level.min(9);
123        self
124    }
125}
126
127impl PackFile {
128    pub fn write_undeltified_sha1<T>(objects: &[T]) -> Result<PackWrite>
129    where
130        T: Borrow<EncodedObject>,
131    {
132        Self::write_undeltified(objects, ObjectFormat::Sha1)
133    }
134
135    /// Write a pack with every object stored undeltified (no delta entries).
136    ///
137    /// This is the simple, self-contained encoding; objects appear in the given
138    /// order. For smaller output that exploits similarity between objects, use
139    /// [`PackFile::write_packed`].
140    pub fn write_undeltified<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
141    where
142        T: Borrow<EncodedObject>,
143    {
144        let options = PackWriteOptions::new().with_depth(0).with_reorder(false);
145        Self::write_packed_impl(objects, format, &options)
146    }
147
148    /// Write a pack using sliding-window delta selection with git-compatible
149    /// defaults (window [`DEFAULT_PACK_WINDOW`], depth [`DEFAULT_PACK_DEPTH`],
150    /// ofs-deltas, self-contained).
151    ///
152    /// Objects are grouped by type and ordered for good deltas, then each is
153    /// compared against a window of previously emitted candidates; the smallest
154    /// acceptable delta is kept, otherwise the object is stored undeltified. The
155    /// result round-trips through [`PackFile::parse`].
156    pub fn write_packed<T>(objects: &[T], format: ObjectFormat) -> Result<PackWrite>
157    where
158        T: Borrow<EncodedObject>,
159    {
160        Self::write_packed_with_options(objects, format, &PackWriteOptions::new())
161    }
162
163    /// Like [`PackFile::write_packed`] but with caller-supplied
164    /// [`PackWriteOptions`] (window, depth, base-reference style, and optional
165    /// external thin bases).
166    pub fn write_packed_with_options<T>(
167        objects: &[T],
168        format: ObjectFormat,
169        options: &PackWriteOptions,
170    ) -> Result<PackWrite>
171    where
172        T: Borrow<EncodedObject>,
173    {
174        Self::write_packed_impl(objects, format, options)
175    }
176
177    /// Like [`PackFile::write_packed`], but uses caller-supplied object ids
178    /// instead of re-hashing each object before pack planning.
179    ///
180    /// This is intended for object-database paths that reached each object by
181    /// its id and already trust that id/object mapping. The function validates
182    /// id formats and duplicate ids, but it does not re-hash object bodies; use
183    /// [`PackFile::write_packed`] when the ids are not already known to be
184    /// canonical.
185    pub fn write_packed_with_known_ids(
186        inputs: &[PackInput<'_>],
187        format: ObjectFormat,
188    ) -> Result<PackWrite> {
189        Self::write_packed_with_known_ids_and_options(inputs, format, &PackWriteOptions::new())
190    }
191
192    /// Like [`PackFile::write_packed_with_known_ids`] but with caller-supplied
193    /// [`PackWriteOptions`].
194    pub fn write_packed_with_known_ids_and_options(
195        inputs: &[PackInput<'_>],
196        format: ObjectFormat,
197        options: &PackWriteOptions,
198    ) -> Result<PackWrite> {
199        if inputs.len() > u32::MAX as usize {
200            return Err(GitError::InvalidFormat("too many pack objects".into()));
201        }
202        let mut objects = Vec::with_capacity(inputs.len());
203        let mut object_ids = Vec::with_capacity(inputs.len());
204        for input in inputs {
205            if input.oid.format() != format {
206                return Err(GitError::InvalidObjectId(format!(
207                    "pack object id {} uses {}, pack uses {}",
208                    input.oid,
209                    input.oid.format().name(),
210                    format.name()
211                )));
212            }
213            objects.push(input.object);
214            object_ids.push(*input.oid);
215        }
216        Self::write_packed_from_parts(objects, object_ids, format, options)
217    }
218
219    pub fn write_packed_with_known_ids_to_writer<W>(
220        inputs: &[PackInput<'_>],
221        format: ObjectFormat,
222        options: &PackWriteOptions,
223        writer: &mut W,
224    ) -> Result<PackWriteSummary>
225    where
226        W: Write,
227    {
228        if inputs.len() > u32::MAX as usize {
229            return Err(GitError::InvalidFormat("too many pack objects".into()));
230        }
231        let mut objects = Vec::with_capacity(inputs.len());
232        let mut object_ids = Vec::with_capacity(inputs.len());
233        for input in inputs {
234            if input.oid.format() != format {
235                return Err(GitError::InvalidObjectId(format!(
236                    "pack object id {} uses {}, pack uses {}",
237                    input.oid,
238                    input.oid.format().name(),
239                    format.name()
240                )));
241            }
242            objects.push(input.object);
243            object_ids.push(*input.oid);
244        }
245        Self::write_packed_from_parts_to_writer(objects, object_ids, format, options, writer)
246    }
247
248    /// Write a thin pack: objects may be deltified against `external_bases`
249    /// that are *not* included in the pack, referenced by ref-delta to their
250    /// object id.
251    ///
252    /// The receiver must already have (or otherwise obtain) those base objects
253    /// and resolve the pack with [`PackFile::parse_thin`]. Window and depth use
254    /// the defaults; pass options via [`PackFile::write_packed_with_options`]
255    /// with [`PackWriteOptions::with_thin_bases`] for finer control.
256    pub fn write_thin<T>(
257        objects: &[T],
258        format: ObjectFormat,
259        external_bases: HashMap<ObjectId, EncodedObject>,
260    ) -> Result<PackWrite>
261    where
262        T: Borrow<EncodedObject>,
263    {
264        let options = PackWriteOptions::new().with_thin_bases(external_bases);
265        Self::write_packed_impl(objects, format, &options)
266    }
267
268    pub(crate) fn write_packed_impl<T>(
269        objects: &[T],
270        format: ObjectFormat,
271        options: &PackWriteOptions,
272    ) -> Result<PackWrite>
273    where
274        T: Borrow<EncodedObject>,
275    {
276        if objects.len() > u32::MAX as usize {
277            return Err(GitError::InvalidFormat("too many pack objects".into()));
278        }
279        let objects: Vec<&EncodedObject> = objects.iter().map(Borrow::borrow).collect();
280
281        // Compute object ids up front; they are needed both for the index and,
282        // for ref-deltas, inside the pack entries themselves.
283        let mut object_ids: Vec<ObjectId> = Vec::with_capacity(objects.len());
284        for object in &objects {
285            object_ids.push(object.object_id(format)?);
286        }
287        Self::write_packed_from_parts(objects, object_ids, format, options)
288    }
289
290    pub(crate) fn write_packed_from_parts(
291        objects: Vec<&EncodedObject>,
292        object_ids: Vec<ObjectId>,
293        format: ObjectFormat,
294        options: &PackWriteOptions,
295    ) -> Result<PackWrite> {
296        let mut seen = HashSet::with_capacity(object_ids.len());
297        for oid in &object_ids {
298            if !seen.insert(oid) {
299                return Err(GitError::InvalidFormat(format!(
300                    "pack contains duplicate object id {oid}"
301                )));
302            }
303        }
304
305        // Validate external thin bases share the pack's hash format.
306        for oid in options.thin_bases.keys() {
307            if oid.format() != format {
308                return Err(GitError::InvalidObjectId(
309                    "thin pack base object id format does not match pack format".into(),
310                ));
311            }
312        }
313
314        // Decide, for each object, whether it is stored undeltified or as a
315        // delta against another object (in-pack or an external thin base), and
316        // obtain the emit order. In-pack deltas only ever reference candidates
317        // that appear earlier in `order`, so emitting in `order` guarantees a
318        // base is always written before any object that deltas against it.
319        let (plan, order) = plan_pack_deltas(&objects, &object_ids, options)?;
320
321        let mut pack = Vec::new();
322        pack.extend_from_slice(b"PACK");
323        pack.extend_from_slice(&2u32.to_be_bytes());
324        pack.extend_from_slice(&(objects.len() as u32).to_be_bytes());
325
326        let mut index_entries = Vec::with_capacity(objects.len());
327        let mut delta_count = 0u32;
328        // Pack offset at which each original object index was written, or
329        // `None` until it has been emitted.
330        let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
331
332        let compressed_payloads =
333            compress_planned_payloads(&objects, &plan, &order, options.compression_level)?;
334
335        for (order_pos, &idx) in order.iter().enumerate() {
336            let offset = pack.len() as u64;
337            let mut entry_bytes = Vec::new();
338            match &plan[idx].base {
339                PlannedBase::None => {
340                    write_entry_header(
341                        &mut entry_bytes,
342                        objects[idx].object_type,
343                        objects[idx].body.len() as u64,
344                    );
345                }
346                PlannedBase::InPack { base_idx, delta } => {
347                    delta_count += 1;
348                    let base_offset = written_offsets[*base_idx].ok_or_else(|| {
349                        GitError::InvalidFormat(
350                            "in-pack delta base emitted after dependent object".into(),
351                        )
352                    })?;
353                    if options.prefer_ofs_delta {
354                        write_pack_entry_header_kind(&mut entry_bytes, 6, delta.len() as u64);
355                        let relative = offset.checked_sub(base_offset).ok_or_else(|| {
356                            GitError::InvalidFormat("ofs-delta base offset is after delta".into())
357                        })?;
358                        write_ofs_delta_offset(&mut entry_bytes, relative)?;
359                    } else {
360                        write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
361                        entry_bytes.extend_from_slice(object_ids[*base_idx].as_bytes());
362                    }
363                }
364                PlannedBase::External { base_oid, delta } => {
365                    delta_count += 1;
366                    write_pack_entry_header_kind(&mut entry_bytes, 7, delta.len() as u64);
367                    entry_bytes.extend_from_slice(base_oid.as_bytes());
368                }
369            }
370            entry_bytes.extend_from_slice(&compressed_payloads[order_pos]);
371            let crc32 = crc32fast::hash(&entry_bytes);
372            pack.extend_from_slice(&entry_bytes);
373            written_offsets[idx] = Some(offset);
374            index_entries.push(PackIndexEntry {
375                oid: object_ids[idx].clone(),
376                crc32,
377                offset,
378            });
379        }
380
381        let checksum = sley_core::digest_bytes(format, &pack)?;
382        pack.extend_from_slice(checksum.as_bytes());
383        let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
384        Ok(PackWrite {
385            pack,
386            index,
387            checksum,
388            entries: index_entries,
389            delta_count,
390        })
391    }
392
393    pub(crate) fn write_packed_from_parts_to_writer<W>(
394        objects: Vec<&EncodedObject>,
395        object_ids: Vec<ObjectId>,
396        format: ObjectFormat,
397        options: &PackWriteOptions,
398        writer: &mut W,
399    ) -> Result<PackWriteSummary>
400    where
401        W: Write,
402    {
403        let mut seen = HashSet::with_capacity(object_ids.len());
404        for oid in &object_ids {
405            if !seen.insert(oid) {
406                return Err(GitError::InvalidFormat(format!(
407                    "pack contains duplicate object id {oid}"
408                )));
409            }
410        }
411
412        for oid in options.thin_bases.keys() {
413            if oid.format() != format {
414                return Err(GitError::InvalidObjectId(
415                    "thin pack base object id format does not match pack format".into(),
416                ));
417            }
418        }
419
420        let (plan, order) = plan_pack_deltas(&objects, &object_ids, options)?;
421        let mut output = PackDigestWriter::new(writer, format);
422        output.write_pack_bytes(b"PACK")?;
423        output.write_pack_bytes(&2u32.to_be_bytes())?;
424        output.write_pack_bytes(&(objects.len() as u32).to_be_bytes())?;
425
426        let mut index_entries = Vec::with_capacity(objects.len());
427        let mut delta_count = 0u32;
428        let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
429
430        for order_window in order.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
431            let compressed_payloads = compress_planned_payloads(
432                &objects,
433                &plan,
434                order_window,
435                options.compression_level,
436            )?;
437            for (&idx, compressed_payload) in order_window.iter().zip(&compressed_payloads) {
438                let offset = output.position();
439                let mut entry_header = Vec::new();
440                match &plan[idx].base {
441                    PlannedBase::None => {
442                        write_entry_header(
443                            &mut entry_header,
444                            objects[idx].object_type,
445                            objects[idx].body.len() as u64,
446                        );
447                    }
448                    PlannedBase::InPack { base_idx, delta } => {
449                        delta_count += 1;
450                        let base_offset = written_offsets[*base_idx].ok_or_else(|| {
451                            GitError::InvalidFormat(
452                                "in-pack delta base emitted after dependent object".into(),
453                            )
454                        })?;
455                        if options.prefer_ofs_delta {
456                            write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
457                            let relative = offset.checked_sub(base_offset).ok_or_else(|| {
458                                GitError::InvalidFormat(
459                                    "ofs-delta base offset is after delta".into(),
460                                )
461                            })?;
462                            write_ofs_delta_offset(&mut entry_header, relative)?;
463                        } else {
464                            write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
465                            entry_header.extend_from_slice(object_ids[*base_idx].as_bytes());
466                        }
467                    }
468                    PlannedBase::External { base_oid, delta } => {
469                        delta_count += 1;
470                        write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
471                        entry_header.extend_from_slice(base_oid.as_bytes());
472                    }
473                }
474                let mut crc32 = crc32fast::Hasher::new();
475                crc32.update(&entry_header);
476                crc32.update(compressed_payload);
477                output.write_pack_bytes(&entry_header)?;
478                output.write_pack_bytes(compressed_payload)?;
479                written_offsets[idx] = Some(offset);
480                index_entries.push(PackIndexEntry {
481                    oid: object_ids[idx],
482                    crc32: crc32.finalize(),
483                    offset,
484                });
485            }
486        }
487
488        let (checksum, pack_size) = output.finish()?;
489        let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
490        Ok(PackWriteSummary {
491            index,
492            checksum,
493            entries: index_entries,
494            delta_count,
495            pack_size,
496        })
497    }
498
499    pub fn write_undeltified_from_source_to_writer<W, F>(
500        object_ids: &[ObjectId],
501        format: ObjectFormat,
502        options: &PackWriteOptions,
503        mut read_object: F,
504        writer: &mut W,
505    ) -> Result<PackWriteSummary>
506    where
507        W: Write,
508        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
509    {
510        let mut seen = HashSet::with_capacity(object_ids.len());
511        for oid in object_ids {
512            if oid.format() != format {
513                return Err(GitError::InvalidObjectId(
514                    "pack object id format does not match pack format".into(),
515                ));
516            }
517            if !seen.insert(oid) {
518                return Err(GitError::InvalidFormat(format!(
519                    "pack contains duplicate object id {oid}"
520                )));
521            }
522        }
523
524        let mut output = PackDigestWriter::new(writer, format);
525        output.write_pack_bytes(b"PACK")?;
526        output.write_pack_bytes(&2u32.to_be_bytes())?;
527        output.write_pack_bytes(&(object_ids.len() as u32).to_be_bytes())?;
528
529        let mut index_entries = Vec::with_capacity(object_ids.len());
530        for oid_window in object_ids.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
531            let mut objects = Vec::with_capacity(oid_window.len());
532            for oid in oid_window {
533                objects.push(read_object(oid)?);
534            }
535            let compressed_payloads =
536                compress_undeltified_payloads(&objects, options.compression_level)?;
537            for ((oid, object), compressed_payload) in
538                oid_window.iter().zip(&objects).zip(&compressed_payloads)
539            {
540                let offset = output.position();
541                let mut entry_header = Vec::new();
542                write_entry_header(
543                    &mut entry_header,
544                    object.object_type,
545                    object.body.len() as u64,
546                );
547                let mut crc32 = crc32fast::Hasher::new();
548                crc32.update(&entry_header);
549                crc32.update(compressed_payload);
550                output.write_pack_bytes(&entry_header)?;
551                output.write_pack_bytes(compressed_payload)?;
552                index_entries.push(PackIndexEntry {
553                    oid: *oid,
554                    crc32: crc32.finalize(),
555                    offset,
556                });
557            }
558        }
559
560        let (checksum, pack_size) = output.finish()?;
561        let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
562        Ok(PackWriteSummary {
563            index,
564            checksum,
565            entries: index_entries,
566            delta_count: 0,
567            pack_size,
568        })
569    }
570
571    pub fn write_packed_from_source_to_writer<W, F>(
572        object_ids: &[ObjectId],
573        format: ObjectFormat,
574        options: &PackWriteOptions,
575        mut read_object: F,
576        writer: &mut W,
577    ) -> Result<PackWriteSummary>
578    where
579        W: Write,
580        F: FnMut(&ObjectId) -> Result<Arc<EncodedObject>>,
581    {
582        if object_ids.len() > u32::MAX as usize {
583            return Err(GitError::InvalidFormat("too many pack objects".into()));
584        }
585
586        let mut seen = HashSet::with_capacity(object_ids.len());
587        for oid in object_ids {
588            if oid.format() != format {
589                return Err(GitError::InvalidObjectId(
590                    "pack object id format does not match pack format".into(),
591                ));
592            }
593            if !seen.insert(*oid) {
594                return Err(GitError::InvalidFormat(format!(
595                    "pack contains duplicate object id {oid}"
596                )));
597            }
598        }
599
600        for oid in options.thin_bases.keys() {
601            if oid.format() != format {
602                return Err(GitError::InvalidObjectId(
603                    "thin pack base object id format does not match pack format".into(),
604                ));
605            }
606        }
607
608        let mut output = PackDigestWriter::new(writer, format);
609        output.write_pack_bytes(b"PACK")?;
610        output.write_pack_bytes(&2u32.to_be_bytes())?;
611        output.write_pack_bytes(&(object_ids.len() as u32).to_be_bytes())?;
612
613        let mut index_entries = Vec::with_capacity(object_ids.len());
614        let mut delta_count = 0u32;
615        let mut base_horizon: VecDeque<StreamingDeltaBase> = VecDeque::new();
616
617        for oid_window in object_ids.chunks(PACK_STREAM_COMPRESSION_WINDOW_OBJECTS) {
618            let mut objects = Vec::with_capacity(oid_window.len());
619            for oid in oid_window {
620                objects.push(read_object(oid)?);
621            }
622
623            let (plan, order) =
624                plan_streaming_window_deltas(&objects, oid_window, &base_horizon, options);
625            let compressed_payloads = compress_streaming_planned_payloads(
626                &objects,
627                &plan,
628                &order,
629                options.compression_level,
630            )?;
631            let mut written_offsets: Vec<Option<u64>> = vec![None; objects.len()];
632
633            for (&idx, compressed_payload) in order.iter().zip(&compressed_payloads) {
634                let offset = output.position();
635                let mut entry_header = Vec::new();
636                match &plan[idx].base {
637                    StreamingPlannedBase::None => {
638                        write_entry_header(
639                            &mut entry_header,
640                            objects[idx].object_type,
641                            objects[idx].body.len() as u64,
642                        );
643                    }
644                    StreamingPlannedBase::Current { base_idx, delta } => {
645                        delta_count += 1;
646                        let base_offset = written_offsets[*base_idx].ok_or_else(|| {
647                            GitError::InvalidFormat(
648                                "in-pack delta base emitted after dependent object".into(),
649                            )
650                        })?;
651                        if options.prefer_ofs_delta {
652                            write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
653                            let relative = offset.checked_sub(base_offset).ok_or_else(|| {
654                                GitError::InvalidFormat(
655                                    "ofs-delta base offset is after delta".into(),
656                                )
657                            })?;
658                            write_ofs_delta_offset(&mut entry_header, relative)?;
659                        } else {
660                            write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
661                            entry_header.extend_from_slice(oid_window[*base_idx].as_bytes());
662                        }
663                    }
664                    StreamingPlannedBase::Previous {
665                        base_oid,
666                        base_offset,
667                        delta,
668                    } => {
669                        delta_count += 1;
670                        if options.prefer_ofs_delta {
671                            write_pack_entry_header_kind(&mut entry_header, 6, delta.len() as u64);
672                            let relative = offset.checked_sub(*base_offset).ok_or_else(|| {
673                                GitError::InvalidFormat(
674                                    "ofs-delta base offset is after delta".into(),
675                                )
676                            })?;
677                            write_ofs_delta_offset(&mut entry_header, relative)?;
678                        } else {
679                            write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
680                            entry_header.extend_from_slice(base_oid.as_bytes());
681                        }
682                    }
683                    StreamingPlannedBase::External { base_oid, delta } => {
684                        delta_count += 1;
685                        write_pack_entry_header_kind(&mut entry_header, 7, delta.len() as u64);
686                        entry_header.extend_from_slice(base_oid.as_bytes());
687                    }
688                }
689
690                let mut crc32 = crc32fast::Hasher::new();
691                crc32.update(&entry_header);
692                crc32.update(compressed_payload);
693                output.write_pack_bytes(&entry_header)?;
694                output.write_pack_bytes(compressed_payload)?;
695                written_offsets[idx] = Some(offset);
696                index_entries.push(PackIndexEntry {
697                    oid: oid_window[idx],
698                    crc32: crc32.finalize(),
699                    offset,
700                });
701
702                if options.depth > 0 && options.window > 0 {
703                    base_horizon.push_back(StreamingDeltaBase {
704                        oid: oid_window[idx],
705                        object: Arc::clone(&objects[idx]),
706                        offset,
707                        depth: plan[idx].depth,
708                    });
709                    while base_horizon.len() > options.window {
710                        base_horizon.pop_front();
711                    }
712                }
713            }
714        }
715
716        let (checksum, pack_size) = output.finish()?;
717        let index = PackIndex::write_v2(format, &index_entries, &checksum)?;
718        Ok(PackWriteSummary {
719            index,
720            checksum,
721            entries: index_entries,
722            delta_count,
723            pack_size,
724        })
725    }
726}
727
728pub(crate) struct PackDigestWriter<'a, W> {
729    writer: &'a mut W,
730    digest: StreamingDigest,
731    position: u64,
732}
733
734impl<'a, W> PackDigestWriter<'a, W>
735where
736    W: Write,
737{
738    pub(crate) fn new(writer: &'a mut W, format: ObjectFormat) -> Self {
739        Self {
740            writer,
741            digest: StreamingDigest::new(format),
742            position: 0,
743        }
744    }
745
746    pub(crate) fn position(&self) -> u64 {
747        self.position
748    }
749
750    pub(crate) fn write_pack_bytes(&mut self, bytes: &[u8]) -> Result<()> {
751        self.writer.write_all(bytes)?;
752        self.digest.update(bytes);
753        self.position = self
754            .position
755            .checked_add(bytes.len() as u64)
756            .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
757        Ok(())
758    }
759
760    pub(crate) fn finish(mut self) -> Result<(ObjectId, u64)> {
761        let checksum = self.digest.finalize()?;
762        self.writer.write_all(checksum.as_bytes())?;
763        self.position = self
764            .position
765            .checked_add(checksum.as_bytes().len() as u64)
766            .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
767        Ok((checksum, self.position))
768    }
769}
770pub(crate) fn compress_planned_payloads(
771    objects: &[&EncodedObject],
772    plan: &[PlannedEntry],
773    order: &[usize],
774    compression_level: u32,
775) -> Result<Vec<Vec<u8>>> {
776    if order.is_empty() {
777        return Ok(Vec::new());
778    }
779
780    let worker_count = std::thread::available_parallelism()
781        .map(|threads| threads.get())
782        .unwrap_or(1)
783        .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
784        .min(order.len());
785    if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
786        let mut payloads = Vec::with_capacity(order.len());
787        for &idx in order {
788            payloads.push(compressed_payload(
789                planned_payload(objects, plan, idx),
790                compression_level,
791            )?);
792        }
793        return Ok(payloads);
794    }
795
796    let chunk_len = order.len().div_ceil(worker_count);
797    let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
798    std::thread::scope(|scope| {
799        let mut handles = Vec::new();
800        for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
801            let chunk_start = chunk_idx * chunk_len;
802            handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
803                let mut chunk_payloads = Vec::with_capacity(chunk.len());
804                for (offset, &idx) in chunk.iter().enumerate() {
805                    chunk_payloads.push((
806                        chunk_start + offset,
807                        compressed_payload(planned_payload(objects, plan, idx), compression_level)?,
808                    ));
809                }
810                Ok(chunk_payloads)
811            }));
812        }
813
814        let mut first_error = None;
815        for handle in handles {
816            match handle.join() {
817                Ok(Ok(chunk_payloads)) => {
818                    if first_error.is_none() {
819                        for (pos, payload) in chunk_payloads {
820                            payloads[pos] = payload;
821                        }
822                    }
823                }
824                Ok(Err(err)) => {
825                    first_error.get_or_insert(err);
826                }
827                Err(_) => {
828                    first_error.get_or_insert_with(|| {
829                        GitError::InvalidObject("pack compression worker panicked".into())
830                    });
831                }
832            }
833        }
834
835        match first_error {
836            Some(err) => Err(err),
837            None => Ok(()),
838        }
839    })?;
840    Ok(payloads)
841}
842
843pub(crate) fn compress_streaming_planned_payloads(
844    objects: &[Arc<EncodedObject>],
845    plan: &[StreamingPlannedEntry],
846    order: &[usize],
847    compression_level: u32,
848) -> Result<Vec<Vec<u8>>> {
849    if order.is_empty() {
850        return Ok(Vec::new());
851    }
852
853    let worker_count = std::thread::available_parallelism()
854        .map(|threads| threads.get())
855        .unwrap_or(1)
856        .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
857        .min(order.len());
858    if worker_count <= 1 || order.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
859        let mut payloads = Vec::with_capacity(order.len());
860        for &idx in order {
861            payloads.push(compressed_payload(
862                streaming_planned_payload(objects, plan, idx),
863                compression_level,
864            )?);
865        }
866        return Ok(payloads);
867    }
868
869    let chunk_len = order.len().div_ceil(worker_count);
870    let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new).take(order.len()).collect();
871    std::thread::scope(|scope| {
872        let mut handles = Vec::new();
873        for (chunk_idx, chunk) in order.chunks(chunk_len).enumerate() {
874            let chunk_start = chunk_idx * chunk_len;
875            handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
876                let mut chunk_payloads = Vec::with_capacity(chunk.len());
877                for (offset, &idx) in chunk.iter().enumerate() {
878                    chunk_payloads.push((
879                        chunk_start + offset,
880                        compressed_payload(
881                            streaming_planned_payload(objects, plan, idx),
882                            compression_level,
883                        )?,
884                    ));
885                }
886                Ok(chunk_payloads)
887            }));
888        }
889
890        let mut first_error = None;
891        for handle in handles {
892            match handle.join() {
893                Ok(Ok(chunk_payloads)) => {
894                    if first_error.is_none() {
895                        for (pos, payload) in chunk_payloads {
896                            payloads[pos] = payload;
897                        }
898                    }
899                }
900                Ok(Err(err)) => {
901                    first_error.get_or_insert(err);
902                }
903                Err(_) => {
904                    first_error.get_or_insert_with(|| {
905                        GitError::InvalidObject("pack compression worker panicked".into())
906                    });
907                }
908            }
909        }
910
911        match first_error {
912            Some(err) => Err(err),
913            None => Ok(()),
914        }
915    })?;
916    Ok(payloads)
917}
918
919pub(crate) fn compress_undeltified_payloads(
920    objects: &[Arc<EncodedObject>],
921    compression_level: u32,
922) -> Result<Vec<Vec<u8>>> {
923    if objects.is_empty() {
924        return Ok(Vec::new());
925    }
926
927    let worker_count = std::thread::available_parallelism()
928        .map(|threads| threads.get())
929        .unwrap_or(1)
930        .min(PACK_PARALLEL_COMPRESSION_MAX_THREADS)
931        .min(objects.len());
932    if worker_count <= 1 || objects.len() < PACK_PARALLEL_COMPRESSION_MIN_OBJECTS {
933        let mut payloads = Vec::with_capacity(objects.len());
934        for object in objects {
935            payloads.push(compressed_payload(&object.body, compression_level)?);
936        }
937        return Ok(payloads);
938    }
939
940    let chunk_len = objects.len().div_ceil(worker_count);
941    let mut payloads: Vec<Vec<u8>> = std::iter::repeat_with(Vec::new)
942        .take(objects.len())
943        .collect();
944    std::thread::scope(|scope| {
945        let mut handles = Vec::new();
946        for (chunk_idx, chunk) in objects.chunks(chunk_len).enumerate() {
947            let chunk_start = chunk_idx * chunk_len;
948            handles.push(scope.spawn(move || -> Result<Vec<(usize, Vec<u8>)>> {
949                let mut chunk_payloads = Vec::with_capacity(chunk.len());
950                for (offset, object) in chunk.iter().enumerate() {
951                    chunk_payloads.push((
952                        chunk_start + offset,
953                        compressed_payload(&object.body, compression_level)?,
954                    ));
955                }
956                Ok(chunk_payloads)
957            }));
958        }
959
960        let mut first_error = None;
961        for handle in handles {
962            match handle.join() {
963                Ok(Ok(chunk_payloads)) => {
964                    if first_error.is_none() {
965                        for (pos, payload) in chunk_payloads {
966                            payloads[pos] = payload;
967                        }
968                    }
969                }
970                Ok(Err(err)) => {
971                    first_error.get_or_insert(err);
972                }
973                Err(_) => {
974                    first_error.get_or_insert_with(|| {
975                        GitError::InvalidObject("pack compression worker panicked".into())
976                    });
977                }
978            }
979        }
980
981        match first_error {
982            Some(err) => Err(err),
983            None => Ok(()),
984        }
985    })?;
986    Ok(payloads)
987}
988
989pub(crate) fn streaming_planned_payload<'a>(
990    objects: &'a [Arc<EncodedObject>],
991    plan: &'a [StreamingPlannedEntry],
992    idx: usize,
993) -> &'a [u8] {
994    match &plan[idx].base {
995        StreamingPlannedBase::None => &objects[idx].body,
996        StreamingPlannedBase::Current { delta, .. }
997        | StreamingPlannedBase::Previous { delta, .. }
998        | StreamingPlannedBase::External { delta, .. } => delta,
999    }
1000}
1001
1002pub(crate) fn planned_payload<'a>(
1003    objects: &'a [&'a EncodedObject],
1004    plan: &'a [PlannedEntry],
1005    idx: usize,
1006) -> &'a [u8] {
1007    match &plan[idx].base {
1008        PlannedBase::None => &objects[idx].body,
1009        PlannedBase::InPack { delta, .. } | PlannedBase::External { delta, .. } => delta,
1010    }
1011}
1012
1013pub(crate) fn compressed_payload(body: &[u8], compression_level: u32) -> Result<Vec<u8>> {
1014    let mut out = Vec::new();
1015    write_compressed_payload(&mut out, body, compression_level)?;
1016    Ok(out)
1017}
1018pub(crate) fn write_compressed_payload(out: &mut Vec<u8>, body: &[u8], compression_level: u32) -> Result<()> {
1019    let mut compressor = Compress::new(Compression::new(compression_level.min(9)), true);
1020    out.reserve(zlib_compress_bound(body.len()));
1021    let status = compressor
1022        .compress_vec(body, out, FlushCompress::Finish)
1023        .map_err(|err| GitError::InvalidObject(format!("zlib compression failed: {err}")))?;
1024    if status != Status::StreamEnd || compressor.total_in() != body.len() as u64 {
1025        return Err(GitError::InvalidObject(
1026            "zlib compression did not finish pack entry".into(),
1027        ));
1028    }
1029    Ok(())
1030}
1031
1032pub(crate) fn zlib_compress_bound(len: usize) -> usize {
1033    len.saturating_add(len >> 12)
1034        .saturating_add(len >> 14)
1035        .saturating_add(len >> 25)
1036        .saturating_add(13)
1037}
1038
1039pub(crate) fn write_entry_header(out: &mut Vec<u8>, object_type: ObjectType, size: u64) {
1040    let type_code = match object_type {
1041        ObjectType::Commit => 1,
1042        ObjectType::Tree => 2,
1043        ObjectType::Blob => 3,
1044        ObjectType::Tag => 4,
1045    };
1046    write_pack_entry_header_kind(out, type_code, size);
1047}
1048
1049pub(crate) fn write_pack_entry_header_kind(out: &mut Vec<u8>, type_code: u8, mut size: u64) {
1050    let mut byte = (type_code << 4) | ((size as u8) & 0x0f);
1051    size >>= 4;
1052    if size != 0 {
1053        byte |= 0x80;
1054    }
1055    out.push(byte);
1056    while size != 0 {
1057        let mut byte = (size as u8) & 0x7f;
1058        size >>= 7;
1059        if size != 0 {
1060            byte |= 0x80;
1061        }
1062        out.push(byte);
1063    }
1064}
1065
1066pub(crate) fn write_ofs_delta_offset(out: &mut Vec<u8>, relative: u64) -> Result<()> {
1067    if relative == 0 {
1068        return Err(GitError::InvalidFormat(
1069            "ofs-delta relative offset cannot be zero".into(),
1070        ));
1071    }
1072    let mut value = relative;
1073    let mut bytes = vec![(value & 0x7f) as u8];
1074    value >>= 7;
1075    while value != 0 {
1076        value -= 1;
1077        bytes.push(((value & 0x7f) as u8) | 0x80);
1078        value >>= 7;
1079    }
1080    bytes.reverse();
1081    out.extend_from_slice(&bytes);
1082    Ok(())
1083}
1084/// Builder that assembles a reachability bitmap (`.bitmap`) for a pack.
1085///
1086/// The writer is constructed from the object layout of a pack (one
1087/// [`ObjectType`] per object, in pack order) and the pack's trailing checksum.
1088/// Callers then register one selected commit per [`add_commit`] call, supplying
1089/// the set of pack positions reachable from that commit. [`build`]/[`write`]
1090/// produce a [`PackBitmapIndex`] / serialised `.bitmap` bytes matching git's
1091/// on-disk format (signature `BITM`, version 1).
1092///
1093/// [`add_commit`]: PackBitmapWriter::add_commit
1094/// [`build`]: PackBitmapWriter::build
1095/// [`write`]: PackBitmapWriter::write
1096#[derive(Debug, Clone)]
1097pub struct PackBitmapWriter {
1098    format: ObjectFormat,
1099    pack_checksum: ObjectId,
1100    object_count: u32,
1101    commit_positions: Vec<u32>,
1102    tree_positions: Vec<u32>,
1103    blob_positions: Vec<u32>,
1104    tag_positions: Vec<u32>,
1105    name_hash_cache: Option<Vec<u32>>,
1106    selected: Vec<SelectedCommit>,
1107    pseudo_merges: Vec<PackBitmapPseudoMerge>,
1108}
1109
1110#[derive(Debug, Clone)]
1111pub(crate) struct SelectedCommit {
1112    /// Oid-sorted `.idx` position (what the on-disk entry records). The
1113    /// commit's pack-order position lives in `reachable` with the rest of the
1114    /// bits.
1115    commit_index_position: u32,
1116    flags: u8,
1117    reachable: Vec<u32>,
1118}
1119
1120impl PackBitmapWriter {
1121    /// `OBJ_NONE` selection flag: this commit's bitmap is stored in full (no XOR
1122    /// compression against a previously selected commit). This is the only flag
1123    /// value this writer emits.
1124    pub const FLAG_NONE: u8 = 0;
1125
1126    /// Creates a writer for a pack whose objects (in pack order) have the given
1127    /// [`ObjectType`]s and whose trailing checksum is `pack_checksum`.
1128    ///
1129    /// Returns an error if the pack contains more than `u32::MAX` objects, if
1130    /// `pack_checksum`'s format does not match `format`, or if any object type
1131    /// is not one of the four reachable git object kinds.
1132    pub fn new(
1133        format: ObjectFormat,
1134        pack_checksum: ObjectId,
1135        object_types: &[ObjectType],
1136    ) -> Result<Self> {
1137        if object_types.len() > u32::MAX as usize {
1138            return Err(GitError::InvalidFormat(
1139                "too many objects for a pack bitmap".into(),
1140            ));
1141        }
1142        if pack_checksum.format() != format {
1143            return Err(GitError::InvalidObjectId(
1144                "pack checksum format does not match bitmap format".into(),
1145            ));
1146        }
1147        let object_count = object_types.len() as u32;
1148        let mut commit_positions = Vec::new();
1149        let mut tree_positions = Vec::new();
1150        let mut blob_positions = Vec::new();
1151        let mut tag_positions = Vec::new();
1152        for (index, object_type) in object_types.iter().enumerate() {
1153            let position = index as u32;
1154            match object_type {
1155                ObjectType::Commit => commit_positions.push(position),
1156                ObjectType::Tree => tree_positions.push(position),
1157                ObjectType::Blob => blob_positions.push(position),
1158                ObjectType::Tag => tag_positions.push(position),
1159            }
1160        }
1161        Ok(Self {
1162            format,
1163            pack_checksum,
1164            object_count,
1165            commit_positions,
1166            tree_positions,
1167            blob_positions,
1168            tag_positions,
1169            name_hash_cache: None,
1170            selected: Vec::new(),
1171            pseudo_merges: Vec::new(),
1172        })
1173    }
1174
1175    /// Attaches a name-hash cache (one `u32` per object, in pack order). When
1176    /// set, the written bitmap advertises [`PackBitmapIndex::OPTION_HASH_CACHE`]
1177    /// and appends the cache after the bitmap entries, exactly as git does.
1178    ///
1179    /// Returns an error if the cache length does not equal the object count.
1180    pub fn with_name_hash_cache(mut self, cache: Vec<u32>) -> Result<Self> {
1181        if cache.len() != self.object_count as usize {
1182            return Err(GitError::InvalidFormat(format!(
1183                "name hash cache has {} entries but pack has {} objects",
1184                cache.len(),
1185                self.object_count
1186            )));
1187        }
1188        self.name_hash_cache = Some(cache);
1189        Ok(self)
1190    }
1191
1192    /// Registers a selected commit and the pack positions reachable from it.
1193    ///
1194    /// `commit_position` is the *pack-order* position of the commit itself (the
1195    /// bit-number space); it must reference a commit object and is implicitly
1196    /// part of the reachable set. `commit_index_position` is the commit's
1197    /// position in the *oid-sorted* pack index — this is what the on-disk entry
1198    /// records (upstream `oid_pos`); bits and entry positions live in different
1199    /// spaces. `reachable` lists the pack-order positions of every object
1200    /// reachable from the commit (it may include or omit `commit_position`;
1201    /// duplicates are fine). All positions must be in range. The commit's full
1202    /// (non-XORed) bitmap is stored.
1203    pub fn add_commit(
1204        &mut self,
1205        commit_position: u32,
1206        commit_index_position: u32,
1207        reachable: &[u32],
1208    ) -> Result<()> {
1209        if commit_position >= self.object_count {
1210            return Err(GitError::InvalidFormat(format!(
1211                "commit position {commit_position} out of range for {} objects",
1212                self.object_count
1213            )));
1214        }
1215        if commit_index_position >= self.object_count {
1216            return Err(GitError::InvalidFormat(format!(
1217                "commit index position {commit_index_position} out of range for {} objects",
1218                self.object_count
1219            )));
1220        }
1221        if !self.commit_positions.contains(&commit_position) {
1222            return Err(GitError::InvalidFormat(format!(
1223                "bitmap commit position {commit_position} is not a commit object"
1224            )));
1225        }
1226        for &position in reachable {
1227            if position >= self.object_count {
1228                return Err(GitError::InvalidFormat(format!(
1229                    "reachable position {position} out of range for {} objects",
1230                    self.object_count
1231                )));
1232            }
1233        }
1234        let mut reachable = reachable.to_vec();
1235        reachable.push(commit_position);
1236        self.selected.push(SelectedCommit {
1237            commit_index_position,
1238            flags: Self::FLAG_NONE,
1239            reachable,
1240        });
1241        Ok(())
1242    }
1243
1244    /// Registers a pseudo-merge bitmap. Both `commits` and `reachable` are
1245    /// positions in the bitmap's bit-numbering order (pack order for a single
1246    /// pack, pseudo-pack order for a MIDX). Every commit position must refer to
1247    /// a commit object; every reachable position must be in range.
1248    pub fn add_pseudo_merge(&mut self, commits: &[u32], reachable: &[u32]) -> Result<()> {
1249        if commits.is_empty() {
1250            return Err(GitError::InvalidFormat(
1251                "pseudo-merge must contain at least one commit".into(),
1252            ));
1253        }
1254        for &position in commits {
1255            if position >= self.object_count {
1256                return Err(GitError::InvalidFormat(format!(
1257                    "pseudo-merge commit position {position} out of range for {} objects",
1258                    self.object_count
1259                )));
1260            }
1261            if !self.commit_positions.contains(&position) {
1262                return Err(GitError::InvalidFormat(format!(
1263                    "pseudo-merge commit position {position} is not a commit object"
1264                )));
1265            }
1266        }
1267        for &position in reachable {
1268            if position >= self.object_count {
1269                return Err(GitError::InvalidFormat(format!(
1270                    "pseudo-merge reachable position {position} out of range for {} objects",
1271                    self.object_count
1272                )));
1273            }
1274        }
1275        self.pseudo_merges.push(PackBitmapPseudoMerge {
1276            commits: EwahBitmap::from_positions(self.object_count, commits)?,
1277            bitmap: EwahBitmap::from_positions(self.object_count, reachable)?,
1278        });
1279        Ok(())
1280    }
1281
1282    /// Builds the in-memory [`PackBitmapIndex`] without serialising it.
1283    ///
1284    /// The resulting index always advertises
1285    /// [`PackBitmapIndex::OPTION_FULL_DAG`] (the four type bitmaps fully cover
1286    /// the pack) and, when a name-hash cache was attached,
1287    /// [`PackBitmapIndex::OPTION_HASH_CACHE`].
1288    pub fn build(&self) -> Result<PackBitmapIndex> {
1289        let commits = EwahBitmap::from_positions(self.object_count, &self.commit_positions)?;
1290        let trees = EwahBitmap::from_positions(self.object_count, &self.tree_positions)?;
1291        let blobs = EwahBitmap::from_positions(self.object_count, &self.blob_positions)?;
1292        let tags = EwahBitmap::from_positions(self.object_count, &self.tag_positions)?;
1293
1294        let mut entries = Vec::with_capacity(self.selected.len());
1295        for selected in &self.selected {
1296            let bitmap = EwahBitmap::from_positions(self.object_count, &selected.reachable)?;
1297            entries.push(PackBitmapEntry {
1298                object_position: selected.commit_index_position,
1299                xor_offset: 0,
1300                flags: selected.flags,
1301                bitmap,
1302            });
1303        }
1304
1305        let mut options = PackBitmapIndex::OPTION_FULL_DAG;
1306        if self.name_hash_cache.is_some() {
1307            options |= PackBitmapIndex::OPTION_HASH_CACHE;
1308        }
1309        if !self.pseudo_merges.is_empty() {
1310            options |= PackBitmapIndex::OPTION_PSEUDO_MERGES;
1311        }
1312
1313        // The index checksum is only known once the body is serialised; the
1314        // dedicated `write` path fills it in. `build` reports a placeholder of
1315        // the correct format so the struct is self-consistent for callers that
1316        // only need the decoded bitmaps.
1317        let placeholder_checksum = ObjectId::null(self.format);
1318        Ok(PackBitmapIndex {
1319            version: 1,
1320            format: self.format,
1321            options,
1322            pack_checksum: self.pack_checksum.clone(),
1323            index_checksum: placeholder_checksum,
1324            type_bitmaps: PackBitmapTypeBitmaps {
1325                commits,
1326                trees,
1327                blobs,
1328                tags,
1329            },
1330            entries,
1331            pseudo_merges: self.pseudo_merges.clone(),
1332            name_hash_cache: self.name_hash_cache.clone(),
1333        })
1334    }
1335
1336    /// Builds and serialises the `.bitmap` file, returning the on-disk bytes
1337    /// (including the trailing index checksum).
1338    pub fn write(&self) -> Result<Vec<u8>> {
1339        self.build()?.write()
1340    }
1341}
1342
1343impl PackBitmapIndex {
1344    /// Serialises this index into git's on-disk `.bitmap` byte layout.
1345    ///
1346    /// This is the exact inverse of [`PackBitmapIndex::parse`]: signature
1347    /// `BITM`, version (u16 BE), options (u16 BE), entry count (u32 BE), the
1348    /// pack checksum, the four type bitmaps (commits, trees, blobs, tags), each
1349    /// commit entry (object position, XOR offset, flags, EWAH bitmap), the
1350    /// optional pseudo-merge extension, the optional name-hash cache, and
1351    /// finally the trailing index checksum over everything written so far.
1352    ///
1353    /// The `index_checksum` field of `self` is ignored and recomputed from the
1354    /// serialised body. Returns an error for unsupported versions, mismatched
1355    /// object-id formats, an oversized entry table, or an inconsistent name-hash
1356    /// cache.
1357    pub fn write(&self) -> Result<Vec<u8>> {
1358        if self.version != 1 {
1359            return Err(GitError::Unsupported(format!(
1360                "bitmap index version {}",
1361                self.version
1362            )));
1363        }
1364        let mut options = self.options;
1365        if !self.pseudo_merges.is_empty() {
1366            options |= Self::OPTION_PSEUDO_MERGES;
1367        }
1368        let known_options =
1369            Self::OPTION_FULL_DAG | Self::OPTION_HASH_CACHE | Self::OPTION_PSEUDO_MERGES;
1370        if options & !known_options != 0 {
1371            return Err(GitError::Unsupported(format!(
1372                "bitmap index options {:#06x}",
1373                options & !known_options
1374            )));
1375        }
1376        if self.pack_checksum.format() != self.format {
1377            return Err(GitError::InvalidObjectId(
1378                "bitmap pack checksum format does not match index format".into(),
1379            ));
1380        }
1381        if self.entries.len() > u32::MAX as usize {
1382            return Err(GitError::InvalidFormat(
1383                "too many bitmap index entries".into(),
1384            ));
1385        }
1386        if options & Self::OPTION_PSEUDO_MERGES != 0 && self.pseudo_merges.is_empty() {
1387            return Err(GitError::InvalidFormat(
1388                "OPTION_PSEUDO_MERGES set without pseudo-merge records".into(),
1389            ));
1390        }
1391        let want_cache = options & Self::OPTION_HASH_CACHE != 0;
1392        match (&self.name_hash_cache, want_cache) {
1393            (Some(_), false) => {
1394                return Err(GitError::InvalidFormat(
1395                    "name hash cache present without OPTION_HASH_CACHE".into(),
1396                ));
1397            }
1398            (None, true) => {
1399                return Err(GitError::InvalidFormat(
1400                    "OPTION_HASH_CACHE set without a name hash cache".into(),
1401                ));
1402            }
1403            _ => {}
1404        }
1405
1406        let mut out = Vec::new();
1407        out.extend_from_slice(b"BITM");
1408        out.extend_from_slice(&self.version.to_be_bytes());
1409        out.extend_from_slice(&options.to_be_bytes());
1410        out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
1411        out.extend_from_slice(self.pack_checksum.as_bytes());
1412
1413        self.type_bitmaps.commits.append_bytes(&mut out);
1414        self.type_bitmaps.trees.append_bytes(&mut out);
1415        self.type_bitmaps.blobs.append_bytes(&mut out);
1416        self.type_bitmaps.tags.append_bytes(&mut out);
1417
1418        for (idx, entry) in self.entries.iter().enumerate() {
1419            if entry.xor_offset as usize > idx {
1420                return Err(GitError::InvalidFormat(
1421                    "bitmap index entry has invalid XOR offset".into(),
1422                ));
1423            }
1424            out.extend_from_slice(&entry.object_position.to_be_bytes());
1425            out.push(entry.xor_offset);
1426            out.push(entry.flags);
1427            entry.bitmap.append_bytes(&mut out);
1428        }
1429
1430        if !self.pseudo_merges.is_empty() {
1431            append_bitmap_pseudo_merges(&mut out, &self.pseudo_merges)?;
1432        }
1433
1434        if let Some(cache) = &self.name_hash_cache {
1435            for value in cache {
1436                out.extend_from_slice(&value.to_be_bytes());
1437            }
1438        }
1439
1440        let checksum = sley_core::digest_bytes(self.format, &out)?;
1441        out.extend_from_slice(checksum.as_bytes());
1442        Ok(out)
1443    }
1444}
1445
1446pub(crate) fn append_bitmap_pseudo_merges(
1447    out: &mut Vec<u8>,
1448    pseudo_merges: &[PackBitmapPseudoMerge],
1449) -> Result<()> {
1450    if pseudo_merges.len() > u32::MAX as usize {
1451        return Err(GitError::InvalidFormat(
1452            "too many pseudo-merge bitmap records".into(),
1453        ));
1454    }
1455    let start = out.len();
1456    let mut pseudo_offsets = Vec::with_capacity(pseudo_merges.len());
1457    let mut commit_to_offsets: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
1458    for merge in pseudo_merges {
1459        let offset = u64::try_from(out.len())
1460            .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
1461        pseudo_offsets.push(offset);
1462        for commit_pos in merge.commits.to_positions()? {
1463            commit_to_offsets
1464                .entry(commit_pos)
1465                .or_default()
1466                .push(offset);
1467        }
1468        merge.commits.append_bytes(out);
1469        merge.bitmap.append_bytes(out);
1470    }
1471    if commit_to_offsets.len() > u32::MAX as usize {
1472        return Err(GitError::InvalidFormat(
1473            "too many pseudo-merge commits".into(),
1474        ));
1475    }
1476
1477    let lookup_start = out.len();
1478    let lookup_len = commit_to_offsets
1479        .len()
1480        .checked_mul(12)
1481        .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?;
1482    let mut next_extended = u64::try_from(
1483        lookup_start
1484            .checked_add(lookup_len)
1485            .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup overflow".into()))?,
1486    )
1487    .map_err(|_| GitError::InvalidFormat("bitmap file offset overflow".into()))?;
1488    let mut rows = Vec::with_capacity(commit_to_offsets.len());
1489    for (commit_pos, offsets) in commit_to_offsets {
1490        let extended_offset = if offsets.len() > 1 {
1491            if next_extended & (1u64 << 63) != 0 {
1492                return Err(GitError::InvalidFormat(
1493                    "pseudo-merge extended offset overflow".into(),
1494                ));
1495            }
1496            let offset = next_extended;
1497            let ext_len = offsets
1498                .len()
1499                .checked_mul(8)
1500                .and_then(|len| len.checked_add(4))
1501                .ok_or_else(|| {
1502                    GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
1503                })?;
1504            next_extended = next_extended.checked_add(ext_len as u64).ok_or_else(|| {
1505                GitError::InvalidFormat("pseudo-merge extended lookup overflow".into())
1506            })?;
1507            Some(offset)
1508        } else {
1509            None
1510        };
1511        rows.push((commit_pos, offsets, extended_offset));
1512    }
1513
1514    for (commit_pos, offsets, extended_offset) in &rows {
1515        out.extend_from_slice(&commit_pos.to_be_bytes());
1516        match extended_offset {
1517            Some(offset) => out.extend_from_slice(&(offset | (1u64 << 63)).to_be_bytes()),
1518            None => out.extend_from_slice(&offsets[0].to_be_bytes()),
1519        }
1520    }
1521
1522    for (_commit_pos, offsets, extended_offset) in &rows {
1523        if extended_offset.is_none() {
1524            continue;
1525        }
1526        let count = u32::try_from(offsets.len())
1527            .map_err(|_| GitError::InvalidFormat("pseudo-merge extended lookup overflow".into()))?;
1528        out.extend_from_slice(&count.to_be_bytes());
1529        for offset in offsets {
1530            out.extend_from_slice(&offset.to_be_bytes());
1531        }
1532    }
1533
1534    for offset in &pseudo_offsets {
1535        out.extend_from_slice(&offset.to_be_bytes());
1536    }
1537    out.extend_from_slice(&(pseudo_merges.len() as u32).to_be_bytes());
1538    out.extend_from_slice(&(rows.len() as u32).to_be_bytes());
1539    let lookup_relative = lookup_start
1540        .checked_sub(start)
1541        .ok_or_else(|| GitError::InvalidFormat("pseudo-merge lookup underflow".into()))?;
1542    out.extend_from_slice(&(lookup_relative as u64).to_be_bytes());
1543    let extension_size = out
1544        .len()
1545        .checked_sub(start)
1546        .and_then(|len| len.checked_add(8))
1547        .ok_or_else(|| GitError::InvalidFormat("pseudo-merge extension overflow".into()))?;
1548    out.extend_from_slice(&(extension_size as u64).to_be_bytes());
1549    Ok(())
1550}
1551
1552/// Convenience wrapper that builds a `.bitmap` file in one call.
1553///
1554/// `object_types` lists the [`ObjectType`] of every pack object in pack order,
1555/// `pack_checksum` is the pack's trailing checksum, and `commits` carries, per
1556/// selected commit, `(pack_position, index_position, reachable_pack_positions)`
1557/// (see [`PackBitmapWriter::add_commit`] for the two position spaces). An
1558/// optional `name_hash_cache` (one entry per object) may be supplied to emit
1559/// the hash-cache extension.
1560pub fn write_bitmap(
1561    format: ObjectFormat,
1562    pack_checksum: ObjectId,
1563    object_types: &[ObjectType],
1564    commits: &[(u32, u32, Vec<u32>)],
1565    name_hash_cache: Option<Vec<u32>>,
1566) -> Result<Vec<u8>> {
1567    let mut writer = PackBitmapWriter::new(format, pack_checksum, object_types)?;
1568    if let Some(cache) = name_hash_cache {
1569        writer = writer.with_name_hash_cache(cache)?;
1570    }
1571    for (commit_position, commit_index_position, reachable) in commits {
1572        writer.add_commit(*commit_position, *commit_index_position, reachable)?;
1573    }
1574    writer.write()
1575}