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