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