Skip to main content

sley_pack/
index.rs

1//! Pack index, reverse index, mtimes, bitmap, multi-pack-index, and EWAH helpers.
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#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct PackIndexBuild {
9    pub index: Vec<u8>,
10    pub pack_checksum: ObjectId,
11    pub entries: Vec<PackIndexEntry>,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct PackStreamIndexBuild {
16    pub index: Vec<u8>,
17    pub pack_checksum: ObjectId,
18    pub entries: Vec<PackIndexEntry>,
19    pub objects: Vec<PackIndexedObject>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PackIndexedObject {
24    pub oid: ObjectId,
25    pub object_type: ObjectType,
26    pub size: u64,
27    pub offset: u64,
28}
29
30/// Streaming pack-receive counters emitted while `index_pack_from_stream`
31/// parses a pack off a reader. All fields are monotonically non-decreasing
32/// within one indexing pass.
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub struct PackStreamProgress {
35    /// Pack bytes consumed from the reader so far (header + object entries),
36    /// i.e. the streaming pack offset. Advances during download for streaming
37    /// transports; during the index walk for already-buffered ones.
38    pub received_bytes: u64,
39    /// Objects parsed from the pack so far.
40    pub received_objects: u64,
41    /// Total objects declared by the pack header (known once the 12-byte header
42    /// is read).
43    pub total_objects: u64,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct PackIndex {
48    pub version: u32,
49    pub fanout: [u32; 256],
50    pub entries: Vec<PackIndexEntry>,
51    pub pack_checksum: ObjectId,
52    pub index_checksum: ObjectId,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct PackIndexView<'a> {
57    pub version: u32,
58    pub count: usize,
59    pub fanout: [u32; 256],
60    pub pack_checksum: ObjectId,
61    pub index_checksum: ObjectId,
62    bytes: &'a [u8],
63    format: ObjectFormat,
64    tables: PackIndexViewTables,
65}
66
67pub trait PackIndexByteSource: fmt::Debug + Send + Sync {
68    fn as_bytes(&self) -> &[u8];
69}
70
71impl<T> PackIndexByteSource for T
72where
73    T: AsRef<[u8]> + fmt::Debug + Send + Sync + ?Sized,
74{
75    fn as_bytes(&self) -> &[u8] {
76        self.as_ref()
77    }
78}
79
80#[derive(Debug)]
81pub(crate) struct SharedIndexBytes(Arc<[u8]>);
82
83impl PackIndexByteSource for SharedIndexBytes {
84    fn as_bytes(&self) -> &[u8] {
85        self.0.as_ref()
86    }
87}
88
89#[derive(Debug, Clone)]
90pub struct PackIndexViewData {
91    pub version: u32,
92    pub count: usize,
93    pub fanout: [u32; 256],
94    pub pack_checksum: ObjectId,
95    pub index_checksum: ObjectId,
96    bytes: Arc<dyn PackIndexByteSource>,
97    format: ObjectFormat,
98    tables: PackIndexViewTables,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct PackIndexEntry {
103    pub oid: ObjectId,
104    pub crc32: u32,
105    pub offset: u64,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct PackIndexLookup {
110    pub crc32: u32,
111    pub offset: u64,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub(crate) enum PackIndexViewTables {
116    V1 {
117        entry_table: Range<usize>,
118    },
119    V2 {
120        oid_table: Range<usize>,
121        crc_table: Range<usize>,
122        small_offset_table: Range<usize>,
123        large_offset_table: Range<usize>,
124    },
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct PackReverseIndex {
129    pub version: u32,
130    pub format: ObjectFormat,
131    pub positions: Vec<u32>,
132    pub pack_checksum: ObjectId,
133    pub index_checksum: ObjectId,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct PackMtimes {
138    pub version: u32,
139    pub format: ObjectFormat,
140    pub mtimes: Vec<u32>,
141    pub pack_checksum: ObjectId,
142    pub index_checksum: ObjectId,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct PackBitmapIndex {
147    pub version: u16,
148    pub format: ObjectFormat,
149    pub options: u16,
150    pub pack_checksum: ObjectId,
151    pub index_checksum: ObjectId,
152    pub type_bitmaps: PackBitmapTypeBitmaps,
153    pub entries: Vec<PackBitmapEntry>,
154    pub pseudo_merges: Vec<PackBitmapPseudoMerge>,
155    /// Whether the serialised bitmap carries the commit lookup-table
156    /// extension. The table itself is derived deterministically from entries.
157    pub lookup_table: bool,
158    pub name_hash_cache: Option<Vec<u32>>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct PackBitmapTypeBitmaps {
163    pub commits: EwahBitmap,
164    pub trees: EwahBitmap,
165    pub blobs: EwahBitmap,
166    pub tags: EwahBitmap,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct PackBitmapEntry {
171    /// The commit's position in the *oid-sorted* pack index (`.idx` order),
172    /// NOT the pack-order position used for the bitmap's bit numbering.
173    /// Upstream writes `oid_pos(...)` here (pack-bitmap-write.c) and reads it
174    /// back via `nth_packed_object_id` (pack-bitmap.c).
175    pub object_position: u32,
176    pub xor_offset: u8,
177    pub flags: u8,
178    /// Reachability bitmap; bit `i` refers to the `i`-th object in *pack
179    /// order* (offset order), as mapped by the pack's reverse index.
180    pub bitmap: EwahBitmap,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct PackBitmapPseudoMerge {
185    /// Commit bits, in the bitmap's bit-numbering order, covered by this
186    /// pseudo-merge.
187    pub commits: EwahBitmap,
188    /// Object reachability closure for the pseudo-merge's commits, in the same
189    /// bit-numbering order.
190    pub bitmap: EwahBitmap,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct EwahBitmap {
195    pub bit_size: u32,
196    pub words: Vec<u64>,
197    pub rlw_position: u32,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct MultiPackIndex {
202    pub version: u8,
203    pub format: ObjectFormat,
204    pub pack_count: u32,
205    pub pack_names: Vec<String>,
206    pub object_count: u32,
207    pub fanout: [u32; 256],
208    pub objects: Vec<MultiPackIndexEntry>,
209    pub reverse_index: Option<Vec<u32>>,
210    pub bitmapped_packs: Option<Vec<MultiPackBitmapPack>>,
211    pub chunks: Vec<MultiPackIndexChunk>,
212    pub checksum: ObjectId,
213}
214
215#[derive(Debug, Clone)]
216pub struct MultiPackIndexOidLookup {
217    format: ObjectFormat,
218    pack_count: u32,
219    pack_names: Vec<String>,
220    fanout: [u32; 256],
221    object_count: usize,
222    oid_lookup_offset: usize,
223    object_offsets_offset: usize,
224    large_offsets_offset: Option<usize>,
225    large_offsets_len: usize,
226    bytes: Arc<dyn PackIndexByteSource>,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct MultiPackIndexEntry {
231    pub oid: ObjectId,
232    pub pack_int_id: u32,
233    pub offset: u64,
234    pub force_large_offset: bool,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct MultiPackBitmapPack {
239    pub bitmap_pos: u32,
240    pub bitmap_nr: u32,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct MultiPackIndexChunk {
245    pub id: [u8; 4],
246    pub offset: u64,
247    pub len: u64,
248}
249impl<'a> PackIndexView<'a> {
250    pub fn parse_v2_sha1(bytes: &'a [u8]) -> Result<Self> {
251        Self::parse(bytes, ObjectFormat::Sha1)
252    }
253
254    pub fn parse(bytes: &'a [u8], format: ObjectFormat) -> Result<Self> {
255        Self::parse_impl(bytes, format, true, true)
256    }
257
258    /// Parse and validate the index layout without recomputing the trailing
259    /// index checksum. The checksum stored in the file is still exposed via
260    /// [`PackIndexView::index_checksum`].
261    pub fn parse_without_checksum(bytes: &'a [u8], format: ObjectFormat) -> Result<Self> {
262        Self::parse_impl(bytes, format, false, true)
263    }
264
265    /// Parse a local/trusted pack index without recomputing the trailing index
266    /// checksum or walking every entry for canonical-order validation.
267    ///
268    /// This still validates the table layout and all lookup paths remain
269    /// bounds-checked, but it avoids O(number-of-objects) startup validation for
270    /// repository-owned `.idx` files in hot read paths.
271    pub fn parse_trusted_without_checksum(bytes: &'a [u8], format: ObjectFormat) -> Result<Self> {
272        Self::parse_impl(bytes, format, false, false)
273    }
274
275    pub fn count(&self) -> usize {
276        self.count
277    }
278
279    pub fn fanout(&self) -> &[u32; 256] {
280        &self.fanout
281    }
282
283    pub fn find(&self, oid: &ObjectId) -> Option<PackIndexLookup> {
284        if oid.format() != self.format {
285            return None;
286        }
287        let bucket = usize::from(oid.as_bytes()[0]);
288        let mut start = if bucket == 0 {
289            0
290        } else {
291            self.fanout[bucket - 1] as usize
292        };
293        let mut end = self.fanout[bucket] as usize;
294        let target = oid.as_bytes();
295
296        while start < end {
297            let mid = start + (end - start) / 2;
298            match self.oid_bytes_at(mid).cmp(target) {
299                std::cmp::Ordering::Less => start = mid + 1,
300                std::cmp::Ordering::Equal => return self.lookup_at(mid),
301                std::cmp::Ordering::Greater => end = mid,
302            }
303        }
304        None
305    }
306
307    pub(crate) fn parse_impl(
308        bytes: &'a [u8],
309        format: ObjectFormat,
310        verify_checksum: bool,
311        validate_entries: bool,
312    ) -> Result<Self> {
313        let hash_len = format.raw_len();
314        if bytes.len() < 4 {
315            return Err(GitError::InvalidFormat("pack index too short".into()));
316        }
317        if bytes[..4] != [0xff, b't', b'O', b'c'] {
318            return Self::parse_v1_impl(bytes, format, verify_checksum, validate_entries);
319        }
320        if bytes.len() < 8 + 256 * 4 + 2 * hash_len {
321            return Err(GitError::InvalidFormat("pack index too short".into()));
322        }
323        let version = u32_be(&bytes[4..8]);
324        if version != 2 {
325            return Err(GitError::Unsupported(format!(
326                "pack index version {version}"
327            )));
328        }
329        let index_checksum_offset = bytes.len() - hash_len;
330        let index_checksum = ObjectId::from_raw(format, &bytes[index_checksum_offset..])?;
331        if verify_checksum {
332            let actual_index_checksum =
333                sley_core::digest_bytes(format, &bytes[..index_checksum_offset])?;
334            if actual_index_checksum != index_checksum {
335                return Err(GitError::InvalidFormat(format!(
336                    "pack index checksum mismatch: expected {index_checksum}, got {actual_index_checksum}"
337                )));
338            }
339        }
340
341        let mut offset = 8usize;
342        let fanout = read_pack_index_fanout(bytes, &mut offset)?;
343        let count = fanout[255] as usize;
344        let oid_table = checked_range(offset, count, hash_len, bytes.len())?;
345        offset = oid_table.end;
346        let crc_table = checked_range(offset, count, 4, bytes.len())?;
347        offset = crc_table.end;
348        let small_offset_table = checked_range(offset, count, 4, bytes.len())?;
349        offset = small_offset_table.end;
350
351        let large_offset_count = (0..count)
352            .filter(|idx| {
353                let start = small_offset_table.start + idx * 4;
354                u32_be(&bytes[start..start + 4]) & 0x8000_0000 != 0
355            })
356            .count();
357        let mut large_offset_table = checked_range(offset, large_offset_count, 8, bytes.len())?;
358        offset = large_offset_table.end;
359
360        let expected_trailer_offset = bytes.len() - hash_len * 2;
361        if offset != expected_trailer_offset {
362            if !verify_checksum && offset < expected_trailer_offset {
363                large_offset_table = large_offset_table.start..expected_trailer_offset;
364                offset = expected_trailer_offset;
365            } else {
366                return Err(GitError::InvalidFormat(format!(
367                    "pack index has {} unexpected bytes before trailer",
368                    expected_trailer_offset.saturating_sub(offset)
369                )));
370            }
371        }
372        let pack_checksum = ObjectId::from_raw(format, &bytes[offset..offset + hash_len])?;
373
374        let view = Self {
375            version,
376            count,
377            fanout,
378            pack_checksum,
379            index_checksum,
380            bytes,
381            format,
382            tables: PackIndexViewTables::V2 {
383                oid_table,
384                crc_table,
385                small_offset_table,
386                large_offset_table,
387            },
388        };
389        if validate_entries {
390            view.validate_v2_entries()?;
391        }
392        Ok(view)
393    }
394
395    pub(crate) fn parse_v1_impl(
396        bytes: &'a [u8],
397        format: ObjectFormat,
398        verify_checksum: bool,
399        validate_entries: bool,
400    ) -> Result<Self> {
401        let hash_len = format.raw_len();
402        if bytes.len() < 256 * 4 + 2 * hash_len {
403            return Err(GitError::InvalidFormat("pack index too short".into()));
404        }
405        let index_checksum_offset = bytes.len() - hash_len;
406        let index_checksum = ObjectId::from_raw(format, &bytes[index_checksum_offset..])?;
407        if verify_checksum {
408            let actual_index_checksum =
409                sley_core::digest_bytes(format, &bytes[..index_checksum_offset])?;
410            if actual_index_checksum != index_checksum {
411                return Err(GitError::InvalidFormat(format!(
412                    "pack index checksum mismatch: expected {index_checksum}, got {actual_index_checksum}"
413                )));
414            }
415        }
416
417        let mut offset = 0usize;
418        let fanout = read_pack_index_fanout(bytes, &mut offset)?;
419        let count = fanout[255] as usize;
420        let entry_len = hash_len
421            .checked_add(4)
422            .ok_or_else(|| GitError::InvalidFormat("pack index entry length overflow".into()))?;
423        let entry_table = checked_range(offset, count, entry_len, bytes.len())?;
424        offset = entry_table.end;
425        let expected_trailer_offset = bytes.len() - hash_len * 2;
426        if offset != expected_trailer_offset {
427            return Err(GitError::InvalidFormat(format!(
428                "pack index has {} unexpected bytes before trailer",
429                expected_trailer_offset.saturating_sub(offset)
430            )));
431        }
432        let pack_checksum = ObjectId::from_raw(format, &bytes[offset..offset + hash_len])?;
433
434        let view = Self {
435            version: 1,
436            count,
437            fanout,
438            pack_checksum,
439            index_checksum,
440            bytes,
441            format,
442            tables: PackIndexViewTables::V1 { entry_table },
443        };
444        if validate_entries {
445            view.validate_v1_entries()?;
446        }
447        Ok(view)
448    }
449
450    pub(crate) fn validate_v2_entries(&self) -> Result<()> {
451        let PackIndexViewTables::V2 {
452            oid_table,
453            small_offset_table,
454            large_offset_table,
455            ..
456        } = &self.tables
457        else {
458            unreachable!("v2 validation only runs for v2 views");
459        };
460        let oid_table = self.slice(oid_table.clone());
461        let small_offset_table = self.slice(small_offset_table.clone());
462        let large_offset_table = self.slice(large_offset_table.clone());
463        let hash_len = self.format.raw_len();
464        for idx in 0..self.count {
465            let oid_start = idx * hash_len;
466            let oid_bytes = &oid_table[oid_start..oid_start + hash_len];
467            if idx > 0 && oid_bytes < &oid_table[oid_start - hash_len..oid_start] {
468                return Err(GitError::InvalidFormat(
469                    "pack index object ids are not sorted".into(),
470                ));
471            }
472            validate_pack_index_oid_fanout(idx, oid_bytes, &self.fanout)?;
473
474            let offset_start = idx * 4;
475            let raw_offset = u32_be(&small_offset_table[offset_start..offset_start + 4]);
476            pack_index_v2_offset(raw_offset, large_offset_table)?;
477        }
478        Ok(())
479    }
480
481    pub(crate) fn validate_v1_entries(&self) -> Result<()> {
482        let PackIndexViewTables::V1 { entry_table } = &self.tables else {
483            unreachable!("v1 validation only runs for v1 views");
484        };
485        let entry_table = self.slice(entry_table.clone());
486        let hash_len = self.format.raw_len();
487        let entry_len = hash_len
488            .checked_add(4)
489            .ok_or_else(|| GitError::InvalidFormat("pack index entry length overflow".into()))?;
490        for idx in 0..self.count {
491            let start = idx * entry_len;
492            let oid_start = start + 4;
493            let oid_bytes = &entry_table[oid_start..start + entry_len];
494            if idx > 0 {
495                let previous_oid_start = oid_start - entry_len;
496                let previous_oid = &entry_table[previous_oid_start..previous_oid_start + hash_len];
497                if previous_oid > oid_bytes {
498                    return Err(GitError::InvalidFormat(
499                        "pack index object ids are not sorted".into(),
500                    ));
501                }
502            }
503            validate_pack_index_oid_fanout(idx, oid_bytes, &self.fanout)?;
504        }
505        Ok(())
506    }
507
508    pub(crate) fn oid_bytes_at(&self, idx: usize) -> &'a [u8] {
509        let hash_len = self.format.raw_len();
510        match &self.tables {
511            PackIndexViewTables::V1 { entry_table } => {
512                let entry_table = self.slice(entry_table.clone());
513                let entry_len = hash_len + 4;
514                let start = idx * entry_len + 4;
515                &entry_table[start..start + hash_len]
516            }
517            PackIndexViewTables::V2 { oid_table, .. } => {
518                let oid_table = self.slice(oid_table.clone());
519                let start = idx * hash_len;
520                &oid_table[start..start + hash_len]
521            }
522        }
523    }
524
525    pub(crate) fn lookup_at(&self, idx: usize) -> Option<PackIndexLookup> {
526        if idx >= self.count {
527            return None;
528        }
529        let hash_len = self.format.raw_len();
530        match &self.tables {
531            PackIndexViewTables::V1 { entry_table } => {
532                let entry_table = self.slice(entry_table.clone());
533                let entry_len = hash_len + 4;
534                let start = idx * entry_len;
535                Some(PackIndexLookup {
536                    crc32: 0,
537                    offset: u64::from(u32_be(&entry_table[start..start + 4])),
538                })
539            }
540            PackIndexViewTables::V2 {
541                crc_table,
542                small_offset_table,
543                large_offset_table,
544                ..
545            } => {
546                let crc_table = self.slice(crc_table.clone());
547                let small_offset_table = self.slice(small_offset_table.clone());
548                let large_offset_table = self.slice(large_offset_table.clone());
549                let crc_start = idx * 4;
550                let raw_offset = u32_be(&small_offset_table[crc_start..crc_start + 4]);
551                Some(PackIndexLookup {
552                    crc32: u32_be(&crc_table[crc_start..crc_start + 4]),
553                    offset: pack_index_v2_offset(raw_offset, large_offset_table).ok()?,
554                })
555            }
556        }
557    }
558
559    pub(crate) fn slice(&self, range: Range<usize>) -> &'a [u8] {
560        &self.bytes[range]
561    }
562}
563
564impl PackIndexViewData {
565    pub fn parse(bytes: Arc<[u8]>, format: ObjectFormat) -> Result<Self> {
566        Self::parse_source(Arc::new(SharedIndexBytes(bytes)), format)
567    }
568
569    /// Parse and validate an owned index view without recomputing the trailing
570    /// index checksum. The stored checksum is still exposed via
571    /// [`PackIndexViewData::index_checksum`].
572    pub fn parse_without_checksum(bytes: Arc<[u8]>, format: ObjectFormat) -> Result<Self> {
573        Self::parse_source_without_checksum(Arc::new(SharedIndexBytes(bytes)), format)
574    }
575
576    /// Parse a local/trusted owned index view without the checksum or full-entry
577    /// validation passes.
578    pub fn parse_trusted_without_checksum(bytes: Arc<[u8]>, format: ObjectFormat) -> Result<Self> {
579        Self::parse_trusted_source_without_checksum(Arc::new(SharedIndexBytes(bytes)), format)
580    }
581
582    pub fn parse_source(bytes: Arc<dyn PackIndexByteSource>, format: ObjectFormat) -> Result<Self> {
583        Self::parse_impl(bytes, format, true, true)
584    }
585
586    pub fn parse_source_without_checksum(
587        bytes: Arc<dyn PackIndexByteSource>,
588        format: ObjectFormat,
589    ) -> Result<Self> {
590        Self::parse_impl(bytes, format, false, true)
591    }
592
593    pub fn parse_trusted_source_without_checksum(
594        bytes: Arc<dyn PackIndexByteSource>,
595        format: ObjectFormat,
596    ) -> Result<Self> {
597        Self::parse_impl(bytes, format, false, false)
598    }
599
600    pub fn count(&self) -> usize {
601        self.count
602    }
603
604    pub fn fanout(&self) -> &[u32; 256] {
605        &self.fanout
606    }
607
608    pub fn find(&self, oid: &ObjectId) -> Option<PackIndexLookup> {
609        self.as_view().find(oid)
610    }
611
612    pub fn as_view(&self) -> PackIndexView<'_> {
613        PackIndexView {
614            version: self.version,
615            count: self.count,
616            fanout: self.fanout,
617            pack_checksum: self.pack_checksum,
618            index_checksum: self.index_checksum,
619            bytes: self.bytes.as_bytes(),
620            format: self.format,
621            tables: self.tables.clone(),
622        }
623    }
624
625    /// Offset/CRC lookup for the entry at `idx` in oid-sorted pack-index order.
626    pub fn lookup_at(&self, idx: usize) -> Option<PackIndexLookup> {
627        self.as_view().lookup_at(idx)
628    }
629
630    /// The object id at `idx` in oid-sorted pack-index order.
631    pub fn oid_at(&self, idx: usize) -> Result<ObjectId> {
632        if idx >= self.count {
633            return Err(GitError::InvalidFormat(
634                "pack index position out of range".into(),
635            ));
636        }
637        ObjectId::from_raw(self.format, self.as_view().oid_bytes_at(idx))
638    }
639
640    /// Resolve a pack offset to its object id by scanning every index entry.
641    pub fn oid_at_offset_linear(&self, offset: u64) -> Option<ObjectId> {
642        let view = self.as_view();
643        for idx in 0..self.count {
644            let lookup = view.lookup_at(idx)?;
645            if lookup.offset == offset {
646                return self.oid_at(idx).ok();
647            }
648        }
649        None
650    }
651
652    pub(crate) fn parse_impl(
653        bytes: Arc<dyn PackIndexByteSource>,
654        format: ObjectFormat,
655        verify_checksum: bool,
656        validate_entries: bool,
657    ) -> Result<Self> {
658        let (version, count, fanout, pack_checksum, index_checksum, tables) = {
659            let view = PackIndexView::parse_impl(
660                bytes.as_bytes(),
661                format,
662                verify_checksum,
663                validate_entries,
664            )?;
665            (
666                view.version,
667                view.count,
668                view.fanout,
669                view.pack_checksum,
670                view.index_checksum,
671                view.tables,
672            )
673        };
674        Ok(Self {
675            version,
676            count,
677            fanout,
678            pack_checksum,
679            index_checksum,
680            bytes,
681            format,
682            tables,
683        })
684    }
685}
686
687impl PackIndex {
688    pub fn write_v2_for_pack_sha1(pack_bytes: &[u8]) -> Result<PackIndexBuild> {
689        Self::write_v2_for_pack(pack_bytes, ObjectFormat::Sha1)
690    }
691
692    pub fn write_v2_for_pack(pack_bytes: &[u8], format: ObjectFormat) -> Result<PackIndexBuild> {
693        Self::write_v2_for_pack_with_base(pack_bytes, format, |_| Ok(None))
694    }
695
696    /// Validate and index a pack while resolving ref-deltas against an external
697    /// object source. This powers `index-pack --fix-thin`; self-contained packs
698    /// use [`Self::write_v2_for_pack`].
699    pub fn write_v2_for_pack_with_base<F>(
700        pack_bytes: &[u8],
701        format: ObjectFormat,
702        mut external_base: F,
703    ) -> Result<PackIndexBuild>
704    where
705        F: FnMut(&ObjectId) -> Result<Option<EncodedObject>>,
706    {
707        let trailer_len = format.raw_len();
708        if pack_bytes.len() < 12 + trailer_len {
709            return Err(GitError::InvalidFormat("pack file too short".into()));
710        }
711        let trailer_offset = pack_bytes.len() - trailer_len;
712        let pack_checksum = sley_core::digest_bytes(format, &pack_bytes[..trailer_offset])?;
713        let expected = ObjectId::from_raw(format, &pack_bytes[trailer_offset..])?;
714        if pack_checksum != expected {
715            return Err(GitError::InvalidFormat(format!(
716                "pack checksum mismatch: expected {expected}, got {pack_checksum}"
717            )));
718        }
719
720        if &pack_bytes[..4] != b"PACK" {
721            return Err(GitError::InvalidFormat("missing PACK signature".into()));
722        }
723        let version = u32_be(&pack_bytes[4..8]);
724        if version != 2 && version != 3 {
725            return Err(GitError::Unsupported(format!("pack version {version}")));
726        }
727        let count = u32_be(&pack_bytes[8..12]) as usize;
728        let mut offset = 12usize;
729        let mut parsed_entries = Vec::with_capacity(count);
730        let mut raw_entries = Vec::with_capacity(count);
731        for _ in 0..count {
732            let entry_offset = offset;
733            let header = parse_entry_header(pack_bytes, &mut offset)?;
734            let base = match header.kind {
735                PackObjectKind::OfsDelta => Some(DeltaBase::Offset(parse_ofs_delta_base_offset(
736                    pack_bytes,
737                    &mut offset,
738                    entry_offset as u64,
739                )?)),
740                PackObjectKind::RefDelta => {
741                    let hash_len = format.raw_len();
742                    if offset + hash_len > trailer_offset {
743                        return Err(GitError::InvalidFormat(
744                            "truncated ref-delta base object id".into(),
745                        ));
746                    }
747                    let oid = ObjectId::from_raw(format, &pack_bytes[offset..offset + hash_len])?;
748                    offset += hash_len;
749                    Some(DeltaBase::Ref(oid))
750                }
751                _ => None,
752            };
753            let mut body = Vec::new();
754            let consumed = inflate_into(
755                &pack_bytes[offset..trailer_offset],
756                &mut body,
757                header.size.min(usize::MAX as u64) as usize,
758            )?;
759            if body.len() as u64 != header.size {
760                return Err(GitError::InvalidObject(format!(
761                    "pack object declared {} bytes, decoded {}",
762                    header.size,
763                    body.len()
764                )));
765            }
766            if consumed == 0 {
767                return Err(GitError::InvalidFormat(
768                    "empty compressed pack entry".into(),
769                ));
770            }
771            offset = offset
772                .checked_add(consumed)
773                .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
774            if offset > trailer_offset {
775                return Err(GitError::InvalidFormat(
776                    "pack entry extends past checksum".into(),
777                ));
778            }
779            raw_entries.push((
780                entry_offset as u64,
781                crc32fast::hash(&pack_bytes[entry_offset..offset]),
782            ));
783            if let Some(base) = base {
784                parsed_entries.push(ParsedPackEntry::Delta {
785                    base,
786                    compressed_size: consumed as u64,
787                    delta_size: header.size,
788                    offset: entry_offset as u64,
789                    delta: body,
790                });
791            } else {
792                let object_type = match header.kind {
793                    PackObjectKind::Commit => ObjectType::Commit,
794                    PackObjectKind::Tree => ObjectType::Tree,
795                    PackObjectKind::Blob => ObjectType::Blob,
796                    PackObjectKind::Tag => ObjectType::Tag,
797                    PackObjectKind::OfsDelta | PackObjectKind::RefDelta => unreachable!(),
798                };
799                let object = EncodedObject::new(object_type, body);
800                let oid = object.object_id(format)?;
801                parsed_entries.push(ParsedPackEntry::Resolved(PackObject {
802                    entry: PackEntry {
803                        oid,
804                        compressed_size: consumed as u64,
805                        uncompressed_size: header.size,
806                        offset: entry_offset as u64,
807                    },
808                    object,
809                }));
810            }
811        }
812        if offset != trailer_offset {
813            return Err(GitError::InvalidFormat(format!(
814                "pack has {} trailing bytes before checksum",
815                trailer_offset - offset
816            )));
817        }
818
819        let resolved = resolve_pack_entries(parsed_entries, format, &mut external_base)?;
820        let entries = resolved
821            .iter()
822            .zip(raw_entries)
823            .map(|(object, (offset, crc32))| PackIndexEntry {
824                oid: object.entry.oid,
825                crc32,
826                offset,
827            })
828            .collect::<Vec<_>>();
829        let index = PackIndex::write_v2(format, &entries, &pack_checksum)?;
830        Ok(PackIndexBuild {
831            index,
832            pack_checksum,
833            entries,
834        })
835    }
836
837    /// Validate and index a pack from the reader's current position to EOF.
838    ///
839    /// This produces the same v2 `.idx` bytes and object metadata as
840    /// [`PackIndex::write_v2_for_pack`] without requiring the caller to provide
841    /// the pack as one contiguous byte slice. The reader is left positioned at
842    /// EOF on success.
843    pub fn write_v2_for_pack_reader<R>(
844        reader: &mut R,
845        format: ObjectFormat,
846    ) -> Result<PackStreamIndexBuild>
847    where
848        R: Read + Seek,
849    {
850        let start = reader.stream_position()?;
851        let end = reader.seek(SeekFrom::End(0))?;
852        let pack_len = end
853            .checked_sub(start)
854            .ok_or_else(|| GitError::InvalidFormat("pack stream position overflow".into()))?;
855        reader.seek(SeekFrom::Start(start))?;
856        index_pack_from_reader(reader, format, pack_len)
857    }
858
859    /// Validate and index a pack from the reader's current position, stopping
860    /// after the pack trailer checksum.
861    ///
862    /// This is for transports where the pack length is not known in advance but
863    /// the stream is expected to contain exactly one pack. It avoids forcing the
864    /// caller to first materialize the pack only to learn its length.
865    pub fn write_v2_for_pack_reader_to_trailer<R>(
866        reader: &mut R,
867        format: ObjectFormat,
868    ) -> Result<PackStreamIndexBuild>
869    where
870        R: Read,
871    {
872        index_pack_from_reader_to_trailer(reader, format)
873    }
874
875    /// `write_v2_for_pack_reader_to_trailer` that reports streaming pack
876    /// progress. `progress` is invoked on a throttled cadence with the pack byte
877    /// offset, objects parsed so far, and the header-declared total, then once
878    /// more at completion with `received_objects == total_objects`.
879    pub fn write_v2_for_pack_reader_to_trailer_with_progress<R, F>(
880        reader: &mut R,
881        format: ObjectFormat,
882        progress: F,
883    ) -> Result<PackStreamIndexBuild>
884    where
885        R: Read,
886        F: FnMut(PackStreamProgress),
887    {
888        index_pack_from_reader_to_trailer_with_progress(reader, format, progress)
889    }
890
891    pub fn write_v2_for_pack_reader_with_len<R>(
892        reader: &mut R,
893        format: ObjectFormat,
894        pack_len: u64,
895    ) -> Result<PackStreamIndexBuild>
896    where
897        R: Read,
898    {
899        index_pack_from_reader(reader, format, pack_len)
900    }
901
902    /// Validate and index a pack from a filesystem path without loading the
903    /// entire pack file into memory.
904    pub fn write_v2_for_pack_path(
905        path: impl AsRef<Path>,
906        format: ObjectFormat,
907    ) -> Result<PackStreamIndexBuild> {
908        let mut file = File::open(path)?;
909        Self::write_v2_for_pack_reader(&mut file, format)
910    }
911
912    pub fn parse_v2_sha1(bytes: &[u8]) -> Result<Self> {
913        Self::parse(bytes, ObjectFormat::Sha1)
914    }
915
916    pub fn parse(bytes: &[u8], format: ObjectFormat) -> Result<Self> {
917        Self::parse_impl(bytes, format, true)
918    }
919
920    pub fn parse_without_checksum(bytes: &[u8], format: ObjectFormat) -> Result<Self> {
921        Self::parse_impl(bytes, format, false)
922    }
923
924    pub(crate) fn parse_impl(
925        bytes: &[u8],
926        format: ObjectFormat,
927        verify_checksum: bool,
928    ) -> Result<Self> {
929        let hash_len = format.raw_len();
930        if bytes.len() < 4 {
931            return Err(GitError::InvalidFormat("pack index too short".into()));
932        }
933        if bytes[..4] != [0xff, b't', b'O', b'c'] {
934            return Self::parse_v1_impl(bytes, format, verify_checksum);
935        }
936        if bytes.len() < 8 + 256 * 4 + 2 * hash_len {
937            return Err(GitError::InvalidFormat("pack index too short".into()));
938        }
939        let version = u32_be(&bytes[4..8]);
940        if version != 2 {
941            return Err(GitError::Unsupported(format!(
942                "pack index version {version}"
943            )));
944        }
945        let index_checksum_offset = bytes.len() - hash_len;
946        let index_checksum = ObjectId::from_raw(format, &bytes[index_checksum_offset..])?;
947        if verify_checksum {
948            let actual_index_checksum =
949                sley_core::digest_bytes(format, &bytes[..index_checksum_offset])?;
950            if actual_index_checksum != index_checksum {
951                return Err(GitError::InvalidFormat(format!(
952                    "pack index checksum mismatch: expected {index_checksum}, got {actual_index_checksum}"
953                )));
954            }
955        }
956
957        let mut offset = 8usize;
958        let mut fanout = [0u32; 256];
959        let mut previous = 0u32;
960        for slot in &mut fanout {
961            *slot = u32_be(&bytes[offset..offset + 4]);
962            if *slot < previous {
963                return Err(GitError::InvalidFormat(
964                    "pack index fanout is not monotonic".into(),
965                ));
966            }
967            previous = *slot;
968            offset += 4;
969        }
970        let count = fanout[255] as usize;
971        let oid_table = checked_range(offset, count, hash_len, bytes.len())?;
972        offset = oid_table.end;
973        let crc_table = checked_range(offset, count, 4, bytes.len())?;
974        offset = crc_table.end;
975        let small_offset_table = checked_range(offset, count, 4, bytes.len())?;
976        offset = small_offset_table.end;
977
978        let large_offset_count = (0..count)
979            .filter(|idx| {
980                let start = small_offset_table.start + idx * 4;
981                u32_be(&bytes[start..start + 4]) & 0x8000_0000 != 0
982            })
983            .count();
984        let mut large_offset_table = checked_range(offset, large_offset_count, 8, bytes.len())?;
985        offset = large_offset_table.end;
986
987        let expected_trailer_offset = bytes.len() - hash_len * 2;
988        if offset != expected_trailer_offset {
989            if !verify_checksum && offset < expected_trailer_offset {
990                large_offset_table = large_offset_table.start..expected_trailer_offset;
991                offset = expected_trailer_offset;
992            } else {
993                return Err(GitError::InvalidFormat(format!(
994                    "pack index has {} unexpected bytes before trailer",
995                    expected_trailer_offset.saturating_sub(offset)
996                )));
997            }
998        }
999        let pack_checksum = ObjectId::from_raw(format, &bytes[offset..offset + hash_len])?;
1000
1001        let mut entries = Vec::with_capacity(count);
1002        for idx in 0..count {
1003            let oid_start = oid_table.start + idx * hash_len;
1004            let crc_start = crc_table.start + idx * 4;
1005            let offset_start = small_offset_table.start + idx * 4;
1006            let oid_bytes = &bytes[oid_start..oid_start + hash_len];
1007            // Object ids must be non-decreasing: Git permits duplicate objects
1008            // in an incoming pack, and represents every copy in the index. The
1009            // fanout must still match the first byte so binary search cannot
1010            // silently miss the duplicate run.
1011            if idx > 0 && oid_bytes < &bytes[oid_start - hash_len..oid_start] {
1012                return Err(GitError::InvalidFormat(
1013                    "pack index object ids are not sorted".into(),
1014                ));
1015            }
1016            let expected_min = if oid_bytes[0] == 0 {
1017                0
1018            } else {
1019                fanout[usize::from(oid_bytes[0] - 1)]
1020            };
1021            if (idx as u32) < expected_min || (idx as u32) >= fanout[usize::from(oid_bytes[0])] {
1022                return Err(GitError::InvalidFormat(
1023                    "pack index object id is outside its fanout bucket".into(),
1024                ));
1025            }
1026            let raw_offset = u32_be(&bytes[offset_start..offset_start + 4]);
1027            let offset = if raw_offset & 0x8000_0000 == 0 {
1028                u64::from(raw_offset)
1029            } else {
1030                let large_idx = (raw_offset & 0x7fff_ffff) as usize;
1031                let large_start = large_offset_table.start + large_idx * 8;
1032                if large_idx >= large_offset_table.len() / 8 {
1033                    return Err(GitError::InvalidFormat(
1034                        "pack index large offset points past table".into(),
1035                    ));
1036                }
1037                u64_be(&bytes[large_start..large_start + 8])
1038            };
1039            entries.push(PackIndexEntry {
1040                oid: ObjectId::from_raw(format, oid_bytes)?,
1041                crc32: u32_be(&bytes[crc_start..crc_start + 4]),
1042                offset,
1043            });
1044        }
1045        Ok(Self {
1046            version,
1047            fanout,
1048            entries,
1049            pack_checksum,
1050            index_checksum,
1051        })
1052    }
1053
1054    pub(crate) fn parse_v1_impl(
1055        bytes: &[u8],
1056        format: ObjectFormat,
1057        verify_checksum: bool,
1058    ) -> Result<Self> {
1059        let hash_len = format.raw_len();
1060        if bytes.len() < 256 * 4 + 2 * hash_len {
1061            return Err(GitError::InvalidFormat("pack index too short".into()));
1062        }
1063        let index_checksum_offset = bytes.len() - hash_len;
1064        let index_checksum = ObjectId::from_raw(format, &bytes[index_checksum_offset..])?;
1065        if verify_checksum {
1066            let actual_index_checksum =
1067                sley_core::digest_bytes(format, &bytes[..index_checksum_offset])?;
1068            if actual_index_checksum != index_checksum {
1069                return Err(GitError::InvalidFormat(format!(
1070                    "pack index checksum mismatch: expected {index_checksum}, got {actual_index_checksum}"
1071                )));
1072            }
1073        }
1074
1075        let mut offset = 0usize;
1076        let mut fanout = [0u32; 256];
1077        let mut previous = 0u32;
1078        for slot in &mut fanout {
1079            *slot = u32_be(&bytes[offset..offset + 4]);
1080            if *slot < previous {
1081                return Err(GitError::InvalidFormat(
1082                    "pack index fanout is not monotonic".into(),
1083                ));
1084            }
1085            previous = *slot;
1086            offset += 4;
1087        }
1088        let count = fanout[255] as usize;
1089        let entry_len = hash_len
1090            .checked_add(4)
1091            .ok_or_else(|| GitError::InvalidFormat("pack index entry length overflow".into()))?;
1092        let entry_table = checked_range(offset, count, entry_len, bytes.len())?;
1093        offset = entry_table.end;
1094        let expected_trailer_offset = bytes.len() - hash_len * 2;
1095        if offset != expected_trailer_offset {
1096            return Err(GitError::InvalidFormat(format!(
1097                "pack index has {} unexpected bytes before trailer",
1098                expected_trailer_offset.saturating_sub(offset)
1099            )));
1100        }
1101        let pack_checksum = ObjectId::from_raw(format, &bytes[offset..offset + hash_len])?;
1102
1103        let mut entries = Vec::with_capacity(count);
1104        let mut previous_oid: Option<ObjectId> = None;
1105        for idx in 0..count {
1106            let start = entry_table.start + idx * entry_len;
1107            let oid = ObjectId::from_raw(format, &bytes[start + 4..start + entry_len])?;
1108            if let Some(previous) = &previous_oid
1109                && previous.as_bytes() > oid.as_bytes()
1110            {
1111                return Err(GitError::InvalidFormat(
1112                    "pack index object ids are not sorted".into(),
1113                ));
1114            }
1115            previous_oid = Some(oid);
1116            entries.push(PackIndexEntry {
1117                oid,
1118                crc32: 0,
1119                offset: u64::from(u32_be(&bytes[start..start + 4])),
1120            });
1121        }
1122        Ok(Self {
1123            version: 1,
1124            fanout,
1125            entries,
1126            pack_checksum,
1127            index_checksum,
1128        })
1129    }
1130
1131    pub fn find(&self, oid: &ObjectId) -> Option<&PackIndexEntry> {
1132        self.entries
1133            .binary_search_by(|entry| entry.oid.as_bytes().cmp(oid.as_bytes()))
1134            .ok()
1135            .map(|idx| &self.entries[idx])
1136    }
1137
1138    pub fn write_v2_sha1(entries: &[PackIndexEntry], pack_checksum: &ObjectId) -> Result<Vec<u8>> {
1139        Self::write_v2(ObjectFormat::Sha1, entries, pack_checksum)
1140    }
1141
1142    pub fn write_v2(
1143        format: ObjectFormat,
1144        entries: &[PackIndexEntry],
1145        pack_checksum: &ObjectId,
1146    ) -> Result<Vec<u8>> {
1147        if pack_checksum.format() != format {
1148            return Err(GitError::InvalidObjectId(
1149                "pack checksum format does not match index format".into(),
1150            ));
1151        }
1152        let mut entries = entries.iter().collect::<Vec<_>>();
1153        entries.sort_by(|left, right| left.oid.as_bytes().cmp(right.oid.as_bytes()));
1154        let mut fanout = [0u32; 256];
1155        for entry in &entries {
1156            if entry.oid.format() != format {
1157                return Err(GitError::InvalidObjectId(
1158                    "pack index entry format does not match index format".into(),
1159                ));
1160            }
1161            let first = entry.oid.as_bytes()[0] as usize;
1162            fanout[first] = fanout[first]
1163                .checked_add(1)
1164                .ok_or_else(|| GitError::InvalidFormat("pack index fanout overflow".into()))?;
1165        }
1166        let mut running = 0u32;
1167        for slot in &mut fanout {
1168            running = running
1169                .checked_add(*slot)
1170                .ok_or_else(|| GitError::InvalidFormat("pack index fanout overflow".into()))?;
1171            *slot = running;
1172        }
1173
1174        let mut index = Vec::new();
1175        index.extend_from_slice(&[0xff, b't', b'O', b'c']);
1176        index.extend_from_slice(&2u32.to_be_bytes());
1177        for count in fanout {
1178            index.extend_from_slice(&count.to_be_bytes());
1179        }
1180        for entry in &entries {
1181            index.extend_from_slice(entry.oid.as_bytes());
1182        }
1183        for entry in &entries {
1184            index.extend_from_slice(&entry.crc32.to_be_bytes());
1185        }
1186
1187        let mut large_offsets = Vec::new();
1188        for entry in &entries {
1189            if entry.offset < 0x8000_0000 {
1190                index.extend_from_slice(&(entry.offset as u32).to_be_bytes());
1191            } else {
1192                if large_offsets.len() > 0x7fff_ffff {
1193                    return Err(GitError::InvalidFormat(
1194                        "too many large pack offsets".into(),
1195                    ));
1196                }
1197                let large_idx = large_offsets.len() as u32;
1198                index.extend_from_slice(&(0x8000_0000 | large_idx).to_be_bytes());
1199                large_offsets.push(entry.offset);
1200            }
1201        }
1202        for offset in large_offsets {
1203            index.extend_from_slice(&offset.to_be_bytes());
1204        }
1205        index.extend_from_slice(pack_checksum.as_bytes());
1206        let index_checksum = sley_core::digest_bytes(format, &index)?;
1207        index.extend_from_slice(index_checksum.as_bytes());
1208        Ok(index)
1209    }
1210
1211    /// Serialise a version-1 pack `.idx`: a 256-entry fanout, then for each
1212    /// object an inline 4-byte big-endian pack offset immediately followed by
1213    /// its object id (sorted by oid), then the pack checksum and a trailing
1214    /// index checksum. v1 has no CRC table and cannot represent offsets that
1215    /// do not fit in 32 bits.
1216    pub fn write_v1(
1217        format: ObjectFormat,
1218        entries: &[PackIndexEntry],
1219        pack_checksum: &ObjectId,
1220    ) -> Result<Vec<u8>> {
1221        if pack_checksum.format() != format {
1222            return Err(GitError::InvalidObjectId(
1223                "pack checksum format does not match index format".into(),
1224            ));
1225        }
1226        let mut entries = entries.iter().collect::<Vec<_>>();
1227        entries.sort_by(|left, right| left.oid.as_bytes().cmp(right.oid.as_bytes()));
1228        let mut fanout = [0u32; 256];
1229        for entry in &entries {
1230            if entry.oid.format() != format {
1231                return Err(GitError::InvalidObjectId(
1232                    "pack index entry format does not match index format".into(),
1233                ));
1234            }
1235            if entry.offset > 0xffff_ffff {
1236                return Err(GitError::InvalidFormat(
1237                    "pack offset too large for a version-1 index".into(),
1238                ));
1239            }
1240            let first = entry.oid.as_bytes()[0] as usize;
1241            fanout[first] = fanout[first]
1242                .checked_add(1)
1243                .ok_or_else(|| GitError::InvalidFormat("pack index fanout overflow".into()))?;
1244        }
1245        let mut running = 0u32;
1246        for slot in &mut fanout {
1247            running = running
1248                .checked_add(*slot)
1249                .ok_or_else(|| GitError::InvalidFormat("pack index fanout overflow".into()))?;
1250            *slot = running;
1251        }
1252
1253        let mut index = Vec::new();
1254        for count in fanout {
1255            index.extend_from_slice(&count.to_be_bytes());
1256        }
1257        for entry in &entries {
1258            index.extend_from_slice(&(entry.offset as u32).to_be_bytes());
1259            index.extend_from_slice(entry.oid.as_bytes());
1260        }
1261        index.extend_from_slice(pack_checksum.as_bytes());
1262        let index_checksum = sley_core::digest_bytes(format, &index)?;
1263        index.extend_from_slice(index_checksum.as_bytes());
1264        Ok(index)
1265    }
1266}
1267/// The `.rev` table for a pack: index positions (the rank of each object in
1268/// the oid-sorted `.idx`) listed in pack order (ascending pack offset), as
1269/// upstream `write_rev_file` lays them out. Accepts `entries` in any order;
1270/// the result feeds [`PackReverseIndex::write`].
1271pub fn pack_order_index_positions(entries: &[PackIndexEntry]) -> Vec<u32> {
1272    let mut oid_sorted: Vec<usize> = (0..entries.len()).collect();
1273    oid_sorted.sort_by(|&a, &b| entries[a].oid.as_bytes().cmp(entries[b].oid.as_bytes()));
1274    let mut index_position = vec![0u32; entries.len()];
1275    for (position, &entry) in oid_sorted.iter().enumerate() {
1276        index_position[entry] = position as u32;
1277    }
1278    let mut by_offset: Vec<usize> = (0..entries.len()).collect();
1279    by_offset.sort_by_key(|&entry| entries[entry].offset);
1280    by_offset
1281        .into_iter()
1282        .map(|entry| index_position[entry])
1283        .collect()
1284}
1285
1286impl PackReverseIndex {
1287    pub fn write(
1288        format: ObjectFormat,
1289        positions: &[u32],
1290        pack_checksum: &ObjectId,
1291    ) -> Result<Vec<u8>> {
1292        if pack_checksum.format() != format {
1293            return Err(GitError::InvalidObjectId(
1294                "pack checksum format does not match reverse index format".into(),
1295            ));
1296        }
1297        validate_position_permutation(positions)?;
1298
1299        let mut out = Vec::new();
1300        out.extend_from_slice(b"RIDX");
1301        out.extend_from_slice(&1u32.to_be_bytes());
1302        out.extend_from_slice(&hash_function_id(format).to_be_bytes());
1303        for position in positions {
1304            out.extend_from_slice(&position.to_be_bytes());
1305        }
1306        out.extend_from_slice(pack_checksum.as_bytes());
1307        let checksum = sley_core::digest_bytes(format, &out)?;
1308        out.extend_from_slice(checksum.as_bytes());
1309        Ok(out)
1310    }
1311
1312    pub fn parse(bytes: &[u8], format: ObjectFormat, object_count: usize) -> Result<Self> {
1313        let hash_len = format.raw_len();
1314        let table_len = object_count
1315            .checked_mul(4)
1316            .ok_or_else(|| GitError::InvalidFormat("reverse index table overflow".into()))?;
1317        let min_len = 12usize
1318            .checked_add(table_len)
1319            .and_then(|len| len.checked_add(hash_len * 2))
1320            .ok_or_else(|| GitError::InvalidFormat("reverse index length overflow".into()))?;
1321        if bytes.len() < min_len {
1322            return Err(GitError::InvalidFormat("reverse index too short".into()));
1323        }
1324        if bytes.len() != min_len {
1325            return Err(GitError::InvalidFormat(format!(
1326                "reverse index has {} trailing bytes",
1327                bytes.len() - min_len
1328            )));
1329        }
1330        if &bytes[..4] != b"RIDX" {
1331            return Err(GitError::InvalidFormat("unknown signature".into()));
1332        }
1333        let version = u32_be(&bytes[4..8]);
1334        if version != 1 {
1335            return Err(GitError::InvalidFormat(format!(
1336                "unsupported version {version}"
1337            )));
1338        }
1339        let hash_id = u32_be(&bytes[8..12]);
1340        if hash_id != hash_function_id(format) {
1341            return Err(GitError::InvalidFormat(format!(
1342                "unsupported hash id {hash_id}"
1343            )));
1344        }
1345
1346        let index_checksum_offset = bytes.len() - hash_len;
1347        let pack_checksum_offset = index_checksum_offset - hash_len;
1348        let pack_checksum =
1349            ObjectId::from_raw(format, &bytes[pack_checksum_offset..index_checksum_offset])?;
1350        let mut positions = Vec::with_capacity(object_count);
1351        let mut offset = 12usize;
1352        for _ in 0..object_count {
1353            let position = u32_be(&bytes[offset..offset + 4]);
1354            positions.push(position);
1355            offset += 4;
1356        }
1357        validate_position_permutation(&positions)?;
1358
1359        // Structural validation intentionally precedes the checksum: fsck
1360        // reports a corrupted table row as an invalid rev-index position.
1361        let actual_index_checksum =
1362            sley_core::digest_bytes(format, &bytes[..index_checksum_offset])?;
1363        let index_checksum = ObjectId::from_raw(format, &bytes[index_checksum_offset..])?;
1364        if actual_index_checksum != index_checksum {
1365            return Err(GitError::InvalidFormat("invalid checksum".into()));
1366        }
1367
1368        Ok(Self {
1369            version,
1370            format,
1371            positions,
1372            pack_checksum,
1373            index_checksum,
1374        })
1375    }
1376
1377    /// Resolve a pack offset to its object id using this reverse index.
1378    ///
1379    /// `positions` are listed in pack offset order; each value names the
1380    /// oid-sorted index position for that object. When the reverse index's pack
1381    /// checksum does not match `index`, returns `None`.
1382    pub fn oid_at_offset(&self, index: &PackIndexViewData, offset: u64) -> Option<ObjectId> {
1383        if self.pack_checksum != index.pack_checksum {
1384            return None;
1385        }
1386        let view = index.as_view();
1387        let positions = &self.positions;
1388        let mut lo = 0usize;
1389        let mut hi = positions.len();
1390        while lo < hi {
1391            let mid = lo + (hi - lo) / 2;
1392            let idx_pos = positions[mid] as usize;
1393            let entry_offset = view.lookup_at(idx_pos)?.offset;
1394            if entry_offset < offset {
1395                lo = mid + 1;
1396            } else if entry_offset > offset {
1397                hi = mid;
1398            } else {
1399                return index.oid_at(idx_pos).ok();
1400            }
1401        }
1402        None
1403    }
1404}
1405
1406impl PackMtimes {
1407    pub fn write(
1408        format: ObjectFormat,
1409        mtimes: &[u32],
1410        pack_checksum: &ObjectId,
1411    ) -> Result<Vec<u8>> {
1412        if pack_checksum.format() != format {
1413            return Err(GitError::InvalidObjectId(
1414                "pack checksum format does not match mtimes format".into(),
1415            ));
1416        }
1417
1418        let mut out = Vec::new();
1419        out.extend_from_slice(b"MTME");
1420        out.extend_from_slice(&1u32.to_be_bytes());
1421        out.extend_from_slice(&hash_function_id(format).to_be_bytes());
1422        for mtime in mtimes {
1423            out.extend_from_slice(&mtime.to_be_bytes());
1424        }
1425        out.extend_from_slice(pack_checksum.as_bytes());
1426        let checksum = sley_core::digest_bytes(format, &out)?;
1427        out.extend_from_slice(checksum.as_bytes());
1428        Ok(out)
1429    }
1430
1431    pub fn parse(bytes: &[u8], format: ObjectFormat, object_count: usize) -> Result<Self> {
1432        let hash_len = format.raw_len();
1433        let table_len = object_count
1434            .checked_mul(4)
1435            .ok_or_else(|| GitError::InvalidFormat("mtimes table overflow".into()))?;
1436        let expected_len = 12usize
1437            .checked_add(table_len)
1438            .and_then(|len| len.checked_add(hash_len * 2))
1439            .ok_or_else(|| GitError::InvalidFormat("mtimes length overflow".into()))?;
1440        if bytes.len() < expected_len {
1441            return Err(GitError::InvalidFormat("mtimes file too short".into()));
1442        }
1443        if bytes.len() != expected_len {
1444            return Err(GitError::InvalidFormat(format!(
1445                "mtimes file has {} trailing bytes",
1446                bytes.len() - expected_len
1447            )));
1448        }
1449        if &bytes[..4] != b"MTME" {
1450            return Err(GitError::InvalidFormat("missing mtimes signature".into()));
1451        }
1452        let version = u32_be(&bytes[4..8]);
1453        if version != 1 {
1454            return Err(GitError::Unsupported(format!("mtimes version {version}")));
1455        }
1456        let hash_id = u32_be(&bytes[8..12]);
1457        if hash_id != hash_function_id(format) {
1458            return Err(GitError::InvalidFormat(format!(
1459                "mtimes hash id {hash_id} does not match {}",
1460                format.name()
1461            )));
1462        }
1463
1464        let index_checksum_offset = bytes.len() - hash_len;
1465        let actual_index_checksum =
1466            sley_core::digest_bytes(format, &bytes[..index_checksum_offset])?;
1467        let index_checksum = ObjectId::from_raw(format, &bytes[index_checksum_offset..])?;
1468        if actual_index_checksum != index_checksum {
1469            return Err(GitError::InvalidFormat(format!(
1470                "mtimes checksum mismatch: expected {index_checksum}, got {actual_index_checksum}"
1471            )));
1472        }
1473
1474        let pack_checksum_offset = index_checksum_offset - hash_len;
1475        let pack_checksum =
1476            ObjectId::from_raw(format, &bytes[pack_checksum_offset..index_checksum_offset])?;
1477        let mut mtimes = Vec::with_capacity(object_count);
1478        let mut offset = 12usize;
1479        for _ in 0..object_count {
1480            mtimes.push(u32_be(&bytes[offset..offset + 4]));
1481            offset += 4;
1482        }
1483
1484        Ok(Self {
1485            version,
1486            format,
1487            mtimes,
1488            pack_checksum,
1489            index_checksum,
1490        })
1491    }
1492}
1493
1494impl PackBitmapIndex {
1495    pub const OPTION_FULL_DAG: u16 = 0x0001;
1496    pub const OPTION_HASH_CACHE: u16 = 0x0004;
1497    pub const OPTION_LOOKUP_TABLE: u16 = 0x0010;
1498    pub const OPTION_PSEUDO_MERGES: u16 = 0x0020;
1499
1500    pub fn parse(bytes: &[u8], format: ObjectFormat, object_count: usize) -> Result<Self> {
1501        let hash_len = format.raw_len();
1502        let min_len = 12usize
1503            .checked_add(hash_len * 2)
1504            .ok_or_else(|| GitError::InvalidFormat("bitmap index length overflow".into()))?;
1505        if bytes.len() < min_len {
1506            return Err(GitError::InvalidFormat("bitmap index too short".into()));
1507        }
1508        if &bytes[..4] != b"BITM" {
1509            return Err(GitError::InvalidFormat(
1510                "missing bitmap index signature".into(),
1511            ));
1512        }
1513        let version = u16_be(&bytes[4..6]);
1514        if version != 1 {
1515            return Err(GitError::Unsupported(format!(
1516                "bitmap index version {version}"
1517            )));
1518        }
1519        let options = u16_be(&bytes[6..8]);
1520        let known_options = Self::OPTION_FULL_DAG
1521            | Self::OPTION_HASH_CACHE
1522            | Self::OPTION_LOOKUP_TABLE
1523            | Self::OPTION_PSEUDO_MERGES;
1524        if options & !known_options != 0 {
1525            return Err(GitError::Unsupported(format!(
1526                "bitmap index options {:#06x}",
1527                options & !known_options
1528            )));
1529        }
1530        let entry_count = u32_be(&bytes[8..12]) as usize;
1531        let checksum_offset = bytes.len() - hash_len;
1532        let index_checksum = ObjectId::from_raw(format, &bytes[checksum_offset..])?;
1533        let mut extras_end = checksum_offset;
1534        let hash_cache_range = if options & Self::OPTION_HASH_CACHE != 0 {
1535            let cache_len = object_count
1536                .checked_mul(4)
1537                .ok_or_else(|| GitError::InvalidFormat("bitmap hash cache overflow".into()))?;
1538            if cache_len > extras_end {
1539                return Err(GitError::InvalidFormat(
1540                    "truncated bitmap hash cache".into(),
1541                ));
1542            }
1543            extras_end -= cache_len;
1544            Some(extras_end..extras_end + cache_len)
1545        } else {
1546            None
1547        };
1548        let lookup_table = options & Self::OPTION_LOOKUP_TABLE != 0;
1549        let lookup_table_range = if lookup_table {
1550            let table_len = entry_count
1551                .checked_mul(16)
1552                .ok_or_else(|| GitError::InvalidFormat("bitmap lookup table overflow".into()))?;
1553            if table_len > extras_end {
1554                return Err(GitError::InvalidFormat(
1555                    "truncated bitmap lookup table".into(),
1556                ));
1557            }
1558            extras_end -= table_len;
1559            Some(extras_end..extras_end + table_len)
1560        } else {
1561            None
1562        };
1563        let pseudo_merge_range = if options & Self::OPTION_PSEUDO_MERGES != 0 {
1564            if extras_end < 24 {
1565                return Err(GitError::InvalidFormat(
1566                    "truncated bitmap pseudo-merge extension".into(),
1567                ));
1568            }
1569            let extension_size = u64_be(&bytes[extras_end - 8..extras_end]) as usize;
1570            if extension_size > extras_end {
1571                return Err(GitError::InvalidFormat(
1572                    "bitmap pseudo-merge extension points before file start".into(),
1573                ));
1574            }
1575            let start = extras_end - extension_size;
1576            Some(start..extras_end)
1577        } else {
1578            None
1579        };
1580        let entries_end = pseudo_merge_range
1581            .as_ref()
1582            .map(|range| range.start)
1583            .unwrap_or(extras_end);
1584
1585        let pack_checksum_end = 12usize
1586            .checked_add(hash_len)
1587            .ok_or_else(|| GitError::InvalidFormat("bitmap index length overflow".into()))?;
1588        let pack_checksum = ObjectId::from_raw(format, &bytes[12..pack_checksum_end])?;
1589        let mut offset = pack_checksum_end;
1590        let commits = parse_bitmap_ewah(bytes, &mut offset, entries_end, object_count)?;
1591        let trees = parse_bitmap_ewah(bytes, &mut offset, entries_end, object_count)?;
1592        let blobs = parse_bitmap_ewah(bytes, &mut offset, entries_end, object_count)?;
1593        let tags = parse_bitmap_ewah(bytes, &mut offset, entries_end, object_count)?;
1594
1595        let mut entries = Vec::with_capacity(entry_count);
1596        for idx in 0..entry_count {
1597            if entries_end.saturating_sub(offset) < 6 {
1598                return Err(GitError::InvalidFormat(
1599                    "truncated bitmap index entry".into(),
1600                ));
1601            }
1602            let object_position = u32_be(&bytes[offset..offset + 4]);
1603            offset += 4;
1604            if object_position as usize >= object_count {
1605                return Err(GitError::InvalidFormat(
1606                    "bitmap index entry points past object table".into(),
1607                ));
1608            }
1609            let xor_offset = bytes[offset];
1610            offset += 1;
1611            if xor_offset as usize > idx || xor_offset > 160 {
1612                return Err(GitError::InvalidFormat(
1613                    "bitmap index entry has invalid XOR offset".into(),
1614                ));
1615            }
1616            let flags = bytes[offset];
1617            offset += 1;
1618            let bitmap = parse_bitmap_ewah(bytes, &mut offset, entries_end, object_count)?;
1619            entries.push(PackBitmapEntry {
1620                object_position,
1621                xor_offset,
1622                flags,
1623                bitmap,
1624            });
1625        }
1626
1627        if offset != entries_end {
1628            return Err(GitError::InvalidFormat(format!(
1629                "bitmap index has {} trailing entry bytes",
1630                entries_end - offset
1631            )));
1632        }
1633
1634        let pseudo_merges = if let Some(range) = pseudo_merge_range {
1635            parse_bitmap_pseudo_merges(bytes, range, object_count)?
1636        } else {
1637            Vec::new()
1638        };
1639
1640        let name_hash_cache = if let Some(range) = hash_cache_range {
1641            let mut cache = Vec::with_capacity(object_count);
1642            let mut offset = range.start;
1643            for _ in 0..object_count {
1644                cache.push(u32_be(&bytes[offset..offset + 4]));
1645                offset += 4;
1646            }
1647            Some(cache)
1648        } else {
1649            None
1650        };
1651        if let Some(range) = lookup_table_range {
1652            for row in bytes[range].chunks_exact(16) {
1653                let commit_position = u32_be(&row[..4]);
1654                let entry_offset = u64_be(&row[4..12]);
1655                let xor_row = u32_be(&row[12..16]);
1656                if commit_position as usize >= object_count
1657                    || entry_offset as usize >= entries_end
1658                    || (xor_row != u32::MAX && xor_row as usize >= entry_count)
1659                {
1660                    return Err(GitError::InvalidFormat(
1661                        "corrupt bitmap lookup table".into(),
1662                    ));
1663                }
1664            }
1665        }
1666
1667        let actual_index_checksum = sley_core::digest_bytes(format, &bytes[..checksum_offset])?;
1668        if actual_index_checksum != index_checksum {
1669            return Err(GitError::InvalidFormat(format!(
1670                "bitmap index checksum mismatch: expected {index_checksum}, got {actual_index_checksum}"
1671            )));
1672        }
1673
1674        Ok(Self {
1675            version,
1676            format,
1677            options,
1678            pack_checksum,
1679            index_checksum,
1680            type_bitmaps: PackBitmapTypeBitmaps {
1681                commits,
1682                trees,
1683                blobs,
1684                tags,
1685            },
1686            entries,
1687            pseudo_merges,
1688            lookup_table,
1689            name_hash_cache,
1690        })
1691    }
1692
1693    /// Looks up the stored entry whose commit sits at `position` in the
1694    /// oid-sorted pack index (`.idx` order; see [`PackBitmapEntry::object_position`]).
1695    pub fn entry_for_index_position(&self, position: u32) -> Option<&PackBitmapEntry> {
1696        self.entries
1697            .iter()
1698            .find(|entry| entry.object_position == position)
1699    }
1700}
1701
1702pub(crate) fn parse_bitmap_pseudo_merges(
1703    bytes: &[u8],
1704    range: std::ops::Range<usize>,
1705    object_count: usize,
1706) -> Result<Vec<PackBitmapPseudoMerge>> {
1707    if range.end < range.start || range.end > bytes.len() || range.end - range.start < 24 {
1708        return Err(GitError::InvalidFormat(
1709            "truncated bitmap pseudo-merge extension".into(),
1710        ));
1711    }
1712    let trailer_start = range.end - 24;
1713    let pseudo_merge_count = u32_be(&bytes[trailer_start..trailer_start + 4]) as usize;
1714    let commit_count = u32_be(&bytes[trailer_start + 4..trailer_start + 8]) as usize;
1715    let lookup_offset = u64_be(&bytes[trailer_start + 8..trailer_start + 16]) as usize;
1716    let extension_size = u64_be(&bytes[trailer_start + 16..trailer_start + 24]) as usize;
1717    if extension_size != range.end - range.start {
1718        return Err(GitError::InvalidFormat(
1719            "bitmap pseudo-merge extension size mismatch".into(),
1720        ));
1721    }
1722    let lookup_start = range
1723        .start
1724        .checked_add(lookup_offset)
1725        .ok_or_else(|| GitError::InvalidFormat("bitmap pseudo-merge lookup overflow".into()))?;
1726    if lookup_start > trailer_start {
1727        return Err(GitError::InvalidFormat(
1728            "bitmap pseudo-merge lookup points past extension".into(),
1729        ));
1730    }
1731    let lookup_len = commit_count
1732        .checked_mul(12)
1733        .ok_or_else(|| GitError::InvalidFormat("bitmap pseudo-merge lookup overflow".into()))?;
1734    if lookup_start
1735        .checked_add(lookup_len)
1736        .is_none_or(|end| end > trailer_start)
1737    {
1738        return Err(GitError::InvalidFormat(
1739            "truncated bitmap pseudo-merge lookup".into(),
1740        ));
1741    }
1742    let position_table_len = pseudo_merge_count.checked_mul(8).ok_or_else(|| {
1743        GitError::InvalidFormat("bitmap pseudo-merge position table overflow".into())
1744    })?;
1745    let position_table_start = trailer_start
1746        .checked_sub(position_table_len)
1747        .filter(|start| *start >= range.start)
1748        .ok_or_else(|| {
1749            GitError::InvalidFormat("truncated bitmap pseudo-merge position table".into())
1750        })?;
1751
1752    let mut pseudo_merges = Vec::with_capacity(pseudo_merge_count);
1753    let mut cursor = position_table_start;
1754    for _ in 0..pseudo_merge_count {
1755        let pseudo_offset = u64_be(&bytes[cursor..cursor + 8]) as usize;
1756        cursor += 8;
1757        if pseudo_offset < range.start || pseudo_offset >= position_table_start {
1758            return Err(GitError::InvalidFormat(
1759                "bitmap pseudo-merge offset out of range".into(),
1760            ));
1761        }
1762        let mut offset = pseudo_offset;
1763        let commits = parse_bitmap_ewah(bytes, &mut offset, range.end, object_count)?;
1764        let bitmap = parse_bitmap_ewah(bytes, &mut offset, range.end, object_count)?;
1765        pseudo_merges.push(PackBitmapPseudoMerge { commits, bitmap });
1766    }
1767    Ok(pseudo_merges)
1768}
1769
1770pub(crate) fn parse_bitmap_ewah(
1771    bytes: &[u8],
1772    offset: &mut usize,
1773    checksum_offset: usize,
1774    _object_count: usize,
1775) -> Result<EwahBitmap> {
1776    if checksum_offset.saturating_sub(*offset) < 12 {
1777        return Err(GitError::InvalidFormat("truncated EWAH bitmap".into()));
1778    }
1779    let bit_size = u32_be(&bytes[*offset..*offset + 4]);
1780    *offset += 4;
1781    let word_count = u32_be(&bytes[*offset..*offset + 4]) as usize;
1782    *offset += 4;
1783    let words_len = word_count
1784        .checked_mul(8)
1785        .ok_or_else(|| GitError::InvalidFormat("EWAH word table overflow".into()))?;
1786    if checksum_offset.saturating_sub(*offset) < words_len + 4 {
1787        return Err(GitError::InvalidFormat("truncated EWAH word table".into()));
1788    }
1789    let mut words = Vec::with_capacity(word_count);
1790    for _ in 0..word_count {
1791        words.push(u64_be(&bytes[*offset..*offset + 8]));
1792        *offset += 8;
1793    }
1794    let rlw_position = u32_be(&bytes[*offset..*offset + 4]);
1795    *offset += 4;
1796    validate_ewah_words(bit_size, &words, rlw_position)?;
1797    Ok(EwahBitmap {
1798        bit_size,
1799        words,
1800        rlw_position,
1801    })
1802}
1803
1804pub(crate) fn validate_ewah_words(bit_size: u32, words: &[u64], rlw_position: u32) -> Result<()> {
1805    if words.is_empty() {
1806        if rlw_position != 0 || bit_size != 0 {
1807            return Err(GitError::InvalidFormat(
1808                "EWAH bitmap has invalid empty RLW".into(),
1809            ));
1810        }
1811        return Ok(());
1812    }
1813    if rlw_position as usize >= words.len() {
1814        return Err(GitError::InvalidFormat(
1815            "EWAH RLW position points past word table".into(),
1816        ));
1817    }
1818    let mut word_idx = 0usize;
1819    let mut decoded_words = 0u64;
1820    while word_idx < words.len() {
1821        let rlw = words[word_idx];
1822        let run_words = (rlw >> 1) & 0xffff_ffff;
1823        let literal_words = (rlw >> 33) as usize;
1824        word_idx += 1;
1825        word_idx = word_idx
1826            .checked_add(literal_words)
1827            .ok_or_else(|| GitError::InvalidFormat("EWAH literal word overflow".into()))?;
1828        if word_idx > words.len() {
1829            return Err(GitError::InvalidFormat(
1830                "EWAH literal words extend past word table".into(),
1831            ));
1832        }
1833        decoded_words = decoded_words
1834            .checked_add(run_words)
1835            .and_then(|value| value.checked_add(literal_words as u64))
1836            .ok_or_else(|| GitError::InvalidFormat("EWAH decoded size overflow".into()))?;
1837    }
1838    let decoded_bits = decoded_words
1839        .checked_mul(64)
1840        .ok_or_else(|| GitError::InvalidFormat("EWAH decoded bit size overflow".into()))?;
1841    if decoded_bits < u64::from(bit_size) {
1842        return Err(GitError::InvalidFormat(
1843            "EWAH bitmap decodes fewer bits than declared".into(),
1844        ));
1845    }
1846    Ok(())
1847}
1848
1849impl MultiPackIndex {
1850    pub fn write(
1851        format: ObjectFormat,
1852        version: u8,
1853        pack_names: &[String],
1854        objects: &[MultiPackIndexEntry],
1855    ) -> Result<Vec<u8>> {
1856        Self::write_with_reverse_index(format, version, pack_names, objects, None)
1857    }
1858
1859    /// Like [`MultiPackIndex::write`], but when `preferred_pack` is `Some`,
1860    /// additionally emits the `RIDX` chunk: the object order a multi-pack
1861    /// `.bitmap` numbers its bits in ("pseudo-pack order" — every object of
1862    /// the preferred pack first, then the rest by pack id, each pack's slice
1863    /// in offset order), stored as one u32 midx position per object.
1864    ///
1865    /// `preferred_pack` is the pack-int-id receiving pseudo-pack priority; it
1866    /// must be in range.
1867    pub fn write_with_reverse_index(
1868        format: ObjectFormat,
1869        version: u8,
1870        pack_names: &[String],
1871        objects: &[MultiPackIndexEntry],
1872        preferred_pack: Option<u32>,
1873    ) -> Result<Vec<u8>> {
1874        Self::write_with_bitmap_packs(format, version, pack_names, objects, preferred_pack, None)
1875    }
1876
1877    pub fn write_with_bitmap_packs(
1878        format: ObjectFormat,
1879        version: u8,
1880        pack_names: &[String],
1881        objects: &[MultiPackIndexEntry],
1882        preferred_pack: Option<u32>,
1883        bitmapped_packs: Option<&[MultiPackBitmapPack]>,
1884    ) -> Result<Vec<u8>> {
1885        if let Some(preferred) = preferred_pack
1886            && preferred as usize >= pack_names.len()
1887        {
1888            return Err(GitError::InvalidFormat(format!(
1889                "preferred pack {preferred} out of range for {} packs",
1890                pack_names.len()
1891            )));
1892        }
1893        if version != 1 && version != 2 {
1894            return Err(GitError::Unsupported(format!(
1895                "multi-pack-index version {version}"
1896            )));
1897        }
1898        if pack_names.len() > u32::MAX as usize {
1899            return Err(GitError::InvalidFormat(
1900                "too many multi-pack-index packs".into(),
1901            ));
1902        }
1903        if objects.len() > u32::MAX as usize {
1904            return Err(GitError::InvalidFormat(
1905                "too many multi-pack-index objects".into(),
1906            ));
1907        }
1908        if let Some(bitmapped_packs) = bitmapped_packs {
1909            if bitmapped_packs.len() != pack_names.len() {
1910                return Err(GitError::InvalidFormat(
1911                    "multi-pack-index BTMP pack count mismatch".into(),
1912                ));
1913            }
1914            for pack in bitmapped_packs {
1915                let bitmap_end = u64::from(pack.bitmap_pos)
1916                    .checked_add(u64::from(pack.bitmap_nr))
1917                    .ok_or_else(|| {
1918                        GitError::InvalidFormat("multi-pack-index BTMP range overflow".into())
1919                    })?;
1920                if bitmap_end > objects.len() as u64 {
1921                    return Err(GitError::InvalidFormat(
1922                        "multi-pack-index BTMP range points past object table".into(),
1923                    ));
1924                }
1925            }
1926        }
1927        validate_midx_pack_names(pack_names)?;
1928        if version == 1 && pack_names.windows(2).any(|pair| pair[0] > pair[1]) {
1929            return Err(GitError::InvalidFormat(
1930                "multi-pack-index v1 pack names must be sorted".into(),
1931            ));
1932        }
1933
1934        let mut objects = objects.iter().collect::<Vec<_>>();
1935        objects.sort_by(|left, right| left.oid.as_bytes().cmp(right.oid.as_bytes()));
1936        let mut previous_oid: Option<&ObjectId> = None;
1937        for object in &objects {
1938            if object.oid.format() != format {
1939                return Err(GitError::InvalidObjectId(
1940                    "multi-pack-index object format does not match index format".into(),
1941                ));
1942            }
1943            if let Some(previous) = previous_oid
1944                && previous.as_bytes() == object.oid.as_bytes()
1945            {
1946                return Err(GitError::InvalidFormat(
1947                    "multi-pack-index contains duplicate object ids".into(),
1948                ));
1949            }
1950            if object.pack_int_id as usize >= pack_names.len() {
1951                return Err(GitError::InvalidFormat(
1952                    "multi-pack-index object points past pack table".into(),
1953                ));
1954            }
1955            previous_oid = Some(&object.oid);
1956        }
1957
1958        let mut large_offsets = Vec::new();
1959        let mut chunks = vec![
1960            (*b"PNAM", write_midx_pack_names(pack_names)),
1961            (*b"OIDF", write_midx_oid_fanout(&objects)?),
1962            (*b"OIDL", write_midx_oid_lookup(&objects)),
1963            (
1964                *b"OOFF",
1965                write_midx_object_offsets(&objects, &mut large_offsets)?,
1966            ),
1967        ];
1968        if !large_offsets.is_empty() {
1969            chunks.push((*b"LOFF", large_offsets));
1970        }
1971        if let Some(preferred) = preferred_pack {
1972            // `objects` is already in midx (oid-sorted) order here; the chunk
1973            // lists each object's midx position in pseudo-pack order.
1974            let mut pseudo: Vec<u32> = (0..objects.len() as u32).collect();
1975            pseudo.sort_by_key(|&midx_pos| {
1976                let object = objects[midx_pos as usize];
1977                (
1978                    object.pack_int_id != preferred,
1979                    object.pack_int_id,
1980                    object.offset,
1981                )
1982            });
1983            let mut ridx = Vec::with_capacity(pseudo.len() * 4);
1984            for midx_pos in pseudo {
1985                ridx.extend_from_slice(&midx_pos.to_be_bytes());
1986            }
1987            chunks.push((*b"RIDX", ridx));
1988        }
1989        if let Some(bitmapped_packs) = bitmapped_packs {
1990            let mut btmp = Vec::with_capacity(bitmapped_packs.len() * 8);
1991            for pack in bitmapped_packs {
1992                btmp.extend_from_slice(&pack.bitmap_pos.to_be_bytes());
1993                btmp.extend_from_slice(&pack.bitmap_nr.to_be_bytes());
1994            }
1995            chunks.push((*b"BTMP", btmp));
1996        }
1997        write_multi_pack_index_chunks(format, version, pack_names.len() as u32, &chunks)
1998    }
1999
2000    pub fn parse(bytes: &[u8], format: ObjectFormat) -> Result<Self> {
2001        Self::parse_impl(bytes, format, true)
2002    }
2003
2004    pub fn parse_without_checksum(bytes: &[u8], format: ObjectFormat) -> Result<Self> {
2005        Self::parse_impl(bytes, format, false)
2006    }
2007
2008    pub(crate) fn parse_impl(
2009        bytes: &[u8],
2010        format: ObjectFormat,
2011        verify_checksum: bool,
2012    ) -> Result<Self> {
2013        let hash_len = format.raw_len();
2014        if bytes.len() < 12 + 12 + hash_len {
2015            return Err(GitError::InvalidFormat(
2016                "multi-pack-index file too short".into(),
2017            ));
2018        }
2019        if &bytes[..4] != b"MIDX" {
2020            return Err(GitError::InvalidFormat(
2021                "missing multi-pack-index signature".into(),
2022            ));
2023        }
2024        let version = bytes[4];
2025        if version != 1 && version != 2 {
2026            return Err(GitError::Unsupported(format!(
2027                "multi-pack-index version {version}"
2028            )));
2029        }
2030        let hash_id = bytes[5];
2031        if u32::from(hash_id) != hash_function_id(format) {
2032            return Err(GitError::InvalidFormat(format!(
2033                "multi-pack-index hash id {hash_id} does not match {}",
2034                format.name()
2035            )));
2036        }
2037        let chunk_count = bytes[6] as usize;
2038        let base_midx_count = bytes[7];
2039        if base_midx_count != 0 {
2040            return Err(GitError::Unsupported(format!(
2041                "multi-pack-index base count {base_midx_count}"
2042            )));
2043        }
2044        let pack_count = u32_be(&bytes[8..12]);
2045        let lookup_len = (chunk_count + 1)
2046            .checked_mul(12)
2047            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index lookup overflow".into()))?;
2048        let data_start = 12usize
2049            .checked_add(lookup_len)
2050            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index lookup overflow".into()))?;
2051        let checksum_offset = bytes.len() - hash_len;
2052        if data_start > checksum_offset {
2053            return Err(GitError::InvalidFormat(
2054                "truncated multi-pack-index chunk lookup".into(),
2055            ));
2056        }
2057
2058        let checksum = ObjectId::from_raw(format, &bytes[checksum_offset..])?;
2059        if verify_checksum {
2060            let actual_checksum = sley_core::digest_bytes(format, &bytes[..checksum_offset])?;
2061            if actual_checksum != checksum {
2062                return Err(GitError::InvalidFormat(format!(
2063                    "multi-pack-index checksum mismatch: expected {checksum}, got {actual_checksum}"
2064                )));
2065            }
2066        }
2067
2068        let mut entries = Vec::with_capacity(chunk_count + 1);
2069        let mut offset = 12usize;
2070        for _ in 0..=chunk_count {
2071            let id = [
2072                bytes[offset],
2073                bytes[offset + 1],
2074                bytes[offset + 2],
2075                bytes[offset + 3],
2076            ];
2077            let chunk_offset = u64_be(&bytes[offset + 4..offset + 12]);
2078            entries.push((id, chunk_offset));
2079            offset += 12;
2080        }
2081        let Some((terminator_id, terminator_offset)) = entries.last().copied() else {
2082            return Err(GitError::InvalidFormat(
2083                "multi-pack-index chunk lookup is empty".into(),
2084            ));
2085        };
2086        if terminator_id != [0, 0, 0, 0] {
2087            return Err(GitError::InvalidFormat(
2088                "multi-pack-index chunk lookup missing terminator".into(),
2089            ));
2090        }
2091        if terminator_offset != checksum_offset as u64 {
2092            return Err(GitError::InvalidFormat(
2093                "multi-pack-index terminator does not point at checksum".into(),
2094            ));
2095        }
2096
2097        let mut chunks = Vec::with_capacity(chunk_count);
2098        let mut previous_offset = data_start as u64;
2099        let mut reported_unaligned = false;
2100        for pair in entries.windows(2) {
2101            let (id, chunk_offset) = pair[0];
2102            let (_next_id, next_offset) = pair[1];
2103            if id == [0, 0, 0, 0] {
2104                return Err(GitError::InvalidFormat(
2105                    "multi-pack-index chunk id is zero before terminator".into(),
2106                ));
2107            }
2108            if chunk_offset < data_start as u64 || chunk_offset < previous_offset {
2109                return Err(GitError::InvalidFormat(
2110                    "multi-pack-index chunk offsets are not monotonic".into(),
2111                ));
2112            }
2113            if chunk_offset % 4 != 0 && !reported_unaligned {
2114                eprintln!(
2115                    "error: chunk id {:08x} not 4-byte aligned",
2116                    u32::from_be_bytes(id)
2117                );
2118                reported_unaligned = true;
2119            }
2120            if next_offset < chunk_offset || next_offset > checksum_offset as u64 {
2121                return Err(GitError::InvalidFormat(
2122                    "multi-pack-index chunk length is invalid".into(),
2123                ));
2124            }
2125            chunks.push(MultiPackIndexChunk {
2126                id,
2127                offset: chunk_offset,
2128                len: next_offset - chunk_offset,
2129            });
2130            previous_offset = chunk_offset;
2131        }
2132
2133        let pack_names = parse_midx_pack_names(bytes, &chunks, pack_count as usize, version)?;
2134        let (fanout, object_count) = parse_midx_oid_fanout(bytes, &chunks)?;
2135        let object_ids = parse_midx_object_ids(bytes, &chunks, format, object_count, &fanout)?;
2136        let objects = parse_midx_object_offsets(bytes, &chunks, object_ids, pack_count)?;
2137        let reverse_index = parse_midx_reverse_index(bytes, &chunks, object_count)?;
2138        let bitmapped_packs =
2139            parse_midx_bitmapped_packs(bytes, &chunks, pack_count as usize, object_count)?;
2140
2141        Ok(Self {
2142            version,
2143            format,
2144            pack_count,
2145            pack_names,
2146            object_count: object_count as u32,
2147            fanout,
2148            objects,
2149            reverse_index,
2150            bitmapped_packs,
2151            chunks,
2152            checksum,
2153        })
2154    }
2155
2156    pub fn find(&self, oid: &ObjectId) -> Option<&MultiPackIndexEntry> {
2157        self.objects
2158            .binary_search_by(|entry| entry.oid.as_bytes().cmp(oid.as_bytes()))
2159            .ok()
2160            .map(|idx| &self.objects[idx])
2161    }
2162}
2163
2164impl MultiPackIndexOidLookup {
2165    pub fn parse(bytes: Arc<dyn PackIndexByteSource>, format: ObjectFormat) -> Result<Self> {
2166        let raw = bytes.as_bytes();
2167        let hash_len = format.raw_len();
2168        if raw.len() < 12 + 12 + hash_len {
2169            return Err(GitError::InvalidFormat(
2170                "multi-pack-index file too short".into(),
2171            ));
2172        }
2173        if &raw[..4] != b"MIDX" {
2174            return Err(GitError::InvalidFormat(
2175                "missing multi-pack-index signature".into(),
2176            ));
2177        }
2178        let version = raw[4];
2179        if version != 1 && version != 2 {
2180            return Err(GitError::Unsupported(format!(
2181                "multi-pack-index version {version}"
2182            )));
2183        }
2184        let hash_id = raw[5];
2185        if u32::from(hash_id) != hash_function_id(format) {
2186            return Err(GitError::InvalidFormat(format!(
2187                "multi-pack-index hash id {hash_id} does not match {}",
2188                format.name()
2189            )));
2190        }
2191        let chunk_count = raw[6] as usize;
2192        let base_midx_count = raw[7];
2193        if base_midx_count != 0 {
2194            return Err(GitError::Unsupported(format!(
2195                "multi-pack-index base count {base_midx_count}"
2196            )));
2197        }
2198        let pack_count = u32_be(&raw[8..12]);
2199        let lookup_len = (chunk_count + 1)
2200            .checked_mul(12)
2201            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index lookup overflow".into()))?;
2202        let data_start = 12usize
2203            .checked_add(lookup_len)
2204            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index lookup overflow".into()))?;
2205        let checksum_offset = raw.len() - hash_len;
2206        if data_start > checksum_offset {
2207            return Err(GitError::InvalidFormat(
2208                "truncated multi-pack-index chunk lookup".into(),
2209            ));
2210        }
2211
2212        let mut entries = Vec::with_capacity(chunk_count + 1);
2213        let mut offset = 12usize;
2214        for _ in 0..=chunk_count {
2215            let id = [
2216                raw[offset],
2217                raw[offset + 1],
2218                raw[offset + 2],
2219                raw[offset + 3],
2220            ];
2221            let chunk_offset = u64_be(&raw[offset + 4..offset + 12]);
2222            entries.push((id, chunk_offset));
2223            offset += 12;
2224        }
2225        let Some((terminator_id, terminator_offset)) = entries.last().copied() else {
2226            return Err(GitError::InvalidFormat(
2227                "multi-pack-index chunk lookup is empty".into(),
2228            ));
2229        };
2230        if terminator_id != [0, 0, 0, 0] {
2231            return Err(GitError::InvalidFormat(
2232                "multi-pack-index chunk lookup missing terminator".into(),
2233            ));
2234        }
2235        if terminator_offset != checksum_offset as u64 {
2236            return Err(GitError::InvalidFormat(
2237                "multi-pack-index terminator does not point at checksum".into(),
2238            ));
2239        }
2240
2241        let mut chunks = Vec::with_capacity(chunk_count);
2242        let mut previous_offset = data_start as u64;
2243        let mut reported_unaligned = false;
2244        for pair in entries.windows(2) {
2245            let (id, chunk_offset) = pair[0];
2246            let (_next_id, next_offset) = pair[1];
2247            if id == [0, 0, 0, 0] {
2248                return Err(GitError::InvalidFormat(
2249                    "multi-pack-index chunk id is zero before terminator".into(),
2250                ));
2251            }
2252            if chunk_offset < data_start as u64 || chunk_offset < previous_offset {
2253                return Err(GitError::InvalidFormat(
2254                    "multi-pack-index chunk offsets are not monotonic".into(),
2255                ));
2256            }
2257            if chunk_offset % 4 != 0 && !reported_unaligned {
2258                eprintln!(
2259                    "error: chunk id {:08x} not 4-byte aligned",
2260                    u32::from_be_bytes(id)
2261                );
2262                reported_unaligned = true;
2263            }
2264            if next_offset < chunk_offset || next_offset > checksum_offset as u64 {
2265                return Err(GitError::InvalidFormat(
2266                    "multi-pack-index chunk length is invalid".into(),
2267                ));
2268            }
2269            chunks.push(MultiPackIndexChunk {
2270                id,
2271                offset: chunk_offset,
2272                len: next_offset - chunk_offset,
2273            });
2274            previous_offset = chunk_offset;
2275        }
2276
2277        let pack_names = parse_midx_pack_names(raw, &chunks, pack_count as usize, version)?;
2278        let (fanout, object_count) = parse_midx_oid_fanout(raw, &chunks)?;
2279        let oid_lookup = midx_chunk_data(raw, &chunks, *b"OIDL", true)?
2280            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index missing OIDL chunk".into()))?;
2281        let expected_len = object_count.checked_mul(hash_len).ok_or_else(|| {
2282            GitError::InvalidFormat("multi-pack-index OIDL chunk overflow".into())
2283        })?;
2284        if oid_lookup.len() != expected_len {
2285            return Err(GitError::InvalidFormat(
2286                "error: multi-pack-index OID lookup chunk is the wrong size\nfatal: multi-pack-index required OID lookup chunk missing or corrupted".into(),
2287            ));
2288        }
2289        let object_offsets = midx_chunk_data(raw, &chunks, *b"OOFF", true)?
2290            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index missing OOFF chunk".into()))?;
2291        let expected_offsets_len = object_count.checked_mul(8).ok_or_else(|| {
2292            GitError::InvalidFormat("multi-pack-index OOFF chunk overflow".into())
2293        })?;
2294        if object_offsets.len() != expected_offsets_len {
2295            return Err(GitError::InvalidFormat(
2296                "error: multi-pack-index object offset chunk is the wrong size\nfatal: multi-pack-index required object offsets chunk missing or corrupted".into(),
2297            ));
2298        }
2299        let large_offsets = midx_chunk_data(raw, &chunks, *b"LOFF", false)?;
2300        if let Some(large_offsets) = large_offsets
2301            && large_offsets.len() % 8 != 0
2302        {
2303            return Err(GitError::InvalidFormat(
2304                "multi-pack-index LOFF chunk has invalid length".into(),
2305            ));
2306        }
2307        let oid_lookup_offset = oid_lookup.as_ptr() as usize - raw.as_ptr() as usize;
2308        let object_offsets_offset = object_offsets.as_ptr() as usize - raw.as_ptr() as usize;
2309        let (large_offsets_offset, large_offsets_len) = match large_offsets {
2310            Some(large_offsets) => (
2311                Some(large_offsets.as_ptr() as usize - raw.as_ptr() as usize),
2312                large_offsets.len(),
2313            ),
2314            None => (None, 0),
2315        };
2316        Ok(Self {
2317            format,
2318            pack_count,
2319            pack_names,
2320            fanout,
2321            object_count,
2322            oid_lookup_offset,
2323            object_offsets_offset,
2324            large_offsets_offset,
2325            large_offsets_len,
2326            bytes,
2327        })
2328    }
2329
2330    pub fn contains(&self, oid: &ObjectId) -> bool {
2331        self.find_position(oid).is_some()
2332    }
2333
2334    pub fn find(&self, oid: &ObjectId) -> Result<Option<MultiPackIndexEntry>> {
2335        let Some(position) = self.find_position(oid) else {
2336            return Ok(None);
2337        };
2338        let bytes = self.bytes.as_bytes();
2339        let hash_len = self.format.raw_len();
2340        let oid_start = self
2341            .oid_lookup_offset
2342            .checked_add(position * hash_len)
2343            .ok_or_else(|| {
2344                GitError::InvalidFormat("multi-pack-index OIDL offset overflow".into())
2345            })?;
2346        let oid = ObjectId::from_raw(self.format, &bytes[oid_start..oid_start + hash_len])?;
2347        let offset_start = self
2348            .object_offsets_offset
2349            .checked_add(position * 8)
2350            .ok_or_else(|| {
2351                GitError::InvalidFormat("multi-pack-index OOFF offset overflow".into())
2352            })?;
2353        let data = &bytes[offset_start..offset_start + 8];
2354        let pack_int_id = u32_be(&data[..4]);
2355        if pack_int_id >= self.pack_count {
2356            return Err(GitError::InvalidFormat(
2357                "multi-pack-index object points past pack table".into(),
2358            ));
2359        }
2360        let raw_offset = u32_be(&data[4..8]);
2361        let offset = if raw_offset & 0x8000_0000 == 0 {
2362            u64::from(raw_offset)
2363        } else {
2364            let Some(large_offsets_offset) = self.large_offsets_offset else {
2365                return Err(GitError::InvalidFormat(
2366                    "multi-pack-index large offset missing LOFF chunk".into(),
2367                ));
2368            };
2369            let large_idx = (raw_offset & 0x7fff_ffff) as usize;
2370            let large_start = large_idx.checked_mul(8).ok_or_else(|| {
2371                GitError::InvalidFormat("multi-pack-index LOFF index overflow".into())
2372            })?;
2373            let large_end = large_start.checked_add(8).ok_or_else(|| {
2374                GitError::InvalidFormat("multi-pack-index LOFF index overflow".into())
2375            })?;
2376            if large_end > self.large_offsets_len {
2377                return Err(GitError::InvalidFormat(
2378                    "fatal: multi-pack-index large offset out of bounds".into(),
2379                ));
2380            }
2381            let start = large_offsets_offset + large_start;
2382            u64_be(&bytes[start..start + 8])
2383        };
2384        Ok(Some(MultiPackIndexEntry {
2385            oid,
2386            pack_int_id,
2387            offset,
2388            force_large_offset: raw_offset & 0x8000_0000 != 0,
2389        }))
2390    }
2391
2392    pub fn pack_name(&self, pack_int_id: u32) -> Option<&str> {
2393        self.pack_names
2394            .get(pack_int_id as usize)
2395            .map(String::as_str)
2396    }
2397
2398    pub(crate) fn find_position(&self, oid: &ObjectId) -> Option<usize> {
2399        if oid.format() != self.format || self.object_count == 0 {
2400            return None;
2401        }
2402        let first = oid.as_bytes()[0] as usize;
2403        let start = if first == 0 {
2404            0
2405        } else {
2406            self.fanout[first - 1] as usize
2407        };
2408        let end = self.fanout[first] as usize;
2409        if start >= end || end > self.object_count {
2410            return None;
2411        }
2412        let hash_len = self.format.raw_len();
2413        let table_start = self.oid_lookup_offset;
2414        let table_end = table_start + self.object_count * hash_len;
2415        let bytes = self.bytes.as_bytes();
2416        let table = &bytes[table_start..table_end];
2417        let needle = oid.as_bytes();
2418        let mut low = start;
2419        let mut high = end;
2420        while low < high {
2421            let mid = low + (high - low) / 2;
2422            let raw = &table[mid * hash_len..(mid + 1) * hash_len];
2423            match raw.cmp(needle) {
2424                std::cmp::Ordering::Less => low = mid + 1,
2425                std::cmp::Ordering::Equal => return Some(mid),
2426                std::cmp::Ordering::Greater => high = mid,
2427            }
2428        }
2429        None
2430    }
2431}
2432
2433pub(crate) fn validate_midx_pack_names(pack_names: &[String]) -> Result<()> {
2434    for name in pack_names {
2435        if name.is_empty() {
2436            return Err(GitError::InvalidFormat(
2437                "multi-pack-index pack name is empty".into(),
2438            ));
2439        }
2440        if name
2441            .bytes()
2442            .any(|byte| byte == 0 || matches!(byte, b'/' | b'\\'))
2443        {
2444            return Err(GitError::InvalidFormat(
2445                "multi-pack-index pack name contains an invalid byte".into(),
2446            ));
2447        }
2448    }
2449    Ok(())
2450}
2451
2452pub(crate) fn write_midx_pack_names(pack_names: &[String]) -> Vec<u8> {
2453    let mut out = Vec::new();
2454    for name in pack_names {
2455        out.extend_from_slice(name.as_bytes());
2456        out.push(0);
2457    }
2458    while out.len() % 4 != 0 {
2459        out.push(0);
2460    }
2461    out
2462}
2463
2464pub(crate) fn write_midx_oid_fanout(objects: &[&MultiPackIndexEntry]) -> Result<Vec<u8>> {
2465    let mut counts = [0u32; 256];
2466    for object in objects {
2467        let first = object.oid.as_bytes()[0] as usize;
2468        counts[first] = counts[first]
2469            .checked_add(1)
2470            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index fanout overflow".into()))?;
2471    }
2472    let mut running = 0u32;
2473    let mut out = Vec::with_capacity(256 * 4);
2474    for count in counts {
2475        running = running
2476            .checked_add(count)
2477            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index fanout overflow".into()))?;
2478        out.extend_from_slice(&running.to_be_bytes());
2479    }
2480    Ok(out)
2481}
2482
2483pub(crate) fn write_midx_oid_lookup(objects: &[&MultiPackIndexEntry]) -> Vec<u8> {
2484    let mut out = Vec::new();
2485    for object in objects {
2486        out.extend_from_slice(object.oid.as_bytes());
2487    }
2488    out
2489}
2490
2491pub(crate) fn write_midx_object_offsets(
2492    objects: &[&MultiPackIndexEntry],
2493    large_offsets: &mut Vec<u8>,
2494) -> Result<Vec<u8>> {
2495    let mut out = Vec::new();
2496    for object in objects {
2497        out.extend_from_slice(&object.pack_int_id.to_be_bytes());
2498        if object.offset < 0x8000_0000 && !object.force_large_offset {
2499            out.extend_from_slice(&(object.offset as u32).to_be_bytes());
2500        } else {
2501            let large_idx = large_offsets.len() / 8;
2502            if large_idx > 0x7fff_ffff {
2503                return Err(GitError::InvalidFormat(
2504                    "too many multi-pack-index large offsets".into(),
2505                ));
2506            }
2507            out.extend_from_slice(&(0x8000_0000 | large_idx as u32).to_be_bytes());
2508            large_offsets.extend_from_slice(&object.offset.to_be_bytes());
2509        }
2510    }
2511    Ok(out)
2512}
2513
2514pub(crate) fn write_multi_pack_index_chunks(
2515    format: ObjectFormat,
2516    version: u8,
2517    pack_count: u32,
2518    chunks: &[([u8; 4], Vec<u8>)],
2519) -> Result<Vec<u8>> {
2520    if chunks.len() > u8::MAX as usize {
2521        return Err(GitError::InvalidFormat(
2522            "too many multi-pack-index chunks".into(),
2523        ));
2524    }
2525    let lookup_len = (chunks.len() + 1)
2526        .checked_mul(12)
2527        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index lookup overflow".into()))?;
2528    let mut out = Vec::new();
2529    out.extend_from_slice(b"MIDX");
2530    out.push(version);
2531    out.push(hash_function_id(format) as u8);
2532    out.push(chunks.len() as u8);
2533    out.push(0);
2534    out.extend_from_slice(&pack_count.to_be_bytes());
2535    let mut chunk_offset = (12usize)
2536        .checked_add(lookup_len)
2537        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index lookup overflow".into()))?
2538        as u64;
2539    for (id, data) in chunks {
2540        out.extend_from_slice(id);
2541        out.extend_from_slice(&chunk_offset.to_be_bytes());
2542        chunk_offset = chunk_offset
2543            .checked_add(data.len() as u64)
2544            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index size overflow".into()))?;
2545    }
2546    out.extend_from_slice(&[0, 0, 0, 0]);
2547    out.extend_from_slice(&chunk_offset.to_be_bytes());
2548    for (_id, data) in chunks {
2549        out.extend_from_slice(data);
2550    }
2551    let checksum = sley_core::digest_bytes(format, &out)?;
2552    out.extend_from_slice(checksum.as_bytes());
2553    Ok(out)
2554}
2555pub(crate) fn read_pack_index_fanout(bytes: &[u8], offset: &mut usize) -> Result<[u32; 256]> {
2556    let mut fanout = [0u32; 256];
2557    let mut previous = 0u32;
2558    for slot in &mut fanout {
2559        *slot = u32_be(&bytes[*offset..*offset + 4]);
2560        if *slot < previous {
2561            return Err(GitError::InvalidFormat(
2562                "pack index fanout is not monotonic".into(),
2563            ));
2564        }
2565        previous = *slot;
2566        *offset += 4;
2567    }
2568    Ok(fanout)
2569}
2570
2571pub(crate) fn validate_pack_index_oid_fanout(
2572    idx: usize,
2573    oid_bytes: &[u8],
2574    fanout: &[u32; 256],
2575) -> Result<()> {
2576    let expected_min = if oid_bytes[0] == 0 {
2577        0
2578    } else {
2579        fanout[usize::from(oid_bytes[0] - 1)]
2580    };
2581    if (idx as u32) < expected_min || (idx as u32) >= fanout[usize::from(oid_bytes[0])] {
2582        return Err(GitError::InvalidFormat(
2583            "pack index object id is outside its fanout bucket".into(),
2584        ));
2585    }
2586    Ok(())
2587}
2588
2589pub(crate) fn pack_index_v2_offset(raw_offset: u32, large_offset_table: &[u8]) -> Result<u64> {
2590    if raw_offset & 0x8000_0000 == 0 {
2591        return Ok(u64::from(raw_offset));
2592    }
2593    let large_idx = (raw_offset & 0x7fff_ffff) as usize;
2594    let large_start = large_idx
2595        .checked_mul(8)
2596        .ok_or_else(|| GitError::InvalidFormat("pack index large offset overflow".into()))?;
2597    let large_end = large_start
2598        .checked_add(8)
2599        .ok_or_else(|| GitError::InvalidFormat("pack index large offset overflow".into()))?;
2600    if large_end > large_offset_table.len() {
2601        return Err(GitError::InvalidFormat(
2602            "pack index large offset points past table".into(),
2603        ));
2604    }
2605    Ok(u64_be(&large_offset_table[large_start..large_end]))
2606}
2607pub(crate) fn parse_midx_pack_names(
2608    bytes: &[u8],
2609    chunks: &[MultiPackIndexChunk],
2610    pack_count: usize,
2611    version: u8,
2612) -> Result<Vec<String>> {
2613    let data = midx_chunk_data(bytes, chunks, *b"PNAM", true)?
2614        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index missing PNAM chunk".into()))?;
2615    let mut names = Vec::with_capacity(pack_count);
2616    let mut offset = 0usize;
2617    while names.len() < pack_count {
2618        let Some(relative_end) = data[offset..].iter().position(|byte| *byte == 0) else {
2619            return Err(GitError::InvalidFormat(
2620                "fatal: multi-pack-index pack-name chunk is too short".into(),
2621            ));
2622        };
2623        let name_bytes = &data[offset..offset + relative_end];
2624        if name_bytes.is_empty() {
2625            return Err(GitError::InvalidFormat(
2626                "multi-pack-index PNAM entry is empty".into(),
2627            ));
2628        }
2629        let name = std::str::from_utf8(name_bytes)
2630            .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
2631        if name.bytes().any(|byte| matches!(byte, b'/' | b'\\')) {
2632            return Err(GitError::InvalidFormat(
2633                "multi-pack-index PNAM entry contains a path separator".into(),
2634            ));
2635        }
2636        names.push(name.to_string());
2637        offset += relative_end + 1;
2638    }
2639    let padding = &data[offset..];
2640    if padding.len() > 3 || padding.iter().any(|byte| *byte != 0) {
2641        return Err(GitError::InvalidFormat(
2642            "multi-pack-index PNAM padding is invalid".into(),
2643        ));
2644    }
2645    if version == 1 && names.windows(2).any(|pair| pair[0] > pair[1]) {
2646        return Err(GitError::InvalidFormat(
2647            "multi-pack-index v1 PNAM entries are not sorted".into(),
2648        ));
2649    }
2650    Ok(names)
2651}
2652
2653pub(crate) fn parse_midx_oid_fanout(
2654    bytes: &[u8],
2655    chunks: &[MultiPackIndexChunk],
2656) -> Result<([u32; 256], usize)> {
2657    let data = midx_chunk_data(bytes, chunks, *b"OIDF", true)?
2658        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index missing OIDF chunk".into()))?;
2659    if data.len() != 256 * 4 {
2660        return Err(GitError::InvalidFormat(
2661            "error: multi-pack-index OID fanout is of the wrong size\nfatal: multi-pack-index required OID fanout chunk missing or corrupted".into(),
2662        ));
2663    }
2664    let mut fanout = [0u32; 256];
2665    let mut previous = 0u32;
2666    for (idx, slot) in fanout.iter_mut().enumerate() {
2667        let start = idx * 4;
2668        *slot = u32_be(&data[start..start + 4]);
2669        if *slot < previous {
2670            return Err(GitError::InvalidFormat(format!(
2671                "error: oid fanout out of order: fanout[{}] = {:x} > {:x} = fanout[{idx}]\nfatal: multi-pack-index required OID fanout chunk missing or corrupted",
2672                idx - 1,
2673                previous,
2674                *slot
2675            )));
2676        }
2677        previous = *slot;
2678    }
2679    Ok((fanout, fanout[255] as usize))
2680}
2681
2682pub(crate) fn parse_midx_object_ids(
2683    bytes: &[u8],
2684    chunks: &[MultiPackIndexChunk],
2685    format: ObjectFormat,
2686    object_count: usize,
2687    fanout: &[u32; 256],
2688) -> Result<Vec<ObjectId>> {
2689    let data = midx_chunk_data(bytes, chunks, *b"OIDL", true)?
2690        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index missing OIDL chunk".into()))?;
2691    let expected_len = object_count
2692        .checked_mul(format.raw_len())
2693        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index OIDL chunk overflow".into()))?;
2694    if data.len() != expected_len {
2695        return Err(GitError::InvalidFormat(
2696            "error: multi-pack-index OID lookup chunk is the wrong size\nfatal: multi-pack-index required OID lookup chunk missing or corrupted".into(),
2697        ));
2698    }
2699
2700    let mut ids = Vec::with_capacity(object_count);
2701    let mut counts = [0u32; 256];
2702    let mut previous_oid: Option<ObjectId> = None;
2703    for idx in 0..object_count {
2704        let start = idx * format.raw_len();
2705        let oid = ObjectId::from_raw(format, &data[start..start + format.raw_len()])?;
2706        if let Some(previous) = &previous_oid
2707            && previous.as_bytes() >= oid.as_bytes()
2708        {
2709            return Err(GitError::InvalidFormat(
2710                "multi-pack-index OIDL object ids are not strictly sorted".into(),
2711            ));
2712        }
2713        counts[oid.as_bytes()[0] as usize] = counts[oid.as_bytes()[0] as usize]
2714            .checked_add(1)
2715            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index fanout overflow".into()))?;
2716        previous_oid = Some(oid);
2717        ids.push(oid);
2718    }
2719
2720    let mut running = 0u32;
2721    for (idx, count) in counts.iter().enumerate() {
2722        running = running
2723            .checked_add(*count)
2724            .ok_or_else(|| GitError::InvalidFormat("multi-pack-index fanout overflow".into()))?;
2725        if fanout[idx] != running {
2726            return Err(GitError::InvalidFormat(
2727                "multi-pack-index OIDF fanout does not match OIDL".into(),
2728            ));
2729        }
2730    }
2731    Ok(ids)
2732}
2733
2734pub(crate) fn parse_midx_object_offsets(
2735    bytes: &[u8],
2736    chunks: &[MultiPackIndexChunk],
2737    object_ids: Vec<ObjectId>,
2738    pack_count: u32,
2739) -> Result<Vec<MultiPackIndexEntry>> {
2740    let data = midx_chunk_data(bytes, chunks, *b"OOFF", true)?
2741        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index missing OOFF chunk".into()))?;
2742    let expected_len = object_ids
2743        .len()
2744        .checked_mul(8)
2745        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index OOFF chunk overflow".into()))?;
2746    if data.len() != expected_len {
2747        return Err(GitError::InvalidFormat(
2748            "error: multi-pack-index object offset chunk is the wrong size\nfatal: multi-pack-index required object offsets chunk missing or corrupted".into(),
2749        ));
2750    }
2751    let large_offsets = midx_chunk_data(bytes, chunks, *b"LOFF", false)?;
2752    if let Some(large_offsets) = large_offsets
2753        && large_offsets.len() % 8 != 0
2754    {
2755        return Err(GitError::InvalidFormat(
2756            "multi-pack-index LOFF chunk has invalid length".into(),
2757        ));
2758    }
2759
2760    let mut entries = Vec::with_capacity(object_ids.len());
2761    for (idx, oid) in object_ids.into_iter().enumerate() {
2762        let start = idx * 8;
2763        let pack_int_id = u32_be(&data[start..start + 4]);
2764        if pack_int_id >= pack_count {
2765            return Err(GitError::InvalidFormat(
2766                "multi-pack-index object points past pack table".into(),
2767            ));
2768        }
2769        let raw_offset = u32_be(&data[start + 4..start + 8]);
2770        let offset = if raw_offset & 0x8000_0000 == 0 {
2771            u64::from(raw_offset)
2772        } else {
2773            let Some(large_offsets) = large_offsets else {
2774                return Err(GitError::InvalidFormat(
2775                    "multi-pack-index large offset missing LOFF chunk".into(),
2776                ));
2777            };
2778            let large_idx = (raw_offset & 0x7fff_ffff) as usize;
2779            let large_start = large_idx.checked_mul(8).ok_or_else(|| {
2780                GitError::InvalidFormat("multi-pack-index LOFF index overflow".into())
2781            })?;
2782            let large_end = large_start.checked_add(8).ok_or_else(|| {
2783                GitError::InvalidFormat("multi-pack-index LOFF index overflow".into())
2784            })?;
2785            if large_end > large_offsets.len() {
2786                return Err(GitError::InvalidFormat(
2787                    "fatal: multi-pack-index large offset out of bounds".into(),
2788                ));
2789            }
2790            u64_be(&large_offsets[large_start..large_end])
2791        };
2792        entries.push(MultiPackIndexEntry {
2793            oid,
2794            pack_int_id,
2795            offset,
2796            force_large_offset: raw_offset & 0x8000_0000 != 0,
2797        });
2798    }
2799    Ok(entries)
2800}
2801
2802pub(crate) fn parse_midx_reverse_index(
2803    bytes: &[u8],
2804    chunks: &[MultiPackIndexChunk],
2805    object_count: usize,
2806) -> Result<Option<Vec<u32>>> {
2807    let Some(data) = midx_chunk_data(bytes, chunks, *b"RIDX", false)? else {
2808        return Ok(None);
2809    };
2810    let expected_len = object_count
2811        .checked_mul(4)
2812        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index RIDX chunk overflow".into()))?;
2813    if data.len() != expected_len {
2814        return Err(GitError::InvalidFormat(
2815            "multi-pack-index reverse-index chunk is the wrong size".into(),
2816        ));
2817    }
2818    let mut positions = Vec::with_capacity(object_count);
2819    for idx in 0..object_count {
2820        let start = idx * 4;
2821        positions.push(u32_be(&data[start..start + 4]));
2822    }
2823    validate_position_permutation(&positions)?;
2824    Ok(Some(positions))
2825}
2826
2827pub(crate) fn parse_midx_bitmapped_packs(
2828    bytes: &[u8],
2829    chunks: &[MultiPackIndexChunk],
2830    pack_count: usize,
2831    object_count: usize,
2832) -> Result<Option<Vec<MultiPackBitmapPack>>> {
2833    let Some(data) = midx_chunk_data(bytes, chunks, *b"BTMP", false)? else {
2834        return Ok(None);
2835    };
2836    let expected_len = pack_count
2837        .checked_mul(8)
2838        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index BTMP chunk overflow".into()))?;
2839    if data.len() != expected_len {
2840        return Err(GitError::InvalidFormat(
2841            "multi-pack-index BTMP chunk has invalid length".into(),
2842        ));
2843    }
2844    let mut entries = Vec::with_capacity(pack_count);
2845    for idx in 0..pack_count {
2846        let start = idx * 8;
2847        let bitmap_pos = u32_be(&data[start..start + 4]);
2848        let bitmap_nr = u32_be(&data[start + 4..start + 8]);
2849        let bitmap_end = u64::from(bitmap_pos)
2850            .checked_add(u64::from(bitmap_nr))
2851            .ok_or_else(|| {
2852                GitError::InvalidFormat("multi-pack-index BTMP range overflow".into())
2853            })?;
2854        if bitmap_end > object_count as u64 {
2855            return Err(GitError::InvalidFormat(
2856                "multi-pack-index BTMP range points past object table".into(),
2857            ));
2858        }
2859        entries.push(MultiPackBitmapPack {
2860            bitmap_pos,
2861            bitmap_nr,
2862        });
2863    }
2864    Ok(Some(entries))
2865}
2866
2867pub(crate) fn midx_chunk_data<'a>(
2868    bytes: &'a [u8],
2869    chunks: &[MultiPackIndexChunk],
2870    id: [u8; 4],
2871    required: bool,
2872) -> Result<Option<&'a [u8]>> {
2873    let Some(chunk) = chunks.iter().find(|chunk| chunk.id == id) else {
2874        if required {
2875            return Err(GitError::InvalidFormat(format!(
2876                "multi-pack-index missing {} chunk",
2877                std::str::from_utf8(&id).unwrap_or("required")
2878            )));
2879        }
2880        return Ok(None);
2881    };
2882    let start = usize::try_from(chunk.offset)
2883        .map_err(|_| GitError::InvalidFormat("multi-pack-index chunk offset overflow".into()))?;
2884    let len = usize::try_from(chunk.len)
2885        .map_err(|_| GitError::InvalidFormat("multi-pack-index chunk length overflow".into()))?;
2886    let end = start
2887        .checked_add(len)
2888        .ok_or_else(|| GitError::InvalidFormat("multi-pack-index chunk range overflow".into()))?;
2889    let Some(data) = bytes.get(start..end) else {
2890        return Err(GitError::InvalidFormat(
2891            "multi-pack-index chunk extends past file".into(),
2892        ));
2893    };
2894    Ok(Some(data))
2895}
2896
2897pub(crate) fn hash_function_id(format: ObjectFormat) -> u32 {
2898    match format {
2899        ObjectFormat::Sha1 => 1,
2900        ObjectFormat::Sha256 => 2,
2901    }
2902}
2903
2904/// Maximum number of clean (run) words that a single EWAH running-length word
2905/// can describe. The field is 32 bits wide (bits 1..=32 of the RLW).
2906pub(crate) const EWAH_MAX_RUNNING_LEN: u64 = 0xffff_ffff;
2907
2908/// Maximum number of literal (dirty) words that can trail a single EWAH
2909/// running-length word. The field is 31 bits wide (bits 33..=63 of the RLW).
2910pub(crate) const EWAH_MAX_LITERAL_LEN: u64 = 0x7fff_ffff;
2911
2912/// All-ones 64-bit word, used to recognise a "clean" run of set bits.
2913pub(crate) const EWAH_ALL_ONES: u64 = u64::MAX;
2914
2915impl EwahBitmap {
2916    /// Constructs an [`EwahBitmap`] in git's canonical EWAH compressed form
2917    /// from a slice of raw uncompressed 64-bit words.
2918    ///
2919    /// Within each word bit `i` corresponds to position `word_index * 64 + i`,
2920    /// matching git's on-disk convention. `bit_size` records the number of
2921    /// logical bits the bitmap spans; it must not exceed `words.len() * 64`.
2922    ///
2923    /// This mirrors libgit's `ewah_add`/`ewah_add_empty_words` incremental
2924    /// encoder: consecutive all-zero or all-one words collapse into a run, and
2925    /// any other word is stored verbatim as a literal. Only the first
2926    /// `bit_size.div_ceil(64)` words back the declared bits; any extra trailing
2927    /// words supplied by the caller are ignored, just as git encodes a bitmap
2928    /// sized to its highest set bit.
2929    pub fn from_words(bit_size: u32, words: &[u64]) -> Result<Self> {
2930        let required_words = bit_size.div_ceil(64) as usize;
2931        if required_words > words.len() {
2932            return Err(GitError::InvalidFormat(format!(
2933                "EWAH bit_size {bit_size} requires {required_words} words but only {} supplied",
2934                words.len()
2935            )));
2936        }
2937        // Only the words that actually back the declared bits matter; libgit
2938        // never emits clean trailing zero words for the unused tail.
2939        let significant = &words[..required_words];
2940        let mut builder = EwahBuilder::new(bit_size);
2941        for &word in significant {
2942            if word == 0 {
2943                builder.add_empty_words(false, 1);
2944            } else if word == EWAH_ALL_ONES {
2945                builder.add_empty_words(true, 1);
2946            } else {
2947                builder.add_literal(word);
2948            }
2949        }
2950        builder.finish()
2951    }
2952
2953    /// Constructs an [`EwahBitmap`] from a set of bit positions.
2954    ///
2955    /// `bit_size` is the number of logical bits (typically the pack object
2956    /// count). Every position in `positions` must be strictly less than
2957    /// `bit_size`. Positions may be given in any order and may repeat.
2958    pub fn from_positions(bit_size: u32, positions: &[u32]) -> Result<Self> {
2959        let word_count = bit_size.div_ceil(64) as usize;
2960        let mut words = vec![0u64; word_count];
2961        for &position in positions {
2962            if position >= bit_size {
2963                return Err(GitError::InvalidFormat(format!(
2964                    "EWAH bit position {position} out of range for bit_size {bit_size}"
2965                )));
2966            }
2967            let word_index = (position / 64) as usize;
2968            let bit_index = position % 64;
2969            words[word_index] |= 1u64 << bit_index;
2970        }
2971        Self::from_words(bit_size, &words)
2972    }
2973
2974    /// An empty EWAH bitmap (no bits, no words). This is what git writes for an
2975    /// all-zero type bitmap (e.g. when a pack has no tags).
2976    pub fn empty() -> Self {
2977        Self {
2978            bit_size: 0,
2979            words: Vec::new(),
2980            rlw_position: 0,
2981        }
2982    }
2983
2984    /// Decodes the compressed EWAH back into raw 64-bit words, LSB-first within
2985    /// each word. The returned vector has `bit_size.div_ceil(64)` entries.
2986    ///
2987    /// This is the inverse of [`EwahBitmap::from_words`] for the bits the
2988    /// bitmap actually covers and is primarily used to validate roundtrips.
2989    pub fn to_words(&self) -> Result<Vec<u64>> {
2990        let mut out = Vec::new();
2991        let mut word_idx = 0usize;
2992        while word_idx < self.words.len() {
2993            let rlw = self.words[word_idx];
2994            let run_bit = rlw & 1;
2995            let run_words = (rlw >> 1) & EWAH_MAX_RUNNING_LEN;
2996            let literal_words = (rlw >> 33) as usize;
2997            word_idx += 1;
2998            let fill = if run_bit == 1 { EWAH_ALL_ONES } else { 0 };
2999            for _ in 0..run_words {
3000                out.push(fill);
3001            }
3002            let literal_end = word_idx
3003                .checked_add(literal_words)
3004                .filter(|end| *end <= self.words.len())
3005                .ok_or_else(|| {
3006                    GitError::InvalidFormat("EWAH literal words extend past word table".into())
3007                })?;
3008            out.extend_from_slice(&self.words[word_idx..literal_end]);
3009            word_idx = literal_end;
3010        }
3011        let required_words = (self.bit_size as usize).div_ceil(64);
3012        if out.len() < required_words {
3013            out.resize(required_words, 0);
3014        }
3015        out.truncate(required_words);
3016        Ok(out)
3017    }
3018
3019    /// Returns the sorted set bit positions covered by this bitmap.
3020    pub fn to_positions(&self) -> Result<Vec<u32>> {
3021        let words = self.to_words()?;
3022        let mut positions = Vec::new();
3023        for (word_index, word) in words.iter().enumerate() {
3024            let mut remaining = *word;
3025            while remaining != 0 {
3026                let bit = remaining.trailing_zeros();
3027                let position = (word_index as u64) * 64 + u64::from(bit);
3028                if position < u64::from(self.bit_size) {
3029                    // position always fits in u32 because bit_size is u32.
3030                    positions.push(position as u32);
3031                }
3032                remaining &= remaining - 1;
3033            }
3034        }
3035        Ok(positions)
3036    }
3037
3038    /// Serialises the bitmap to git's on-disk EWAH byte layout: `bit_size`
3039    /// (u32 BE), word count (u32 BE), each compressed word (u64 BE), then the
3040    /// running-length-word position (u32 BE).
3041    pub fn to_bytes(&self) -> Vec<u8> {
3042        let mut out = Vec::with_capacity(12 + self.words.len() * 8);
3043        self.append_bytes(&mut out);
3044        out
3045    }
3046
3047    pub(crate) fn append_bytes(&self, out: &mut Vec<u8>) {
3048        out.extend_from_slice(&self.bit_size.to_be_bytes());
3049        out.extend_from_slice(&(self.words.len() as u32).to_be_bytes());
3050        for word in &self.words {
3051            out.extend_from_slice(&word.to_be_bytes());
3052        }
3053        out.extend_from_slice(&self.rlw_position.to_be_bytes());
3054    }
3055}
3056
3057/// Incremental EWAH compressed-buffer builder mirroring libgit's `ewah_add`.
3058///
3059/// The buffer is a sequence of blocks. Each block begins with a running-length
3060/// word (RLW) and is followed by zero or more literal words:
3061///   * bit 0      => value of the clean run words (0 or 1)
3062///   * bits 1..=32 => number of clean run words (32-bit field)
3063///   * bits 33..=63 => number of trailing literal words (31-bit field)
3064pub(crate) struct EwahBuilder {
3065    bit_size: u32,
3066    words: Vec<u64>,
3067    rlw_position: usize,
3068}
3069
3070impl EwahBuilder {
3071    pub(crate) fn new(bit_size: u32) -> Self {
3072        // Every EWAH buffer begins with an RLW, even an empty one.
3073        Self {
3074            bit_size,
3075            words: vec![0u64],
3076            rlw_position: 0,
3077        }
3078    }
3079
3080    pub(crate) fn rlw(&self) -> u64 {
3081        self.words[self.rlw_position]
3082    }
3083
3084    pub(crate) fn set_rlw(&mut self, value: u64) {
3085        self.words[self.rlw_position] = value;
3086    }
3087
3088    pub(crate) fn rlw_running_len(&self) -> u64 {
3089        (self.rlw() >> 1) & EWAH_MAX_RUNNING_LEN
3090    }
3091
3092    pub(crate) fn rlw_running_bit(&self) -> bool {
3093        self.rlw() & 1 == 1
3094    }
3095
3096    pub(crate) fn rlw_literal_len(&self) -> u64 {
3097        self.rlw() >> 33
3098    }
3099
3100    pub(crate) fn set_running_bit(&mut self, bit: bool) {
3101        let mut value = self.rlw();
3102        value &= !1;
3103        value |= u64::from(bit);
3104        self.set_rlw(value);
3105    }
3106
3107    pub(crate) fn set_running_len(&mut self, len: u64) {
3108        let mut value = self.rlw();
3109        value &= !(EWAH_MAX_RUNNING_LEN << 1);
3110        value |= (len & EWAH_MAX_RUNNING_LEN) << 1;
3111        self.set_rlw(value);
3112    }
3113
3114    pub(crate) fn set_literal_len(&mut self, len: u64) {
3115        let mut value = self.rlw();
3116        value &= (1u64 << 33) - 1;
3117        value |= (len & EWAH_MAX_LITERAL_LEN) << 33;
3118        self.set_rlw(value);
3119    }
3120
3121    /// Begins a fresh RLW block at the end of the buffer.
3122    pub(crate) fn push_rlw(&mut self) {
3123        self.rlw_position = self.words.len();
3124        self.words.push(0);
3125    }
3126
3127    /// Appends `number` clean words whose bits are all `value`, mirroring
3128    /// libgit's `ewah_add_empty_words`.
3129    ///
3130    /// A run can only be merged into the current RLW when that RLW has not yet
3131    /// emitted any literal words and its run either is empty or already carries
3132    /// the same fill value. Otherwise a fresh RLW block must be started, because
3133    /// every block stores its run strictly before its literals.
3134    pub(crate) fn add_empty_words(&mut self, value: bool, mut number: u64) {
3135        while number > 0 {
3136            // The current RLW can absorb more run words only when it has no
3137            // literals yet, its run is either empty or already the right fill
3138            // value, and the 32-bit run-length field is not already saturated.
3139            let can_extend = self.rlw_literal_len() == 0
3140                && (self.rlw_running_len() == 0 || self.rlw_running_bit() == value)
3141                && self.rlw_running_len() < EWAH_MAX_RUNNING_LEN;
3142            if !can_extend {
3143                self.push_rlw();
3144            }
3145            if self.rlw_running_len() == 0 {
3146                self.set_running_bit(value);
3147            }
3148            let available = EWAH_MAX_RUNNING_LEN - self.rlw_running_len();
3149            let take = available.min(number);
3150            self.set_running_len(self.rlw_running_len() + take);
3151            number -= take;
3152        }
3153    }
3154
3155    /// Appends a single literal (dirty) word verbatim, mirroring libgit's
3156    /// `ewah_add_dirty_words` for a count of one.
3157    pub(crate) fn add_literal(&mut self, word: u64) {
3158        if self.rlw_literal_len() >= EWAH_MAX_LITERAL_LEN {
3159            self.push_rlw();
3160        }
3161        let literal_len = self.rlw_literal_len();
3162        self.set_literal_len(literal_len + 1);
3163        self.words.push(word);
3164    }
3165
3166    pub(crate) fn finish(self) -> Result<EwahBitmap> {
3167        let rlw_position = u32::try_from(self.rlw_position)
3168            .map_err(|_| GitError::InvalidFormat("EWAH RLW position overflow".into()))?;
3169        if self.words.len() > u32::MAX as usize {
3170            return Err(GitError::InvalidFormat("EWAH word table overflow".into()));
3171        }
3172        Ok(EwahBitmap {
3173            bit_size: self.bit_size,
3174            words: self.words,
3175            rlw_position,
3176        })
3177    }
3178}