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