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