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) entry_offset: u64,
5210 pub(crate) entry_end: usize,
5212 pub(crate) zlib_offset: usize,
5214 pub(crate) data_end: usize,
5216 pub(crate) kind: ObjectKind,
5218 pub(crate) size: u64,
5220 pub(crate) index_crc: Option<u32>,
5222}
5223
5224pub(crate) fn count_local_packs_holding(objects_dir: &Path, oid: &ObjectId) -> Result<usize> {
5226 Ok(packs_holding(objects_dir, oid, PackPreference::Newest)?.len())
5227}
5228
5229pub(crate) enum PackedEntry {
5231 Whole(PackedWholeObject),
5233 NotWhole,
5236 Absent,
5238}
5239
5240pub(crate) fn locate_packed_whole_object(
5252 objects_dir: &Path,
5253 oid: &ObjectId,
5254 verify_crc: bool,
5255) -> Result<PackedEntry> {
5256 let indexes = read_local_pack_indexes_cached(objects_dir)?;
5257 let mut found = PackedEntry::Absent;
5258 for idx in &indexes {
5259 let Some(entry_offset) = idx.find_offset(oid)? else {
5260 continue;
5261 };
5262 found = PackedEntry::NotWhole;
5263 let pack = read_pack_bytes_cached(&idx.pack_path)?;
5264 let Some(data_end) = pack.len().checked_sub(idx.hash_bytes()) else {
5265 continue;
5266 };
5267 let Ok(mut pos) = usize::try_from(entry_offset) else {
5268 continue;
5269 };
5270 if pos >= data_end {
5271 continue;
5272 }
5273 let Ok((packed_type, size)) = parse_pack_object_header(&pack[..data_end], &mut pos) else {
5274 continue;
5275 };
5276 let Ok(kind) = packed_type_to_kind(packed_type) else {
5277 continue;
5278 };
5279 if verify_crc && !packed_entry_matches_index_crc(idx, &pack, entry_offset)? {
5280 continue;
5281 }
5282 let entry_end = pack_entry_raw_end(idx, &pack, entry_offset)?.unwrap_or(data_end);
5283 let index_crc = if verify_crc {
5284 None
5285 } else {
5286 idx.crc32_for_pack_offset(entry_offset).ok().flatten()
5287 };
5288 return Ok(PackedEntry::Whole(PackedWholeObject {
5289 pack,
5290 entry_offset,
5291 entry_end,
5292 zlib_offset: pos,
5293 data_end,
5294 kind,
5295 size,
5296 index_crc,
5297 }));
5298 }
5299 Ok(found)
5300}
5301
5302pub(crate) fn packed_entry_matches_index_crc(
5311 idx: &PackIndex,
5312 pack: &PackData,
5313 entry_offset: u64,
5314) -> Result<bool> {
5315 let Some(recorded) = idx.crc32_for_pack_offset(entry_offset)? else {
5316 return Ok(true);
5317 };
5318 let Some(end) = pack_entry_raw_end(idx, pack, entry_offset)? else {
5319 return Ok(false);
5320 };
5321 let start = usize::try_from(entry_offset)
5322 .map_err(|_| Error::CorruptObject(format!("pack offset {entry_offset} overflows")))?;
5323 if start >= end {
5324 return Ok(false);
5325 }
5326 let budget = ResidentBudget::new(pack, VERIFY_RESIDENT_PACK_BYTES);
5327 Ok(budget.crc32(&pack[start..end]) == recorded)
5328}
5329
5330const VERIFY_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
5333
5334pub fn packed_full_object_slice(objects_dir: &Path, oid: &ObjectId) -> Result<Option<Vec<u8>>> {
5353 Ok(packed_full_object_range(objects_dir, oid)?
5354 .map(|(bytes, start, end)| bytes[start..end].to_vec()))
5355}
5356
5357pub(crate) fn packed_full_object_range(
5360 objects_dir: &Path,
5361 oid: &ObjectId,
5362) -> Result<Option<(Arc<PackData>, usize, usize)>> {
5363 for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
5364 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5365 let start = entry_offset as usize;
5366 let mut p = start;
5367 let Ok((packed_type, _size)) = parse_pack_object_header(&pack_bytes, &mut p) else {
5368 continue;
5369 };
5370 if matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta) {
5371 continue;
5373 }
5374 let Some(end) = pack_entry_raw_end(&idx, &pack_bytes, entry_offset)? else {
5375 continue;
5376 };
5377 if end <= start {
5378 continue;
5379 }
5380 if !packed_entry_matches_index_crc(&idx, &pack_bytes, entry_offset)? {
5384 continue;
5385 }
5386 return Ok(Some((pack_bytes, start, end)));
5387 }
5388 Ok(None)
5389}
5390
5391pub(crate) fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
5392 let first = *bytes.get(*pos).ok_or_else(|| {
5393 Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
5394 })?;
5395 *pos += 1;
5396
5397 let type_code = (first >> 4) & 0x7;
5398 let mut size = (first & 0x0f) as u64;
5399 let mut shift = 4u32;
5400 let mut c = first;
5401 while (c & 0x80) != 0 {
5402 c = *bytes.get(*pos).ok_or_else(|| {
5403 Error::CorruptObject("unexpected end of variable size header".to_owned())
5404 })?;
5405 *pos += 1;
5406 size |= ((c & 0x7f) as u64) << shift;
5407 shift += 7;
5408 }
5409
5410 let packed_type = match type_code {
5411 1 => PackedType::Commit,
5412 2 => PackedType::Tree,
5413 3 => PackedType::Blob,
5414 4 => PackedType::Tag,
5415 6 => PackedType::OfsDelta,
5416 7 => PackedType::RefDelta,
5417 _ => {
5418 return Err(Error::CorruptObject(format!(
5419 "unsupported packed object type {}",
5420 type_code
5421 )))
5422 }
5423 };
5424 Ok((packed_type, size))
5425}
5426
5427#[derive(Debug, Clone, Copy)]
5429pub enum PackedDeltaDependency {
5430 OfsBase {
5432 base_offset: u64,
5434 },
5435 RefBase {
5437 base_oid: ObjectId,
5439 },
5440}
5441
5442pub fn read_packed_delta_dependency(
5446 pack_bytes: &[u8],
5447 object_offset: u64,
5448 hash_bytes: usize,
5449) -> Result<Option<PackedDeltaDependency>> {
5450 let mut pos = object_offset as usize;
5451 let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
5452 match ty {
5453 PackedType::OfsDelta => {
5454 let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
5455 Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
5456 }
5457 PackedType::RefDelta => {
5458 if pos + hash_bytes > pack_bytes.len() {
5459 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5460 }
5461 let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + hash_bytes])?;
5462 Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
5463 }
5464 _ => Ok(None),
5465 }
5466}
5467
5468pub(crate) fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
5469 let mut c = *bytes
5470 .get(*pos)
5471 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5472 *pos += 1;
5473 let mut value = (c & 0x7f) as u64;
5474 while (c & 0x80) != 0 {
5475 c = *bytes
5476 .get(*pos)
5477 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5478 *pos += 1;
5479 value = ((value + 1) << 7) | (c & 0x7f) as u64;
5480 }
5481 this_offset
5482 .checked_sub(value)
5483 .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
5484}
5485
5486pub fn slice_one_pack_object(
5494 bytes: &[u8],
5495 object_start_offset: u64,
5496 hash_bytes: usize,
5497) -> Result<&[u8]> {
5498 let start = object_start_offset as usize;
5499 let mut pos = start;
5500 skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
5501 Ok(&bytes[start..pos])
5502}
5503
5504pub fn skip_one_pack_object(
5506 bytes: &[u8],
5507 pos: &mut usize,
5508 object_start_offset: u64,
5509 hash_bytes: usize,
5510) -> Result<()> {
5511 let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
5512 let mut scratch = ZlibInflateScratch::default();
5513 match packed_type {
5514 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
5515 scratch.skip_zlib_stream(bytes, pos, size)?;
5516 }
5517 PackedType::RefDelta => {
5518 if *pos + hash_bytes > bytes.len() {
5519 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5520 }
5521 *pos += hash_bytes;
5522 scratch.skip_zlib_stream(bytes, pos, size)?;
5523 }
5524 PackedType::OfsDelta => {
5525 let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
5526 scratch.skip_zlib_stream(bytes, pos, size)?;
5527 }
5528 }
5529 Ok(())
5530}
5531
5532pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
5534 let index = read_pack_index(idx_path)?;
5535 let mut out = Vec::new();
5536 let hash_bytes = index.hash_bytes();
5537 for e in index.iter() {
5538 if e.oid().len() == hash_bytes {
5539 out.push(ObjectId::from_bytes(e.oid())?);
5540 }
5541 }
5542 Ok(out)
5543}
5544
5545mod pack_cache_test_sync {
5546 use std::cell::Cell;
5547 use std::sync::Mutex;
5548 use std::sync::MutexGuard;
5549
5550 struct PackCacheTestCoordinator {
5552 top_level_guards: u32,
5553 }
5554
5555 static COORD: Mutex<PackCacheTestCoordinator> = Mutex::new(PackCacheTestCoordinator {
5557 top_level_guards: 0,
5558 });
5559
5560 thread_local! {
5562 static DEPTH: Cell<u32> = const { Cell::new(0) };
5563 }
5564
5565 pub struct PackCacheTestGuard {
5567 _inner: Option<MutexGuard<'static, PackCacheTestCoordinator>>,
5568 }
5569
5570 pub fn acquire() -> PackCacheTestGuard {
5571 let depth = DEPTH.with(|d| {
5572 let n = d.get();
5573 d.set(n + 1);
5574 n
5575 });
5576 PackCacheTestGuard {
5577 _inner: if depth == 0 {
5578 let mut g = COORD.lock().unwrap_or_else(|e| e.into_inner());
5579 assert_eq!(
5580 g.top_level_guards, 0,
5581 "pack cache test coordinator already held"
5582 );
5583 g.top_level_guards = 1;
5584 Some(g)
5585 } else {
5586 None
5587 },
5588 }
5589 }
5590
5591 impl Drop for PackCacheTestGuard {
5592 fn drop(&mut self) {
5593 DEPTH.with(|d| {
5594 let n = d.get();
5595 assert!(n > 0, "pack cache test guard depth underflow");
5596 d.set(n - 1);
5597 if n == 1 {
5598 if let Some(mut g) = self._inner.take() {
5599 assert_eq!(g.top_level_guards, 1);
5600 g.top_level_guards = 0;
5601 }
5602 }
5603 });
5604 }
5605 }
5606}
5607
5608#[cfg(test)]
5609pub(crate) fn pack_cache_test_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5610 pack_cache_test_sync::acquire()
5611}
5612
5613pub fn test_pack_cache_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5615 pack_cache_test_sync::acquire()
5616}
5617
5618#[cfg(test)]
5619mod tests {
5620 use super::*;
5621 use crate::delta_encode::encode_lcp_delta;
5622 use crate::odb::Odb;
5623 use crate::pack_map::PackData;
5624 use crate::transfer::{build_pack, PackBuildOptions};
5625 use flate2::write::ZlibEncoder;
5626 use flate2::Compression;
5627 use std::io::Write;
5628 use std::process::Command;
5629
5630 fn git_try(dir: &std::path::Path, args: &[&str]) -> bool {
5631 Command::new("git") .current_dir(dir)
5633 .args(args)
5634 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5635 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5636 .output()
5637 .map(|o| o.status.success())
5638 .unwrap_or(false)
5639 }
5640
5641 fn git(dir: &std::path::Path, args: &[&str]) {
5642 let out = Command::new("git") .current_dir(dir)
5644 .args(args)
5645 .env("GIT_AUTHOR_NAME", "T")
5646 .env("GIT_AUTHOR_EMAIL", "t@example.com")
5647 .env("GIT_COMMITTER_NAME", "T")
5648 .env("GIT_COMMITTER_EMAIL", "t@example.com")
5649 .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
5650 .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
5651 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5652 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5653 .output()
5654 .expect("git");
5655 assert!(
5656 out.status.success(),
5657 "git {:?}: {}",
5658 args,
5659 String::from_utf8_lossy(&out.stderr)
5660 );
5661 }
5662
5663 fn append_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
5664 let mut size = payload_len;
5665 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
5666 size >>= 4;
5667 if size > 0 {
5668 buf.push(first | 0x80);
5669 while size > 0 {
5670 let b = (size & 0x7f) as u8;
5671 size >>= 7;
5672 buf.push(if size > 0 { b | 0x80 } else { b });
5673 }
5674 } else {
5675 buf.push(first);
5676 }
5677 }
5678
5679 fn zlib_pack(payload: &[u8]) -> Vec<u8> {
5680 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5681 enc.write_all(payload).unwrap();
5682 enc.finish().unwrap()
5683 }
5684
5685 fn append_sha1_pack_trailer(buf: &mut Vec<u8>) {
5686 buf.extend_from_slice(HashAlgo::Sha1.checksum_digest(&*buf).as_slice());
5687 }
5688
5689 fn append_ref_delta(buf: &mut Vec<u8>, base_oid: &ObjectId, delta: &[u8]) {
5690 let compressed = zlib_pack(delta);
5691 append_pack_object_header(buf, 7, delta.len());
5692 buf.extend_from_slice(base_oid.as_bytes());
5693 buf.extend_from_slice(&compressed);
5694 }
5695
5696 fn append_ofs_delta(buf: &mut Vec<u8>, base_offset: u64, delta: &[u8]) -> u64 {
5697 let object_start = buf.len() as u64;
5698 let compressed = zlib_pack(delta);
5699 append_pack_object_header(buf, 6, delta.len());
5700 let mut ofs = object_start - base_offset;
5701 while ofs >= 0x80 {
5702 buf.push((ofs as u8 & 0x7f) | 0x80);
5703 ofs >>= 7;
5704 }
5705 buf.push(ofs as u8);
5706 buf.extend_from_slice(&compressed);
5707 object_start
5708 }
5709
5710 fn append_whole_blob(buf: &mut Vec<u8>, data: &[u8]) -> u64 {
5711 let off = buf.len() as u64;
5712 let compressed = zlib_pack(data);
5713 append_pack_object_header(buf, 3, data.len());
5714 buf.extend_from_slice(&compressed);
5715 off
5716 }
5717
5718 fn append_corrupt_whole_blob(buf: &mut Vec<u8>) -> u64 {
5719 let off = buf.len() as u64;
5720 append_pack_object_header(buf, 3, 8);
5721 buf.extend_from_slice(&[0xff, 0xfe, 0xfd]);
5722 off
5723 }
5724
5725 fn write_v2_idx_for_test(
5726 idx_path: &Path,
5727 pack_path: &Path,
5728 entries: &[(ObjectId, u64)],
5729 ) -> Result<()> {
5730 let pack_bytes = std::fs::read(pack_path)?;
5731 let mut with_crc = Vec::with_capacity(entries.len());
5732 for (oid, off) in entries {
5733 let start = *off as usize;
5734 let mut end = start;
5735 skip_one_pack_object(&pack_bytes, &mut end, *off, 20)
5736 .map_err(|e| Error::CorruptObject(format!("idx crc walk: {e}")))?;
5737 with_crc.push((*oid, *off, crc32fast::hash(&pack_bytes[start..end])));
5738 }
5739 write_v2_pack_index(idx_path, pack_path, &with_crc, 20)
5740 }
5741
5742 fn single_blob_pack(data: &[u8]) -> (Vec<u8>, ObjectId, u64) {
5743 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
5744 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5745 let mut pack = Vec::new();
5746 pack.extend_from_slice(b"PACK");
5747 pack.extend_from_slice(&2u32.to_be_bytes());
5748 pack.extend_from_slice(&1u32.to_be_bytes());
5749 let off = append_whole_blob(&mut pack, data);
5750 append_sha1_pack_trailer(&mut pack);
5751 (pack, oid, off)
5752 }
5753
5754 #[test]
5755 fn single_local_pack_reader_batch_check_caches_deltified_entries() {
5756 let _guard = pack_cache_test_guard();
5757 clear_pack_cache();
5758 let Some((tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(2) else {
5759 return;
5760 };
5761 let objects = tmp.path().join("objects");
5762 let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
5763 crate::pack_store::PackStore::with_context(store, || {
5764 let mut reader = SingleLocalPackReader::open(&objects)
5765 .expect("open")
5766 .expect("single pack");
5767 let info1 = reader.read_info(&tip).expect("first read");
5768 let info2 = reader.read_info(&tip).expect("cached read");
5769 assert_eq!(info1, info2);
5770 let full = read_object_from_pack(&idx, &tip).expect("full read");
5771 assert_eq!(info1.kind, full.kind);
5772 assert_eq!(info1.size, full.data.len() as u64);
5773 });
5774 let _ = odb;
5775 }
5776
5777 #[test]
5778 fn pack_read_info_cache_is_scoped_per_repository_and_pack_bytes() {
5779 let _guard = pack_cache_test_guard();
5780 clear_pack_cache();
5781 const ENTRY_OFFSET: u64 = 200;
5782 let dir_a = tempfile::tempdir().expect("tempdir a");
5783 let dir_b = tempfile::tempdir().expect("tempdir b");
5784 let objects_a = dir_a.path().join("objects");
5785 let objects_b = dir_b.path().join("objects");
5786 fs::create_dir_all(objects_a.join("pack")).expect("pack dir a");
5787 fs::create_dir_all(objects_b.join("pack")).expect("pack dir b");
5788 let pack_a = objects_a.join("pack/p-a.pack");
5789 let pack_b = objects_b.join("pack/p-b.pack");
5790 let (oid_a, _) =
5791 write_sparse_single_blob_pack(dir_a.path(), &pack_a, b"five!", ENTRY_OFFSET)
5792 .expect("pack a");
5793 let (oid_b, _) =
5794 write_sparse_single_blob_pack(dir_b.path(), &pack_b, b"1234567890", ENTRY_OFFSET)
5795 .expect("pack b");
5796 let idx_a = pack_a.with_extension("idx");
5797 let idx_b = pack_b.with_extension("idx");
5798 write_v2_idx_for_test(&idx_a, &pack_a, &[(oid_a, ENTRY_OFFSET)]).expect("idx a");
5799 write_v2_idx_for_test(&idx_b, &pack_b, &[(oid_b, ENTRY_OFFSET)]).expect("idx b");
5800 let idx_a = read_pack_index(&idx_a).expect("parse idx a");
5801 let idx_b = read_pack_index(&idx_b).expect("parse idx b");
5802
5803 let read_size = |objects: &Path, idx: &PackIndex| -> u64 {
5804 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects);
5805 crate::pack_store::PackStore::with_context(store, || {
5806 super::read_object_info_from_pack_at_offset(idx, ENTRY_OFFSET)
5807 .expect("read_info")
5808 .size
5809 })
5810 };
5811
5812 let size_a = read_size(&objects_a, &idx_a);
5813 let size_b = read_size(&objects_b, &idx_b);
5814 assert_eq!(size_a, 5);
5815 assert_eq!(size_b, 10);
5816 assert_eq!(read_size(&objects_a, &idx_a), 5);
5818 assert_eq!(read_size(&objects_b, &idx_b), 10);
5819 assert_eq!(read_size(&objects_a, &idx_a), 5);
5820 }
5821 fn write_sparse_single_blob_pack(
5824 repo_root: &Path,
5825 pack_path: &Path,
5826 data: &[u8],
5827 object_offset: u64,
5828 ) -> Result<(ObjectId, u64)> {
5829 use std::fs::OpenOptions;
5830 use std::io::{Seek, SeekFrom, Write};
5831
5832 let odb = Odb::new(repo_root);
5833 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5834 let mut file = OpenOptions::new()
5835 .create(true)
5836 .write(true)
5837 .read(true)
5838 .truncate(true)
5839 .open(pack_path)
5840 .map_err(Error::Io)?;
5841 file.write_all(b"PACK").map_err(Error::Io)?;
5842 file.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
5843 file.write_all(&1u32.to_be_bytes()).map_err(Error::Io)?;
5844 file.seek(SeekFrom::Start(object_offset))
5845 .map_err(Error::Io)?;
5846 let mut tail = Vec::new();
5847 append_whole_blob(&mut tail, data);
5848 file.write_all(&tail).map_err(Error::Io)?;
5849 let trailer = sha1_trailer_for_open_file(&mut file)?;
5850 file.write_all(trailer.as_bytes()).map_err(Error::Io)?;
5851 file.flush().map_err(Error::Io)?;
5852 Ok((oid, object_offset))
5853 }
5854
5855 #[test]
5856 fn skip_one_pack_object_rejects_zero_size_with_zlib_payload() {
5857 let mut bytes = Vec::new();
5858 bytes.push(0x30);
5859 bytes.extend_from_slice(&zlib_pack(b"x"));
5860 let mut pos = 0usize;
5861 let err = skip_one_pack_object(&bytes, &mut pos, 0, 20).unwrap_err();
5862 assert!(matches!(err, Error::CorruptObject(_)), "got {err:?}");
5863 }
5864
5865 fn install_synthetic_pack(
5866 objects_dir: &Path,
5867 stem: &str,
5868 pack: &[u8],
5869 entries: &[(ObjectId, u64)],
5870 ) -> PackIndex {
5871 install_synthetic_pack_inner(objects_dir, stem, pack, entries, true)
5872 }
5873
5874 fn install_synthetic_pack_no_clear(
5875 objects_dir: &Path,
5876 stem: &str,
5877 pack: &[u8],
5878 entries: &[(ObjectId, u64)],
5879 ) -> PackIndex {
5880 install_synthetic_pack_inner(objects_dir, stem, pack, entries, false)
5881 }
5882
5883 fn install_synthetic_pack_inner(
5884 objects_dir: &Path,
5885 stem: &str,
5886 pack: &[u8],
5887 entries: &[(ObjectId, u64)],
5888 clear_cache: bool,
5889 ) -> PackIndex {
5890 let pack_dir = objects_dir.join("pack");
5891 std::fs::create_dir_all(&pack_dir).expect("pack dir");
5892 let pack_path = pack_dir.join(format!("{stem}.pack"));
5893 let idx_path = pack_dir.join(format!("{stem}.idx"));
5894 std::fs::write(&pack_path, pack).expect("write pack");
5895 write_v2_idx_for_test(&idx_path, &pack_path, entries).expect("write idx");
5896 if clear_cache {
5897 clear_pack_cache();
5898 }
5899 read_pack_index(&idx_path).expect("read idx")
5900 }
5901
5902 fn pack_index_for_objects(
5903 pack_path: std::path::PathBuf,
5904 objects: &[(ObjectId, u64)],
5905 ) -> PackIndex {
5906 let idx_path = pack_path.with_extension("idx");
5907 let pack_bytes = fs::read(&pack_path).expect("read pack for idx trailer");
5908 let trailer = &pack_bytes[pack_bytes.len().saturating_sub(20)..];
5909 let rows: Vec<(ObjectId, u64, u32)> = objects
5910 .iter()
5911 .map(|(oid, offset)| (*oid, *offset, 0))
5912 .collect();
5913 write_v2_pack_index_with_trailer(&idx_path, &rows, trailer, 20, None).expect("write idx");
5914 read_pack_index(&idx_path).expect("read idx")
5915 }
5916
5917 fn index_pack_in_scratch(pack: &[u8]) -> (tempfile::TempDir, PackIndex) {
5918 let dir = tempfile::tempdir().expect("tempdir");
5919 let pack_path = dir.path().join("chain.pack");
5920 std::fs::write(&pack_path, pack).expect("write pack");
5921 let out = Command::new("git") .current_dir(dir.path())
5923 .args(["index-pack", "chain.pack"])
5924 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5925 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5926 .output()
5927 .expect("index-pack");
5928 assert!(
5929 out.status.success(),
5930 "git index-pack failed: {}",
5931 String::from_utf8_lossy(&out.stderr)
5932 );
5933 let idx_path = dir.path().join("chain.idx");
5934 let idx = read_pack_index(&idx_path).expect("read idx");
5935 (dir, idx)
5936 }
5937
5938 fn build_deep_delta_repo(commit_count: usize) -> Option<(tempfile::TempDir, ObjectId, Odb)> {
5940 let tmp = tempfile::tempdir().ok()?;
5941 let dir = tmp.path();
5942 if !git_try(dir, &["init", "-q", "-b", "main", "."]) {
5943 return None;
5944 }
5945 let mut body = String::new();
5946 for i in 0..200 {
5947 body.push_str(&format!("seed line {i:04}\n"));
5948 }
5949 for rev in 0..commit_count {
5950 body.push_str(&format!("edit-{rev}\n"));
5951 std::fs::write(dir.join("blob.txt"), body.as_bytes()).ok()?;
5952 git(dir, &["add", "blob.txt"]);
5953 git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
5954 }
5955 let tip_hex = Command::new("git") .current_dir(dir)
5957 .args(["rev-parse", "HEAD"])
5958 .env("GIT_CONFIG_GLOBAL", "/dev/null")
5959 .env("GIT_CONFIG_SYSTEM", "/dev/null")
5960 .output()
5961 .ok()?;
5962 if !tip_hex.status.success() {
5963 return None;
5964 }
5965 let tip = ObjectId::from_hex(std::str::from_utf8(&tip_hex.stdout).ok()?.trim()).ok()?;
5966 let git_dir = dir.join(".git");
5967 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
5968 Some((tmp, tip, odb))
5969 }
5970
5971 #[test]
5972 fn verify_packed_object_hash_sha256_detects_corruption() {
5973 use crate::hash::hash_object;
5974 use std::fs;
5975
5976 let data = b"sha256 packed blob for hash verify\n";
5977 let oid = hash_object(HashAlgo::Sha256, ObjectKind::Blob, data).expect("hash");
5978 let wrong_oid =
5979 hash_object(HashAlgo::Sha256, ObjectKind::Blob, b"different payload\n").expect("hash");
5980 let mut pack = Vec::new();
5981 pack.extend_from_slice(b"PACK");
5982 pack.extend_from_slice(&2u32.to_be_bytes());
5983 pack.extend_from_slice(&1u32.to_be_bytes());
5984 let off = append_whole_blob(&mut pack, data);
5985 pack.extend_from_slice(HashAlgo::Sha256.checksum_digest(&pack).as_slice());
5986
5987 let dir = tempfile::tempdir().expect("tempdir");
5988 let pack_path = dir.path().join("sha256-corrupt.pack");
5989 fs::write(&pack_path, &pack).expect("write pack");
5990
5991 let idx_path = pack_path.with_extension("idx");
5992 let trailer = &pack[pack.len() - 32..];
5993 write_v2_pack_index_with_trailer(&idx_path, &[(wrong_oid, off, 0)], trailer, 32, None)
5994 .expect("write idx");
5995 let idx = read_pack_index(&idx_path).expect("read idx");
5996
5997 let err = read_object_from_pack_bytes(&pack, &idx, wrong_oid.as_bytes()).unwrap_err();
5998 match err {
5999 Error::CorruptObject(msg) => {
6000 assert!(
6001 msg.contains("hashes to"),
6002 "expected hash mismatch message, got: {msg}"
6003 );
6004 assert!(
6005 msg.contains(&oid.to_hex()),
6006 "expected recomputed oid in message, got: {msg}"
6007 );
6008 }
6009 other => panic!("expected CorruptObject, got {other:?}"),
6010 }
6011 }
6012
6013 #[cfg_attr(
6014 not(feature = "full-tests"),
6015 ignore = "starts system git or is heavy; run with --features full-tests"
6016 )]
6017 #[test]
6018 fn reads_delta_chain_deeper_than_50() {
6019 let _guard = pack_cache_test_guard();
6020 let Some((_repo, tip, odb)) = build_deep_delta_repo(120) else {
6021 let _ = "SKIP: git unavailable for deep delta fixture";
6022 return;
6023 };
6024 let pack = build_pack(
6025 &odb,
6026 &[tip],
6027 &[],
6028 &PackBuildOptions {
6029 delta: true,
6030 max_depth: 120,
6031 use_ofs_delta: true,
6032 ..PackBuildOptions::default()
6033 },
6034 )
6035 .expect("build deep delta pack");
6036
6037 let (_scratch, idx) = index_pack_in_scratch(&pack);
6038 assert!(
6039 idx.len() > 1,
6040 "expected a multi-object pack, got {}",
6041 idx.len()
6042 );
6043
6044 for entry in idx.iter() {
6045 let oid = ObjectId::from_bytes(entry.oid()).expect("oid in idx");
6046 let from_pack = read_object_from_pack(&idx, &oid).expect("read packed object");
6047 let expected = odb.read(&oid).expect("loose/alt copy for oid");
6048 assert_eq!(from_pack.kind, expected.kind);
6049 assert_eq!(from_pack.data, expected.data);
6050 assert_eq!(
6051 odb.hash(from_pack.kind, &from_pack.data).expect("hash"),
6052 oid
6053 );
6054 }
6055 }
6056
6057 #[test]
6058 fn cyclic_ref_delta_chain_errors() {
6059 let _guard = pack_cache_test_guard();
6060 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6061 let content_a = b"aaa".as_slice();
6062 let content_b = b"bbb".as_slice();
6063 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6064 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6065 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6066 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6067
6068 let mut pack = Vec::new();
6069 pack.extend_from_slice(b"PACK");
6070 pack.extend_from_slice(&2u32.to_be_bytes());
6071 pack.extend_from_slice(&2u32.to_be_bytes());
6072 let off_a = pack.len() as u64;
6073 append_ref_delta(&mut pack, &oid_b, &delta_a);
6074 let off_b = pack.len() as u64;
6075 append_ref_delta(&mut pack, &oid_a, &delta_b);
6076 append_sha1_pack_trailer(&mut pack);
6077
6078 let dir = tempfile::tempdir().expect("tempdir");
6079 let pack_path = dir.path().join("cycle.pack");
6080 std::fs::write(&pack_path, &pack).expect("write cyclic pack");
6081 let idx = pack_index_for_objects(pack_path, &[(oid_a, off_a), (oid_b, off_b)]);
6082 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
6083 for err in [
6084 read_object_from_pack(&idx, &oid_a).expect_err("read_object_from_pack"),
6085 read_object_from_pack_bytes(&pack_bytes, &idx, oid_a.as_bytes())
6086 .expect_err("read_object_from_pack_bytes"),
6087 ] {
6088 assert!(
6089 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6090 "expected typed delta chain limit, got {err:?}"
6091 );
6092 }
6093 }
6094
6095 #[test]
6096 fn resolve_cross_pack_delta_without_index_clone() {
6097 let _guard = pack_cache_test_guard();
6098 clear_pack_cache();
6099 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6100 let base = b"cross-pack-base-payload".as_slice();
6101 let tip = b"cross-pack-base-payload!".as_slice();
6102 let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
6103 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6104 let delta = encode_lcp_delta(base, tip).expect("delta");
6105
6106 let mut pack_base = Vec::new();
6107 pack_base.extend_from_slice(b"PACK");
6108 pack_base.extend_from_slice(&2u32.to_be_bytes());
6109 pack_base.extend_from_slice(&1u32.to_be_bytes());
6110 let off_base = append_whole_blob(&mut pack_base, base);
6111 append_sha1_pack_trailer(&mut pack_base);
6112
6113 let mut pack_delta = Vec::new();
6114 pack_delta.extend_from_slice(b"PACK");
6115 pack_delta.extend_from_slice(&2u32.to_be_bytes());
6116 pack_delta.extend_from_slice(&1u32.to_be_bytes());
6117 let off_tip = pack_delta.len() as u64;
6118 append_ref_delta(&mut pack_delta, &oid_base, &delta);
6119 append_sha1_pack_trailer(&mut pack_delta);
6120
6121 let dir = tempfile::tempdir().expect("tempdir");
6122 let objects = dir.path().join("objects");
6123 let _idx_base =
6124 install_synthetic_pack(&objects, "cross-base", &pack_base, &[(oid_base, off_base)]);
6125 let idx_delta =
6126 install_synthetic_pack(&objects, "cross-delta", &pack_delta, &[(oid_tip, off_tip)]);
6127
6128 let from_delta_pack = read_object_from_pack(&idx_delta, &oid_tip).expect("delta pack read");
6129 assert_eq!(from_delta_pack.kind, ObjectKind::Blob);
6130 assert_eq!(from_delta_pack.data, tip);
6131 assert_eq!(
6132 odb.hash(from_delta_pack.kind, &from_delta_pack.data)
6133 .expect("hash"),
6134 oid_tip
6135 );
6136
6137 let from_packs = read_object_from_packs(&objects, &oid_tip).expect("packs dir read");
6138 assert_eq!(from_packs.kind, ObjectKind::Blob);
6139 assert_eq!(from_packs.data, tip);
6140 assert_eq!(
6141 odb.hash(from_packs.kind, &from_packs.data).expect("hash"),
6142 oid_tip
6143 );
6144 }
6145
6146 #[cfg_attr(
6147 not(feature = "full-tests"),
6148 ignore = "starts system git or is heavy; run with --features full-tests"
6149 )]
6150 #[test]
6151 fn read_object_from_pack_bytes_uses_in_memory_index_without_on_disk_idx() {
6152 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6153 let content = b"in-memory-index-blob";
6154 let oid = odb.hash(ObjectKind::Blob, content).expect("hash");
6155
6156 let mut pack = Vec::new();
6157 pack.extend_from_slice(b"PACK");
6158 pack.extend_from_slice(&2u32.to_be_bytes());
6159 pack.extend_from_slice(&1u32.to_be_bytes());
6160 let off = append_whole_blob(&mut pack, content);
6161 append_sha1_pack_trailer(&mut pack);
6162
6163 let dir = tempfile::tempdir().expect("tempdir");
6164 let pack_path = dir.path().join("mem.pack");
6165 std::fs::write(&pack_path, &pack).expect("write pack");
6166 let idx = pack_index_for_objects(pack_path, &[(oid, off)]);
6167 std::fs::remove_file(&idx.idx_path).expect("drop on-disk idx");
6168 assert!(
6169 !idx.idx_path.exists(),
6170 "fixture must not rely on an on-disk .idx"
6171 );
6172
6173 let got = read_object_from_pack_bytes(&pack, &idx, oid.as_bytes()).expect("read bytes");
6174 assert_eq!(got.kind, ObjectKind::Blob);
6175 assert_eq!(got.data, content);
6176 }
6177
6178 fn build_ofs_delta_chain_pack(
6179 depth: usize,
6180 ) -> Option<(tempfile::TempDir, PackIndex, ObjectId, Odb)> {
6181 if depth == 0 {
6182 return None;
6183 }
6184 let tmp = tempfile::tempdir().ok()?;
6185 let odb = Odb::new(&tmp.path().join("objects"));
6186 let mut content = b"ofs-delta-chain-base\n".to_vec();
6187 let mut pack = Vec::new();
6188 pack.extend_from_slice(b"PACK");
6189 pack.extend_from_slice(&2u32.to_be_bytes());
6190 pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
6191 let mut base_off = append_whole_blob(&mut pack, &content);
6192 let mut entries = vec![(
6193 odb.hash(ObjectKind::Blob, &content).expect("hash"),
6194 base_off,
6195 )];
6196 for _ in 0..depth {
6197 let mut next = content.clone();
6198 next.push(b'!');
6199 let delta = encode_lcp_delta(&content, &next).ok()?;
6200 let off = append_ofs_delta(&mut pack, base_off, &delta);
6201 let oid = odb.hash(ObjectKind::Blob, &next).expect("hash");
6202 entries.push((oid, off));
6203 content = next;
6204 base_off = off;
6205 }
6206 append_sha1_pack_trailer(&mut pack);
6207 let tip = entries.last()?.0;
6208 let dir = tmp.path();
6209 std::fs::create_dir_all(dir.join("objects/pack")).ok()?;
6210 let pack_path = dir.join("objects/pack/chain.pack");
6211 std::fs::write(&pack_path, &pack).ok()?;
6212 write_v2_idx_for_test(&pack_path.with_extension("idx"), &pack_path, &entries).ok()?;
6213 clear_pack_cache();
6214 let idx = read_pack_index(&pack_path.with_extension("idx")).ok()?;
6215 Some((tmp, idx, tip, odb))
6216 }
6217
6218 #[test]
6219 fn bounded_pack_cache_resolves_deltas_without_changing_shared_cache() {
6220 let _guard = pack_cache_test_guard();
6221 let (_tmp, idx, tip, odb) = build_ofs_delta_chain_pack(800).expect("chain");
6222 let normal = odb.read(&tip).expect("normal");
6223 let shared_bytes = odb.pack_store().expect("store").delta_bases().bytes_used();
6224 let bounded = odb.with_bounded_pack_cache();
6225 assert!(!Arc::ptr_eq(
6226 odb.pack_store().expect("store"),
6227 bounded.pack_store().expect("store")
6228 ));
6229 bounded.with_pack_read_context(|| {
6230 let got = bounded.read(&tip).expect("bounded");
6231 assert_eq!(got.data, normal.data);
6232 let pack = read_pack_bytes_cached(&idx.pack_path).expect("pack");
6233 assert!(pack.uses_positioned_reads());
6234 let cached = bounded
6235 .pack_store()
6236 .expect("store")
6237 .delta_bases()
6238 .bytes_used();
6239 assert!(cached > 0 && cached <= crate::pack_store::BOUNDED_DELTA_BASE_BYTES);
6240 });
6241 assert_eq!(
6242 odb.pack_store().expect("store").delta_bases().bytes_used(),
6243 shared_bytes
6244 );
6245 }
6246
6247 #[test]
6248 fn ofs_delta_chain_depth_4096_resolves() {
6249 let _guard = pack_cache_test_guard();
6250 clear_pack_cache();
6251 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(4096) else {
6252 return;
6253 };
6254 let got = read_object_from_pack(&idx, &tip).expect("4096-deep chain");
6255 let expect = odb.hash(ObjectKind::Blob, &got.data).expect("hash");
6256 assert_eq!(expect, tip);
6257 }
6258
6259 #[test]
6260 fn ofs_delta_chain_depth_4097_errors() {
6261 let _guard = pack_cache_test_guard();
6262 clear_pack_cache();
6263 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(4097) else {
6264 return;
6265 };
6266 let err = read_object_from_pack(&idx, &tip).expect_err("4097-deep chain");
6267 assert!(
6268 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6269 "got {err:?}"
6270 );
6271 }
6272
6273 #[test]
6274 fn delta_base_cache_limit_zero_disables_caching() {
6275 let _guard = pack_cache_test_guard();
6276 clear_pack_cache();
6277 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6278 return;
6279 };
6280 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 0);
6281 let _ = read_object_from_pack(&idx, &tip).expect("read chain");
6282 assert_eq!(
6283 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6284 0,
6285 "limit 0 must not retain delta bases"
6286 );
6287 pack_cache::test_set_delta_base_cache_byte_limit(
6288 &idx.pack_path,
6289 pack_cache::DELTA_BASE_CACHE_DEFAULT,
6290 );
6291 }
6292
6293 #[test]
6294 fn delta_base_cache_eviction_respects_byte_limit() {
6295 let _guard = pack_cache_test_guard();
6296 clear_pack_cache();
6297 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(32) else {
6298 return;
6299 };
6300 let expected = read_object_from_pack(&idx, &tip)
6301 .expect("baseline read")
6302 .data;
6303 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 4096);
6304 let _ = read_object_from_pack(&idx, &tip).expect("prime cache");
6305 assert!(
6306 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) <= 4096,
6307 "cache bytes {} exceeded cap",
6308 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path)
6309 );
6310 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 512);
6311 for round in 0..8 {
6312 let got = read_object_from_pack(&idx, &tip).expect("read after eviction");
6313 assert_eq!(got.data, expected, "round {round} must stay byte-identical");
6314 }
6315 pack_cache::test_set_delta_base_cache_byte_limit(
6316 &idx.pack_path,
6317 pack_cache::DELTA_BASE_CACHE_DEFAULT,
6318 );
6319 }
6320
6321 #[test]
6322 fn odb_read_into_caching_delta_bases_fills_cache_read_into_does_not() {
6323 let _guard = pack_cache_test_guard();
6324 clear_pack_cache();
6325 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(12) else {
6326 return;
6327 };
6328 let expected = read_object_from_pack(&idx, &tip)
6329 .expect("baseline read")
6330 .data;
6331 clear_pack_cache();
6332 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
6333 crate::pack_store::PackStore::with_context(store, || {
6334 let mut buf = Vec::new();
6335 assert_eq!(
6336 odb.read_into(&tip, &mut buf).expect("read_into"),
6337 ObjectKind::Blob
6338 );
6339 assert_eq!(buf, expected);
6340 assert_eq!(
6341 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6342 0,
6343 "read_into must not retain delta bases"
6344 );
6345
6346 let mut cached = Vec::new();
6347 let kind = odb
6348 .read_into_caching_delta_bases(&tip, &mut cached)
6349 .expect("caching read");
6350 assert_eq!(kind, ObjectKind::Blob);
6351 assert_eq!(cached, expected);
6352 assert!(
6353 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) > 0,
6354 "caching read must retain the chain's delta bases"
6355 );
6356
6357 let again = odb
6358 .read_into_caching_delta_bases(&tip, &mut cached)
6359 .expect("second caching read");
6360 assert_eq!(again, ObjectKind::Blob);
6361 assert_eq!(cached, expected);
6362 });
6363 }
6364
6365 #[test]
6366 fn repository_odb_read_into_caching_delta_bases_fills_cache_through_files_primary() {
6367 use crate::repo::{InitOptions, Repository};
6368 let _guard = pack_cache_test_guard();
6369 clear_pack_cache();
6370 let Some((tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6371 return;
6372 };
6373 let expected = read_object_from_pack(&idx, &tip)
6374 .expect("baseline read")
6375 .data;
6376 Repository::init(
6377 tmp.path(),
6378 &InitOptions {
6379 bare: true,
6380 ..InitOptions::default()
6381 },
6382 )
6383 .expect("init over packed objects");
6384 let repo = Repository::open(tmp.path(), &crate::environment::RepositoryOptions::empty())
6385 .expect("open");
6386 let odb = repo.odb();
6387 let store = Arc::clone(odb.pack_store().expect("pack store"));
6388 store.invalidate_packs();
6389
6390 let mut buf = Vec::new();
6391 assert_eq!(
6392 odb.read_into(&tip, &mut buf).expect("read_into"),
6393 ObjectKind::Blob
6394 );
6395 assert_eq!(buf, expected);
6396 assert_eq!(store.delta_bases().bytes_used(), 0);
6397
6398 let mut cached = Vec::new();
6399 assert_eq!(
6400 odb.read_into_caching_delta_bases(&tip, &mut cached)
6401 .expect("caching read"),
6402 ObjectKind::Blob
6403 );
6404 assert_eq!(cached, expected);
6405 assert!(store.delta_bases().bytes_used() > 0);
6406 }
6407
6408 #[test]
6409 fn odb_read_into_caching_delta_bases_falls_back_outside_packs() {
6410 let tmp = tempfile::tempdir().expect("tempdir");
6411 let odb = Odb::new(&tmp.path().join("objects"));
6412 let oid = odb
6413 .write(ObjectKind::Blob, b"loose only\n")
6414 .expect("write loose");
6415 let mut buf = vec![0u8; 4];
6416 let kind = odb
6417 .read_into_caching_delta_bases(&oid, &mut buf)
6418 .expect("loose read");
6419 assert_eq!(kind, ObjectKind::Blob);
6420 assert_eq!(buf, b"loose only\n");
6421 }
6422
6423 #[test]
6424 fn cached_delta_reads_support_overlay_alternates_and_custom_stores() {
6425 use crate::odb::store::MemoryStore;
6426 use crate::odb::OdbBuilder;
6427
6428 let tmp = tempfile::tempdir().expect("tempdir");
6429 let local_objects = tmp.path().join("local/objects");
6430 let alternate_objects = tmp.path().join("alternate/objects");
6431 let alternate = Odb::new(&alternate_objects);
6432 let alternate_oid = alternate
6433 .write(ObjectKind::Blob, b"alternate\n")
6434 .expect("alternate object");
6435 fs::create_dir_all(local_objects.join("info")).expect("info");
6436 fs::write(
6437 local_objects.join("info/alternates"),
6438 format!("{}\n", alternate_objects.display()),
6439 )
6440 .expect("alternates");
6441
6442 let odb = Odb::new(&local_objects);
6443 let mut buf = Vec::with_capacity(1024);
6444 assert_eq!(
6445 odb.read_into_caching_delta_bases(&alternate_oid, &mut buf)
6446 .expect("alternate read"),
6447 ObjectKind::Blob
6448 );
6449 assert_eq!(buf, b"alternate\n");
6450
6451 odb.enable_mem_overlay();
6452 let overlay_oid = odb
6453 .write(ObjectKind::Blob, b"overlay\n")
6454 .expect("overlay object");
6455 odb.read_into_caching_delta_bases(&overlay_oid, &mut buf)
6456 .expect("overlay read");
6457 assert_eq!(buf, b"overlay\n");
6458
6459 let custom = OdbBuilder::files(tmp.path().join("custom/objects"))
6460 .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
6461 .build();
6462 let custom_oid = custom
6463 .write(ObjectKind::Blob, b"custom\n")
6464 .expect("custom object");
6465 custom
6466 .read_into_caching_delta_bases(&custom_oid, &mut buf)
6467 .expect("custom read");
6468 assert_eq!(buf, b"custom\n");
6469
6470 let missing = HashAlgo::Sha1
6471 .hash_object(ObjectKind::Blob, b"missing")
6472 .expect("missing id");
6473 assert!(matches!(
6474 custom.read_into_caching_delta_bases(&missing, &mut buf),
6475 Err(Error::ObjectNotFound(_))
6476 ));
6477 }
6478
6479 #[test]
6480 fn delta_base_cache_invalidated_when_pack_bytes_reloaded() {
6481 use filetime::{set_file_mtime, FileTime};
6482 let _guard = pack_cache_test_guard();
6483 clear_pack_cache();
6484 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(8) else {
6485 return;
6486 };
6487 let pack_path = idx.pack_path.clone();
6488 let pack_id = pack_cache::pack_id_for(&pack_path);
6489 let mid_off = idx
6490 .iter()
6491 .nth(4)
6492 .map(|e| e.offset())
6493 .expect("mid hop")
6494 .expect("mid off");
6495 let _ = read_object_from_pack(&idx, &tip).expect("warm cache");
6496 assert!(
6497 pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6498 "expected intermediate cached at offset {mid_off}"
6499 );
6500 set_file_mtime(&pack_path, FileTime::now()).expect("touch pack mtime");
6501 assert!(
6502 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6503 "expected pack bytes reload"
6504 );
6505 assert!(
6506 !pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6507 "cache must drop when pack bytes reload"
6508 );
6509 }
6510
6511 #[cfg_attr(
6512 not(feature = "full-tests"),
6513 ignore = "starts system git or is heavy; run with --features full-tests"
6514 )]
6515 #[test]
6516 fn git_repack_depth_250_matches_cat_file_batch() {
6517 let _guard = pack_cache_test_guard();
6518 clear_pack_cache();
6519 let tmp = tempfile::tempdir().expect("tempdir");
6520 let dir = tmp.path();
6521 if Command::new("git") .current_dir(dir)
6523 .args(["init", "-q"])
6524 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6525 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6526 .status()
6527 .is_err()
6528 {
6529 let _ = "SKIP: git unavailable";
6530 return;
6531 }
6532 for i in 0..64 {
6533 std::fs::write(dir.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
6534 git(dir, &["add", &format!("f{i}.txt")]);
6535 git(dir, &["commit", "-qm", &format!("c{i}")]);
6536 }
6537 git(dir, &["repack", "-adf", "--depth=250", "--window=250"]);
6538 let objects = dir.join(".git/objects");
6539 let indexes = read_local_pack_indexes_cached(&objects).expect("pack indexes");
6540 let idx = indexes
6541 .into_iter()
6542 .max_by_key(|i| i.len())
6543 .expect("repack pack");
6544 for entry in idx.iter() {
6545 let oid = ObjectId::from_bytes(entry.oid()).expect("oid");
6546 let grit_obj = read_object_from_pack(&idx, &oid).expect("grit read");
6547 let git_type = Command::new("git") .current_dir(dir)
6549 .args(["cat-file", "-t", &oid.to_hex()])
6550 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6551 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6552 .output()
6553 .expect("git cat-file -t");
6554 assert!(git_type.status.success());
6555 let git_kind = ObjectKind::from_bytes(
6556 std::str::from_utf8(git_type.stdout.trim_ascii_end())
6557 .expect("utf8 type")
6558 .as_bytes(),
6559 )
6560 .expect("git type");
6561 assert_eq!(git_kind, grit_obj.kind);
6562 let type_arg = match grit_obj.kind {
6563 ObjectKind::Blob => "blob",
6564 ObjectKind::Tree => "tree",
6565 ObjectKind::Commit => "commit",
6566 ObjectKind::Tag => "tag",
6567 };
6568 let git_payload = Command::new("git") .current_dir(dir)
6570 .args(["cat-file", type_arg, &oid.to_hex()])
6571 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6572 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6573 .output()
6574 .expect("git cat-file raw");
6575 assert!(git_payload.status.success());
6576 assert_eq!(git_payload.stdout, grit_obj.data);
6577 }
6578 }
6579
6580 #[test]
6581 fn cyclic_ref_delta_chain_across_two_packs_errors() {
6582 let _guard = pack_cache_test_guard();
6583 clear_pack_cache();
6584 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6585 let content_a = b"aaa".as_slice();
6586 let content_b = b"bbb".as_slice();
6587 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6588 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6589 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6590 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6591
6592 let mut pack_a = Vec::new();
6593 pack_a.extend_from_slice(b"PACK");
6594 pack_a.extend_from_slice(&2u32.to_be_bytes());
6595 pack_a.extend_from_slice(&1u32.to_be_bytes());
6596 let off_a = pack_a.len() as u64;
6597 append_ref_delta(&mut pack_a, &oid_b, &delta_a);
6598 append_sha1_pack_trailer(&mut pack_a);
6599
6600 let mut pack_b = Vec::new();
6601 pack_b.extend_from_slice(b"PACK");
6602 pack_b.extend_from_slice(&2u32.to_be_bytes());
6603 pack_b.extend_from_slice(&1u32.to_be_bytes());
6604 let off_b = pack_b.len() as u64;
6605 append_ref_delta(&mut pack_b, &oid_a, &delta_b);
6606 append_sha1_pack_trailer(&mut pack_b);
6607
6608 let dir = tempfile::tempdir().expect("tempdir");
6609 let objects = dir.path().join("objects");
6610 let idx_b = install_synthetic_pack(&objects, "cycle-b", &pack_b, &[(oid_b, off_b)]);
6611 let idx_a = install_synthetic_pack(&objects, "cycle-a", &pack_a, &[(oid_a, off_a)]);
6612 assert_eq!(idx_a.len(), 1);
6613 assert_eq!(idx_b.len(), 1);
6614 assert!(idx_b.contains(&oid_b).expect("contains"));
6615 let _ = idx_b;
6616
6617 let err = read_object_from_pack(&idx_a, &oid_a).expect_err("cross-pack cycle");
6618 assert!(
6619 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6620 "expected typed delta chain limit, got {err:?}"
6621 );
6622 }
6623
6624 #[test]
6625 fn cross_pack_ref_delta_resolves_through_odb_read() {
6626 let _guard = pack_cache_test_guard();
6627 clear_pack_cache();
6628 let tmp = tempfile::tempdir().expect("tempdir");
6629 let objects = tmp.path().join("objects");
6630 let odb = Odb::new(&objects);
6631 let mut random = 17u64;
6632 let base: Vec<u8> = (0..20000)
6633 .map(|_| {
6634 random ^= random << 13;
6635 random ^= random >> 7;
6636 random ^= random << 17;
6637 random as u8
6638 })
6639 .collect();
6640 let mut target = base.clone();
6641 target.extend_from_slice(b"target suffix");
6642 let oid_base = odb.hash(ObjectKind::Blob, &base).expect("hash");
6643 let delta = encode_lcp_delta(&base, &target).expect("delta");
6644 let oid_target = odb.hash(ObjectKind::Blob, &target).expect("hash");
6645
6646 let mut pack_base = Vec::new();
6647 pack_base.extend_from_slice(b"PACK");
6648 pack_base.extend_from_slice(&2u32.to_be_bytes());
6649 pack_base.extend_from_slice(&1u32.to_be_bytes());
6650 let off_base = append_whole_blob(&mut pack_base, &base);
6651 append_sha1_pack_trailer(&mut pack_base);
6652
6653 let mut pack_tip = Vec::new();
6654 pack_tip.extend_from_slice(b"PACK");
6655 pack_tip.extend_from_slice(&2u32.to_be_bytes());
6656 pack_tip.extend_from_slice(&1u32.to_be_bytes());
6657 let off_tip = pack_tip.len() as u64;
6658 append_ref_delta(&mut pack_tip, &oid_base, &delta);
6659 append_sha1_pack_trailer(&mut pack_tip);
6660
6661 install_synthetic_pack(&objects, "base", &pack_base, &[(oid_base, off_base)]);
6662 install_synthetic_pack(&objects, "tip", &pack_tip, &[(oid_target, off_tip)]);
6663
6664 for reader in [&odb, &odb.with_bounded_pack_cache()] {
6665 let got = reader.read(&oid_target).expect("cross-pack ref-delta");
6666 assert_eq!(got.kind, ObjectKind::Blob);
6667 assert_eq!(got.data, target);
6668 assert_eq!(reader.hash(got.kind, &got.data).expect("hash"), oid_target);
6669 }
6670 }
6671
6672 #[test]
6673 fn corrupt_in_pack_base_rescued_from_loose() {
6674 let _guard = pack_cache_test_guard();
6675 let tmp = tempfile::tempdir().expect("tempdir");
6676 let objects = tmp.path().join("objects");
6677 std::fs::create_dir_all(objects.join("pack")).expect("dirs");
6678 let odb = Odb::new(&objects);
6679 let base = b"good-base-bytes";
6680 let tip = b"good-base-bytes!";
6681 let oid_base = odb.write_local(ObjectKind::Blob, base).expect("loose base");
6682 let delta = encode_lcp_delta(base, tip).expect("delta");
6683 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6684
6685 let mut pack = Vec::new();
6686 pack.extend_from_slice(b"PACK");
6687 pack.extend_from_slice(&2u32.to_be_bytes());
6688 pack.extend_from_slice(&2u32.to_be_bytes());
6689 let off_base = append_corrupt_whole_blob(&mut pack);
6690 let off_tip = pack.len() as u64;
6691 append_ref_delta(&mut pack, &oid_base, &delta);
6692 append_sha1_pack_trailer(&mut pack);
6693
6694 let pack_path = objects.join("pack/rescue.pack");
6695 std::fs::write(&pack_path, &pack).expect("write pack");
6696 let idx = pack_index_for_objects(pack_path, &[(oid_base, off_base), (oid_tip, off_tip)]);
6697
6698 let got = read_object_from_pack(&idx, &oid_tip).expect("rescue read");
6699 assert_eq!(got.kind, ObjectKind::Blob);
6700 assert_eq!(got.data, tip);
6701 }
6702
6703 #[test]
6704 fn pack_added_after_first_lookup_found_on_miss() {
6705 let _guard = pack_cache_test_guard();
6706 clear_pack_cache();
6707 let tmp = tempfile::tempdir().expect("tempdir");
6708 let objects = tmp.path().join("objects");
6709 let pack_dir = objects.join("pack");
6710 let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
6711 install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
6712 pack_cache::test_reset_dir_rescan_count(&pack_dir);
6713
6714 read_object_from_packs(&objects, &oid_a).expect("warm cache");
6715 assert_eq!(
6716 pack_cache::test_dir_rescan_count(&pack_dir),
6717 1,
6718 "initial lookup should scan the pack directory once"
6719 );
6720
6721 std::thread::sleep(std::time::Duration::from_secs(1));
6723
6724 let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b");
6725 install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
6726 assert_eq!(
6727 pack_cache::test_dir_rescan_count(&pack_dir),
6728 1,
6729 "adding a pack must not rescan until a miss"
6730 );
6731
6732 read_object_from_packs(&objects, &oid_b).expect("new pack via miss path");
6733 assert_eq!(
6734 pack_cache::test_dir_rescan_count(&pack_dir),
6735 2,
6736 "miss path should re-scan when pack dir mtime changed"
6737 );
6738 }
6739
6740 #[test]
6741 fn known_object_lookup_does_not_rescan() {
6742 let _guard = pack_cache_test_guard();
6743 clear_pack_cache();
6744 let tmp = tempfile::tempdir().expect("tempdir");
6745 let objects = tmp.path().join("objects");
6746 let pack_dir = objects.join("pack");
6747 pack_cache::test_reset_dir_rescan_count(&pack_dir);
6748 let (pack, oid, off) = single_blob_pack(b"cached-blob");
6749 install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
6750
6751 read_object_from_packs(&objects, &oid).expect("prime cache");
6752 let after_first = pack_cache::test_dir_rescan_count(&pack_dir);
6753 for _ in 0..64 {
6754 read_object_from_packs(&objects, &oid).expect("repeat read");
6755 }
6756 assert_eq!(
6757 pack_cache::test_dir_rescan_count(&pack_dir),
6758 after_first,
6759 "repeated hits must not re-scan the pack directory"
6760 );
6761 }
6762
6763 #[test]
6764 fn two_repositories_concurrent_reads() {
6765 use crate::odb::Odb;
6766 use std::sync::Arc;
6767
6768 let _guard = pack_cache_test_guard();
6769 clear_pack_cache();
6770 let tmp_a = tempfile::tempdir().expect("tempdir a");
6771 let tmp_b = tempfile::tempdir().expect("tempdir b");
6772 let objects_a = tmp_a.path().join("objects");
6773 let objects_b = tmp_b.path().join("objects");
6774 let pack_a_dir = objects_a.join("pack");
6775 let pack_b_dir = objects_b.join("pack");
6776 let (pack_a, oid_a, off_a) = single_blob_pack(b"parallel-a");
6777 let (pack_b, oid_b, off_b) = single_blob_pack(b"parallel-b");
6778 install_synthetic_pack(&objects_a, "a", &pack_a, &[(oid_a, off_a)]);
6779 install_synthetic_pack(&objects_b, "b", &pack_b, &[(oid_b, off_b)]);
6780 let odb_a = Odb::new(&objects_a);
6781 let odb_b = Odb::new(&objects_b);
6782 assert!(
6783 !Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
6784 "independent Odb opens must not share pack stores"
6785 );
6786 pack_cache::test_reset_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir);
6787 pack_cache::test_reset_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir);
6788
6789 let barrier = Arc::new(std::sync::Barrier::new(2));
6790 let b0 = Arc::clone(&barrier);
6791 let b1 = Arc::clone(&barrier);
6792 let odb_a_t = odb_a.clone();
6793 let odb_b_t = odb_b.clone();
6794 let id_a = oid_a;
6795 let id_b = oid_b;
6796
6797 let h1 = std::thread::spawn(move || {
6798 b0.wait();
6799 odb_a_t.read(&id_a).expect("read a");
6800 });
6801 let h2 = std::thread::spawn(move || {
6802 b1.wait();
6803 odb_b_t.read(&id_b).expect("read b");
6804 });
6805 h1.join().expect("thread a");
6806 h2.join().expect("thread b");
6807
6808 assert_eq!(
6809 pack_cache::test_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir),
6810 1
6811 );
6812 assert_eq!(
6813 pack_cache::test_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir),
6814 1
6815 );
6816 }
6817
6818 #[test]
6819 fn stale_cached_pack_bytes_revalidated_before_corruption_error() {
6820 let _guard = pack_cache_test_guard();
6821 clear_pack_cache();
6822 let tmp = tempfile::tempdir().expect("tempdir");
6823 let objects = tmp.path().join("objects");
6824 let (pack, oid, off) = single_blob_pack(b"revalidate-me");
6825 let idx = install_synthetic_pack(&objects, "good", &pack, &[(oid, off)]);
6826 read_object_from_pack(&idx, &oid).expect("prime pack bytes cache");
6827
6828 let good_on_disk = std::fs::read(&idx.pack_path).expect("read pack");
6829 pack_cache::test_inject_stale_pack_bytes(
6830 &idx.pack_path,
6831 PackData::from_owned(b"BAD".to_vec()),
6832 );
6833 std::fs::write(&idx.pack_path, &good_on_disk).expect("refresh on-disk pack");
6834 let got = read_object_from_pack(&idx, &oid).expect("revalidate from disk");
6835 assert_eq!(got.data, b"revalidate-me");
6836 }
6837
6838 #[test]
6839 fn replaced_pack_same_path_reread_after_invalidation() {
6840 let _guard = pack_cache_test_guard();
6841 clear_pack_cache();
6842 let tmp = tempfile::tempdir().expect("tempdir");
6843 let objects = tmp.path().join("objects");
6844 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6845 let pack_path = objects.join("pack/repack-me.pack");
6846 let idx_path = objects.join("pack/repack-me.idx");
6847 const OBJECT_OFFSET: u64 = 9000;
6848 let (oid_a, off_a) =
6849 write_sparse_single_blob_pack(tmp.path(), &pack_path, b"before-repack", OBJECT_OFFSET)
6850 .expect("sparse pack a");
6851 assert!(
6852 std::fs::metadata(&pack_path).expect("stat").len() > 8192,
6853 "fixture pack must exceed mmap owned-buffer threshold"
6854 );
6855 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_a, off_a)]).expect("idx a");
6856 let idx = read_pack_index(&idx_path).expect("parse idx a");
6857 read_object_from_pack(&idx, &oid_a).expect("prime cache");
6858
6859 let (oid_b, off_b) = write_sparse_single_blob_pack(
6860 tmp.path(),
6861 &pack_path,
6862 b"after-repack-content",
6863 OBJECT_OFFSET,
6864 )
6865 .expect("sparse pack b");
6866 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_b, off_b)]).expect("idx b");
6867
6868 assert!(
6869 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6870 "replacement must invalidate cached bytes"
6871 );
6872 let idx = read_pack_index(&idx_path).expect("reload idx after pack swap");
6873 let got = read_object_from_pack(&idx, &oid_b).expect("read from replaced pack");
6874 assert_eq!(got.data, b"after-repack-content");
6875 }
6876
6877 #[test]
6878 fn read_object_at_large_pack_offset_via_mmap() {
6879 use std::fs::OpenOptions;
6880 use std::io::{Seek, SeekFrom, Write};
6881
6882 let _guard = pack_cache_test_guard();
6883 clear_pack_cache();
6884 let tmp = tempfile::tempdir().expect("tempdir");
6885 let objects = tmp.path().join("objects");
6886 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6887
6888 let data = b"large-offset-blob";
6889 let odb = Odb::new(tmp.path());
6890 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
6891
6892 let pack_path = objects.join("pack/large-off.pack");
6893 let mut file = OpenOptions::new()
6894 .create(true)
6895 .write(true)
6896 .read(true)
6897 .open(&pack_path)
6898 .expect("create sparse pack");
6899 file.write_all(b"PACK").expect("sig");
6900 file.write_all(&2u32.to_be_bytes()).expect("ver");
6901 file.write_all(&1u32.to_be_bytes()).expect("count");
6902 let object_offset: u64 = 1 << 32;
6903 file.seek(SeekFrom::Start(object_offset))
6904 .expect("seek past 4GiB");
6905 let mut tail = Vec::new();
6906 append_whole_blob(&mut tail, data);
6907 file.write_all(&tail).expect("object at huge offset");
6908 let trailer = sha1_trailer_for_open_file(&mut file).expect("trailer hash");
6909 file.write_all(trailer.as_bytes()).expect("append trailer");
6910 file.flush().expect("flush");
6911 let file_len = file.metadata().expect("meta").len();
6912 drop(file);
6913 assert!(file_len > object_offset);
6914
6915 let idx_path = pack_path.with_extension("idx");
6916 write_v2_pack_index_with_trailer(
6917 &idx_path,
6918 &[(oid, object_offset, 0)],
6919 trailer.as_bytes(),
6920 20,
6921 None,
6922 )
6923 .expect("idx");
6924
6925 let idx = read_pack_index(&idx_path).expect("parse idx");
6926 assert_eq!(idx.offset_at(0).expect("off"), object_offset);
6927 let got = read_object_from_pack(&idx, &oid).expect("read at large offset");
6928 assert_eq!(got.data, data);
6929 }
6930
6931 fn synthetic_index_entries(n: u32) -> Vec<(ObjectId, u64, u32)> {
6932 (0..n)
6933 .map(|i| {
6934 let digest = HashAlgo::Sha1.checksum_digest(&i.to_be_bytes());
6935 let oid = ObjectId::from_bytes(&digest).expect("oid");
6936 let offset = if i % 7 == 0 {
6937 (1u64 << 31) + u64::from(i)
6938 } else {
6939 12 + u64::from(i) * 40
6940 };
6941 (oid, offset, i.wrapping_mul(2_654_435_761))
6942 })
6943 .collect()
6944 }
6945
6946 #[test]
6947 fn index_entries_sort_in_place_on_any_thread_count() {
6948 for n in [0_u32, 1, 300, 20_000] {
6949 let mut reference = synthetic_index_entries(n);
6950 reference.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6951 let mut fanout_ref = [0u32; 256];
6952 for (oid, _, _) in &reference {
6953 for slot in &mut fanout_ref[usize::from(oid.as_bytes()[0])..] {
6954 *slot += 1;
6955 }
6956 }
6957 for threads in [None, Some(1), Some(2), Some(3), Some(8)] {
6958 let mut entries = synthetic_index_entries(n);
6959 let fanout = sort_index_entries(&mut entries, threads);
6960 assert_eq!(entries, reference, "n={n} threads={threads:?}");
6961 assert_eq!(fanout, fanout_ref, "n={n} threads={threads:?}");
6962 }
6963 }
6964 }
6965
6966 #[test]
6967 fn index_written_in_place_reads_back_every_row_with_large_offsets() {
6968 let tmp = tempfile::tempdir().expect("tempdir");
6969 let idx_path = tmp.path().join("p.idx");
6970 let rows = synthetic_index_entries(20_000);
6971 let trailer = [0x5a_u8; 20];
6972 let mut entries = rows.clone();
6973 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer, 20, Some(4))
6974 .expect("write idx");
6975 let idx = read_pack_index(&idx_path).expect("read idx");
6976 assert_eq!(idx.len(), rows.len());
6977 let mut want = rows;
6978 want.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6979 for (pos, (oid, offset, crc)) in want.iter().enumerate() {
6980 assert_eq!(idx.oid_at(pos), oid.as_bytes(), "row {pos}");
6981 assert_eq!(idx.offset_at(pos).expect("offset"), *offset, "row {pos}");
6982 assert_eq!(idx.crc32_at(pos).expect("crc"), *crc, "row {pos}");
6983 }
6984
6985 let mut bad = vec![(ObjectId::from_bytes(&[1; 32]).expect("oid"), 12, 0)];
6986 assert!(matches!(
6987 write_v2_pack_index_sorting(&idx_path, &mut bad, &trailer, 20, None),
6988 Err(Error::CorruptObject(_))
6989 ));
6990 assert!(matches!(
6991 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer[..19], 20, None),
6992 Err(Error::CorruptObject(_))
6993 ));
6994 }
6995
6996 fn sha1_trailer_for_open_file(file: &mut std::fs::File) -> Result<ObjectId> {
6998 use std::io::{Read, Seek, SeekFrom};
6999 let len = file.metadata().map_err(Error::Io)?.len();
7000 file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
7001 let mut hasher = HashAlgo::Sha1.checksum_hasher();
7002 let mut buf = [0u8; 64 * 1024];
7003 let mut left = len;
7004 while left > 0 {
7005 let chunk = left.min(buf.len() as u64) as usize;
7006 let n = file.read(&mut buf[..chunk]).map_err(Error::Io)?;
7007 if n == 0 {
7008 break;
7009 }
7010 hasher.update(&buf[..n]);
7011 left -= n as u64;
7012 }
7013 let digest = hasher.finalize();
7014 ObjectId::from_bytes(&digest).map_err(|e| Error::InvalidObjectId(format!("{e}")))
7015 }
7016
7017 #[test]
7018 fn cleared_cache_sees_removed_packs_disappear() {
7019 let _guard = pack_cache_test_guard();
7020 clear_pack_cache();
7021 let tmp = tempfile::tempdir().expect("tempdir");
7022 let objects = tmp.path().join("objects");
7023 let (pack, oid, off) = single_blob_pack(b"ephemeral");
7024 install_synthetic_pack(&objects, "gone", &pack, &[(oid, off)]);
7025 read_object_from_packs(&objects, &oid).expect("warm cache");
7026
7027 let pack_path = objects.join("pack/gone.pack");
7028 let idx_path = objects.join("pack/gone.idx");
7029 std::fs::remove_file(&pack_path).expect("remove pack");
7030 std::fs::remove_file(&idx_path).expect("remove idx");
7031 clear_pack_cache();
7032
7033 let indexes = read_local_pack_indexes_cached(&objects).expect("rescan");
7034 assert!(
7035 indexes.is_empty(),
7036 "after clear, listing should not retain removed packs"
7037 );
7038 }
7039
7040 #[test]
7041 fn repack_while_sibling_odb_holds_store_reprepares_on_miss() {
7042 use crate::odb::Odb;
7043
7044 let _guard = pack_cache_test_guard();
7045 clear_pack_cache();
7046 let tmp = tempfile::tempdir().expect("tempdir");
7047 let objects = tmp.path().join("objects");
7048 let (pack_old, oid_old, off_old) = single_blob_pack(b"before");
7049 install_synthetic_pack(&objects, "old", &pack_old, &[(oid_old, off_old)]);
7050 let odb_a = Odb::new(&objects);
7051 let odb_b = Odb::new(&objects);
7052 assert!(
7053 !std::sync::Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
7054 "sibling Odbs must have distinct pack stores"
7055 );
7056 odb_b.read(&oid_old).expect("warm b store");
7057
7058 std::fs::remove_file(objects.join("pack/old.pack")).expect("rm pack");
7059 std::fs::remove_file(objects.join("pack/old.idx")).expect("rm idx");
7060 let (pack_new, oid_new, off_new) = single_blob_pack(b"after");
7061 install_synthetic_pack(&objects, "new", &pack_new, &[(oid_new, off_new)]);
7062 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now())
7063 .expect("bump pack dir mtime for reprepare");
7064 odb_a.invalidate_packs();
7065
7066 let got = odb_b.read(&oid_new).expect("b reprepares after miss");
7068 assert_eq!(got.data.as_slice(), b"after");
7069 }
7070
7071 #[test]
7072 fn fresh_store_does_not_trust_a_listing_left_by_another_store() {
7073 let dir = tempfile::tempdir().expect("tempdir");
7074 let dir = dir.path();
7075 git(dir, &["init", "-q"]);
7076 git(dir, &["commit", "-q", "--allow-empty", "-m", "a"]);
7077 git(dir, &["repack", "-adq"]);
7078 let objects = dir.join(".git/objects");
7079 let pack_names = |indexes: Vec<Arc<PackIndex>>| -> Vec<PathBuf> {
7080 indexes
7081 .into_iter()
7082 .map(|idx| idx.pack_path.clone())
7083 .collect()
7084 };
7085
7086 let first = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7087 let before = crate::pack_store::PackStore::with_context(first, || {
7088 read_local_pack_indexes_cached(&objects)
7089 })
7090 .expect("first listing");
7091
7092 git(dir, &["commit", "-q", "--allow-empty", "-m", "b"]);
7094 git(dir, &["repack", "-adq"]);
7095
7096 let second = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7097 let after = crate::pack_store::PackStore::with_context(second, || {
7098 read_local_pack_indexes_cached(&objects)
7099 })
7100 .expect("second listing");
7101 let (before, after) = (pack_names(before), pack_names(after));
7102 assert_ne!(before, after);
7103 assert!(after.iter().all(|p| p.is_file()), "{after:?}");
7104 }
7105}
7106#[cfg(test)]
7107mod cached_lookup_tests {
7108 use super::*;
7109 use std::fs;
7110 use std::process::Command;
7111 use tempfile::TempDir;
7112
7113 fn git(dir: &Path, args: &[&str]) {
7114 let out = Command::new("git") .current_dir(dir)
7116 .args(args)
7117 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7118 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7119 .env("GIT_AUTHOR_NAME", "t")
7120 .env("GIT_AUTHOR_EMAIL", "t@example.com")
7121 .env("GIT_COMMITTER_NAME", "t")
7122 .env("GIT_COMMITTER_EMAIL", "t@example.com")
7123 .output()
7124 .expect("spawn git");
7125 assert!(
7126 out.status.success(),
7127 "git {:?}: {}",
7128 args,
7129 String::from_utf8_lossy(&out.stderr)
7130 );
7131 }
7132
7133 fn repack_all(dir: &Path) {
7134 git(dir, &["repack", "-a", "-d"]);
7135 }
7136
7137 fn git_stdout(dir: &Path, args: &[&str], stdin: Option<&str>) -> String {
7138 use std::io::Write;
7139 let mut child = Command::new("git") .current_dir(dir)
7141 .args(args)
7142 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7143 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7144 .stdin(std::process::Stdio::piped())
7145 .stdout(std::process::Stdio::piped())
7146 .spawn()
7147 .expect("spawn git");
7148 if let Some(input) = stdin {
7149 child
7150 .stdin
7151 .as_mut()
7152 .expect("stdin")
7153 .write_all(input.as_bytes())
7154 .expect("write stdin");
7155 }
7156 let out = child.wait_with_output().expect("git output");
7157 assert!(out.status.success(), "git {args:?}");
7158 String::from_utf8(out.stdout)
7159 .expect("utf-8")
7160 .trim()
7161 .to_owned()
7162 }
7163
7164 fn oid_of(dir: &Path, rev: &str) -> ObjectId {
7165 ObjectId::from_hex(&git_stdout(dir, &["rev-parse", rev], None)).expect("oid")
7166 }
7167
7168 #[cfg_attr(
7169 not(feature = "full-tests"),
7170 ignore = "starts system git or is heavy; run with --features full-tests"
7171 )]
7172 #[test]
7173 fn packed_commit_header_block_parses_like_the_whole_commit() {
7174 let _guard = pack_cache_test_guard();
7175 clear_pack_cache();
7176 let dir = TempDir::new().unwrap();
7177 let root = dir.path();
7178 git(root, &["init", "-q"]);
7179 fs::write(root.join("f"), "one\n").unwrap();
7180 git(root, &["add", "f"]);
7181 git(root, &["commit", "-qm", "first"]);
7182 fs::write(root.join("f"), "two\n").unwrap();
7183 let long_message = format!("second\n\n{}", "a long body line\n".repeat(200));
7184 git(root, &["commit", "-qam", &long_message]);
7185 git(root, &["tag", "-a", "-m", "annotated", "v1"]);
7186 let signature: String = (0..60)
7187 .map(|i| format!(" signature line {i:03} with some padding text\n"))
7188 .collect();
7189 let signed_body = format!(
7190 "tree {}\nparent {}\nauthor t <t@example.com> 1 +0000\n\
7191 committer t <t@example.com> 2 +0000\ngpgsig -----BEGIN-----\n{signature} \
7192 -----END-----\n\nsigned\n",
7193 oid_of(root, "HEAD^{tree}"),
7194 oid_of(root, "HEAD"),
7195 );
7196 let signed = git_stdout(
7197 root,
7198 &["hash-object", "-t", "commit", "-w", "--stdin"],
7199 Some(&signed_body),
7200 );
7201 git(root, &["update-ref", "refs/heads/signed", &signed]);
7202 git(root, &["repack", "-a", "-d", "--window=0"]);
7203 fs::write(root.join("f"), "three\n").unwrap();
7204 git(root, &["commit", "-qam", "second pack"]);
7205 git(root, &["repack", "-d", "--window=0"]);
7206 fs::write(root.join("f"), "four\n").unwrap();
7207 git(root, &["commit", "-qam", "loose"]);
7208
7209 let objects = root.join(".git/objects");
7210 let mut buf = Vec::new();
7211 for rev in ["HEAD~3", "HEAD~2", "HEAD~1"] {
7212 let oid = oid_of(root, rev);
7213 assert!(
7214 read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7215 "{rev} is a whole packed commit"
7216 );
7217 assert!(buf.ends_with(b"\n\n"), "{rev} ends at the blank line");
7218 let whole = git_stdout(root, &["cat-file", "commit", rev], None);
7219 let whole = crate::objects::CommitRef::parse(whole.as_bytes()).unwrap();
7220 let header = crate::objects::CommitRef::parse(&buf).unwrap();
7221 assert_eq!(header.tree(), whole.tree(), "{rev}");
7222 assert_eq!(header.parents(), whole.parents(), "{rev}");
7223 assert_eq!(header.committer_raw(), whole.committer_raw(), "{rev}");
7224 assert!(header.message_bytes().is_empty(), "{rev}");
7225 }
7226 let signed = ObjectId::from_hex(&signed).unwrap();
7227 for (oid, why) in [
7228 (signed, "headers longer than the inflate budget"),
7229 (oid_of(root, "v1"), "an annotated tag"),
7230 (oid_of(root, "HEAD~1^{tree}"), "a packed tree"),
7231 (oid_of(root, "HEAD"), "a loose commit"),
7232 ] {
7233 assert!(
7234 !read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7235 "{why}"
7236 );
7237 }
7238 }
7239
7240 fn init_repo_with_pack(dir: &Path) -> ObjectId {
7241 git(dir, &["init", "-q"]);
7242 std::fs::write(dir.join("blob.txt"), b"pack cached lookup fixture\n").unwrap();
7243 git(dir, &["add", "blob.txt"]);
7244 git(dir, &["commit", "-qm", "c"]);
7245 repack_all(dir);
7246 let hex = Command::new("git") .current_dir(dir)
7248 .args(["rev-parse", "HEAD^{tree}"])
7249 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7250 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7251 .output()
7252 .unwrap();
7253 let tree_hex = String::from_utf8(hex.stdout).unwrap();
7254 ObjectId::from_hex(tree_hex.trim()).unwrap()
7255 }
7256
7257 fn pack_dir(objects: &Path) -> PathBuf {
7258 objects.join("pack")
7259 }
7260
7261 fn v2_idx_path(objects: &Path) -> PathBuf {
7262 pack_dir(objects)
7263 .read_dir()
7264 .unwrap()
7265 .filter_map(|e| e.ok())
7266 .map(|e| e.path())
7267 .find(|p| p.extension().is_some_and(|x| x == "idx"))
7268 .expect("pack idx")
7269 }
7270
7271 fn convert_pack_idx_to_v1(objects: &Path) -> PathBuf {
7273 let idx_path = v2_idx_path(objects);
7274 let parsed = read_pack_index(&idx_path).expect("v2 idx");
7275 let mut body = Vec::new();
7276 for slot in parsed.fanout {
7277 body.extend_from_slice(&slot.to_be_bytes());
7278 }
7279 for entry in parsed.iter() {
7280 body.extend_from_slice(&(entry.offset().expect("off") as u32).to_be_bytes());
7281 body.extend_from_slice(entry.oid());
7282 }
7283 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7284 rewrite_test_file(&idx_path, &body);
7285 clear_pack_cache();
7286 idx_path
7287 }
7288
7289 fn rewrite_test_file(path: &Path, body: &[u8]) {
7290 let _ = fs::remove_file(path);
7291 fs::write(path, body).unwrap();
7292 }
7293
7294 fn synthetic_fanout_index(entries: &[(Vec<u8>, u64)]) -> PackIndex {
7295 let mut sorted = entries.to_vec();
7296 sorted.sort_by(|a, b| a.0.cmp(&b.0));
7297 let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
7298 let fanout = compute_fanout_from_oid_slices(&slices);
7299 let mut body = Vec::new();
7300 for f in fanout {
7301 body.extend_from_slice(&f.to_be_bytes());
7302 }
7303 for (oid, off) in sorted {
7304 body.extend_from_slice(&(off as u32).to_be_bytes());
7305 body.extend_from_slice(&oid);
7306 }
7307 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7308 parse_pack_index_bytes(Path::new("synthetic.idx"), body, false).expect("v1 idx")
7309 }
7310
7311 #[test]
7312 fn find_offset_fanout_bucket_first_last_and_miss() {
7313 let first = vec![0x05u8; 20];
7314 let mut mid = vec![0x05u8; 20];
7315 mid[19] = 1;
7316 let mut last = vec![0x05u8; 20];
7317 last[19] = 2;
7318 let miss = vec![0x06u8; 20];
7319 let idx =
7320 synthetic_fanout_index(&[(first.clone(), 12), (mid.clone(), 24), (last.clone(), 36)]);
7321
7322 let oid_first = ObjectId::from_bytes(&first).unwrap();
7323 let oid_mid = ObjectId::from_bytes(&mid).unwrap();
7324 let oid_last = ObjectId::from_bytes(&last).unwrap();
7325 let oid_miss = ObjectId::from_bytes(&miss).unwrap();
7326
7327 assert_eq!(idx.find_offset(&oid_first).expect("f"), Some(12));
7328 assert_eq!(idx.find_offset(&oid_mid).expect("m"), Some(24));
7329 assert_eq!(idx.find_offset(&oid_last).expect("l"), Some(36));
7330 assert_eq!(idx.find_offset(&oid_miss).expect("miss"), None);
7331 }
7332
7333 #[cfg_attr(
7334 not(feature = "full-tests"),
7335 ignore = "starts system git or is heavy; run with --features full-tests"
7336 )]
7337 #[test]
7338 fn cached_lookup_hit_v2_idx() {
7339 let _guard = pack_cache_test_guard();
7340 clear_pack_cache();
7341 let dir = TempDir::new().unwrap();
7342 let tree = init_repo_with_pack(dir.path());
7343 let objects = dir.path().join(".git").join("objects");
7344 let got = read_object_from_packs(&objects, &tree).expect("read tree from v2 pack");
7345 assert_eq!(got.kind, ObjectKind::Tree);
7346 let again = read_object_from_packs(&objects, &tree).expect("cached read");
7348 assert_eq!(again.data, got.data);
7349 }
7350
7351 #[cfg_attr(
7352 not(feature = "full-tests"),
7353 ignore = "starts system git or is heavy; run with --features full-tests"
7354 )]
7355 #[test]
7356 fn cached_lookup_hit_v1_idx() {
7357 let _guard = pack_cache_test_guard();
7358 clear_pack_cache();
7359 let dir = TempDir::new().unwrap();
7360 let tree = init_repo_with_pack(dir.path());
7361 let objects = dir.path().join(".git").join("objects");
7362 let v1 = convert_pack_idx_to_v1(&objects);
7363 assert!(pack_index_is_v1(&v1));
7364 let idx = read_pack_index_cached(&v1).expect("parse v1 idx");
7365 let off = idx
7366 .find_offset(&tree)
7367 .expect("fanout lookup in v1 idx")
7368 .expect("off");
7369 assert!(off > 0);
7370 let obj = read_object_from_pack(&idx, &tree).expect("read via v1 index");
7371 assert_eq!(obj.kind, ObjectKind::Tree);
7372 }
7373
7374 #[cfg_attr(
7375 not(feature = "full-tests"),
7376 ignore = "starts system git or is heavy; run with --features full-tests"
7377 )]
7378 #[test]
7379 fn cached_lookup_miss() {
7380 let _guard = pack_cache_test_guard();
7381 clear_pack_cache();
7382 let dir = TempDir::new().unwrap();
7383 init_repo_with_pack(dir.path());
7384 let objects = dir.path().join(".git").join("objects");
7385 let miss = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
7386 assert!(read_object_from_packs(&objects, &miss).is_err());
7387 assert!(
7388 try_read_object_from_local_packs_cached(&objects, &miss, || {
7389 Err(Error::ObjectNotFound(miss.to_hex()))
7390 })
7391 .unwrap()
7392 .is_none()
7393 );
7394 }
7395
7396 #[cfg_attr(
7397 not(feature = "full-tests"),
7398 ignore = "starts system git or is heavy; run with --features full-tests"
7399 )]
7400 #[test]
7401 fn cached_lookup_loose_only() {
7402 let _guard = pack_cache_test_guard();
7403 clear_pack_cache();
7404 let dir = TempDir::new().unwrap();
7405 git(dir.path(), &["init", "-q"]);
7406 let out = Command::new("git") .current_dir(dir.path())
7408 .args(["hash-object", "-w", "--stdin"])
7409 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7410 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7411 .stdin(std::process::Stdio::piped())
7412 .stdout(std::process::Stdio::piped())
7413 .spawn()
7414 .and_then(|mut c| {
7415 use std::io::Write;
7416 c.stdin
7417 .as_mut()
7418 .unwrap()
7419 .write_all(b"loose-only\n")
7420 .unwrap();
7421 c.wait_with_output()
7422 })
7423 .unwrap();
7424 assert!(out.status.success());
7425 let oid = ObjectId::from_hex(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
7426 let objects = dir.path().join(".git").join("objects");
7427 assert!(try_read_object_from_local_packs_cached(&objects, &oid, || {
7428 Err(Error::ObjectNotFound(oid.to_hex()))
7429 })
7430 .unwrap()
7431 .is_none());
7432 }
7433
7434 #[cfg_attr(
7435 not(feature = "full-tests"),
7436 ignore = "starts system git or is heavy; run with --features full-tests"
7437 )]
7438 #[test]
7439 fn cached_lookup_multiple_packs() {
7440 let _guard = pack_cache_test_guard();
7441 clear_pack_cache();
7442 let dir = TempDir::new().unwrap();
7443 git(dir.path(), &["init", "-q"]);
7444 std::fs::write(dir.path().join("a.txt"), b"a").unwrap();
7445 git(dir.path(), &["add", "a.txt"]);
7446 git(dir.path(), &["commit", "-qm", "a"]);
7447 repack_all(dir.path());
7448 let oid_a = ObjectId::from_hex(
7449 String::from_utf8(
7450 Command::new("git") .current_dir(dir.path())
7452 .args(["rev-parse", "HEAD:a.txt"])
7453 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7454 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7455 .output()
7456 .unwrap()
7457 .stdout,
7458 )
7459 .unwrap()
7460 .trim(),
7461 )
7462 .unwrap();
7463 std::fs::write(dir.path().join("b.txt"), b"b").unwrap();
7464 git(dir.path(), &["add", "b.txt"]);
7465 git(dir.path(), &["commit", "-qm", "b"]);
7466 git(dir.path(), &["repack", "-d"]);
7467 let oid_b = ObjectId::from_hex(
7468 String::from_utf8(
7469 Command::new("git") .current_dir(dir.path())
7471 .args(["rev-parse", "HEAD:b.txt"])
7472 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7473 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7474 .output()
7475 .unwrap()
7476 .stdout,
7477 )
7478 .unwrap()
7479 .trim(),
7480 )
7481 .unwrap();
7482 let objects = dir.path().join(".git").join("objects");
7483 let pack_count = pack_dir(&objects)
7484 .read_dir()
7485 .unwrap()
7486 .filter_map(|e| e.ok())
7487 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
7488 .count();
7489 assert!(
7490 pack_count >= 2,
7491 "expected multiple pack files, got {pack_count}"
7492 );
7493 let obj_a = read_object_from_packs(&objects, &oid_a).expect("pack a");
7494 let obj_b = read_object_from_packs(&objects, &oid_b).expect("pack b");
7495 assert_eq!(obj_a.kind, ObjectKind::Blob);
7496 assert_eq!(obj_b.kind, ObjectKind::Blob);
7497 }
7498
7499 #[cfg_attr(
7500 not(feature = "full-tests"),
7501 ignore = "starts system git or is heavy; run with --features full-tests"
7502 )]
7503 #[test]
7504 fn packed_delta_base_oid_uses_cached_index() {
7505 let _guard = pack_cache_test_guard();
7506 clear_pack_cache();
7507 let dir = TempDir::new().unwrap();
7508 init_repo_with_pack(dir.path());
7509 let objects = dir.path().join(".git").join("objects");
7510 let miss = ObjectId::from_hex("0101010101010101010101010101010101010101").unwrap();
7511 assert!(packed_delta_base_oid(&objects, &miss).unwrap().is_none());
7512 }
7513
7514 #[test]
7515 fn content_addressed_pack_bytes_skip_stat_revalidation() {
7516 let _guard = pack_cache_test_guard();
7517 clear_pack_cache();
7518 let dir = tempfile::tempdir().expect("tempdir");
7519 let pack_path = dir
7520 .path()
7521 .join("pack-1111111111111111111111111111111111111111.pack");
7522 let v1 = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
7523 std::fs::write(&pack_path, v1).expect("write v1");
7524 let cached = read_pack_bytes_cached(&pack_path).expect("prime cache");
7525 assert_eq!(&cached[..], v1);
7526
7527 let v2 = b"PACK-replaced-by-repack";
7528 std::fs::write(&pack_path, v2).expect("overwrite on disk");
7529 let again = read_pack_bytes_cached(&pack_path).expect("cached without stat");
7530 assert_eq!(
7531 &again[..],
7532 v1,
7533 "content-addressed name pins cached bytes in-process"
7534 );
7535 }
7536
7537 #[test]
7538 fn temporary_pack_bytes_served_from_cache_until_cleared() {
7539 let _guard = pack_cache_test_guard();
7540 clear_pack_cache();
7541 let dir = tempfile::tempdir().expect("tempdir");
7542 for pack_path in [
7543 dir.path().join("tmp_pack_abc123.pack"),
7544 dir.path().join("pack-custom.pack"),
7545 ] {
7546 clear_pack_cache();
7547 let v1 = b"PACK-temp-v1";
7548 std::fs::write(&pack_path, v1).expect("write v1");
7549 let _ = read_pack_bytes_cached(&pack_path).expect("prime cache");
7550
7551 let v2 = b"PACK-temp-v2-longer-body";
7552 std::fs::write(&pack_path, v2).expect("overwrite pack");
7553 let stale = read_pack_bytes_cached(&pack_path).expect("cache hit");
7554 assert_eq!(
7555 &stale[..],
7556 v1,
7557 "{} serves cached bytes without stat on hit",
7558 pack_path.display()
7559 );
7560 clear_pack_cache();
7561 let fresh = read_pack_bytes_cached(&pack_path).expect("after clear");
7562 assert_eq!(
7563 &fresh[..],
7564 v2,
7565 "{} reloads from disk after clear",
7566 pack_path.display()
7567 );
7568 }
7569 }
7570 #[cfg_attr(
7571 not(feature = "full-tests"),
7572 ignore = "starts system git or is heavy; run with --features full-tests"
7573 )]
7574 #[test]
7575 fn system_git_repack_requires_clear_pack_cache_for_fresh_listing() {
7576 let _guard = pack_cache_test_guard();
7577 clear_pack_cache();
7578 let dir = TempDir::new().unwrap();
7579 init_repo_with_pack(dir.path());
7580 let objects = dir.path().join(".git").join("objects");
7581 read_local_pack_indexes_cached(&objects).expect("warm listing");
7582 for entry in pack_dir(&objects).read_dir().unwrap().flatten() {
7583 let path = entry.path();
7584 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
7585 fs::remove_file(path).unwrap();
7586 }
7587 }
7588 clear_pack_cache();
7589 let indexes =
7590 read_local_pack_indexes_cached(&objects).expect("list after repack-like delete");
7591 assert!(
7592 indexes.is_empty(),
7593 "callers must clear_pack_cache after removing pack files"
7594 );
7595 }
7596
7597 fn test_oid(n: u8) -> ObjectId {
7598 ObjectId::from_hex(&format!("{n:040x}")).unwrap()
7599 }
7600
7601 fn max_delta_map_chain_edges(map: &HashMap<ObjectId, ObjectId>) -> usize {
7602 let value_set: HashSet<ObjectId> = map.values().copied().collect();
7603 let tips: Vec<ObjectId> = map
7604 .keys()
7605 .copied()
7606 .filter(|k| !value_set.contains(k))
7607 .collect();
7608 let mut max = 0usize;
7609 for tip in tips {
7610 let mut edges = 0usize;
7611 let mut cur = tip;
7612 let mut seen = HashSet::new();
7613 while seen.insert(cur) {
7614 let Some(&b) = map.get(&cur) else {
7615 break;
7616 };
7617 edges += 1;
7618 cur = b;
7619 }
7620 max = max.max(edges);
7621 }
7622 max
7623 }
7624
7625 #[test]
7626 fn apply_delta_depth_limit_hits_exact_cap_on_seventy_edge_chain() {
7627 let n = 70usize;
7628 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7629 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7630 for i in 1..n {
7631 map.insert(oids[i], oids[i - 1]);
7632 }
7633 let original = map.clone();
7634 apply_delta_depth_limit(&mut map, 50);
7635 assert_eq!(max_delta_map_chain_edges(&map), 50);
7636 let preserved = map
7637 .iter()
7638 .filter(|(t, b)| original.get(t) == Some(b))
7639 .count();
7640 assert!(
7641 preserved > 40,
7642 "non-cut edges should keep original bases (preserved {preserved})"
7643 );
7644 }
7645
7646 #[test]
7647 fn delta_cycle_breaks_drops_smallest_oid_of_each_cycle_only() {
7648 let o = |n: u8| test_oid(n);
7649 let map = HashMap::from([
7652 (o(1), o(2)),
7653 (o(2), o(1)),
7654 (o(3), o(5)),
7655 (o(4), o(3)),
7656 (o(5), o(4)),
7657 (o(6), o(4)),
7658 (o(8), o(7)),
7659 ]);
7660 let mut keys: Vec<ObjectId> = map.keys().copied().collect();
7661 for order in [false, true] {
7662 keys.sort();
7663 if order {
7664 keys.reverse();
7665 }
7666 let mut breaks = delta_cycle_breaks(keys.iter().copied(), |oid| map.get(oid).copied());
7667 breaks.sort();
7668 assert_eq!(breaks, vec![o(1), o(3)], "independent of walk order");
7669 }
7670 }
7671
7672 #[test]
7673 fn delta_cycle_breaks_handles_self_delta_and_acyclic_maps() {
7674 let a = test_oid(9);
7675 assert_eq!(delta_cycle_breaks([a], |_| Some(a)), vec![a]);
7676 let chain: HashMap<ObjectId, ObjectId> =
7677 (1..60u8).map(|i| (test_oid(i), test_oid(i - 1))).collect();
7678 assert!(delta_cycle_breaks(chain.keys().copied(), |o| chain.get(o).copied()).is_empty());
7679 }
7680
7681 #[test]
7682 fn apply_delta_depth_limit_breaks_cycles_shorter_than_depth() {
7683 let (a, b, c, d) = (test_oid(1), test_oid(2), test_oid(3), test_oid(4));
7684 let mut map = HashMap::from([(a, b), (b, a), (c, b), (d, c)]);
7686 apply_delta_depth_limit(&mut map, 50);
7687 assert_eq!(map, HashMap::from([(b, a), (c, b), (d, c)]));
7688 assert_eq!(max_delta_map_chain_edges(&map), 3);
7689 }
7690
7691 #[test]
7692 fn apply_delta_depth_limit_leaves_short_chains_untouched() {
7693 let a = test_oid(1);
7694 let b = test_oid(2);
7695 let c = test_oid(3);
7696 let mut map = HashMap::from([(b, a), (c, b)]);
7697 apply_delta_depth_limit(&mut map, 50);
7698 assert_eq!(map.get(&b), Some(&a));
7699 assert_eq!(map.get(&c), Some(&b));
7700 assert_eq!(max_delta_map_chain_edges(&map), 2);
7701 }
7702
7703 #[test]
7704 fn apply_delta_depth_limit_caps_long_chain_and_preserves_blobs() {
7705 let n = 120usize;
7706 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7707 let mut blobs: HashMap<ObjectId, Vec<u8>> = HashMap::new();
7708 blobs.insert(oids[0], b"base".to_vec());
7709 for i in 1..n {
7710 let mut next = blobs[&oids[i - 1]].clone();
7711 next.push(i as u8);
7712 blobs.insert(oids[i], next);
7713 }
7714 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7715 for i in 1..n {
7716 map.insert(oids[i], oids[i - 1]);
7717 }
7718 let original = map.clone();
7719 apply_delta_depth_limit(&mut map, 50);
7720 assert_eq!(
7721 max_delta_map_chain_edges(&map),
7722 50,
7723 "long chain must cap at exactly max_depth edges"
7724 );
7725 let preserved = map
7726 .iter()
7727 .filter(|(target, base)| original.get(target) == Some(base))
7728 .count();
7729 assert!(
7730 preserved > 60,
7731 "segment links should stay on original bases (preserved {preserved} of {})",
7732 map.len()
7733 );
7734
7735 use crate::delta_encode::encode_prefix_extension_delta;
7736 use crate::unpack_objects::apply_delta;
7737
7738 for (&target, &base) in &map {
7739 let delta = encode_prefix_extension_delta(&blobs[&base], &blobs[&target]).unwrap();
7740 let got = apply_delta(&blobs[&base], &delta).unwrap();
7741 assert_eq!(got, blobs[&target]);
7742 }
7743 }
7744
7745 #[cfg_attr(
7746 not(feature = "full-tests"),
7747 ignore = "starts system git or is heavy; run with --features full-tests"
7748 )]
7749 #[test]
7750 fn v2_idx_crc_matches_crc32fast_over_entry_bytes() {
7751 let _guard = pack_cache_test_guard();
7752 clear_pack_cache();
7753 let dir = TempDir::new().unwrap();
7754 let tree = init_repo_with_pack(dir.path());
7755 let objects = dir.path().join(".git").join("objects");
7756 let idx_path = v2_idx_path(&objects);
7757 let idx = read_pack_index(&idx_path).expect("read idx");
7758 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
7759 for entry in idx.iter() {
7760 let crc = entry.crc32().expect("v2 idx entry crc");
7761 let off = entry.offset().expect("entry off");
7762 let start = off as usize;
7763 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7764 .expect("entry end")
7765 .expect("end");
7766 assert_eq!(crc32fast::hash(&pack_bytes[start..end]), crc);
7767 }
7768 let _ = tree;
7769 }
7770
7771 #[cfg_attr(
7772 not(feature = "full-tests"),
7773 ignore = "starts system git or is heavy; run with --features full-tests"
7774 )]
7775 #[test]
7776 fn packed_full_object_slice_returns_on_disk_bytes() {
7777 let _guard = pack_cache_test_guard();
7778 clear_pack_cache();
7779 let dir = TempDir::new().unwrap();
7780 let tree = init_repo_with_pack(dir.path());
7781 let objects = dir.path().join(".git").join("objects");
7782 let idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7783 let pack_bytes = std::fs::read(&idx.pack_path).expect("pack");
7784 let off = idx.find_offset(&tree).expect("tree in pack").expect("off");
7785 let start = off as usize;
7786 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7787 .expect("entry end")
7788 .expect("end");
7789 let expected = &pack_bytes[start..end];
7790 let slice = packed_full_object_slice(&objects, &tree)
7791 .expect("lookup")
7792 .expect("full tree slice");
7793 assert_eq!(slice, expected);
7794 }
7795
7796 fn touch_mtime(path: &Path, when: filetime::FileTime) {
7797 filetime::set_file_mtime(path, when).expect("set mtime");
7798 }
7799
7800 #[cfg_attr(
7801 not(feature = "full-tests"),
7802 ignore = "starts system git or is heavy; run with --features full-tests"
7803 )]
7804 #[test]
7805 fn packed_full_object_slice_crc_mismatch_falls_back_to_redundant_pack() {
7806 let _guard = pack_cache_test_guard();
7807 clear_pack_cache();
7808 let dir = TempDir::new().unwrap();
7809 let tree = init_repo_with_pack(dir.path());
7810 let objects = dir.path().join(".git").join("objects");
7811 let pack_dir = pack_dir(&objects);
7812 let primary_idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7813 let primary_pack = primary_idx.pack_path.clone();
7814 let primary_idx_path = primary_idx.idx_path.clone();
7815
7816 let redundant_pack = pack_dir.join("pack-redundant.pack");
7817 let redundant_idx = pack_dir.join("pack-redundant.idx");
7818 fs::copy(&primary_pack, &redundant_pack).expect("copy pack");
7819 fs::copy(&primary_idx_path, &redundant_idx).expect("copy idx");
7820
7821 let older = filetime::FileTime::from_unix_time(1_700_000_000, 0);
7822 let newer = filetime::FileTime::from_unix_time(1_700_000_100, 0);
7823 touch_mtime(&primary_pack, older);
7824 touch_mtime(&primary_idx_path, older);
7825 touch_mtime(&redundant_pack, newer);
7826 touch_mtime(&redundant_idx, newer);
7827 clear_pack_cache();
7828
7829 let off = primary_idx
7830 .find_offset(&tree)
7831 .expect("tree offset")
7832 .expect("off");
7833 let start = off as usize;
7834 let end = super::pack_entry_raw_end(&primary_idx, &fs::read(&primary_pack).unwrap(), off)
7835 .expect("entry end")
7836 .expect("end");
7837 let flip = start + (end - start) / 2;
7838 let mut corrupt = fs::read(&redundant_pack).expect("read redundant");
7839 corrupt[flip] ^= 0x01;
7840 rewrite_test_file(&redundant_pack, &corrupt);
7841 clear_pack_cache();
7842
7843 let recorded_crc = read_pack_index(&redundant_idx)
7844 .expect("redundant idx")
7845 .crc32_for_pack_offset(off)
7846 .expect("crc")
7847 .expect("crc value");
7848 let corrupt_slice = &corrupt[start..end];
7849 assert_ne!(
7850 crc32fast::hash(corrupt_slice),
7851 recorded_crc,
7852 "test must flip bytes without updating idx CRC"
7853 );
7854 assert!(
7855 packed_full_object_slice(&objects, &tree)
7856 .expect("lookup")
7857 .is_some(),
7858 "valid redundant pack should serve the slice after CRC rejects corrupt copy"
7859 );
7860
7861 let obj = read_object_from_packs(&objects, &tree).expect("read tree");
7862 assert_eq!(obj.kind, ObjectKind::Tree);
7863 let slice = packed_full_object_slice(&objects, &tree)
7864 .expect("lookup")
7865 .expect("slice");
7866 let good_pack = fs::read(&primary_pack).expect("primary pack");
7867 assert_eq!(&slice, &good_pack[start..end]);
7868 }
7869
7870 #[test]
7871 fn wanted_index_entries_lookups_match_a_full_scan() {
7872 let tmp = TempDir::new().unwrap();
7873 let bare = grit_test_support::git_packed_history(tmp.path(), 30);
7874 let objects = bare.join("objects");
7875 let indexes = read_local_pack_indexes_cached(&objects).unwrap();
7876 assert_eq!(indexes.len(), 1);
7877 let idx = &indexes[0];
7878 let all: Vec<(ObjectId, u64)> = idx
7879 .iter()
7880 .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
7881 .collect();
7882 assert!(all.len() > 64);
7883 let absent = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
7884 let sorted = |mut v: Vec<(ObjectId, u64)>| {
7885 v.sort();
7886 v
7887 };
7888
7889 let few: HashSet<ObjectId> = [all[3].0, all[all.len() - 1].0, absent].into();
7890 let looked_up = wanted_index_entries(idx, few.iter(), |oid| few.contains(oid)).unwrap();
7891 assert_eq!(sorted(looked_up), sorted(vec![all[3], all[all.len() - 1]]));
7892
7893 let many: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).chain([absent]).collect();
7894 let scanned = wanted_index_entries(idx, many.iter(), |oid| many.contains(oid)).unwrap();
7895 assert_eq!(sorted(scanned), sorted(all));
7896
7897 let slots = locate_packed_objects(&objects, &few.iter().copied().collect()).unwrap();
7898 assert_eq!(slots.len(), 2);
7899 assert!(!slots.contains_key(&absent));
7900 }
7901}