Skip to main content

sley_pack/
index.rs

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