1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct PackIndexProgress {
26 pub completed_objects: u64,
28 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 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 pub object_position: u32,
162 pub xor_offset: u8,
163 pub flags: u8,
164 pub bitmap: EwahBitmap,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct PackBitmapPseudoMerge {
171 pub commits: EwahBitmap,
174 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 pub fn parse_without_checksum(bytes: &'a [u8], format: ObjectFormat) -> Result<Self> {
248 Self::parse_impl(bytes, format, false, true)
249 }
250
251 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 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 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 pub fn lookup_at(&self, idx: usize) -> Option<PackIndexLookup> {
613 self.as_view().lookup_at(idx)
614 }
615
616 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 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 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 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 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 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}
1102pub 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 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 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 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 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 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
2739pub(crate) const EWAH_MAX_RUNNING_LEN: u64 = 0xffff_ffff;
2742
2743pub(crate) const EWAH_MAX_LITERAL_LEN: u64 = 0x7fff_ffff;
2746
2747pub(crate) const EWAH_ALL_ONES: u64 = u64::MAX;
2749
2750impl EwahBitmap {
2751 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 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 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 pub fn empty() -> Self {
2812 Self {
2813 bit_size: 0,
2814 words: Vec::new(),
2815 rlw_position: 0,
2816 }
2817 }
2818
2819 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 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 positions.push(position as u32);
2866 }
2867 remaining &= remaining - 1;
2868 }
2869 }
2870 Ok(positions)
2871 }
2872
2873 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
2892pub(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 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 pub(crate) fn push_rlw(&mut self) {
2958 self.rlw_position = self.words.len();
2959 self.words.push(0);
2960 }
2961
2962 pub(crate) fn add_empty_words(&mut self, value: bool, mut number: u64) {
2970 while number > 0 {
2971 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 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}