Skip to main content

sley_pack/
index.rs

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