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