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