1use crate::error::{Error, Result};
4use crate::hash::verify_trailer;
5use crate::objects::{HashAlgo, ObjectId};
6use crate::pack_map::PackData;
7use std::cmp::Ordering;
8use std::ops::Deref;
9use std::path::{Path, PathBuf};
10use std::sync::{Arc, Mutex};
11
12type OffsetLookupCache = Arc<Mutex<Option<Vec<(u64, u32)>>>>;
13
14const IDX_MIN_LEN: u64 = 256 * 4;
16
17#[derive(Debug, Clone)]
19struct PackIndexStorage(Arc<PackData>);
20
21impl Deref for PackIndexStorage {
22 type Target = [u8];
23
24 fn deref(&self) -> &[u8] {
25 self.0.deref()
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum PackIndexVersion {
32 V1,
33 V2,
34}
35
36#[derive(Debug, Clone)]
38struct PackIndexLayout {
39 version: PackIndexVersion,
40 hash_bytes: usize,
41 count: usize,
42 oid_base: usize,
43 crc_base: usize,
44 off32_base: usize,
45 large_off_base: usize,
46 large_off_count: usize,
47}
48
49#[derive(Debug, Clone)]
51pub struct PackIndex {
52 pub idx_path: PathBuf,
54 pub pack_path: PathBuf,
56 storage: PackIndexStorage,
57 layout: PackIndexLayout,
58 pub fanout: [u32; 256],
60 pub is_promisor: bool,
62 pub is_cruft: bool,
64 offset_lookup: OffsetLookupCache,
65}
66
67#[derive(Debug, Clone, Copy)]
69pub struct PackIndexEntryRef<'a> {
70 index: &'a PackIndex,
71 position: usize,
72}
73
74impl<'a> PackIndexEntryRef<'a> {
75 #[must_use]
77 pub fn oid(&self) -> &'a [u8] {
78 self.index.oid_at(self.position)
79 }
80
81 pub fn offset(&self) -> Result<u64> {
87 self.index.offset_at(self.position)
88 }
89
90 #[must_use]
92 pub fn crc32(&self) -> Option<u32> {
93 self.index.crc32_at(self.position)
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct PackIndexEntry {
100 pub oid: Vec<u8>,
102 pub offset: u64,
104 pub crc32: Option<u32>,
106}
107
108impl TryFrom<PackIndexEntryRef<'_>> for PackIndexEntry {
109 type Error = Error;
110
111 fn try_from(r: PackIndexEntryRef<'_>) -> Result<Self> {
112 Ok(Self {
113 oid: r.oid().to_vec(),
114 offset: r.offset()?,
115 crc32: r.crc32(),
116 })
117 }
118}
119
120pub struct PackIndexIter<'a> {
122 index: &'a PackIndex,
123 next: usize,
124}
125
126impl<'a> Iterator for PackIndexIter<'a> {
127 type Item = PackIndexEntryRef<'a>;
128
129 fn next(&mut self) -> Option<Self::Item> {
130 if self.next >= self.index.len() {
131 return None;
132 }
133 let pos = self.next;
134 self.next += 1;
135 Some(PackIndexEntryRef {
136 index: self.index,
137 position: pos,
138 })
139 }
140
141 fn size_hint(&self) -> (usize, Option<usize>) {
142 let rem = self.index.len().saturating_sub(self.next);
143 (rem, Some(rem))
144 }
145}
146
147impl ExactSizeIterator for PackIndexIter<'_> {}
148
149impl PackIndex {
150 #[must_use]
152 pub fn len(&self) -> usize {
153 self.layout.count
154 }
155
156 #[must_use]
158 pub fn is_empty(&self) -> bool {
159 self.layout.count == 0
160 }
161
162 #[must_use]
164 pub fn hash_bytes(&self) -> usize {
165 self.layout.hash_bytes
166 }
167
168 #[must_use]
170 pub fn oid_at(&self, i: usize) -> &[u8] {
171 debug_assert!(i < self.layout.count);
172 let start = match self.layout.version {
173 PackIndexVersion::V1 => {
174 let record = 4 + self.layout.hash_bytes;
175 self.layout.oid_base + i * record + 4
176 }
177 PackIndexVersion::V2 => self.layout.oid_base + i * self.layout.hash_bytes,
178 };
179 let end = start + self.layout.hash_bytes;
180 &self.storage[start..end]
181 }
182
183 pub fn offset_at(&self, i: usize) -> Result<u64> {
190 if i >= self.layout.count {
191 return Err(Error::CorruptObject(format!(
192 "index row {i} out of range in {}",
193 self.idx_path.display()
194 )));
195 }
196 match self.layout.version {
197 PackIndexVersion::V1 => {
198 let start = self.layout.oid_base + i * (4 + self.layout.hash_bytes);
199 Ok(read_u32_be_slice(&self.storage, start) as u64)
200 }
201 PackIndexVersion::V2 => {
202 let start = self.layout.off32_base + i * 4;
203 let raw = read_u32_be_slice(&self.storage, start);
204 self.decode_v2_offset_at(i, raw)
205 }
206 }
207 }
208
209 fn decode_v2_offset_at(&self, _row: usize, raw: u32) -> Result<u64> {
210 if (raw & 0x8000_0000) == 0 {
211 return Ok(u64::from(raw));
212 }
213 let slot = (raw & 0x7fff_ffff) as usize;
214 if slot >= self.layout.large_off_count {
215 return Err(Error::CorruptObject(format!(
216 "large offset slot {slot} out of range ({} slots) in {}",
217 self.layout.large_off_count,
218 self.idx_path.display()
219 )));
220 }
221 let loff = self.layout.large_off_base + slot * 8;
222 let trailer = self.layout.hash_bytes * 2;
223 if loff + 8 > self.storage.len().saturating_sub(trailer) {
224 return Err(Error::CorruptObject(format!(
225 "truncated large offset table in {}",
226 self.idx_path.display()
227 )));
228 }
229 Ok(read_u64_be_slice(&self.storage, loff))
230 }
231
232 #[must_use]
234 pub fn crc32_at(&self, i: usize) -> Option<u32> {
235 debug_assert!(i < self.layout.count);
236 if self.layout.version != PackIndexVersion::V2 {
237 return None;
238 }
239 let start = self.layout.crc_base + i * 4;
240 Some(read_u32_be_slice(&self.storage, start))
241 }
242
243 #[must_use]
245 pub fn find_position(&self, oid: &ObjectId) -> Option<usize> {
246 let needle = oid.as_bytes();
247 if needle.len() != self.layout.hash_bytes {
248 return None;
249 }
250 let first = needle[0] as usize;
251 let lo = if first == 0 {
252 0
253 } else {
254 self.fanout[first - 1] as usize
255 };
256 let hi = self.fanout[first] as usize;
257 if lo >= hi || hi > self.layout.count {
258 return None;
259 }
260 self.find_position_in_bucket(lo, hi, needle)
261 }
262
263 pub fn find_offset(&self, oid: &ObjectId) -> Result<Option<u64>> {
269 match self.find_position(oid) {
270 Some(i) => Ok(Some(self.offset_at(i)?)),
271 None => Ok(None),
272 }
273 }
274
275 pub fn find_offset_monotonic(&self, oid: &ObjectId, cursor: &mut usize) -> Result<Option<u64>> {
280 const FORWARD_SCAN_MAX: usize = 128;
281 let needle = oid.as_bytes();
282 if needle.len() != self.layout.hash_bytes {
283 return Ok(None);
284 }
285 let len = self.len();
286 if len == 0 {
287 return Ok(None);
288 }
289 if *cursor >= len {
290 *cursor = 0;
291 }
292 if *cursor < len && compare_oid_bytes(self.oid_at(*cursor), needle) == Ordering::Greater {
293 if let Some(pos) = self.find_position(oid) {
294 *cursor = pos;
295 return Ok(Some(self.offset_at(pos)?));
296 }
297 return Ok(None);
298 }
299 let mut pos = *cursor;
300 let scan_start = pos;
301 let window_last = (scan_start + FORWARD_SCAN_MAX).min(len) - 1;
304 if compare_oid_bytes(self.oid_at(window_last), needle) == Ordering::Less {
305 if let Some(found) = self.find_position(oid) {
306 *cursor = found;
307 return Ok(Some(self.offset_at(found)?));
308 }
309 return Ok(None);
310 }
311 while pos < len && pos - scan_start < FORWARD_SCAN_MAX {
312 match compare_oid_bytes(self.oid_at(pos), needle) {
313 Ordering::Less => pos += 1,
314 Ordering::Equal => {
315 *cursor = pos;
316 return Ok(Some(self.offset_at(pos)?));
317 }
318 Ordering::Greater => return Ok(None),
319 }
320 }
321 if pos < len {
322 if let Some(found) = self.find_position(oid) {
323 *cursor = found;
324 return Ok(Some(self.offset_at(found)?));
325 }
326 return Ok(None);
327 }
328 Ok(None)
329 }
330 pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
332 Ok(self.find_offset(oid)?.is_some())
333 }
334
335 pub fn iter(&self) -> PackIndexIter<'_> {
337 PackIndexIter {
338 index: self,
339 next: 0,
340 }
341 }
342
343 pub fn next_pack_offset_after(&self, entry_offset: u64, pack_end: u64) -> Result<u64> {
351 self.with_sorted_offsets(|pairs| {
352 let next = pairs.partition_point(|p| p.0 <= entry_offset);
353 pairs
354 .get(next)
355 .map_or(pack_end, |&(off, _)| off.min(pack_end))
356 })
357 }
358
359 fn with_sorted_offsets<R>(&self, f: impl FnOnce(&[(u64, u32)]) -> R) -> Result<R> {
365 let mut guard = self.offset_lookup.lock().unwrap_or_else(|e| e.into_inner());
366 if let Some(pairs) = guard.as_deref() {
367 return Ok(f(pairs));
368 }
369 let pairs = match self.sorted_offsets_from_rev()? {
370 Some(pairs) => pairs,
371 None => {
372 let mut pairs: Vec<(u64, u32)> = Vec::with_capacity(self.len());
373 for i in 0..self.len() {
374 pairs.push((self.offset_at(i)?, i as u32));
375 }
376 pairs.sort_unstable_by_key(|p| p.0);
377 pairs
378 }
379 };
380 Ok(f(guard.insert(pairs)))
381 }
382
383 fn sorted_offsets_from_rev(&self) -> Result<Option<Vec<(u64, u32)>>> {
388 let rev_path = crate::pack_rev::rev_path_for_index(&self.idx_path);
389 let Ok(data) = PackData::open_with_min_len(&rev_path, 12) else {
390 return Ok(None);
391 };
392 let Some(rows) = crate::pack_rev::try_rev_positions_in_pack_order(&data, self.len()) else {
393 return Ok(None);
394 };
395 let mut pairs: Vec<(u64, u32)> = Vec::with_capacity(rows.len());
396 for row in rows {
397 let offset = self.offset_at(row as usize)?;
398 if pairs.last().is_some_and(|&(prev, _)| prev >= offset) {
399 return Ok(None);
400 }
401 pairs.push((offset, row));
402 }
403 Ok(Some(pairs))
404 }
405
406 pub fn crc32_for_pack_offset(&self, entry_offset: u64) -> Result<Option<u32>> {
408 Ok(match self.find_position_by_offset_sorted(entry_offset)? {
409 Some(i) => self.crc32_at(i),
410 None => None,
411 })
412 }
413
414 #[must_use]
416 pub(crate) fn with_sidecar_flags(&self, is_promisor: bool, is_cruft: bool) -> Self {
417 let mut next = self.clone();
418 next.is_promisor = is_promisor;
419 next.is_cruft = is_cruft;
420 next
421 }
422
423 pub fn find_position_by_offset_sorted(&self, offset: u64) -> Result<Option<usize>> {
434 self.with_sorted_offsets(|pairs| {
435 pairs
436 .binary_search_by_key(&offset, |p| p.0)
437 .ok()
438 .map(|i| pairs[i].1 as usize)
439 })
440 }
441
442 fn find_position_in_bucket(
443 &self,
444 mut lo: usize,
445 mut hi: usize,
446 needle: &[u8],
447 ) -> Option<usize> {
448 if hi - lo < 64 {
449 return self.binary_search(lo, hi, needle);
450 }
451
452 let fraction = u32::from_be_bytes([0, needle[1], needle[2], needle[3]]);
456 let guess = lo + ((u64::from(fraction) * (hi - lo) as u64) >> 24) as usize;
457 let mut step = 1usize;
458 match compare_oid_bytes(self.oid_at(guess), needle) {
459 Ordering::Equal => return Some(guess),
460 Ordering::Less => {
461 lo = guess + 1;
462 while lo < hi {
463 let pos = guess.saturating_add(step).min(hi - 1);
464 match compare_oid_bytes(self.oid_at(pos), needle) {
465 Ordering::Equal => return Some(pos),
466 Ordering::Greater => {
467 hi = pos;
468 break;
469 }
470 Ordering::Less => lo = pos + 1,
471 }
472 step = step.saturating_mul(2);
473 }
474 }
475 Ordering::Greater => {
476 hi = guess;
477 while lo < hi {
478 let pos = guess.saturating_sub(step).max(lo);
479 match compare_oid_bytes(self.oid_at(pos), needle) {
480 Ordering::Equal => return Some(pos),
481 Ordering::Less => {
482 lo = pos + 1;
483 break;
484 }
485 Ordering::Greater => hi = pos,
486 }
487 step = step.saturating_mul(2);
488 }
489 }
490 }
491 self.binary_search(lo, hi, needle)
494 }
495
496 fn binary_search(&self, mut lo: usize, mut hi: usize, needle: &[u8]) -> Option<usize> {
497 while lo < hi {
498 let mid = lo + (hi - lo) / 2;
499 match compare_oid_bytes(self.oid_at(mid), needle) {
500 Ordering::Less => lo = mid + 1,
501 Ordering::Greater => hi = mid,
502 Ordering::Equal => return Some(mid),
503 }
504 }
505 None
506 }
507}
508
509#[inline]
511fn compare_oid_bytes(a: &[u8], b: &[u8]) -> Ordering {
512 if a.len() == 20 && b.len() == 20 {
513 let a0 = u64::from_be_bytes([a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]]);
514 let b0 = u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
515 if a0 != b0 {
516 return a0.cmp(&b0);
517 }
518 let a1 = u64::from_be_bytes([a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15]]);
519 let b1 = u64::from_be_bytes([b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]]);
520 if a1 != b1 {
521 return a1.cmp(&b1);
522 }
523 let a2 = u32::from_be_bytes([a[16], a[17], a[18], a[19]]);
524 let b2 = u32::from_be_bytes([b[16], b[17], b[18], b[19]]);
525 return a2.cmp(&b2);
526 }
527 a.cmp(b)
528}
529
530const OID_INTERP_PREFIX_LEN: usize = 8;
532
533pub(crate) fn oid_interp_prefix_u64(oid: &[u8]) -> u64 {
534 let take = oid.len().min(OID_INTERP_PREFIX_LEN);
535 let mut v = 0u64;
536 for &b in &oid[..take] {
537 v = (v << 8) | u64::from(b);
538 }
539 for _ in take..OID_INTERP_PREFIX_LEN {
540 v <<= 8;
541 }
542 v
543}
544
545#[must_use]
547pub fn pack_index_entry_matches_sha1_oid(entry: &PackIndexEntryRef<'_>, oid: &ObjectId) -> bool {
548 entry.oid().len() == 20 && entry.oid() == oid.as_bytes()
549}
550
551#[must_use]
553pub fn compute_fanout_from_oid_slices(entries: &[&[u8]]) -> [u32; 256] {
554 let mut fanout = [0u32; 256];
555 let mut idx = 0usize;
556 for byte in 0u32..256 {
557 let needle = byte as u8;
558 while idx < entries.len() && entries[idx].first().copied().unwrap_or(0) <= needle {
559 idx += 1;
560 }
561 fanout[byte as usize] = u32::try_from(idx).unwrap_or(u32::MAX);
562 }
563 fanout
564}
565
566#[must_use]
568pub fn compute_fanout_from_entries(entries: &[PackIndexEntry]) -> [u32; 256] {
569 let slices: Vec<&[u8]> = entries.iter().map(|e| e.oid.as_slice()).collect();
570 compute_fanout_from_oid_slices(&slices)
571}
572
573pub fn parse_pack_index_bytes(idx_path: &Path, bytes: Vec<u8>, verify: bool) -> Result<PackIndex> {
575 if bytes.len() < 8 {
576 return Err(Error::CorruptObject(format!(
577 "index file {} is too small",
578 idx_path.display()
579 )));
580 }
581 let storage = PackIndexStorage(PackData::from_owned(bytes));
582 parse_pack_index_storage(idx_path, &storage, verify)
583}
584
585pub fn open_pack_index_file(idx_path: &Path, verify: bool) -> Result<PackIndex> {
592 let storage = PackIndexStorage(PackData::open_with_min_len(idx_path, IDX_MIN_LEN)?);
593 parse_pack_index_storage(idx_path, &storage, verify)
594}
595
596fn parse_pack_index_storage(
597 idx_path: &Path,
598 storage: &PackIndexStorage,
599 verify: bool,
600) -> Result<PackIndex> {
601 if storage.len() < 8 {
602 return Err(Error::CorruptObject(format!(
603 "index file {} is too small",
604 idx_path.display()
605 )));
606 }
607 let magic = &storage[0..4];
608 if magic == [0xff, b't', b'O', b'c'] {
609 read_pack_index_v2(idx_path, storage, verify)
610 } else {
611 read_pack_index_v1(idx_path, storage, verify)
612 }
613}
614
615fn read_pack_index_v1(
616 idx_path: &Path,
617 storage: &PackIndexStorage,
618 verify: bool,
619) -> Result<PackIndex> {
620 let bytes = storage.deref();
621 let mut pos = 0usize;
622 if bytes.len() < 256 * 4 + 20 {
623 return Err(Error::CorruptObject(format!(
624 "index file {} is too small",
625 idx_path.display()
626 )));
627 }
628 let mut fanout = [0u32; 256];
629 for slot in &mut fanout {
630 *slot = read_u32_be(bytes, &mut pos)?;
631 }
632 check_fanout_monotonic(&fanout, idx_path)?;
633 let object_count = fanout[255] as usize;
634 let record_size = 4 + 20;
635 let need = pos
636 .saturating_add(object_count.saturating_mul(record_size))
637 .saturating_add(20);
638 if bytes.len() < need {
639 return Err(Error::CorruptObject(format!(
640 "truncated idx file {}",
641 idx_path.display()
642 )));
643 }
644 let oid_base = pos;
645 if verify {
646 for i in 0..object_count {
647 let off_start = oid_base + i * record_size;
648 let oid_start = off_start + 4;
649 if i > 0 {
650 let prev_start = oid_base + (i - 1) * record_size + 4;
651 if bytes[prev_start..prev_start + 20] > bytes[oid_start..oid_start + 20] {
652 return Err(Error::CorruptObject(format!(
653 "oid lookup out of order in {}",
654 idx_path.display()
655 )));
656 }
657 }
658 }
659 }
660 if verify {
661 verify_idx_trailing_checksum(idx_path, bytes, 20)?;
662 }
663 let mut pack_path = idx_path.to_path_buf();
664 pack_path.set_extension("pack");
665 Ok(PackIndex {
666 idx_path: idx_path.to_path_buf(),
667 pack_path,
668 storage: storage.clone(),
669 layout: PackIndexLayout {
670 version: PackIndexVersion::V1,
671 hash_bytes: 20,
672 count: object_count,
673 oid_base,
674 crc_base: 0,
675 off32_base: 0,
676 large_off_base: 0,
677 large_off_count: 0,
678 },
679 fanout,
680 is_promisor: false,
681 is_cruft: false,
682 offset_lookup: Arc::new(Mutex::new(None)),
683 })
684}
685
686fn read_pack_index_v2(
687 idx_path: &Path,
688 storage: &PackIndexStorage,
689 verify: bool,
690) -> Result<PackIndex> {
691 let bytes = storage.deref();
692 if bytes.len() < 8 + 256 * 4 + 40 {
693 return Err(Error::CorruptObject(format!(
694 "index file {} is too small",
695 idx_path.display()
696 )));
697 }
698 let mut pos = 4usize;
699 let version = read_u32_be(bytes, &mut pos)?;
700 if version != 2 {
701 return Err(Error::CorruptObject(format!(
702 "unsupported idx version {version} in {}",
703 idx_path.display()
704 )));
705 }
706 let mut fanout = [0u32; 256];
707 for slot in &mut fanout {
708 *slot = read_u32_be(bytes, &mut pos)?;
709 }
710 check_fanout_monotonic(&fanout, idx_path)?;
711 let object_count = fanout[255] as usize;
712 let idx_file_len = bytes.len();
713 let hash_bytes = detect_idx_hash_bytes_v2(idx_file_len, pos, object_count, idx_path)?;
714 let oid_base = pos;
715 let crc_base = oid_base + object_count * hash_bytes;
716 let off32_base = crc_base + object_count * 4;
717 let need = off32_base + object_count * 4;
718 if bytes.len() < need {
719 return Err(Error::CorruptObject(format!(
720 "truncated idx file {}",
721 idx_path.display()
722 )));
723 }
724 let large_off_base = off32_base + object_count * 4;
725 let large_off_count = if verify {
726 pos = off32_base;
727 let mut count = 0usize;
728 let mut next_large = 0usize;
729 for i in 0..object_count {
730 let v = read_u32_be(bytes, &mut pos)?;
731 if (v & 0x8000_0000) != 0 {
732 let slot = (v & 0x7fff_ffff) as usize;
733 if slot != next_large {
734 return Err(Error::CorruptObject(format!(
735 "inconsistent 64b offset index at entry {i} in {}",
736 idx_path.display()
737 )));
738 }
739 next_large += 1;
740 count += 1;
741 }
742 }
743 count
744 } else {
745 let tail = hash_bytes * 2;
746 if bytes.len() < large_off_base + tail {
747 return Err(Error::CorruptObject(format!(
748 "truncated idx file {}",
749 idx_path.display()
750 )));
751 }
752 (bytes.len() - large_off_base - tail) / 8
753 };
754 if bytes.len() < large_off_base + large_off_count * 8 + 2 * hash_bytes {
755 return Err(Error::CorruptObject(format!(
756 "truncated large offset table in {}",
757 idx_path.display()
758 )));
759 }
760 if verify {
761 for i in 0..object_count {
762 let start = oid_base + i * hash_bytes;
763 if i > 0 {
764 let prev = oid_base + (i - 1) * hash_bytes;
765 if bytes[prev..prev + hash_bytes] > bytes[start..start + hash_bytes] {
766 return Err(Error::CorruptObject(format!(
767 "oid lookup out of order in {}",
768 idx_path.display()
769 )));
770 }
771 }
772 }
773 }
774 if verify {
775 verify_idx_trailing_checksum(idx_path, bytes, hash_bytes)?;
776 }
777 let mut pack_path = idx_path.to_path_buf();
778 pack_path.set_extension("pack");
779 Ok(PackIndex {
780 idx_path: idx_path.to_path_buf(),
781 pack_path,
782 storage: storage.clone(),
783 layout: PackIndexLayout {
784 version: PackIndexVersion::V2,
785 hash_bytes,
786 count: object_count,
787 oid_base,
788 crc_base,
789 off32_base,
790 large_off_base,
791 large_off_count,
792 },
793 fanout,
794 is_promisor: false,
795 is_cruft: false,
796 offset_lookup: Arc::new(Mutex::new(None)),
797 })
798}
799
800fn verify_idx_trailing_checksum(idx_path: &Path, bytes: &[u8], hash_bytes: usize) -> Result<()> {
801 let Some(algo) = HashAlgo::from_len(hash_bytes) else {
802 return Err(Error::CorruptObject(format!(
803 "unsupported index hash width {hash_bytes}"
804 )));
805 };
806 verify_trailer(algo, bytes).map_err(|mismatch| Error::PackIndexChecksumMismatch {
807 path: idx_path.to_path_buf(),
808 mismatch,
809 })
810}
811
812fn check_fanout_monotonic(fanout: &[u32; 256], idx_path: &Path) -> Result<()> {
813 let mut prev = 0u32;
814 for &n in fanout {
815 if n < prev {
816 return Err(Error::CorruptObject(format!(
817 "non-monotonic index {}",
818 idx_path.display()
819 )));
820 }
821 prev = n;
822 }
823 Ok(())
824}
825
826fn detect_idx_hash_bytes_v2(
827 idx_file_len: usize,
828 fanout_end: usize,
829 object_count: usize,
830 idx_path: &Path,
831) -> Result<usize> {
832 if object_count == 0 {
833 let sha1_len = fanout_end.saturating_add(2 * 20);
834 let sha256_len = fanout_end.saturating_add(2 * 32);
835 return match idx_file_len {
836 n if n == sha1_len => Ok(20),
837 n if n == sha256_len => Ok(32),
838 _ => Err(Error::CorruptObject(format!(
839 "wrong index v2 file size in {}",
840 idx_path.display()
841 ))),
842 };
843 }
844 for &hb in &[20usize, 32] {
845 let fixed = fanout_end
846 .saturating_add(object_count.saturating_mul(hb + 4 + 4))
847 .saturating_add(2 * hb);
848 if idx_file_len < fixed {
849 continue;
850 }
851 let extra = idx_file_len - fixed;
852 if !extra.is_multiple_of(8) {
853 continue;
854 }
855 if extra / 8 > object_count {
856 continue;
857 }
858 return Ok(hb);
859 }
860 Err(Error::CorruptObject(format!(
861 "wrong index v2 file size in {}",
862 idx_path.display()
863 )))
864}
865
866fn read_u32_be_slice(bytes: &[u8], start: usize) -> u32 {
867 u32::from_be_bytes([
868 bytes[start],
869 bytes[start + 1],
870 bytes[start + 2],
871 bytes[start + 3],
872 ])
873}
874
875fn read_u64_be_slice(bytes: &[u8], start: usize) -> u64 {
876 u64::from_be_bytes([
877 bytes[start],
878 bytes[start + 1],
879 bytes[start + 2],
880 bytes[start + 3],
881 bytes[start + 4],
882 bytes[start + 5],
883 bytes[start + 6],
884 bytes[start + 7],
885 ])
886}
887
888fn read_u32_be(bytes: &[u8], pos: &mut usize) -> Result<u32> {
889 if bytes.len() < *pos + 4 {
890 return Err(Error::CorruptObject(
891 "unexpected end of idx while reading u32".to_owned(),
892 ));
893 }
894 let v = u32::from_be_bytes(
895 bytes[*pos..*pos + 4]
896 .try_into()
897 .map_err(|_| Error::CorruptObject("failed to parse u32".to_owned()))?,
898 );
899 *pos += 4;
900 Ok(v)
901}
902
903#[cfg(test)]
904mod tests {
905 use super::*;
906 use crate::objects::ObjectId;
907
908 #[test]
909 fn pack_index_api_coverage_smoke() {
910 let oid_a = vec![0x01u8; 20];
911 let oid_b = vec![0x02u8; 20];
912 let fanout = compute_fanout_from_oid_slices(&[&oid_a, &oid_b]);
913 assert_eq!(fanout[1], 1);
914 assert_eq!(fanout[2], 2);
915 }
916
917 #[test]
918 fn find_position_empty_bucket() {
919 let entries = vec![PackIndexEntry {
920 oid: vec![0x05; 20],
921 offset: 1,
922 crc32: None,
923 }];
924 let fanout = compute_fanout_from_entries(&entries);
925 let idx = PackIndex {
926 idx_path: PathBuf::from("t.idx"),
927 pack_path: PathBuf::from("t.pack"),
928 storage: PackIndexStorage(PackData::from_owned(Vec::new())),
929 layout: PackIndexLayout {
930 version: PackIndexVersion::V1,
931 hash_bytes: 20,
932 count: 0,
933 oid_base: 0,
934 crc_base: 0,
935 off32_base: 0,
936 large_off_base: 0,
937 large_off_count: 0,
938 },
939 fanout,
940 is_promisor: false,
941 is_cruft: false,
942 offset_lookup: Arc::new(Mutex::new(None)),
943 };
944 let miss = ObjectId::from_bytes(&[0x06; 20]).unwrap();
945 assert!(idx.find_position(&miss).is_none());
946 let _ = idx.len();
947 let _ = idx.is_empty();
948 let _ = idx.hash_bytes();
949 let _ = idx.iter().next();
950 }
951
952 fn build_v1_idx_bytes(entries: &[(Vec<u8>, u64)]) -> Vec<u8> {
953 let mut sorted = entries.to_vec();
954 sorted.sort_by(|a, b| a.0.cmp(&b.0));
955 let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
956 let fanout = compute_fanout_from_oid_slices(&slices);
957 let mut body = Vec::new();
958 for f in fanout {
959 body.extend_from_slice(&f.to_be_bytes());
960 }
961 for (oid, off) in sorted {
962 body.extend_from_slice(&(off as u32).to_be_bytes());
963 body.extend_from_slice(&oid);
964 }
965 body.extend_from_slice(&crate::objects::HashAlgo::Sha1.checksum_digest(&body));
966 body
967 }
968
969 #[test]
970 fn find_position_large_fanout_bucket_sha1_v1_no_panic() {
971 let mut rows = Vec::new();
972 for i in 0..64u8 {
973 let mut oid = vec![0x05u8; 20];
974 oid[19] = i;
975 rows.push((oid, u64::from(i) * 12 + 100));
976 }
977 let body = build_v1_idx_bytes(&rows);
978 let idx =
979 parse_pack_index_bytes(Path::new("big-v1.idx"), body, false).expect("parse v1 idx");
980 assert_eq!(idx.len(), 64);
981 for (oid, _) in &rows {
982 let id = ObjectId::from_bytes(oid).expect("oid");
983 assert!(
984 idx.find_position(&id).is_some(),
985 "missing {}",
986 hex::encode(oid)
987 );
988 }
989 let miss = ObjectId::from_bytes(&[0x06; 20]).unwrap();
990 assert!(idx.find_position(&miss).is_none());
991 }
992
993 #[test]
994 fn predicted_lookup_matches_sorted_oracle_across_index_layouts() {
995 for hash_bytes in [20, 32] {
996 let rows: Vec<_> = [0u8, 0x55, 0xfe, 0xff]
998 .into_iter()
999 .flat_map(|first| {
1000 (0..1024u32).map(move |i| {
1001 let mut bytes = vec![0; hash_bytes];
1002 bytes[..4].copy_from_slice(&i.wrapping_mul(2_654_435_761).to_be_bytes());
1003 bytes[0] = first;
1004 bytes[hash_bytes - 4..].copy_from_slice(&(i * 2).to_be_bytes());
1005 (bytes, 12 + u64::from(i) * 40)
1006 })
1007 })
1008 .collect();
1009 assert_lookup_oracle(&rows, hash_bytes);
1010 }
1011 }
1012
1013 #[test]
1014 fn predicted_lookup_handles_identical_prefixes_and_bucket_edges() {
1015 for hash_bytes in [20, 32] {
1016 for count in [0, 1, 63, 64, 65, 1025] {
1017 for prefix in [0, 0xff] {
1018 let rows: Vec<_> = (0..count)
1019 .map(|i: u32| {
1020 let mut bytes = vec![prefix; hash_bytes];
1021 bytes[hash_bytes - 4..].copy_from_slice(&(2 * i + 2).to_be_bytes());
1022 (bytes, 12 + u64::from(i) * 40)
1023 })
1024 .collect();
1025 assert_lookup_oracle(&rows, hash_bytes);
1026 }
1027 }
1028 }
1029 }
1030
1031 fn assert_lookup_oracle(rows: &[(Vec<u8>, u64)], hash_bytes: usize) {
1032 let tmp = tempfile::tempdir().expect("tempdir");
1033 let path = tmp.path().join("lookup.idx");
1034 let entries: Vec<_> = rows
1035 .iter()
1036 .map(|(bytes, offset)| (ObjectId::from_bytes(bytes).expect("oid"), *offset, 0))
1037 .collect();
1038 crate::pack::write_v2_pack_index_with_trailer(
1039 &path,
1040 &entries,
1041 &vec![0; hash_bytes],
1042 hash_bytes,
1043 None,
1044 )
1045 .expect("write v2");
1046 let mut indexes = vec![open_pack_index_file(&path, true).expect("v2")];
1047 if hash_bytes == 20 {
1048 indexes
1049 .push(parse_pack_index_bytes(&path, build_v1_idx_bytes(rows), true).expect("v1"));
1050 }
1051 let mut sorted: Vec<_> = entries.iter().map(|(oid, _, _)| *oid).collect();
1052 sorted.sort();
1053 let probes = sorted.iter().flat_map(|oid| {
1054 let mut missing = oid.as_bytes().to_vec();
1055 missing[hash_bytes - 1] ^= 1;
1056 [*oid, ObjectId::from_bytes(&missing).expect("nearby oid")]
1057 });
1058 let edges = (0..=255).flat_map(|first| {
1059 let mut low = vec![0; hash_bytes];
1060 low[0] = first;
1061 let mut high = vec![0xff; hash_bytes];
1062 high[0] = first;
1063 [
1064 ObjectId::from_bytes(&low).expect("low oid"),
1065 ObjectId::from_bytes(&high).expect("high oid"),
1066 ]
1067 });
1068 for probe in probes.chain(edges) {
1069 let expected = sorted.binary_search(&probe).ok();
1070 for index in &indexes {
1071 assert_eq!(index.find_position(&probe), expected, "probe {probe}");
1072 }
1073 }
1074 }
1075
1076 #[test]
1077 fn find_offset_monotonic_matches_find_offset_for_any_probe_order() {
1078 let rows: Vec<(Vec<u8>, u64)> = (0..1200u32)
1080 .map(|i| {
1081 let mut oid = vec![0u8; 20];
1082 oid[..4].copy_from_slice(&i.wrapping_mul(2_654_435_761).to_be_bytes());
1083 oid[19] = 1;
1084 (oid, 12 + u64::from(i) * 40)
1085 })
1086 .collect();
1087 let idx = parse_pack_index_bytes(Path::new("mono.idx"), build_v1_idx_bytes(&rows), false)
1088 .expect("parse v1 idx");
1089 let mut sorted: Vec<ObjectId> = rows
1090 .iter()
1091 .map(|(oid, _)| ObjectId::from_bytes(oid).expect("oid"))
1092 .collect();
1093 sorted.sort();
1094 let missing = |oid: &ObjectId| {
1095 let mut bytes = oid.as_bytes().to_vec();
1096 bytes[19] = 2;
1097 ObjectId::from_bytes(&bytes).expect("oid")
1098 };
1099 let mut probes: Vec<ObjectId> = Vec::new();
1100 probes.extend(sorted.iter().copied());
1101 probes.extend(sorted.iter().step_by(300).copied());
1102 probes.extend(sorted.iter().rev().step_by(7).copied());
1103 probes.extend(sorted.iter().step_by(11).map(missing));
1104 probes.extend(
1105 rows.iter()
1106 .map(|(oid, _)| ObjectId::from_bytes(oid).expect("oid")),
1107 );
1108 let mut cursor = 0usize;
1109 for probe in &probes {
1110 assert_eq!(
1111 idx.find_offset_monotonic(probe, &mut cursor)
1112 .expect("monotonic"),
1113 idx.find_offset(probe).expect("binary search"),
1114 "probe {probe}"
1115 );
1116 }
1117
1118 let empty = parse_pack_index_bytes(Path::new("empty.idx"), build_v1_idx_bytes(&[]), false)
1119 .expect("parse empty idx");
1120 let mut cursor = 5usize;
1121 assert_eq!(
1122 empty
1123 .find_offset_monotonic(&sorted[0], &mut cursor)
1124 .expect("empty"),
1125 None
1126 );
1127 }
1128
1129 #[test]
1130 fn next_pack_offset_after_matches_linear_scan() {
1131 let rows: Vec<(Vec<u8>, u64)> = (0..50u8)
1133 .map(|i| {
1134 let mut oid = vec![i; 20];
1135 oid[0] = i.wrapping_mul(37);
1136 (oid, 12 + (u64::from(i) * 7 % 50) * 30)
1137 })
1138 .collect();
1139 let idx = parse_pack_index_bytes(Path::new("order.idx"), build_v1_idx_bytes(&rows), false)
1140 .expect("parse v1 idx");
1141 let offsets: Vec<u64> = rows.iter().map(|r| r.1).collect();
1142 let pack_end = 12 + 50 * 30 - 10;
1143 for probe in (0..pack_end + 40).step_by(5) {
1144 let expected = offsets
1145 .iter()
1146 .copied()
1147 .filter(|&o| o > probe && o <= pack_end)
1148 .min()
1149 .unwrap_or(pack_end);
1150 assert_eq!(
1151 idx.next_pack_offset_after(probe, pack_end).expect("next"),
1152 expected,
1153 "probe {probe}"
1154 );
1155 }
1156 for (i, &off) in offsets.iter().enumerate() {
1157 let pos = idx.find_position_by_offset_sorted(off).expect("lookup");
1158 let pos = pos.expect("every row offset resolves");
1159 assert_eq!(idx.offset_at(pos).expect("offset"), off, "row {i}");
1160 }
1161 assert_eq!(idx.find_position_by_offset_sorted(13).expect("miss"), None);
1162 }
1163
1164 #[test]
1165 fn detect_empty_v2_index_hash_width_from_file_size() {
1166 let fanout_end = 8 + 256 * 4;
1167 assert_eq!(
1168 detect_idx_hash_bytes_v2(1072, fanout_end, 0, Path::new("t.idx")).expect("sha1"),
1169 20
1170 );
1171 assert_eq!(
1172 detect_idx_hash_bytes_v2(1096, fanout_end, 0, Path::new("t.idx")).expect("sha256"),
1173 32
1174 );
1175 assert!(detect_idx_hash_bytes_v2(1073, fanout_end, 0, Path::new("t.idx")).is_err());
1176 }
1177
1178 #[test]
1179 fn duplicate_oid_rows_parse_and_resolve_first_offset() {
1180 use crate::pack::write_v2_pack_index_with_trailer;
1181 use std::fs;
1182
1183 let oid = ObjectId::from_bytes(&[0x05; 20]).unwrap();
1184 let dir = tempfile::tempdir().expect("tempdir");
1185 let idx_path = dir.path().join("dup-oid.idx");
1186 let pack_path = dir.path().join("dup-oid.pack");
1187 fs::write(&pack_path, b"PACK\x00\x00\x00\x02\x00\x00\x00\x00").expect("pack stub");
1188 write_v2_pack_index_with_trailer(
1189 &idx_path,
1190 &[(oid, 100, 0), (oid, 200, 0)],
1191 &[0u8; 20],
1192 20,
1193 None,
1194 )
1195 .expect("write dup rows");
1196 let mut idx =
1197 parse_pack_index_bytes(&idx_path, fs::read(&idx_path).unwrap(), false).expect("parse");
1198 idx.pack_path = pack_path;
1199 assert_eq!(idx.len(), 2);
1200 assert_eq!(idx.offset_at(0).expect("off0"), 100);
1201 assert_eq!(idx.offset_at(1).expect("off1"), 200);
1202 assert_eq!(idx.find_offset(&oid).expect("lookup"), Some(200));
1203 }
1204
1205 #[test]
1206 fn verified_open_reports_idx_trailer_mismatch_as_typed_error() {
1207 use crate::pack::write_v2_pack_index_with_trailer;
1208 use std::fs;
1209
1210 let oid = ObjectId::from_bytes(&[0x07; 20]).unwrap();
1211 let dir = tempfile::tempdir().expect("tempdir");
1212 let idx_path = dir.path().join("trailer.idx");
1213 write_v2_pack_index_with_trailer(&idx_path, &[(oid, 12, 0)], &[0u8; 20], 20, None)
1214 .expect("write idx");
1215 open_pack_index_file(&idx_path, true).expect("intact trailer verifies");
1216
1217 let mut bytes = fs::read(&idx_path).unwrap();
1218 let last = bytes.len() - 1;
1219 bytes[last] ^= 0xff;
1220 fs::write(&idx_path, &bytes).unwrap();
1221
1222 open_pack_index_file(&idx_path, false).expect("unverified open ignores the trailer");
1223 let err = open_pack_index_file(&idx_path, true).expect_err("bad trailer");
1224 let message = err.to_string();
1225 assert!(
1226 message.starts_with("corrupt object: index checksum mismatch for "),
1227 "{message}"
1228 );
1229 let Error::PackIndexChecksumMismatch { path, mismatch } = err else {
1230 panic!("expected PackIndexChecksumMismatch: {message}");
1231 };
1232 assert_eq!(path, idx_path);
1233 assert_eq!(mismatch.found, bytes[bytes.len() - 20..]);
1234 assert_ne!(mismatch.expected, mismatch.found);
1235 }
1236
1237 #[test]
1238 fn find_position_rejects_mismatched_hash_width() {
1239 let body = build_v1_idx_bytes(&[(vec![0x01; 20], 12)]);
1240 let idx = parse_pack_index_bytes(Path::new("w.idx"), body, false).expect("idx");
1241 let wide = ObjectId::from_bytes(&[0x01; 32]).expect("wide");
1242 assert!(idx.find_position(&wide).is_none());
1243 }
1244
1245 #[test]
1246 fn pack_index_entry_matches_sha1_oid_rejects_wide_rows() {
1247 let body = build_v1_idx_bytes(&[(vec![0x01; 20], 12)]);
1248 let idx = parse_pack_index_bytes(Path::new("w.idx"), body, false).expect("idx");
1249 let entry = idx.iter().next().expect("row");
1250 let sha1 = ObjectId::from_bytes(&[0x01; 20]).expect("sha1");
1251 assert!(pack_index_entry_matches_sha1_oid(&entry, &sha1));
1252 let wide = ObjectId::from_bytes(&[0x01; 32]).expect("wide");
1253 assert!(!pack_index_entry_matches_sha1_oid(&entry, &wide));
1254 }
1255
1256 #[test]
1257 fn v2_parse_rejects_inconsistent_large_offset_slot() {
1258 let oid = ObjectId::from_bytes(&[0x01; 20]).unwrap();
1259 let pack_trailer = [0u8; 20];
1260 let mut body = Vec::new();
1261 body.extend_from_slice(b"\xfftOc");
1262 body.extend_from_slice(&2u32.to_be_bytes());
1263 let fanout = compute_fanout_from_oid_slices(&[oid.as_bytes()]);
1264 for f in fanout {
1265 body.extend_from_slice(&f.to_be_bytes());
1266 }
1267 body.extend_from_slice(oid.as_bytes());
1268 body.extend_from_slice(&0u32.to_be_bytes());
1269 body.extend_from_slice(&0x8000_0001u32.to_be_bytes());
1270 body.extend_from_slice(&1000u64.to_be_bytes());
1271 body.extend_from_slice(&pack_trailer);
1272 body.extend_from_slice(&pack_trailer);
1273 parse_pack_index_bytes(Path::new("bad-large.idx"), body.clone(), false)
1274 .expect("lazy open skips large-slot assignment scan");
1275 let err = parse_pack_index_bytes(Path::new("bad-large.idx"), body, true).unwrap_err();
1276 assert!(matches!(err, Error::CorruptObject(_)));
1277 }
1278
1279 #[test]
1280 fn v2_large_offset_table_direct_index_many_rows() {
1281 use crate::pack::write_v2_pack_index_with_trailer;
1282 use std::time::Instant;
1283
1284 const N: u32 = 20_000;
1285 let mut rows: Vec<(ObjectId, u64, u32)> = Vec::with_capacity(N as usize);
1286 for i in 0..N {
1287 let mut bytes = [0u8; 20];
1288 bytes[0] = 0x07;
1289 bytes[1..5].copy_from_slice(&i.to_be_bytes());
1290 let oid = ObjectId::from_bytes(&bytes).expect("oid");
1291 let offset = (1u64 << 31) + u64::from(i) * 64;
1292 rows.push((oid, offset, 0));
1293 }
1294 let last_offset = rows.last().expect("row").1;
1295 let tmp = tempfile::tempdir().expect("tempdir");
1296 let idx_path = tmp.path().join("many-large.idx");
1297 write_v2_pack_index_with_trailer(&idx_path, &rows, &[0u8; 20], 20, None).expect("write");
1298 let bytes = std::fs::read(&idx_path).expect("read");
1299 let lazy = parse_pack_index_bytes(&idx_path, bytes.clone(), false).expect("lazy");
1300 let last_row = (N - 1) as usize;
1301 assert_eq!(lazy.offset_at(last_row).expect("last"), last_offset);
1302 assert_eq!(lazy.offset_at(0).expect("first"), (1u64 << 31));
1303
1304 let t0 = Instant::now();
1305 for _ in 0..50 {
1306 let _ = lazy.offset_at(last_row).expect("last");
1307 }
1308 let last_elapsed = t0.elapsed();
1309 let t1 = Instant::now();
1310 for _ in 0..50 {
1311 let _ = lazy.offset_at(0).expect("first");
1312 }
1313 let first_elapsed = t1.elapsed();
1314 assert!(
1315 last_elapsed <= first_elapsed * 10,
1316 "last row lookup {last_elapsed:?} vs first {first_elapsed:?}"
1317 );
1318
1319 let mut swapped = bytes;
1320 let off32_base = 8 + 256 * 4 + 20 * (N as usize) + 4 * (N as usize);
1321 let second_off32 = off32_base + 4;
1322 swapped[second_off32..second_off32 + 4].copy_from_slice(&0x8000_0000u32.to_be_bytes());
1323 let lazy_swap =
1324 parse_pack_index_bytes(&idx_path, swapped, false).expect("lazy swapped slot");
1325 assert_eq!(lazy_swap.offset_at(1).expect("swapped"), 1u64 << 31);
1326 let mut swapped_for_verify = std::fs::read(&idx_path).expect("read");
1327 swapped_for_verify[second_off32..second_off32 + 4]
1328 .copy_from_slice(&0x8000_0000u32.to_be_bytes());
1329 let err = parse_pack_index_bytes(&idx_path, swapped_for_verify, true).expect_err("verify");
1330 assert!(matches!(err, Error::CorruptObject(_)));
1331 }
1332
1333 #[test]
1334 fn find_position_large_fanout_bucket_sha256_v2_no_panic() {
1335 use crate::pack::write_v2_pack_index_with_trailer;
1336 let mut rows: Vec<(ObjectId, u64, u32)> = Vec::new();
1337 for i in 0..64u8 {
1338 let mut bytes = [0u8; 32];
1339 bytes[0] = 0x07;
1340 bytes[31] = i;
1341 let oid = ObjectId::from_bytes(&bytes).expect("oid");
1342 rows.push((oid, u64::from(i) * 8 + 512, 0));
1343 }
1344 let tmp = tempfile::tempdir().expect("tempdir");
1345 let idx_path = tmp.path().join("big-v2.idx");
1346 write_v2_pack_index_with_trailer(&idx_path, &rows, &[0u8; 32], 32, None).expect("write v2");
1347 let bytes = std::fs::read(&idx_path).expect("read idx");
1348 let idx = parse_pack_index_bytes(&idx_path, bytes, false).expect("parse v2 idx");
1349 assert_eq!(idx.len(), 64);
1350 assert_eq!(idx.hash_bytes(), 32);
1351 for (oid, _, _) in &rows {
1352 assert!(idx.find_position(oid).is_some(), "missing {}", oid.to_hex());
1353 }
1354 }
1355}