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