1pub use crate::pack_index::{
7 compute_fanout_from_entries, compute_fanout_from_oid_slices, pack_index_entry_matches_sha1_oid,
8 parse_pack_index_bytes, PackIndex, PackIndexEntry, PackIndexEntryRef,
9};
10
11use crate::error::{Error, Result};
12use crate::hash::{hash_object, verify_trailer};
13use crate::objects::{HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind};
14use crate::odb::BoundedRead;
15pub use crate::pack_map::PackData;
16use crate::unpack_objects::apply_delta_into;
17use crate::zlib_inflate::ZlibInflateScratch;
18use std::cell::RefCell;
19use std::collections::{BTreeMap, HashMap, HashSet};
20use std::fs;
21use std::io;
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24use std::time::SystemTime;
25
26#[derive(Debug, Clone, Default)]
28pub struct LocalPackInfo {
29 pub pack_count: usize,
31 pub object_count: usize,
33 pub size_bytes: u64,
35 pub object_ids: HashSet<ObjectId>,
37}
38
39pub fn read_local_pack_indexes(objects_dir: &Path) -> Result<Vec<PackIndex>> {
46 let pack_dir = objects_dir.join("pack");
47 let rd = match fs::read_dir(&pack_dir) {
48 Ok(rd) => rd,
49 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
50 Err(err) => return Err(Error::Io(err)),
51 };
52
53 let mut out = Vec::new();
54 for entry in rd {
55 let entry = entry.map_err(Error::Io)?;
56 let path = entry.path();
57 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
58 continue;
59 }
60 if let Ok(idx) = read_pack_index(&path) {
61 if !idx.pack_path.is_file() {
64 continue;
65 }
66 out.push(idx);
67 }
68 }
69 Ok(out)
70}
71
72pub(crate) mod pack_cache {
83 use super::{read_pack_index_no_verify, Error, ObjectKind, PackIndex, Result};
84 use crate::pack_map::{fingerprint_from_file, PackData, PackFingerprint};
85 use std::collections::{HashMap, HashSet};
86 use std::fs;
87 use std::hash::{Hash, Hasher};
88 use std::io;
89 use std::path::{Path, PathBuf};
90 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
91 use std::sync::{Arc, Mutex};
92 use std::time::SystemTime;
93
94 const DELTA_LRU_SHARD_COUNT: usize = 32;
95
96 type ThreadLocalDirListing = Option<(PathBuf, SystemTime, Vec<Arc<PackIndex>>)>;
97
98 thread_local! {
100 static PACK_DIR_INDEXES: std::cell::RefCell<ThreadLocalDirListing> =
101 const { std::cell::RefCell::new(None) };
102 static PACK_HIT_INDEX: std::cell::RefCell<Option<(PathBuf, Arc<PackIndex>)>> =
103 const { std::cell::RefCell::new(None) };
104 static PACK_BYTES: std::cell::RefCell<HashMap<PathBuf, Arc<PackData>>> =
105 std::cell::RefCell::new(HashMap::new());
106 static PACK_INDEXES: std::cell::RefCell<HashMap<PathBuf, Arc<PackIndex>>> =
107 std::cell::RefCell::new(HashMap::new());
108 static PACK_IDS: std::cell::RefCell<HashMap<PathBuf, u32>> =
109 std::cell::RefCell::new(HashMap::new());
110 static PACK_COUNT_VALIDATED: std::cell::RefCell<HashSet<PathBuf>> =
111 std::cell::RefCell::new(HashSet::new());
112 static PACK_DIR_LISTING_TRUSTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
113 static THREAD_WARM_EPOCH: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
115 static THREAD_WARM_STORE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
117 static PACK_MONOTONIC_CURSOR: std::cell::RefCell<Option<(PathBuf, usize)>> =
118 const { std::cell::RefCell::new(None) };
119 static SINGLE_READER_MEMO: std::cell::RefCell<Option<SingleReaderMemo>> =
120 const { std::cell::RefCell::new(None) };
121 }
122
123 struct SingleReaderMemo {
128 objects_dir: PathBuf,
129 pack_dir: PathBuf,
130 idx: Arc<PackIndex>,
131 pack_bytes: Arc<PackData>,
132 pack_id: u32,
133 }
134
135 fn forget_single_reader_memo() {
136 SINGLE_READER_MEMO.with(|cell| *cell.borrow_mut() = None);
137 }
138
139 pub(crate) fn remember_single_reader(
141 objects_dir: &Path,
142 idx: &Arc<PackIndex>,
143 pack_bytes: &Arc<PackData>,
144 pack_id: u32,
145 ) {
146 SINGLE_READER_MEMO.with(|cell| {
147 *cell.borrow_mut() = Some(SingleReaderMemo {
148 objects_dir: objects_dir.to_path_buf(),
149 pack_dir: objects_dir.join("pack"),
150 idx: Arc::clone(idx),
151 pack_bytes: Arc::clone(pack_bytes),
152 pack_id,
153 });
154 });
155 }
156
157 pub(crate) fn single_reader_for(
163 objects_dir: &Path,
164 ) -> Option<(Arc<PackIndex>, Arc<PackData>, u32)> {
165 if !PACK_DIR_LISTING_TRUSTED.get() {
166 return None;
167 }
168 SINGLE_READER_MEMO.with(|memo| {
169 let memo = memo.borrow();
170 let memo = memo.as_ref()?;
171 if memo.objects_dir.as_os_str() != objects_dir.as_os_str() {
172 return None;
173 }
174 let listing_unchanged = PACK_DIR_INDEXES.with(|cell| {
175 cell.borrow().as_ref().is_some_and(|(dir, _, indexes)| {
176 dir.as_os_str() == memo.pack_dir.as_os_str()
177 && indexes.len() == 1
178 && Arc::ptr_eq(&indexes[0], &memo.idx)
179 })
180 });
181 if !listing_unchanged {
182 return None;
183 }
184 let hit_current = PACK_HIT_INDEX.with(|cell| {
185 cell.borrow().as_ref().is_some_and(|(dir, idx)| {
186 dir.as_os_str() == objects_dir.as_os_str() && Arc::ptr_eq(idx, &memo.idx)
187 })
188 });
189 if !hit_current {
190 return None;
191 }
192 Some((
193 Arc::clone(&memo.idx),
194 Arc::clone(&memo.pack_bytes),
195 memo.pack_id,
196 ))
197 })
198 }
199
200 fn store_thread_local_key(store: &crate::pack_store::PackStore) -> usize {
201 store as *const crate::pack_store::PackStore as usize
202 }
203
204 pub(crate) fn clear_thread_local_dir_indexes() {
205 PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = None);
206 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
207 PACK_BYTES.with(|cell| cell.borrow_mut().clear());
208 PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
209 PACK_IDS.with(|cell| cell.borrow_mut().clear());
210 PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
211 PACK_DIR_LISTING_TRUSTED.set(false);
212 THREAD_WARM_EPOCH.set(0);
213 THREAD_WARM_STORE.set(0);
214 super::clear_pack_read_info_cache();
215 PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = None);
216 forget_single_reader_memo();
217 }
218
219 pub(crate) fn try_read_object_via_single_pack_reader(
220 objects_dir: &Path,
221 oid: &crate::objects::ObjectId,
222 ) -> Result<Option<crate::objects::Object>> {
223 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
224 return Ok(None);
225 }
226 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
227 return Ok(None);
228 };
229 match reader.read_object(oid) {
230 Ok(obj) => Ok(Some(obj)),
231 Err(Error::ObjectNotFound(_)) => Ok(None),
232 Err(err) => Err(err),
233 }
234 }
235
236 pub(crate) fn try_read_object_via_single_pack_reader_unless_whole_larger(
240 objects_dir: &Path,
241 oid: &crate::objects::ObjectId,
242 limit: u64,
243 ) -> Result<Option<super::BoundedRead>> {
244 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
245 return Ok(None);
246 }
247 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
248 return Ok(None);
249 };
250 match reader.read_object_unless_whole_larger(oid, limit) {
251 Ok(read) => Ok(Some(read)),
252 Err(Error::ObjectNotFound(_)) => Ok(None),
253 Err(err) => Err(err),
254 }
255 }
256
257 pub(crate) fn try_read_object_via_single_pack_reader_into(
258 objects_dir: &Path,
259 oid: &crate::objects::ObjectId,
260 populate_delta_lru: bool,
261 buf: &mut Vec<u8>,
262 ) -> Result<Option<crate::objects::ObjectKind>> {
263 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
264 return Ok(None);
265 }
266 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
267 return Ok(None);
268 };
269 match reader.read_object_into_with_delta_policy(oid, populate_delta_lru, buf) {
270 Ok(kind) => Ok(Some(kind)),
271 Err(Error::ObjectNotFound(_)) => Ok(None),
272 Err(err) => Err(err),
273 }
274 }
275
276 pub(crate) fn clear_thread_local_after_invalidation(new_epoch: u64) {
278 clear_thread_local_dir_indexes();
279 THREAD_WARM_EPOCH.set(new_epoch);
280 }
281
282 pub(crate) fn sync_thread_local_with_store(store: &crate::pack_store::PackStore) {
283 if !crate::pack_store::PackStore::has_context() {
284 return;
285 }
286 let epoch = store.cache_epoch();
287 let store_key = store_thread_local_key(store);
288 if THREAD_WARM_EPOCH.get() == epoch && THREAD_WARM_STORE.get() == store_key {
289 return;
290 }
291 clear_thread_local_dir_indexes();
292 THREAD_WARM_EPOCH.set(epoch);
293 THREAD_WARM_STORE.set(store_key);
294 }
295
296 fn drop_thread_local_dir_listing(pack_dir: &Path) {
297 PACK_DIR_INDEXES.with(|cell| {
298 if cell
299 .borrow()
300 .as_ref()
301 .is_some_and(|(dir, _, _)| dir == pack_dir)
302 {
303 *cell.borrow_mut() = None;
304 PACK_DIR_LISTING_TRUSTED.set(false);
305 }
306 });
307 }
308
309 fn drop_thread_local_pack_bytes(pack_path: &Path) {
310 PACK_BYTES.with(|cell| {
311 cell.borrow_mut().remove(pack_path);
312 });
313 forget_single_reader_memo();
314 }
315
316 fn invalidate_thread_local_for_pack_path_change(pack_path: &Path) {
318 let idx_path = pack_path.with_extension("idx");
319 drop_thread_local_pack_bytes(pack_path);
320 PACK_INDEXES.with(|cell| {
321 cell.borrow_mut().remove(&idx_path);
322 });
323 PACK_IDS.with(|cell| {
324 cell.borrow_mut().remove(pack_path);
325 });
326 PACK_COUNT_VALIDATED.with(|cell| {
327 cell.borrow_mut().remove(pack_path);
328 });
329 PACK_HIT_INDEX.with(|cell| {
330 let drop_hint = cell
331 .borrow()
332 .as_ref()
333 .is_some_and(|(_, idx)| idx.pack_path == pack_path || idx.idx_path == idx_path);
334 if drop_hint {
335 *cell.borrow_mut() = None;
336 }
337 });
338 if let Some(pack_dir) = pack_path.parent() {
339 drop_thread_local_dir_listing(pack_dir);
340 }
341 super::clear_pack_read_info_cache();
342 }
343
344 pub(crate) fn warm_thread_local_from_store(store: &crate::pack_store::PackStore) {
346 if !crate::pack_store::PackStore::has_context() {
347 return;
348 }
349 let epoch = store.cache_epoch();
350 let store_key = store_thread_local_key(store);
351 if THREAD_WARM_EPOCH.get() == epoch
352 && THREAD_WARM_STORE.get() == store_key
353 && PACK_DIR_LISTING_TRUSTED.get()
354 {
355 return;
356 }
357 PACK_BYTES.with(|cell| cell.borrow_mut().clear());
358 PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
359 PACK_IDS.with(|cell| cell.borrow_mut().clear());
360 PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
361 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
362 forget_single_reader_memo();
363 super::clear_pack_read_info_cache();
364 let objects_dir = store.objects_dir();
365 let pack_dir = objects_dir.join("pack");
366 store.with_catalog_read(|g| {
367 if let Some(cached) = g.by_dir.get(&pack_dir) {
368 PACK_DIR_INDEXES.with(|cell| {
369 *cell.borrow_mut() =
370 Some((pack_dir.clone(), cached.dir_mtime, cached.indexes.clone()));
371 });
372 }
373 for (path, entry) in &g.by_pack {
374 remember_pack_bytes(path, &entry.bytes);
375 }
376 for (path, entry) in &g.by_idx {
377 remember_index(path, &entry.idx);
378 }
379 PACK_IDS.with(|cell| {
380 cell.borrow_mut()
381 .extend(g.pack_ids.iter().map(|(k, v)| (k.clone(), *v)));
382 });
383 PACK_COUNT_VALIDATED.with(|cell| {
384 cell.borrow_mut()
385 .extend(g.validated_pack_counts.iter().cloned());
386 });
387 PACK_DIR_LISTING_TRUSTED.set(true);
388 THREAD_WARM_EPOCH.set(store.cache_epoch());
389 THREAD_WARM_STORE.set(store_thread_local_key(store));
390 });
391 }
392
393 fn cached_pack_bytes(pack_path: &Path) -> Option<Arc<PackData>> {
394 if !crate::pack_store::PackStore::has_context() {
395 return None;
396 }
397 PACK_BYTES.with(|cell| cell.borrow().get(pack_path).map(Arc::clone))
398 }
399
400 fn remember_pack_bytes(pack_path: &Path, bytes: &Arc<PackData>) {
401 if !crate::pack_store::PackStore::has_context() {
402 return;
403 }
404 PACK_BYTES.with(|cell| {
405 cell.borrow_mut()
406 .insert(pack_path.to_path_buf(), Arc::clone(bytes));
407 });
408 forget_single_reader_memo();
409 }
410
411 pub(crate) fn remember_pack_hit(objects_dir: &Path, idx: &Arc<PackIndex>) {
412 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
413 return;
414 }
415 promote_pack_index(objects_dir, &idx.idx_path);
416 PACK_HIT_INDEX.with(|cell| {
417 *cell.borrow_mut() = Some((objects_dir.to_path_buf(), Arc::clone(idx)));
418 });
419 }
420
421 pub(crate) fn find_offset_in_pack_read_context(
422 objects_dir: &Path,
423 idx: &PackIndex,
424 oid: &crate::objects::ObjectId,
425 ) -> Result<Option<u64>> {
426 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
427 return idx.find_offset(oid);
428 }
429 PACK_MONOTONIC_CURSOR.with(|cell| {
430 let mut slot = cell.borrow_mut();
431 if slot.as_ref().is_none_or(|(dir, _)| dir != objects_dir) {
432 *slot = Some((objects_dir.to_path_buf(), 0));
433 }
434 let Some((_, cursor)) = slot.as_mut() else {
435 return idx.find_offset(oid);
436 };
437 idx.find_offset_monotonic(oid, cursor)
438 })
439 }
440
441 pub(crate) fn try_read_from_hinted_pack(
442 objects_dir: &Path,
443 oid: &crate::objects::ObjectId,
444 ) -> Result<Option<crate::objects::Object>> {
445 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
446 return Ok(None);
447 }
448 let idx = PACK_HIT_INDEX.with(|cell| {
449 cell.borrow()
450 .as_ref()
451 .and_then(|(dir, idx)| (dir == objects_dir).then(|| Arc::clone(idx)))
452 });
453 let Some(idx) = idx else {
454 return Ok(None);
455 };
456 let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
457 return Ok(None);
458 };
459 match super::read_object_from_pack_at_offset(&idx, offset) {
460 Ok(obj) => Ok(Some(obj)),
461 Err(Error::ObjectNotFound(_)) => Ok(None),
462 Err(err) => Err(err),
463 }
464 }
465
466 pub(crate) fn try_read_from_hinted_pack_into(
467 objects_dir: &Path,
468 oid: &crate::objects::ObjectId,
469 populate_delta_lru: bool,
470 buf: &mut Vec<u8>,
471 ) -> Result<Option<crate::objects::ObjectKind>> {
472 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
473 return Ok(None);
474 }
475 let idx = PACK_HIT_INDEX.with(|cell| {
476 cell.borrow()
477 .as_ref()
478 .and_then(|(dir, idx)| (dir == objects_dir).then(|| Arc::clone(idx)))
479 });
480 let Some(idx) = idx else {
481 return Ok(None);
482 };
483 let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
484 return Ok(None);
485 };
486 match super::read_object_at_depth_into(&idx, offset, 0, populate_delta_lru, buf) {
487 Ok(kind) => Ok(Some(kind)),
488 Err(Error::ObjectNotFound(_)) => Ok(None),
489 Err(err) => Err(err),
490 }
491 }
492
493 pub(crate) fn try_read_info_from_hinted_pack(
494 objects_dir: &Path,
495 oid: &crate::objects::ObjectId,
496 ) -> Result<Option<crate::objects::ObjectInfo>> {
497 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
498 return Ok(None);
499 }
500 let idx = PACK_HIT_INDEX.with(|cell| {
501 cell.borrow()
502 .as_ref()
503 .and_then(|(dir, idx)| (dir == objects_dir).then(|| Arc::clone(idx)))
504 });
505 let Some(idx) = idx else {
506 return Ok(None);
507 };
508 let Some(offset) = idx.find_offset(oid)? else {
509 return Ok(None);
510 };
511 match super::read_object_info_from_pack_at_offset(&idx, offset) {
512 Ok(info) => Ok(Some(info)),
513 Err(Error::ObjectNotFound(_)) => Ok(None),
514 Err(err) => Err(err),
515 }
516 }
517 fn cached_dir_indexes(objects_dir: &Path, pack_dir: &Path) -> Option<Vec<Arc<PackIndex>>> {
518 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
519 return None;
520 }
521 let trust_listing = PACK_DIR_LISTING_TRUSTED.get();
522 let dir_mt = if trust_listing {
523 None
524 } else {
525 Some(dir_mtime(pack_dir))
526 };
527 PACK_DIR_INDEXES.with(|cell| {
528 cell.borrow()
529 .as_ref()
530 .and_then(|(dir, cached_mt, indexes)| {
531 if dir != pack_dir {
532 return None;
533 }
534 if trust_listing || dir_mt == Some(*cached_mt) {
535 Some(indexes.clone())
536 } else {
537 None
538 }
539 })
540 })
541 }
542
543 fn remember_dir_indexes(objects_dir: &Path, pack_dir: &Path, indexes: &[Arc<PackIndex>]) {
544 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
545 return;
546 }
547 let dir_mt = dir_mtime(pack_dir);
548 PACK_DIR_INDEXES.with(|cell| {
549 *cell.borrow_mut() = Some((pack_dir.to_path_buf(), dir_mt, indexes.to_vec()));
550 });
551 }
552
553 struct CachedDir {
554 dir_mtime: SystemTime,
555 indexes: Vec<Arc<PackIndex>>,
556 midx_mtime: SystemTime,
557 midx_pack_names: HashSet<String>,
558 }
559
560 struct CachedIdx {
561 idx: Arc<PackIndex>,
562 }
563
564 struct CachedPack {
565 mtime: SystemTime,
566 size: u64,
567 bytes: Arc<PackData>,
568 }
569
570 pub(super) const DELTA_BASE_CACHE_DEFAULT: usize = 96 * 1024 * 1024;
572
573 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
574 pub(crate) struct DeltaCacheKey {
575 pack_id: u32,
576 offset: u64,
577 }
578
579 struct DeltaBaseLru {
581 byte_limit: usize,
582 bytes_used: usize,
583 entries: HashMap<DeltaCacheKey, (ObjectKind, Arc<[u8]>)>,
584 head: Option<DeltaCacheKey>,
585 tail: Option<DeltaCacheKey>,
586 links: HashMap<DeltaCacheKey, (Option<DeltaCacheKey>, Option<DeltaCacheKey>)>,
587 }
588
589 impl Default for DeltaBaseLru {
590 fn default() -> Self {
591 Self {
592 byte_limit: DELTA_BASE_CACHE_DEFAULT,
593 bytes_used: 0,
594 entries: HashMap::new(),
595 head: None,
596 tail: None,
597 links: HashMap::new(),
598 }
599 }
600 }
601
602 impl DeltaBaseLru {
603 fn set_byte_limit(&mut self, limit: usize) {
604 if limit == 0 {
605 self.clear();
606 self.byte_limit = 0;
607 return;
608 }
609 self.byte_limit = limit;
610 while self.bytes_used > self.byte_limit {
611 let Some(key) = self.head else {
612 break;
613 };
614 self.evict_key(key);
615 }
616 }
617
618 fn unlink(&mut self, key: DeltaCacheKey) {
619 let Some((prev, next)) = self.links.remove(&key) else {
620 return;
621 };
622 match prev {
623 Some(p) => {
624 if let Some(link) = self.links.get_mut(&p) {
625 link.1 = next;
626 }
627 }
628 None => self.head = next,
629 }
630 match next {
631 Some(n) => {
632 if let Some(link) = self.links.get_mut(&n) {
633 link.0 = prev;
634 }
635 }
636 None => self.tail = prev,
637 }
638 }
639
640 fn append_tail(&mut self, key: DeltaCacheKey) {
641 let prev = self.tail;
642 self.links.insert(key, (prev, None));
643 if let Some(p) = prev {
644 if let Some(link) = self.links.get_mut(&p) {
645 link.1 = Some(key);
646 }
647 } else {
648 self.head = Some(key);
649 }
650 self.tail = Some(key);
651 }
652
653 fn touch(&mut self, key: DeltaCacheKey) {
654 self.unlink(key);
655 self.append_tail(key);
656 }
657
658 fn evict_key(&mut self, key: DeltaCacheKey) {
659 self.unlink(key);
660 if let Some((_, data)) = self.entries.remove(&key) {
661 self.bytes_used = self.bytes_used.saturating_sub(data.len());
662 }
663 }
664
665 fn get(&mut self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
666 let (kind, data) = self.entries.get(&key).map(|(k, d)| (*k, Arc::clone(d)))?;
667 self.touch(key);
668 Some((kind, data))
669 }
670
671 fn put(&mut self, key: DeltaCacheKey, kind: ObjectKind, data: Arc<[u8]>) {
672 if self.byte_limit == 0 {
673 return;
674 }
675 let sz = data.len();
676 if sz > self.byte_limit {
677 return;
678 }
679 use std::collections::hash_map::Entry;
680 match self.entries.entry(key) {
681 Entry::Occupied(mut slot) => {
682 let old_len = slot.get().1.len();
683 slot.insert((kind, Arc::clone(&data)));
684 self.bytes_used = self.bytes_used.saturating_sub(old_len).saturating_add(sz);
685 self.touch(key);
686 }
687 Entry::Vacant(slot) => {
688 slot.insert((kind, Arc::clone(&data)));
689 self.bytes_used = self.bytes_used.saturating_add(sz);
690 self.append_tail(key);
691 }
692 }
693 while self.bytes_used > self.byte_limit {
694 let Some(evict) = self.head else {
695 break;
696 };
697 self.evict_key(evict);
698 }
699 }
700
701 fn drop_pack(&mut self, pack_id: u32) {
702 let keys: Vec<DeltaCacheKey> = self
703 .entries
704 .keys()
705 .copied()
706 .filter(|k| k.pack_id == pack_id)
707 .collect();
708 for key in keys {
709 self.evict_key(key);
710 }
711 }
712
713 fn clear(&mut self) {
714 self.entries.clear();
715 self.links.clear();
716 self.head = None;
717 self.tail = None;
718 self.bytes_used = 0;
719 }
720 }
721
722 fn shard_index(key: DeltaCacheKey) -> usize {
723 let mut hasher = std::collections::hash_map::DefaultHasher::new();
724 key.hash(&mut hasher);
725 (hasher.finish() as usize) % DELTA_LRU_SHARD_COUNT
726 }
727
728 pub(crate) struct ShardedDeltaBaseLru {
730 cache_enabled: AtomicBool,
731 lru_populated: AtomicBool,
732 total_byte_limit: AtomicUsize,
733 shards: Box<[Mutex<DeltaBaseLru>; DELTA_LRU_SHARD_COUNT]>,
734 }
735
736 impl ShardedDeltaBaseLru {
737 pub(crate) fn new() -> Self {
738 let per_shard = DELTA_BASE_CACHE_DEFAULT / DELTA_LRU_SHARD_COUNT;
739 Self {
740 cache_enabled: AtomicBool::new(true),
741 lru_populated: AtomicBool::new(false),
742 total_byte_limit: AtomicUsize::new(DELTA_BASE_CACHE_DEFAULT),
743 shards: Box::new(std::array::from_fn(|_| {
744 Mutex::new(DeltaBaseLru {
745 byte_limit: per_shard,
746 ..DeltaBaseLru::default()
747 })
748 })),
749 }
750 }
751
752 pub(crate) fn set_byte_limit(&self, limit: usize) {
753 self.total_byte_limit.store(limit, Ordering::Release);
754 if limit == 0 {
755 self.cache_enabled.store(false, Ordering::Release);
756 self.clear();
757 return;
758 }
759 self.cache_enabled.store(true, Ordering::Release);
760 let base = limit / DELTA_LRU_SHARD_COUNT;
761 let rem = limit % DELTA_LRU_SHARD_COUNT;
762 let mut any = false;
763 for (i, shard) in self.shards.iter().enumerate() {
764 let cap = base + usize::from(i < rem);
765 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
766 g.set_byte_limit(cap);
767 any |= !g.entries.is_empty();
768 }
769 self.lru_populated.store(any, Ordering::Release);
770 }
771
772 pub(crate) fn get(&self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
773 if !self.cache_enabled.load(Ordering::Acquire)
774 || !self.lru_populated.load(Ordering::Acquire)
775 {
776 return None;
777 }
778 let shard = &self.shards[shard_index(key)];
779 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
780 g.get(key)
781 }
782
783 pub(crate) fn put_many(&self, kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
784 if items.is_empty() || !self.cache_enabled.load(Ordering::Acquire) {
785 return;
786 }
787 for &(pack_id, offset, ref data) in items {
788 let key = DeltaCacheKey { pack_id, offset };
789 let shard = &self.shards[shard_index(key)];
790 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
791 if g.byte_limit == 0 {
792 continue;
793 }
794 g.put(key, kind, Arc::clone(data));
795 }
796 self.lru_populated.store(true, Ordering::Release);
797 }
798
799 pub(crate) fn clear(&self) {
800 for shard in self.shards.iter() {
801 shard.lock().unwrap_or_else(|e| e.into_inner()).clear();
802 }
803 self.lru_populated.store(false, Ordering::Release);
804 }
805
806 pub(crate) fn drop_pack(&self, pack_id: u32) {
807 let mut any = false;
808 for shard in self.shards.iter() {
809 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
810 g.drop_pack(pack_id);
811 any |= !g.entries.is_empty();
812 }
813 self.lru_populated.store(any, Ordering::Release);
814 }
815
816 pub(crate) fn caching_enabled(&self) -> bool {
817 self.cache_enabled.load(Ordering::Acquire)
818 }
819
820 #[cfg(test)]
821 pub(crate) fn bytes_used(&self) -> usize {
822 self.shards
823 .iter()
824 .map(|s| s.lock().unwrap_or_else(|e| e.into_inner()).bytes_used)
825 .sum()
826 }
827
828 #[cfg(test)]
829 pub(crate) fn contains(&self, pack_id: u32, offset: u64) -> bool {
830 let key = DeltaCacheKey { pack_id, offset };
831 self.shards[shard_index(key)]
832 .lock()
833 .unwrap_or_else(|e| e.into_inner())
834 .entries
835 .contains_key(&key)
836 }
837 }
838
839 #[derive(Default)]
840 pub(crate) struct CatalogState {
841 by_dir: HashMap<PathBuf, CachedDir>,
842 by_idx: HashMap<PathBuf, CachedIdx>,
843 by_pack: HashMap<PathBuf, CachedPack>,
844 pack_ids: HashMap<PathBuf, u32>,
846 next_pack_id: u32,
847 validated_pack_counts: HashSet<PathBuf>,
849 #[cfg(test)]
851 test_dir_rescan_counts: HashMap<PathBuf, u64>,
852 }
853
854 pub(crate) fn with_current<R>(f: impl FnOnce(&mut CatalogState) -> R) -> R {
855 crate::pack_store::PackStore::with_current_or_implicit(f)
856 }
857
858 pub(crate) fn clear_catalog_on(m: &std::sync::RwLock<CatalogState>) {
859 let mut g = m.write().unwrap_or_else(|e| e.into_inner());
860 clear_inner(&mut g);
861 }
862
863 pub(crate) fn configure_delta_base_on(
864 delta: &ShardedDeltaBaseLru,
865 cfg: Option<&crate::config::ConfigSet>,
866 ) {
867 let bytes = cfg
868 .and_then(|config| config.get("core.deltaBaseCacheLimit"))
869 .as_ref()
870 .and_then(|value| crate::config::parse_i64(value).ok())
871 .and_then(|value| usize::try_from(value).ok())
872 .unwrap_or(DELTA_BASE_CACHE_DEFAULT);
873 delta.set_byte_limit(bytes);
874 }
875
876 #[cfg(test)]
877 thread_local! {
878 static TEST_MARKER_STAT_COUNT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
879 }
880
881 fn marker_sidecar_is_file(path: &Path) -> bool {
882 #[cfg(test)]
883 TEST_MARKER_STAT_COUNT.with(|count| count.set(count.get().saturating_add(1)));
884 path.is_file()
885 }
886
887 pub fn sidecar_flags(pack_path: &Path) -> (bool, bool) {
889 (
890 marker_sidecar_is_file(&pack_path.with_extension("promisor")),
891 marker_sidecar_is_file(&pack_path.with_extension("mtimes")),
892 )
893 }
894
895 fn refresh_pack_sidecar_flags(idx: Arc<PackIndex>) -> Arc<PackIndex> {
896 let (is_promisor, is_cruft) = sidecar_flags(&idx.pack_path);
897 if idx.is_promisor == is_promisor && idx.is_cruft == is_cruft {
898 return idx;
899 }
900 let updated = Arc::new(idx.with_sidecar_flags(is_promisor, is_cruft));
901 with_current(|g| {
902 g.by_idx.insert(
903 idx.idx_path.clone(),
904 CachedIdx {
905 idx: Arc::clone(&updated),
906 },
907 );
908 });
909 updated
910 }
911
912 fn dir_mtime(path: &Path) -> SystemTime {
913 fs::metadata(path)
914 .and_then(|m| m.modified())
915 .unwrap_or(SystemTime::UNIX_EPOCH)
916 }
917
918 fn file_signature(path: &Path) -> Option<(SystemTime, u64)> {
919 #[cfg(test)]
920 crate::hot_path_test_metrics::record_pack_signature_stat_for_active_scope();
921 let m = fs::metadata(path).ok()?;
922 let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
923 Some((mtime, m.len()))
924 }
925
926 pub fn pack_object_count_validated(pack_path: &Path) -> bool {
927 if crate::pack_store::PackStore::has_context()
928 && PACK_COUNT_VALIDATED.with(|cell| cell.borrow().contains(pack_path))
929 {
930 return true;
931 }
932 crate::pack_store::PackStore::active_for_reads()
933 .with_catalog_read(|g| g.validated_pack_counts.contains(pack_path))
934 }
935
936 pub fn mark_pack_object_count_validated(pack_path: &Path) {
937 if crate::pack_store::PackStore::has_context() {
938 PACK_COUNT_VALIDATED.with(|cell| {
939 cell.borrow_mut().insert(pack_path.to_path_buf());
940 });
941 }
942 let store = crate::pack_store::PackStore::active_for_reads();
943 if store.with_catalog_read(|g| g.validated_pack_counts.contains(pack_path)) {
944 return;
945 }
946 let _ = store.try_with_catalog(|g| {
947 g.validated_pack_counts.insert(pack_path.to_path_buf());
948 });
949 }
950
951 fn cached_index(idx_path: &Path) -> Option<Arc<PackIndex>> {
952 if !crate::pack_store::PackStore::has_context() {
953 return None;
954 }
955 PACK_INDEXES.with(|cell| cell.borrow().get(idx_path).map(Arc::clone))
956 }
957
958 fn remember_index(idx_path: &Path, idx: &Arc<PackIndex>) {
959 if !crate::pack_store::PackStore::has_context() {
960 return;
961 }
962 PACK_INDEXES.with(|cell| {
963 cell.borrow_mut()
964 .insert(idx_path.to_path_buf(), Arc::clone(idx));
965 });
966 }
967
968 fn index_cache_hit_stale(idx: &PackIndex) -> bool {
969 !idx.pack_path.is_file()
970 }
971
972 pub fn get_index(idx_path: &Path) -> Result<Arc<PackIndex>> {
974 if let Some(hit) = cached_index(idx_path) {
975 if index_cache_hit_stale(&hit) {
976 PACK_INDEXES.with(|cell| {
977 cell.borrow_mut().remove(idx_path);
978 });
979 } else {
980 return Ok(hit);
981 }
982 }
983 let store = crate::pack_store::PackStore::active_for_reads();
984 if let Some(hit) =
985 store.with_catalog_read(|g| g.by_idx.get(idx_path).map(|c| Arc::clone(&c.idx)))
986 {
987 if index_cache_hit_stale(&hit) {
988 store.with_catalog(|g| {
989 g.by_idx.remove(idx_path);
990 });
991 } else {
992 remember_index(idx_path, &hit);
993 return Ok(hit);
994 }
995 }
996 crate::trace::trace_span!("grit.pack.index.open");
997 let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
998 store.with_catalog(|g| {
999 g.by_idx.insert(
1000 idx_path.to_path_buf(),
1001 CachedIdx {
1002 idx: Arc::clone(&parsed),
1003 },
1004 );
1005 });
1006 remember_index(idx_path, &parsed);
1007 Ok(parsed)
1008 }
1009
1010 fn rescan_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1011 let pack_dir = objects_dir.join("pack");
1012 drop_thread_local_dir_listing(&pack_dir);
1013 let dir_mt = dir_mtime(&pack_dir);
1014
1015 let rd = match fs::read_dir(&pack_dir) {
1016 Ok(rd) => rd,
1017 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1018 with_current(|g| {
1019 g.by_dir.insert(
1020 pack_dir.clone(),
1021 CachedDir {
1022 dir_mtime: dir_mt,
1023 indexes: Vec::new(),
1024 midx_mtime: SystemTime::UNIX_EPOCH,
1025 midx_pack_names: HashSet::new(),
1026 },
1027 );
1028 });
1029 return Ok(Vec::new());
1030 }
1031 Err(err) => return Err(Error::Io(err)),
1032 };
1033
1034 let mut out = Vec::new();
1035 for entry in rd {
1036 let entry = entry.map_err(Error::Io)?;
1037 let path = entry.path();
1038 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1039 continue;
1040 }
1041 let Ok(idx) = get_index(&path) else {
1042 continue;
1043 };
1044 if !idx.pack_path.is_file() {
1045 continue;
1046 }
1047 out.push(refresh_pack_sidecar_flags(idx));
1048 }
1049
1050 let midx_pack_names = load_midx_pack_names(&pack_dir);
1051 with_current(|g| {
1052 #[cfg(test)]
1053 {
1054 *g.test_dir_rescan_counts
1055 .entry(pack_dir.clone())
1056 .or_insert(0) += 1;
1057 }
1058 g.by_dir.insert(
1059 pack_dir,
1060 CachedDir {
1061 dir_mtime: dir_mt,
1062 indexes: out.clone(),
1063 midx_mtime: midx_pack_names.0,
1064 midx_pack_names: midx_pack_names.1,
1065 },
1066 );
1067 });
1068 Ok(out)
1069 }
1070
1071 fn load_midx_pack_names(pack_dir: &Path) -> (SystemTime, HashSet<String>) {
1072 let Some(midx_path) = crate::midx::resolve_tip_midx_path(pack_dir) else {
1073 return (SystemTime::UNIX_EPOCH, HashSet::new());
1074 };
1075 let mtime = fs::metadata(&midx_path)
1076 .and_then(|m| m.modified())
1077 .unwrap_or(SystemTime::UNIX_EPOCH);
1078 let objects_dir = pack_dir.parent().unwrap_or(pack_dir);
1079 let names = crate::midx::read_midx_pack_idx_names(objects_dir)
1080 .unwrap_or_default()
1081 .into_iter()
1082 .collect();
1083 (mtime, names)
1084 }
1085
1086 fn refresh_midx_pack_names_locked(pack_dir: &Path, cached: &mut CachedDir) {
1087 let (mtime, names) = load_midx_pack_names(pack_dir);
1088 if cached.midx_mtime != mtime {
1089 cached.midx_mtime = mtime;
1090 cached.midx_pack_names = names;
1091 }
1092 }
1093
1094 pub fn promote_pack_index(objects_dir: &Path, idx_path: &Path) {
1096 let pack_dir = objects_dir.join("pack");
1097 let store = crate::pack_store::PackStore::active_for_reads();
1098 let _ = store.try_with_catalog(|g| {
1099 let Some(cached) = g.by_dir.get_mut(&pack_dir) else {
1100 return;
1101 };
1102 let Some(pos) = cached
1103 .indexes
1104 .iter()
1105 .position(|idx| idx.idx_path == idx_path)
1106 else {
1107 return;
1108 };
1109 if pos == 0 {
1110 return;
1111 }
1112 let hit = cached.indexes.remove(pos);
1113 cached.indexes.insert(0, hit);
1114 });
1115 }
1116
1117 pub fn midx_covered_pack_names(objects_dir: &Path) -> HashSet<String> {
1119 let pack_dir = objects_dir.join("pack");
1120 let cached = with_current(|g| {
1121 let cached = g.by_dir.get_mut(&pack_dir)?;
1122 refresh_midx_pack_names_locked(&pack_dir, cached);
1123 Some(cached.midx_pack_names.clone())
1124 });
1125 if let Some(names) = cached {
1126 return names;
1127 }
1128 load_midx_pack_names(&pack_dir).1
1129 }
1130
1131 pub fn get_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1133 let pack_dir = objects_dir.join("pack");
1134 let store = crate::pack_store::PackStore::active_for_reads();
1135 if crate::pack_store::PackStore::has_context() {
1136 sync_thread_local_with_store(&store);
1137 }
1138 if let Some(hit) = cached_dir_indexes(objects_dir, &pack_dir) {
1139 return Ok(hit);
1140 }
1141 let dir_mt = dir_mtime(&pack_dir);
1142 if let Some(hit) = store.with_catalog_read(|g| {
1143 g.by_dir
1144 .get(&pack_dir)
1145 .and_then(|c| (c.dir_mtime == dir_mt).then(|| c.indexes.clone()))
1146 }) {
1147 remember_dir_indexes(objects_dir, &pack_dir, &hit);
1148 return Ok(hit);
1149 }
1150 let fresh = rescan_dir_indexes(objects_dir)?;
1151 remember_dir_indexes(objects_dir, &pack_dir, &fresh);
1152 Ok(fresh)
1153 }
1154
1155 fn idx_paths_on_disk(pack_dir: &Path) -> Result<HashSet<PathBuf>> {
1156 let mut out = HashSet::new();
1157 let rd = match fs::read_dir(pack_dir) {
1158 Ok(rd) => rd,
1159 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(out),
1160 Err(err) => return Err(Error::Io(err)),
1161 };
1162 for entry in rd {
1163 let entry = entry.map_err(Error::Io)?;
1164 let path = entry.path();
1165 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1166 continue;
1167 }
1168 if path.with_extension("pack").is_file() {
1169 out.insert(path);
1170 }
1171 }
1172 Ok(out)
1173 }
1174
1175 fn cached_dir_listing_stale(
1176 pack_dir: &Path,
1177 dir_mt: SystemTime,
1178 cached: &CachedDir,
1179 ) -> Result<bool> {
1180 if cached.dir_mtime != dir_mt {
1181 return Ok(true);
1182 }
1183 let on_disk = idx_paths_on_disk(pack_dir)?;
1186 let cached_paths: HashSet<PathBuf> = cached
1187 .indexes
1188 .iter()
1189 .map(|idx| idx.idx_path.clone())
1190 .collect();
1191 Ok(on_disk != cached_paths)
1192 }
1193
1194 pub fn reprepare_dir_on_miss(objects_dir: &Path) -> Result<bool> {
1198 let pack_dir = objects_dir.join("pack");
1199 let dir_mt = dir_mtime(&pack_dir);
1200 let needs_rescan =
1201 crate::pack_store::PackStore::active_for_reads().with_catalog_read(|g| {
1202 match g.by_dir.get(&pack_dir) {
1203 None => true,
1204 Some(c) => cached_dir_listing_stale(&pack_dir, dir_mt, c).unwrap_or(true),
1205 }
1206 });
1207 if !needs_rescan {
1208 return Ok(false);
1209 }
1210 rescan_dir_indexes(objects_dir)?;
1211 Ok(true)
1212 }
1213
1214 fn load_pack_bytes_from_disk(
1215 pack_path: &Path,
1216 mtime: SystemTime,
1217 size: u64,
1218 ) -> Result<Arc<PackData>> {
1219 invalidate_thread_local_for_pack_path_change(pack_path);
1220 let bytes = PackData::open(pack_path)?;
1221 let store = crate::pack_store::PackStore::active_for_reads();
1222 store.with_catalog(|g| {
1223 drop_delta_entries_locked(g, pack_path, store.delta_bases());
1224 g.by_idx.remove(&pack_path.with_extension("idx"));
1225 g.validated_pack_counts.remove(pack_path);
1226 g.by_pack.insert(
1227 pack_path.to_path_buf(),
1228 CachedPack {
1229 mtime,
1230 size,
1231 bytes: Arc::clone(&bytes),
1232 },
1233 );
1234 });
1235 remember_pack_bytes(pack_path, &bytes);
1236 Ok(bytes)
1237 }
1238
1239 pub fn get_pack_bytes(pack_path: &Path) -> Result<Arc<PackData>> {
1244 if let Some(hit) = cached_pack_bytes(pack_path) {
1245 return Ok(hit);
1246 }
1247 let store = crate::pack_store::PackStore::active_for_reads();
1248 if let Some(hit) =
1249 store.with_catalog_read(|g| g.by_pack.get(pack_path).map(|c| Arc::clone(&c.bytes)))
1250 {
1251 remember_pack_bytes(pack_path, &hit);
1252 return Ok(hit);
1253 }
1254 let (mtime, size) = file_signature(pack_path).ok_or_else(|| {
1255 Error::Io(io::Error::new(
1256 io::ErrorKind::NotFound,
1257 format!("pack not found: {}", pack_path.display()),
1258 ))
1259 })?;
1260 load_pack_bytes_from_disk(pack_path, mtime, size)
1261 }
1262
1263 fn revalidate_stale_pack_bytes_in_context(pack_path: &Path) -> Result<bool> {
1264 let Some((mtime, size)) = file_signature(pack_path) else {
1265 let store = crate::pack_store::PackStore::active_for_reads();
1266 let removed = store.with_catalog(|g| {
1267 let removed = g.by_pack.remove(pack_path).is_some();
1268 if removed {
1269 drop_delta_entries_locked(g, pack_path, store.delta_bases());
1270 }
1271 removed
1272 });
1273 invalidate_thread_local_for_pack_path_change(pack_path);
1274 super::drop_pack_read_info_cache_for(pack_path);
1275 return Ok(removed);
1276 };
1277 let store = crate::pack_store::PackStore::active_for_reads();
1278 let stale = store.with_catalog_read(|g| match g.by_pack.get(pack_path) {
1279 Some(c) => c.mtime != mtime || c.size != size,
1280 None => true,
1281 });
1282 if !stale {
1283 return Ok(false);
1284 }
1285 load_pack_bytes_from_disk(pack_path, mtime, size)?;
1286 super::drop_pack_read_info_cache_for(pack_path);
1287 Ok(true)
1288 }
1289
1290 pub fn revalidate_stale_pack_bytes(pack_path: &Path) -> Result<bool> {
1294 if crate::pack_store::PackStore::has_context() {
1295 return revalidate_stale_pack_bytes_in_context(pack_path);
1296 }
1297 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1298 crate::pack_store::PackStore::with_context(store, || {
1299 revalidate_stale_pack_bytes_in_context(pack_path)
1300 })
1301 }
1302
1303 pub fn reload_pack_bytes_after_parse_failure(pack_path: &Path) -> Result<bool> {
1311 if revalidate_stale_pack_bytes(pack_path)? {
1312 return Ok(true);
1313 }
1314 let Some((mtime, size)) = file_signature(pack_path) else {
1315 return Ok(false);
1316 };
1317 let hash_bytes = hash_bytes_for_pack_file(pack_path)?;
1318 let disk_fp = fingerprint_from_file(pack_path, hash_bytes)?;
1319 let needs_reload = with_current(|g| {
1320 g.by_pack.get(pack_path).is_some_and(|c| {
1321 pack_fingerprint_mismatch(&c.bytes, hash_bytes, &disk_fp).unwrap_or(true)
1322 })
1323 });
1324 if !needs_reload {
1325 return Ok(false);
1326 }
1327 load_pack_bytes_from_disk(pack_path, mtime, size)?;
1328 super::drop_pack_read_info_cache_for(pack_path);
1329 Ok(true)
1330 }
1331
1332 fn hash_bytes_for_pack_file(pack_path: &Path) -> Result<usize> {
1333 let idx_path = pack_path.with_extension("idx");
1334 if let Some(hash_bytes) =
1335 with_current(|g| g.by_idx.get(&idx_path).map(|c| c.idx.hash_bytes()))
1336 {
1337 return Ok(hash_bytes);
1338 }
1339 Ok(read_pack_index_no_verify(&idx_path)?.hash_bytes())
1340 }
1341
1342 fn pack_fingerprint_mismatch(
1343 cached: &PackData,
1344 hash_bytes: usize,
1345 disk: &PackFingerprint,
1346 ) -> Result<bool> {
1347 Ok(cached.fingerprint(hash_bytes)? != *disk)
1348 }
1349
1350 #[cfg(test)]
1351 pub fn test_reset_dir_rescan_count(pack_dir: &Path) {
1352 let objects_dir = pack_dir.parent().expect("pack dir under objects/");
1353 crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir).with_pack(|g| {
1354 g.by_dir.remove(pack_dir);
1355 g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
1356 });
1357 }
1358
1359 #[cfg(test)]
1360 pub fn test_reset_marker_stat_count() {
1361 TEST_MARKER_STAT_COUNT.with(|count| count.set(0));
1362 }
1363
1364 #[cfg(test)]
1365 #[must_use]
1366 pub fn test_marker_stat_count() -> u64 {
1367 TEST_MARKER_STAT_COUNT.with(std::cell::Cell::get)
1368 }
1369
1370 #[cfg(test)]
1371 #[must_use]
1372 pub fn test_dir_rescan_count(pack_dir: &Path) -> u64 {
1373 let objects_dir = pack_dir.parent().expect("pack dir under objects/");
1374 test_dir_rescan_count_on_store(
1375 &crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir),
1376 pack_dir,
1377 )
1378 }
1379
1380 #[cfg(test)]
1381 #[must_use]
1382 pub fn test_dir_rescan_count_on_store(
1383 store: &std::sync::Arc<crate::pack_store::PackStore>,
1384 pack_dir: &Path,
1385 ) -> u64 {
1386 store.with_pack(|g| g.test_dir_rescan_counts.get(pack_dir).copied().unwrap_or(0))
1387 }
1388
1389 #[cfg(test)]
1390 pub fn test_reset_dir_rescan_count_on_store(
1391 store: &std::sync::Arc<crate::pack_store::PackStore>,
1392 pack_dir: &Path,
1393 ) {
1394 store.with_pack(|g| {
1395 g.by_dir.remove(pack_dir);
1396 g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
1397 });
1398 }
1399
1400 #[cfg(test)]
1402 pub fn test_inject_stale_pack_bytes(pack_path: &Path, stale: Arc<PackData>) {
1403 crate::pack_store::PackStore::legacy_for_pack_path(pack_path).with_pack(|g| {
1404 let stamp = g
1405 .by_pack
1406 .get(pack_path)
1407 .map(|c| (c.mtime, c.size))
1408 .unwrap_or((SystemTime::UNIX_EPOCH, stale.len() as u64));
1409 g.by_pack.insert(
1410 pack_path.to_path_buf(),
1411 CachedPack {
1412 mtime: stamp.0,
1413 size: stamp.1,
1414 bytes: stale,
1415 },
1416 );
1417 });
1418 }
1419
1420 fn clear_inner(g: &mut CatalogState) {
1421 g.by_dir.clear();
1422 g.by_idx.clear();
1423 g.by_pack.clear();
1424 g.pack_ids.clear();
1425 }
1426
1427 pub fn set_delta_base_cache_byte_limit(limit: usize) {
1429 crate::pack_store::PackStore::active_for_reads()
1430 .delta_bases()
1431 .set_byte_limit(limit);
1432 }
1433
1434 #[cfg(test)]
1435 pub fn test_delta_base_cache_bytes_used(pack_path: &Path) -> usize {
1436 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1437 .delta_bases()
1438 .bytes_used()
1439 }
1440
1441 #[cfg(test)]
1442 pub fn test_set_delta_base_cache_byte_limit(pack_path: &Path, limit: usize) {
1443 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1444 .delta_bases()
1445 .set_byte_limit(limit);
1446 }
1447
1448 #[cfg(test)]
1449 pub fn test_delta_base_cached(pack_path: &Path, pack_id: u32, offset: u64) -> bool {
1450 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1451 .delta_bases()
1452 .contains(pack_id, offset)
1453 }
1454
1455 fn pack_id_for_in_context(pack_path: &Path) -> u32 {
1456 let store = crate::pack_store::PackStore::active_for_reads();
1457 if let Some(id) = PACK_IDS.with(|cell| cell.borrow().get(pack_path).copied()) {
1458 return id;
1459 }
1460 if let Some(id) = store.with_catalog_read(|g| g.pack_ids.get(pack_path).copied()) {
1461 PACK_IDS.with(|cell| {
1462 cell.borrow_mut().insert(pack_path.to_path_buf(), id);
1463 });
1464 return id;
1465 }
1466 store.with_catalog(|g| {
1467 if let Some(&id) = g.pack_ids.get(pack_path) {
1468 return id;
1469 }
1470 let id = g.next_pack_id;
1471 g.next_pack_id = g.next_pack_id.saturating_add(1);
1472 g.pack_ids.insert(pack_path.to_path_buf(), id);
1473 PACK_IDS.with(|cell| {
1474 cell.borrow_mut().insert(pack_path.to_path_buf(), id);
1475 });
1476 id
1477 })
1478 }
1479
1480 pub fn pack_id_for(pack_path: &Path) -> u32 {
1482 if crate::pack_store::PackStore::has_context() {
1483 return pack_id_for_in_context(pack_path);
1484 }
1485 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1486 crate::pack_store::PackStore::with_context(store, || pack_id_for_in_context(pack_path))
1487 }
1488
1489 fn drop_delta_entries_locked(
1491 g: &mut CatalogState,
1492 pack_path: &Path,
1493 delta: &ShardedDeltaBaseLru,
1494 ) {
1495 if let Some(&pack_id) = g.pack_ids.get(pack_path) {
1496 delta.drop_pack(pack_id);
1497 }
1498 }
1499
1500 pub fn get_delta_base(pack_id: u32, offset: u64) -> Option<(ObjectKind, Arc<[u8]>)> {
1502 crate::pack_store::PackStore::active_for_reads()
1503 .delta_bases()
1504 .get(DeltaCacheKey { pack_id, offset })
1505 }
1506
1507 #[must_use]
1508 pub fn delta_base_caching_enabled() -> bool {
1509 crate::pack_store::PackStore::active_for_reads()
1510 .delta_bases()
1511 .caching_enabled()
1512 }
1513
1514 pub fn put_delta_bases(kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
1516 if items.is_empty() {
1517 return;
1518 }
1519 crate::pack_store::PackStore::active_for_reads()
1520 .delta_bases()
1521 .put_many(kind, items);
1522 }
1523
1524 pub fn clear_delta_bases() {
1526 crate::pack_store::PackStore::active_for_reads()
1527 .delta_bases()
1528 .clear();
1529 }
1530
1531 pub fn refresh_pack_signature(pack_path: &Path, touched_at: SystemTime) {
1538 with_current(|g| {
1539 if let Some(c) = g.by_pack.get_mut(pack_path) {
1540 c.mtime = touched_at;
1541 }
1542 });
1543 }
1544}
1545
1546pub use pack_cache::revalidate_stale_pack_bytes;
1547
1548pub fn read_local_pack_indexes_cached(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1560 if crate::pack_store::PackStore::has_context() {
1561 return pack_cache::get_dir_indexes(objects_dir);
1562 }
1563 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
1564 crate::pack_store::PackStore::with_context(store, || pack_cache::get_dir_indexes(objects_dir))
1565}
1566
1567pub fn reprepare_pack_directory_on_miss(objects_dir: &Path) -> Result<bool> {
1576 if crate::pack_store::PackStore::has_context() {
1577 return reprepare_pack_directory_on_miss_in_context(objects_dir);
1578 }
1579 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
1580 crate::pack_store::PackStore::with_context(store, || {
1581 reprepare_pack_directory_on_miss_in_context(objects_dir)
1582 })
1583}
1584
1585fn reprepare_pack_directory_on_miss_in_context(objects_dir: &Path) -> Result<bool> {
1586 let refreshed = pack_cache::reprepare_dir_on_miss(objects_dir)?;
1587 if refreshed {
1588 crate::midx::evict_midx_read_cache_for_pack_dir(&objects_dir.join("pack"));
1589 }
1590 Ok(refreshed)
1591}
1592
1593pub fn read_pack_index_cached(idx_path: &Path) -> Result<Arc<PackIndex>> {
1601 if crate::pack_store::PackStore::has_context() {
1602 return pack_cache::get_index(idx_path);
1603 }
1604 let pack_path = idx_path.with_extension("pack");
1605 let store = crate::pack_store::PackStore::legacy_for_pack_path(&pack_path);
1606 crate::pack_store::PackStore::with_context(store, || pack_cache::get_index(idx_path))
1607}
1608
1609pub fn read_pack_bytes_cached(pack_path: &Path) -> Result<Arc<PackData>> {
1615 if crate::pack_store::PackStore::has_context() {
1616 return pack_cache::get_pack_bytes(pack_path);
1617 }
1618 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1619 crate::pack_store::PackStore::with_context(store, || pack_cache::get_pack_bytes(pack_path))
1620}
1621
1622pub fn clear_pack_cache() {
1627 #[cfg(test)]
1628 let _guard = pack_cache_test_guard();
1629 clear_pack_read_info_cache();
1630 pack_cache::clear_thread_local_dir_indexes();
1631 crate::pack_store::PackStore::invalidate_all_legacy();
1632}
1633
1634#[cfg(test)]
1636pub fn test_reset_pack_marker_stat_count() {
1637 pack_cache::test_reset_marker_stat_count();
1638}
1639
1640#[cfg(test)]
1642#[must_use]
1643pub fn test_pack_marker_stat_count() -> u64 {
1644 pack_cache::test_marker_stat_count()
1645}
1646
1647pub fn clear_pack_delta_base_cache() {
1649 pack_cache::clear_delta_bases();
1650}
1651
1652pub fn configure_delta_base_cache_from_config(cfg: Option<&crate::config::ConfigSet>) {
1654 let bytes = cfg
1655 .and_then(|c| c.get("core.deltaBaseCacheLimit"))
1656 .as_ref()
1657 .and_then(|v| crate::config::parse_i64(v).ok())
1658 .and_then(|n| usize::try_from(n).ok())
1659 .unwrap_or(pack_cache::DELTA_BASE_CACHE_DEFAULT);
1660 pack_cache::set_delta_base_cache_byte_limit(bytes);
1661}
1662
1663pub fn refresh_pack_bytes_signature(pack_path: &Path, touched_at: SystemTime) {
1666 pack_cache::refresh_pack_signature(pack_path, touched_at);
1667}
1668
1669pub fn collect_local_pack_info(objects_dir: &Path) -> Result<LocalPackInfo> {
1675 let indexes = read_local_pack_indexes(objects_dir)?;
1676 let mut info = LocalPackInfo::default();
1677 for idx in indexes {
1678 let pack_meta = fs::metadata(&idx.pack_path).map_err(Error::Io)?;
1679 let idx_meta = fs::metadata(&idx.idx_path).map_err(Error::Io)?;
1680 info.pack_count += 1;
1681 info.object_count += idx.len();
1682 info.size_bytes += pack_meta.len() + idx_meta.len();
1683 let hash_bytes = idx.hash_bytes();
1684 for entry in idx.iter() {
1685 if entry.oid().len() == hash_bytes {
1686 if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
1687 info.object_ids.insert(oid);
1688 }
1689 }
1690 }
1691 }
1692 Ok(info)
1693}
1694
1695pub fn write_v2_pack_index_with_trailer(
1710 idx_path: &Path,
1711 entries: &[(ObjectId, u64, u32)],
1712 pack_trailer: &[u8],
1713 hash_bytes: usize,
1714 sort_threads: Option<usize>,
1715) -> Result<()> {
1716 write_v2_pack_index_sorting(
1717 idx_path,
1718 &mut entries.to_vec(),
1719 pack_trailer,
1720 hash_bytes,
1721 sort_threads,
1722 )
1723}
1724
1725pub fn write_v2_pack_index(
1727 idx_path: &Path,
1728 pack_path: &Path,
1729 entries: &[(ObjectId, u64, u32)],
1730 hash_bytes: usize,
1731) -> Result<()> {
1732 let pack_bytes = fs::read(pack_path).map_err(Error::Io)?;
1733 if pack_bytes.len() < hash_bytes {
1734 return Err(Error::CorruptObject(
1735 "pack too small for idx trailer".into(),
1736 ));
1737 }
1738 let trailer = &pack_bytes[pack_bytes.len() - hash_bytes..];
1739 write_v2_pack_index_with_trailer(idx_path, entries, trailer, hash_bytes, None)
1740}
1741
1742const PARALLEL_INDEX_SORT_MIN: usize = 8_192;
1743
1744fn sort_index_entries(
1750 entries: &mut [(ObjectId, u64, u32)],
1751 sort_threads: Option<usize>,
1752) -> [u32; 256] {
1753 let lead = |entry: &(ObjectId, u64, u32)| usize::from(entry.0.as_bytes()[0]);
1754 let mut counts = [0usize; 256];
1755 for entry in entries.iter() {
1756 counts[lead(entry)] += 1;
1757 }
1758 let mut ends = [0usize; 256];
1759 let mut total = 0usize;
1760 for (end, count) in ends.iter_mut().zip(counts) {
1761 total += count;
1762 *end = total;
1763 }
1764 let mut next = [0usize; 256];
1765 next[1..].copy_from_slice(&ends[..255]);
1766 for bucket in 0..256 {
1767 while next[bucket] < ends[bucket] {
1768 let at = next[bucket];
1769 let home = lead(&entries[at]);
1770 if home == bucket {
1771 next[bucket] += 1;
1772 } else {
1773 entries.swap(at, next[home]);
1774 next[home] += 1;
1775 }
1776 }
1777 }
1778
1779 let by_oid =
1780 |a: &(ObjectId, u64, u32), b: &(ObjectId, u64, u32)| a.0.as_bytes().cmp(b.0.as_bytes());
1781 let threads = sort_threads.unwrap_or(1).max(1);
1782 if threads <= 1 || entries.len() < PARALLEL_INDEX_SORT_MIN {
1783 let mut start = 0;
1784 for end in ends {
1785 entries[start..end].sort_unstable_by(by_oid);
1786 start = end;
1787 }
1788 } else {
1789 let share = entries.len().div_ceil(threads);
1790 std::thread::scope(|scope| {
1791 let mut rest = &mut *entries;
1792 let mut taken = 0usize;
1793 for end in ends {
1794 if end - taken >= share || end == total {
1795 let (run, tail) = std::mem::take(&mut rest).split_at_mut(end - taken);
1796 rest = tail;
1797 taken = end;
1798 if run.len() > 1 {
1799 scope.spawn(move || run.sort_unstable_by(by_oid));
1800 }
1801 }
1802 }
1803 });
1804 }
1805 ends.map(|end| u32::try_from(end).unwrap_or(u32::MAX))
1806}
1807
1808pub(crate) fn write_v2_pack_index_sorting(
1819 idx_path: &Path,
1820 entries: &mut [(ObjectId, u64, u32)],
1821 pack_trailer: &[u8],
1822 hash_bytes: usize,
1823 sort_threads: Option<usize>,
1824) -> Result<()> {
1825 use std::io::Write;
1826
1827 let Some(algo) = HashAlgo::from_len(hash_bytes) else {
1828 return Err(Error::CorruptObject(format!(
1829 "unsupported index hash width {hash_bytes}"
1830 )));
1831 };
1832 if pack_trailer.len() != hash_bytes {
1833 return Err(Error::CorruptObject(format!(
1834 "pack trailer length {} does not match hash width {hash_bytes}",
1835 pack_trailer.len()
1836 )));
1837 }
1838 if let Some((oid, _, _)) = entries
1839 .iter()
1840 .find(|(oid, _, _)| oid.as_bytes().len() != hash_bytes)
1841 {
1842 return Err(Error::CorruptObject(format!(
1843 "OID width {} does not match index hash width {hash_bytes}",
1844 oid.as_bytes().len()
1845 )));
1846 }
1847 let fanout = sort_index_entries(entries, sort_threads);
1848
1849 let file = fs::File::create(idx_path).map_err(Error::Io)?;
1850 let mut out = std::io::BufWriter::with_capacity(256 * 1024, file);
1851 let mut hasher = algo.checksum_hasher();
1852 let mut put = |bytes: &[u8]| -> Result<()> {
1853 hasher.update(bytes);
1854 out.write_all(bytes).map_err(Error::Io)
1855 };
1856 put(b"\xfftOc")?;
1857 put(&2u32.to_be_bytes())?;
1858 for f in fanout {
1859 put(&f.to_be_bytes())?;
1860 }
1861 for (oid, _, _) in entries.iter() {
1862 put(oid.as_bytes())?;
1863 }
1864 for (_, _, crc) in entries.iter() {
1865 put(&crc.to_be_bytes())?;
1866 }
1867 let mut large = 0u32;
1868 for (_, off, _) in entries.iter() {
1869 match u32::try_from(*off) {
1870 Ok(small) if small < 0x8000_0000 => put(&small.to_be_bytes())?,
1871 _ => {
1872 if large >= 0x8000_0000 {
1873 return Err(Error::CorruptObject(
1874 "pack index large-offset table overflow".to_owned(),
1875 ));
1876 }
1877 put(&(0x8000_0000u32 | large).to_be_bytes())?;
1878 large += 1;
1879 }
1880 }
1881 }
1882 for (_, off, _) in entries.iter() {
1883 if *off >= 0x8000_0000 {
1884 put(&off.to_be_bytes())?;
1885 }
1886 }
1887 put(pack_trailer)?;
1888 let digest = hasher.finalize();
1889 out.write_all(&digest).map_err(Error::Io)?;
1890 out.flush().map_err(Error::Io)?;
1891 Ok(())
1892}
1893
1894#[must_use]
1895pub fn oid_bytes_to_hex(oid: &[u8]) -> String {
1897 hex::encode(oid)
1898}
1899
1900pub fn hash_object_bytes(kind: ObjectKind, data: &[u8], hash_bytes: usize) -> Result<Vec<u8>> {
1902 let Some(algo) = HashAlgo::from_len(hash_bytes) else {
1903 return Err(Error::CorruptObject(format!(
1904 "unsupported object hash width: {hash_bytes}"
1905 )));
1906 };
1907 Ok(crate::hash::hash_object(algo, kind, data)?
1908 .as_bytes()
1909 .to_vec())
1910}
1911
1912pub fn read_pack_index(idx_path: &Path) -> Result<PackIndex> {
1924 crate::pack_index::open_pack_index_file(idx_path, true)
1925}
1926
1927pub fn read_pack_index_no_verify(idx_path: &Path) -> Result<PackIndex> {
1933 crate::pack_index::open_pack_index_file(idx_path, false)
1934}
1935
1936#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1938pub enum PackedType {
1939 Commit,
1941 Tree,
1943 Blob,
1945 Tag,
1947 OfsDelta,
1949 RefDelta,
1951}
1952
1953impl PackedType {
1954 #[must_use]
1956 pub fn as_str(self) -> &'static str {
1957 match self {
1958 Self::Commit => "commit",
1959 Self::Tree => "tree",
1960 Self::Blob => "blob",
1961 Self::Tag => "tag",
1962 Self::OfsDelta => "ofs-delta",
1963 Self::RefDelta => "ref-delta",
1964 }
1965 }
1966}
1967
1968#[derive(Debug, Clone)]
1970pub struct VerifyObjectRecord {
1971 pub oid: Vec<u8>,
1973 pub packed_type: PackedType,
1975 pub size: u64,
1977 pub size_in_pack: u64,
1979 pub offset: u64,
1981 pub depth: Option<u64>,
1983 pub base_oid: Option<Vec<u8>>,
1985 pub base_pack_offset: Option<u64>,
1987}
1988
1989enum DeltaBaseLink {
1991 Oid(Vec<u8>),
1993 Offset(u64),
1995}
1996
1997fn resolve_delta_depth(
2008 i: usize,
2009 base_links: &[Option<DeltaBaseLink>],
2010 by_oid: &HashMap<Vec<u8>, usize>,
2011 by_offset_idx: &HashMap<u64, usize>,
2012 records: &mut [VerifyObjectRecord],
2013) -> Result<u64> {
2014 if let Some(d) = records[i].depth {
2015 return Ok(d);
2016 }
2017 let Some(link) = &base_links[i] else {
2018 return Ok(0);
2019 };
2020 let base_idx = match link {
2021 DeltaBaseLink::Oid(oid) => by_oid.get(oid).copied(),
2022 DeltaBaseLink::Offset(off) => by_offset_idx.get(off).copied(),
2023 };
2024 records[i].depth = Some(1);
2026 let depth = match base_idx {
2027 Some(b) if b != i => {
2028 resolve_delta_depth(b, base_links, by_oid, by_offset_idx, records)?.saturating_add(1)
2029 }
2030 _ => 1,
2032 };
2033 records[i].depth = Some(depth);
2034 Ok(depth)
2035}
2036
2037pub const DEFAULT_PACK_DEPTH: usize = 50;
2039
2040#[must_use]
2042pub fn max_verify_pack_delta_depth(records: &[VerifyObjectRecord]) -> u64 {
2043 records.iter().filter_map(|r| r.depth).max().unwrap_or(0)
2044}
2045
2046pub fn apply_delta_depth_limit(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2053 let keys: Vec<ObjectId> = map.keys().copied().collect();
2054 let value_set: HashSet<ObjectId> = map.values().copied().collect();
2055 let mut tips: Vec<ObjectId> = keys
2056 .into_iter()
2057 .filter(|k| !value_set.contains(k))
2058 .collect();
2059 tips.sort();
2060
2061 let modulus = max_depth.saturating_add(1);
2062 let mut snip: HashSet<ObjectId> = HashSet::new();
2063
2064 for tip in tips {
2065 let mut chain: Vec<ObjectId> = Vec::new();
2066 let mut cur = tip;
2067 let mut seen = HashSet::new();
2068 while seen.insert(cur) {
2069 chain.push(cur);
2070 let Some(&b) = map.get(&cur) else {
2071 break;
2072 };
2073 cur = b;
2074 }
2075
2076 let n = chain.len();
2077 if n < 2 || n - 1 <= max_depth {
2078 continue;
2079 }
2080
2081 let mut total_depth = (n - 1) as u32;
2082 for &oid in &chain {
2083 let assigned = (total_depth as usize) % modulus;
2084 total_depth = total_depth.saturating_sub(1);
2085 if assigned == 0 {
2086 snip.insert(oid);
2087 }
2088 }
2089 }
2090
2091 for oid in snip {
2092 map.remove(&oid);
2093 }
2094}
2095
2096#[derive(Debug)]
2101pub(crate) struct VerifiedPackForFsck {
2102 pub idx: PackIndex,
2103 pub pack_bytes: Arc<PackData>,
2105 pub records: Vec<VerifyObjectRecord>,
2106}
2107
2108pub(crate) fn verify_pack_for_fsck(idx_path: &Path) -> Result<VerifiedPackForFsck> {
2116 let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, true)?;
2117 Ok(VerifiedPackForFsck {
2118 idx,
2119 pack_bytes,
2120 records,
2121 })
2122}
2123
2124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2126pub(crate) enum PackFsckTrailerIssue {
2127 PackChecksum,
2129 IdxMismatch,
2131}
2132
2133pub(crate) fn prepare_pack_for_object_fsck(
2137 idx_path: &Path,
2138) -> Result<(VerifiedPackForFsck, Option<PackFsckTrailerIssue>)> {
2139 let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, false)?;
2140 let trailer = pack_trailer_fsck_issue(idx_path, &idx, &pack_bytes);
2141 Ok((
2142 VerifiedPackForFsck {
2143 idx,
2144 pack_bytes,
2145 records,
2146 },
2147 trailer,
2148 ))
2149}
2150
2151fn pack_trailer_fsck_issue(
2152 idx_path: &Path,
2153 idx: &PackIndex,
2154 pack_bytes: &[u8],
2155) -> Option<PackFsckTrailerIssue> {
2156 let hb = idx.hash_bytes();
2157 let algo = HashAlgo::from_len(hb)?;
2158 if verify_trailer(algo, pack_bytes).is_err() {
2159 return Some(PackFsckTrailerIssue::PackChecksum);
2160 }
2161 let idx_file_bytes = fs::read(idx_path).ok()?;
2162 if idx_file_bytes.len() >= 2 * hb {
2163 let pack_end = pack_bytes.len().checked_sub(hb)?;
2164 let n = idx_file_bytes.len();
2165 let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
2166 if embedded != &pack_bytes[pack_end..] {
2167 return Some(PackFsckTrailerIssue::IdxMismatch);
2168 }
2169 }
2170 None
2171}
2172
2173pub fn verify_pack_and_collect(idx_path: &Path) -> Result<Vec<VerifyObjectRecord>> {
2179 let VerifiedPackForFsck {
2180 idx,
2181 pack_bytes,
2182 records,
2183 } = verify_pack_for_fsck(idx_path)?;
2184 let hb = idx.hash_bytes();
2185 for entry in idx.iter() {
2186 let obj = read_object_from_pack_bytes(&pack_bytes, &idx, entry.oid())?;
2187 let computed = hash_object_bytes(obj.kind, &obj.data, hb)?;
2188 if computed.as_slice() != entry.oid() {
2189 return Err(Error::CorruptObject(format!(
2190 "pack object hash mismatch at offset {} (index says {})",
2191 entry.offset()?,
2192 oid_bytes_to_hex(entry.oid())
2193 )));
2194 }
2195 }
2196 Ok(records)
2197}
2198
2199fn verify_pack_structure_and_records(
2200 idx_path: &Path,
2201 verify_pack_trailer: bool,
2202) -> Result<(PackIndex, Arc<PackData>, Vec<VerifyObjectRecord>)> {
2203 let idx = read_pack_index(idx_path)?;
2204 let idx_file_bytes = fs::read(idx_path).map_err(Error::Io)?;
2205 let pack_bytes = PackData::open_with_min_len(&idx.pack_path, 0)?;
2206 let hb = idx.hash_bytes();
2207 if pack_bytes.len() < 12 + hb {
2208 return Err(Error::CorruptObject(format!(
2209 "pack file {} is too small",
2210 idx.pack_path.display()
2211 )));
2212 }
2213 let pack_end = pack_bytes.len() - hb;
2214 let Some(algo) = HashAlgo::from_len(hb) else {
2215 return Err(Error::CorruptObject(format!(
2216 "unsupported OID width {hb} for pack {}",
2217 idx.pack_path.display()
2218 )));
2219 };
2220 if verify_pack_trailer {
2221 verify_trailer(algo, &pack_bytes).map_err(|e| {
2222 Error::CorruptObject(format!(
2223 "pack trailing checksum mismatch for {}: {e}",
2224 idx.pack_path.display()
2225 ))
2226 })?;
2227 if idx_file_bytes.len() >= 2 * hb {
2230 let n = idx_file_bytes.len();
2231 let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
2232 if embedded != &pack_bytes[pack_end..] {
2233 return Err(Error::CorruptObject(format!(
2234 "pack checksum in index does not match {}",
2235 idx.pack_path.display()
2236 )));
2237 }
2238 }
2239 }
2240 if &pack_bytes[0..4] != b"PACK" {
2241 return Err(Error::CorruptObject(format!(
2242 "pack file {} has invalid signature",
2243 idx.pack_path.display()
2244 )));
2245 }
2246 let version = u32::from_be_bytes(pack_bytes[4..8].try_into().unwrap_or([0, 0, 0, 0]));
2247 if version != 2 && version != 3 {
2248 return Err(Error::CorruptObject(format!(
2249 "unsupported pack version {} in {}",
2250 version,
2251 idx.pack_path.display()
2252 )));
2253 }
2254 let count = u32::from_be_bytes(pack_bytes[8..12].try_into().unwrap_or([0, 0, 0, 0])) as usize;
2255 if count != idx.len() {
2256 return Err(Error::CorruptObject(format!(
2257 "pack/index object count mismatch for {}",
2258 idx.pack_path.display()
2259 )));
2260 }
2261
2262 let mut by_offset: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
2263 for entry in idx.iter() {
2264 by_offset.insert(entry.offset()?, entry.oid().to_vec());
2265 }
2266 let offsets: Vec<u64> = by_offset.keys().copied().collect();
2267 if offsets.is_empty() {
2268 return Ok((idx, pack_bytes, Vec::new()));
2269 }
2270
2271 let mut by_oid: HashMap<Vec<u8>, usize> = HashMap::new();
2272 let mut by_offset_idx: HashMap<u64, usize> = HashMap::new();
2273 let mut records: Vec<VerifyObjectRecord> = Vec::with_capacity(offsets.len());
2274 let mut base_links: Vec<Option<DeltaBaseLink>> = Vec::with_capacity(offsets.len());
2278 for (i, offset) in offsets.iter().copied().enumerate() {
2279 let oid = by_offset.get(&offset).cloned().ok_or_else(|| {
2280 Error::CorruptObject(format!("missing object id for offset {}", offset))
2281 })?;
2282 let next_off = offsets
2283 .get(i + 1)
2284 .copied()
2285 .unwrap_or((pack_bytes.len() - hb) as u64);
2286 if next_off <= offset || next_off > (pack_bytes.len() - hb) as u64 {
2287 return Err(Error::CorruptObject(format!(
2288 "invalid object boundaries at offset {} in {}",
2289 offset,
2290 idx.pack_path.display()
2291 )));
2292 }
2293 let mut p = offset as usize;
2294 let (packed_type, size) = parse_pack_object_header(&pack_bytes, &mut p)?;
2295 let mut base_oid: Option<Vec<u8>> = None;
2296 let mut base_link: Option<DeltaBaseLink> = None;
2297
2298 match packed_type {
2299 PackedType::RefDelta => {
2300 if p + hb > pack_bytes.len() {
2301 return Err(Error::CorruptObject(format!(
2302 "truncated ref-delta base at offset {}",
2303 offset
2304 )));
2305 }
2306 let raw = pack_bytes[p..p + hb].to_vec();
2307 base_oid = Some(raw.clone());
2308 base_link = Some(DeltaBaseLink::Oid(raw));
2309 }
2310 PackedType::OfsDelta => {
2311 let base_offset = parse_ofs_delta_base(&pack_bytes, &mut p, offset)?;
2312 base_link = Some(DeltaBaseLink::Offset(base_offset));
2313 }
2314 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {}
2315 }
2316
2317 let size_in_pack = next_off - offset;
2318 let base_pack_offset = match &base_link {
2319 Some(DeltaBaseLink::Offset(off)) => Some(*off),
2320 _ => None,
2321 };
2322 records.push(VerifyObjectRecord {
2323 oid: oid.clone(),
2324 packed_type,
2325 size,
2326 size_in_pack,
2327 offset,
2328 depth: None,
2329 base_oid,
2330 base_pack_offset,
2331 });
2332 base_links.push(base_link);
2333 by_oid.insert(oid, i);
2334 by_offset_idx.insert(offset, i);
2335 }
2336
2337 for i in 0..records.len() {
2341 if base_links[i].is_some() {
2342 let _ = resolve_delta_depth(i, &base_links, &by_oid, &by_offset_idx, &mut records)?;
2343 }
2344 }
2345
2346 Ok((idx, pack_bytes, records))
2347}
2348
2349pub fn read_alternates_recursive(objects_dir: &Path) -> Result<Vec<PathBuf>> {
2355 let mut visited = HashSet::new();
2356 let mut out = Vec::new();
2357 read_alternates_inner(objects_dir, &mut visited, &mut out, 0)?;
2358 Ok(out)
2359}
2360
2361const MAX_ALTERNATE_DEPTH: usize = 5;
2363
2364fn parse_alternates_file_line(raw: &str) -> Option<PathBuf> {
2369 let line = raw.trim();
2370 if line.is_empty() || line.starts_with('#') {
2371 return None;
2372 }
2373 if line.starts_with('"') {
2374 let path = unquote_alternates_c_style(line)?;
2375 if path.is_empty() {
2376 return None;
2377 }
2378 return Some(PathBuf::from(path));
2379 }
2380 Some(PathBuf::from(line))
2381}
2382
2383fn unquote_alternates_c_style(line: &str) -> Option<String> {
2385 let bytes = line.as_bytes();
2386 if bytes.first() != Some(&b'"') {
2387 return None;
2388 }
2389 let mut out = Vec::new();
2390 let mut i = 1usize;
2391 while i < bytes.len() {
2392 if bytes[i] == b'"' {
2393 return String::from_utf8(out).ok();
2394 }
2395 if bytes[i] == b'\\' {
2396 i += 1;
2397 if i >= bytes.len() {
2398 return None;
2399 }
2400 match bytes[i] {
2401 b'\\' => out.push(b'\\'),
2402 b'"' => out.push(b'"'),
2403 b'a' => out.push(7),
2404 b'b' => out.push(8),
2405 b'f' => out.push(12),
2406 b'n' => out.push(b'\n'),
2407 b'r' => out.push(b'\r'),
2408 b't' => out.push(b'\t'),
2409 b'v' => out.push(11),
2410 c if c.is_ascii_digit() => {
2411 if i + 2 >= bytes.len() {
2412 return None;
2413 }
2414 let oct = std::str::from_utf8(&bytes[i..i + 3]).ok()?;
2415 out.push(u8::from_str_radix(oct, 8).ok()?);
2416 i += 2;
2417 }
2418 other => out.push(other),
2419 }
2420 i += 1;
2421 continue;
2422 }
2423 out.push(bytes[i]);
2424 i += 1;
2425 }
2426 None
2427}
2428
2429fn read_alternates_inner(
2430 objects_dir: &Path,
2431 visited: &mut HashSet<PathBuf>,
2432 out: &mut Vec<PathBuf>,
2433 depth: usize,
2434) -> Result<()> {
2435 if depth > MAX_ALTERNATE_DEPTH {
2436 return Ok(());
2437 }
2438 let alt_file = objects_dir.join("info").join("alternates");
2442 let text = match fs::read_to_string(&alt_file) {
2443 Ok(text) => text,
2444 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
2445 Err(err) => return Err(Error::Io(err)),
2446 };
2447 let canonical = canonical_or_self(objects_dir);
2448
2449 for raw in text.lines() {
2450 let Some(path) = parse_alternates_file_line(raw) else {
2451 continue;
2452 };
2453 let candidate = if path.is_absolute() {
2454 path
2455 } else {
2456 canonical.join(path)
2457 };
2458 let candidate = canonical_or_self(&candidate);
2459 if visited.insert(candidate.clone()) {
2460 out.push(candidate.clone());
2461 read_alternates_inner(&candidate, visited, out, depth + 1)?;
2462 }
2463 }
2464 Ok(())
2465}
2466
2467fn canonical_or_self(path: &Path) -> PathBuf {
2468 fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
2469}
2470
2471fn packed_type_to_kind(pt: PackedType) -> Result<ObjectKind> {
2473 match pt {
2474 PackedType::Commit => Ok(ObjectKind::Commit),
2475 PackedType::Tree => Ok(ObjectKind::Tree),
2476 PackedType::Blob => Ok(ObjectKind::Blob),
2477 PackedType::Tag => Ok(ObjectKind::Tag),
2478 PackedType::OfsDelta | PackedType::RefDelta => Err(Error::CorruptObject(
2479 "cannot convert delta type to object kind directly".to_owned(),
2480 )),
2481 }
2482}
2483
2484thread_local! {
2486 static DELTA_SIZE_ZLIB: std::cell::RefCell<crate::zlib_inflate::ZlibInflateScratch> =
2487 std::cell::RefCell::new(crate::zlib_inflate::ZlibInflateScratch::default());
2488}
2489thread_local! {
2491 static PACK_READ_INFO_CACHE: std::cell::RefCell<HashMap<PackReadInfoCacheKey, ObjectInfo>> =
2492 std::cell::RefCell::new(HashMap::new());
2493}
2494
2495const PACK_READ_INFO_CACHE_CAP: usize = 600_000;
2496
2497#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2498struct PackReadInfoCacheKey {
2499 pack_id: u32,
2500 offset: u64,
2501}
2502
2503fn pack_read_info_cache_key(pack_path: &Path, offset: u64) -> Option<PackReadInfoCacheKey> {
2504 if !crate::pack_store::PackStore::has_context() {
2505 return None;
2506 }
2507 Some(PackReadInfoCacheKey {
2508 pack_id: pack_cache::pack_id_for(pack_path),
2509 offset,
2510 })
2511}
2512
2513pub(crate) fn clear_pack_read_info_cache() {
2515 PACK_READ_INFO_CACHE.with(|c| c.borrow_mut().clear());
2516}
2517
2518fn drop_pack_read_info_cache_for(pack_path: &Path) {
2519 let pack_id = pack_cache::pack_id_for(pack_path);
2520 PACK_READ_INFO_CACHE.with(|c| {
2521 c.borrow_mut().retain(|k, _| k.pack_id != pack_id);
2522 });
2523}
2524
2525fn cached_pack_read_info(pack_path: &Path, offset: u64) -> Option<ObjectInfo> {
2526 let key = pack_read_info_cache_key(pack_path, offset)?;
2527 PACK_READ_INFO_CACHE.with(|c| c.borrow().get(&key).cloned())
2528}
2529
2530fn remember_pack_read_info(pack_path: &Path, offset: u64, info: ObjectInfo) {
2531 let Some(key) = pack_read_info_cache_key(pack_path, offset) else {
2532 return;
2533 };
2534 PACK_READ_INFO_CACHE.with(|c| {
2535 let mut g = c.borrow_mut();
2536 if g.len() >= PACK_READ_INFO_CACHE_CAP {
2537 g.clear();
2538 }
2539 g.insert(key, info);
2540 });
2541}
2542pub(crate) fn read_delta_result_size_from_pack_zlib(bytes: &[u8], pos: &mut usize) -> Result<u64> {
2546 DELTA_SIZE_ZLIB.with(|slot| slot.borrow_mut().read_pack_delta_result_size(bytes, pos))
2547}
2548
2549fn decompress_pack_data_into(
2550 bytes: &[u8],
2551 pos: &mut usize,
2552 expected_size: u64,
2553 scratch: &mut ZlibInflateScratch,
2554 out: &mut Vec<u8>,
2555) -> Result<()> {
2556 scratch.decompress_fixed_into(bytes, pos, expected_size, out)
2557}
2558
2559const MAX_DELTA_CHAIN_READ_DEPTH: usize = 4096;
2568
2569#[derive(Clone)]
2571struct PendingDeltaFrame {
2572 pack_id: u32,
2573 offset: u64,
2574 delta: Vec<u8>,
2575}
2576
2577#[derive(Default)]
2581struct DeltaChainState {
2582 depth: usize,
2583 visited: HashSet<(u32, u64)>,
2584 pending: Vec<PendingDeltaFrame>,
2585 inflate: ZlibInflateScratch,
2586 whole_object: Vec<u8>,
2587 delta_decompress: Vec<u8>,
2588 chain_a: Vec<u8>,
2589 chain_b: Vec<u8>,
2590}
2591
2592thread_local! {
2594 static PACK_READ_DELTA_STATE: std::cell::RefCell<DeltaChainState> =
2595 std::cell::RefCell::new(DeltaChainState::default());
2596}
2597
2598fn with_pack_read_delta_state<R>(
2599 start_depth: usize,
2600 f: impl FnOnce(&mut DeltaChainState) -> R,
2601) -> R {
2602 PACK_READ_DELTA_STATE.with(|slot| {
2603 let mut state = slot.borrow_mut();
2604 state.depth = start_depth;
2605 state.visited.clear();
2606 f(&mut state)
2607 })
2608}
2609
2610impl DeltaChainState {
2611 fn visit(&mut self, pack_id: u32, offset: u64) -> Result<()> {
2612 if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
2613 return Err(Error::DeltaChainTooDeep {
2614 limit: MAX_DELTA_CHAIN_READ_DEPTH,
2615 });
2616 }
2617 if !self.visited.insert((pack_id, offset)) {
2618 return Err(Error::DeltaChainTooDeep {
2619 limit: MAX_DELTA_CHAIN_READ_DEPTH,
2620 });
2621 }
2622 Ok(())
2623 }
2624
2625 fn next_hop(&mut self) -> Result<()> {
2626 self.depth += 1;
2627 if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
2628 return Err(Error::DeltaChainTooDeep {
2629 limit: MAX_DELTA_CHAIN_READ_DEPTH,
2630 });
2631 }
2632 Ok(())
2633 }
2634}
2635
2636fn copy_payload_preserve_capacity(out: &mut Vec<u8>, payload: &[u8]) {
2638 out.clear();
2639 out.extend_from_slice(payload);
2640}
2641
2642#[allow(clippy::too_many_arguments)]
2645fn apply_delta_chain_forward_into(
2646 out: &mut Vec<u8>,
2647 kind: ObjectKind,
2648 base_data: &[u8],
2649 whole_base: Option<(u32, u64)>,
2650 pending: &[PendingDeltaFrame],
2651 result_pack_id: u32,
2652 result_offset: u64,
2653 populate_delta_lru: bool,
2654 state: &mut DeltaChainState,
2655) -> Result<()> {
2656 if pending.is_empty() {
2657 copy_payload_preserve_capacity(out, base_data);
2658 return Ok(());
2659 }
2660
2661 let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
2662 let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
2663 Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
2664 if caching {
2665 if let Some((pid, off)) = whole_base {
2666 cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
2667 }
2668 }
2669
2670 let chain_a = &mut state.chain_a;
2671 let chain_b = &mut state.chain_b;
2672 let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
2673 let last = rev
2674 .pop()
2675 .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
2676 if rev.is_empty() {
2677 apply_delta_into(out, base_data, &last.delta)?;
2678 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
2679 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
2680 }
2681 if caching {
2682 pack_cache::put_delta_bases(kind, &cache_inserts);
2683 }
2684 return Ok(());
2685 }
2686
2687 let Some(first) = rev.first().copied() else {
2688 return Err(Error::CorruptObject(
2689 "delta chain missing base-adjacent frame".to_owned(),
2690 ));
2691 };
2692 apply_delta_into(chain_a, base_data, &first.delta)?;
2693 if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
2694 cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
2695 }
2696 let mut src_is_a = true;
2697 for frame in rev.iter().skip(1) {
2698 if src_is_a {
2699 apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
2700 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
2701 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
2702 }
2703 } else {
2704 apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
2705 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
2706 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
2707 }
2708 }
2709 src_is_a = !src_is_a;
2710 }
2711 let penultimate_base = if src_is_a {
2712 chain_a.as_slice()
2713 } else {
2714 chain_b.as_slice()
2715 };
2716 apply_delta_into(out, penultimate_base, &last.delta)?;
2717 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
2718 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
2719 }
2720 if caching {
2721 pack_cache::put_delta_bases(kind, &cache_inserts);
2722 }
2723 Ok(())
2724}
2725
2726#[allow(clippy::too_many_arguments)]
2727fn finish_pack_read_with_pending(
2728 out: &mut Vec<u8>,
2729 kind: ObjectKind,
2730 base: &[u8],
2731 whole_base: Option<(u32, u64)>,
2732 result_pack_id: u32,
2733 result_offset: u64,
2734 populate_delta_lru: bool,
2735 state: &mut DeltaChainState,
2736) -> Result<ObjectKind> {
2737 let pending = std::mem::take(&mut state.pending);
2738 apply_delta_chain_forward_into(
2739 out,
2740 kind,
2741 base,
2742 whole_base,
2743 &pending,
2744 result_pack_id,
2745 result_offset,
2746 populate_delta_lru,
2747 state,
2748 )?;
2749 Ok(kind)
2750}
2751
2752enum PackIndexHandle<'a> {
2755 Borrowed(&'a PackIndex),
2756 Shared(Arc<PackIndex>),
2757}
2758
2759impl<'a> Clone for PackIndexHandle<'a> {
2760 fn clone(&self) -> Self {
2761 match self {
2762 Self::Borrowed(idx) => Self::Borrowed(idx),
2763 Self::Shared(arc) => Self::Shared(Arc::clone(arc)),
2764 }
2765 }
2766}
2767
2768impl PackIndexHandle<'_> {
2769 fn get(&self) -> &PackIndex {
2770 match self {
2771 Self::Borrowed(idx) => idx,
2772 Self::Shared(idx) => idx.as_ref(),
2773 }
2774 }
2775}
2776
2777fn find_other_pack_index(
2778 objects_dir: &Path,
2779 current: &PackIndex,
2780 oid: &ObjectId,
2781) -> Result<Option<Arc<PackIndex>>> {
2782 for idx in read_local_pack_indexes_cached(objects_dir)? {
2783 if idx.idx_path == current.idx_path {
2784 continue;
2785 }
2786 if idx.contains(oid)? {
2787 return Ok(Some(Arc::clone(&idx)));
2788 }
2789 }
2790 Ok(None)
2791}
2792
2793fn rescue_in_pack_base(
2796 idx: &PackIndex,
2797 base_offset: u64,
2798 objects_dir: Option<&Path>,
2799 state: &mut DeltaChainState,
2800) -> Result<Option<(ObjectKind, Vec<u8>)>> {
2801 let Some(pos) = idx.find_position_by_offset_sorted(base_offset)? else {
2802 return Ok(None);
2803 };
2804 let entry_oid = idx.oid_at(pos);
2805 if entry_oid.len() != idx.hash_bytes() {
2806 return Ok(None);
2807 }
2808 let base_oid = ObjectId::from_bytes(entry_oid)?;
2809 let Some(dir) = objects_dir else {
2810 return Ok(None);
2811 };
2812 let loose = dir
2813 .join(base_oid.loose_prefix())
2814 .join(base_oid.loose_suffix());
2815 if loose.is_file() {
2816 if let Ok(obj) = crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid) {
2817 return Ok(Some((obj.kind, obj.data)));
2818 }
2819 }
2820 if let Some(other_idx) = find_other_pack_index(dir, idx, &base_oid)? {
2821 let Some(off) = other_idx.find_offset(&base_oid)? else {
2822 return Ok(None);
2823 };
2824 if let Ok((kind, data)) =
2825 resolve_pack_object_at(PackIndexHandle::Shared(other_idx), off, Some(dir), state)
2826 {
2827 return Ok(Some((kind, data)));
2828 }
2829 }
2830 Ok(None)
2831}
2832
2833fn pack_bytes_parse_may_be_stale(err: &Error) -> bool {
2835 matches!(err, Error::CorruptObject(_) | Error::Zlib(_))
2836}
2837
2838fn resolve_pack_object_at(
2844 start_idx: PackIndexHandle<'_>,
2845 start_offset: u64,
2846 objects_dir: Option<&Path>,
2847 state: &mut DeltaChainState,
2848) -> Result<(ObjectKind, Vec<u8>)> {
2849 resolve_pack_object_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
2850}
2851
2852fn resolve_pack_object_at_into(
2853 start_idx: PackIndexHandle<'_>,
2854 start_offset: u64,
2855 objects_dir: Option<&Path>,
2856 state: &mut DeltaChainState,
2857 populate_delta_lru: bool,
2858 out: &mut Vec<u8>,
2859) -> Result<ObjectKind> {
2860 resolve_pack_object_at_with_bytes_into(
2861 start_idx,
2862 start_offset,
2863 objects_dir,
2864 state,
2865 None,
2866 populate_delta_lru,
2867 out,
2868 )
2869}
2870
2871fn resolve_pack_object_at_with_bytes(
2872 start_idx: PackIndexHandle<'_>,
2873 start_offset: u64,
2874 objects_dir: Option<&Path>,
2875 state: &mut DeltaChainState,
2876 pack_bytes_override: Option<&[u8]>,
2877) -> Result<(ObjectKind, Vec<u8>)> {
2878 let mut out = Vec::new();
2879 let kind = resolve_pack_object_at_with_bytes_into(
2880 start_idx,
2881 start_offset,
2882 objects_dir,
2883 state,
2884 pack_bytes_override,
2885 true,
2886 &mut out,
2887 )?;
2888 Ok((kind, out))
2889}
2890
2891fn resolve_pack_object_at_with_bytes_into(
2892 start_idx: PackIndexHandle<'_>,
2893 start_offset: u64,
2894 objects_dir: Option<&Path>,
2895 state: &mut DeltaChainState,
2896 pack_bytes_override: Option<&[u8]>,
2897 populate_delta_lru: bool,
2898 out: &mut Vec<u8>,
2899) -> Result<ObjectKind> {
2900 let pack_path = start_idx.get().pack_path.clone();
2901 let start_pack_id = pack_cache::pack_id_for(&pack_path);
2902 for attempt in 0..2 {
2903 match resolve_pack_object_at_body(
2904 start_idx.clone(),
2905 start_offset,
2906 start_pack_id,
2907 objects_dir,
2908 state,
2909 pack_bytes_override,
2910 populate_delta_lru,
2911 out,
2912 ) {
2913 Ok(kind) => return Ok(kind),
2914 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
2915 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
2916 state.visited.retain(|(id, _)| *id != start_pack_id);
2917 continue;
2918 }
2919 return Err(e);
2920 }
2921 Err(e) => return Err(e),
2922 }
2923 }
2924 unreachable!("at most two resolution attempts")
2925}
2926
2927#[allow(clippy::too_many_arguments)]
2928fn resolve_pack_object_at_body(
2929 mut cur_idx: PackIndexHandle<'_>,
2930 start_offset: u64,
2931 result_pack_id: u32,
2932 objects_dir: Option<&Path>,
2933 state: &mut DeltaChainState,
2934 pack_bytes_override: Option<&[u8]>,
2935 populate_delta_lru: bool,
2936 out: &mut Vec<u8>,
2937) -> Result<ObjectKind> {
2938 let result_offset = start_offset;
2939 let mut cur_offset = start_offset;
2940 state.pending.clear();
2941
2942 let mut cur_pack_path = cur_idx.get().pack_path.clone();
2943 let mut cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
2944 let mut validated_pack_count = false;
2945
2946 enum PackBytesSource<'a> {
2947 Borrowed(&'a [u8]),
2948 Cached(Arc<PackData>),
2949 }
2950
2951 let mut pack_bytes = match pack_bytes_override {
2952 Some(b) => PackBytesSource::Borrowed(b),
2953 None => PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?),
2954 };
2955
2956 loop {
2957 let idx_ref = cur_idx.get();
2958 if idx_ref.pack_path != cur_pack_path {
2959 cur_pack_path = idx_ref.pack_path.clone();
2960 pack_bytes = PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?);
2961 cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
2962 validated_pack_count = false;
2963 }
2964 let pack_id = cur_pack_id;
2965 state.visit(pack_id, cur_offset)?;
2966
2967 let bytes: &[u8] = match &pack_bytes {
2968 PackBytesSource::Borrowed(b) => b,
2969 PackBytesSource::Cached(c) => c.as_ref(),
2970 };
2971
2972 if !validated_pack_count {
2973 validate_pack_index_object_count(bytes, idx_ref)?;
2974 validated_pack_count = true;
2975 }
2976
2977 let object_start = cur_offset;
2978 let mut pos = cur_offset as usize;
2979 let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
2980 Ok(v) => v,
2981 Err(err) => {
2982 if let Some((kind, data)) =
2983 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
2984 {
2985 return finish_pack_read_with_pending(
2986 out,
2987 kind,
2988 &data,
2989 None,
2990 result_pack_id,
2991 result_offset,
2992 populate_delta_lru,
2993 state,
2994 );
2995 }
2996 return Err(err);
2997 }
2998 };
2999
3000 match packed_type {
3001 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3002 let kind = packed_type_to_kind(packed_type)?;
3003 if state.pending.is_empty() {
3004 if let Err(err) =
3005 decompress_pack_data_into(bytes, &mut pos, size, &mut state.inflate, out)
3006 {
3007 if let Some((rescue_kind, rescued)) =
3008 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3009 {
3010 return finish_pack_read_with_pending(
3011 out,
3012 rescue_kind,
3013 &rescued,
3014 None,
3015 result_pack_id,
3016 result_offset,
3017 populate_delta_lru,
3018 state,
3019 );
3020 }
3021 return Err(err);
3022 }
3023 return Ok(kind);
3024 }
3025 state.whole_object.clear();
3026 if let Err(err) = decompress_pack_data_into(
3027 bytes,
3028 &mut pos,
3029 size,
3030 &mut state.inflate,
3031 &mut state.whole_object,
3032 ) {
3033 if let Some((rescue_kind, rescued)) =
3034 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3035 {
3036 return finish_pack_read_with_pending(
3037 out,
3038 rescue_kind,
3039 &rescued,
3040 None,
3041 result_pack_id,
3042 result_offset,
3043 populate_delta_lru,
3044 state,
3045 );
3046 }
3047 return Err(err);
3048 }
3049 let whole = std::mem::take(&mut state.whole_object);
3050 return finish_pack_read_with_pending(
3051 out,
3052 kind,
3053 &whole,
3054 Some((pack_id, cur_offset)),
3055 result_pack_id,
3056 result_offset,
3057 populate_delta_lru,
3058 state,
3059 );
3060 }
3061 PackedType::OfsDelta => {
3062 state.next_hop()?;
3063 let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
3064 state.delta_decompress.clear();
3065 decompress_pack_data_into(
3066 bytes,
3067 &mut pos,
3068 size,
3069 &mut state.inflate,
3070 &mut state.delta_decompress,
3071 )?;
3072 state.pending.push(PendingDeltaFrame {
3073 pack_id,
3074 offset: object_start,
3075 delta: std::mem::take(&mut state.delta_decompress),
3076 });
3077 if let Some((base_kind, base_data)) =
3078 pack_cache::get_delta_base(pack_id, base_offset)
3079 {
3080 return finish_pack_read_with_pending(
3081 out,
3082 base_kind,
3083 base_data.as_ref(),
3084 None,
3085 result_pack_id,
3086 result_offset,
3087 populate_delta_lru,
3088 state,
3089 );
3090 }
3091 cur_offset = base_offset;
3092 }
3093 PackedType::RefDelta => {
3094 state.next_hop()?;
3095 let hb = idx_ref.hash_bytes();
3096 if pos + hb > bytes.len() {
3097 return Err(Error::CorruptObject(
3098 "truncated ref-delta base OID".to_owned(),
3099 ));
3100 }
3101 let base_raw = bytes[pos..pos + hb].to_vec();
3102 pos += hb;
3103 state.delta_decompress.clear();
3104 decompress_pack_data_into(
3105 bytes,
3106 &mut pos,
3107 size,
3108 &mut state.inflate,
3109 &mut state.delta_decompress,
3110 )?;
3111 state.pending.push(PendingDeltaFrame {
3112 pack_id,
3113 offset: object_start,
3114 delta: std::mem::take(&mut state.delta_decompress),
3115 });
3116
3117 if let Ok(base_oid) = ObjectId::from_bytes(base_raw.as_slice()) {
3118 if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
3119 if let Some((base_kind, base_data)) =
3120 pack_cache::get_delta_base(pack_id, base_offset)
3121 {
3122 return finish_pack_read_with_pending(
3123 out,
3124 base_kind,
3125 base_data.as_ref(),
3126 None,
3127 result_pack_id,
3128 result_offset,
3129 populate_delta_lru,
3130 state,
3131 );
3132 }
3133 cur_offset = base_offset;
3134 continue;
3135 }
3136 }
3137
3138 if hb == 20 {
3139 if let (Some(dir), Ok(base_oid)) =
3140 (objects_dir, ObjectId::from_bytes(base_raw.as_slice()))
3141 {
3142 let loose = dir
3143 .join(base_oid.loose_prefix())
3144 .join(base_oid.loose_suffix());
3145 if loose.is_file() {
3146 if let Ok(obj) =
3147 crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid)
3148 {
3149 return finish_pack_read_with_pending(
3150 out,
3151 obj.kind,
3152 &obj.data,
3153 None,
3154 result_pack_id,
3155 result_offset,
3156 populate_delta_lru,
3157 state,
3158 );
3159 }
3160 }
3161 if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
3162 let Some(off) = other_idx.find_offset(&base_oid)? else {
3163 return Err(Error::ObjectNotFound(base_oid.to_hex()));
3164 };
3165 cur_idx = PackIndexHandle::Shared(other_idx);
3166 cur_offset = off;
3167 continue;
3168 }
3169 }
3170 }
3171 return Err(Error::CorruptObject(format!(
3172 "ref-delta base {} not found in pack",
3173 oid_bytes_to_hex(&base_raw)
3174 )));
3175 }
3176 }
3177 }
3178}
3179
3180fn resolve_pack_object_info_at(
3182 start_idx: PackIndexHandle<'_>,
3183 start_offset: u64,
3184 objects_dir: Option<&Path>,
3185 state: &mut DeltaChainState,
3186) -> Result<ObjectInfo> {
3187 resolve_pack_object_info_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
3188}
3189
3190fn resolve_pack_object_info_at_body(
3191 cur_idx: PackIndexHandle<'_>,
3192 start_offset: u64,
3193 objects_dir: Option<&Path>,
3194 state: &mut DeltaChainState,
3195 pack_bytes_override: Option<&[u8]>,
3196) -> Result<ObjectInfo> {
3197 let cached_pack;
3198 let cur_bytes: &[u8] = if let Some(b) = pack_bytes_override {
3199 b
3200 } else {
3201 cached_pack = read_pack_bytes_cached(&cur_idx.get().pack_path)?;
3202 cached_pack.as_ref()
3203 };
3204 let mut cur_offset = start_offset;
3205 let mut result_size: Option<u64> = None;
3206
3207 loop {
3208 let idx_ref = cur_idx.get();
3209 let pack_id = pack_cache::pack_id_for(&idx_ref.pack_path);
3210 state.visit(pack_id, cur_offset)?;
3211
3212 if let Some(base) = cached_pack_read_info(&idx_ref.pack_path, cur_offset) {
3213 if let Some(size) = result_size {
3214 return Ok(ObjectInfo {
3215 kind: base.kind,
3216 size,
3217 });
3218 }
3219 return Ok(base);
3220 }
3221 let object_start = cur_offset;
3222 let mut pos = cur_offset as usize;
3223 let (packed_type, header_size) = parse_pack_object_header(cur_bytes, &mut pos)?;
3224
3225 match packed_type {
3226 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3227 let kind = packed_type_to_kind(packed_type)?;
3228 remember_pack_read_info(
3229 &idx_ref.pack_path,
3230 cur_offset,
3231 ObjectInfo {
3232 kind,
3233 size: header_size,
3234 },
3235 );
3236 let size = result_size.unwrap_or(header_size);
3237 return Ok(ObjectInfo { kind, size });
3238 }
3239 PackedType::OfsDelta => {
3240 state.next_hop()?;
3241 let base_offset = parse_ofs_delta_base(cur_bytes, &mut pos, object_start)?;
3242 if result_size.is_none() {
3243 result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
3244 }
3245 cur_offset = base_offset;
3246 }
3247 PackedType::RefDelta => {
3248 state.next_hop()?;
3249 let hb = idx_ref.hash_bytes();
3250 if pos + hb > cur_bytes.len() {
3251 return Err(Error::CorruptObject(
3252 "truncated ref-delta base OID".to_owned(),
3253 ));
3254 }
3255 let base_raw = &cur_bytes[pos..pos + hb];
3256 pos += hb;
3257 if result_size.is_none() {
3258 result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
3259 }
3260
3261 let base_oid = ObjectId::from_bytes(base_raw)?;
3262 if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
3263 cur_offset = base_offset;
3264 continue;
3265 }
3266
3267 if let Some(dir) = objects_dir {
3268 let loose = dir
3269 .join(base_oid.loose_prefix())
3270 .join(base_oid.loose_suffix());
3271 if loose.is_file() {
3272 let info = crate::odb::read_loose_object_info(&loose)?;
3273 let size = result_size.unwrap_or(info.size);
3274 return Ok(ObjectInfo {
3275 kind: info.kind,
3276 size,
3277 });
3278 }
3279 if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
3280 let Some(off) = other_idx.find_offset(&base_oid)? else {
3281 return Err(Error::ObjectNotFound(base_oid.to_hex()));
3282 };
3283 let base_info = resolve_pack_object_info_at(
3284 PackIndexHandle::Shared(other_idx),
3285 off,
3286 objects_dir,
3287 state,
3288 )?;
3289 let size = result_size.unwrap_or(base_info.size);
3290 return Ok(ObjectInfo {
3291 kind: base_info.kind,
3292 size,
3293 });
3294 }
3295 }
3296 return Err(Error::CorruptObject(format!(
3297 "ref-delta base {} not found in pack",
3298 oid_bytes_to_hex(base_raw)
3299 )));
3300 }
3301 }
3302 }
3303}
3304
3305fn read_pack_object_at(
3306 pack_bytes: &[u8],
3307 start_offset: u64,
3308 idx: &PackIndex,
3309 objects_dir: Option<&Path>,
3310 start_depth: usize,
3311) -> Result<(ObjectKind, Vec<u8>)> {
3312 let mut state = DeltaChainState {
3313 depth: start_depth,
3314 visited: HashSet::new(),
3315 ..Default::default()
3316 };
3317 resolve_pack_object_at_with_bytes(
3318 PackIndexHandle::Borrowed(idx),
3319 start_offset,
3320 objects_dir,
3321 &mut state,
3322 Some(pack_bytes),
3323 )
3324}
3325
3326pub fn read_object_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<Object> {
3335 if crate::pack_store::PackStore::has_context() {
3336 return read_object_from_pack_at_depth(idx, oid, 0);
3337 }
3338 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3339 crate::pack_store::PackStore::with_context(store, || {
3340 read_object_from_pack_at_depth(idx, oid, 0)
3341 })
3342}
3343
3344pub fn read_object_from_pack_into(
3350 idx: &PackIndex,
3351 oid: &ObjectId,
3352 buf: &mut Vec<u8>,
3353) -> Result<ObjectKind> {
3354 let Some(offset) = idx.find_offset(oid)? else {
3355 return Err(Error::ObjectNotFound(oid.to_hex()));
3356 };
3357 if crate::pack_store::PackStore::has_context() {
3358 return read_object_at_depth_into(idx, offset, 0, false, buf);
3359 }
3360 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3361 crate::pack_store::PackStore::with_context(store, || {
3362 read_object_at_depth_into(idx, offset, 0, false, buf)
3363 })
3364}
3365
3366pub fn read_object_info_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<ObjectInfo> {
3372 let Some(offset) = idx.find_offset(oid)? else {
3373 return Err(Error::ObjectNotFound(oid.to_hex()));
3374 };
3375 read_object_info_from_pack_at_offset(idx, offset)
3376}
3377
3378pub fn read_object_info_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<ObjectInfo> {
3384 let pack_path = idx.pack_path.clone();
3385 for attempt in 0..2 {
3386 let pack_bytes = read_pack_bytes_cached(&pack_path)?;
3387 if !pack_cache::pack_object_count_validated(&pack_path) {
3388 match validate_pack_index_object_count(&pack_bytes, idx) {
3389 Ok(()) => pack_cache::mark_pack_object_count_validated(&pack_path),
3390 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3391 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3392 continue;
3393 }
3394 return Err(e);
3395 }
3396 Err(e) => return Err(e),
3397 }
3398 }
3399 match read_object_info_from_pack_at_offset_with_bytes(idx, offset, pack_bytes.as_ref()) {
3400 Ok(info) => return Ok(info),
3401 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3402 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3403 continue;
3404 }
3405 return Err(e);
3406 }
3407 Err(e) => return Err(e),
3408 }
3409 }
3410 unreachable!("at most two read attempts")
3411}
3412
3413fn read_object_info_from_pack_at_offset_with_bytes(
3414 idx: &PackIndex,
3415 offset: u64,
3416 pack_bytes: &[u8],
3417) -> Result<ObjectInfo> {
3418 if let Some(info) = read_whole_pack_entry_info_fast(idx, offset, pack_bytes) {
3419 return Ok(info);
3420 }
3421 let objects_dir = idx.pack_path.parent().and_then(Path::parent);
3422 with_pack_read_delta_state(0, |state| {
3423 resolve_pack_object_info_at_with_bytes(
3424 PackIndexHandle::Borrowed(idx),
3425 offset,
3426 objects_dir,
3427 state,
3428 Some(pack_bytes),
3429 )
3430 })
3431}
3432
3433fn resolve_pack_object_info_at_with_bytes(
3434 start_idx: PackIndexHandle<'_>,
3435 start_offset: u64,
3436 objects_dir: Option<&Path>,
3437 state: &mut DeltaChainState,
3438 pack_bytes_override: Option<&[u8]>,
3439) -> Result<ObjectInfo> {
3440 let idx_ref = start_idx.get();
3441 let pack_path = idx_ref.pack_path.clone();
3442 let start_pack_id = pack_cache::pack_id_for(&pack_path);
3443 if let Some(info) = cached_pack_read_info(&pack_path, start_offset) {
3444 return Ok(info);
3445 }
3446 for attempt in 0..2 {
3447 match resolve_pack_object_info_at_body(
3448 start_idx.clone(),
3449 start_offset,
3450 objects_dir,
3451 state,
3452 pack_bytes_override,
3453 ) {
3454 Ok(v) => {
3455 remember_pack_read_info(&pack_path, start_offset, v);
3456 return Ok(v);
3457 }
3458 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3459 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3460 state.visited.retain(|(id, _)| *id != start_pack_id);
3461 continue;
3462 }
3463 return Err(e);
3464 }
3465 Err(e) => return Err(e),
3466 }
3467 }
3468 unreachable!("at most two resolution attempts")
3469}
3470
3471#[inline]
3473fn read_whole_pack_entry_info_fast(
3474 _idx: &PackIndex,
3475 offset: u64,
3476 pack_bytes: &[u8],
3477) -> Option<ObjectInfo> {
3478 let mut pos = offset as usize;
3479 if pos >= pack_bytes.len() {
3480 return None;
3481 }
3482 let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
3483 match packed_type {
3484 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3485 let kind = packed_type_to_kind(packed_type).ok()?;
3486 Some(ObjectInfo {
3487 kind,
3488 size: header_size,
3489 })
3490 }
3491 PackedType::OfsDelta | PackedType::RefDelta => None,
3492 }
3493}
3494
3495thread_local! {
3497 static WHOLE_OBJECT_ZLIB: RefCell<ZlibInflateScratch> =
3498 RefCell::new(ZlibInflateScratch::default());
3499}
3500
3501pub(crate) const COMMIT_HEADER_READ_PREFIX: usize = 512;
3503
3504fn read_whole_pack_entry_object_fast(
3505 idx: &PackIndex,
3506 offset: u64,
3507 pack_bytes: &[u8],
3508) -> Option<Object> {
3509 let mut data = Vec::new();
3510 read_whole_pack_entry_object_into(idx, offset, pack_bytes, &mut data)
3511 .map(|kind| Object::new(kind, data))
3512}
3513
3514fn read_whole_pack_entry_object_into(
3515 idx: &PackIndex,
3516 offset: u64,
3517 pack_bytes: &[u8],
3518 out: &mut Vec<u8>,
3519) -> Option<ObjectKind> {
3520 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
3521 if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
3522 out.clear();
3523 out.extend_from_slice(data.as_ref());
3524 return Some(kind);
3525 }
3526 let mut pos = offset as usize;
3527 if pos >= pack_bytes.len() {
3528 return None;
3529 }
3530 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
3531 match packed_type {
3532 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3533 let kind = packed_type_to_kind(packed_type).ok()?;
3534 WHOLE_OBJECT_ZLIB.with(|slot| {
3535 let mut inflate = slot.borrow_mut();
3536 decompress_pack_data_into(pack_bytes, &mut pos, size, &mut inflate, out)
3537 .ok()
3538 .map(|()| kind)
3539 })
3540 }
3541 PackedType::OfsDelta | PackedType::RefDelta => None,
3542 }
3543}
3544
3545fn read_object_from_packs_in_context_into(
3546 objects_dir: &Path,
3547 oid: &ObjectId,
3548 buf: &mut Vec<u8>,
3549) -> Result<ObjectKind> {
3550 match try_read_object_from_packs_with_options_into(
3551 objects_dir,
3552 oid,
3553 PackLookupOptions::ALL_LOCAL_PACKS,
3554 false,
3555 buf,
3556 ) {
3557 Ok(kind) => Ok(kind),
3558 Err(err @ Error::ObjectNotFound(_)) => {
3559 if reprepare_pack_directory_on_miss(objects_dir)? {
3560 try_read_object_from_packs_with_options_into(
3561 objects_dir,
3562 oid,
3563 PackLookupOptions::ALL_LOCAL_PACKS,
3564 false,
3565 buf,
3566 )
3567 } else {
3568 Err(err)
3569 }
3570 }
3571 Err(err) => Err(err),
3572 }
3573}
3574
3575fn read_object_from_packs_in_context(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
3576 match try_read_object_from_packs_with_options(
3577 objects_dir,
3578 oid,
3579 PackLookupOptions::ALL_LOCAL_PACKS,
3580 ) {
3581 Ok(obj) => Ok(obj),
3582 Err(err @ Error::ObjectNotFound(_)) => {
3583 if reprepare_pack_directory_on_miss_in_context(objects_dir)? {
3584 try_read_object_from_packs_with_options(
3585 objects_dir,
3586 oid,
3587 PackLookupOptions::ALL_LOCAL_PACKS,
3588 )
3589 } else {
3590 Err(err)
3591 }
3592 }
3593 Err(err) => Err(err),
3594 }
3595}
3596
3597pub(crate) fn read_object_at(idx: &PackIndex, offset: u64) -> Result<Object> {
3602 read_object_at_depth(idx, offset, 0)
3603}
3604
3605pub fn read_object_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<Object> {
3609 read_object_at(idx, offset)
3610}
3611
3612fn read_object_from_pack_at_depth(idx: &PackIndex, oid: &ObjectId, depth: usize) -> Result<Object> {
3615 let Some(offset) = idx.find_offset(oid)? else {
3616 return Err(Error::ObjectNotFound(oid.to_hex()));
3617 };
3618 read_object_at_depth(idx, offset, depth)
3619}
3620
3621fn read_object_at_depth(idx: &PackIndex, offset: u64, depth: usize) -> Result<Object> {
3622 let mut data = Vec::new();
3623 let kind = read_object_at_depth_into(idx, offset, depth, true, &mut data)?;
3624 Ok(Object::new(kind, data))
3625}
3626
3627fn read_object_at_depth_into(
3628 idx: &PackIndex,
3629 offset: u64,
3630 depth: usize,
3631 populate_delta_lru: bool,
3632 out: &mut Vec<u8>,
3633) -> Result<ObjectKind> {
3634 if depth == 0 {
3635 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
3636 if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
3637 out.clear();
3638 out.extend_from_slice(data.as_ref());
3639 return Ok(kind);
3640 }
3641 }
3642 let pack_path = &idx.pack_path;
3643 let objects_dir = idx.pack_path.parent().and_then(Path::parent);
3644 for attempt in 0..2 {
3645 let pack_bytes = read_pack_bytes_cached(pack_path)?;
3646 if !pack_cache::pack_object_count_validated(pack_path) {
3647 match validate_pack_index_object_count(&pack_bytes, idx) {
3648 Ok(()) => pack_cache::mark_pack_object_count_validated(pack_path),
3649 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3650 if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
3651 continue;
3652 }
3653 return Err(e);
3654 }
3655 Err(e) => return Err(e),
3656 }
3657 }
3658 if depth == 0 {
3659 if let Some(kind) = read_whole_pack_entry_object_into(idx, offset, &pack_bytes, out) {
3660 return Ok(kind);
3661 }
3662 }
3663 let resolved = with_pack_read_delta_state(depth, |state| {
3664 resolve_pack_object_at_into(
3665 PackIndexHandle::Borrowed(idx),
3666 offset,
3667 objects_dir,
3668 state,
3669 populate_delta_lru,
3670 out,
3671 )
3672 });
3673 match resolved {
3674 Ok(kind) => return Ok(kind),
3675 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3676 if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
3677 continue;
3678 }
3679 return Err(e);
3680 }
3681 Err(e) => return Err(e),
3682 }
3683 }
3684 unreachable!("at most two read attempts")
3685}
3686
3687pub fn read_object_from_pack_bytes(
3689 pack_bytes: &[u8],
3690 idx: &PackIndex,
3691 oid: &[u8],
3692) -> Result<Object> {
3693 validate_pack_index_object_count(pack_bytes, idx)?;
3694 let id = ObjectId::from_bytes(oid).map_err(|_| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
3695 let entry_offset = idx
3696 .find_offset(&id)?
3697 .ok_or_else(|| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
3698 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3699 crate::pack_store::PackStore::with_context(store, || {
3700 let (kind, data) = read_pack_object_at(pack_bytes, entry_offset, idx, None, 0)?;
3701 verify_packed_object_hash(kind, &data, oid)?;
3702 Ok(Object::new(kind, data))
3703 })
3704}
3705
3706fn validate_pack_index_object_count(pack_bytes: &[u8], idx: &PackIndex) -> Result<()> {
3707 if pack_bytes.len() < 12 || &pack_bytes[0..4] != b"PACK" {
3708 return Err(Error::CorruptObject("bad pack header".to_owned()));
3709 }
3710 let count =
3711 u32::from_be_bytes([pack_bytes[8], pack_bytes[9], pack_bytes[10], pack_bytes[11]]) as usize;
3712 if count != idx.len() {
3713 return Err(Error::CorruptObject(format!(
3714 "pack object count mismatch: pack has {count}, index has {}",
3715 idx.len()
3716 )));
3717 }
3718 Ok(())
3719}
3720
3721fn verify_packed_object_hash(kind: ObjectKind, data: &[u8], expected_oid: &[u8]) -> Result<()> {
3722 let algo = HashAlgo::from_len(expected_oid.len()).ok_or_else(|| {
3723 Error::CorruptObject(format!(
3724 "unsupported packed object id width: {}",
3725 expected_oid.len()
3726 ))
3727 })?;
3728 let actual = hash_object(algo, kind, data)?;
3729 if actual.as_bytes() != expected_oid {
3730 return Err(Error::CorruptObject(format!(
3731 "packed object {} hashes to {}",
3732 oid_bytes_to_hex(expected_oid),
3733 actual.to_hex()
3734 )));
3735 }
3736 Ok(())
3737}
3738
3739#[derive(Clone, Copy, Debug, Default)]
3741pub struct PackLookupOptions {
3742 pub skip_midx_covered_packs: bool,
3744 pub only_midx_covered_packs: bool,
3746}
3747
3748impl PackLookupOptions {
3749 pub const ALL_LOCAL_PACKS: Self = Self {
3751 skip_midx_covered_packs: false,
3752 only_midx_covered_packs: false,
3753 };
3754
3755 pub const MIDX_COVERED_PACKS: Self = Self {
3757 skip_midx_covered_packs: false,
3758 only_midx_covered_packs: true,
3759 };
3760
3761 fn allows_pack_shortcuts(self) -> bool {
3766 !self.skip_midx_covered_packs && !self.only_midx_covered_packs
3767 }
3768}
3769
3770fn select_single_local_pack_index(
3772 indexes: &[Arc<PackIndex>],
3773 commit_hint: Option<&crate::objects::ObjectId>,
3774) -> Result<Option<Arc<PackIndex>>> {
3775 if indexes.is_empty() {
3776 return Ok(None);
3777 }
3778 if indexes.len() == 1 {
3779 return Ok(Some(Arc::clone(&indexes[0])));
3780 }
3781 if let Some(oid) = commit_hint {
3782 let mut found: Option<Arc<PackIndex>> = None;
3783 for idx in indexes {
3784 if idx.find_offset(oid)?.is_some() {
3785 if found.is_some() {
3786 return Ok(None);
3787 }
3788 found = Some(Arc::clone(idx));
3789 }
3790 }
3791 if found.is_some() {
3792 return Ok(found);
3793 }
3794 }
3795 Ok(None)
3796}
3797
3798pub struct SingleLocalPackReader {
3802 idx: Arc<PackIndex>,
3803 pack_bytes: Arc<PackData>,
3804 pack_id: u32,
3805 cursor: usize,
3806 info_cache: HashMap<u64, ObjectInfo>,
3807 delta_scratch: Option<ZlibInflateScratch>,
3810 commit_header_buf: Vec<u8>,
3811}
3812
3813fn objects_dir_has_loose_objects(objects_dir: &Path) -> bool {
3814 let Ok(entries) = std::fs::read_dir(objects_dir) else {
3815 return false;
3816 };
3817 for entry in entries.flatten() {
3818 let name = entry.file_name();
3819 let name = name.to_string_lossy();
3820 if name.len() != 2 || !name.bytes().all(|b| b.is_ascii_hexdigit()) {
3821 continue;
3822 }
3823 let Ok(hex_entries) = std::fs::read_dir(entry.path()) else {
3824 continue;
3825 };
3826 if hex_entries.flatten().any(|e| e.path().is_file()) {
3827 return true;
3828 }
3829 }
3830 false
3831}
3832
3833impl SingleLocalPackReader {
3834 pub fn open(objects_dir: &Path) -> Result<Option<Self>> {
3840 Self::open_with_options(objects_dir, false, None)
3841 }
3842
3843 pub fn open_allow_loose(objects_dir: &Path) -> Result<Option<Self>> {
3845 Self::open_with_options(objects_dir, true, None)
3846 }
3847
3848 pub(crate) fn open_allow_loose_for_commits(
3851 objects_dir: &Path,
3852 commit_hint: &crate::objects::ObjectId,
3853 ) -> Result<Option<Self>> {
3854 Self::open_with_options(objects_dir, true, Some(commit_hint))
3855 }
3856
3857 pub(crate) fn open_for_pack_index(
3859 objects_dir: &Path,
3860 idx: Arc<PackIndex>,
3861 ) -> Result<Option<Self>> {
3862 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
3863 return Ok(None);
3864 }
3865 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
3866 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
3867 validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
3868 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
3869 pack_cache::remember_pack_hit(objects_dir, &idx);
3870 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
3871 Ok(Some(Self {
3872 idx,
3873 pack_bytes,
3874 pack_id,
3875 cursor: 0,
3876 info_cache: HashMap::new(),
3877 delta_scratch: None,
3878 commit_header_buf: Vec::new(),
3879 }))
3880 }
3881
3882 pub(crate) fn object_kinds_at_sorted_offsets(
3893 &self,
3894 offsets: &[u64],
3895 ) -> Result<Vec<Option<ObjectKind>>> {
3896 let pack_bytes = self.pack_bytes.as_ref();
3897 let hash_bytes = self.idx.hash_bytes();
3898 let mut kinds: Vec<Option<ObjectKind>> = Vec::with_capacity(offsets.len());
3899 for (rank, &offset) in offsets.iter().enumerate() {
3900 let mut pos = usize::try_from(offset)
3901 .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
3902 let (packed_type, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
3903 let base_offset = match packed_type {
3904 PackedType::OfsDelta => Some(parse_ofs_delta_base(pack_bytes, &mut pos, offset)?),
3905 PackedType::RefDelta => {
3906 let raw = pack_bytes.get(pos..pos + hash_bytes).ok_or_else(|| {
3907 Error::CorruptObject("truncated ref-delta base OID".to_owned())
3908 })?;
3909 self.idx.find_offset(&ObjectId::from_bytes(raw)?)?
3910 }
3911 _ => {
3912 kinds.push(Some(packed_type_to_kind(packed_type)?));
3913 continue;
3914 }
3915 };
3916 let base_kind = base_offset
3917 .and_then(|base| offsets.binary_search(&base).ok())
3918 .filter(|&base_rank| base_rank < rank)
3919 .and_then(|base_rank| kinds[base_rank]);
3920 kinds.push(base_kind);
3921 }
3922 Ok(kinds)
3923 }
3924
3925 pub(crate) fn contains_oid(&self, oid: &crate::objects::ObjectId) -> Result<bool> {
3927 Ok(self.idx.find_offset(oid)?.is_some())
3928 }
3929
3930 fn open_with_options(
3931 objects_dir: &Path,
3932 allow_loose: bool,
3933 commit_hint: Option<&crate::objects::ObjectId>,
3934 ) -> Result<Option<Self>> {
3935 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
3936 return Ok(None);
3937 }
3938 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
3939 let memo_eligible = allow_loose && commit_hint.is_none();
3940 if memo_eligible {
3941 if let Some((idx, pack_bytes, pack_id)) = pack_cache::single_reader_for(objects_dir) {
3942 return Ok(Some(Self {
3943 idx,
3944 pack_bytes,
3945 pack_id,
3946 cursor: 0,
3947 info_cache: HashMap::new(),
3948 delta_scratch: None,
3949 commit_header_buf: Vec::new(),
3950 }));
3951 }
3952 }
3953 let indexes = pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)?;
3954 let idx = match select_single_local_pack_index(&indexes, commit_hint)? {
3955 Some(idx) => idx,
3956 None => return Ok(None),
3957 };
3958 if !allow_loose && objects_dir_has_loose_objects(objects_dir) {
3959 return Ok(None);
3960 }
3961 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
3962 validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
3963 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
3964 pack_cache::remember_pack_hit(objects_dir, &idx);
3965 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
3966 if memo_eligible && indexes.len() == 1 {
3967 pack_cache::remember_single_reader(objects_dir, &idx, &pack_bytes, pack_id);
3968 }
3969 Ok(Some(Self {
3970 idx,
3971 pack_bytes,
3972 pack_id,
3973 cursor: 0,
3974 info_cache: HashMap::new(),
3975 delta_scratch: None,
3976 commit_header_buf: Vec::new(),
3977 }))
3978 }
3979
3980 pub fn read_commit_author_prefix(&mut self, oid: &ObjectId) -> Result<Option<&[u8]>> {
3988 self.read_commit_author_prefix_at(oid, true)
3989 }
3990
3991 pub(crate) fn read_commit_author_prefix_unordered(
3993 &mut self,
3994 oid: &ObjectId,
3995 ) -> Result<Option<&[u8]>> {
3996 self.read_commit_author_prefix_at(oid, false)
3997 }
3998
3999 pub(crate) fn read_commit_author_prefix_at_pack_row(
4001 &mut self,
4002 row: usize,
4003 ) -> Result<Option<&[u8]>> {
4004 self.cursor = row;
4005 let offset = self.idx.offset_at(row)?;
4006 self.read_commit_author_prefix_at_pack_offset(offset)
4007 }
4008
4009 fn read_commit_author_prefix_at(
4010 &mut self,
4011 oid: &ObjectId,
4012 monotonic: bool,
4013 ) -> Result<Option<&[u8]>> {
4014 let offset = if monotonic {
4015 self.idx.find_offset_monotonic(oid, &mut self.cursor)?
4016 } else {
4017 self.idx.find_offset(oid)?
4018 };
4019 let Some(offset) = offset else {
4020 return Ok(None);
4021 };
4022 self.read_commit_author_prefix_at_pack_offset(offset)
4023 }
4024
4025 fn read_commit_author_prefix_at_pack_offset(&mut self, offset: u64) -> Result<Option<&[u8]>> {
4026 let pack_bytes = self.pack_bytes.as_ref();
4027 let mut pos = offset as usize;
4028 if pos >= pack_bytes.len() {
4029 return Ok(None);
4030 }
4031 let (packed_type, _) = match parse_pack_object_header(pack_bytes, &mut pos) {
4032 Ok(v) => v,
4033 Err(_) => return Ok(None),
4034 };
4035 if !matches!(packed_type, PackedType::Commit) {
4036 return Ok(None);
4037 }
4038 self.commit_header_buf.clear();
4039 self.delta_scratch
4040 .get_or_insert_with(ZlibInflateScratch::default)
4041 .decompress_prefix_into(
4042 &pack_bytes[pos..],
4043 COMMIT_HEADER_READ_PREFIX,
4044 &mut self.commit_header_buf,
4045 )?;
4046 if self.commit_header_buf.is_empty() {
4047 return Ok(None);
4048 }
4049 Ok(Some(&self.commit_header_buf))
4050 }
4051
4052 pub(crate) fn clear_commit_header_prefix(&mut self) {
4054 self.commit_header_buf.clear();
4055 }
4056
4057 fn read_info_at_pack_offset(&mut self, start_offset: u64) -> Result<ObjectInfo> {
4058 if let Some(info) = self.info_cache.get(&start_offset) {
4059 return Ok(*info);
4060 }
4061 let pack_path = self.idx.pack_path.clone();
4062 let info = with_pack_read_delta_state(0, |state| {
4063 self.resolve_info_at_pack_offset_body(start_offset, state)
4064 })?;
4065 self.info_cache.insert(start_offset, info);
4066 remember_pack_read_info(&pack_path, start_offset, info);
4067 Ok(info)
4068 }
4069
4070 fn resolve_info_at_pack_offset_body(
4071 &mut self,
4072 start_offset: u64,
4073 state: &mut DeltaChainState,
4074 ) -> Result<ObjectInfo> {
4075 let pack_bytes = self.pack_bytes.as_ref();
4076 let mut cur_offset = start_offset;
4077 let mut result_size: Option<u64> = None;
4078 loop {
4079 state.visit(self.pack_id, cur_offset)?;
4080 if let Some(base) = self.info_cache.get(&cur_offset) {
4081 if let Some(size) = result_size {
4082 return Ok(ObjectInfo {
4083 kind: base.kind,
4084 size,
4085 });
4086 }
4087 return Ok(*base);
4088 }
4089
4090 let object_start = cur_offset;
4091 let mut pos = cur_offset as usize;
4092 let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos)?;
4093 match packed_type {
4094 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4095 let kind = packed_type_to_kind(packed_type)?;
4096 let undeltified = ObjectInfo {
4097 kind,
4098 size: header_size,
4099 };
4100 self.info_cache.insert(cur_offset, undeltified);
4101 let size = result_size.unwrap_or(header_size);
4102 return Ok(ObjectInfo { kind, size });
4103 }
4104 PackedType::OfsDelta => {
4105 state.next_hop()?;
4106 let base_offset = parse_ofs_delta_base(pack_bytes, &mut pos, object_start)?;
4107 if result_size.is_none() {
4108 result_size = Some(
4109 self.delta_scratch
4110 .get_or_insert_with(ZlibInflateScratch::default)
4111 .read_pack_delta_result_size(pack_bytes, &mut pos)?,
4112 );
4113 }
4114 cur_offset = base_offset;
4115 }
4116 PackedType::RefDelta => {
4117 state.next_hop()?;
4118 let hb = self.idx.hash_bytes();
4119 if pos + hb > pack_bytes.len() {
4120 return Err(Error::CorruptObject(
4121 "truncated ref-delta base OID".to_owned(),
4122 ));
4123 }
4124 let base_raw = &pack_bytes[pos..pos + hb];
4125 pos += hb;
4126 if result_size.is_none() {
4127 result_size = Some(
4128 self.delta_scratch
4129 .get_or_insert_with(ZlibInflateScratch::default)
4130 .read_pack_delta_result_size(pack_bytes, &mut pos)?,
4131 );
4132 }
4133 let base_oid = ObjectId::from_bytes(base_raw)?;
4134 let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
4135 return Err(Error::ObjectNotFound(base_oid.to_hex()));
4136 };
4137 cur_offset = base_offset;
4138 }
4139 }
4140 }
4141 }
4142
4143 pub fn read_info(&mut self, oid: &ObjectId) -> Result<ObjectInfo> {
4149 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4150 return Err(Error::ObjectNotFound(oid.to_hex()));
4151 };
4152 if let Some(info) =
4153 read_whole_pack_entry_info_fast(&self.idx, offset, self.pack_bytes.as_ref())
4154 {
4155 self.info_cache.insert(offset, info);
4156 return Ok(info);
4157 }
4158 self.read_info_at_pack_offset(offset)
4159 }
4160
4161 pub fn read_object(&mut self, oid: &ObjectId) -> Result<Object> {
4167 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4168 return Err(Error::ObjectNotFound(oid.to_hex()));
4169 };
4170 if let Some(obj) =
4171 read_whole_pack_entry_object_fast(&self.idx, offset, self.pack_bytes.as_ref())
4172 {
4173 return Ok(obj);
4174 }
4175 read_object_from_pack_at_offset(&self.idx, offset)
4176 }
4177
4178 pub(crate) fn read_object_unless_whole_larger(
4187 &mut self,
4188 oid: &ObjectId,
4189 limit: u64,
4190 ) -> Result<BoundedRead> {
4191 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4192 return Err(Error::ObjectNotFound(oid.to_hex()));
4193 };
4194 let mut pos = usize::try_from(offset)
4195 .map_err(|_| Error::CorruptObject(format!("pack offset {offset} overflows")))?;
4196 if let Ok((packed_type, size)) = parse_pack_object_header(&self.pack_bytes, &mut pos) {
4197 let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
4198 if whole && size > limit {
4199 return Ok(BoundedRead::Larger);
4200 }
4201 }
4202 if let Some(obj) =
4203 read_whole_pack_entry_object_fast(&self.idx, offset, self.pack_bytes.as_ref())
4204 {
4205 return Ok(BoundedRead::Object(obj));
4206 }
4207 read_object_from_pack_at_offset(&self.idx, offset).map(BoundedRead::Object)
4208 }
4209
4210 pub fn read_object_into(&mut self, oid: &ObjectId, out: &mut Vec<u8>) -> Result<ObjectKind> {
4214 self.read_object_into_with_delta_policy(oid, false, out)
4215 }
4216
4217 pub(crate) fn read_object_into_with_delta_policy(
4220 &mut self,
4221 oid: &ObjectId,
4222 populate_delta_lru: bool,
4223 out: &mut Vec<u8>,
4224 ) -> Result<ObjectKind> {
4225 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4226 return Err(Error::ObjectNotFound(oid.to_hex()));
4227 };
4228 if let Some(kind) =
4229 read_whole_pack_entry_object_into(&self.idx, offset, self.pack_bytes.as_ref(), out)
4230 {
4231 return Ok(kind);
4232 }
4233 read_object_at_depth_into(&self.idx, offset, 0, populate_delta_lru, out)
4234 }
4235
4236 pub fn pack_offset(&self, oid: &ObjectId) -> Result<Option<u64>> {
4242 self.idx.find_offset(oid)
4243 }
4244}
4245fn pack_indexes_for_lookup(
4246 objects_dir: &Path,
4247 opts: PackLookupOptions,
4248) -> Result<Vec<Arc<PackIndex>>> {
4249 let indexes = read_local_pack_indexes_cached(objects_dir)?;
4250 if !opts.skip_midx_covered_packs && !opts.only_midx_covered_packs {
4251 return Ok(indexes);
4252 }
4253 let covered = pack_cache::midx_covered_pack_names(objects_dir);
4254 if covered.is_empty() {
4255 return if opts.only_midx_covered_packs {
4256 Ok(Vec::new())
4257 } else {
4258 Ok(indexes)
4259 };
4260 }
4261 Ok(indexes
4262 .into_iter()
4263 .filter(|idx| {
4264 idx.idx_path
4265 .file_name()
4266 .and_then(|n| n.to_str())
4267 .map(|name| {
4268 if opts.only_midx_covered_packs {
4269 covered.contains(name)
4270 } else {
4271 !covered.contains(name)
4272 }
4273 })
4274 .unwrap_or(!opts.only_midx_covered_packs)
4275 })
4276 .collect())
4277}
4278
4279pub fn read_object_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
4291 if crate::pack_store::PackStore::has_context() {
4292 return read_object_from_packs_in_context(objects_dir, oid);
4293 }
4294 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
4295 crate::pack_store::PackStore::with_context(store, || {
4296 read_object_from_packs_in_context(objects_dir, oid)
4297 })
4298}
4299
4300pub fn read_object_from_packs_into(
4306 objects_dir: &Path,
4307 oid: &ObjectId,
4308 buf: &mut Vec<u8>,
4309) -> Result<ObjectKind> {
4310 if crate::pack_store::PackStore::has_context() {
4311 return read_object_from_packs_in_context_into(objects_dir, oid, buf);
4312 }
4313 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
4314 crate::pack_store::PackStore::with_context(store, || {
4315 read_object_from_packs_in_context_into(objects_dir, oid, buf)
4316 })
4317}
4318
4319pub fn read_object_info_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<ObjectInfo> {
4325 match try_read_object_info_from_packs_with_options(
4326 objects_dir,
4327 oid,
4328 PackLookupOptions::ALL_LOCAL_PACKS,
4329 ) {
4330 Ok(info) => Ok(info),
4331 Err(err @ Error::ObjectNotFound(_)) => {
4332 if reprepare_pack_directory_on_miss(objects_dir)? {
4333 try_read_object_info_from_packs_with_options(
4334 objects_dir,
4335 oid,
4336 PackLookupOptions::ALL_LOCAL_PACKS,
4337 )
4338 } else {
4339 Err(err)
4340 }
4341 }
4342 Err(err) => Err(err),
4343 }
4344}
4345
4346pub(crate) fn try_read_object_info_from_packs_with_options(
4348 objects_dir: &Path,
4349 oid: &ObjectId,
4350 opts: PackLookupOptions,
4351) -> Result<ObjectInfo> {
4352 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4353 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4354 }
4355 if opts.allows_pack_shortcuts() {
4356 if let Some(info) = pack_cache::try_read_info_from_hinted_pack(objects_dir, oid)? {
4357 return Ok(info);
4358 }
4359 }
4360 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
4361 let mut last_err: Option<Error> = None;
4362 for idx in &indexes {
4363 let Some(offset) = idx.find_offset(oid)? else {
4364 continue;
4365 };
4366 match read_object_info_from_pack_at_offset(idx, offset) {
4367 Ok(info) => {
4368 pack_cache::remember_pack_hit(objects_dir, idx);
4369 return Ok(info);
4370 }
4371 Err(Error::ObjectNotFound(_)) => {}
4372 Err(err) => last_err = Some(err),
4373 }
4374 }
4375 Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
4376}
4377
4378pub(crate) fn try_read_object_from_packs_with_options(
4380 objects_dir: &Path,
4381 oid: &ObjectId,
4382 opts: PackLookupOptions,
4383) -> Result<Object> {
4384 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4385 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4386 }
4387 if opts.allows_pack_shortcuts() {
4388 if let Some(obj) = pack_cache::try_read_object_via_single_pack_reader(objects_dir, oid)? {
4389 return Ok(obj);
4390 }
4391 if let Some(obj) = pack_cache::try_read_from_hinted_pack(objects_dir, oid)? {
4392 return Ok(obj);
4393 }
4394 }
4395 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
4396 let mut last_err: Option<Error> = None;
4397 for idx in &indexes {
4398 let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
4399 else {
4400 continue;
4401 };
4402 match read_object_from_pack_at_offset(idx, offset) {
4403 Ok(obj) => {
4404 pack_cache::remember_pack_hit(objects_dir, idx);
4405 return Ok(obj);
4406 }
4407 Err(Error::ObjectNotFound(_)) => {}
4410 Err(err) => last_err = Some(err),
4413 }
4414 }
4415 Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
4416}
4417
4418pub(crate) fn try_read_object_from_packs_with_options_into(
4423 objects_dir: &Path,
4424 oid: &ObjectId,
4425 opts: PackLookupOptions,
4426 populate_delta_lru: bool,
4427 buf: &mut Vec<u8>,
4428) -> Result<ObjectKind> {
4429 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4430 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4431 }
4432 if opts.allows_pack_shortcuts() {
4433 if let Some(kind) = pack_cache::try_read_object_via_single_pack_reader_into(
4434 objects_dir,
4435 oid,
4436 populate_delta_lru,
4437 buf,
4438 )? {
4439 return Ok(kind);
4440 }
4441 if let Some(kind) =
4442 pack_cache::try_read_from_hinted_pack_into(objects_dir, oid, populate_delta_lru, buf)?
4443 {
4444 return Ok(kind);
4445 }
4446 }
4447 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
4448 let mut last_err: Option<Error> = None;
4449 for idx in &indexes {
4450 let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
4451 else {
4452 continue;
4453 };
4454 match read_object_at_depth_into(idx, offset, 0, populate_delta_lru, buf) {
4455 Ok(kind) => {
4456 pack_cache::remember_pack_hit(objects_dir, idx);
4457 return Ok(kind);
4458 }
4459 Err(Error::ObjectNotFound(_)) => {}
4460 Err(err) => last_err = Some(err),
4461 }
4462 }
4463 Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
4464}
4465
4466#[must_use]
4468pub fn pack_index_is_v1(idx_path: &Path) -> bool {
4469 fs::read(idx_path)
4470 .ok()
4471 .is_some_and(|bytes| bytes.len() >= 4 && bytes[0..4] != [0xff, b't', b'O', b'c'])
4472}
4473
4474pub fn try_read_object_from_local_packs_cached(
4486 objects_dir: &Path,
4487 oid: &ObjectId,
4488 mut alternate_read: impl FnMut() -> Result<Object>,
4489) -> Result<Option<Object>> {
4490 let indexes = read_local_pack_indexes_cached(objects_dir)?;
4491 for idx in &indexes {
4492 if idx.find_offset(oid)?.is_none() {
4493 continue;
4494 }
4495 match read_object_from_pack(idx, oid) {
4496 Ok(obj) => return Ok(Some(obj)),
4497 Err(_) if pack_index_is_v1(&idx.idx_path) => {
4498 return Ok(Some(Object::new(ObjectKind::Blob, Vec::new())));
4499 }
4500 Err(_) => {
4501 if let Ok(obj) = alternate_read() {
4502 return Ok(Some(obj));
4503 }
4504 }
4505 }
4506 }
4507 Ok(None)
4508}
4509
4510fn parse_in_pack_delta_reuse_candidate(
4514 idx: &PackIndex,
4515 pack_bytes: &[u8],
4516 entry_offset: u64,
4517 in_pack: &HashSet<ObjectId>,
4518) -> Result<Option<(ObjectId, Vec<u8>)>> {
4519 let hb = idx.hash_bytes();
4520 let mut p = entry_offset as usize;
4521 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
4522 let base = match packed_type {
4523 PackedType::RefDelta => {
4524 if p + hb > pack_bytes.len() {
4525 return Err(Error::CorruptObject(
4526 "truncated ref-delta base oid while scanning for reuse".to_owned(),
4527 ));
4528 }
4529 let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
4530 p += hb;
4531 bo
4532 }
4533 PackedType::OfsDelta => {
4534 let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
4535 let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
4536 return Ok(None);
4537 };
4538 let base_oid_bytes = idx.oid_at(pos);
4539 if base_oid_bytes.len() != hb {
4540 return Ok(None);
4541 }
4542 ObjectId::from_bytes(base_oid_bytes)?
4543 }
4544 _ => return Ok(None),
4545 };
4546 if !in_pack.contains(&base) {
4547 return Ok(None);
4548 }
4549 let zlib_start = p;
4550 let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
4551 return Ok(None);
4552 };
4553 if zlib_start >= end_pos {
4554 return Ok(None);
4555 }
4556 let compressed = &pack_bytes[zlib_start..end_pos];
4557 let mut zpos = 0usize;
4558 let mut scratch = ZlibInflateScratch::default();
4559 let delta = match scratch.decompress_fixed(compressed, &mut zpos, size) {
4560 Ok(d) => d,
4561 Err(_) => return Ok(None),
4562 };
4563 Ok(Some((base, delta)))
4564}
4565
4566fn parse_in_pack_delta_reuse_zlib(
4569 idx: &PackIndex,
4570 pack_bytes: &[u8],
4571 entry_offset: u64,
4572 in_pack: &HashSet<ObjectId>,
4573) -> Result<Option<(ObjectId, u64, usize, usize)>> {
4574 let start = entry_offset as usize;
4575 let hb = idx.hash_bytes();
4576 let mut p = start;
4577 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
4578 let base = match packed_type {
4579 PackedType::RefDelta => {
4580 if p + hb > pack_bytes.len() {
4581 return Err(Error::CorruptObject(
4582 "truncated ref-delta base oid while scanning for reuse".to_owned(),
4583 ));
4584 }
4585 let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
4586 p += hb;
4587 bo
4588 }
4589 PackedType::OfsDelta => {
4590 let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
4591 let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
4592 return Ok(None);
4593 };
4594 let base_oid_bytes = idx.oid_at(pos);
4595 if base_oid_bytes.len() != hb {
4596 return Ok(None);
4597 }
4598 ObjectId::from_bytes(base_oid_bytes)?
4599 }
4600 _ => return Ok(None),
4601 };
4602 if !in_pack.contains(&base) {
4603 return Ok(None);
4604 }
4605 let zlib_start = p;
4606 let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
4607 return Ok(None);
4608 };
4609 if zlib_start >= end_pos {
4610 return Ok(None);
4611 }
4612 let entry_bytes = &pack_bytes[start..end_pos];
4613 if let Some(crc) = idx.crc32_for_pack_offset(entry_offset)? {
4614 if crc32fast::hash(entry_bytes) != crc {
4615 return Ok(None);
4616 }
4617 }
4618 Ok(Some((base, size, zlib_start, end_pos)))
4619}
4620
4621fn reuse_delta_matches_pack_resolution(
4622 idx: &PackIndex,
4623 target: &ObjectId,
4624 base: ObjectId,
4625 delta: &[u8],
4626) -> bool {
4627 let Ok(base_obj) = read_object_from_pack(idx, &base) else {
4628 return false;
4629 };
4630 let Ok(target_obj) = read_object_from_pack(idx, target) else {
4631 return false;
4632 };
4633 crate::unpack_objects::apply_delta(&base_obj.data, delta)
4634 .ok()
4635 .is_some_and(|applied| applied == target_obj.data)
4636}
4637
4638fn on_disk_delta_chain_in_send_map(
4639 oid: ObjectId,
4640 immediate_base: &HashMap<ObjectId, ObjectId>,
4641 in_pack: &HashSet<ObjectId>,
4642 memo: &mut HashMap<ObjectId, bool>,
4643) -> bool {
4644 if let Some(&ok) = memo.get(&oid) {
4645 return ok;
4646 }
4647 let mut cur = oid;
4648 let mut seen = HashSet::new();
4649 loop {
4650 if !seen.insert(cur) {
4651 memo.insert(oid, true);
4652 return true;
4653 }
4654 if !in_pack.contains(&cur) {
4655 memo.insert(oid, false);
4656 return false;
4657 }
4658 let Some(&base) = immediate_base.get(&cur) else {
4659 memo.insert(oid, true);
4660 return true;
4661 };
4662 cur = base;
4663 }
4664}
4665
4666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4668pub(crate) struct PackedSlot {
4669 pub pack: u32,
4671 pub whole: bool,
4673}
4674
4675pub(crate) fn locate_packed_objects(
4685 objects_dir: &Path,
4686 wanted: &crate::objects::OidHashSet,
4687) -> Result<crate::objects::OidHashMap<PackedSlot>> {
4688 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
4689 sort_pack_indexes_oldest_first(&mut indexes);
4690 let mut slots = crate::objects::oid_hash_map_with_capacity(wanted.len());
4691 for (pack, idx) in (0u32..).zip(&indexes) {
4692 let bytes = read_pack_bytes_cached(&idx.pack_path)?;
4693 for entry in idx.iter() {
4694 let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
4695 continue;
4696 };
4697 if !wanted.contains(&oid) || slots.contains_key(&oid) {
4698 continue;
4699 }
4700 let mut pos = usize::try_from(entry.offset()?)
4701 .map_err(|_| Error::CorruptObject("pack offset exceeds memory".to_owned()))?;
4702 let (packed_type, _) = parse_pack_object_header(&bytes, &mut pos)?;
4703 let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
4704 slots.insert(oid, PackedSlot { pack, whole });
4705 }
4706 }
4707 Ok(slots)
4708}
4709
4710pub(crate) type OnDiskReuseZlibEntry = (ObjectId, u64, Arc<PackData>, usize, usize);
4719
4720pub fn collect_packed_delta_reuse_for_send(
4722 objects_dir: &Path,
4723 send: &HashSet<ObjectId>,
4724 in_pack: &HashSet<ObjectId>,
4725 resolved: &HashMap<ObjectId, Object>,
4726) -> Result<HashMap<ObjectId, OnDiskReuseZlibEntry>> {
4727 let _ = resolved;
4728 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
4729 sort_pack_indexes_oldest_first(&mut indexes);
4730
4731 let pack_bytes_by_index: Vec<(Arc<PackIndex>, Arc<PackData>)> = indexes
4732 .iter()
4733 .filter(|idx| idx.hash_bytes() == 20)
4734 .map(|idx| read_pack_bytes_cached(&idx.pack_path).map(|bytes| (Arc::clone(idx), bytes)))
4735 .collect::<Result<Vec<_>>>()?;
4736
4737 struct OnDiskDelta {
4738 base: ObjectId,
4739 size: u64,
4740 pack_bytes: Arc<PackData>,
4741 zlib_start: usize,
4742 zlib_end: usize,
4743 }
4744
4745 let mut on_disk: HashMap<ObjectId, OnDiskDelta> = HashMap::new();
4746 for (idx, pack_bytes) in &pack_bytes_by_index {
4747 for entry in idx.iter() {
4748 let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
4749 continue;
4750 };
4751 if !send.contains(&oid) || on_disk.contains_key(&oid) {
4752 continue;
4753 }
4754 let Some((base, size, zlib_start, zlib_end)) =
4755 parse_in_pack_delta_reuse_zlib(idx, pack_bytes, entry.offset()?, in_pack)?
4756 else {
4757 continue;
4758 };
4759 on_disk.insert(
4760 oid,
4761 OnDiskDelta {
4762 base,
4763 size,
4764 pack_bytes: Arc::clone(pack_bytes),
4765 zlib_start,
4766 zlib_end,
4767 },
4768 );
4769 }
4770 }
4771
4772 let immediate_base: HashMap<ObjectId, ObjectId> =
4773 on_disk.iter().map(|(&oid, d)| (oid, d.base)).collect();
4774
4775 let mut chain_ok: HashMap<ObjectId, bool> = HashMap::new();
4776 for &oid in on_disk.keys() {
4777 on_disk_delta_chain_in_send_map(oid, &immediate_base, in_pack, &mut chain_ok);
4778 }
4779
4780 let mut results = HashMap::new();
4781 for (oid, slot) in on_disk {
4782 if !chain_ok.get(&oid).copied().unwrap_or(false) {
4783 continue;
4784 }
4785 results.insert(
4786 oid,
4787 (
4788 slot.base,
4789 slot.size,
4790 slot.pack_bytes,
4791 slot.zlib_start,
4792 slot.zlib_end,
4793 ),
4794 );
4795 }
4796 Ok(results)
4797}
4798
4799pub fn packed_ref_delta_reuse_slice(
4808 objects_dir: &Path,
4809 oid: &ObjectId,
4810 packed_set: &HashSet<ObjectId>,
4811) -> Result<Option<(ObjectId, Vec<u8>)>> {
4812 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
4813 sort_pack_indexes_oldest_first(&mut indexes);
4814 for idx in indexes {
4815 if idx.hash_bytes() != 20 {
4816 continue;
4817 }
4818 let Some(entry_offset) = idx.find_offset(oid)? else {
4819 continue;
4820 };
4821 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
4822 let Some((base, delta)) =
4823 parse_in_pack_delta_reuse_candidate(&idx, &pack_bytes, entry_offset, packed_set)?
4824 else {
4825 continue;
4826 };
4827 if reuse_delta_matches_pack_resolution(&idx, oid, base, &delta) {
4828 return Ok(Some((base, delta)));
4829 }
4830 }
4831 Ok(None)
4832}
4833
4834pub(crate) fn sort_pack_indexes_oldest_first(indexes: &mut [Arc<PackIndex>]) {
4837 indexes.sort_by(|a, b| {
4838 let ta = fs::metadata(&a.pack_path)
4839 .and_then(|m| m.modified())
4840 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
4841 let tb = fs::metadata(&b.pack_path)
4842 .and_then(|m| m.modified())
4843 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
4844 ta.cmp(&tb).then_with(|| a.pack_path.cmp(&b.pack_path))
4845 });
4846}
4847
4848fn sort_pack_indexes_newest_first(indexes: &mut [Arc<PackIndex>]) {
4849 indexes.sort_by(|a, b| {
4850 let ta = fs::metadata(&a.pack_path)
4851 .and_then(|m| m.modified())
4852 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
4853 let tb = fs::metadata(&b.pack_path)
4854 .and_then(|m| m.modified())
4855 .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
4856 tb.cmp(&ta).then_with(|| b.pack_path.cmp(&a.pack_path))
4857 });
4858}
4859
4860pub fn packed_delta_base_oid(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectId>> {
4862 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
4863 sort_pack_indexes_newest_first(&mut indexes);
4864 for idx in &indexes {
4865 if idx.hash_bytes() != 20 {
4866 continue;
4867 }
4868 let Some(entry_offset) = idx.find_offset(oid)? else {
4869 continue;
4870 };
4871 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
4872 let mut p = entry_offset as usize;
4873 let (packed_type, _) = parse_pack_object_header(&pack_bytes, &mut p)?;
4874 match packed_type {
4875 PackedType::RefDelta => {
4876 let hb = idx.hash_bytes();
4877 if p + hb > pack_bytes.len() {
4878 return Err(Error::CorruptObject("truncated ref-delta base".to_owned()));
4879 }
4880 return Ok(Some(ObjectId::from_bytes(&pack_bytes[p..p + hb])?));
4881 }
4882 PackedType::OfsDelta => {
4883 let base_off = parse_ofs_delta_base(&pack_bytes, &mut p, entry_offset)?;
4884 return Ok(idx
4885 .find_position_by_offset_sorted(base_off)?
4886 .and_then(|pos| ObjectId::from_bytes(idx.oid_at(pos)).ok()));
4887 }
4888 _ => continue,
4889 }
4890 }
4891 Ok(None)
4892}
4893
4894pub(crate) fn pack_entry_raw_end(
4898 idx: &PackIndex,
4899 pack_bytes: &[u8],
4900 entry_offset: u64,
4901) -> Result<Option<usize>> {
4902 let hb = idx.hash_bytes();
4903 let Some(pack_end) = pack_bytes.len().checked_sub(hb) else {
4904 return Ok(None);
4905 };
4906 let start = entry_offset as usize;
4907 if start >= pack_end {
4908 return Ok(None);
4909 }
4910 Ok(Some(
4911 idx.next_pack_offset_after(entry_offset, pack_end as u64)? as usize,
4912 ))
4913}
4914
4915pub(crate) struct PackedWholeObject {
4917 pub(crate) pack: Arc<PackData>,
4919 pub(crate) zlib_offset: usize,
4921 pub(crate) data_end: usize,
4923 pub(crate) kind: ObjectKind,
4925 pub(crate) size: u64,
4927}
4928
4929pub(crate) enum PackedEntry {
4931 Whole(PackedWholeObject),
4933 NotWhole,
4936 Absent,
4938}
4939
4940pub(crate) fn locate_packed_whole_object(
4952 objects_dir: &Path,
4953 oid: &ObjectId,
4954 verify_crc: bool,
4955) -> Result<PackedEntry> {
4956 let indexes = read_local_pack_indexes_cached(objects_dir)?;
4957 let mut found = PackedEntry::Absent;
4958 for idx in &indexes {
4959 let Some(entry_offset) = idx.find_offset(oid)? else {
4960 continue;
4961 };
4962 found = PackedEntry::NotWhole;
4963 let pack = read_pack_bytes_cached(&idx.pack_path)?;
4964 let Some(data_end) = pack.len().checked_sub(idx.hash_bytes()) else {
4965 continue;
4966 };
4967 let Ok(mut pos) = usize::try_from(entry_offset) else {
4968 continue;
4969 };
4970 if pos >= data_end {
4971 continue;
4972 }
4973 let Ok((packed_type, size)) = parse_pack_object_header(&pack[..data_end], &mut pos) else {
4974 continue;
4975 };
4976 let Ok(kind) = packed_type_to_kind(packed_type) else {
4977 continue;
4978 };
4979 if verify_crc && !packed_entry_matches_index_crc(idx, &pack, entry_offset)? {
4980 continue;
4981 }
4982 return Ok(PackedEntry::Whole(PackedWholeObject {
4983 pack,
4984 zlib_offset: pos,
4985 data_end,
4986 kind,
4987 size,
4988 }));
4989 }
4990 Ok(found)
4991}
4992
4993fn packed_entry_matches_index_crc(idx: &PackIndex, pack: &[u8], entry_offset: u64) -> Result<bool> {
4998 let Some(recorded) = idx.crc32_for_pack_offset(entry_offset)? else {
4999 return Ok(true);
5000 };
5001 let Some(end) = pack_entry_raw_end(idx, pack, entry_offset)? else {
5002 return Ok(false);
5003 };
5004 let start = usize::try_from(entry_offset)
5005 .map_err(|_| Error::CorruptObject(format!("pack offset {entry_offset} overflows")))?;
5006 Ok(start < end && crc32fast::hash(&pack[start..end]) == recorded)
5007}
5008
5009pub fn packed_full_object_slice(objects_dir: &Path, oid: &ObjectId) -> Result<Option<Vec<u8>>> {
5028 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
5029 sort_pack_indexes_newest_first(&mut indexes);
5030 for idx in &indexes {
5031 let Some(entry_offset) = idx.find_offset(oid)? else {
5032 continue;
5033 };
5034 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5035 let start = entry_offset as usize;
5036 let mut p = start;
5037 let Ok((packed_type, _size)) = parse_pack_object_header(&pack_bytes, &mut p) else {
5038 continue;
5039 };
5040 if matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta) {
5041 continue;
5043 }
5044 let Some(end) = pack_entry_raw_end(idx, &pack_bytes, entry_offset)? else {
5045 continue;
5046 };
5047 if end <= start {
5048 continue;
5049 }
5050 let slice = &pack_bytes[start..end];
5051 let recorded_crc = idx.crc32_for_pack_offset(entry_offset)?;
5055 match recorded_crc {
5056 Some(crc) if crc32fast::hash(slice) != crc => continue,
5057 None if read_object_from_pack(idx, oid)
5059 .map(|obj| {
5060 HashAlgo::from_len(idx.hash_bytes())
5061 .map(|algo| {
5062 hash_object(algo, obj.kind, &obj.data)
5063 .map(|h| h != *oid)
5064 .unwrap_or(true)
5065 })
5066 .unwrap_or(true)
5067 })
5068 .unwrap_or(true) =>
5069 {
5070 continue;
5071 }
5072 _ => {}
5073 }
5074 return Ok(Some(slice.to_vec()));
5075 }
5076 Ok(None)
5077}
5078
5079pub(crate) fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
5080 let first = *bytes.get(*pos).ok_or_else(|| {
5081 Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
5082 })?;
5083 *pos += 1;
5084
5085 let type_code = (first >> 4) & 0x7;
5086 let mut size = (first & 0x0f) as u64;
5087 let mut shift = 4u32;
5088 let mut c = first;
5089 while (c & 0x80) != 0 {
5090 c = *bytes.get(*pos).ok_or_else(|| {
5091 Error::CorruptObject("unexpected end of variable size header".to_owned())
5092 })?;
5093 *pos += 1;
5094 size |= ((c & 0x7f) as u64) << shift;
5095 shift += 7;
5096 }
5097
5098 let packed_type = match type_code {
5099 1 => PackedType::Commit,
5100 2 => PackedType::Tree,
5101 3 => PackedType::Blob,
5102 4 => PackedType::Tag,
5103 6 => PackedType::OfsDelta,
5104 7 => PackedType::RefDelta,
5105 _ => {
5106 return Err(Error::CorruptObject(format!(
5107 "unsupported packed object type {}",
5108 type_code
5109 )))
5110 }
5111 };
5112 Ok((packed_type, size))
5113}
5114
5115#[derive(Debug, Clone, Copy)]
5117pub enum PackedDeltaDependency {
5118 OfsBase {
5120 base_offset: u64,
5122 },
5123 RefBase {
5125 base_oid: ObjectId,
5127 },
5128}
5129
5130pub fn read_packed_delta_dependency(
5134 pack_bytes: &[u8],
5135 object_offset: u64,
5136 hash_bytes: usize,
5137) -> Result<Option<PackedDeltaDependency>> {
5138 let mut pos = object_offset as usize;
5139 let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
5140 match ty {
5141 PackedType::OfsDelta => {
5142 let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
5143 Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
5144 }
5145 PackedType::RefDelta => {
5146 if pos + hash_bytes > pack_bytes.len() {
5147 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5148 }
5149 let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + hash_bytes])?;
5150 Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
5151 }
5152 _ => Ok(None),
5153 }
5154}
5155
5156pub(crate) fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
5157 let mut c = *bytes
5158 .get(*pos)
5159 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5160 *pos += 1;
5161 let mut value = (c & 0x7f) as u64;
5162 while (c & 0x80) != 0 {
5163 c = *bytes
5164 .get(*pos)
5165 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5166 *pos += 1;
5167 value = ((value + 1) << 7) | (c & 0x7f) as u64;
5168 }
5169 this_offset
5170 .checked_sub(value)
5171 .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
5172}
5173
5174pub fn slice_one_pack_object(
5182 bytes: &[u8],
5183 object_start_offset: u64,
5184 hash_bytes: usize,
5185) -> Result<&[u8]> {
5186 let start = object_start_offset as usize;
5187 let mut pos = start;
5188 skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
5189 Ok(&bytes[start..pos])
5190}
5191
5192pub fn skip_one_pack_object(
5194 bytes: &[u8],
5195 pos: &mut usize,
5196 object_start_offset: u64,
5197 hash_bytes: usize,
5198) -> Result<()> {
5199 let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
5200 let mut scratch = ZlibInflateScratch::default();
5201 match packed_type {
5202 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
5203 scratch.skip_zlib_stream(bytes, pos, size)?;
5204 }
5205 PackedType::RefDelta => {
5206 if *pos + hash_bytes > bytes.len() {
5207 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5208 }
5209 *pos += hash_bytes;
5210 scratch.skip_zlib_stream(bytes, pos, size)?;
5211 }
5212 PackedType::OfsDelta => {
5213 let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
5214 scratch.skip_zlib_stream(bytes, pos, size)?;
5215 }
5216 }
5217 Ok(())
5218}
5219
5220pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
5222 let index = read_pack_index(idx_path)?;
5223 let mut out = Vec::new();
5224 let hash_bytes = index.hash_bytes();
5225 for e in index.iter() {
5226 if e.oid().len() == hash_bytes {
5227 out.push(ObjectId::from_bytes(e.oid())?);
5228 }
5229 }
5230 Ok(out)
5231}
5232
5233mod pack_cache_test_sync {
5234 use std::cell::Cell;
5235 use std::sync::Mutex;
5236 use std::sync::MutexGuard;
5237
5238 struct PackCacheTestCoordinator {
5240 top_level_guards: u32,
5241 }
5242
5243 static COORD: Mutex<PackCacheTestCoordinator> = Mutex::new(PackCacheTestCoordinator {
5245 top_level_guards: 0,
5246 });
5247
5248 thread_local! {
5250 static DEPTH: Cell<u32> = const { Cell::new(0) };
5251 }
5252
5253 pub struct PackCacheTestGuard {
5255 _inner: Option<MutexGuard<'static, PackCacheTestCoordinator>>,
5256 }
5257
5258 pub fn acquire() -> PackCacheTestGuard {
5259 let depth = DEPTH.with(|d| {
5260 let n = d.get();
5261 d.set(n + 1);
5262 n
5263 });
5264 PackCacheTestGuard {
5265 _inner: if depth == 0 {
5266 let mut g = COORD.lock().unwrap_or_else(|e| e.into_inner());
5267 assert_eq!(
5268 g.top_level_guards, 0,
5269 "pack cache test coordinator already held"
5270 );
5271 g.top_level_guards = 1;
5272 Some(g)
5273 } else {
5274 None
5275 },
5276 }
5277 }
5278
5279 impl Drop for PackCacheTestGuard {
5280 fn drop(&mut self) {
5281 DEPTH.with(|d| {
5282 let n = d.get();
5283 assert!(n > 0, "pack cache test guard depth underflow");
5284 d.set(n - 1);
5285 if n == 1 {
5286 if let Some(mut g) = self._inner.take() {
5287 assert_eq!(g.top_level_guards, 1);
5288 g.top_level_guards = 0;
5289 }
5290 }
5291 });
5292 }
5293 }
5294}
5295
5296#[cfg(test)]
5297pub(crate) fn pack_cache_test_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5298 pack_cache_test_sync::acquire()
5299}
5300
5301pub fn test_pack_cache_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5303 pack_cache_test_sync::acquire()
5304}
5305
5306#[cfg(test)]
5307mod tests {
5308 use super::*;
5309 use crate::delta_encode::encode_lcp_delta;
5310 use crate::odb::Odb;
5311 use crate::pack_map::PackData;
5312 use crate::transfer::{build_pack, PackBuildOptions};
5313 use flate2::write::ZlibEncoder;
5314 use flate2::Compression;
5315 use std::io::Write;
5316 use std::process::Command;
5317
5318 fn git_try(dir: &std::path::Path, args: &[&str]) -> bool {
5319 Command::new("git") .current_dir(dir)
5321 .args(args)
5322 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5323 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5324 .output()
5325 .map(|o| o.status.success())
5326 .unwrap_or(false)
5327 }
5328
5329 fn git(dir: &std::path::Path, args: &[&str]) {
5330 let out = Command::new("git") .current_dir(dir)
5332 .args(args)
5333 .env("GIT_AUTHOR_NAME", "T")
5334 .env("GIT_AUTHOR_EMAIL", "t@example.com")
5335 .env("GIT_COMMITTER_NAME", "T")
5336 .env("GIT_COMMITTER_EMAIL", "t@example.com")
5337 .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
5338 .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
5339 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5340 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5341 .output()
5342 .expect("git");
5343 assert!(
5344 out.status.success(),
5345 "git {:?}: {}",
5346 args,
5347 String::from_utf8_lossy(&out.stderr)
5348 );
5349 }
5350
5351 fn append_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
5352 let mut size = payload_len;
5353 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
5354 size >>= 4;
5355 if size > 0 {
5356 buf.push(first | 0x80);
5357 while size > 0 {
5358 let b = (size & 0x7f) as u8;
5359 size >>= 7;
5360 buf.push(if size > 0 { b | 0x80 } else { b });
5361 }
5362 } else {
5363 buf.push(first);
5364 }
5365 }
5366
5367 fn zlib_pack(payload: &[u8]) -> Vec<u8> {
5368 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5369 enc.write_all(payload).unwrap();
5370 enc.finish().unwrap()
5371 }
5372
5373 fn append_sha1_pack_trailer(buf: &mut Vec<u8>) {
5374 buf.extend_from_slice(HashAlgo::Sha1.checksum_digest(&*buf).as_slice());
5375 }
5376
5377 fn append_ref_delta(buf: &mut Vec<u8>, base_oid: &ObjectId, delta: &[u8]) {
5378 let compressed = zlib_pack(delta);
5379 append_pack_object_header(buf, 7, delta.len());
5380 buf.extend_from_slice(base_oid.as_bytes());
5381 buf.extend_from_slice(&compressed);
5382 }
5383
5384 fn append_ofs_delta(buf: &mut Vec<u8>, base_offset: u64, delta: &[u8]) -> u64 {
5385 let object_start = buf.len() as u64;
5386 let compressed = zlib_pack(delta);
5387 append_pack_object_header(buf, 6, delta.len());
5388 let mut ofs = object_start - base_offset;
5389 while ofs >= 0x80 {
5390 buf.push((ofs as u8 & 0x7f) | 0x80);
5391 ofs >>= 7;
5392 }
5393 buf.push(ofs as u8);
5394 buf.extend_from_slice(&compressed);
5395 object_start
5396 }
5397
5398 fn append_whole_blob(buf: &mut Vec<u8>, data: &[u8]) -> u64 {
5399 let off = buf.len() as u64;
5400 let compressed = zlib_pack(data);
5401 append_pack_object_header(buf, 3, data.len());
5402 buf.extend_from_slice(&compressed);
5403 off
5404 }
5405
5406 fn append_corrupt_whole_blob(buf: &mut Vec<u8>) -> u64 {
5407 let off = buf.len() as u64;
5408 append_pack_object_header(buf, 3, 8);
5409 buf.extend_from_slice(&[0xff, 0xfe, 0xfd]);
5410 off
5411 }
5412
5413 fn write_v2_idx_for_test(
5414 idx_path: &Path,
5415 pack_path: &Path,
5416 entries: &[(ObjectId, u64)],
5417 ) -> Result<()> {
5418 let pack_bytes = std::fs::read(pack_path)?;
5419 let mut with_crc = Vec::with_capacity(entries.len());
5420 for (oid, off) in entries {
5421 let start = *off as usize;
5422 let mut end = start;
5423 skip_one_pack_object(&pack_bytes, &mut end, *off, 20)
5424 .map_err(|e| Error::CorruptObject(format!("idx crc walk: {e}")))?;
5425 with_crc.push((*oid, *off, crc32fast::hash(&pack_bytes[start..end])));
5426 }
5427 write_v2_pack_index(idx_path, pack_path, &with_crc, 20)
5428 }
5429
5430 fn single_blob_pack(data: &[u8]) -> (Vec<u8>, ObjectId, u64) {
5431 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
5432 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5433 let mut pack = Vec::new();
5434 pack.extend_from_slice(b"PACK");
5435 pack.extend_from_slice(&2u32.to_be_bytes());
5436 pack.extend_from_slice(&1u32.to_be_bytes());
5437 let off = append_whole_blob(&mut pack, data);
5438 append_sha1_pack_trailer(&mut pack);
5439 (pack, oid, off)
5440 }
5441
5442 #[test]
5443 fn single_local_pack_reader_batch_check_caches_deltified_entries() {
5444 let _guard = pack_cache_test_guard();
5445 clear_pack_cache();
5446 let Some((tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(2) else {
5447 return;
5448 };
5449 let objects = tmp.path().join("objects");
5450 let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
5451 crate::pack_store::PackStore::with_context(store, || {
5452 let mut reader = SingleLocalPackReader::open(&objects)
5453 .expect("open")
5454 .expect("single pack");
5455 let info1 = reader.read_info(&tip).expect("first read");
5456 let info2 = reader.read_info(&tip).expect("cached read");
5457 assert_eq!(info1, info2);
5458 let full = read_object_from_pack(&idx, &tip).expect("full read");
5459 assert_eq!(info1.kind, full.kind);
5460 assert_eq!(info1.size, full.data.len() as u64);
5461 });
5462 let _ = odb;
5463 }
5464
5465 #[test]
5466 fn pack_read_info_cache_is_scoped_per_repository_and_pack_bytes() {
5467 let _guard = pack_cache_test_guard();
5468 clear_pack_cache();
5469 const ENTRY_OFFSET: u64 = 200;
5470 let dir_a = tempfile::tempdir().expect("tempdir a");
5471 let dir_b = tempfile::tempdir().expect("tempdir b");
5472 let objects_a = dir_a.path().join("objects");
5473 let objects_b = dir_b.path().join("objects");
5474 fs::create_dir_all(objects_a.join("pack")).expect("pack dir a");
5475 fs::create_dir_all(objects_b.join("pack")).expect("pack dir b");
5476 let pack_a = objects_a.join("pack/p-a.pack");
5477 let pack_b = objects_b.join("pack/p-b.pack");
5478 let (oid_a, _) =
5479 write_sparse_single_blob_pack(dir_a.path(), &pack_a, b"five!", ENTRY_OFFSET)
5480 .expect("pack a");
5481 let (oid_b, _) =
5482 write_sparse_single_blob_pack(dir_b.path(), &pack_b, b"1234567890", ENTRY_OFFSET)
5483 .expect("pack b");
5484 let idx_a = pack_a.with_extension("idx");
5485 let idx_b = pack_b.with_extension("idx");
5486 write_v2_idx_for_test(&idx_a, &pack_a, &[(oid_a, ENTRY_OFFSET)]).expect("idx a");
5487 write_v2_idx_for_test(&idx_b, &pack_b, &[(oid_b, ENTRY_OFFSET)]).expect("idx b");
5488 let idx_a = read_pack_index(&idx_a).expect("parse idx a");
5489 let idx_b = read_pack_index(&idx_b).expect("parse idx b");
5490
5491 let read_size = |objects: &Path, idx: &PackIndex| -> u64 {
5492 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects);
5493 crate::pack_store::PackStore::with_context(store, || {
5494 super::read_object_info_from_pack_at_offset(idx, ENTRY_OFFSET)
5495 .expect("read_info")
5496 .size
5497 })
5498 };
5499
5500 let size_a = read_size(&objects_a, &idx_a);
5501 let size_b = read_size(&objects_b, &idx_b);
5502 assert_eq!(size_a, 5);
5503 assert_eq!(size_b, 10);
5504 assert_eq!(read_size(&objects_a, &idx_a), 5);
5506 assert_eq!(read_size(&objects_b, &idx_b), 10);
5507 assert_eq!(read_size(&objects_a, &idx_a), 5);
5508 }
5509 fn write_sparse_single_blob_pack(
5512 repo_root: &Path,
5513 pack_path: &Path,
5514 data: &[u8],
5515 object_offset: u64,
5516 ) -> Result<(ObjectId, u64)> {
5517 use std::fs::OpenOptions;
5518 use std::io::{Seek, SeekFrom, Write};
5519
5520 let odb = Odb::new(repo_root);
5521 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5522 let mut file = OpenOptions::new()
5523 .create(true)
5524 .write(true)
5525 .read(true)
5526 .truncate(true)
5527 .open(pack_path)
5528 .map_err(Error::Io)?;
5529 file.write_all(b"PACK").map_err(Error::Io)?;
5530 file.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
5531 file.write_all(&1u32.to_be_bytes()).map_err(Error::Io)?;
5532 file.seek(SeekFrom::Start(object_offset))
5533 .map_err(Error::Io)?;
5534 let mut tail = Vec::new();
5535 append_whole_blob(&mut tail, data);
5536 file.write_all(&tail).map_err(Error::Io)?;
5537 let trailer = sha1_trailer_for_open_file(&mut file)?;
5538 file.write_all(trailer.as_bytes()).map_err(Error::Io)?;
5539 file.flush().map_err(Error::Io)?;
5540 Ok((oid, object_offset))
5541 }
5542
5543 #[test]
5544 fn skip_one_pack_object_rejects_zero_size_with_zlib_payload() {
5545 let mut bytes = Vec::new();
5546 bytes.push(0x30);
5547 bytes.extend_from_slice(&zlib_pack(b"x"));
5548 let mut pos = 0usize;
5549 let err = skip_one_pack_object(&bytes, &mut pos, 0, 20).unwrap_err();
5550 assert!(matches!(err, Error::CorruptObject(_)), "got {err:?}");
5551 }
5552
5553 fn install_synthetic_pack(
5554 objects_dir: &Path,
5555 stem: &str,
5556 pack: &[u8],
5557 entries: &[(ObjectId, u64)],
5558 ) -> PackIndex {
5559 install_synthetic_pack_inner(objects_dir, stem, pack, entries, true)
5560 }
5561
5562 fn install_synthetic_pack_no_clear(
5563 objects_dir: &Path,
5564 stem: &str,
5565 pack: &[u8],
5566 entries: &[(ObjectId, u64)],
5567 ) -> PackIndex {
5568 install_synthetic_pack_inner(objects_dir, stem, pack, entries, false)
5569 }
5570
5571 fn install_synthetic_pack_inner(
5572 objects_dir: &Path,
5573 stem: &str,
5574 pack: &[u8],
5575 entries: &[(ObjectId, u64)],
5576 clear_cache: bool,
5577 ) -> PackIndex {
5578 let pack_dir = objects_dir.join("pack");
5579 std::fs::create_dir_all(&pack_dir).expect("pack dir");
5580 let pack_path = pack_dir.join(format!("{stem}.pack"));
5581 let idx_path = pack_dir.join(format!("{stem}.idx"));
5582 std::fs::write(&pack_path, pack).expect("write pack");
5583 write_v2_idx_for_test(&idx_path, &pack_path, entries).expect("write idx");
5584 if clear_cache {
5585 clear_pack_cache();
5586 }
5587 read_pack_index(&idx_path).expect("read idx")
5588 }
5589
5590 fn pack_index_for_objects(
5591 pack_path: std::path::PathBuf,
5592 objects: &[(ObjectId, u64)],
5593 ) -> PackIndex {
5594 let idx_path = pack_path.with_extension("idx");
5595 let pack_bytes = fs::read(&pack_path).expect("read pack for idx trailer");
5596 let trailer = &pack_bytes[pack_bytes.len().saturating_sub(20)..];
5597 let rows: Vec<(ObjectId, u64, u32)> = objects
5598 .iter()
5599 .map(|(oid, offset)| (*oid, *offset, 0))
5600 .collect();
5601 write_v2_pack_index_with_trailer(&idx_path, &rows, trailer, 20, None).expect("write idx");
5602 read_pack_index(&idx_path).expect("read idx")
5603 }
5604
5605 fn index_pack_in_scratch(pack: &[u8]) -> (tempfile::TempDir, PackIndex) {
5606 let dir = tempfile::tempdir().expect("tempdir");
5607 let pack_path = dir.path().join("chain.pack");
5608 std::fs::write(&pack_path, pack).expect("write pack");
5609 let out = Command::new("git") .current_dir(dir.path())
5611 .args(["index-pack", "chain.pack"])
5612 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5613 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5614 .output()
5615 .expect("index-pack");
5616 assert!(
5617 out.status.success(),
5618 "git index-pack failed: {}",
5619 String::from_utf8_lossy(&out.stderr)
5620 );
5621 let idx_path = dir.path().join("chain.idx");
5622 let idx = read_pack_index(&idx_path).expect("read idx");
5623 (dir, idx)
5624 }
5625
5626 fn build_deep_delta_repo(commit_count: usize) -> Option<(tempfile::TempDir, ObjectId, Odb)> {
5628 let tmp = tempfile::tempdir().ok()?;
5629 let dir = tmp.path();
5630 if !git_try(dir, &["init", "-q", "-b", "main", "."]) {
5631 return None;
5632 }
5633 let mut body = String::new();
5634 for i in 0..200 {
5635 body.push_str(&format!("seed line {i:04}\n"));
5636 }
5637 for rev in 0..commit_count {
5638 body.push_str(&format!("edit-{rev}\n"));
5639 std::fs::write(dir.join("blob.txt"), body.as_bytes()).ok()?;
5640 git(dir, &["add", "blob.txt"]);
5641 git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
5642 }
5643 let tip_hex = Command::new("git") .current_dir(dir)
5645 .args(["rev-parse", "HEAD"])
5646 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5647 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5648 .output()
5649 .ok()?;
5650 if !tip_hex.status.success() {
5651 return None;
5652 }
5653 let tip = ObjectId::from_hex(std::str::from_utf8(&tip_hex.stdout).ok()?.trim()).ok()?;
5654 let git_dir = dir.join(".git");
5655 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
5656 Some((tmp, tip, odb))
5657 }
5658
5659 #[test]
5660 fn verify_packed_object_hash_sha256_detects_corruption() {
5661 use crate::hash::hash_object;
5662 use std::fs;
5663
5664 let data = b"sha256 packed blob for hash verify\n";
5665 let oid = hash_object(HashAlgo::Sha256, ObjectKind::Blob, data).expect("hash");
5666 let wrong_oid =
5667 hash_object(HashAlgo::Sha256, ObjectKind::Blob, b"different payload\n").expect("hash");
5668 let mut pack = Vec::new();
5669 pack.extend_from_slice(b"PACK");
5670 pack.extend_from_slice(&2u32.to_be_bytes());
5671 pack.extend_from_slice(&1u32.to_be_bytes());
5672 let off = append_whole_blob(&mut pack, data);
5673 pack.extend_from_slice(HashAlgo::Sha256.checksum_digest(&pack).as_slice());
5674
5675 let dir = tempfile::tempdir().expect("tempdir");
5676 let pack_path = dir.path().join("sha256-corrupt.pack");
5677 fs::write(&pack_path, &pack).expect("write pack");
5678
5679 let idx_path = pack_path.with_extension("idx");
5680 let trailer = &pack[pack.len() - 32..];
5681 write_v2_pack_index_with_trailer(&idx_path, &[(wrong_oid, off, 0)], trailer, 32, None)
5682 .expect("write idx");
5683 let idx = read_pack_index(&idx_path).expect("read idx");
5684
5685 let err = read_object_from_pack_bytes(&pack, &idx, wrong_oid.as_bytes()).unwrap_err();
5686 match err {
5687 Error::CorruptObject(msg) => {
5688 assert!(
5689 msg.contains("hashes to"),
5690 "expected hash mismatch message, got: {msg}"
5691 );
5692 assert!(
5693 msg.contains(&oid.to_hex()),
5694 "expected recomputed oid in message, got: {msg}"
5695 );
5696 }
5697 other => panic!("expected CorruptObject, got {other:?}"),
5698 }
5699 }
5700
5701 #[test]
5702 fn reads_delta_chain_deeper_than_50() {
5703 let _guard = pack_cache_test_guard();
5704 let Some((_repo, tip, odb)) = build_deep_delta_repo(120) else {
5705 let _ = "SKIP: git unavailable for deep delta fixture";
5706 return;
5707 };
5708 let pack = build_pack(
5709 &odb,
5710 &[tip],
5711 &[],
5712 &PackBuildOptions {
5713 delta: true,
5714 max_depth: 120,
5715 use_ofs_delta: true,
5716 ..PackBuildOptions::default()
5717 },
5718 )
5719 .expect("build deep delta pack");
5720
5721 let (_scratch, idx) = index_pack_in_scratch(&pack);
5722 assert!(
5723 idx.len() > 1,
5724 "expected a multi-object pack, got {}",
5725 idx.len()
5726 );
5727
5728 for entry in idx.iter() {
5729 let oid = ObjectId::from_bytes(entry.oid()).expect("oid in idx");
5730 let from_pack = read_object_from_pack(&idx, &oid).expect("read packed object");
5731 let expected = odb.read(&oid).expect("loose/alt copy for oid");
5732 assert_eq!(from_pack.kind, expected.kind);
5733 assert_eq!(from_pack.data, expected.data);
5734 assert_eq!(
5735 odb.hash(from_pack.kind, &from_pack.data).expect("hash"),
5736 oid
5737 );
5738 }
5739 }
5740
5741 #[test]
5742 fn cyclic_ref_delta_chain_errors() {
5743 let _guard = pack_cache_test_guard();
5744 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
5745 let content_a = b"aaa".as_slice();
5746 let content_b = b"bbb".as_slice();
5747 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
5748 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
5749 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
5750 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
5751
5752 let mut pack = Vec::new();
5753 pack.extend_from_slice(b"PACK");
5754 pack.extend_from_slice(&2u32.to_be_bytes());
5755 pack.extend_from_slice(&2u32.to_be_bytes());
5756 let off_a = pack.len() as u64;
5757 append_ref_delta(&mut pack, &oid_b, &delta_a);
5758 let off_b = pack.len() as u64;
5759 append_ref_delta(&mut pack, &oid_a, &delta_b);
5760 append_sha1_pack_trailer(&mut pack);
5761
5762 let dir = tempfile::tempdir().expect("tempdir");
5763 let pack_path = dir.path().join("cycle.pack");
5764 std::fs::write(&pack_path, &pack).expect("write cyclic pack");
5765 let idx = pack_index_for_objects(pack_path, &[(oid_a, off_a), (oid_b, off_b)]);
5766 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
5767 for err in [
5768 read_object_from_pack(&idx, &oid_a).expect_err("read_object_from_pack"),
5769 read_object_from_pack_bytes(&pack_bytes, &idx, oid_a.as_bytes())
5770 .expect_err("read_object_from_pack_bytes"),
5771 ] {
5772 assert!(
5773 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
5774 "expected typed delta chain limit, got {err:?}"
5775 );
5776 }
5777 }
5778
5779 #[test]
5780 fn resolve_cross_pack_delta_without_index_clone() {
5781 let _guard = pack_cache_test_guard();
5782 clear_pack_cache();
5783 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
5784 let base = b"cross-pack-base-payload".as_slice();
5785 let tip = b"cross-pack-base-payload!".as_slice();
5786 let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
5787 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
5788 let delta = encode_lcp_delta(base, tip).expect("delta");
5789
5790 let mut pack_base = Vec::new();
5791 pack_base.extend_from_slice(b"PACK");
5792 pack_base.extend_from_slice(&2u32.to_be_bytes());
5793 pack_base.extend_from_slice(&1u32.to_be_bytes());
5794 let off_base = append_whole_blob(&mut pack_base, base);
5795 append_sha1_pack_trailer(&mut pack_base);
5796
5797 let mut pack_delta = Vec::new();
5798 pack_delta.extend_from_slice(b"PACK");
5799 pack_delta.extend_from_slice(&2u32.to_be_bytes());
5800 pack_delta.extend_from_slice(&1u32.to_be_bytes());
5801 let off_tip = pack_delta.len() as u64;
5802 append_ref_delta(&mut pack_delta, &oid_base, &delta);
5803 append_sha1_pack_trailer(&mut pack_delta);
5804
5805 let dir = tempfile::tempdir().expect("tempdir");
5806 let objects = dir.path().join("objects");
5807 let _idx_base =
5808 install_synthetic_pack(&objects, "cross-base", &pack_base, &[(oid_base, off_base)]);
5809 let idx_delta =
5810 install_synthetic_pack(&objects, "cross-delta", &pack_delta, &[(oid_tip, off_tip)]);
5811
5812 let from_delta_pack = read_object_from_pack(&idx_delta, &oid_tip).expect("delta pack read");
5813 assert_eq!(from_delta_pack.kind, ObjectKind::Blob);
5814 assert_eq!(from_delta_pack.data, tip);
5815 assert_eq!(
5816 odb.hash(from_delta_pack.kind, &from_delta_pack.data)
5817 .expect("hash"),
5818 oid_tip
5819 );
5820
5821 let from_packs = read_object_from_packs(&objects, &oid_tip).expect("packs dir read");
5822 assert_eq!(from_packs.kind, ObjectKind::Blob);
5823 assert_eq!(from_packs.data, tip);
5824 assert_eq!(
5825 odb.hash(from_packs.kind, &from_packs.data).expect("hash"),
5826 oid_tip
5827 );
5828 }
5829
5830 #[test]
5831 fn read_object_from_pack_bytes_uses_in_memory_index_without_on_disk_idx() {
5832 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
5833 let content = b"in-memory-index-blob";
5834 let oid = odb.hash(ObjectKind::Blob, content).expect("hash");
5835
5836 let mut pack = Vec::new();
5837 pack.extend_from_slice(b"PACK");
5838 pack.extend_from_slice(&2u32.to_be_bytes());
5839 pack.extend_from_slice(&1u32.to_be_bytes());
5840 let off = append_whole_blob(&mut pack, content);
5841 append_sha1_pack_trailer(&mut pack);
5842
5843 let dir = tempfile::tempdir().expect("tempdir");
5844 let pack_path = dir.path().join("mem.pack");
5845 std::fs::write(&pack_path, &pack).expect("write pack");
5846 let idx = pack_index_for_objects(pack_path, &[(oid, off)]);
5847 std::fs::remove_file(&idx.idx_path).expect("drop on-disk idx");
5848 assert!(
5849 !idx.idx_path.exists(),
5850 "fixture must not rely on an on-disk .idx"
5851 );
5852
5853 let got = read_object_from_pack_bytes(&pack, &idx, oid.as_bytes()).expect("read bytes");
5854 assert_eq!(got.kind, ObjectKind::Blob);
5855 assert_eq!(got.data, content);
5856 }
5857
5858 fn build_ofs_delta_chain_pack(
5859 depth: usize,
5860 ) -> Option<(tempfile::TempDir, PackIndex, ObjectId, Odb)> {
5861 if depth == 0 {
5862 return None;
5863 }
5864 let tmp = tempfile::tempdir().ok()?;
5865 let odb = Odb::new(&tmp.path().join("objects"));
5866 let mut content = b"ofs-delta-chain-base\n".to_vec();
5867 let mut pack = Vec::new();
5868 pack.extend_from_slice(b"PACK");
5869 pack.extend_from_slice(&2u32.to_be_bytes());
5870 pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
5871 let mut base_off = append_whole_blob(&mut pack, &content);
5872 let mut entries = vec![(
5873 odb.hash(ObjectKind::Blob, &content).expect("hash"),
5874 base_off,
5875 )];
5876 for _ in 0..depth {
5877 let mut next = content.clone();
5878 next.push(b'!');
5879 let delta = encode_lcp_delta(&content, &next).ok()?;
5880 let off = append_ofs_delta(&mut pack, base_off, &delta);
5881 let oid = odb.hash(ObjectKind::Blob, &next).expect("hash");
5882 entries.push((oid, off));
5883 content = next;
5884 base_off = off;
5885 }
5886 append_sha1_pack_trailer(&mut pack);
5887 let tip = entries.last()?.0;
5888 let dir = tmp.path();
5889 std::fs::create_dir_all(dir.join("objects/pack")).ok()?;
5890 let pack_path = dir.join("objects/pack/chain.pack");
5891 std::fs::write(&pack_path, &pack).ok()?;
5892 write_v2_idx_for_test(&pack_path.with_extension("idx"), &pack_path, &entries).ok()?;
5893 clear_pack_cache();
5894 let idx = read_pack_index(&pack_path.with_extension("idx")).ok()?;
5895 Some((tmp, idx, tip, odb))
5896 }
5897
5898 #[test]
5899 fn ofs_delta_chain_depth_4096_resolves() {
5900 let _guard = pack_cache_test_guard();
5901 clear_pack_cache();
5902 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(4096) else {
5903 return;
5904 };
5905 let got = read_object_from_pack(&idx, &tip).expect("4096-deep chain");
5906 let expect = odb.hash(ObjectKind::Blob, &got.data).expect("hash");
5907 assert_eq!(expect, tip);
5908 }
5909
5910 #[test]
5911 fn ofs_delta_chain_depth_4097_errors() {
5912 let _guard = pack_cache_test_guard();
5913 clear_pack_cache();
5914 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(4097) else {
5915 return;
5916 };
5917 let err = read_object_from_pack(&idx, &tip).expect_err("4097-deep chain");
5918 assert!(
5919 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
5920 "got {err:?}"
5921 );
5922 }
5923
5924 #[test]
5925 fn delta_base_cache_limit_zero_disables_caching() {
5926 let _guard = pack_cache_test_guard();
5927 clear_pack_cache();
5928 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
5929 return;
5930 };
5931 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 0);
5932 let _ = read_object_from_pack(&idx, &tip).expect("read chain");
5933 assert_eq!(
5934 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
5935 0,
5936 "limit 0 must not retain delta bases"
5937 );
5938 pack_cache::test_set_delta_base_cache_byte_limit(
5939 &idx.pack_path,
5940 pack_cache::DELTA_BASE_CACHE_DEFAULT,
5941 );
5942 }
5943
5944 #[test]
5945 fn delta_base_cache_eviction_respects_byte_limit() {
5946 let _guard = pack_cache_test_guard();
5947 clear_pack_cache();
5948 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(32) else {
5949 return;
5950 };
5951 let expected = read_object_from_pack(&idx, &tip)
5952 .expect("baseline read")
5953 .data;
5954 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 4096);
5955 let _ = read_object_from_pack(&idx, &tip).expect("prime cache");
5956 assert!(
5957 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) <= 4096,
5958 "cache bytes {} exceeded cap",
5959 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path)
5960 );
5961 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 512);
5962 for round in 0..8 {
5963 let got = read_object_from_pack(&idx, &tip).expect("read after eviction");
5964 assert_eq!(got.data, expected, "round {round} must stay byte-identical");
5965 }
5966 pack_cache::test_set_delta_base_cache_byte_limit(
5967 &idx.pack_path,
5968 pack_cache::DELTA_BASE_CACHE_DEFAULT,
5969 );
5970 }
5971
5972 #[test]
5973 fn odb_read_into_caching_delta_bases_fills_cache_read_into_does_not() {
5974 let _guard = pack_cache_test_guard();
5975 clear_pack_cache();
5976 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(12) else {
5977 return;
5978 };
5979 let expected = read_object_from_pack(&idx, &tip)
5980 .expect("baseline read")
5981 .data;
5982 clear_pack_cache();
5983 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
5984 crate::pack_store::PackStore::with_context(store, || {
5985 let mut buf = Vec::new();
5986 assert_eq!(
5987 odb.read_into(&tip, &mut buf).expect("read_into"),
5988 ObjectKind::Blob
5989 );
5990 assert_eq!(buf, expected);
5991 assert_eq!(
5992 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
5993 0,
5994 "read_into must not retain delta bases"
5995 );
5996
5997 let mut cached = Vec::new();
5998 let kind = odb
5999 .read_into_caching_delta_bases(&tip, &mut cached)
6000 .expect("caching read");
6001 assert_eq!(kind, ObjectKind::Blob);
6002 assert_eq!(cached, expected);
6003 assert!(
6004 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) > 0,
6005 "caching read must retain the chain's delta bases"
6006 );
6007
6008 let again = odb
6009 .read_into_caching_delta_bases(&tip, &mut cached)
6010 .expect("second caching read");
6011 assert_eq!(again, ObjectKind::Blob);
6012 assert_eq!(cached, expected);
6013 });
6014 }
6015
6016 #[test]
6017 fn repository_odb_read_into_caching_delta_bases_fills_cache_through_files_primary() {
6018 use crate::repo::{InitOptions, Repository};
6019 let _guard = pack_cache_test_guard();
6020 clear_pack_cache();
6021 let Some((tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6022 return;
6023 };
6024 let expected = read_object_from_pack(&idx, &tip)
6025 .expect("baseline read")
6026 .data;
6027 Repository::init(
6028 tmp.path(),
6029 &InitOptions {
6030 bare: true,
6031 ..InitOptions::default()
6032 },
6033 )
6034 .expect("init over packed objects");
6035 let repo = Repository::open(tmp.path(), &crate::environment::RepositoryOptions::empty())
6036 .expect("open");
6037 let odb = repo.odb();
6038 let store = Arc::clone(odb.pack_store().expect("pack store"));
6039 store.invalidate_packs();
6040
6041 let mut buf = Vec::new();
6042 assert_eq!(
6043 odb.read_into(&tip, &mut buf).expect("read_into"),
6044 ObjectKind::Blob
6045 );
6046 assert_eq!(buf, expected);
6047 assert_eq!(store.delta_bases().bytes_used(), 0);
6048
6049 let mut cached = Vec::new();
6050 assert_eq!(
6051 odb.read_into_caching_delta_bases(&tip, &mut cached)
6052 .expect("caching read"),
6053 ObjectKind::Blob
6054 );
6055 assert_eq!(cached, expected);
6056 assert!(store.delta_bases().bytes_used() > 0);
6057 }
6058
6059 #[test]
6060 fn odb_read_into_caching_delta_bases_falls_back_outside_packs() {
6061 let tmp = tempfile::tempdir().expect("tempdir");
6062 let odb = Odb::new(&tmp.path().join("objects"));
6063 let oid = odb
6064 .write(ObjectKind::Blob, b"loose only\n")
6065 .expect("write loose");
6066 let mut buf = vec![0u8; 4];
6067 let kind = odb
6068 .read_into_caching_delta_bases(&oid, &mut buf)
6069 .expect("loose read");
6070 assert_eq!(kind, ObjectKind::Blob);
6071 assert_eq!(buf, b"loose only\n");
6072 }
6073
6074 #[test]
6075 fn cached_delta_reads_support_overlay_alternates_and_custom_stores() {
6076 use crate::odb::store::MemoryStore;
6077 use crate::odb::OdbBuilder;
6078
6079 let tmp = tempfile::tempdir().expect("tempdir");
6080 let local_objects = tmp.path().join("local/objects");
6081 let alternate_objects = tmp.path().join("alternate/objects");
6082 let alternate = Odb::new(&alternate_objects);
6083 let alternate_oid = alternate
6084 .write(ObjectKind::Blob, b"alternate\n")
6085 .expect("alternate object");
6086 fs::create_dir_all(local_objects.join("info")).expect("info");
6087 fs::write(
6088 local_objects.join("info/alternates"),
6089 format!("{}\n", alternate_objects.display()),
6090 )
6091 .expect("alternates");
6092
6093 let odb = Odb::new(&local_objects);
6094 let mut buf = Vec::with_capacity(1024);
6095 assert_eq!(
6096 odb.read_into_caching_delta_bases(&alternate_oid, &mut buf)
6097 .expect("alternate read"),
6098 ObjectKind::Blob
6099 );
6100 assert_eq!(buf, b"alternate\n");
6101
6102 odb.enable_mem_overlay();
6103 let overlay_oid = odb
6104 .write(ObjectKind::Blob, b"overlay\n")
6105 .expect("overlay object");
6106 odb.read_into_caching_delta_bases(&overlay_oid, &mut buf)
6107 .expect("overlay read");
6108 assert_eq!(buf, b"overlay\n");
6109
6110 let custom = OdbBuilder::files(tmp.path().join("custom/objects"))
6111 .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
6112 .build();
6113 let custom_oid = custom
6114 .write(ObjectKind::Blob, b"custom\n")
6115 .expect("custom object");
6116 custom
6117 .read_into_caching_delta_bases(&custom_oid, &mut buf)
6118 .expect("custom read");
6119 assert_eq!(buf, b"custom\n");
6120
6121 let missing = HashAlgo::Sha1
6122 .hash_object(ObjectKind::Blob, b"missing")
6123 .expect("missing id");
6124 assert!(matches!(
6125 custom.read_into_caching_delta_bases(&missing, &mut buf),
6126 Err(Error::ObjectNotFound(_))
6127 ));
6128 }
6129
6130 #[test]
6131 fn delta_base_cache_invalidated_when_pack_bytes_reloaded() {
6132 use filetime::{set_file_mtime, FileTime};
6133 let _guard = pack_cache_test_guard();
6134 clear_pack_cache();
6135 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(8) else {
6136 return;
6137 };
6138 let pack_path = idx.pack_path.clone();
6139 let pack_id = pack_cache::pack_id_for(&pack_path);
6140 let mid_off = idx
6141 .iter()
6142 .nth(4)
6143 .map(|e| e.offset())
6144 .expect("mid hop")
6145 .expect("mid off");
6146 let _ = read_object_from_pack(&idx, &tip).expect("warm cache");
6147 assert!(
6148 pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6149 "expected intermediate cached at offset {mid_off}"
6150 );
6151 set_file_mtime(&pack_path, FileTime::now()).expect("touch pack mtime");
6152 assert!(
6153 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6154 "expected pack bytes reload"
6155 );
6156 assert!(
6157 !pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6158 "cache must drop when pack bytes reload"
6159 );
6160 }
6161
6162 #[test]
6163 fn git_repack_depth_250_matches_cat_file_batch() {
6164 let _guard = pack_cache_test_guard();
6165 clear_pack_cache();
6166 let tmp = tempfile::tempdir().expect("tempdir");
6167 let dir = tmp.path();
6168 if Command::new("git") .current_dir(dir)
6170 .args(["init", "-q"])
6171 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6172 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6173 .status()
6174 .is_err()
6175 {
6176 let _ = "SKIP: git unavailable";
6177 return;
6178 }
6179 for i in 0..64 {
6180 std::fs::write(dir.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
6181 git(dir, &["add", &format!("f{i}.txt")]);
6182 git(dir, &["commit", "-qm", &format!("c{i}")]);
6183 }
6184 git(dir, &["repack", "-adf", "--depth=250", "--window=250"]);
6185 let objects = dir.join(".git/objects");
6186 let indexes = read_local_pack_indexes_cached(&objects).expect("pack indexes");
6187 let idx = indexes
6188 .into_iter()
6189 .max_by_key(|i| i.len())
6190 .expect("repack pack");
6191 for entry in idx.iter() {
6192 let oid = ObjectId::from_bytes(entry.oid()).expect("oid");
6193 let grit_obj = read_object_from_pack(&idx, &oid).expect("grit read");
6194 let git_type = Command::new("git") .current_dir(dir)
6196 .args(["cat-file", "-t", &oid.to_hex()])
6197 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6198 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6199 .output()
6200 .expect("git cat-file -t");
6201 assert!(git_type.status.success());
6202 let git_kind = ObjectKind::from_bytes(
6203 std::str::from_utf8(git_type.stdout.trim_ascii_end())
6204 .expect("utf8 type")
6205 .as_bytes(),
6206 )
6207 .expect("git type");
6208 assert_eq!(git_kind, grit_obj.kind);
6209 let type_arg = match grit_obj.kind {
6210 ObjectKind::Blob => "blob",
6211 ObjectKind::Tree => "tree",
6212 ObjectKind::Commit => "commit",
6213 ObjectKind::Tag => "tag",
6214 };
6215 let git_payload = Command::new("git") .current_dir(dir)
6217 .args(["cat-file", type_arg, &oid.to_hex()])
6218 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6219 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6220 .output()
6221 .expect("git cat-file raw");
6222 assert!(git_payload.status.success());
6223 assert_eq!(git_payload.stdout, grit_obj.data);
6224 }
6225 }
6226
6227 #[test]
6228 fn cyclic_ref_delta_chain_across_two_packs_errors() {
6229 let _guard = pack_cache_test_guard();
6230 clear_pack_cache();
6231 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6232 let content_a = b"aaa".as_slice();
6233 let content_b = b"bbb".as_slice();
6234 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6235 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6236 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6237 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6238
6239 let mut pack_a = Vec::new();
6240 pack_a.extend_from_slice(b"PACK");
6241 pack_a.extend_from_slice(&2u32.to_be_bytes());
6242 pack_a.extend_from_slice(&1u32.to_be_bytes());
6243 let off_a = pack_a.len() as u64;
6244 append_ref_delta(&mut pack_a, &oid_b, &delta_a);
6245 append_sha1_pack_trailer(&mut pack_a);
6246
6247 let mut pack_b = Vec::new();
6248 pack_b.extend_from_slice(b"PACK");
6249 pack_b.extend_from_slice(&2u32.to_be_bytes());
6250 pack_b.extend_from_slice(&1u32.to_be_bytes());
6251 let off_b = pack_b.len() as u64;
6252 append_ref_delta(&mut pack_b, &oid_a, &delta_b);
6253 append_sha1_pack_trailer(&mut pack_b);
6254
6255 let dir = tempfile::tempdir().expect("tempdir");
6256 let objects = dir.path().join("objects");
6257 let idx_b = install_synthetic_pack(&objects, "cycle-b", &pack_b, &[(oid_b, off_b)]);
6258 let idx_a = install_synthetic_pack(&objects, "cycle-a", &pack_a, &[(oid_a, off_a)]);
6259 assert_eq!(idx_a.len(), 1);
6260 assert_eq!(idx_b.len(), 1);
6261 assert!(idx_b.contains(&oid_b).expect("contains"));
6262 let _ = idx_b;
6263
6264 let err = read_object_from_pack(&idx_a, &oid_a).expect_err("cross-pack cycle");
6265 assert!(
6266 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6267 "expected typed delta chain limit, got {err:?}"
6268 );
6269 }
6270
6271 #[test]
6272 fn cross_pack_ref_delta_resolves_through_odb_read() {
6273 let _guard = pack_cache_test_guard();
6274 clear_pack_cache();
6275 let tmp = tempfile::tempdir().expect("tempdir");
6276 let objects = tmp.path().join("objects");
6277 let odb = Odb::new(&objects);
6278 let base = b"common prefix base";
6279 let target = b"common prefix target";
6280 let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
6281 let delta = encode_lcp_delta(base, target).expect("delta");
6282 let oid_target = odb.hash(ObjectKind::Blob, target).expect("hash");
6283
6284 let mut pack_base = Vec::new();
6285 pack_base.extend_from_slice(b"PACK");
6286 pack_base.extend_from_slice(&2u32.to_be_bytes());
6287 pack_base.extend_from_slice(&1u32.to_be_bytes());
6288 let off_base = append_whole_blob(&mut pack_base, base);
6289 append_sha1_pack_trailer(&mut pack_base);
6290
6291 let mut pack_tip = Vec::new();
6292 pack_tip.extend_from_slice(b"PACK");
6293 pack_tip.extend_from_slice(&2u32.to_be_bytes());
6294 pack_tip.extend_from_slice(&1u32.to_be_bytes());
6295 let off_tip = pack_tip.len() as u64;
6296 append_ref_delta(&mut pack_tip, &oid_base, &delta);
6297 append_sha1_pack_trailer(&mut pack_tip);
6298
6299 install_synthetic_pack(&objects, "base", &pack_base, &[(oid_base, off_base)]);
6300 install_synthetic_pack(&objects, "tip", &pack_tip, &[(oid_target, off_tip)]);
6301
6302 let got = odb
6303 .read(&oid_target)
6304 .expect("Odb::read cross-pack ref-delta");
6305 assert_eq!(got.kind, ObjectKind::Blob);
6306 assert_eq!(got.data.as_slice(), target);
6307 assert_eq!(odb.hash(got.kind, &got.data).expect("hash"), oid_target);
6308 }
6309
6310 #[test]
6311 fn corrupt_in_pack_base_rescued_from_loose() {
6312 let _guard = pack_cache_test_guard();
6313 let tmp = tempfile::tempdir().expect("tempdir");
6314 let objects = tmp.path().join("objects");
6315 std::fs::create_dir_all(objects.join("pack")).expect("dirs");
6316 let odb = Odb::new(&objects);
6317 let base = b"good-base-bytes";
6318 let tip = b"good-base-bytes!";
6319 let oid_base = odb.write_local(ObjectKind::Blob, base).expect("loose base");
6320 let delta = encode_lcp_delta(base, tip).expect("delta");
6321 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6322
6323 let mut pack = Vec::new();
6324 pack.extend_from_slice(b"PACK");
6325 pack.extend_from_slice(&2u32.to_be_bytes());
6326 pack.extend_from_slice(&2u32.to_be_bytes());
6327 let off_base = append_corrupt_whole_blob(&mut pack);
6328 let off_tip = pack.len() as u64;
6329 append_ref_delta(&mut pack, &oid_base, &delta);
6330 append_sha1_pack_trailer(&mut pack);
6331
6332 let pack_path = objects.join("pack/rescue.pack");
6333 std::fs::write(&pack_path, &pack).expect("write pack");
6334 let idx = pack_index_for_objects(pack_path, &[(oid_base, off_base), (oid_tip, off_tip)]);
6335
6336 let got = read_object_from_pack(&idx, &oid_tip).expect("rescue read");
6337 assert_eq!(got.kind, ObjectKind::Blob);
6338 assert_eq!(got.data, tip);
6339 }
6340
6341 #[test]
6342 fn pack_added_after_first_lookup_found_on_miss() {
6343 let _guard = pack_cache_test_guard();
6344 clear_pack_cache();
6345 let tmp = tempfile::tempdir().expect("tempdir");
6346 let objects = tmp.path().join("objects");
6347 let pack_dir = objects.join("pack");
6348 let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
6349 install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
6350 pack_cache::test_reset_dir_rescan_count(&pack_dir);
6351
6352 read_object_from_packs(&objects, &oid_a).expect("warm cache");
6353 assert_eq!(
6354 pack_cache::test_dir_rescan_count(&pack_dir),
6355 1,
6356 "initial lookup should scan the pack directory once"
6357 );
6358
6359 std::thread::sleep(std::time::Duration::from_secs(1));
6361
6362 let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b");
6363 install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
6364 assert_eq!(
6365 pack_cache::test_dir_rescan_count(&pack_dir),
6366 1,
6367 "adding a pack must not rescan until a miss"
6368 );
6369
6370 read_object_from_packs(&objects, &oid_b).expect("new pack via miss path");
6371 assert_eq!(
6372 pack_cache::test_dir_rescan_count(&pack_dir),
6373 2,
6374 "miss path should re-scan when pack dir mtime changed"
6375 );
6376 }
6377
6378 #[test]
6379 fn known_object_lookup_does_not_rescan() {
6380 let _guard = pack_cache_test_guard();
6381 clear_pack_cache();
6382 let tmp = tempfile::tempdir().expect("tempdir");
6383 let objects = tmp.path().join("objects");
6384 let pack_dir = objects.join("pack");
6385 pack_cache::test_reset_dir_rescan_count(&pack_dir);
6386 let (pack, oid, off) = single_blob_pack(b"cached-blob");
6387 install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
6388
6389 read_object_from_packs(&objects, &oid).expect("prime cache");
6390 let after_first = pack_cache::test_dir_rescan_count(&pack_dir);
6391 for _ in 0..64 {
6392 read_object_from_packs(&objects, &oid).expect("repeat read");
6393 }
6394 assert_eq!(
6395 pack_cache::test_dir_rescan_count(&pack_dir),
6396 after_first,
6397 "repeated hits must not re-scan the pack directory"
6398 );
6399 }
6400
6401 #[test]
6402 fn two_repositories_concurrent_reads() {
6403 use crate::odb::Odb;
6404 use std::sync::Arc;
6405
6406 let _guard = pack_cache_test_guard();
6407 clear_pack_cache();
6408 let tmp_a = tempfile::tempdir().expect("tempdir a");
6409 let tmp_b = tempfile::tempdir().expect("tempdir b");
6410 let objects_a = tmp_a.path().join("objects");
6411 let objects_b = tmp_b.path().join("objects");
6412 let pack_a_dir = objects_a.join("pack");
6413 let pack_b_dir = objects_b.join("pack");
6414 let (pack_a, oid_a, off_a) = single_blob_pack(b"parallel-a");
6415 let (pack_b, oid_b, off_b) = single_blob_pack(b"parallel-b");
6416 install_synthetic_pack(&objects_a, "a", &pack_a, &[(oid_a, off_a)]);
6417 install_synthetic_pack(&objects_b, "b", &pack_b, &[(oid_b, off_b)]);
6418 let odb_a = Odb::new(&objects_a);
6419 let odb_b = Odb::new(&objects_b);
6420 assert!(
6421 !Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
6422 "independent Odb opens must not share pack stores"
6423 );
6424 pack_cache::test_reset_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir);
6425 pack_cache::test_reset_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir);
6426
6427 let barrier = Arc::new(std::sync::Barrier::new(2));
6428 let b0 = Arc::clone(&barrier);
6429 let b1 = Arc::clone(&barrier);
6430 let odb_a_t = odb_a.clone();
6431 let odb_b_t = odb_b.clone();
6432 let id_a = oid_a;
6433 let id_b = oid_b;
6434
6435 let h1 = std::thread::spawn(move || {
6436 b0.wait();
6437 odb_a_t.read(&id_a).expect("read a");
6438 });
6439 let h2 = std::thread::spawn(move || {
6440 b1.wait();
6441 odb_b_t.read(&id_b).expect("read b");
6442 });
6443 h1.join().expect("thread a");
6444 h2.join().expect("thread b");
6445
6446 assert_eq!(
6447 pack_cache::test_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir),
6448 1
6449 );
6450 assert_eq!(
6451 pack_cache::test_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir),
6452 1
6453 );
6454 }
6455
6456 #[test]
6457 fn stale_cached_pack_bytes_revalidated_before_corruption_error() {
6458 let _guard = pack_cache_test_guard();
6459 clear_pack_cache();
6460 let tmp = tempfile::tempdir().expect("tempdir");
6461 let objects = tmp.path().join("objects");
6462 let (pack, oid, off) = single_blob_pack(b"revalidate-me");
6463 let idx = install_synthetic_pack(&objects, "good", &pack, &[(oid, off)]);
6464 read_object_from_pack(&idx, &oid).expect("prime pack bytes cache");
6465
6466 let good_on_disk = std::fs::read(&idx.pack_path).expect("read pack");
6467 pack_cache::test_inject_stale_pack_bytes(
6468 &idx.pack_path,
6469 PackData::from_owned(b"BAD".to_vec()),
6470 );
6471 std::fs::write(&idx.pack_path, &good_on_disk).expect("refresh on-disk pack");
6472 let got = read_object_from_pack(&idx, &oid).expect("revalidate from disk");
6473 assert_eq!(got.data, b"revalidate-me");
6474 }
6475
6476 #[test]
6477 fn replaced_pack_same_path_reread_after_invalidation() {
6478 let _guard = pack_cache_test_guard();
6479 clear_pack_cache();
6480 let tmp = tempfile::tempdir().expect("tempdir");
6481 let objects = tmp.path().join("objects");
6482 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6483 let pack_path = objects.join("pack/repack-me.pack");
6484 let idx_path = objects.join("pack/repack-me.idx");
6485 const OBJECT_OFFSET: u64 = 9000;
6486 let (oid_a, off_a) =
6487 write_sparse_single_blob_pack(tmp.path(), &pack_path, b"before-repack", OBJECT_OFFSET)
6488 .expect("sparse pack a");
6489 assert!(
6490 std::fs::metadata(&pack_path).expect("stat").len() > 8192,
6491 "fixture pack must exceed mmap owned-buffer threshold"
6492 );
6493 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_a, off_a)]).expect("idx a");
6494 let idx = read_pack_index(&idx_path).expect("parse idx a");
6495 read_object_from_pack(&idx, &oid_a).expect("prime cache");
6496
6497 let (oid_b, off_b) = write_sparse_single_blob_pack(
6498 tmp.path(),
6499 &pack_path,
6500 b"after-repack-content",
6501 OBJECT_OFFSET,
6502 )
6503 .expect("sparse pack b");
6504 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_b, off_b)]).expect("idx b");
6505
6506 assert!(
6507 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6508 "replacement must invalidate cached bytes"
6509 );
6510 let idx = read_pack_index(&idx_path).expect("reload idx after pack swap");
6511 let got = read_object_from_pack(&idx, &oid_b).expect("read from replaced pack");
6512 assert_eq!(got.data, b"after-repack-content");
6513 }
6514
6515 #[test]
6516 fn read_object_at_large_pack_offset_via_mmap() {
6517 use std::fs::OpenOptions;
6518 use std::io::{Seek, SeekFrom, Write};
6519
6520 let _guard = pack_cache_test_guard();
6521 clear_pack_cache();
6522 let tmp = tempfile::tempdir().expect("tempdir");
6523 let objects = tmp.path().join("objects");
6524 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6525
6526 let data = b"large-offset-blob";
6527 let odb = Odb::new(tmp.path());
6528 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
6529
6530 let pack_path = objects.join("pack/large-off.pack");
6531 let mut file = OpenOptions::new()
6532 .create(true)
6533 .write(true)
6534 .read(true)
6535 .open(&pack_path)
6536 .expect("create sparse pack");
6537 file.write_all(b"PACK").expect("sig");
6538 file.write_all(&2u32.to_be_bytes()).expect("ver");
6539 file.write_all(&1u32.to_be_bytes()).expect("count");
6540 let object_offset: u64 = 1 << 32;
6541 file.seek(SeekFrom::Start(object_offset))
6542 .expect("seek past 4GiB");
6543 let mut tail = Vec::new();
6544 append_whole_blob(&mut tail, data);
6545 file.write_all(&tail).expect("object at huge offset");
6546 let trailer = sha1_trailer_for_open_file(&mut file).expect("trailer hash");
6547 file.write_all(trailer.as_bytes()).expect("append trailer");
6548 file.flush().expect("flush");
6549 let file_len = file.metadata().expect("meta").len();
6550 drop(file);
6551 assert!(file_len > object_offset);
6552
6553 let idx_path = pack_path.with_extension("idx");
6554 write_v2_pack_index_with_trailer(
6555 &idx_path,
6556 &[(oid, object_offset, 0)],
6557 trailer.as_bytes(),
6558 20,
6559 None,
6560 )
6561 .expect("idx");
6562
6563 let idx = read_pack_index(&idx_path).expect("parse idx");
6564 assert_eq!(idx.offset_at(0).expect("off"), object_offset);
6565 let got = read_object_from_pack(&idx, &oid).expect("read at large offset");
6566 assert_eq!(got.data, data);
6567 }
6568
6569 fn synthetic_index_entries(n: u32) -> Vec<(ObjectId, u64, u32)> {
6570 (0..n)
6571 .map(|i| {
6572 let digest = HashAlgo::Sha1.checksum_digest(&i.to_be_bytes());
6573 let oid = ObjectId::from_bytes(&digest).expect("oid");
6574 let offset = if i % 7 == 0 {
6575 (1u64 << 31) + u64::from(i)
6576 } else {
6577 12 + u64::from(i) * 40
6578 };
6579 (oid, offset, i.wrapping_mul(2_654_435_761))
6580 })
6581 .collect()
6582 }
6583
6584 #[test]
6585 fn index_entries_sort_in_place_on_any_thread_count() {
6586 for n in [0_u32, 1, 300, 20_000] {
6587 let mut reference = synthetic_index_entries(n);
6588 reference.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6589 let mut fanout_ref = [0u32; 256];
6590 for (oid, _, _) in &reference {
6591 for slot in &mut fanout_ref[usize::from(oid.as_bytes()[0])..] {
6592 *slot += 1;
6593 }
6594 }
6595 for threads in [None, Some(1), Some(2), Some(3), Some(8)] {
6596 let mut entries = synthetic_index_entries(n);
6597 let fanout = sort_index_entries(&mut entries, threads);
6598 assert_eq!(entries, reference, "n={n} threads={threads:?}");
6599 assert_eq!(fanout, fanout_ref, "n={n} threads={threads:?}");
6600 }
6601 }
6602 }
6603
6604 #[test]
6605 fn index_written_in_place_reads_back_every_row_with_large_offsets() {
6606 let tmp = tempfile::tempdir().expect("tempdir");
6607 let idx_path = tmp.path().join("p.idx");
6608 let rows = synthetic_index_entries(20_000);
6609 let trailer = [0x5a_u8; 20];
6610 let mut entries = rows.clone();
6611 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer, 20, Some(4))
6612 .expect("write idx");
6613 let idx = read_pack_index(&idx_path).expect("read idx");
6614 assert_eq!(idx.len(), rows.len());
6615 let mut want = rows;
6616 want.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6617 for (pos, (oid, offset, crc)) in want.iter().enumerate() {
6618 assert_eq!(idx.oid_at(pos), oid.as_bytes(), "row {pos}");
6619 assert_eq!(idx.offset_at(pos).expect("offset"), *offset, "row {pos}");
6620 assert_eq!(idx.crc32_at(pos).expect("crc"), *crc, "row {pos}");
6621 }
6622
6623 let mut bad = vec![(ObjectId::from_bytes(&[1; 32]).expect("oid"), 12, 0)];
6624 assert!(matches!(
6625 write_v2_pack_index_sorting(&idx_path, &mut bad, &trailer, 20, None),
6626 Err(Error::CorruptObject(_))
6627 ));
6628 assert!(matches!(
6629 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer[..19], 20, None),
6630 Err(Error::CorruptObject(_))
6631 ));
6632 }
6633
6634 fn sha1_trailer_for_open_file(file: &mut std::fs::File) -> Result<ObjectId> {
6636 use std::io::{Read, Seek, SeekFrom};
6637 let len = file.metadata().map_err(Error::Io)?.len();
6638 file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
6639 let mut hasher = HashAlgo::Sha1.checksum_hasher();
6640 let mut buf = [0u8; 64 * 1024];
6641 let mut left = len;
6642 while left > 0 {
6643 let chunk = left.min(buf.len() as u64) as usize;
6644 let n = file.read(&mut buf[..chunk]).map_err(Error::Io)?;
6645 if n == 0 {
6646 break;
6647 }
6648 hasher.update(&buf[..n]);
6649 left -= n as u64;
6650 }
6651 let digest = hasher.finalize();
6652 ObjectId::from_bytes(&digest).map_err(|e| Error::InvalidObjectId(format!("{e}")))
6653 }
6654
6655 #[test]
6656 fn cleared_cache_sees_removed_packs_disappear() {
6657 let _guard = pack_cache_test_guard();
6658 clear_pack_cache();
6659 let tmp = tempfile::tempdir().expect("tempdir");
6660 let objects = tmp.path().join("objects");
6661 let (pack, oid, off) = single_blob_pack(b"ephemeral");
6662 install_synthetic_pack(&objects, "gone", &pack, &[(oid, off)]);
6663 read_object_from_packs(&objects, &oid).expect("warm cache");
6664
6665 let pack_path = objects.join("pack/gone.pack");
6666 let idx_path = objects.join("pack/gone.idx");
6667 std::fs::remove_file(&pack_path).expect("remove pack");
6668 std::fs::remove_file(&idx_path).expect("remove idx");
6669 clear_pack_cache();
6670
6671 let indexes = read_local_pack_indexes_cached(&objects).expect("rescan");
6672 assert!(
6673 indexes.is_empty(),
6674 "after clear, listing should not retain removed packs"
6675 );
6676 }
6677
6678 #[test]
6679 fn repack_while_sibling_odb_holds_store_reprepares_on_miss() {
6680 use crate::odb::Odb;
6681
6682 let _guard = pack_cache_test_guard();
6683 clear_pack_cache();
6684 let tmp = tempfile::tempdir().expect("tempdir");
6685 let objects = tmp.path().join("objects");
6686 let (pack_old, oid_old, off_old) = single_blob_pack(b"before");
6687 install_synthetic_pack(&objects, "old", &pack_old, &[(oid_old, off_old)]);
6688 let odb_a = Odb::new(&objects);
6689 let odb_b = Odb::new(&objects);
6690 assert!(
6691 !std::sync::Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
6692 "sibling Odbs must have distinct pack stores"
6693 );
6694 odb_b.read(&oid_old).expect("warm b store");
6695
6696 std::fs::remove_file(objects.join("pack/old.pack")).expect("rm pack");
6697 std::fs::remove_file(objects.join("pack/old.idx")).expect("rm idx");
6698 let (pack_new, oid_new, off_new) = single_blob_pack(b"after");
6699 install_synthetic_pack(&objects, "new", &pack_new, &[(oid_new, off_new)]);
6700 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now())
6701 .expect("bump pack dir mtime for reprepare");
6702 odb_a.invalidate_packs();
6703
6704 let got = odb_b.read(&oid_new).expect("b reprepares after miss");
6706 assert_eq!(got.data.as_slice(), b"after");
6707 }
6708}
6709#[cfg(test)]
6710mod cached_lookup_tests {
6711 use super::*;
6712 use std::fs;
6713 use std::process::Command;
6714 use tempfile::TempDir;
6715
6716 fn git(dir: &Path, args: &[&str]) {
6717 let out = Command::new("git") .current_dir(dir)
6719 .args(args)
6720 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6721 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6722 .env("GIT_AUTHOR_NAME", "t")
6723 .env("GIT_AUTHOR_EMAIL", "t@example.com")
6724 .env("GIT_COMMITTER_NAME", "t")
6725 .env("GIT_COMMITTER_EMAIL", "t@example.com")
6726 .output()
6727 .expect("spawn git");
6728 assert!(
6729 out.status.success(),
6730 "git {:?}: {}",
6731 args,
6732 String::from_utf8_lossy(&out.stderr)
6733 );
6734 }
6735
6736 fn repack_all(dir: &Path) {
6737 git(dir, &["repack", "-a", "-d"]);
6738 }
6739
6740 fn init_repo_with_pack(dir: &Path) -> ObjectId {
6741 git(dir, &["init", "-q"]);
6742 std::fs::write(dir.join("blob.txt"), b"pack cached lookup fixture\n").unwrap();
6743 git(dir, &["add", "blob.txt"]);
6744 git(dir, &["commit", "-qm", "c"]);
6745 repack_all(dir);
6746 let hex = Command::new("git") .current_dir(dir)
6748 .args(["rev-parse", "HEAD^{tree}"])
6749 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6750 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6751 .output()
6752 .unwrap();
6753 let tree_hex = String::from_utf8(hex.stdout).unwrap();
6754 ObjectId::from_hex(tree_hex.trim()).unwrap()
6755 }
6756
6757 fn pack_dir(objects: &Path) -> PathBuf {
6758 objects.join("pack")
6759 }
6760
6761 fn v2_idx_path(objects: &Path) -> PathBuf {
6762 pack_dir(objects)
6763 .read_dir()
6764 .unwrap()
6765 .filter_map(|e| e.ok())
6766 .map(|e| e.path())
6767 .find(|p| p.extension().is_some_and(|x| x == "idx"))
6768 .expect("pack idx")
6769 }
6770
6771 fn convert_pack_idx_to_v1(objects: &Path) -> PathBuf {
6773 let idx_path = v2_idx_path(objects);
6774 let parsed = read_pack_index(&idx_path).expect("v2 idx");
6775 let mut body = Vec::new();
6776 for slot in parsed.fanout {
6777 body.extend_from_slice(&slot.to_be_bytes());
6778 }
6779 for entry in parsed.iter() {
6780 body.extend_from_slice(&(entry.offset().expect("off") as u32).to_be_bytes());
6781 body.extend_from_slice(entry.oid());
6782 }
6783 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
6784 rewrite_test_file(&idx_path, &body);
6785 clear_pack_cache();
6786 idx_path
6787 }
6788
6789 fn rewrite_test_file(path: &Path, body: &[u8]) {
6790 let _ = fs::remove_file(path);
6791 fs::write(path, body).unwrap();
6792 }
6793
6794 fn synthetic_fanout_index(entries: &[(Vec<u8>, u64)]) -> PackIndex {
6795 let mut sorted = entries.to_vec();
6796 sorted.sort_by(|a, b| a.0.cmp(&b.0));
6797 let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
6798 let fanout = compute_fanout_from_oid_slices(&slices);
6799 let mut body = Vec::new();
6800 for f in fanout {
6801 body.extend_from_slice(&f.to_be_bytes());
6802 }
6803 for (oid, off) in sorted {
6804 body.extend_from_slice(&(off as u32).to_be_bytes());
6805 body.extend_from_slice(&oid);
6806 }
6807 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
6808 parse_pack_index_bytes(Path::new("synthetic.idx"), body, false).expect("v1 idx")
6809 }
6810
6811 #[test]
6812 fn find_offset_fanout_bucket_first_last_and_miss() {
6813 let first = vec![0x05u8; 20];
6814 let mut mid = vec![0x05u8; 20];
6815 mid[19] = 1;
6816 let mut last = vec![0x05u8; 20];
6817 last[19] = 2;
6818 let miss = vec![0x06u8; 20];
6819 let idx =
6820 synthetic_fanout_index(&[(first.clone(), 12), (mid.clone(), 24), (last.clone(), 36)]);
6821
6822 let oid_first = ObjectId::from_bytes(&first).unwrap();
6823 let oid_mid = ObjectId::from_bytes(&mid).unwrap();
6824 let oid_last = ObjectId::from_bytes(&last).unwrap();
6825 let oid_miss = ObjectId::from_bytes(&miss).unwrap();
6826
6827 assert_eq!(idx.find_offset(&oid_first).expect("f"), Some(12));
6828 assert_eq!(idx.find_offset(&oid_mid).expect("m"), Some(24));
6829 assert_eq!(idx.find_offset(&oid_last).expect("l"), Some(36));
6830 assert_eq!(idx.find_offset(&oid_miss).expect("miss"), None);
6831 }
6832
6833 #[test]
6834 fn cached_lookup_hit_v2_idx() {
6835 let _guard = pack_cache_test_guard();
6836 clear_pack_cache();
6837 let dir = TempDir::new().unwrap();
6838 let tree = init_repo_with_pack(dir.path());
6839 let objects = dir.path().join(".git").join("objects");
6840 let got = read_object_from_packs(&objects, &tree).expect("read tree from v2 pack");
6841 assert_eq!(got.kind, ObjectKind::Tree);
6842 let again = read_object_from_packs(&objects, &tree).expect("cached read");
6844 assert_eq!(again.data, got.data);
6845 }
6846
6847 #[test]
6848 fn cached_lookup_hit_v1_idx() {
6849 let _guard = pack_cache_test_guard();
6850 clear_pack_cache();
6851 let dir = TempDir::new().unwrap();
6852 let tree = init_repo_with_pack(dir.path());
6853 let objects = dir.path().join(".git").join("objects");
6854 let v1 = convert_pack_idx_to_v1(&objects);
6855 assert!(pack_index_is_v1(&v1));
6856 let idx = read_pack_index_cached(&v1).expect("parse v1 idx");
6857 let off = idx
6858 .find_offset(&tree)
6859 .expect("fanout lookup in v1 idx")
6860 .expect("off");
6861 assert!(off > 0);
6862 let obj = read_object_from_pack(&idx, &tree).expect("read via v1 index");
6863 assert_eq!(obj.kind, ObjectKind::Tree);
6864 }
6865
6866 #[test]
6867 fn cached_lookup_miss() {
6868 let _guard = pack_cache_test_guard();
6869 clear_pack_cache();
6870 let dir = TempDir::new().unwrap();
6871 init_repo_with_pack(dir.path());
6872 let objects = dir.path().join(".git").join("objects");
6873 let miss = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
6874 assert!(read_object_from_packs(&objects, &miss).is_err());
6875 assert!(
6876 try_read_object_from_local_packs_cached(&objects, &miss, || {
6877 Err(Error::ObjectNotFound(miss.to_hex()))
6878 })
6879 .unwrap()
6880 .is_none()
6881 );
6882 }
6883
6884 #[test]
6885 fn cached_lookup_loose_only() {
6886 let _guard = pack_cache_test_guard();
6887 clear_pack_cache();
6888 let dir = TempDir::new().unwrap();
6889 git(dir.path(), &["init", "-q"]);
6890 let out = Command::new("git") .current_dir(dir.path())
6892 .args(["hash-object", "-w", "--stdin"])
6893 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6894 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6895 .stdin(std::process::Stdio::piped())
6896 .stdout(std::process::Stdio::piped())
6897 .spawn()
6898 .and_then(|mut c| {
6899 use std::io::Write;
6900 c.stdin
6901 .as_mut()
6902 .unwrap()
6903 .write_all(b"loose-only\n")
6904 .unwrap();
6905 c.wait_with_output()
6906 })
6907 .unwrap();
6908 assert!(out.status.success());
6909 let oid = ObjectId::from_hex(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
6910 let objects = dir.path().join(".git").join("objects");
6911 assert!(try_read_object_from_local_packs_cached(&objects, &oid, || {
6912 Err(Error::ObjectNotFound(oid.to_hex()))
6913 })
6914 .unwrap()
6915 .is_none());
6916 }
6917
6918 #[test]
6919 fn cached_lookup_multiple_packs() {
6920 let _guard = pack_cache_test_guard();
6921 clear_pack_cache();
6922 let dir = TempDir::new().unwrap();
6923 git(dir.path(), &["init", "-q"]);
6924 std::fs::write(dir.path().join("a.txt"), b"a").unwrap();
6925 git(dir.path(), &["add", "a.txt"]);
6926 git(dir.path(), &["commit", "-qm", "a"]);
6927 repack_all(dir.path());
6928 let oid_a = ObjectId::from_hex(
6929 String::from_utf8(
6930 Command::new("git") .current_dir(dir.path())
6932 .args(["rev-parse", "HEAD:a.txt"])
6933 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6934 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6935 .output()
6936 .unwrap()
6937 .stdout,
6938 )
6939 .unwrap()
6940 .trim(),
6941 )
6942 .unwrap();
6943 std::fs::write(dir.path().join("b.txt"), b"b").unwrap();
6944 git(dir.path(), &["add", "b.txt"]);
6945 git(dir.path(), &["commit", "-qm", "b"]);
6946 git(dir.path(), &["repack", "-d"]);
6947 let oid_b = ObjectId::from_hex(
6948 String::from_utf8(
6949 Command::new("git") .current_dir(dir.path())
6951 .args(["rev-parse", "HEAD:b.txt"])
6952 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6953 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6954 .output()
6955 .unwrap()
6956 .stdout,
6957 )
6958 .unwrap()
6959 .trim(),
6960 )
6961 .unwrap();
6962 let objects = dir.path().join(".git").join("objects");
6963 let pack_count = pack_dir(&objects)
6964 .read_dir()
6965 .unwrap()
6966 .filter_map(|e| e.ok())
6967 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
6968 .count();
6969 assert!(
6970 pack_count >= 2,
6971 "expected multiple pack files, got {pack_count}"
6972 );
6973 let obj_a = read_object_from_packs(&objects, &oid_a).expect("pack a");
6974 let obj_b = read_object_from_packs(&objects, &oid_b).expect("pack b");
6975 assert_eq!(obj_a.kind, ObjectKind::Blob);
6976 assert_eq!(obj_b.kind, ObjectKind::Blob);
6977 }
6978
6979 #[test]
6980 fn packed_delta_base_oid_uses_cached_index() {
6981 let _guard = pack_cache_test_guard();
6982 clear_pack_cache();
6983 let dir = TempDir::new().unwrap();
6984 init_repo_with_pack(dir.path());
6985 let objects = dir.path().join(".git").join("objects");
6986 let miss = ObjectId::from_hex("0101010101010101010101010101010101010101").unwrap();
6987 assert!(packed_delta_base_oid(&objects, &miss).unwrap().is_none());
6988 }
6989
6990 #[test]
6991 fn content_addressed_pack_bytes_skip_stat_revalidation() {
6992 let _guard = pack_cache_test_guard();
6993 clear_pack_cache();
6994 let dir = tempfile::tempdir().expect("tempdir");
6995 let pack_path = dir
6996 .path()
6997 .join("pack-1111111111111111111111111111111111111111.pack");
6998 let v1 = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
6999 std::fs::write(&pack_path, v1).expect("write v1");
7000 let cached = read_pack_bytes_cached(&pack_path).expect("prime cache");
7001 assert_eq!(&cached[..], v1);
7002
7003 let v2 = b"PACK-replaced-by-repack";
7004 std::fs::write(&pack_path, v2).expect("overwrite on disk");
7005 let again = read_pack_bytes_cached(&pack_path).expect("cached without stat");
7006 assert_eq!(
7007 &again[..],
7008 v1,
7009 "content-addressed name pins cached bytes in-process"
7010 );
7011 }
7012
7013 #[test]
7014 fn temporary_pack_bytes_served_from_cache_until_cleared() {
7015 let _guard = pack_cache_test_guard();
7016 clear_pack_cache();
7017 let dir = tempfile::tempdir().expect("tempdir");
7018 for pack_path in [
7019 dir.path().join("tmp_pack_abc123.pack"),
7020 dir.path().join("pack-custom.pack"),
7021 ] {
7022 clear_pack_cache();
7023 let v1 = b"PACK-temp-v1";
7024 std::fs::write(&pack_path, v1).expect("write v1");
7025 let _ = read_pack_bytes_cached(&pack_path).expect("prime cache");
7026
7027 let v2 = b"PACK-temp-v2-longer-body";
7028 std::fs::write(&pack_path, v2).expect("overwrite pack");
7029 let stale = read_pack_bytes_cached(&pack_path).expect("cache hit");
7030 assert_eq!(
7031 &stale[..],
7032 v1,
7033 "{} serves cached bytes without stat on hit",
7034 pack_path.display()
7035 );
7036 clear_pack_cache();
7037 let fresh = read_pack_bytes_cached(&pack_path).expect("after clear");
7038 assert_eq!(
7039 &fresh[..],
7040 v2,
7041 "{} reloads from disk after clear",
7042 pack_path.display()
7043 );
7044 }
7045 }
7046 #[test]
7047 fn system_git_repack_requires_clear_pack_cache_for_fresh_listing() {
7048 let _guard = pack_cache_test_guard();
7049 clear_pack_cache();
7050 let dir = TempDir::new().unwrap();
7051 init_repo_with_pack(dir.path());
7052 let objects = dir.path().join(".git").join("objects");
7053 read_local_pack_indexes_cached(&objects).expect("warm listing");
7054 for entry in pack_dir(&objects).read_dir().unwrap().flatten() {
7055 let path = entry.path();
7056 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
7057 fs::remove_file(path).unwrap();
7058 }
7059 }
7060 clear_pack_cache();
7061 let indexes =
7062 read_local_pack_indexes_cached(&objects).expect("list after repack-like delete");
7063 assert!(
7064 indexes.is_empty(),
7065 "callers must clear_pack_cache after removing pack files"
7066 );
7067 }
7068
7069 fn test_oid(n: u8) -> ObjectId {
7070 ObjectId::from_hex(&format!("{n:040x}")).unwrap()
7071 }
7072
7073 fn max_delta_map_chain_edges(map: &HashMap<ObjectId, ObjectId>) -> usize {
7074 let value_set: HashSet<ObjectId> = map.values().copied().collect();
7075 let tips: Vec<ObjectId> = map
7076 .keys()
7077 .copied()
7078 .filter(|k| !value_set.contains(k))
7079 .collect();
7080 let mut max = 0usize;
7081 for tip in tips {
7082 let mut edges = 0usize;
7083 let mut cur = tip;
7084 let mut seen = HashSet::new();
7085 while seen.insert(cur) {
7086 let Some(&b) = map.get(&cur) else {
7087 break;
7088 };
7089 edges += 1;
7090 cur = b;
7091 }
7092 max = max.max(edges);
7093 }
7094 max
7095 }
7096
7097 #[test]
7098 fn apply_delta_depth_limit_hits_exact_cap_on_seventy_edge_chain() {
7099 let n = 70usize;
7100 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7101 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7102 for i in 1..n {
7103 map.insert(oids[i], oids[i - 1]);
7104 }
7105 let original = map.clone();
7106 apply_delta_depth_limit(&mut map, 50);
7107 assert_eq!(max_delta_map_chain_edges(&map), 50);
7108 let preserved = map
7109 .iter()
7110 .filter(|(t, b)| original.get(t) == Some(b))
7111 .count();
7112 assert!(
7113 preserved > 40,
7114 "non-cut edges should keep original bases (preserved {preserved})"
7115 );
7116 }
7117
7118 #[test]
7119 fn apply_delta_depth_limit_leaves_short_chains_untouched() {
7120 let a = test_oid(1);
7121 let b = test_oid(2);
7122 let c = test_oid(3);
7123 let mut map = HashMap::from([(b, a), (c, b)]);
7124 apply_delta_depth_limit(&mut map, 50);
7125 assert_eq!(map.get(&b), Some(&a));
7126 assert_eq!(map.get(&c), Some(&b));
7127 assert_eq!(max_delta_map_chain_edges(&map), 2);
7128 }
7129
7130 #[test]
7131 fn apply_delta_depth_limit_caps_long_chain_and_preserves_blobs() {
7132 let n = 120usize;
7133 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7134 let mut blobs: HashMap<ObjectId, Vec<u8>> = HashMap::new();
7135 blobs.insert(oids[0], b"base".to_vec());
7136 for i in 1..n {
7137 let mut next = blobs[&oids[i - 1]].clone();
7138 next.push(i as u8);
7139 blobs.insert(oids[i], next);
7140 }
7141 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7142 for i in 1..n {
7143 map.insert(oids[i], oids[i - 1]);
7144 }
7145 let original = map.clone();
7146 apply_delta_depth_limit(&mut map, 50);
7147 assert_eq!(
7148 max_delta_map_chain_edges(&map),
7149 50,
7150 "long chain must cap at exactly max_depth edges"
7151 );
7152 let preserved = map
7153 .iter()
7154 .filter(|(target, base)| original.get(target) == Some(base))
7155 .count();
7156 assert!(
7157 preserved > 60,
7158 "segment links should stay on original bases (preserved {preserved} of {})",
7159 map.len()
7160 );
7161
7162 use crate::delta_encode::encode_prefix_extension_delta;
7163 use crate::unpack_objects::apply_delta;
7164
7165 for (&target, &base) in &map {
7166 let delta = encode_prefix_extension_delta(&blobs[&base], &blobs[&target]).unwrap();
7167 let got = apply_delta(&blobs[&base], &delta).unwrap();
7168 assert_eq!(got, blobs[&target]);
7169 }
7170 }
7171
7172 #[test]
7173 fn v2_idx_crc_matches_crc32fast_over_entry_bytes() {
7174 let _guard = pack_cache_test_guard();
7175 clear_pack_cache();
7176 let dir = TempDir::new().unwrap();
7177 let tree = init_repo_with_pack(dir.path());
7178 let objects = dir.path().join(".git").join("objects");
7179 let idx_path = v2_idx_path(&objects);
7180 let idx = read_pack_index(&idx_path).expect("read idx");
7181 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
7182 for entry in idx.iter() {
7183 let crc = entry.crc32().expect("v2 idx entry crc");
7184 let off = entry.offset().expect("entry off");
7185 let start = off as usize;
7186 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7187 .expect("entry end")
7188 .expect("end");
7189 assert_eq!(crc32fast::hash(&pack_bytes[start..end]), crc);
7190 }
7191 let _ = tree;
7192 }
7193
7194 #[test]
7195 fn packed_full_object_slice_returns_on_disk_bytes() {
7196 let _guard = pack_cache_test_guard();
7197 clear_pack_cache();
7198 let dir = TempDir::new().unwrap();
7199 let tree = init_repo_with_pack(dir.path());
7200 let objects = dir.path().join(".git").join("objects");
7201 let idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7202 let pack_bytes = std::fs::read(&idx.pack_path).expect("pack");
7203 let off = idx.find_offset(&tree).expect("tree in pack").expect("off");
7204 let start = off as usize;
7205 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7206 .expect("entry end")
7207 .expect("end");
7208 let expected = &pack_bytes[start..end];
7209 let slice = packed_full_object_slice(&objects, &tree)
7210 .expect("lookup")
7211 .expect("full tree slice");
7212 assert_eq!(slice, expected);
7213 }
7214
7215 fn touch_mtime(path: &Path, when: filetime::FileTime) {
7216 filetime::set_file_mtime(path, when).expect("set mtime");
7217 }
7218
7219 #[test]
7220 fn packed_full_object_slice_crc_mismatch_falls_back_to_redundant_pack() {
7221 let _guard = pack_cache_test_guard();
7222 clear_pack_cache();
7223 let dir = TempDir::new().unwrap();
7224 let tree = init_repo_with_pack(dir.path());
7225 let objects = dir.path().join(".git").join("objects");
7226 let pack_dir = pack_dir(&objects);
7227 let primary_idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7228 let primary_pack = primary_idx.pack_path.clone();
7229 let primary_idx_path = primary_idx.idx_path.clone();
7230
7231 let redundant_pack = pack_dir.join("pack-redundant.pack");
7232 let redundant_idx = pack_dir.join("pack-redundant.idx");
7233 fs::copy(&primary_pack, &redundant_pack).expect("copy pack");
7234 fs::copy(&primary_idx_path, &redundant_idx).expect("copy idx");
7235
7236 let older = filetime::FileTime::from_unix_time(1_700_000_000, 0);
7237 let newer = filetime::FileTime::from_unix_time(1_700_000_100, 0);
7238 touch_mtime(&primary_pack, older);
7239 touch_mtime(&primary_idx_path, older);
7240 touch_mtime(&redundant_pack, newer);
7241 touch_mtime(&redundant_idx, newer);
7242 clear_pack_cache();
7243
7244 let off = primary_idx
7245 .find_offset(&tree)
7246 .expect("tree offset")
7247 .expect("off");
7248 let start = off as usize;
7249 let end = super::pack_entry_raw_end(&primary_idx, &fs::read(&primary_pack).unwrap(), off)
7250 .expect("entry end")
7251 .expect("end");
7252 let flip = start + (end - start) / 2;
7253 let mut corrupt = fs::read(&redundant_pack).expect("read redundant");
7254 corrupt[flip] ^= 0x01;
7255 rewrite_test_file(&redundant_pack, &corrupt);
7256 clear_pack_cache();
7257
7258 let recorded_crc = read_pack_index(&redundant_idx)
7259 .expect("redundant idx")
7260 .crc32_for_pack_offset(off)
7261 .expect("crc")
7262 .expect("crc value");
7263 let corrupt_slice = &corrupt[start..end];
7264 assert_ne!(
7265 crc32fast::hash(corrupt_slice),
7266 recorded_crc,
7267 "test must flip bytes without updating idx CRC"
7268 );
7269 assert!(
7270 packed_full_object_slice(&objects, &tree)
7271 .expect("lookup")
7272 .is_some(),
7273 "valid redundant pack should serve the slice after CRC rejects corrupt copy"
7274 );
7275
7276 let obj = read_object_from_packs(&objects, &tree).expect("read tree");
7277 assert_eq!(obj.kind, ObjectKind::Tree);
7278 let slice = packed_full_object_slice(&objects, &tree)
7279 .expect("lookup")
7280 .expect("slice");
7281 let good_pack = fs::read(&primary_pack).expect("primary pack");
7282 assert_eq!(&slice, &good_pack[start..end]);
7283 }
7284}