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, apply_delta_prefix_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
74struct PackReadDamage {
85 pack: PathBuf,
86 offset: u64,
87 err: Error,
88}
89
90fn finish_pack_object_lookup(oid: &ObjectId, last: Option<PackReadDamage>) -> Error {
91 match last {
92 Some(d) => Error::wrap_packed_read_failure(*oid, d.pack, d.offset, d.err),
93 None => Error::ObjectNotFound(oid.to_hex()),
94 }
95}
96
97fn wrap_pack_cache_read_err(
98 objects_dir: &Path,
99 idx: &PackIndex,
100 oid: &ObjectId,
101 err: Error,
102) -> Error {
103 let offset = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)
104 .ok()
105 .flatten()
106 .unwrap_or(0);
107 Error::wrap_packed_read_failure(*oid, idx.pack_path.clone(), offset, err)
108}
109
110pub(crate) mod pack_cache {
111 use super::{read_pack_index_no_verify, Error, ObjectKind, PackIndex, Result};
112 use crate::pack_map::{fingerprint_from_file, PackData, PackFingerprint};
113 use std::collections::{HashMap, HashSet, VecDeque};
114 use std::fs;
115 use std::hash::{Hash, Hasher};
116 use std::io;
117 use std::path::{Path, PathBuf};
118 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
119 use std::sync::{Arc, Mutex, Weak};
120 use std::time::SystemTime;
121
122 const DELTA_LRU_SHARD_COUNT: usize = 32;
123 const BOUNDED_PACK_READERS: usize = 32;
126
127 type ThreadLocalDirListing = Option<(PathBuf, SystemTime, Vec<Arc<PackIndex>>)>;
128
129 enum CachedPackBytes {
130 Retained(Arc<PackData>),
131 Positioned(Weak<PackData>),
132 }
133
134 impl CachedPackBytes {
135 fn get(&self) -> Option<Arc<PackData>> {
136 match self {
137 Self::Retained(bytes) => Some(Arc::clone(bytes)),
138 Self::Positioned(bytes) => bytes.upgrade(),
139 }
140 }
141 }
142
143 thread_local! {
145 static PACK_DIR_INDEXES: std::cell::RefCell<ThreadLocalDirListing> =
146 const { std::cell::RefCell::new(None) };
147 static PACK_HIT_INDEX: std::cell::RefCell<Option<(PathBuf, Arc<PackIndex>)>> =
148 const { std::cell::RefCell::new(None) };
149 static PACK_BYTES: std::cell::RefCell<HashMap<PathBuf, CachedPackBytes>> =
150 std::cell::RefCell::new(HashMap::new());
151 static PACK_INDEXES: std::cell::RefCell<HashMap<PathBuf, Arc<PackIndex>>> =
152 std::cell::RefCell::new(HashMap::new());
153 static PACK_IDS: std::cell::RefCell<hashbrown::HashMap<std::ffi::OsString, u32>> =
156 std::cell::RefCell::new(hashbrown::HashMap::new());
157 static PACK_COUNT_VALIDATED: std::cell::RefCell<HashSet<PathBuf>> =
158 std::cell::RefCell::new(HashSet::new());
159 static PACK_DIR_LISTING_TRUSTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
160 static THREAD_WARM_EPOCH: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
162 static THREAD_WARM_STORE: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
164 static PACK_MONOTONIC_CURSOR: std::cell::RefCell<Option<(PathBuf, usize)>> =
165 const { std::cell::RefCell::new(None) };
166 static SINGLE_READER_MEMO: std::cell::RefCell<Option<SingleReaderMemo>> =
167 const { std::cell::RefCell::new(None) };
168 }
169
170 struct SingleReaderMemo {
175 objects_dir: PathBuf,
176 pack_dir: PathBuf,
177 idx: Arc<PackIndex>,
178 pack_bytes: Arc<PackData>,
179 pack_id: u32,
180 }
181
182 fn forget_single_reader_memo() {
183 SINGLE_READER_MEMO.with(|cell| *cell.borrow_mut() = None);
184 }
185
186 pub(crate) fn remember_single_reader(
188 objects_dir: &Path,
189 idx: &Arc<PackIndex>,
190 pack_bytes: &Arc<PackData>,
191 pack_id: u32,
192 ) {
193 SINGLE_READER_MEMO.with(|cell| {
194 *cell.borrow_mut() = Some(SingleReaderMemo {
195 objects_dir: objects_dir.to_path_buf(),
196 pack_dir: objects_dir.join("pack"),
197 idx: Arc::clone(idx),
198 pack_bytes: Arc::clone(pack_bytes),
199 pack_id,
200 });
201 });
202 }
203
204 pub(crate) fn single_reader_for(
210 objects_dir: &Path,
211 ) -> Option<(Arc<PackIndex>, Arc<PackData>, u32)> {
212 if !PACK_DIR_LISTING_TRUSTED.get() {
213 return None;
214 }
215 SINGLE_READER_MEMO.with(|memo| {
216 let memo = memo.borrow();
217 let memo = memo.as_ref()?;
218 if memo.objects_dir.as_os_str() != objects_dir.as_os_str() {
219 return None;
220 }
221 let listing_unchanged = PACK_DIR_INDEXES.with(|cell| {
222 cell.borrow().as_ref().is_some_and(|(dir, _, indexes)| {
223 dir.as_os_str() == memo.pack_dir.as_os_str()
224 && indexes.len() == 1
225 && Arc::ptr_eq(&indexes[0], &memo.idx)
226 })
227 });
228 if !listing_unchanged {
229 return None;
230 }
231 let hit_current = PACK_HIT_INDEX.with(|cell| {
232 cell.borrow().as_ref().is_some_and(|(dir, idx)| {
233 dir.as_os_str() == objects_dir.as_os_str() && Arc::ptr_eq(idx, &memo.idx)
234 })
235 });
236 if !hit_current {
237 return None;
238 }
239 Some((
240 Arc::clone(&memo.idx),
241 Arc::clone(&memo.pack_bytes),
242 memo.pack_id,
243 ))
244 })
245 }
246
247 fn store_thread_local_key(store: &crate::pack_store::PackStore) -> u64 {
248 store.id()
249 }
250
251 pub(crate) fn clear_thread_local_dir_indexes() {
252 PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = None);
253 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
254 PACK_BYTES.with(|cell| cell.borrow_mut().clear());
255 PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
256 PACK_IDS.with(|cell| cell.borrow_mut().clear());
257 PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
258 PACK_DIR_LISTING_TRUSTED.set(false);
259 THREAD_WARM_EPOCH.set(0);
260 THREAD_WARM_STORE.set(0);
261 super::clear_pack_read_info_cache();
262 PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = None);
263 forget_single_reader_memo();
264 }
265
266 pub(crate) fn try_read_object_via_single_pack_reader(
267 objects_dir: &Path,
268 oid: &crate::objects::ObjectId,
269 ) -> Result<Option<crate::objects::Object>> {
270 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
271 return Ok(None);
272 }
273 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
274 return Ok(None);
275 };
276 match reader.read_object(oid) {
277 Ok(obj) => Ok(Some(obj)),
278 Err(Error::ObjectNotFound(_)) => Ok(None),
279 Err(err) => Err(super::wrap_pack_cache_read_err(
280 objects_dir,
281 reader.idx.as_ref(),
282 oid,
283 err,
284 )),
285 }
286 }
287
288 pub(crate) fn try_read_object_via_single_pack_reader_unless_whole_larger(
292 objects_dir: &Path,
293 oid: &crate::objects::ObjectId,
294 limit: u64,
295 ) -> Result<Option<super::BoundedRead>> {
296 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
297 return Ok(None);
298 }
299 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
300 return Ok(None);
301 };
302 match reader.read_object_unless_whole_larger(oid, limit) {
303 Ok(read) => Ok(Some(read)),
304 Err(Error::ObjectNotFound(_)) => Ok(None),
305 Err(err) => Err(super::wrap_pack_cache_read_err(
306 objects_dir,
307 reader.idx.as_ref(),
308 oid,
309 err,
310 )),
311 }
312 }
313
314 pub(crate) fn try_read_commit_through_subject_via_single_pack_reader(
318 objects_dir: &Path,
319 oid: &crate::objects::ObjectId,
320 ) -> Result<Option<Vec<u8>>> {
321 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
322 return Ok(None);
323 }
324 sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
325 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
326 return Ok(None);
327 };
328 reader.read_commit_through_subject(oid)
329 }
330
331 pub(crate) fn try_read_object_via_single_pack_reader_into(
332 objects_dir: &Path,
333 oid: &crate::objects::ObjectId,
334 populate_delta_lru: bool,
335 buf: &mut Vec<u8>,
336 ) -> Result<Option<crate::objects::ObjectKind>> {
337 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
338 return Ok(None);
339 }
340 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
341 return Ok(None);
342 };
343 match reader.read_object_into_with_delta_policy(oid, populate_delta_lru, buf) {
344 Ok(kind) => Ok(Some(kind)),
345 Err(Error::ObjectNotFound(_)) => Ok(None),
346 Err(err) => Err(super::wrap_pack_cache_read_err(
347 objects_dir,
348 reader.idx.as_ref(),
349 oid,
350 err,
351 )),
352 }
353 }
354
355 pub(crate) fn clear_thread_local_after_invalidation(new_epoch: u64) {
357 clear_thread_local_dir_indexes();
358 THREAD_WARM_EPOCH.set(new_epoch);
359 }
360
361 pub(crate) fn sync_thread_local_with_store(store: &crate::pack_store::PackStore) {
362 if !crate::pack_store::PackStore::has_context() {
363 return;
364 }
365 let epoch = store.cache_epoch();
366 let store_key = store_thread_local_key(store);
367 if THREAD_WARM_EPOCH.get() == epoch && THREAD_WARM_STORE.get() == store_key {
368 return;
369 }
370 clear_thread_local_dir_indexes();
371 THREAD_WARM_EPOCH.set(epoch);
372 THREAD_WARM_STORE.set(store_key);
373 }
374
375 fn drop_thread_local_dir_listing(pack_dir: &Path) {
376 PACK_DIR_INDEXES.with(|cell| {
377 if cell
378 .borrow()
379 .as_ref()
380 .is_some_and(|(dir, _, _)| dir == pack_dir)
381 {
382 *cell.borrow_mut() = None;
383 PACK_DIR_LISTING_TRUSTED.set(false);
384 }
385 });
386 }
387
388 fn drop_thread_local_pack_bytes(pack_path: &Path) {
389 PACK_BYTES.with(|cell| {
390 cell.borrow_mut().remove(pack_path);
391 });
392 forget_single_reader_memo();
393 }
394
395 fn invalidate_thread_local_for_pack_path_change(pack_path: &Path) {
397 let idx_path = pack_path.with_extension("idx");
398 drop_thread_local_pack_bytes(pack_path);
399 PACK_INDEXES.with(|cell| {
400 cell.borrow_mut().remove(&idx_path);
401 });
402 PACK_IDS.with(|cell| {
403 cell.borrow_mut().remove(pack_path.as_os_str());
404 });
405 PACK_COUNT_VALIDATED.with(|cell| {
406 cell.borrow_mut().remove(pack_path);
407 });
408 PACK_HIT_INDEX.with(|cell| {
409 let drop_hint = cell
410 .borrow()
411 .as_ref()
412 .is_some_and(|(_, idx)| idx.pack_path == pack_path || idx.idx_path == idx_path);
413 if drop_hint {
414 *cell.borrow_mut() = None;
415 }
416 });
417 if let Some(pack_dir) = pack_path.parent() {
418 drop_thread_local_dir_listing(pack_dir);
419 }
420 super::clear_pack_read_info_cache();
421 }
422
423 pub(crate) fn warm_thread_local_from_store(store: &crate::pack_store::PackStore) {
425 if !crate::pack_store::PackStore::has_context() {
426 return;
427 }
428 let epoch = store.cache_epoch();
429 let store_key = store_thread_local_key(store);
430 if THREAD_WARM_EPOCH.get() == epoch
431 && THREAD_WARM_STORE.get() == store_key
432 && PACK_DIR_LISTING_TRUSTED.get()
433 {
434 return;
435 }
436 PACK_BYTES.with(|cell| cell.borrow_mut().clear());
437 PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
438 PACK_IDS.with(|cell| cell.borrow_mut().clear());
439 PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
440 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
441 forget_single_reader_memo();
442 super::clear_pack_read_info_cache();
443 let objects_dir = store.objects_dir();
444 let pack_dir = objects_dir.join("pack");
445 store.with_catalog_read(|g| {
446 let listing = g
449 .by_dir
450 .get(&pack_dir)
451 .map(|cached| (pack_dir.clone(), cached.dir_mtime, cached.indexes.clone()));
452 PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = listing);
453 for (path, entry) in &g.by_pack {
454 remember_pack_bytes(path, &entry.bytes);
455 }
456 for (path, entry) in &g.by_idx {
457 remember_index(path, &entry.idx);
458 }
459 PACK_IDS.with(|cell| {
460 cell.borrow_mut().extend(
461 g.pack_ids
462 .iter()
463 .map(|(k, v)| (k.as_os_str().to_owned(), *v)),
464 );
465 });
466 PACK_COUNT_VALIDATED.with(|cell| {
467 cell.borrow_mut()
468 .extend(g.validated_pack_counts.iter().cloned());
469 });
470 PACK_DIR_LISTING_TRUSTED.set(true);
471 THREAD_WARM_EPOCH.set(store.cache_epoch());
472 THREAD_WARM_STORE.set(store_thread_local_key(store));
473 });
474 }
475
476 fn cached_pack_bytes(pack_path: &Path) -> Option<Arc<PackData>> {
477 if !crate::pack_store::PackStore::has_context() {
478 return None;
479 }
480 PACK_BYTES.with(|cell| cell.borrow().get(pack_path).and_then(CachedPackBytes::get))
481 }
482
483 fn remember_pack_bytes(pack_path: &Path, bytes: &Arc<PackData>) {
484 if !crate::pack_store::PackStore::has_context() {
487 return;
488 }
489 let cached = if bytes.uses_positioned_reads() {
490 CachedPackBytes::Positioned(Arc::downgrade(bytes))
491 } else {
492 CachedPackBytes::Retained(Arc::clone(bytes))
493 };
494 PACK_BYTES.with(|cell| {
495 cell.borrow_mut().insert(pack_path.to_path_buf(), cached);
496 });
497 forget_single_reader_memo();
498 }
499
500 pub(crate) fn remember_pack_hit(objects_dir: &Path, idx: &Arc<PackIndex>) {
501 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
502 return;
503 }
504 promote_pack_index(objects_dir, &idx.idx_path);
505 PACK_HIT_INDEX.with(|cell| {
506 *cell.borrow_mut() = Some((objects_dir.to_path_buf(), Arc::clone(idx)));
507 });
508 }
509
510 pub(crate) fn find_offset_in_pack_read_context(
511 objects_dir: &Path,
512 idx: &PackIndex,
513 oid: &crate::objects::ObjectId,
514 ) -> Result<Option<u64>> {
515 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
516 return idx.find_offset(oid);
517 }
518 PACK_MONOTONIC_CURSOR.with(|cell| {
519 let mut slot = cell.borrow_mut();
520 if slot.as_ref().is_none_or(|(dir, _)| dir != objects_dir) {
521 *slot = Some((objects_dir.to_path_buf(), 0));
522 }
523 let Some((_, cursor)) = slot.as_mut() else {
524 return idx.find_offset(oid);
525 };
526 idx.find_offset_monotonic(oid, cursor)
527 })
528 }
529
530 pub(crate) fn hinted_pack_index(objects_dir: &Path) -> Option<Arc<PackIndex>> {
533 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
534 return None;
535 }
536 PACK_HIT_INDEX.with(|cell| {
537 cell.borrow()
538 .as_ref()
539 .and_then(|(dir, idx)| (dir == objects_dir).then(|| Arc::clone(idx)))
540 })
541 }
542
543 pub(crate) fn try_read_from_hinted_pack(
544 objects_dir: &Path,
545 oid: &crate::objects::ObjectId,
546 ) -> Result<Option<crate::objects::Object>> {
547 let Some(idx) = hinted_pack_index(objects_dir) else {
548 return Ok(None);
549 };
550 let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
551 return Ok(None);
552 };
553 match super::read_object_from_pack_at_offset(&idx, offset) {
554 Ok(obj) => Ok(Some(obj)),
555 Err(Error::ObjectNotFound(_)) => Ok(None),
556 Err(err) if err.is_object_read_wrap_source() => Ok(None),
557 Err(err) => Err(err),
558 }
559 }
560
561 pub(crate) fn try_read_from_hinted_pack_into(
562 objects_dir: &Path,
563 oid: &crate::objects::ObjectId,
564 populate_delta_lru: bool,
565 buf: &mut Vec<u8>,
566 ) -> Result<Option<crate::objects::ObjectKind>> {
567 let Some(idx) = hinted_pack_index(objects_dir) else {
568 return Ok(None);
569 };
570 let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
571 return Ok(None);
572 };
573 match super::read_object_at_depth_into(&idx, offset, 0, populate_delta_lru, buf) {
574 Ok(kind) => Ok(Some(kind)),
575 Err(Error::ObjectNotFound(_)) => Ok(None),
576 Err(err) if err.is_object_read_wrap_source() => Ok(None),
577 Err(err) => Err(err),
578 }
579 }
580
581 pub(crate) fn try_read_info_from_hinted_pack(
582 objects_dir: &Path,
583 oid: &crate::objects::ObjectId,
584 ) -> Result<Option<crate::objects::ObjectInfo>> {
585 let Some(idx) = hinted_pack_index(objects_dir) else {
586 return Ok(None);
587 };
588 let Some(offset) = idx.find_offset(oid)? else {
589 return Ok(None);
590 };
591 match super::read_object_info_from_pack_at_offset(&idx, offset) {
592 Ok(info) => Ok(Some(info)),
593 Err(Error::ObjectNotFound(_)) => Ok(None),
594 Err(err) => Err(err),
595 }
596 }
597 fn cached_dir_indexes(objects_dir: &Path, pack_dir: &Path) -> Option<Vec<Arc<PackIndex>>> {
598 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
599 return None;
600 }
601 let trust_listing = PACK_DIR_LISTING_TRUSTED.get();
602 let dir_mt = if trust_listing {
603 None
604 } else {
605 Some(dir_mtime(pack_dir))
606 };
607 PACK_DIR_INDEXES.with(|cell| {
608 cell.borrow()
609 .as_ref()
610 .and_then(|(dir, cached_mt, indexes)| {
611 if dir != pack_dir {
612 return None;
613 }
614 if trust_listing || dir_mt == Some(*cached_mt) {
615 Some(indexes.clone())
616 } else {
617 None
618 }
619 })
620 })
621 }
622
623 fn remember_dir_indexes(objects_dir: &Path, pack_dir: &Path, indexes: &[Arc<PackIndex>]) {
624 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
625 return;
626 }
627 let dir_mt = dir_mtime(pack_dir);
628 PACK_DIR_INDEXES.with(|cell| {
629 *cell.borrow_mut() = Some((pack_dir.to_path_buf(), dir_mt, indexes.to_vec()));
630 });
631 }
632
633 struct CachedDir {
634 dir_mtime: SystemTime,
635 indexes: Vec<Arc<PackIndex>>,
636 midx_mtime: SystemTime,
637 midx_pack_names: HashSet<String>,
638 idx_listing_verified: bool,
640 }
641
642 struct CachedIdx {
643 idx: Arc<PackIndex>,
644 }
645
646 struct CachedPack {
647 mtime: SystemTime,
648 size: u64,
649 bytes: Arc<PackData>,
650 }
651
652 pub(super) const DELTA_BASE_CACHE_DEFAULT: usize = 96 * 1024 * 1024;
654
655 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
656 pub(crate) struct DeltaCacheKey {
657 pack_id: u32,
658 offset: u64,
659 }
660
661 struct DeltaBaseLru {
663 byte_limit: usize,
664 bytes_used: usize,
665 entries: HashMap<DeltaCacheKey, (ObjectKind, Arc<[u8]>)>,
666 head: Option<DeltaCacheKey>,
667 tail: Option<DeltaCacheKey>,
668 links: HashMap<DeltaCacheKey, (Option<DeltaCacheKey>, Option<DeltaCacheKey>)>,
669 }
670
671 impl Default for DeltaBaseLru {
672 fn default() -> Self {
673 Self {
674 byte_limit: DELTA_BASE_CACHE_DEFAULT,
675 bytes_used: 0,
676 entries: HashMap::new(),
677 head: None,
678 tail: None,
679 links: HashMap::new(),
680 }
681 }
682 }
683
684 impl DeltaBaseLru {
685 fn set_byte_limit(&mut self, limit: usize) {
686 if limit == 0 {
687 self.clear();
688 self.byte_limit = 0;
689 return;
690 }
691 self.byte_limit = limit;
692 while self.bytes_used > self.byte_limit {
693 let Some(key) = self.head else {
694 break;
695 };
696 self.evict_key(key);
697 }
698 }
699
700 fn unlink(&mut self, key: DeltaCacheKey) {
701 let Some((prev, next)) = self.links.remove(&key) else {
702 return;
703 };
704 match prev {
705 Some(p) => {
706 if let Some(link) = self.links.get_mut(&p) {
707 link.1 = next;
708 }
709 }
710 None => self.head = next,
711 }
712 match next {
713 Some(n) => {
714 if let Some(link) = self.links.get_mut(&n) {
715 link.0 = prev;
716 }
717 }
718 None => self.tail = prev,
719 }
720 }
721
722 fn append_tail(&mut self, key: DeltaCacheKey) {
723 let prev = self.tail;
724 self.links.insert(key, (prev, None));
725 if let Some(p) = prev {
726 if let Some(link) = self.links.get_mut(&p) {
727 link.1 = Some(key);
728 }
729 } else {
730 self.head = Some(key);
731 }
732 self.tail = Some(key);
733 }
734
735 fn touch(&mut self, key: DeltaCacheKey) {
736 self.unlink(key);
737 self.append_tail(key);
738 }
739
740 fn evict_key(&mut self, key: DeltaCacheKey) {
741 self.unlink(key);
742 if let Some((_, data)) = self.entries.remove(&key) {
743 self.bytes_used = self.bytes_used.saturating_sub(data.len());
744 }
745 }
746
747 fn get(&mut self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
748 let (kind, data) = self.entries.get(&key).map(|(k, d)| (*k, Arc::clone(d)))?;
749 self.touch(key);
750 Some((kind, data))
751 }
752
753 fn put(&mut self, key: DeltaCacheKey, kind: ObjectKind, data: Arc<[u8]>) {
754 if self.byte_limit == 0 {
755 return;
756 }
757 let sz = data.len();
758 if sz > self.byte_limit {
759 return;
760 }
761 use std::collections::hash_map::Entry;
762 match self.entries.entry(key) {
763 Entry::Occupied(mut slot) => {
764 let old_len = slot.get().1.len();
765 slot.insert((kind, Arc::clone(&data)));
766 self.bytes_used = self.bytes_used.saturating_sub(old_len).saturating_add(sz);
767 self.touch(key);
768 }
769 Entry::Vacant(slot) => {
770 slot.insert((kind, Arc::clone(&data)));
771 self.bytes_used = self.bytes_used.saturating_add(sz);
772 self.append_tail(key);
773 }
774 }
775 while self.bytes_used > self.byte_limit {
776 let Some(evict) = self.head else {
777 break;
778 };
779 self.evict_key(evict);
780 }
781 }
782
783 fn drop_pack(&mut self, pack_id: u32) {
784 let keys: Vec<DeltaCacheKey> = self
785 .entries
786 .keys()
787 .copied()
788 .filter(|k| k.pack_id == pack_id)
789 .collect();
790 for key in keys {
791 self.evict_key(key);
792 }
793 }
794
795 fn clear(&mut self) {
796 self.entries.clear();
797 self.links.clear();
798 self.head = None;
799 self.tail = None;
800 self.bytes_used = 0;
801 }
802 }
803
804 fn shard_index(key: DeltaCacheKey) -> usize {
805 let mut hasher = std::collections::hash_map::DefaultHasher::new();
806 key.hash(&mut hasher);
807 (hasher.finish() as usize) % DELTA_LRU_SHARD_COUNT
808 }
809
810 pub(crate) struct ShardedDeltaBaseLru {
812 cache_enabled: AtomicBool,
813 lru_populated: AtomicBool,
814 total_byte_limit: AtomicUsize,
815 shards: Box<[Mutex<DeltaBaseLru>; DELTA_LRU_SHARD_COUNT]>,
816 }
817
818 impl ShardedDeltaBaseLru {
819 pub(crate) fn new() -> Self {
820 let per_shard = DELTA_BASE_CACHE_DEFAULT / DELTA_LRU_SHARD_COUNT;
821 Self {
822 cache_enabled: AtomicBool::new(true),
823 lru_populated: AtomicBool::new(false),
824 total_byte_limit: AtomicUsize::new(DELTA_BASE_CACHE_DEFAULT),
825 shards: Box::new(std::array::from_fn(|_| {
826 Mutex::new(DeltaBaseLru {
827 byte_limit: per_shard,
828 ..DeltaBaseLru::default()
829 })
830 })),
831 }
832 }
833
834 pub(crate) fn set_byte_limit(&self, limit: usize) {
835 self.total_byte_limit.store(limit, Ordering::Release);
836 if limit == 0 {
837 self.cache_enabled.store(false, Ordering::Release);
838 self.clear();
839 return;
840 }
841 self.cache_enabled.store(true, Ordering::Release);
842 let base = limit / DELTA_LRU_SHARD_COUNT;
843 let rem = limit % DELTA_LRU_SHARD_COUNT;
844 let mut any = false;
845 for (i, shard) in self.shards.iter().enumerate() {
846 let cap = base + usize::from(i < rem);
847 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
848 g.set_byte_limit(cap);
849 any |= !g.entries.is_empty();
850 }
851 self.lru_populated.store(any, Ordering::Release);
852 }
853
854 pub(crate) fn get(&self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
855 if !self.cache_enabled.load(Ordering::Acquire)
856 || !self.lru_populated.load(Ordering::Acquire)
857 {
858 return None;
859 }
860 let shard = &self.shards[shard_index(key)];
861 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
862 g.get(key)
863 }
864
865 pub(crate) fn put_many(&self, kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
866 if items.is_empty() || !self.cache_enabled.load(Ordering::Acquire) {
867 return;
868 }
869 for &(pack_id, offset, ref data) in items {
870 let key = DeltaCacheKey { pack_id, offset };
871 let shard = &self.shards[shard_index(key)];
872 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
873 if g.byte_limit == 0 {
874 continue;
875 }
876 g.put(key, kind, Arc::clone(data));
877 }
878 self.lru_populated.store(true, Ordering::Release);
879 }
880
881 pub(crate) fn clear(&self) {
882 for shard in self.shards.iter() {
883 shard.lock().unwrap_or_else(|e| e.into_inner()).clear();
884 }
885 self.lru_populated.store(false, Ordering::Release);
886 }
887
888 pub(crate) fn drop_pack(&self, pack_id: u32) {
889 let mut any = false;
890 for shard in self.shards.iter() {
891 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
892 g.drop_pack(pack_id);
893 any |= !g.entries.is_empty();
894 }
895 self.lru_populated.store(any, Ordering::Release);
896 }
897
898 pub(crate) fn caching_enabled(&self) -> bool {
899 self.cache_enabled.load(Ordering::Acquire)
900 }
901
902 #[cfg(test)]
903 pub(crate) fn bytes_used(&self) -> usize {
904 self.shards
905 .iter()
906 .map(|s| s.lock().unwrap_or_else(|e| e.into_inner()).bytes_used)
907 .sum()
908 }
909
910 #[cfg(test)]
911 pub(crate) fn contains(&self, pack_id: u32, offset: u64) -> bool {
912 let key = DeltaCacheKey { pack_id, offset };
913 self.shards[shard_index(key)]
914 .lock()
915 .unwrap_or_else(|e| e.into_inner())
916 .entries
917 .contains_key(&key)
918 }
919 }
920
921 #[derive(Default)]
922 pub(crate) struct CatalogState {
923 by_dir: HashMap<PathBuf, CachedDir>,
924 by_idx: HashMap<PathBuf, CachedIdx>,
925 by_pack: HashMap<PathBuf, CachedPack>,
926 bounded_pack_order: VecDeque<PathBuf>,
927 pack_ids: HashMap<PathBuf, u32>,
929 next_pack_id: u32,
930 validated_pack_counts: HashSet<PathBuf>,
932 #[cfg(test)]
934 test_dir_rescan_counts: HashMap<PathBuf, u64>,
935 }
936
937 impl CatalogState {
938 fn retain_bounded_pack(&mut self, pack_path: &Path) {
939 if let Some(pos) = self.bounded_pack_order.iter().position(|p| p == pack_path) {
940 self.bounded_pack_order.remove(pos);
941 }
942 while self.bounded_pack_order.len() >= BOUNDED_PACK_READERS {
943 if let Some(path) = self.bounded_pack_order.pop_front() {
944 self.by_pack.remove(&path);
945 }
946 }
947 self.bounded_pack_order.push_back(pack_path.to_path_buf());
948 }
949 }
950
951 pub(crate) fn with_current<R>(f: impl FnOnce(&mut CatalogState) -> R) -> R {
952 crate::pack_store::PackStore::with_current_or_implicit(f)
953 }
954
955 pub(crate) fn clear_catalog_on(m: &std::sync::RwLock<CatalogState>) {
956 let mut g = m.write().unwrap_or_else(|e| e.into_inner());
957 clear_inner(&mut g);
958 }
959
960 pub(crate) fn configure_delta_base_on(
961 delta: &ShardedDeltaBaseLru,
962 cfg: Option<&crate::config::ConfigSet>,
963 ) {
964 let bytes = cfg
965 .and_then(|config| config.get("core.deltaBaseCacheLimit"))
966 .as_ref()
967 .and_then(|value| crate::config::parse_i64(value).ok())
968 .and_then(|value| usize::try_from(value).ok())
969 .unwrap_or(DELTA_BASE_CACHE_DEFAULT);
970 delta.set_byte_limit(bytes);
971 }
972
973 #[cfg(test)]
974 thread_local! {
975 static TEST_MARKER_STAT_COUNT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
976 }
977
978 fn marker_sidecar_is_file(path: &Path) -> bool {
979 #[cfg(test)]
980 TEST_MARKER_STAT_COUNT.with(|count| count.set(count.get().saturating_add(1)));
981 path.is_file()
982 }
983
984 pub fn sidecar_flags(pack_path: &Path) -> (bool, bool) {
986 (
987 marker_sidecar_is_file(&pack_path.with_extension("promisor")),
988 marker_sidecar_is_file(&pack_path.with_extension("mtimes")),
989 )
990 }
991
992 fn refresh_pack_sidecar_flags(idx: Arc<PackIndex>) -> Arc<PackIndex> {
993 let (is_promisor, is_cruft) = sidecar_flags(&idx.pack_path);
994 if idx.is_promisor == is_promisor && idx.is_cruft == is_cruft {
995 return idx;
996 }
997 let updated = Arc::new(idx.with_sidecar_flags(is_promisor, is_cruft));
998 with_current(|g| {
999 g.by_idx.insert(
1000 idx.idx_path.clone(),
1001 CachedIdx {
1002 idx: Arc::clone(&updated),
1003 },
1004 );
1005 });
1006 updated
1007 }
1008
1009 fn dir_mtime(path: &Path) -> SystemTime {
1010 fs::metadata(path)
1011 .and_then(|m| m.modified())
1012 .unwrap_or(SystemTime::UNIX_EPOCH)
1013 }
1014
1015 fn file_signature(path: &Path) -> Option<(SystemTime, u64)> {
1016 #[cfg(test)]
1017 crate::hot_path_test_metrics::record_pack_signature_stat_for_active_scope();
1018 let m = fs::metadata(path).ok()?;
1019 let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
1020 Some((mtime, m.len()))
1021 }
1022
1023 pub fn pack_object_count_validated(pack_path: &Path) -> bool {
1024 if crate::pack_store::PackStore::has_context()
1025 && PACK_COUNT_VALIDATED.with(|cell| cell.borrow().contains(pack_path))
1026 {
1027 return true;
1028 }
1029 crate::pack_store::PackStore::active_for_reads()
1030 .with_catalog_read(|g| g.validated_pack_counts.contains(pack_path))
1031 }
1032
1033 pub fn mark_pack_object_count_validated(pack_path: &Path) {
1034 if crate::pack_store::PackStore::has_context() {
1035 PACK_COUNT_VALIDATED.with(|cell| {
1036 cell.borrow_mut().insert(pack_path.to_path_buf());
1037 });
1038 }
1039 let store = crate::pack_store::PackStore::active_for_reads();
1040 if store.with_catalog_read(|g| g.validated_pack_counts.contains(pack_path)) {
1041 return;
1042 }
1043 let _ = store.try_with_catalog(|g| {
1044 g.validated_pack_counts.insert(pack_path.to_path_buf());
1045 });
1046 }
1047
1048 fn cached_index(idx_path: &Path) -> Option<Arc<PackIndex>> {
1049 if !crate::pack_store::PackStore::has_context() {
1050 return None;
1051 }
1052 PACK_INDEXES.with(|cell| cell.borrow().get(idx_path).map(Arc::clone))
1053 }
1054
1055 fn remember_index(idx_path: &Path, idx: &Arc<PackIndex>) {
1056 if !crate::pack_store::PackStore::has_context() {
1057 return;
1058 }
1059 PACK_INDEXES.with(|cell| {
1060 cell.borrow_mut()
1061 .insert(idx_path.to_path_buf(), Arc::clone(idx));
1062 });
1063 }
1064
1065 fn index_cache_hit_stale(idx: &PackIndex) -> bool {
1066 !idx.pack_path.is_file()
1067 }
1068
1069 pub fn get_index(idx_path: &Path) -> Result<Arc<PackIndex>> {
1071 if let Some(hit) = cached_index(idx_path) {
1072 if index_cache_hit_stale(&hit) {
1073 PACK_INDEXES.with(|cell| {
1074 cell.borrow_mut().remove(idx_path);
1075 });
1076 } else {
1077 return Ok(hit);
1078 }
1079 }
1080 let store = crate::pack_store::PackStore::active_for_reads();
1081 if let Some(hit) =
1082 store.with_catalog_read(|g| g.by_idx.get(idx_path).map(|c| Arc::clone(&c.idx)))
1083 {
1084 if index_cache_hit_stale(&hit) {
1085 store.with_catalog(|g| {
1086 g.by_idx.remove(idx_path);
1087 });
1088 } else {
1089 remember_index(idx_path, &hit);
1090 return Ok(hit);
1091 }
1092 }
1093 crate::trace::trace_span!("grit.pack.index.open");
1094 let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
1095 store.with_catalog(|g| {
1096 g.by_idx.insert(
1097 idx_path.to_path_buf(),
1098 CachedIdx {
1099 idx: Arc::clone(&parsed),
1100 },
1101 );
1102 });
1103 remember_index(idx_path, &parsed);
1104 Ok(parsed)
1105 }
1106
1107 fn rescan_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1108 let pack_dir = objects_dir.join("pack");
1109 drop_thread_local_dir_listing(&pack_dir);
1110 let dir_mt = dir_mtime(&pack_dir);
1111
1112 let rd = match fs::read_dir(&pack_dir) {
1113 Ok(rd) => rd,
1114 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1115 with_current(|g| {
1116 g.by_dir.insert(
1117 pack_dir.clone(),
1118 CachedDir {
1119 dir_mtime: dir_mt,
1120 indexes: Vec::new(),
1121 midx_mtime: SystemTime::UNIX_EPOCH,
1122 midx_pack_names: HashSet::new(),
1123 idx_listing_verified: true,
1124 },
1125 );
1126 });
1127 return Ok(Vec::new());
1128 }
1129 Err(err) => return Err(Error::Io(err)),
1130 };
1131
1132 let mut out = Vec::new();
1133 for entry in rd {
1134 let entry = entry.map_err(Error::Io)?;
1135 let path = entry.path();
1136 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1137 continue;
1138 }
1139 let Ok(idx) = get_index(&path) else {
1140 continue;
1141 };
1142 if !idx.pack_path.is_file() {
1143 continue;
1144 }
1145 out.push(refresh_pack_sidecar_flags(idx));
1146 }
1147
1148 let midx_pack_names = load_midx_pack_names(&pack_dir);
1149 with_current(|g| {
1150 #[cfg(test)]
1151 {
1152 *g.test_dir_rescan_counts
1153 .entry(pack_dir.clone())
1154 .or_insert(0) += 1;
1155 }
1156 g.by_dir.insert(
1157 pack_dir,
1158 CachedDir {
1159 dir_mtime: dir_mt,
1160 indexes: out.clone(),
1161 midx_mtime: midx_pack_names.0,
1162 midx_pack_names: midx_pack_names.1,
1163 idx_listing_verified: false,
1164 },
1165 );
1166 });
1167 Ok(out)
1168 }
1169
1170 fn load_midx_pack_names(pack_dir: &Path) -> (SystemTime, HashSet<String>) {
1171 let Some(midx_path) = crate::midx::resolve_tip_midx_path(pack_dir) else {
1172 return (SystemTime::UNIX_EPOCH, HashSet::new());
1173 };
1174 let metadata = fs::metadata(&midx_path).ok();
1175 let mtime = metadata
1176 .as_ref()
1177 .and_then(|m| m.modified().ok())
1178 .unwrap_or(SystemTime::UNIX_EPOCH);
1179 let names = metadata
1180 .as_ref()
1181 .and_then(|metadata| {
1182 crate::midx::read_midx_pack_idx_names_from_path(&midx_path, metadata).ok()
1183 })
1184 .unwrap_or_default()
1185 .into_iter()
1186 .collect();
1187 (mtime, names)
1188 }
1189
1190 fn refresh_midx_pack_names_locked(pack_dir: &Path, cached: &mut CachedDir) {
1191 let (mtime, names) = load_midx_pack_names(pack_dir);
1192 if cached.midx_mtime != mtime {
1193 cached.midx_mtime = mtime;
1194 cached.midx_pack_names = names;
1195 }
1196 }
1197
1198 pub fn promote_pack_index(objects_dir: &Path, idx_path: &Path) {
1200 let pack_dir = objects_dir.join("pack");
1201 let store = crate::pack_store::PackStore::active_for_reads();
1202 let _ = store.try_with_catalog(|g| {
1203 let Some(cached) = g.by_dir.get_mut(&pack_dir) else {
1204 return;
1205 };
1206 let Some(pos) = cached
1207 .indexes
1208 .iter()
1209 .position(|idx| idx.idx_path == idx_path)
1210 else {
1211 return;
1212 };
1213 if pos == 0 {
1214 return;
1215 }
1216 let hit = cached.indexes.remove(pos);
1217 cached.indexes.insert(0, hit);
1218 });
1219 }
1220
1221 pub fn midx_covered_pack_names(objects_dir: &Path) -> HashSet<String> {
1223 let pack_dir = objects_dir.join("pack");
1224 let cached = with_current(|g| {
1225 let cached = g.by_dir.get_mut(&pack_dir)?;
1226 refresh_midx_pack_names_locked(&pack_dir, cached);
1227 Some(cached.midx_pack_names.clone())
1228 });
1229 if let Some(names) = cached {
1230 return names;
1231 }
1232 load_midx_pack_names(&pack_dir).1
1233 }
1234
1235 pub fn get_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1237 let pack_dir = objects_dir.join("pack");
1238 let store = crate::pack_store::PackStore::active_for_reads();
1239 if crate::pack_store::PackStore::has_context() {
1240 sync_thread_local_with_store(&store);
1241 }
1242 if let Some(hit) = cached_dir_indexes(objects_dir, &pack_dir) {
1243 return Ok(hit);
1244 }
1245 let dir_mt = dir_mtime(&pack_dir);
1246 if let Some(hit) = store.with_catalog_read(|g| {
1247 g.by_dir
1248 .get(&pack_dir)
1249 .and_then(|c| (c.dir_mtime == dir_mt).then(|| c.indexes.clone()))
1250 }) {
1251 remember_dir_indexes(objects_dir, &pack_dir, &hit);
1252 return Ok(hit);
1253 }
1254 let fresh = rescan_dir_indexes(objects_dir)?;
1255 remember_dir_indexes(objects_dir, &pack_dir, &fresh);
1256 Ok(fresh)
1257 }
1258
1259 fn idx_paths_on_disk(pack_dir: &Path) -> Result<HashSet<PathBuf>> {
1260 let mut out = HashSet::new();
1261 let rd = match fs::read_dir(pack_dir) {
1262 Ok(rd) => rd,
1263 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(out),
1264 Err(err) => return Err(Error::Io(err)),
1265 };
1266 for entry in rd {
1267 let entry = entry.map_err(Error::Io)?;
1268 let path = entry.path();
1269 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1270 continue;
1271 }
1272 if path.with_extension("pack").is_file() {
1273 out.insert(path);
1274 }
1275 }
1276 Ok(out)
1277 }
1278
1279 pub fn reprepare_dir_on_miss(objects_dir: &Path) -> Result<bool> {
1283 let pack_dir = objects_dir.join("pack");
1284 let dir_mt = dir_mtime(&pack_dir);
1285 let store = crate::pack_store::PackStore::active_for_reads();
1286 enum ReprepareDecision {
1287 Fresh,
1288 Rescan,
1289 VerifyIdxSet,
1290 }
1291 let decision = store.with_catalog_read(|g| match g.by_dir.get(&pack_dir) {
1292 None => ReprepareDecision::Rescan,
1293 Some(c) if c.dir_mtime != dir_mt => ReprepareDecision::Rescan,
1294 Some(c) if c.idx_listing_verified => ReprepareDecision::Fresh,
1295 Some(_) => ReprepareDecision::VerifyIdxSet,
1296 });
1297 match decision {
1298 ReprepareDecision::Fresh => return Ok(false),
1299 ReprepareDecision::Rescan => {
1300 rescan_dir_indexes(objects_dir)?;
1301 return Ok(true);
1302 }
1303 ReprepareDecision::VerifyIdxSet => {}
1304 }
1305 let cached_paths = store.with_catalog_read(|g| {
1306 g.by_dir.get(&pack_dir).map(|c| {
1307 c.indexes
1308 .iter()
1309 .map(|idx| idx.idx_path.clone())
1310 .collect::<HashSet<_>>()
1311 })
1312 });
1313 let Some(cached_paths) = cached_paths else {
1314 rescan_dir_indexes(objects_dir)?;
1315 return Ok(true);
1316 };
1317 let on_disk = idx_paths_on_disk(&pack_dir)?;
1318 if on_disk == cached_paths {
1319 store.with_catalog(|g| {
1320 if let Some(c) = g.by_dir.get_mut(&pack_dir) {
1321 c.idx_listing_verified = true;
1322 }
1323 });
1324 return Ok(false);
1325 }
1326 rescan_dir_indexes(objects_dir)?;
1327 Ok(true)
1328 }
1329
1330 fn load_pack_bytes_from_disk(
1331 pack_path: &Path,
1332 mtime: SystemTime,
1333 size: u64,
1334 ) -> Result<Arc<PackData>> {
1335 invalidate_thread_local_for_pack_path_change(pack_path);
1336 let store = crate::pack_store::PackStore::active_for_reads();
1337 let bytes = if store.bounded_reads() {
1338 PackData::open_random_access(pack_path)?
1339 } else {
1340 PackData::open_for_sparse_reads(pack_path, &store.sparse_read_files)?
1341 };
1342 store.with_catalog(|g| {
1343 drop_delta_entries_locked(g, pack_path, store.delta_bases());
1344 g.by_idx.remove(&pack_path.with_extension("idx"));
1345 g.validated_pack_counts.remove(pack_path);
1346 if store.bounded_reads() {
1347 g.retain_bounded_pack(pack_path);
1348 }
1349 g.by_pack.insert(
1350 pack_path.to_path_buf(),
1351 CachedPack {
1352 mtime,
1353 size,
1354 bytes: Arc::clone(&bytes),
1355 },
1356 );
1357 });
1358 remember_pack_bytes(pack_path, &bytes);
1359 Ok(bytes)
1360 }
1361
1362 pub fn get_pack_bytes(pack_path: &Path) -> Result<Arc<PackData>> {
1367 if let Some(hit) = cached_pack_bytes(pack_path) {
1368 return Ok(hit);
1369 }
1370 let store = crate::pack_store::PackStore::active_for_reads();
1371 let hit = if store.bounded_reads() {
1372 store.with_catalog(|g| {
1373 let bytes = Arc::clone(&g.by_pack.get(pack_path)?.bytes);
1374 g.retain_bounded_pack(pack_path);
1375 Some(bytes)
1376 })
1377 } else {
1378 store.with_catalog_read(|g| g.by_pack.get(pack_path).map(|c| Arc::clone(&c.bytes)))
1379 };
1380 if let Some(hit) = hit {
1381 remember_pack_bytes(pack_path, &hit);
1382 return Ok(hit);
1383 }
1384 let (mtime, size) = file_signature(pack_path).ok_or_else(|| {
1385 Error::Io(io::Error::new(
1386 io::ErrorKind::NotFound,
1387 format!("pack not found: {}", pack_path.display()),
1388 ))
1389 })?;
1390 load_pack_bytes_from_disk(pack_path, mtime, size)
1391 }
1392
1393 fn revalidate_stale_pack_bytes_in_context(pack_path: &Path) -> Result<bool> {
1394 let Some((mtime, size)) = file_signature(pack_path) else {
1395 let store = crate::pack_store::PackStore::active_for_reads();
1396 let removed = store.with_catalog(|g| {
1397 let removed = g.by_pack.remove(pack_path).is_some();
1398 if removed {
1399 drop_delta_entries_locked(g, pack_path, store.delta_bases());
1400 }
1401 removed
1402 });
1403 invalidate_thread_local_for_pack_path_change(pack_path);
1404 super::drop_pack_read_info_cache_for(pack_path);
1405 return Ok(removed);
1406 };
1407 let store = crate::pack_store::PackStore::active_for_reads();
1408 let stale = store.with_catalog_read(|g| match g.by_pack.get(pack_path) {
1409 Some(c) => c.mtime != mtime || c.size != size,
1410 None => true,
1411 });
1412 if !stale {
1413 return Ok(false);
1414 }
1415 load_pack_bytes_from_disk(pack_path, mtime, size)?;
1416 super::drop_pack_read_info_cache_for(pack_path);
1417 Ok(true)
1418 }
1419
1420 pub fn revalidate_stale_pack_bytes(pack_path: &Path) -> Result<bool> {
1424 if crate::pack_store::PackStore::has_context() {
1425 return revalidate_stale_pack_bytes_in_context(pack_path);
1426 }
1427 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1428 crate::pack_store::PackStore::with_context(store, || {
1429 revalidate_stale_pack_bytes_in_context(pack_path)
1430 })
1431 }
1432
1433 pub fn reload_pack_bytes_after_parse_failure(pack_path: &Path) -> Result<bool> {
1441 if revalidate_stale_pack_bytes(pack_path)? {
1442 return Ok(true);
1443 }
1444 let Some((mtime, size)) = file_signature(pack_path) else {
1445 return Ok(false);
1446 };
1447 let hash_bytes = hash_bytes_for_pack_file(pack_path)?;
1448 let disk_fp = fingerprint_from_file(pack_path, hash_bytes)?;
1449 let needs_reload = with_current(|g| {
1450 g.by_pack.get(pack_path).is_some_and(|c| {
1451 pack_fingerprint_mismatch(&c.bytes, hash_bytes, &disk_fp).unwrap_or(true)
1452 })
1453 });
1454 if !needs_reload {
1455 return Ok(false);
1456 }
1457 load_pack_bytes_from_disk(pack_path, mtime, size)?;
1458 super::drop_pack_read_info_cache_for(pack_path);
1459 Ok(true)
1460 }
1461
1462 fn hash_bytes_for_pack_file(pack_path: &Path) -> Result<usize> {
1463 let idx_path = pack_path.with_extension("idx");
1464 if let Some(hash_bytes) =
1465 with_current(|g| g.by_idx.get(&idx_path).map(|c| c.idx.hash_bytes()))
1466 {
1467 return Ok(hash_bytes);
1468 }
1469 Ok(read_pack_index_no_verify(&idx_path)?.hash_bytes())
1470 }
1471
1472 fn pack_fingerprint_mismatch(
1473 cached: &PackData,
1474 hash_bytes: usize,
1475 disk: &PackFingerprint,
1476 ) -> Result<bool> {
1477 Ok(cached.fingerprint(hash_bytes)? != *disk)
1478 }
1479
1480 #[cfg(test)]
1481 pub fn test_reset_dir_rescan_count(pack_dir: &Path) {
1482 let objects_dir = pack_dir.parent().expect("pack dir under objects/");
1483 crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir).with_pack(|g| {
1484 g.by_dir.remove(pack_dir);
1485 g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
1486 });
1487 }
1488
1489 #[cfg(test)]
1490 pub fn test_reset_marker_stat_count() {
1491 TEST_MARKER_STAT_COUNT.with(|count| count.set(0));
1492 }
1493
1494 #[cfg(test)]
1495 #[must_use]
1496 pub fn test_marker_stat_count() -> u64 {
1497 TEST_MARKER_STAT_COUNT.with(std::cell::Cell::get)
1498 }
1499
1500 #[cfg(test)]
1501 #[must_use]
1502 pub fn test_dir_rescan_count(pack_dir: &Path) -> u64 {
1503 let objects_dir = pack_dir.parent().expect("pack dir under objects/");
1504 test_dir_rescan_count_on_store(
1505 &crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir),
1506 pack_dir,
1507 )
1508 }
1509
1510 #[cfg(test)]
1511 #[must_use]
1512 pub fn test_dir_rescan_count_on_store(
1513 store: &std::sync::Arc<crate::pack_store::PackStore>,
1514 pack_dir: &Path,
1515 ) -> u64 {
1516 store.with_pack(|g| g.test_dir_rescan_counts.get(pack_dir).copied().unwrap_or(0))
1517 }
1518
1519 #[cfg(test)]
1520 pub fn test_reset_dir_rescan_count_on_store(
1521 store: &std::sync::Arc<crate::pack_store::PackStore>,
1522 pack_dir: &Path,
1523 ) {
1524 store.with_pack(|g| {
1525 g.by_dir.remove(pack_dir);
1526 g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
1527 });
1528 }
1529
1530 #[cfg(test)]
1532 pub fn test_inject_stale_pack_bytes(pack_path: &Path, stale: Arc<PackData>) {
1533 crate::pack_store::PackStore::legacy_for_pack_path(pack_path).with_pack(|g| {
1534 let stamp = g
1535 .by_pack
1536 .get(pack_path)
1537 .map(|c| (c.mtime, c.size))
1538 .unwrap_or((SystemTime::UNIX_EPOCH, stale.len() as u64));
1539 g.by_pack.insert(
1540 pack_path.to_path_buf(),
1541 CachedPack {
1542 mtime: stamp.0,
1543 size: stamp.1,
1544 bytes: stale,
1545 },
1546 );
1547 });
1548 }
1549
1550 fn clear_inner(g: &mut CatalogState) {
1551 g.by_dir.clear();
1552 g.by_idx.clear();
1553 g.by_pack.clear();
1554 g.bounded_pack_order.clear();
1555 g.pack_ids.clear();
1556 }
1557
1558 pub fn set_delta_base_cache_byte_limit(limit: usize) {
1560 crate::pack_store::PackStore::active_for_reads()
1561 .delta_bases()
1562 .set_byte_limit(limit);
1563 }
1564
1565 #[cfg(test)]
1566 pub fn test_delta_base_cache_bytes_used(pack_path: &Path) -> usize {
1567 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1568 .delta_bases()
1569 .bytes_used()
1570 }
1571
1572 #[cfg(test)]
1573 pub fn test_set_delta_base_cache_byte_limit(pack_path: &Path, limit: usize) {
1574 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1575 .delta_bases()
1576 .set_byte_limit(limit);
1577 }
1578
1579 #[cfg(test)]
1580 pub fn test_delta_base_cached(pack_path: &Path, pack_id: u32, offset: u64) -> bool {
1581 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1582 .delta_bases()
1583 .contains(pack_id, offset)
1584 }
1585
1586 fn pack_id_for_in_context(pack_path: &Path) -> u32 {
1587 if let Some(id) = PACK_IDS.with(|cell| cell.borrow().get(pack_path.as_os_str()).copied()) {
1588 return id;
1589 }
1590 let store = crate::pack_store::PackStore::active_for_reads();
1591 if let Some(id) = store.with_catalog_read(|g| g.pack_ids.get(pack_path).copied()) {
1592 PACK_IDS.with(|cell| {
1593 cell.borrow_mut()
1594 .insert(pack_path.as_os_str().to_owned(), id);
1595 });
1596 return id;
1597 }
1598 store.with_catalog(|g| {
1599 if let Some(&id) = g.pack_ids.get(pack_path) {
1600 return id;
1601 }
1602 let id = g.next_pack_id;
1603 g.next_pack_id = g.next_pack_id.saturating_add(1);
1604 g.pack_ids.insert(pack_path.to_path_buf(), id);
1605 PACK_IDS.with(|cell| {
1606 cell.borrow_mut()
1607 .insert(pack_path.as_os_str().to_owned(), id);
1608 });
1609 id
1610 })
1611 }
1612
1613 pub fn pack_id_for(pack_path: &Path) -> u32 {
1615 if crate::pack_store::PackStore::has_context() {
1616 return pack_id_for_in_context(pack_path);
1617 }
1618 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1619 crate::pack_store::PackStore::with_context(store, || pack_id_for_in_context(pack_path))
1620 }
1621
1622 fn drop_delta_entries_locked(
1624 g: &mut CatalogState,
1625 pack_path: &Path,
1626 delta: &ShardedDeltaBaseLru,
1627 ) {
1628 if let Some(&pack_id) = g.pack_ids.get(pack_path) {
1629 delta.drop_pack(pack_id);
1630 }
1631 }
1632
1633 pub fn get_delta_base(pack_id: u32, offset: u64) -> Option<(ObjectKind, Arc<[u8]>)> {
1635 crate::pack_store::PackStore::active_for_reads()
1636 .delta_bases()
1637 .get(DeltaCacheKey { pack_id, offset })
1638 }
1639
1640 #[must_use]
1641 pub fn delta_base_caching_enabled() -> bool {
1642 crate::pack_store::PackStore::active_for_reads()
1643 .delta_bases()
1644 .caching_enabled()
1645 }
1646
1647 pub fn put_delta_bases(kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
1649 if items.is_empty() {
1650 return;
1651 }
1652 crate::pack_store::PackStore::active_for_reads()
1653 .delta_bases()
1654 .put_many(kind, items);
1655 }
1656
1657 pub fn clear_delta_bases() {
1659 crate::pack_store::PackStore::active_for_reads()
1660 .delta_bases()
1661 .clear();
1662 }
1663
1664 pub fn refresh_pack_signature(pack_path: &Path, touched_at: SystemTime) {
1671 with_current(|g| {
1672 if let Some(c) = g.by_pack.get_mut(pack_path) {
1673 c.mtime = touched_at;
1674 }
1675 });
1676 }
1677
1678 #[cfg(test)]
1679 #[test]
1680 fn bounded_pack_cache_releases_inactive_readers() {
1681 let dir = tempfile::tempdir().expect("dir");
1682 let store = Arc::new(crate::pack_store::PackStore::for_bounded_reads(
1683 dir.path().to_path_buf(),
1684 ));
1685 crate::pack_store::PackStore::with_context(Arc::clone(&store), || {
1686 let mut previous = Vec::new();
1687 let hot_path = dir.path().join("0.pack");
1688 let mut hot = None;
1689 for i in 0..70 {
1690 let path = dir.path().join(format!("{i}.pack"));
1691 let mut bytes = vec![i as u8; 32768];
1692 bytes[..12].copy_from_slice(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00");
1693 fs::write(&path, bytes).expect("write");
1694 let pack = get_pack_bytes(&path).expect("pack");
1695 assert!(pack.uses_positioned_reads());
1696 previous.push(Arc::downgrade(&pack));
1697 hot.get_or_insert_with(|| Arc::clone(&pack));
1698 assert!(Arc::ptr_eq(
1699 &get_pack_bytes(&hot_path).expect("hot pack"),
1700 hot.as_ref().expect("hot")
1701 ));
1702 }
1703 warm_thread_local_from_store(&store);
1704 assert!(store.with_catalog_read(|g| g.by_pack.len()) <= 32);
1705 assert!(previous[1..32].iter().all(|pack| pack.upgrade().is_none()));
1706 assert!(previous[69].upgrade().is_some());
1707 let reloaded = get_pack_bytes(&dir.path().join("1.pack")).expect("reopen");
1708 assert_eq!(
1709 reloaded
1710 .read_range(100, 200, &mut Vec::new())
1711 .expect("read"),
1712 &[1; 100]
1713 );
1714 });
1715 }
1716}
1717
1718pub use pack_cache::revalidate_stale_pack_bytes;
1719
1720pub fn read_local_pack_indexes_cached(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1732 if crate::pack_store::PackStore::has_context() {
1733 return pack_cache::get_dir_indexes(objects_dir);
1734 }
1735 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
1736 crate::pack_store::PackStore::with_context(store, || pack_cache::get_dir_indexes(objects_dir))
1737}
1738
1739pub fn reprepare_pack_directory_on_miss(objects_dir: &Path) -> Result<bool> {
1748 if crate::pack_store::PackStore::has_context() {
1749 return reprepare_pack_directory_on_miss_in_context(objects_dir);
1750 }
1751 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
1752 crate::pack_store::PackStore::with_context(store, || {
1753 reprepare_pack_directory_on_miss_in_context(objects_dir)
1754 })
1755}
1756
1757fn reprepare_pack_directory_on_miss_in_context(objects_dir: &Path) -> Result<bool> {
1758 let refreshed = pack_cache::reprepare_dir_on_miss(objects_dir)?;
1759 if refreshed {
1760 crate::midx::evict_midx_read_cache_for_pack_dir(&objects_dir.join("pack"));
1761 }
1762 Ok(refreshed)
1763}
1764
1765pub fn read_pack_index_cached(idx_path: &Path) -> Result<Arc<PackIndex>> {
1773 if crate::pack_store::PackStore::has_context() {
1774 return pack_cache::get_index(idx_path);
1775 }
1776 let pack_path = idx_path.with_extension("pack");
1777 let store = crate::pack_store::PackStore::legacy_for_pack_path(&pack_path);
1778 crate::pack_store::PackStore::with_context(store, || pack_cache::get_index(idx_path))
1779}
1780
1781pub fn read_pack_bytes_cached(pack_path: &Path) -> Result<Arc<PackData>> {
1787 if crate::pack_store::PackStore::has_context() {
1788 return pack_cache::get_pack_bytes(pack_path);
1789 }
1790 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1791 crate::pack_store::PackStore::with_context(store, || pack_cache::get_pack_bytes(pack_path))
1792}
1793
1794pub fn clear_pack_cache() {
1799 #[cfg(test)]
1800 let _guard = pack_cache_test_guard();
1801 clear_pack_read_info_cache();
1802 pack_cache::clear_thread_local_dir_indexes();
1803 crate::pack_store::PackStore::invalidate_all_legacy();
1804}
1805
1806#[cfg(test)]
1808pub fn test_reset_pack_marker_stat_count() {
1809 pack_cache::test_reset_marker_stat_count();
1810}
1811
1812#[cfg(test)]
1814#[must_use]
1815pub fn test_pack_marker_stat_count() -> u64 {
1816 pack_cache::test_marker_stat_count()
1817}
1818
1819pub fn clear_pack_delta_base_cache() {
1821 pack_cache::clear_delta_bases();
1822}
1823
1824pub fn configure_delta_base_cache_from_config(cfg: Option<&crate::config::ConfigSet>) {
1826 let bytes = cfg
1827 .and_then(|c| c.get("core.deltaBaseCacheLimit"))
1828 .as_ref()
1829 .and_then(|v| crate::config::parse_i64(v).ok())
1830 .and_then(|n| usize::try_from(n).ok())
1831 .unwrap_or(pack_cache::DELTA_BASE_CACHE_DEFAULT);
1832 pack_cache::set_delta_base_cache_byte_limit(bytes);
1833}
1834
1835pub fn refresh_pack_bytes_signature(pack_path: &Path, touched_at: SystemTime) {
1838 pack_cache::refresh_pack_signature(pack_path, touched_at);
1839}
1840
1841pub fn collect_local_pack_info(objects_dir: &Path) -> Result<LocalPackInfo> {
1847 let indexes = read_local_pack_indexes(objects_dir)?;
1848 let mut info = LocalPackInfo::default();
1849 for idx in indexes {
1850 let pack_meta = fs::metadata(&idx.pack_path).map_err(Error::Io)?;
1851 let idx_meta = fs::metadata(&idx.idx_path).map_err(Error::Io)?;
1852 info.pack_count += 1;
1853 info.object_count += idx.len();
1854 info.size_bytes += pack_meta.len() + idx_meta.len();
1855 let hash_bytes = idx.hash_bytes();
1856 for entry in idx.iter() {
1857 if entry.oid().len() == hash_bytes {
1858 if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
1859 info.object_ids.insert(oid);
1860 }
1861 }
1862 }
1863 }
1864 Ok(info)
1865}
1866
1867pub fn write_v2_pack_index_with_trailer(
1882 idx_path: &Path,
1883 entries: &[(ObjectId, u64, u32)],
1884 pack_trailer: &[u8],
1885 hash_bytes: usize,
1886 sort_threads: Option<usize>,
1887) -> Result<()> {
1888 write_v2_pack_index_sorting(
1889 idx_path,
1890 &mut entries.to_vec(),
1891 pack_trailer,
1892 hash_bytes,
1893 sort_threads,
1894 )
1895}
1896
1897pub fn write_v2_pack_index(
1899 idx_path: &Path,
1900 pack_path: &Path,
1901 entries: &[(ObjectId, u64, u32)],
1902 hash_bytes: usize,
1903) -> Result<()> {
1904 let pack_bytes = fs::read(pack_path).map_err(Error::Io)?;
1905 if pack_bytes.len() < hash_bytes {
1906 return Err(Error::CorruptObject(
1907 "pack too small for idx trailer".into(),
1908 ));
1909 }
1910 let trailer = &pack_bytes[pack_bytes.len() - hash_bytes..];
1911 write_v2_pack_index_with_trailer(idx_path, entries, trailer, hash_bytes, None)
1912}
1913
1914const PARALLEL_INDEX_SORT_MIN: usize = 8_192;
1915
1916fn sort_index_entries(
1922 entries: &mut [(ObjectId, u64, u32)],
1923 sort_threads: Option<usize>,
1924) -> [u32; 256] {
1925 let lead = |entry: &(ObjectId, u64, u32)| usize::from(entry.0.as_bytes()[0]);
1926 let mut counts = [0usize; 256];
1927 for entry in entries.iter() {
1928 counts[lead(entry)] += 1;
1929 }
1930 let mut ends = [0usize; 256];
1931 let mut total = 0usize;
1932 for (end, count) in ends.iter_mut().zip(counts) {
1933 total += count;
1934 *end = total;
1935 }
1936 let mut next = [0usize; 256];
1937 next[1..].copy_from_slice(&ends[..255]);
1938 for bucket in 0..256 {
1939 while next[bucket] < ends[bucket] {
1940 let at = next[bucket];
1941 let home = lead(&entries[at]);
1942 if home == bucket {
1943 next[bucket] += 1;
1944 } else {
1945 entries.swap(at, next[home]);
1946 next[home] += 1;
1947 }
1948 }
1949 }
1950
1951 let by_oid =
1952 |a: &(ObjectId, u64, u32), b: &(ObjectId, u64, u32)| a.0.as_bytes().cmp(b.0.as_bytes());
1953 let threads = sort_threads.unwrap_or(1).max(1);
1954 if threads <= 1 || entries.len() < PARALLEL_INDEX_SORT_MIN {
1955 let mut start = 0;
1956 for end in ends {
1957 entries[start..end].sort_unstable_by(by_oid);
1958 start = end;
1959 }
1960 } else {
1961 let share = entries.len().div_ceil(threads);
1962 std::thread::scope(|scope| {
1963 let mut rest = &mut *entries;
1964 let mut taken = 0usize;
1965 for end in ends {
1966 if end - taken >= share || end == total {
1967 let (run, tail) = std::mem::take(&mut rest).split_at_mut(end - taken);
1968 rest = tail;
1969 taken = end;
1970 if run.len() > 1 {
1971 scope.spawn(move || run.sort_unstable_by(by_oid));
1972 }
1973 }
1974 }
1975 });
1976 }
1977 ends.map(|end| u32::try_from(end).unwrap_or(u32::MAX))
1978}
1979
1980pub(crate) fn write_v2_pack_index_sorting(
1991 idx_path: &Path,
1992 entries: &mut [(ObjectId, u64, u32)],
1993 pack_trailer: &[u8],
1994 hash_bytes: usize,
1995 sort_threads: Option<usize>,
1996) -> Result<()> {
1997 use std::io::Write;
1998
1999 let Some(algo) = HashAlgo::from_len(hash_bytes) else {
2000 return Err(Error::CorruptObject(format!(
2001 "unsupported index hash width {hash_bytes}"
2002 )));
2003 };
2004 if pack_trailer.len() != hash_bytes {
2005 return Err(Error::CorruptObject(format!(
2006 "pack trailer length {} does not match hash width {hash_bytes}",
2007 pack_trailer.len()
2008 )));
2009 }
2010 if let Some((oid, _, _)) = entries
2011 .iter()
2012 .find(|(oid, _, _)| oid.as_bytes().len() != hash_bytes)
2013 {
2014 return Err(Error::CorruptObject(format!(
2015 "OID width {} does not match index hash width {hash_bytes}",
2016 oid.as_bytes().len()
2017 )));
2018 }
2019 let fanout = sort_index_entries(entries, sort_threads);
2020
2021 let file = fs::File::create(idx_path).map_err(Error::Io)?;
2022 let mut out = std::io::BufWriter::with_capacity(256 * 1024, file);
2023 let mut hasher = algo.checksum_hasher();
2024 let mut put = |bytes: &[u8]| -> Result<()> {
2025 hasher.update(bytes);
2026 out.write_all(bytes).map_err(Error::Io)
2027 };
2028 put(b"\xfftOc")?;
2029 put(&2u32.to_be_bytes())?;
2030 for f in fanout {
2031 put(&f.to_be_bytes())?;
2032 }
2033 for (oid, _, _) in entries.iter() {
2034 put(oid.as_bytes())?;
2035 }
2036 for (_, _, crc) in entries.iter() {
2037 put(&crc.to_be_bytes())?;
2038 }
2039 let mut large = 0u32;
2040 for (_, off, _) in entries.iter() {
2041 match u32::try_from(*off) {
2042 Ok(small) if small < 0x8000_0000 => put(&small.to_be_bytes())?,
2043 _ => {
2044 if large >= 0x8000_0000 {
2045 return Err(Error::CorruptObject(
2046 "pack index large-offset table overflow".to_owned(),
2047 ));
2048 }
2049 put(&(0x8000_0000u32 | large).to_be_bytes())?;
2050 large += 1;
2051 }
2052 }
2053 }
2054 for (_, off, _) in entries.iter() {
2055 if *off >= 0x8000_0000 {
2056 put(&off.to_be_bytes())?;
2057 }
2058 }
2059 put(pack_trailer)?;
2060 let digest = hasher.finalize();
2061 out.write_all(&digest).map_err(Error::Io)?;
2062 out.flush().map_err(Error::Io)?;
2063 Ok(())
2064}
2065
2066#[must_use]
2067pub fn oid_bytes_to_hex(oid: &[u8]) -> String {
2069 hex::encode(oid)
2070}
2071
2072pub fn hash_object_bytes(kind: ObjectKind, data: &[u8], hash_bytes: usize) -> Result<Vec<u8>> {
2074 let Some(algo) = HashAlgo::from_len(hash_bytes) else {
2075 return Err(Error::CorruptObject(format!(
2076 "unsupported object hash width: {hash_bytes}"
2077 )));
2078 };
2079 Ok(crate::hash::hash_object(algo, kind, data)?
2080 .as_bytes()
2081 .to_vec())
2082}
2083
2084pub fn read_pack_index(idx_path: &Path) -> Result<PackIndex> {
2096 crate::pack_index::open_pack_index_file(idx_path, true)
2097}
2098
2099pub fn read_pack_index_no_verify(idx_path: &Path) -> Result<PackIndex> {
2105 crate::pack_index::open_pack_index_file(idx_path, false)
2106}
2107
2108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2110pub enum PackedType {
2111 Commit,
2113 Tree,
2115 Blob,
2117 Tag,
2119 OfsDelta,
2121 RefDelta,
2123}
2124
2125impl PackedType {
2126 #[must_use]
2128 pub fn as_str(self) -> &'static str {
2129 match self {
2130 Self::Commit => "commit",
2131 Self::Tree => "tree",
2132 Self::Blob => "blob",
2133 Self::Tag => "tag",
2134 Self::OfsDelta => "ofs-delta",
2135 Self::RefDelta => "ref-delta",
2136 }
2137 }
2138}
2139
2140#[derive(Debug, Clone)]
2142pub struct VerifyObjectRecord {
2143 pub oid: Vec<u8>,
2145 pub packed_type: PackedType,
2147 pub size: u64,
2149 pub size_in_pack: u64,
2151 pub offset: u64,
2153 pub depth: Option<u64>,
2155 pub base_oid: Option<Vec<u8>>,
2157 pub base_pack_offset: Option<u64>,
2159}
2160
2161enum DeltaBaseLink {
2163 Oid(Vec<u8>),
2165 Offset(u64),
2167}
2168
2169fn resolve_delta_depth(
2180 i: usize,
2181 base_links: &[Option<DeltaBaseLink>],
2182 by_oid: &HashMap<Vec<u8>, usize>,
2183 by_offset_idx: &HashMap<u64, usize>,
2184 records: &mut [VerifyObjectRecord],
2185) -> Result<u64> {
2186 if let Some(d) = records[i].depth {
2187 return Ok(d);
2188 }
2189 let Some(link) = &base_links[i] else {
2190 return Ok(0);
2191 };
2192 let base_idx = match link {
2193 DeltaBaseLink::Oid(oid) => by_oid.get(oid).copied(),
2194 DeltaBaseLink::Offset(off) => by_offset_idx.get(off).copied(),
2195 };
2196 records[i].depth = Some(1);
2198 let depth = match base_idx {
2199 Some(b) if b != i => {
2200 resolve_delta_depth(b, base_links, by_oid, by_offset_idx, records)?.saturating_add(1)
2201 }
2202 _ => 1,
2204 };
2205 records[i].depth = Some(depth);
2206 Ok(depth)
2207}
2208
2209pub const DEFAULT_PACK_DEPTH: usize = 50;
2211
2212#[must_use]
2214pub fn max_verify_pack_delta_depth(records: &[VerifyObjectRecord]) -> u64 {
2215 records.iter().filter_map(|r| r.depth).max().unwrap_or(0)
2216}
2217
2218pub(crate) fn delta_cycle_breaks(
2228 starts: impl IntoIterator<Item = ObjectId>,
2229 base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2230) -> Vec<ObjectId> {
2231 let mut seen: crate::objects::OidHashMap<Option<usize>> = crate::objects::oid_hash_map();
2233 let mut path: Vec<ObjectId> = Vec::new();
2234 let mut breaks = Vec::new();
2235 for start in starts {
2236 let mut cur = Some(start);
2237 while let Some(oid) = cur {
2238 match seen.get(&oid) {
2239 Some(None) => break,
2240 Some(Some(pos)) => {
2241 breaks.extend(path[*pos..].iter().min().copied());
2242 break;
2243 }
2244 None => {
2245 seen.insert(oid, Some(path.len()));
2246 path.push(oid);
2247 cur = base_of(&oid);
2248 }
2249 }
2250 }
2251 for oid in path.drain(..) {
2252 seen.insert(oid, None);
2253 }
2254 }
2255 breaks
2256}
2257
2258pub fn apply_delta_depth_limit(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2268 for target in delta_cycle_breaks(map.keys().copied(), |oid| map.get(oid).copied()) {
2269 map.remove(&target);
2270 }
2271 cap_delta_chain_depth(map, max_depth);
2272}
2273
2274pub(crate) fn cap_delta_chain_depth(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2277 let keys: Vec<ObjectId> = map.keys().copied().collect();
2278 let value_set: HashSet<ObjectId> = map.values().copied().collect();
2279 let mut tips: Vec<ObjectId> = keys
2280 .into_iter()
2281 .filter(|k| !value_set.contains(k))
2282 .collect();
2283 tips.sort();
2284
2285 let modulus = max_depth.saturating_add(1);
2286 let mut snip: HashSet<ObjectId> = HashSet::new();
2287
2288 for tip in tips {
2289 let mut chain: Vec<ObjectId> = Vec::new();
2290 let mut cur = tip;
2291 let mut seen = HashSet::new();
2292 while seen.insert(cur) {
2293 chain.push(cur);
2294 let Some(&b) = map.get(&cur) else {
2295 break;
2296 };
2297 cur = b;
2298 }
2299
2300 let n = chain.len();
2301 if n < 2 || n - 1 <= max_depth {
2302 continue;
2303 }
2304
2305 let mut total_depth = (n - 1) as u32;
2306 for &oid in &chain {
2307 let assigned = (total_depth as usize) % modulus;
2308 total_depth = total_depth.saturating_sub(1);
2309 if assigned == 0 {
2310 snip.insert(oid);
2311 }
2312 }
2313 }
2314
2315 for oid in snip {
2316 map.remove(&oid);
2317 }
2318}
2319
2320#[derive(Debug)]
2325pub(crate) struct VerifiedPackForFsck {
2326 pub idx: PackIndex,
2327 pub pack_bytes: Arc<PackData>,
2329 pub records: Vec<VerifyObjectRecord>,
2330}
2331
2332pub(crate) fn verify_pack_for_fsck(idx_path: &Path) -> Result<VerifiedPackForFsck> {
2340 let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, true)?;
2341 Ok(VerifiedPackForFsck {
2342 idx,
2343 pack_bytes,
2344 records,
2345 })
2346}
2347
2348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2350pub(crate) enum PackFsckTrailerIssue {
2351 PackChecksum,
2353 IdxMismatch,
2355}
2356
2357pub(crate) fn prepare_pack_for_object_fsck(
2361 idx_path: &Path,
2362) -> Result<(VerifiedPackForFsck, Option<PackFsckTrailerIssue>)> {
2363 let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, false)?;
2364 let trailer = pack_trailer_fsck_issue(idx_path, &idx, &pack_bytes);
2365 Ok((
2366 VerifiedPackForFsck {
2367 idx,
2368 pack_bytes,
2369 records,
2370 },
2371 trailer,
2372 ))
2373}
2374
2375fn pack_trailer_fsck_issue(
2376 idx_path: &Path,
2377 idx: &PackIndex,
2378 pack_bytes: &[u8],
2379) -> Option<PackFsckTrailerIssue> {
2380 let hb = idx.hash_bytes();
2381 let algo = HashAlgo::from_len(hb)?;
2382 if verify_trailer(algo, pack_bytes).is_err() {
2383 return Some(PackFsckTrailerIssue::PackChecksum);
2384 }
2385 let idx_file_bytes = fs::read(idx_path).ok()?;
2386 if idx_file_bytes.len() >= 2 * hb {
2387 let pack_end = pack_bytes.len().checked_sub(hb)?;
2388 let n = idx_file_bytes.len();
2389 let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
2390 if embedded != &pack_bytes[pack_end..] {
2391 return Some(PackFsckTrailerIssue::IdxMismatch);
2392 }
2393 }
2394 None
2395}
2396
2397pub fn verify_pack_and_collect(idx_path: &Path) -> Result<Vec<VerifyObjectRecord>> {
2403 let VerifiedPackForFsck {
2404 idx,
2405 pack_bytes,
2406 records,
2407 } = verify_pack_for_fsck(idx_path)?;
2408 let hb = idx.hash_bytes();
2409 for entry in idx.iter() {
2410 let obj = read_object_from_pack_bytes(&pack_bytes, &idx, entry.oid())?;
2411 let computed = hash_object_bytes(obj.kind, &obj.data, hb)?;
2412 if computed.as_slice() != entry.oid() {
2413 return Err(Error::CorruptObject(format!(
2414 "pack object hash mismatch at offset {} (index says {})",
2415 entry.offset()?,
2416 oid_bytes_to_hex(entry.oid())
2417 )));
2418 }
2419 }
2420 Ok(records)
2421}
2422
2423fn verify_pack_structure_and_records(
2424 idx_path: &Path,
2425 verify_pack_trailer: bool,
2426) -> Result<(PackIndex, Arc<PackData>, Vec<VerifyObjectRecord>)> {
2427 let idx = read_pack_index(idx_path)?;
2428 let idx_file_bytes = fs::read(idx_path).map_err(Error::Io)?;
2429 let pack_bytes = PackData::open_with_min_len(&idx.pack_path, 0)?;
2430 let hb = idx.hash_bytes();
2431 if pack_bytes.len() < 12 + hb {
2432 return Err(Error::CorruptObject(format!(
2433 "pack file {} is too small",
2434 idx.pack_path.display()
2435 )));
2436 }
2437 let pack_end = pack_bytes.len() - hb;
2438 let Some(algo) = HashAlgo::from_len(hb) else {
2439 return Err(Error::CorruptObject(format!(
2440 "unsupported OID width {hb} for pack {}",
2441 idx.pack_path.display()
2442 )));
2443 };
2444 if verify_pack_trailer {
2445 verify_trailer(algo, &pack_bytes).map_err(|e| {
2446 Error::CorruptObject(format!(
2447 "pack trailing checksum mismatch for {}: {e}",
2448 idx.pack_path.display()
2449 ))
2450 })?;
2451 if idx_file_bytes.len() >= 2 * hb {
2454 let n = idx_file_bytes.len();
2455 let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
2456 if embedded != &pack_bytes[pack_end..] {
2457 return Err(Error::CorruptObject(format!(
2458 "pack checksum in index does not match {}",
2459 idx.pack_path.display()
2460 )));
2461 }
2462 }
2463 }
2464 if &pack_bytes[0..4] != b"PACK" {
2465 return Err(Error::CorruptObject(format!(
2466 "pack file {} has invalid signature",
2467 idx.pack_path.display()
2468 )));
2469 }
2470 let version = u32::from_be_bytes(pack_bytes[4..8].try_into().unwrap_or([0, 0, 0, 0]));
2471 if version != 2 && version != 3 {
2472 return Err(Error::CorruptObject(format!(
2473 "unsupported pack version {} in {}",
2474 version,
2475 idx.pack_path.display()
2476 )));
2477 }
2478 let count = u32::from_be_bytes(pack_bytes[8..12].try_into().unwrap_or([0, 0, 0, 0])) as usize;
2479 if count != idx.len() {
2480 return Err(Error::CorruptObject(format!(
2481 "pack/index object count mismatch for {}",
2482 idx.pack_path.display()
2483 )));
2484 }
2485
2486 let mut by_offset: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
2487 for entry in idx.iter() {
2488 by_offset.insert(entry.offset()?, entry.oid().to_vec());
2489 }
2490 let offsets: Vec<u64> = by_offset.keys().copied().collect();
2491 if offsets.is_empty() {
2492 return Ok((idx, pack_bytes, Vec::new()));
2493 }
2494
2495 let mut by_oid: HashMap<Vec<u8>, usize> = HashMap::new();
2496 let mut by_offset_idx: HashMap<u64, usize> = HashMap::new();
2497 let mut records: Vec<VerifyObjectRecord> = Vec::with_capacity(offsets.len());
2498 let mut base_links: Vec<Option<DeltaBaseLink>> = Vec::with_capacity(offsets.len());
2502 for (i, offset) in offsets.iter().copied().enumerate() {
2503 let oid = by_offset.get(&offset).cloned().ok_or_else(|| {
2504 Error::CorruptObject(format!("missing object id for offset {}", offset))
2505 })?;
2506 let next_off = offsets
2507 .get(i + 1)
2508 .copied()
2509 .unwrap_or((pack_bytes.len() - hb) as u64);
2510 if next_off <= offset || next_off > (pack_bytes.len() - hb) as u64 {
2511 return Err(Error::CorruptObject(format!(
2512 "invalid object boundaries at offset {} in {}",
2513 offset,
2514 idx.pack_path.display()
2515 )));
2516 }
2517 let mut p = offset as usize;
2518 let (packed_type, size) = parse_pack_object_header(&pack_bytes, &mut p)?;
2519 let mut base_oid: Option<Vec<u8>> = None;
2520 let mut base_link: Option<DeltaBaseLink> = None;
2521
2522 match packed_type {
2523 PackedType::RefDelta => {
2524 if p + hb > pack_bytes.len() {
2525 return Err(Error::CorruptObject(format!(
2526 "truncated ref-delta base at offset {}",
2527 offset
2528 )));
2529 }
2530 let raw = pack_bytes[p..p + hb].to_vec();
2531 base_oid = Some(raw.clone());
2532 base_link = Some(DeltaBaseLink::Oid(raw));
2533 }
2534 PackedType::OfsDelta => {
2535 let base_offset = parse_ofs_delta_base(&pack_bytes, &mut p, offset)?;
2536 base_link = Some(DeltaBaseLink::Offset(base_offset));
2537 }
2538 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {}
2539 }
2540
2541 let size_in_pack = next_off - offset;
2542 let base_pack_offset = match &base_link {
2543 Some(DeltaBaseLink::Offset(off)) => Some(*off),
2544 _ => None,
2545 };
2546 records.push(VerifyObjectRecord {
2547 oid: oid.clone(),
2548 packed_type,
2549 size,
2550 size_in_pack,
2551 offset,
2552 depth: None,
2553 base_oid,
2554 base_pack_offset,
2555 });
2556 base_links.push(base_link);
2557 by_oid.insert(oid, i);
2558 by_offset_idx.insert(offset, i);
2559 }
2560
2561 for i in 0..records.len() {
2565 if base_links[i].is_some() {
2566 let _ = resolve_delta_depth(i, &base_links, &by_oid, &by_offset_idx, &mut records)?;
2567 }
2568 }
2569
2570 Ok((idx, pack_bytes, records))
2571}
2572
2573pub fn read_alternates_recursive(objects_dir: &Path) -> Result<Vec<PathBuf>> {
2579 let mut visited = HashSet::new();
2580 let mut out = Vec::new();
2581 read_alternates_inner(objects_dir, &mut visited, &mut out, 0)?;
2582 Ok(out)
2583}
2584
2585const MAX_ALTERNATE_DEPTH: usize = 5;
2587
2588fn parse_alternates_file_line(raw: &str) -> Option<PathBuf> {
2593 let line = raw.trim();
2594 if line.is_empty() || line.starts_with('#') {
2595 return None;
2596 }
2597 if line.starts_with('"') {
2598 let path = unquote_alternates_c_style(line)?;
2599 if path.is_empty() {
2600 return None;
2601 }
2602 return Some(PathBuf::from(path));
2603 }
2604 Some(PathBuf::from(line))
2605}
2606
2607fn unquote_alternates_c_style(line: &str) -> Option<String> {
2609 let bytes = line.as_bytes();
2610 if bytes.first() != Some(&b'"') {
2611 return None;
2612 }
2613 let mut out = Vec::new();
2614 let mut i = 1usize;
2615 while i < bytes.len() {
2616 if bytes[i] == b'"' {
2617 return String::from_utf8(out).ok();
2618 }
2619 if bytes[i] == b'\\' {
2620 i += 1;
2621 if i >= bytes.len() {
2622 return None;
2623 }
2624 match bytes[i] {
2625 b'\\' => out.push(b'\\'),
2626 b'"' => out.push(b'"'),
2627 b'a' => out.push(7),
2628 b'b' => out.push(8),
2629 b'f' => out.push(12),
2630 b'n' => out.push(b'\n'),
2631 b'r' => out.push(b'\r'),
2632 b't' => out.push(b'\t'),
2633 b'v' => out.push(11),
2634 c if c.is_ascii_digit() => {
2635 if i + 2 >= bytes.len() {
2636 return None;
2637 }
2638 let oct = std::str::from_utf8(&bytes[i..i + 3]).ok()?;
2639 out.push(u8::from_str_radix(oct, 8).ok()?);
2640 i += 2;
2641 }
2642 other => out.push(other),
2643 }
2644 i += 1;
2645 continue;
2646 }
2647 out.push(bytes[i]);
2648 i += 1;
2649 }
2650 None
2651}
2652
2653fn read_alternates_inner(
2654 objects_dir: &Path,
2655 visited: &mut HashSet<PathBuf>,
2656 out: &mut Vec<PathBuf>,
2657 depth: usize,
2658) -> Result<()> {
2659 if depth > MAX_ALTERNATE_DEPTH {
2660 return Ok(());
2661 }
2662 let alt_file = objects_dir.join("info").join("alternates");
2666 let text = match fs::read_to_string(&alt_file) {
2667 Ok(text) => text,
2668 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
2669 Err(err) => return Err(Error::Io(err)),
2670 };
2671 let canonical = canonical_or_self(objects_dir);
2672
2673 for raw in text.lines() {
2674 let Some(path) = parse_alternates_file_line(raw) else {
2675 continue;
2676 };
2677 let candidate = if path.is_absolute() {
2678 path
2679 } else {
2680 canonical.join(path)
2681 };
2682 let candidate = canonical_or_self(&candidate);
2683 if visited.insert(candidate.clone()) {
2684 out.push(candidate.clone());
2685 read_alternates_inner(&candidate, visited, out, depth + 1)?;
2686 }
2687 }
2688 Ok(())
2689}
2690
2691fn canonical_or_self(path: &Path) -> PathBuf {
2692 path.canonicalize_simplified()
2693 .unwrap_or_else(|_| path.to_path_buf())
2694}
2695
2696fn packed_type_to_kind(pt: PackedType) -> Result<ObjectKind> {
2698 match pt {
2699 PackedType::Commit => Ok(ObjectKind::Commit),
2700 PackedType::Tree => Ok(ObjectKind::Tree),
2701 PackedType::Blob => Ok(ObjectKind::Blob),
2702 PackedType::Tag => Ok(ObjectKind::Tag),
2703 PackedType::OfsDelta | PackedType::RefDelta => Err(Error::CorruptObject(
2704 "cannot convert delta type to object kind directly".to_owned(),
2705 )),
2706 }
2707}
2708
2709thread_local! {
2711 static DELTA_SIZE_ZLIB: std::cell::RefCell<crate::zlib_inflate::ZlibInflateScratch> =
2712 std::cell::RefCell::new(crate::zlib_inflate::ZlibInflateScratch::default());
2713}
2714thread_local! {
2716 static PACK_READ_INFO_CACHE: std::cell::RefCell<HashMap<PackReadInfoCacheKey, ObjectInfo>> =
2717 std::cell::RefCell::new(HashMap::new());
2718}
2719
2720const PACK_READ_INFO_CACHE_CAP: usize = 600_000;
2721
2722#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2723struct PackReadInfoCacheKey {
2724 pack_id: u32,
2725 offset: u64,
2726}
2727
2728fn pack_read_info_cache_key(pack_path: &Path, offset: u64) -> Option<PackReadInfoCacheKey> {
2729 if !crate::pack_store::PackStore::has_context() {
2730 return None;
2731 }
2732 Some(PackReadInfoCacheKey {
2733 pack_id: pack_cache::pack_id_for(pack_path),
2734 offset,
2735 })
2736}
2737
2738pub(crate) fn clear_pack_read_info_cache() {
2740 PACK_READ_INFO_CACHE.with(|c| c.borrow_mut().clear());
2741}
2742
2743fn drop_pack_read_info_cache_for(pack_path: &Path) {
2744 let pack_id = pack_cache::pack_id_for(pack_path);
2745 PACK_READ_INFO_CACHE.with(|c| {
2746 c.borrow_mut().retain(|k, _| k.pack_id != pack_id);
2747 });
2748}
2749
2750fn cached_pack_read_info(pack_path: &Path, offset: u64) -> Option<ObjectInfo> {
2751 let key = pack_read_info_cache_key(pack_path, offset)?;
2752 PACK_READ_INFO_CACHE.with(|c| c.borrow().get(&key).cloned())
2753}
2754
2755fn remember_pack_read_info(pack_path: &Path, offset: u64, info: ObjectInfo) {
2756 let Some(key) = pack_read_info_cache_key(pack_path, offset) else {
2757 return;
2758 };
2759 PACK_READ_INFO_CACHE.with(|c| {
2760 let mut g = c.borrow_mut();
2761 if g.len() >= PACK_READ_INFO_CACHE_CAP {
2762 g.clear();
2763 }
2764 g.insert(key, info);
2765 });
2766}
2767pub(crate) fn read_delta_result_size_from_pack_zlib(bytes: &[u8], pos: &mut usize) -> Result<u64> {
2771 DELTA_SIZE_ZLIB.with(|slot| slot.borrow_mut().read_pack_delta_result_size(bytes, pos))
2772}
2773
2774fn decompress_pack_data_into(
2775 bytes: &[u8],
2776 pos: &mut usize,
2777 expected_size: u64,
2778 scratch: &mut ZlibInflateScratch,
2779 out: &mut Vec<u8>,
2780) -> Result<()> {
2781 scratch.decompress_fixed_into(bytes, pos, expected_size, out)
2782}
2783
2784const MAX_DELTA_CHAIN_READ_DEPTH: usize = 4096;
2793
2794#[derive(Clone)]
2796struct PendingDeltaFrame {
2797 pack_id: u32,
2798 offset: u64,
2799 delta: Vec<u8>,
2800}
2801
2802#[derive(Default)]
2806struct DeltaChainState {
2807 depth: usize,
2808 visited: HashSet<(u32, u64)>,
2809 pending: Vec<PendingDeltaFrame>,
2810 inflate: ZlibInflateScratch,
2811 whole_object: Vec<u8>,
2812 delta_decompress: Vec<u8>,
2813 chain_a: Vec<u8>,
2814 chain_b: Vec<u8>,
2815}
2816
2817thread_local! {
2819 static PACK_READ_DELTA_STATE: std::cell::RefCell<DeltaChainState> =
2820 std::cell::RefCell::new(DeltaChainState::default());
2821}
2822
2823fn with_pack_read_delta_state<R>(
2824 start_depth: usize,
2825 f: impl FnOnce(&mut DeltaChainState) -> R,
2826) -> R {
2827 PACK_READ_DELTA_STATE.with(|slot| {
2828 let mut state = slot.borrow_mut();
2829 state.depth = start_depth;
2830 state.visited.clear();
2831 f(&mut state)
2832 })
2833}
2834
2835impl DeltaChainState {
2836 fn visit(&mut self, pack_id: u32, offset: u64) -> Result<()> {
2837 if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
2838 return Err(Error::DeltaChainTooDeep {
2839 limit: MAX_DELTA_CHAIN_READ_DEPTH,
2840 });
2841 }
2842 if !self.visited.insert((pack_id, offset)) {
2843 return Err(Error::DeltaChainTooDeep {
2844 limit: MAX_DELTA_CHAIN_READ_DEPTH,
2845 });
2846 }
2847 Ok(())
2848 }
2849
2850 fn next_hop(&mut self) -> Result<()> {
2851 self.depth += 1;
2852 if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
2853 return Err(Error::DeltaChainTooDeep {
2854 limit: MAX_DELTA_CHAIN_READ_DEPTH,
2855 });
2856 }
2857 Ok(())
2858 }
2859}
2860
2861fn copy_payload_preserve_capacity(out: &mut Vec<u8>, payload: &[u8]) {
2863 out.clear();
2864 out.extend_from_slice(payload);
2865}
2866
2867#[allow(clippy::too_many_arguments)]
2870fn apply_delta_chain_forward_into(
2871 out: &mut Vec<u8>,
2872 kind: ObjectKind,
2873 base_data: &[u8],
2874 whole_base: Option<(u32, u64)>,
2875 pending: &[PendingDeltaFrame],
2876 result_pack_id: u32,
2877 result_offset: u64,
2878 populate_delta_lru: bool,
2879 state: &mut DeltaChainState,
2880) -> Result<()> {
2881 if pending.is_empty() {
2882 copy_payload_preserve_capacity(out, base_data);
2883 return Ok(());
2884 }
2885
2886 let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
2887 let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
2888 Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
2889 if caching {
2890 if let Some((pid, off)) = whole_base {
2891 cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
2892 }
2893 }
2894
2895 let chain_a = &mut state.chain_a;
2896 let chain_b = &mut state.chain_b;
2897 let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
2898 let last = rev
2899 .pop()
2900 .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
2901 if rev.is_empty() {
2902 apply_delta_into(out, base_data, &last.delta)?;
2903 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
2904 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
2905 }
2906 if caching {
2907 pack_cache::put_delta_bases(kind, &cache_inserts);
2908 }
2909 return Ok(());
2910 }
2911
2912 let Some(first) = rev.first().copied() else {
2913 return Err(Error::CorruptObject(
2914 "delta chain missing base-adjacent frame".to_owned(),
2915 ));
2916 };
2917 apply_delta_into(chain_a, base_data, &first.delta)?;
2918 if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
2919 cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
2920 }
2921 let mut src_is_a = true;
2922 for frame in rev.iter().skip(1) {
2923 if src_is_a {
2924 apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
2925 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
2926 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
2927 }
2928 } else {
2929 apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
2930 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
2931 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
2932 }
2933 }
2934 src_is_a = !src_is_a;
2935 }
2936 let penultimate_base = if src_is_a {
2937 chain_a.as_slice()
2938 } else {
2939 chain_b.as_slice()
2940 };
2941 apply_delta_into(out, penultimate_base, &last.delta)?;
2942 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
2943 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
2944 }
2945 if caching {
2946 pack_cache::put_delta_bases(kind, &cache_inserts);
2947 }
2948 Ok(())
2949}
2950
2951fn commit_prefix_has_author(data: &[u8]) -> bool {
2952 let mut pos = 0usize;
2953 while pos < data.len() {
2954 let line_start = pos;
2955 let mut line_end = pos;
2956 while line_end < data.len() && data[line_end] != b'\n' {
2957 line_end += 1;
2958 }
2959 let line = &data[line_start..line_end];
2960 let after_nl = line_end.saturating_add(1);
2961 if line.is_empty() {
2962 return false;
2963 }
2964 if line.first() == Some(&b' ') {
2965 return false;
2966 }
2967 let Some(key_end) = line.iter().position(|&b| b == b' ') else {
2968 return false;
2969 };
2970 if &line[..key_end] == b"author" {
2971 return true;
2972 }
2973 pos = after_nl;
2974 }
2975 false
2976}
2977
2978#[allow(clippy::too_many_arguments)]
2979fn apply_delta_chain_forward_prefix_into(
2980 out: &mut Vec<u8>,
2981 kind: ObjectKind,
2982 base_data: &[u8],
2983 whole_base: Option<(u32, u64)>,
2984 pending: &[PendingDeltaFrame],
2985 result_pack_id: u32,
2986 result_offset: u64,
2987 populate_delta_lru: bool,
2988 state: &mut DeltaChainState,
2989 max_out: usize,
2990) -> Result<()> {
2991 if pending.is_empty() {
2992 out.clear();
2993 let take = base_data.len().min(max_out);
2994 out.extend_from_slice(&base_data[..take]);
2995 return Ok(());
2996 }
2997
2998 let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
2999 let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
3000 Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
3001 if caching {
3002 if let Some((pid, off)) = whole_base {
3003 cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
3004 }
3005 }
3006
3007 let chain_a = &mut state.chain_a;
3008 let chain_b = &mut state.chain_b;
3009 let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
3010 let last = rev
3011 .pop()
3012 .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
3013 let prefix_ok = |buf: &[u8]| commit_prefix_has_author(buf);
3014 if rev.is_empty() {
3015 apply_delta_prefix_into(out, base_data, &last.delta, max_out, prefix_ok)?;
3016 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3017 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3018 }
3019 if caching {
3020 pack_cache::put_delta_bases(kind, &cache_inserts);
3021 }
3022 return Ok(());
3023 }
3024
3025 let Some(first) = rev.first().copied() else {
3026 return Err(Error::CorruptObject(
3027 "delta chain missing base-adjacent frame".to_owned(),
3028 ));
3029 };
3030 apply_delta_into(chain_a, base_data, &first.delta)?;
3031 if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
3032 cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
3033 }
3034 let mut src_is_a = true;
3035 for frame in rev.iter().skip(1) {
3036 if src_is_a {
3037 apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
3038 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3039 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
3040 }
3041 } else {
3042 apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
3043 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3044 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
3045 }
3046 }
3047 src_is_a = !src_is_a;
3048 }
3049 let penultimate_base = if src_is_a {
3050 chain_a.as_slice()
3051 } else {
3052 chain_b.as_slice()
3053 };
3054 apply_delta_prefix_into(out, penultimate_base, &last.delta, max_out, prefix_ok)?;
3055 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3056 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3057 }
3058 if caching {
3059 pack_cache::put_delta_bases(kind, &cache_inserts);
3060 }
3061 Ok(())
3062}
3063
3064#[allow(clippy::too_many_arguments)]
3065fn finish_pack_read_prefix_with_pending(
3066 out: &mut Vec<u8>,
3067 kind: ObjectKind,
3068 base: &[u8],
3069 whole_base: Option<(u32, u64)>,
3070 result_pack_id: u32,
3071 result_offset: u64,
3072 populate_delta_lru: bool,
3073 state: &mut DeltaChainState,
3074) -> Result<ObjectKind> {
3075 let pending = std::mem::take(&mut state.pending);
3076 apply_delta_chain_forward_prefix_into(
3077 out,
3078 kind,
3079 base,
3080 whole_base,
3081 &pending,
3082 result_pack_id,
3083 result_offset,
3084 populate_delta_lru,
3085 state,
3086 COMMIT_HEADER_READ_PREFIX,
3087 )?;
3088 Ok(kind)
3089}
3090
3091#[allow(clippy::too_many_arguments)]
3092fn finish_pack_read_with_pending(
3093 out: &mut Vec<u8>,
3094 kind: ObjectKind,
3095 base: &[u8],
3096 whole_base: Option<(u32, u64)>,
3097 result_pack_id: u32,
3098 result_offset: u64,
3099 populate_delta_lru: bool,
3100 state: &mut DeltaChainState,
3101) -> Result<ObjectKind> {
3102 let pending = std::mem::take(&mut state.pending);
3103 apply_delta_chain_forward_into(
3104 out,
3105 kind,
3106 base,
3107 whole_base,
3108 &pending,
3109 result_pack_id,
3110 result_offset,
3111 populate_delta_lru,
3112 state,
3113 )?;
3114 Ok(kind)
3115}
3116
3117enum PackIndexHandle<'a> {
3120 Borrowed(&'a PackIndex),
3121 Shared(Arc<PackIndex>),
3122}
3123
3124impl<'a> Clone for PackIndexHandle<'a> {
3125 fn clone(&self) -> Self {
3126 match self {
3127 Self::Borrowed(idx) => Self::Borrowed(idx),
3128 Self::Shared(arc) => Self::Shared(Arc::clone(arc)),
3129 }
3130 }
3131}
3132
3133impl PackIndexHandle<'_> {
3134 fn get(&self) -> &PackIndex {
3135 match self {
3136 Self::Borrowed(idx) => idx,
3137 Self::Shared(idx) => idx.as_ref(),
3138 }
3139 }
3140}
3141
3142fn find_other_pack_index(
3143 objects_dir: &Path,
3144 current: &PackIndex,
3145 oid: &ObjectId,
3146) -> Result<Option<Arc<PackIndex>>> {
3147 for idx in read_local_pack_indexes_cached(objects_dir)? {
3148 if idx.idx_path == current.idx_path {
3149 continue;
3150 }
3151 if idx.contains(oid)? {
3152 return Ok(Some(Arc::clone(&idx)));
3153 }
3154 }
3155 Ok(None)
3156}
3157
3158fn rescue_in_pack_base(
3161 idx: &PackIndex,
3162 base_offset: u64,
3163 objects_dir: Option<&Path>,
3164 state: &mut DeltaChainState,
3165) -> Result<Option<(ObjectKind, Vec<u8>)>> {
3166 let Some(pos) = idx.find_position_by_offset_sorted(base_offset)? else {
3167 return Ok(None);
3168 };
3169 let entry_oid = idx.oid_at(pos);
3170 if entry_oid.len() != idx.hash_bytes() {
3171 return Ok(None);
3172 }
3173 let base_oid = ObjectId::from_bytes(entry_oid)?;
3174 let Some(dir) = objects_dir else {
3175 return Ok(None);
3176 };
3177 let loose = dir
3178 .join(base_oid.loose_prefix())
3179 .join(base_oid.loose_suffix());
3180 if loose.is_file() {
3181 if let Ok(obj) = crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid) {
3182 return Ok(Some((obj.kind, obj.data)));
3183 }
3184 }
3185 if let Some(other_idx) = find_other_pack_index(dir, idx, &base_oid)? {
3186 let Some(off) = other_idx.find_offset(&base_oid)? else {
3187 return Ok(None);
3188 };
3189 let caller_pending = std::mem::take(&mut state.pending);
3192 let rescued =
3193 resolve_pack_object_at(PackIndexHandle::Shared(other_idx), off, Some(dir), state);
3194 state.pending = caller_pending;
3195 if let Ok((kind, data)) = rescued {
3196 return Ok(Some((kind, data)));
3197 }
3198 }
3199 Ok(None)
3200}
3201
3202fn pack_bytes_parse_may_be_stale(err: &Error) -> bool {
3204 matches!(err, Error::CorruptObject(_) | Error::Zlib(_))
3205}
3206
3207fn resolve_pack_object_at(
3213 start_idx: PackIndexHandle<'_>,
3214 start_offset: u64,
3215 objects_dir: Option<&Path>,
3216 state: &mut DeltaChainState,
3217) -> Result<(ObjectKind, Vec<u8>)> {
3218 resolve_pack_object_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
3219}
3220
3221fn resolve_pack_object_at_into(
3222 start_idx: PackIndexHandle<'_>,
3223 start_offset: u64,
3224 objects_dir: Option<&Path>,
3225 state: &mut DeltaChainState,
3226 populate_delta_lru: bool,
3227 out: &mut Vec<u8>,
3228) -> Result<ObjectKind> {
3229 resolve_pack_object_at_with_bytes_into(
3230 start_idx,
3231 start_offset,
3232 objects_dir,
3233 state,
3234 None,
3235 populate_delta_lru,
3236 out,
3237 )
3238}
3239
3240fn resolve_pack_object_at_with_bytes(
3241 start_idx: PackIndexHandle<'_>,
3242 start_offset: u64,
3243 objects_dir: Option<&Path>,
3244 state: &mut DeltaChainState,
3245 pack_bytes_override: Option<&[u8]>,
3246) -> Result<(ObjectKind, Vec<u8>)> {
3247 let mut out = Vec::new();
3248 let kind = resolve_pack_object_at_with_bytes_into(
3249 start_idx,
3250 start_offset,
3251 objects_dir,
3252 state,
3253 pack_bytes_override,
3254 true,
3255 &mut out,
3256 )?;
3257 Ok((kind, out))
3258}
3259
3260fn resolve_pack_object_at_with_bytes_into(
3261 start_idx: PackIndexHandle<'_>,
3262 start_offset: u64,
3263 objects_dir: Option<&Path>,
3264 state: &mut DeltaChainState,
3265 pack_bytes_override: Option<&[u8]>,
3266 populate_delta_lru: bool,
3267 out: &mut Vec<u8>,
3268) -> Result<ObjectKind> {
3269 let pack_path = start_idx.get().pack_path.clone();
3270 let start_pack_id = pack_cache::pack_id_for(&pack_path);
3271 for attempt in 0..2 {
3272 match resolve_pack_object_at_body(
3273 start_idx.clone(),
3274 start_offset,
3275 start_pack_id,
3276 objects_dir,
3277 state,
3278 pack_bytes_override,
3279 populate_delta_lru,
3280 out,
3281 ) {
3282 Ok(kind) => return Ok(kind),
3283 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3284 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3285 state.visited.retain(|(id, _)| *id != start_pack_id);
3286 continue;
3287 }
3288 return Err(e);
3289 }
3290 Err(e) => return Err(e),
3291 }
3292 }
3293 unreachable!("at most two resolution attempts")
3294}
3295
3296#[allow(clippy::too_many_arguments)]
3297fn resolve_pack_object_at_body(
3298 mut cur_idx: PackIndexHandle<'_>,
3299 start_offset: u64,
3300 result_pack_id: u32,
3301 objects_dir: Option<&Path>,
3302 state: &mut DeltaChainState,
3303 pack_bytes_override: Option<&[u8]>,
3304 populate_delta_lru: bool,
3305 out: &mut Vec<u8>,
3306) -> Result<ObjectKind> {
3307 let result_offset = start_offset;
3308 let mut cur_offset = start_offset;
3309 state.pending.clear();
3310
3311 let mut cur_pack_path = cur_idx.get().pack_path.clone();
3312 let mut cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3313 let mut validated_pack_count = false;
3314
3315 enum PackBytesSource<'a> {
3316 Borrowed(&'a [u8]),
3317 Cached(Arc<PackData>),
3318 }
3319
3320 let mut pack_bytes = match pack_bytes_override {
3321 Some(b) => PackBytesSource::Borrowed(b),
3322 None => PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?),
3323 };
3324
3325 let mut compressed = Vec::new();
3326 loop {
3327 let idx_ref = cur_idx.get();
3328 if idx_ref.pack_path != cur_pack_path {
3329 cur_pack_path = idx_ref.pack_path.clone();
3330 pack_bytes = PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?);
3331 cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3332 validated_pack_count = false;
3333 }
3334 let pack_id = cur_pack_id;
3335 state.visit(pack_id, cur_offset)?;
3336
3337 if !validated_pack_count {
3338 let header: &[u8] = match &pack_bytes {
3339 PackBytesSource::Borrowed(b) => b,
3340 PackBytesSource::Cached(c) => c,
3341 };
3342 validate_pack_index_object_count(header, idx_ref)?;
3343 validated_pack_count = true;
3344 }
3345 let (bytes, mut pos) = match &pack_bytes {
3346 PackBytesSource::Borrowed(b) => (*b, cur_offset as usize),
3347 PackBytesSource::Cached(c) => {
3348 pack_entry_bytes(idx_ref, cur_offset, c, &mut compressed)?
3349 }
3350 };
3351 let object_start = cur_offset;
3352 let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
3353 Ok(v) => v,
3354 Err(err) => {
3355 if let Some((kind, data)) =
3356 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3357 {
3358 return finish_pack_read_with_pending(
3359 out,
3360 kind,
3361 &data,
3362 None,
3363 result_pack_id,
3364 result_offset,
3365 populate_delta_lru,
3366 state,
3367 );
3368 }
3369 return Err(err);
3370 }
3371 };
3372
3373 match packed_type {
3374 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3375 let kind = packed_type_to_kind(packed_type)?;
3376 if state.pending.is_empty() {
3377 if let Err(err) =
3378 decompress_pack_data_into(bytes, &mut pos, size, &mut state.inflate, out)
3379 {
3380 if let Some((rescue_kind, rescued)) =
3381 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3382 {
3383 return finish_pack_read_with_pending(
3384 out,
3385 rescue_kind,
3386 &rescued,
3387 None,
3388 result_pack_id,
3389 result_offset,
3390 populate_delta_lru,
3391 state,
3392 );
3393 }
3394 return Err(err);
3395 }
3396 return Ok(kind);
3397 }
3398 state.whole_object.clear();
3399 if let Err(err) = decompress_pack_data_into(
3400 bytes,
3401 &mut pos,
3402 size,
3403 &mut state.inflate,
3404 &mut state.whole_object,
3405 ) {
3406 if let Some((rescue_kind, rescued)) =
3407 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3408 {
3409 return finish_pack_read_with_pending(
3410 out,
3411 rescue_kind,
3412 &rescued,
3413 None,
3414 result_pack_id,
3415 result_offset,
3416 populate_delta_lru,
3417 state,
3418 );
3419 }
3420 return Err(err);
3421 }
3422 let whole = std::mem::take(&mut state.whole_object);
3423 return finish_pack_read_with_pending(
3424 out,
3425 kind,
3426 &whole,
3427 Some((pack_id, cur_offset)),
3428 result_pack_id,
3429 result_offset,
3430 populate_delta_lru,
3431 state,
3432 );
3433 }
3434 PackedType::OfsDelta => {
3435 state.next_hop()?;
3436 let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
3437 state.delta_decompress.clear();
3438 decompress_pack_data_into(
3439 bytes,
3440 &mut pos,
3441 size,
3442 &mut state.inflate,
3443 &mut state.delta_decompress,
3444 )?;
3445 state.pending.push(PendingDeltaFrame {
3446 pack_id,
3447 offset: object_start,
3448 delta: std::mem::take(&mut state.delta_decompress),
3449 });
3450 if let Some((base_kind, base_data)) =
3451 pack_cache::get_delta_base(pack_id, base_offset)
3452 {
3453 return finish_pack_read_with_pending(
3454 out,
3455 base_kind,
3456 base_data.as_ref(),
3457 None,
3458 result_pack_id,
3459 result_offset,
3460 populate_delta_lru,
3461 state,
3462 );
3463 }
3464 cur_offset = base_offset;
3465 }
3466 PackedType::RefDelta => {
3467 state.next_hop()?;
3468 let hb = idx_ref.hash_bytes();
3469 if pos + hb > bytes.len() {
3470 return Err(Error::CorruptObject(
3471 "truncated ref-delta base OID".to_owned(),
3472 ));
3473 }
3474 let base_raw = bytes[pos..pos + hb].to_vec();
3475 pos += hb;
3476 state.delta_decompress.clear();
3477 decompress_pack_data_into(
3478 bytes,
3479 &mut pos,
3480 size,
3481 &mut state.inflate,
3482 &mut state.delta_decompress,
3483 )?;
3484 state.pending.push(PendingDeltaFrame {
3485 pack_id,
3486 offset: object_start,
3487 delta: std::mem::take(&mut state.delta_decompress),
3488 });
3489
3490 if let Ok(base_oid) = ObjectId::from_bytes(base_raw.as_slice()) {
3491 if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
3492 if let Some((base_kind, base_data)) =
3493 pack_cache::get_delta_base(pack_id, base_offset)
3494 {
3495 return finish_pack_read_with_pending(
3496 out,
3497 base_kind,
3498 base_data.as_ref(),
3499 None,
3500 result_pack_id,
3501 result_offset,
3502 populate_delta_lru,
3503 state,
3504 );
3505 }
3506 cur_offset = base_offset;
3507 continue;
3508 }
3509 }
3510
3511 if hb == 20 {
3512 if let (Some(dir), Ok(base_oid)) =
3513 (objects_dir, ObjectId::from_bytes(base_raw.as_slice()))
3514 {
3515 let loose = dir
3516 .join(base_oid.loose_prefix())
3517 .join(base_oid.loose_suffix());
3518 if loose.is_file() {
3519 if let Ok(obj) =
3520 crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid)
3521 {
3522 return finish_pack_read_with_pending(
3523 out,
3524 obj.kind,
3525 &obj.data,
3526 None,
3527 result_pack_id,
3528 result_offset,
3529 populate_delta_lru,
3530 state,
3531 );
3532 }
3533 }
3534 if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
3535 let Some(off) = other_idx.find_offset(&base_oid)? else {
3536 return Err(Error::ObjectNotFound(base_oid.to_hex()));
3537 };
3538 cur_idx = PackIndexHandle::Shared(other_idx);
3539 cur_offset = off;
3540 continue;
3541 }
3542 }
3543 }
3544 return Err(Error::CorruptObject(format!(
3545 "ref-delta base {} not found in pack",
3546 oid_bytes_to_hex(&base_raw)
3547 )));
3548 }
3549 }
3550 }
3551}
3552
3553fn resolve_pack_object_info_at(
3555 start_idx: PackIndexHandle<'_>,
3556 start_offset: u64,
3557 objects_dir: Option<&Path>,
3558 state: &mut DeltaChainState,
3559) -> Result<ObjectInfo> {
3560 resolve_pack_object_info_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
3561}
3562
3563fn resolve_pack_object_info_at_body(
3564 cur_idx: PackIndexHandle<'_>,
3565 start_offset: u64,
3566 objects_dir: Option<&Path>,
3567 state: &mut DeltaChainState,
3568 pack_bytes_override: Option<&[u8]>,
3569) -> Result<ObjectInfo> {
3570 let cached_pack;
3571 let cur_bytes: &[u8] = if let Some(b) = pack_bytes_override {
3572 b
3573 } else {
3574 cached_pack = read_pack_bytes_cached(&cur_idx.get().pack_path)?;
3575 cached_pack.as_ref()
3576 };
3577 let mut cur_offset = start_offset;
3578 let mut result_size: Option<u64> = None;
3579
3580 loop {
3581 let idx_ref = cur_idx.get();
3582 let pack_id = pack_cache::pack_id_for(&idx_ref.pack_path);
3583 state.visit(pack_id, cur_offset)?;
3584
3585 if let Some(base) = cached_pack_read_info(&idx_ref.pack_path, cur_offset) {
3586 if let Some(size) = result_size {
3587 return Ok(ObjectInfo {
3588 kind: base.kind,
3589 size,
3590 });
3591 }
3592 return Ok(base);
3593 }
3594 let object_start = cur_offset;
3595 let mut pos = cur_offset as usize;
3596 let (packed_type, header_size) = parse_pack_object_header(cur_bytes, &mut pos)?;
3597
3598 match packed_type {
3599 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3600 let kind = packed_type_to_kind(packed_type)?;
3601 remember_pack_read_info(
3602 &idx_ref.pack_path,
3603 cur_offset,
3604 ObjectInfo {
3605 kind,
3606 size: header_size,
3607 },
3608 );
3609 let size = result_size.unwrap_or(header_size);
3610 return Ok(ObjectInfo { kind, size });
3611 }
3612 PackedType::OfsDelta => {
3613 state.next_hop()?;
3614 let base_offset = parse_ofs_delta_base(cur_bytes, &mut pos, object_start)?;
3615 if result_size.is_none() {
3616 result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
3617 }
3618 cur_offset = base_offset;
3619 }
3620 PackedType::RefDelta => {
3621 state.next_hop()?;
3622 let hb = idx_ref.hash_bytes();
3623 if pos + hb > cur_bytes.len() {
3624 return Err(Error::CorruptObject(
3625 "truncated ref-delta base OID".to_owned(),
3626 ));
3627 }
3628 let base_raw = &cur_bytes[pos..pos + hb];
3629 pos += hb;
3630 if result_size.is_none() {
3631 result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
3632 }
3633
3634 let base_oid = ObjectId::from_bytes(base_raw)?;
3635 if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
3636 cur_offset = base_offset;
3637 continue;
3638 }
3639
3640 if let Some(dir) = objects_dir {
3641 let loose = dir
3642 .join(base_oid.loose_prefix())
3643 .join(base_oid.loose_suffix());
3644 if loose.is_file() {
3645 let info = crate::odb::read_loose_object_info(&loose)?;
3646 let size = result_size.unwrap_or(info.size);
3647 return Ok(ObjectInfo {
3648 kind: info.kind,
3649 size,
3650 });
3651 }
3652 if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
3653 let Some(off) = other_idx.find_offset(&base_oid)? else {
3654 return Err(Error::ObjectNotFound(base_oid.to_hex()));
3655 };
3656 let base_info = resolve_pack_object_info_at(
3657 PackIndexHandle::Shared(other_idx),
3658 off,
3659 objects_dir,
3660 state,
3661 )?;
3662 let size = result_size.unwrap_or(base_info.size);
3663 return Ok(ObjectInfo {
3664 kind: base_info.kind,
3665 size,
3666 });
3667 }
3668 }
3669 return Err(Error::CorruptObject(format!(
3670 "ref-delta base {} not found in pack",
3671 oid_bytes_to_hex(base_raw)
3672 )));
3673 }
3674 }
3675 }
3676}
3677
3678fn read_pack_object_at(
3679 pack_bytes: &[u8],
3680 start_offset: u64,
3681 idx: &PackIndex,
3682 objects_dir: Option<&Path>,
3683 start_depth: usize,
3684) -> Result<(ObjectKind, Vec<u8>)> {
3685 let mut state = DeltaChainState {
3686 depth: start_depth,
3687 visited: HashSet::new(),
3688 ..Default::default()
3689 };
3690 resolve_pack_object_at_with_bytes(
3691 PackIndexHandle::Borrowed(idx),
3692 start_offset,
3693 objects_dir,
3694 &mut state,
3695 Some(pack_bytes),
3696 )
3697}
3698
3699pub fn read_object_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<Object> {
3708 if crate::pack_store::PackStore::has_context() {
3709 return read_object_from_pack_at_depth(idx, oid, 0);
3710 }
3711 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3712 crate::pack_store::PackStore::with_context(store, || {
3713 read_object_from_pack_at_depth(idx, oid, 0)
3714 })
3715}
3716
3717pub fn read_object_from_pack_into(
3723 idx: &PackIndex,
3724 oid: &ObjectId,
3725 buf: &mut Vec<u8>,
3726) -> Result<ObjectKind> {
3727 let Some(offset) = idx.find_offset(oid)? else {
3728 return Err(Error::ObjectNotFound(oid.to_hex()));
3729 };
3730 if crate::pack_store::PackStore::has_context() {
3731 return read_object_at_depth_into(idx, offset, 0, false, buf);
3732 }
3733 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3734 crate::pack_store::PackStore::with_context(store, || {
3735 read_object_at_depth_into(idx, offset, 0, false, buf)
3736 })
3737}
3738
3739pub fn read_object_info_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<ObjectInfo> {
3745 let Some(offset) = idx.find_offset(oid)? else {
3746 return Err(Error::ObjectNotFound(oid.to_hex()));
3747 };
3748 read_object_info_from_pack_at_offset(idx, offset)
3749}
3750
3751pub fn read_object_info_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<ObjectInfo> {
3757 let pack_path = idx.pack_path.clone();
3758 for attempt in 0..2 {
3759 let pack_bytes = read_pack_bytes_cached(&pack_path)?;
3760 if !pack_cache::pack_object_count_validated(&pack_path) {
3761 match validate_pack_index_object_count(&pack_bytes, idx) {
3762 Ok(()) => pack_cache::mark_pack_object_count_validated(&pack_path),
3763 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3764 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3765 continue;
3766 }
3767 return Err(e);
3768 }
3769 Err(e) => return Err(e),
3770 }
3771 }
3772 let mut header = [0u8; 16];
3775 if let Some(len) = pack_bytes.read_sparse(offset, &mut header) {
3776 if let Some(info) = read_whole_pack_entry_info_fast(idx, 0, &header[..len]) {
3777 if info.kind == ObjectKind::Tag {
3778 remember_pack_read_info(&pack_path, offset, info);
3779 }
3780 return Ok(info);
3781 }
3782 }
3783 match read_object_info_from_pack_at_offset_with_bytes(idx, offset, pack_bytes.as_ref()) {
3784 Ok(info) => return Ok(info),
3785 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3786 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3787 continue;
3788 }
3789 return Err(e);
3790 }
3791 Err(e) => return Err(e),
3792 }
3793 }
3794 unreachable!("at most two read attempts")
3795}
3796
3797const COMMIT_HEADER_BLOCK_WINDOW: usize = 4096;
3799
3800const COMMIT_HEADER_BLOCK_MAX: usize = 1024;
3803
3804pub(crate) fn read_packed_commit_header_block_into(
3820 objects_dir: &Path,
3821 oid: &ObjectId,
3822 out: &mut Vec<u8>,
3823) -> Result<bool> {
3824 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
3825 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
3826 }
3827 if let Some(idx) = pack_cache::hinted_pack_index(objects_dir) {
3828 if let Some(offset) = idx.find_offset(oid)? {
3829 return Ok(read_commit_header_block_at(&idx, offset, out));
3830 }
3831 }
3832 for idx in pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)? {
3833 if let Some(offset) = idx.find_offset(oid)? {
3834 pack_cache::remember_pack_hit(objects_dir, &idx);
3835 return Ok(read_commit_header_block_at(&idx, offset, out));
3836 }
3837 }
3838 Ok(false)
3839}
3840
3841fn read_commit_header_block_at(idx: &PackIndex, offset: u64, out: &mut Vec<u8>) -> bool {
3842 let Ok(pack_bytes) = read_pack_bytes_cached(&idx.pack_path) else {
3843 return false;
3844 };
3845 if !pack_cache::pack_object_count_validated(&idx.pack_path) {
3846 if validate_pack_index_object_count(&pack_bytes, idx).is_err() {
3847 return false;
3848 }
3849 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
3850 }
3851 let mut window = [0u8; COMMIT_HEADER_BLOCK_WINDOW];
3852 let Some(len) = pack_bytes.read_sparse(offset, &mut window) else {
3853 return false;
3854 };
3855 let window = &window[..len];
3856 let mut pos = 0usize;
3857 let Ok((PackedType::Commit, size)) = parse_pack_object_header(window, &mut pos) else {
3858 return false;
3859 };
3860 let want = usize::try_from(size).map_or(COMMIT_HEADER_BLOCK_MAX, |size| {
3861 size.min(COMMIT_HEADER_BLOCK_MAX)
3862 });
3863 let inflated = WHOLE_OBJECT_ZLIB.with(|slot| {
3864 slot.borrow_mut()
3865 .decompress_prefix_into(&window[pos..], want, out)
3866 });
3867 if inflated.is_err() || out.len() != want {
3868 return false;
3869 }
3870 let Some(blank) = out.windows(2).position(|pair| pair == b"\n\n") else {
3871 return false;
3872 };
3873 out.truncate(blank + 2);
3874 true
3875}
3876
3877fn read_object_info_from_pack_at_offset_with_bytes(
3878 idx: &PackIndex,
3879 offset: u64,
3880 pack_bytes: &[u8],
3881) -> Result<ObjectInfo> {
3882 if let Some(info) = read_whole_pack_entry_info_fast(idx, offset, pack_bytes) {
3883 return Ok(info);
3884 }
3885 let objects_dir = idx.pack_path.parent().and_then(Path::parent);
3886 with_pack_read_delta_state(0, |state| {
3887 resolve_pack_object_info_at_with_bytes(
3888 PackIndexHandle::Borrowed(idx),
3889 offset,
3890 objects_dir,
3891 state,
3892 Some(pack_bytes),
3893 )
3894 })
3895}
3896
3897fn resolve_pack_object_info_at_with_bytes(
3898 start_idx: PackIndexHandle<'_>,
3899 start_offset: u64,
3900 objects_dir: Option<&Path>,
3901 state: &mut DeltaChainState,
3902 pack_bytes_override: Option<&[u8]>,
3903) -> Result<ObjectInfo> {
3904 let idx_ref = start_idx.get();
3905 let pack_path = idx_ref.pack_path.clone();
3906 let start_pack_id = pack_cache::pack_id_for(&pack_path);
3907 if let Some(info) = cached_pack_read_info(&pack_path, start_offset) {
3908 return Ok(info);
3909 }
3910 for attempt in 0..2 {
3911 match resolve_pack_object_info_at_body(
3912 start_idx.clone(),
3913 start_offset,
3914 objects_dir,
3915 state,
3916 pack_bytes_override,
3917 ) {
3918 Ok(v) => {
3919 remember_pack_read_info(&pack_path, start_offset, v);
3920 return Ok(v);
3921 }
3922 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3923 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3924 state.visited.retain(|(id, _)| *id != start_pack_id);
3925 continue;
3926 }
3927 return Err(e);
3928 }
3929 Err(e) => return Err(e),
3930 }
3931 }
3932 unreachable!("at most two resolution attempts")
3933}
3934
3935#[inline]
3937fn read_whole_pack_entry_info_fast(
3938 _idx: &PackIndex,
3939 offset: u64,
3940 pack_bytes: &[u8],
3941) -> Option<ObjectInfo> {
3942 let mut pos = offset as usize;
3943 if pos >= pack_bytes.len() {
3944 return None;
3945 }
3946 let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
3947 match packed_type {
3948 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3949 let kind = packed_type_to_kind(packed_type).ok()?;
3950 Some(ObjectInfo {
3951 kind,
3952 size: header_size,
3953 })
3954 }
3955 PackedType::OfsDelta | PackedType::RefDelta => None,
3956 }
3957}
3958
3959thread_local! {
3961 static WHOLE_OBJECT_ZLIB: RefCell<ZlibInflateScratch> =
3962 RefCell::new(ZlibInflateScratch::default());
3963}
3964
3965pub(crate) const COMMIT_HEADER_READ_PREFIX: usize = 512;
3967
3968const COMMIT_SUBJECT_READ_STEP: usize = 128;
3973
3974fn pack_entry_bytes<'a>(
3976 idx: &PackIndex,
3977 offset: u64,
3978 pack: &'a PackData,
3979 buf: &'a mut Vec<u8>,
3980) -> Result<(&'a [u8], usize)> {
3981 let start = usize::try_from(offset)
3982 .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
3983 if !pack.uses_positioned_reads() {
3984 return Ok((pack, start));
3985 }
3986 let pack_end = pack
3987 .len()
3988 .checked_sub(idx.hash_bytes())
3989 .ok_or_else(|| Error::CorruptObject("truncated pack trailer".to_owned()))?;
3990 let end = idx.next_pack_offset_after(offset, pack_end as u64)? as usize;
3991 Ok((pack.read_range(start, end, buf)?, 0))
3992}
3993
3994fn read_whole_pack_entry_object_fast(
3995 idx: &PackIndex,
3996 offset: u64,
3997 pack_bytes: &PackData,
3998) -> Option<Object> {
3999 let mut data = Vec::new();
4000 read_whole_pack_entry_object_into(idx, offset, pack_bytes, &mut data)
4001 .map(|kind| Object::new(kind, data))
4002}
4003
4004fn read_whole_pack_entry_object_into(
4005 idx: &PackIndex,
4006 offset: u64,
4007 pack_bytes: &PackData,
4008 out: &mut Vec<u8>,
4009) -> Option<ObjectKind> {
4010 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4011 if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
4012 out.clear();
4013 out.extend_from_slice(data.as_ref());
4014 return Some(kind);
4015 }
4016 if cached_pack_read_info(&idx.pack_path, offset)
4019 .is_some_and(|info| info.kind == ObjectKind::Tag)
4020 {
4021 let mut bytes = [0u8; 4096];
4022 if let Some(len) = pack_bytes.read_sparse(offset, &mut bytes) {
4023 if let Some(kind) = inflate_whole_pack_entry_into(&bytes[..len], 0, out) {
4024 return Some(kind);
4025 }
4026 }
4027 }
4028 let mut compressed = Vec::new();
4029 let (pack_bytes, pos) = pack_entry_bytes(idx, offset, pack_bytes, &mut compressed).ok()?;
4030 inflate_whole_pack_entry_into(pack_bytes, pos as u64, out)
4031}
4032
4033fn inflate_whole_pack_entry_into(
4034 pack_bytes: &[u8],
4035 offset: u64,
4036 out: &mut Vec<u8>,
4037) -> Option<ObjectKind> {
4038 let mut pos = offset as usize;
4039 if pos >= pack_bytes.len() {
4040 return None;
4041 }
4042 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
4043 match packed_type {
4044 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4045 let kind = packed_type_to_kind(packed_type).ok()?;
4046 WHOLE_OBJECT_ZLIB.with(|slot| {
4047 let mut inflate = slot.borrow_mut();
4048 decompress_pack_data_into(pack_bytes, &mut pos, size, &mut inflate, out)
4049 .ok()
4050 .map(|()| kind)
4051 })
4052 }
4053 PackedType::OfsDelta | PackedType::RefDelta => None,
4054 }
4055}
4056
4057fn read_object_from_packs_in_context_into(
4058 objects_dir: &Path,
4059 oid: &ObjectId,
4060 buf: &mut Vec<u8>,
4061) -> Result<ObjectKind> {
4062 match try_read_object_from_packs_with_options_into(
4063 objects_dir,
4064 oid,
4065 PackLookupOptions::ALL_LOCAL_PACKS,
4066 false,
4067 buf,
4068 ) {
4069 Ok(kind) => Ok(kind),
4070 Err(err @ Error::ObjectNotFound(_)) => {
4071 if reprepare_pack_directory_on_miss(objects_dir)? {
4072 try_read_object_from_packs_with_options_into(
4073 objects_dir,
4074 oid,
4075 PackLookupOptions::ALL_LOCAL_PACKS,
4076 false,
4077 buf,
4078 )
4079 } else {
4080 Err(err)
4081 }
4082 }
4083 Err(err) => Err(err),
4084 }
4085}
4086
4087fn read_object_from_packs_in_context(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
4088 match try_read_object_from_packs_with_options(
4089 objects_dir,
4090 oid,
4091 PackLookupOptions::ALL_LOCAL_PACKS,
4092 ) {
4093 Ok(obj) => Ok(obj),
4094 Err(err @ Error::ObjectNotFound(_)) => {
4095 if reprepare_pack_directory_on_miss_in_context(objects_dir)? {
4096 try_read_object_from_packs_with_options(
4097 objects_dir,
4098 oid,
4099 PackLookupOptions::ALL_LOCAL_PACKS,
4100 )
4101 } else {
4102 Err(err)
4103 }
4104 }
4105 Err(err) => Err(err),
4106 }
4107}
4108
4109pub(crate) fn read_object_at(idx: &PackIndex, offset: u64) -> Result<Object> {
4114 read_object_at_depth(idx, offset, 0)
4115}
4116
4117pub fn read_object_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<Object> {
4121 read_object_at(idx, offset)
4122}
4123
4124fn read_object_from_pack_at_depth(idx: &PackIndex, oid: &ObjectId, depth: usize) -> Result<Object> {
4127 let Some(offset) = idx.find_offset(oid)? else {
4128 return Err(Error::ObjectNotFound(oid.to_hex()));
4129 };
4130 read_object_at_depth(idx, offset, depth)
4131}
4132
4133fn read_object_at_depth(idx: &PackIndex, offset: u64, depth: usize) -> Result<Object> {
4134 let mut data = Vec::new();
4135 let kind = read_object_at_depth_into(idx, offset, depth, true, &mut data)?;
4136 Ok(Object::new(kind, data))
4137}
4138
4139fn read_object_at_depth_into(
4140 idx: &PackIndex,
4141 offset: u64,
4142 depth: usize,
4143 populate_delta_lru: bool,
4144 out: &mut Vec<u8>,
4145) -> Result<ObjectKind> {
4146 if depth == 0 {
4147 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4148 if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
4149 out.clear();
4150 out.extend_from_slice(data.as_ref());
4151 return Ok(kind);
4152 }
4153 }
4154 let pack_path = &idx.pack_path;
4155 let objects_dir = idx.pack_path.parent().and_then(Path::parent);
4156 for attempt in 0..2 {
4157 let pack_bytes = read_pack_bytes_cached(pack_path)?;
4158 if !pack_cache::pack_object_count_validated(pack_path) {
4159 match validate_pack_index_object_count(&pack_bytes, idx) {
4160 Ok(()) => pack_cache::mark_pack_object_count_validated(pack_path),
4161 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4162 if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
4163 continue;
4164 }
4165 return Err(e);
4166 }
4167 Err(e) => return Err(e),
4168 }
4169 }
4170 if depth == 0 && !pack_bytes.uses_positioned_reads() {
4171 if let Some(kind) = read_whole_pack_entry_object_into(idx, offset, &pack_bytes, out) {
4172 return Ok(kind);
4173 }
4174 }
4175 let resolved = with_pack_read_delta_state(depth, |state| {
4176 resolve_pack_object_at_into(
4177 PackIndexHandle::Borrowed(idx),
4178 offset,
4179 objects_dir,
4180 state,
4181 populate_delta_lru,
4182 out,
4183 )
4184 });
4185 match resolved {
4186 Ok(kind) => return Ok(kind),
4187 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4188 if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
4189 continue;
4190 }
4191 return Err(e);
4192 }
4193 Err(e) => return Err(e),
4194 }
4195 }
4196 unreachable!("at most two read attempts")
4197}
4198
4199pub fn read_object_from_pack_bytes(
4201 pack_bytes: &[u8],
4202 idx: &PackIndex,
4203 oid: &[u8],
4204) -> Result<Object> {
4205 validate_pack_index_object_count(pack_bytes, idx)?;
4206 let id = ObjectId::from_bytes(oid).map_err(|_| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
4207 let entry_offset = idx
4208 .find_offset(&id)?
4209 .ok_or_else(|| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
4210 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
4211 crate::pack_store::PackStore::with_context(store, || {
4212 let (kind, data) = read_pack_object_at(pack_bytes, entry_offset, idx, None, 0)?;
4213 verify_packed_object_hash(kind, &data, oid)?;
4214 Ok(Object::new(kind, data))
4215 })
4216}
4217
4218fn validate_pack_index_object_count(pack_bytes: &[u8], idx: &PackIndex) -> Result<()> {
4219 if pack_bytes.len() < 12 || &pack_bytes[0..4] != b"PACK" {
4220 return Err(Error::CorruptObject("bad pack header".to_owned()));
4221 }
4222 let count =
4223 u32::from_be_bytes([pack_bytes[8], pack_bytes[9], pack_bytes[10], pack_bytes[11]]) as usize;
4224 if count != idx.len() {
4225 return Err(Error::CorruptObject(format!(
4226 "pack object count mismatch: pack has {count}, index has {}",
4227 idx.len()
4228 )));
4229 }
4230 Ok(())
4231}
4232
4233fn verify_packed_object_hash(kind: ObjectKind, data: &[u8], expected_oid: &[u8]) -> Result<()> {
4234 let algo = HashAlgo::from_len(expected_oid.len()).ok_or_else(|| {
4235 Error::CorruptObject(format!(
4236 "unsupported packed object id width: {}",
4237 expected_oid.len()
4238 ))
4239 })?;
4240 let actual = hash_object(algo, kind, data)?;
4241 if actual.as_bytes() != expected_oid {
4242 return Err(Error::CorruptObject(format!(
4243 "packed object {} hashes to {}",
4244 oid_bytes_to_hex(expected_oid),
4245 actual.to_hex()
4246 )));
4247 }
4248 Ok(())
4249}
4250
4251#[derive(Clone, Copy, Debug, Default)]
4253pub struct PackLookupOptions {
4254 pub skip_midx_covered_packs: bool,
4256 pub only_midx_covered_packs: bool,
4258}
4259
4260impl PackLookupOptions {
4261 pub const ALL_LOCAL_PACKS: Self = Self {
4263 skip_midx_covered_packs: false,
4264 only_midx_covered_packs: false,
4265 };
4266
4267 pub const MIDX_COVERED_PACKS: Self = Self {
4269 skip_midx_covered_packs: false,
4270 only_midx_covered_packs: true,
4271 };
4272
4273 fn allows_pack_shortcuts(self) -> bool {
4278 !self.skip_midx_covered_packs && !self.only_midx_covered_packs
4279 }
4280}
4281
4282fn select_single_local_pack_index(
4284 indexes: &[Arc<PackIndex>],
4285 commit_hint: Option<&crate::objects::ObjectId>,
4286) -> Result<Option<Arc<PackIndex>>> {
4287 if indexes.is_empty() {
4288 return Ok(None);
4289 }
4290 if indexes.len() == 1 {
4291 return Ok(Some(Arc::clone(&indexes[0])));
4292 }
4293 if let Some(oid) = commit_hint {
4294 let mut found: Option<Arc<PackIndex>> = None;
4295 for idx in indexes {
4296 if idx.find_offset(oid)?.is_some() {
4297 if found.is_some() {
4298 return Ok(None);
4299 }
4300 found = Some(Arc::clone(idx));
4301 }
4302 }
4303 if found.is_some() {
4304 return Ok(found);
4305 }
4306 }
4307 Ok(None)
4308}
4309
4310pub struct SingleLocalPackReader {
4314 idx: Arc<PackIndex>,
4315 pack_bytes: Arc<PackData>,
4316 pack_id: u32,
4317 cursor: usize,
4318 info_cache: HashMap<u64, ObjectInfo>,
4319 delta_scratch: Option<ZlibInflateScratch>,
4322 commit_header_buf: Vec<u8>,
4323}
4324
4325fn objects_dir_has_loose_objects(objects_dir: &Path) -> bool {
4326 let Ok(entries) = std::fs::read_dir(objects_dir) else {
4327 return false;
4328 };
4329 for entry in entries.flatten() {
4330 let name = entry.file_name();
4331 let name = name.to_string_lossy();
4332 if name.len() != 2 || !name.bytes().all(|b| b.is_ascii_hexdigit()) {
4333 continue;
4334 }
4335 let Ok(hex_entries) = std::fs::read_dir(entry.path()) else {
4336 continue;
4337 };
4338 if hex_entries.flatten().any(|e| e.path().is_file()) {
4339 return true;
4340 }
4341 }
4342 false
4343}
4344
4345impl SingleLocalPackReader {
4346 pub fn open(objects_dir: &Path) -> Result<Option<Self>> {
4352 Self::open_with_options(objects_dir, false, None)
4353 }
4354
4355 pub fn open_allow_loose(objects_dir: &Path) -> Result<Option<Self>> {
4357 Self::open_with_options(objects_dir, true, None)
4358 }
4359
4360 pub(crate) fn open_allow_loose_for_commits(
4363 objects_dir: &Path,
4364 commit_hint: &crate::objects::ObjectId,
4365 ) -> Result<Option<Self>> {
4366 Self::open_with_options(objects_dir, true, Some(commit_hint))
4367 }
4368
4369 pub(crate) fn open_for_pack_index(
4371 objects_dir: &Path,
4372 idx: Arc<PackIndex>,
4373 ) -> Result<Option<Self>> {
4374 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4375 return Ok(None);
4376 }
4377 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4378 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
4379 validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
4380 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
4381 pack_cache::remember_pack_hit(objects_dir, &idx);
4382 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4383 Ok(Some(Self {
4384 idx,
4385 pack_bytes,
4386 pack_id,
4387 cursor: 0,
4388 info_cache: HashMap::new(),
4389 delta_scratch: None,
4390 commit_header_buf: Vec::new(),
4391 }))
4392 }
4393
4394 pub(crate) fn object_kinds_at_sorted_offsets(
4405 &self,
4406 offsets: &[u64],
4407 ) -> Result<Vec<Option<ObjectKind>>> {
4408 let pack_bytes = self.pack_bytes.as_ref();
4409 let hash_bytes = self.idx.hash_bytes();
4410 let mut kinds: Vec<Option<ObjectKind>> = Vec::with_capacity(offsets.len());
4411 for (rank, &offset) in offsets.iter().enumerate() {
4412 let mut pos = usize::try_from(offset)
4413 .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
4414 let (packed_type, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
4415 let base_offset = match packed_type {
4416 PackedType::OfsDelta => Some(parse_ofs_delta_base(pack_bytes, &mut pos, offset)?),
4417 PackedType::RefDelta => {
4418 let raw = pack_bytes.get(pos..pos + hash_bytes).ok_or_else(|| {
4419 Error::CorruptObject("truncated ref-delta base OID".to_owned())
4420 })?;
4421 self.idx.find_offset(&ObjectId::from_bytes(raw)?)?
4422 }
4423 _ => {
4424 kinds.push(Some(packed_type_to_kind(packed_type)?));
4425 continue;
4426 }
4427 };
4428 let base_kind = base_offset
4429 .and_then(|base| offsets.binary_search(&base).ok())
4430 .filter(|&base_rank| base_rank < rank)
4431 .and_then(|base_rank| kinds[base_rank]);
4432 kinds.push(base_kind);
4433 }
4434 Ok(kinds)
4435 }
4436
4437 pub(crate) fn contains_oid(&self, oid: &crate::objects::ObjectId) -> Result<bool> {
4439 Ok(self.idx.find_offset(oid)?.is_some())
4440 }
4441
4442 fn open_with_options(
4443 objects_dir: &Path,
4444 allow_loose: bool,
4445 commit_hint: Option<&crate::objects::ObjectId>,
4446 ) -> Result<Option<Self>> {
4447 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4448 return Ok(None);
4449 }
4450 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4451 let memo_eligible = allow_loose && commit_hint.is_none();
4452 if memo_eligible {
4453 if let Some((idx, pack_bytes, pack_id)) = pack_cache::single_reader_for(objects_dir) {
4454 return Ok(Some(Self {
4455 idx,
4456 pack_bytes,
4457 pack_id,
4458 cursor: 0,
4459 info_cache: HashMap::new(),
4460 delta_scratch: None,
4461 commit_header_buf: Vec::new(),
4462 }));
4463 }
4464 }
4465 let indexes = pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)?;
4466 let idx = match select_single_local_pack_index(&indexes, commit_hint)? {
4467 Some(idx) => idx,
4468 None => return Ok(None),
4469 };
4470 if !allow_loose && objects_dir_has_loose_objects(objects_dir) {
4471 return Ok(None);
4472 }
4473 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
4474 validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
4475 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
4476 pack_cache::remember_pack_hit(objects_dir, &idx);
4477 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4478 if memo_eligible && indexes.len() == 1 {
4479 pack_cache::remember_single_reader(objects_dir, &idx, &pack_bytes, pack_id);
4480 }
4481 Ok(Some(Self {
4482 idx,
4483 pack_bytes,
4484 pack_id,
4485 cursor: 0,
4486 info_cache: HashMap::new(),
4487 delta_scratch: None,
4488 commit_header_buf: Vec::new(),
4489 }))
4490 }
4491
4492 pub fn read_commit_author_prefix(&mut self, oid: &ObjectId) -> Result<Option<&[u8]>> {
4501 self.read_commit_author_prefix_at(oid, true)
4502 }
4503
4504 pub(crate) fn read_commit_author_prefix_unordered(
4506 &mut self,
4507 oid: &ObjectId,
4508 ) -> Result<Option<&[u8]>> {
4509 self.read_commit_author_prefix_at(oid, false)
4510 }
4511
4512 pub(crate) fn read_commit_author_prefix_at_pack_row(
4514 &mut self,
4515 row: usize,
4516 ) -> Result<Option<&[u8]>> {
4517 self.cursor = row;
4518 let offset = self.idx.offset_at(row)?;
4519 self.read_commit_author_prefix_at_pack_offset(offset)
4520 }
4521
4522 fn read_commit_author_prefix_at(
4523 &mut self,
4524 oid: &ObjectId,
4525 monotonic: bool,
4526 ) -> Result<Option<&[u8]>> {
4527 let offset = if monotonic {
4528 self.idx.find_offset_monotonic(oid, &mut self.cursor)?
4529 } else {
4530 self.idx.find_offset(oid)?
4531 };
4532 let Some(offset) = offset else {
4533 return Ok(None);
4534 };
4535 self.read_commit_author_prefix_at_pack_offset(offset)
4536 }
4537
4538 fn read_commit_author_prefix_at_pack_offset(&mut self, offset: u64) -> Result<Option<&[u8]>> {
4539 self.commit_header_buf.clear();
4540 let filled = with_pack_read_delta_state(0, |state| {
4541 self.resolve_commit_author_prefix_at_pack_offset(offset, state)
4542 })?;
4543 if !filled || self.commit_header_buf.is_empty() {
4544 return Ok(None);
4545 }
4546 Ok(Some(&self.commit_header_buf))
4547 }
4548
4549 fn resolve_commit_author_prefix_at_pack_offset(
4550 &mut self,
4551 start_offset: u64,
4552 state: &mut DeltaChainState,
4553 ) -> Result<bool> {
4554 let pack_bytes = self.pack_bytes.as_ref();
4555 let mut cur_offset = start_offset;
4556 let result_offset = start_offset;
4557 let result_pack_id = self.pack_id;
4558 state.pending.clear();
4559
4560 let mut compressed = Vec::new();
4561 loop {
4562 state.visit(self.pack_id, cur_offset)?;
4563 let (bytes, mut pos) =
4564 pack_entry_bytes(&self.idx, cur_offset, pack_bytes, &mut compressed)?;
4565 let object_start = cur_offset;
4566 let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
4567 Ok(v) => v,
4568 Err(_) => return Ok(false),
4569 };
4570 match packed_type {
4571 PackedType::Commit => {
4572 let kind = ObjectKind::Commit;
4573 if state.pending.is_empty() {
4574 self.delta_scratch
4575 .get_or_insert_with(ZlibInflateScratch::default)
4576 .decompress_prefix_into(
4577 &bytes[pos..],
4578 COMMIT_HEADER_READ_PREFIX,
4579 &mut self.commit_header_buf,
4580 )?;
4581 return Ok(!self.commit_header_buf.is_empty());
4582 }
4583 if let Some((base_kind, base_data)) =
4584 pack_cache::get_delta_base(self.pack_id, cur_offset)
4585 {
4586 if base_kind != ObjectKind::Commit {
4587 return Ok(false);
4588 }
4589 finish_pack_read_prefix_with_pending(
4590 &mut self.commit_header_buf,
4591 kind,
4592 base_data.as_ref(),
4593 None,
4594 result_pack_id,
4595 result_offset,
4596 true,
4597 state,
4598 )?;
4599 return Ok(commit_prefix_has_author(&self.commit_header_buf));
4600 }
4601 state.whole_object.clear();
4602 decompress_pack_data_into(
4603 bytes,
4604 &mut pos,
4605 size,
4606 &mut state.inflate,
4607 &mut state.whole_object,
4608 )?;
4609 let whole = std::mem::take(&mut state.whole_object);
4610 finish_pack_read_prefix_with_pending(
4611 &mut self.commit_header_buf,
4612 kind,
4613 &whole,
4614 Some((self.pack_id, cur_offset)),
4615 result_pack_id,
4616 result_offset,
4617 true,
4618 state,
4619 )?;
4620 return Ok(commit_prefix_has_author(&self.commit_header_buf));
4621 }
4622 PackedType::Tree | PackedType::Blob | PackedType::Tag => return Ok(false),
4623 PackedType::OfsDelta => {
4624 state.next_hop()?;
4625 let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
4626 state.delta_decompress.clear();
4627 decompress_pack_data_into(
4628 bytes,
4629 &mut pos,
4630 size,
4631 &mut state.inflate,
4632 &mut state.delta_decompress,
4633 )?;
4634 state.pending.push(PendingDeltaFrame {
4635 pack_id: self.pack_id,
4636 offset: object_start,
4637 delta: std::mem::take(&mut state.delta_decompress),
4638 });
4639 if let Some((base_kind, base_data)) =
4640 pack_cache::get_delta_base(self.pack_id, base_offset)
4641 {
4642 if base_kind != ObjectKind::Commit {
4643 return Ok(false);
4644 }
4645 finish_pack_read_prefix_with_pending(
4646 &mut self.commit_header_buf,
4647 base_kind,
4648 base_data.as_ref(),
4649 None,
4650 result_pack_id,
4651 result_offset,
4652 true,
4653 state,
4654 )?;
4655 return Ok(commit_prefix_has_author(&self.commit_header_buf));
4656 }
4657 cur_offset = base_offset;
4658 }
4659 PackedType::RefDelta => {
4660 state.next_hop()?;
4661 let hb = self.idx.hash_bytes();
4662 if pos + hb > bytes.len() {
4663 return Ok(false);
4664 }
4665 let base_raw = &bytes[pos..pos + hb];
4666 pos += hb;
4667 state.delta_decompress.clear();
4668 decompress_pack_data_into(
4669 bytes,
4670 &mut pos,
4671 size,
4672 &mut state.inflate,
4673 &mut state.delta_decompress,
4674 )?;
4675 state.pending.push(PendingDeltaFrame {
4676 pack_id: self.pack_id,
4677 offset: object_start,
4678 delta: std::mem::take(&mut state.delta_decompress),
4679 });
4680 let base_oid = ObjectId::from_bytes(base_raw)?;
4681 let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
4682 return Ok(false);
4683 };
4684 if let Some((base_kind, base_data)) =
4685 pack_cache::get_delta_base(self.pack_id, base_offset)
4686 {
4687 if base_kind != ObjectKind::Commit {
4688 return Ok(false);
4689 }
4690 finish_pack_read_prefix_with_pending(
4691 &mut self.commit_header_buf,
4692 base_kind,
4693 base_data.as_ref(),
4694 None,
4695 result_pack_id,
4696 result_offset,
4697 true,
4698 state,
4699 )?;
4700 return Ok(commit_prefix_has_author(&self.commit_header_buf));
4701 }
4702 cur_offset = base_offset;
4703 }
4704 }
4705 }
4706 }
4707
4708 pub(crate) fn clear_commit_header_prefix(&mut self) {
4710 self.commit_header_buf.clear();
4711 }
4712
4713 pub(crate) fn commit_header_prefix(&self) -> &[u8] {
4715 &self.commit_header_buf
4716 }
4717
4718 fn read_info_at_pack_offset(&mut self, start_offset: u64) -> Result<ObjectInfo> {
4719 if let Some(info) = self.info_cache.get(&start_offset) {
4720 return Ok(*info);
4721 }
4722 let pack_path = self.idx.pack_path.clone();
4723 let info = with_pack_read_delta_state(0, |state| {
4724 self.resolve_info_at_pack_offset_body(start_offset, state)
4725 })?;
4726 self.info_cache.insert(start_offset, info);
4727 remember_pack_read_info(&pack_path, start_offset, info);
4728 Ok(info)
4729 }
4730
4731 fn resolve_info_at_pack_offset_body(
4732 &mut self,
4733 start_offset: u64,
4734 state: &mut DeltaChainState,
4735 ) -> Result<ObjectInfo> {
4736 let pack_bytes = self.pack_bytes.as_ref();
4737 let mut cur_offset = start_offset;
4738 let mut result_size: Option<u64> = None;
4739 loop {
4740 state.visit(self.pack_id, cur_offset)?;
4741 if let Some(base) = self.info_cache.get(&cur_offset) {
4742 if let Some(size) = result_size {
4743 return Ok(ObjectInfo {
4744 kind: base.kind,
4745 size,
4746 });
4747 }
4748 return Ok(*base);
4749 }
4750
4751 let object_start = cur_offset;
4752 let mut pos = cur_offset as usize;
4753 let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos)?;
4754 match packed_type {
4755 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4756 let kind = packed_type_to_kind(packed_type)?;
4757 let undeltified = ObjectInfo {
4758 kind,
4759 size: header_size,
4760 };
4761 self.info_cache.insert(cur_offset, undeltified);
4762 let size = result_size.unwrap_or(header_size);
4763 return Ok(ObjectInfo { kind, size });
4764 }
4765 PackedType::OfsDelta => {
4766 state.next_hop()?;
4767 let base_offset = parse_ofs_delta_base(pack_bytes, &mut pos, object_start)?;
4768 if result_size.is_none() {
4769 result_size = Some(
4770 self.delta_scratch
4771 .get_or_insert_with(ZlibInflateScratch::default)
4772 .read_pack_delta_result_size(pack_bytes, &mut pos)?,
4773 );
4774 }
4775 cur_offset = base_offset;
4776 }
4777 PackedType::RefDelta => {
4778 state.next_hop()?;
4779 let hb = self.idx.hash_bytes();
4780 if pos + hb > pack_bytes.len() {
4781 return Err(Error::CorruptObject(
4782 "truncated ref-delta base OID".to_owned(),
4783 ));
4784 }
4785 let base_raw = &pack_bytes[pos..pos + hb];
4786 pos += hb;
4787 if result_size.is_none() {
4788 result_size = Some(
4789 self.delta_scratch
4790 .get_or_insert_with(ZlibInflateScratch::default)
4791 .read_pack_delta_result_size(pack_bytes, &mut pos)?,
4792 );
4793 }
4794 let base_oid = ObjectId::from_bytes(base_raw)?;
4795 let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
4796 return Err(Error::ObjectNotFound(base_oid.to_hex()));
4797 };
4798 cur_offset = base_offset;
4799 }
4800 }
4801 }
4802 }
4803
4804 pub fn read_info(&mut self, oid: &ObjectId) -> Result<ObjectInfo> {
4810 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4811 return Err(Error::ObjectNotFound(oid.to_hex()));
4812 };
4813 if let Some(info) =
4814 read_whole_pack_entry_info_fast(&self.idx, offset, self.pack_bytes.as_ref())
4815 {
4816 self.info_cache.insert(offset, info);
4817 return Ok(info);
4818 }
4819 self.read_info_at_pack_offset(offset)
4820 }
4821
4822 pub fn read_object(&mut self, oid: &ObjectId) -> Result<Object> {
4828 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4829 return Err(Error::ObjectNotFound(oid.to_hex()));
4830 };
4831 if !self.pack_bytes.uses_positioned_reads() {
4832 if let Some(obj) =
4833 read_whole_pack_entry_object_fast(&self.idx, offset, &self.pack_bytes)
4834 {
4835 return Ok(obj);
4836 }
4837 }
4838 read_object_from_pack_at_offset(&self.idx, offset)
4839 }
4840
4841 pub(crate) fn read_commit_through_subject(
4858 &mut self,
4859 oid: &ObjectId,
4860 ) -> Result<Option<Vec<u8>>> {
4861 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4862 return Ok(None);
4863 };
4864 let mut compressed = Vec::new();
4865 let Ok((pack_bytes, mut pos)) =
4866 pack_entry_bytes(&self.idx, offset, &self.pack_bytes, &mut compressed)
4867 else {
4868 return Ok(None);
4869 };
4870 let Ok((PackedType::Commit, size)) = parse_pack_object_header(pack_bytes, &mut pos) else {
4871 return Ok(None);
4872 };
4873 let Some(input) = pack_bytes.get(pos..) else {
4874 return Ok(None);
4875 };
4876 let mut out = Vec::new();
4877 let inflated = WHOLE_OBJECT_ZLIB.with(|slot| {
4878 slot.borrow_mut().decompress_prefix_until(
4879 input,
4880 size,
4881 COMMIT_SUBJECT_READ_STEP,
4882 &mut out,
4883 crate::objects::commit_through_subject_len,
4884 )
4885 });
4886 Ok(inflated.is_ok().then_some(out))
4887 }
4888
4889 pub(crate) fn read_object_unless_whole_larger(
4898 &mut self,
4899 oid: &ObjectId,
4900 limit: u64,
4901 ) -> Result<BoundedRead> {
4902 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4903 return Err(Error::ObjectNotFound(oid.to_hex()));
4904 };
4905 let mut pos = usize::try_from(offset)
4906 .map_err(|_| Error::CorruptObject(format!("pack offset {offset} overflows")))?;
4907 if let Ok((packed_type, size)) = parse_pack_object_header(&self.pack_bytes, &mut pos) {
4908 let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
4909 if whole && size > limit {
4910 return Ok(BoundedRead::Larger);
4911 }
4912 }
4913 if let Some(obj) =
4914 read_whole_pack_entry_object_fast(&self.idx, offset, self.pack_bytes.as_ref())
4915 {
4916 return Ok(BoundedRead::Object(obj));
4917 }
4918 read_object_from_pack_at_offset(&self.idx, offset).map(BoundedRead::Object)
4919 }
4920
4921 pub fn read_object_into(&mut self, oid: &ObjectId, out: &mut Vec<u8>) -> Result<ObjectKind> {
4925 self.read_object_into_with_delta_policy(oid, false, out)
4926 }
4927
4928 pub(crate) fn read_object_into_with_delta_policy(
4931 &mut self,
4932 oid: &ObjectId,
4933 populate_delta_lru: bool,
4934 out: &mut Vec<u8>,
4935 ) -> Result<ObjectKind> {
4936 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4937 return Err(Error::ObjectNotFound(oid.to_hex()));
4938 };
4939 if !self.pack_bytes.uses_positioned_reads() {
4940 if let Some(kind) =
4941 read_whole_pack_entry_object_into(&self.idx, offset, &self.pack_bytes, out)
4942 {
4943 return Ok(kind);
4944 }
4945 }
4946 read_object_at_depth_into(&self.idx, offset, 0, populate_delta_lru, out)
4947 }
4948
4949 pub fn pack_offset(&self, oid: &ObjectId) -> Result<Option<u64>> {
4955 self.idx.find_offset(oid)
4956 }
4957}
4958fn pack_indexes_for_lookup(
4959 objects_dir: &Path,
4960 opts: PackLookupOptions,
4961) -> Result<Vec<Arc<PackIndex>>> {
4962 let indexes = read_local_pack_indexes_cached(objects_dir)?;
4963 if !opts.skip_midx_covered_packs && !opts.only_midx_covered_packs {
4964 return Ok(indexes);
4965 }
4966 let covered = pack_cache::midx_covered_pack_names(objects_dir);
4967 if covered.is_empty() {
4968 return if opts.only_midx_covered_packs {
4969 Ok(Vec::new())
4970 } else {
4971 Ok(indexes)
4972 };
4973 }
4974 Ok(indexes
4975 .into_iter()
4976 .filter(|idx| {
4977 idx.idx_path
4978 .file_name()
4979 .and_then(|n| n.to_str())
4980 .map(|name| {
4981 if opts.only_midx_covered_packs {
4982 covered.contains(name)
4983 } else {
4984 !covered.contains(name)
4985 }
4986 })
4987 .unwrap_or(!opts.only_midx_covered_packs)
4988 })
4989 .collect())
4990}
4991
4992pub fn read_object_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
5004 if crate::pack_store::PackStore::has_context() {
5005 return read_object_from_packs_in_context(objects_dir, oid);
5006 }
5007 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
5008 crate::pack_store::PackStore::with_context(store, || {
5009 read_object_from_packs_in_context(objects_dir, oid)
5010 })
5011}
5012
5013pub fn read_object_from_packs_into(
5019 objects_dir: &Path,
5020 oid: &ObjectId,
5021 buf: &mut Vec<u8>,
5022) -> Result<ObjectKind> {
5023 if crate::pack_store::PackStore::has_context() {
5024 return read_object_from_packs_in_context_into(objects_dir, oid, buf);
5025 }
5026 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
5027 crate::pack_store::PackStore::with_context(store, || {
5028 read_object_from_packs_in_context_into(objects_dir, oid, buf)
5029 })
5030}
5031
5032pub fn read_object_info_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<ObjectInfo> {
5038 match try_read_object_info_from_packs_with_options(
5039 objects_dir,
5040 oid,
5041 PackLookupOptions::ALL_LOCAL_PACKS,
5042 ) {
5043 Ok(info) => Ok(info),
5044 Err(err @ Error::ObjectNotFound(_)) => {
5045 if reprepare_pack_directory_on_miss(objects_dir)? {
5046 try_read_object_info_from_packs_with_options(
5047 objects_dir,
5048 oid,
5049 PackLookupOptions::ALL_LOCAL_PACKS,
5050 )
5051 } else {
5052 Err(err)
5053 }
5054 }
5055 Err(err) => Err(err),
5056 }
5057}
5058
5059pub(crate) fn try_read_object_info_from_packs_with_options(
5061 objects_dir: &Path,
5062 oid: &ObjectId,
5063 opts: PackLookupOptions,
5064) -> Result<ObjectInfo> {
5065 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
5066 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
5067 }
5068 if opts.allows_pack_shortcuts() {
5069 if let Some(info) = pack_cache::try_read_info_from_hinted_pack(objects_dir, oid)? {
5070 return Ok(info);
5071 }
5072 }
5073 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
5074 let mut last_err: Option<Error> = None;
5075 for idx in &indexes {
5076 let Some(offset) = idx.find_offset(oid)? else {
5077 continue;
5078 };
5079 match read_object_info_from_pack_at_offset(idx, offset) {
5080 Ok(info) => {
5081 pack_cache::remember_pack_hit(objects_dir, idx);
5082 return Ok(info);
5083 }
5084 Err(Error::ObjectNotFound(_)) => {}
5085 Err(err) => last_err = Some(err),
5086 }
5087 }
5088 Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
5089}
5090
5091pub(crate) fn try_read_object_from_packs_with_options(
5093 objects_dir: &Path,
5094 oid: &ObjectId,
5095 opts: PackLookupOptions,
5096) -> Result<Object> {
5097 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
5098 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
5099 }
5100 if opts.allows_pack_shortcuts() {
5101 if let Some(obj) = pack_cache::try_read_object_via_single_pack_reader(objects_dir, oid)? {
5102 return Ok(obj);
5103 }
5104 if let Some(obj) = pack_cache::try_read_from_hinted_pack(objects_dir, oid)? {
5105 return Ok(obj);
5106 }
5107 }
5108 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
5109 let mut last_damage: Option<PackReadDamage> = None;
5110 let mut last_err: Option<Error> = None;
5111 for idx in &indexes {
5112 let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
5113 else {
5114 continue;
5115 };
5116 match read_object_from_pack_at_offset(idx, offset) {
5117 Ok(obj) => {
5118 pack_cache::remember_pack_hit(objects_dir, idx);
5119 return Ok(obj);
5120 }
5121 Err(Error::ObjectNotFound(_)) => {}
5124 Err(err) if err.is_object_read_wrap_source() => {
5127 last_damage = Some(PackReadDamage {
5128 pack: idx.pack_path.clone(),
5129 offset,
5130 err,
5131 });
5132 }
5133 Err(err) => last_err = Some(err),
5134 }
5135 }
5136 if let Some(err) = last_err {
5137 return Err(err);
5138 }
5139 Err(finish_pack_object_lookup(oid, last_damage))
5140}
5141
5142pub(crate) fn try_read_object_from_packs_with_options_into(
5147 objects_dir: &Path,
5148 oid: &ObjectId,
5149 opts: PackLookupOptions,
5150 populate_delta_lru: bool,
5151 buf: &mut Vec<u8>,
5152) -> Result<ObjectKind> {
5153 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
5154 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
5155 }
5156 if opts.allows_pack_shortcuts() {
5157 if let Some(kind) = pack_cache::try_read_object_via_single_pack_reader_into(
5158 objects_dir,
5159 oid,
5160 populate_delta_lru,
5161 buf,
5162 )? {
5163 return Ok(kind);
5164 }
5165 if let Some(kind) =
5166 pack_cache::try_read_from_hinted_pack_into(objects_dir, oid, populate_delta_lru, buf)?
5167 {
5168 return Ok(kind);
5169 }
5170 }
5171 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
5172 let mut last_damage: Option<PackReadDamage> = None;
5173 let mut last_err: Option<Error> = None;
5174 for idx in &indexes {
5175 let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
5176 else {
5177 continue;
5178 };
5179 match read_object_at_depth_into(idx, offset, 0, populate_delta_lru, buf) {
5180 Ok(kind) => {
5181 pack_cache::remember_pack_hit(objects_dir, idx);
5182 return Ok(kind);
5183 }
5184 Err(Error::ObjectNotFound(_)) => {}
5185 Err(err) if err.is_object_read_wrap_source() => {
5186 last_damage = Some(PackReadDamage {
5187 pack: idx.pack_path.clone(),
5188 offset,
5189 err,
5190 });
5191 }
5192 Err(err) => last_err = Some(err),
5193 }
5194 }
5195 if let Some(err) = last_err {
5196 return Err(err);
5197 }
5198 Err(finish_pack_object_lookup(oid, last_damage))
5199}
5200
5201#[must_use]
5203pub fn pack_index_is_v1(idx_path: &Path) -> bool {
5204 fs::read(idx_path)
5205 .ok()
5206 .is_some_and(|bytes| bytes.len() >= 4 && bytes[0..4] != [0xff, b't', b'O', b'c'])
5207}
5208
5209pub fn try_read_object_from_local_packs_cached(
5221 objects_dir: &Path,
5222 oid: &ObjectId,
5223 mut alternate_read: impl FnMut() -> Result<Object>,
5224) -> Result<Option<Object>> {
5225 let indexes = read_local_pack_indexes_cached(objects_dir)?;
5226 for idx in &indexes {
5227 if idx.find_offset(oid)?.is_none() {
5228 continue;
5229 }
5230 match read_object_from_pack(idx, oid) {
5231 Ok(obj) => return Ok(Some(obj)),
5232 Err(_) if pack_index_is_v1(&idx.idx_path) => {
5233 return Ok(Some(Object::new(ObjectKind::Blob, Vec::new())));
5234 }
5235 Err(_) => {
5236 if let Ok(obj) = alternate_read() {
5237 return Ok(Some(obj));
5238 }
5239 }
5240 }
5241 }
5242 Ok(None)
5243}
5244
5245fn parse_in_pack_delta_reuse_candidate(
5249 idx: &PackIndex,
5250 pack_bytes: &[u8],
5251 entry_offset: u64,
5252 in_pack: &HashSet<ObjectId>,
5253) -> Result<Option<(ObjectId, Vec<u8>)>> {
5254 let hb = idx.hash_bytes();
5255 let mut p = entry_offset as usize;
5256 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
5257 let base = match packed_type {
5258 PackedType::RefDelta => {
5259 if p + hb > pack_bytes.len() {
5260 return Err(Error::CorruptObject(
5261 "truncated ref-delta base oid while scanning for reuse".to_owned(),
5262 ));
5263 }
5264 let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
5265 p += hb;
5266 bo
5267 }
5268 PackedType::OfsDelta => {
5269 let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
5270 let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
5271 return Ok(None);
5272 };
5273 let base_oid_bytes = idx.oid_at(pos);
5274 if base_oid_bytes.len() != hb {
5275 return Ok(None);
5276 }
5277 ObjectId::from_bytes(base_oid_bytes)?
5278 }
5279 _ => return Ok(None),
5280 };
5281 if !in_pack.contains(&base) {
5282 return Ok(None);
5283 }
5284 let zlib_start = p;
5285 let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
5286 return Ok(None);
5287 };
5288 if zlib_start >= end_pos {
5289 return Ok(None);
5290 }
5291 let compressed = &pack_bytes[zlib_start..end_pos];
5292 let mut zpos = 0usize;
5293 let mut scratch = ZlibInflateScratch::default();
5294 let delta = match scratch.decompress_fixed(compressed, &mut zpos, size) {
5295 Ok(d) => d,
5296 Err(_) => return Ok(None),
5297 };
5298 Ok(Some((base, delta)))
5299}
5300
5301fn parse_in_pack_delta_reuse_zlib(
5304 idx: &PackIndex,
5305 pack_bytes: &[u8],
5306 entry_offset: u64,
5307 reusable_base: &dyn Fn(&ObjectId) -> bool,
5308) -> Result<Option<(ObjectId, u64, usize, usize)>> {
5309 let start = entry_offset as usize;
5310 let hb = idx.hash_bytes();
5311 let mut p = start;
5312 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
5313 let base = match packed_type {
5314 PackedType::RefDelta => {
5315 if p + hb > pack_bytes.len() {
5316 return Err(Error::CorruptObject(
5317 "truncated ref-delta base oid while scanning for reuse".to_owned(),
5318 ));
5319 }
5320 let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
5321 p += hb;
5322 bo
5323 }
5324 PackedType::OfsDelta => {
5325 let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
5326 let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
5327 return Ok(None);
5328 };
5329 let base_oid_bytes = idx.oid_at(pos);
5330 if base_oid_bytes.len() != hb {
5331 return Ok(None);
5332 }
5333 ObjectId::from_bytes(base_oid_bytes)?
5334 }
5335 _ => return Ok(None),
5336 };
5337 if !reusable_base(&base) {
5338 return Ok(None);
5339 }
5340 let zlib_start = p;
5341 let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
5342 return Ok(None);
5343 };
5344 if zlib_start >= end_pos {
5345 return Ok(None);
5346 }
5347 let entry_bytes = &pack_bytes[start..end_pos];
5348 if let Some(crc) = idx.crc32_for_pack_offset(entry_offset)? {
5349 if crc32fast::hash(entry_bytes) != crc {
5350 return Ok(None);
5351 }
5352 }
5353 Ok(Some((base, size, zlib_start, end_pos)))
5354}
5355
5356fn reuse_delta_matches_pack_resolution(
5357 idx: &PackIndex,
5358 target: &ObjectId,
5359 base: ObjectId,
5360 delta: &[u8],
5361) -> bool {
5362 let Ok(base_obj) = read_object_from_pack(idx, &base) else {
5363 return false;
5364 };
5365 let Ok(target_obj) = read_object_from_pack(idx, target) else {
5366 return false;
5367 };
5368 crate::unpack_objects::apply_delta(&base_obj.data, delta)
5369 .ok()
5370 .is_some_and(|applied| applied == target_obj.data)
5371}
5372
5373fn wanted_index_entries<'a>(
5378 idx: &PackIndex,
5379 wanted: impl ExactSizeIterator<Item = &'a ObjectId>,
5380 contains: impl Fn(&ObjectId) -> bool,
5381) -> Result<Vec<(ObjectId, u64)>> {
5382 let lookup_cost = wanted
5384 .len()
5385 .saturating_mul(usize::BITS.saturating_sub(idx.len().leading_zeros()) as usize);
5386 if lookup_cost < idx.len() {
5387 let mut out = Vec::new();
5388 for oid in wanted {
5389 if let Some(offset) = idx.find_offset(oid)? {
5390 out.push((*oid, offset));
5391 }
5392 }
5393 return Ok(out);
5394 }
5395 let mut out = Vec::new();
5396 for entry in idx.iter() {
5397 let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
5398 continue;
5399 };
5400 if contains(&oid) {
5401 out.push((oid, entry.offset()?));
5402 }
5403 }
5404 Ok(out)
5405}
5406
5407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5409pub(crate) struct PackedSlot {
5410 pub pack: u32,
5412 pub whole: bool,
5414}
5415
5416pub(crate) fn locate_packed_objects(
5426 objects_dir: &Path,
5427 wanted: &crate::objects::OidHashSet,
5428) -> Result<crate::objects::OidHashMap<PackedSlot>> {
5429 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
5430 sort_pack_indexes_oldest_first(&mut indexes);
5431 let mut slots = crate::objects::oid_hash_map_with_capacity(wanted.len());
5432 for (pack, idx) in (0u32..).zip(&indexes) {
5433 let bytes = read_pack_bytes_cached(&idx.pack_path)?;
5434 for (oid, offset) in wanted_index_entries(idx, wanted.iter(), |oid| wanted.contains(oid))? {
5435 if slots.contains_key(&oid) {
5436 continue;
5437 }
5438 let mut pos = usize::try_from(offset)
5439 .map_err(|_| Error::CorruptObject("pack offset exceeds memory".to_owned()))?;
5440 let (packed_type, _) = parse_pack_object_header(&bytes, &mut pos)?;
5441 let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
5442 slots.insert(oid, PackedSlot { pack, whole });
5443 }
5444 }
5445 Ok(slots)
5446}
5447
5448pub(crate) type OnDiskReuseZlibEntry = (ObjectId, u64, Arc<PackData>, usize, usize);
5451
5452pub(crate) fn collect_packed_delta_reuse_for_send(
5471 objects_dir: &Path,
5472 send: &HashSet<ObjectId>,
5473 reusable_base: &dyn Fn(&ObjectId) -> bool,
5474) -> Result<HashMap<ObjectId, OnDiskReuseZlibEntry>> {
5475 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
5476 sort_pack_indexes_oldest_first(&mut indexes);
5477
5478 let mut found: HashMap<ObjectId, OnDiskReuseZlibEntry> = HashMap::new();
5479 for idx in indexes.iter().filter(|idx| idx.hash_bytes() == 20) {
5480 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5481 for (oid, offset) in wanted_index_entries(idx, send.iter(), |oid| send.contains(oid))? {
5482 if found.contains_key(&oid) || !reusable_base(&oid) {
5483 continue;
5484 }
5485 let Some((base, size, zlib_start, zlib_end)) =
5486 parse_in_pack_delta_reuse_zlib(idx, &pack_bytes, offset, reusable_base)?
5487 else {
5488 continue;
5489 };
5490 found.insert(
5491 oid,
5492 (base, size, Arc::clone(&pack_bytes), zlib_start, zlib_end),
5493 );
5494 }
5495 }
5496 let crosses_packs = |(_, (base, _, pack, ..)): &(&ObjectId, &OnDiskReuseZlibEntry)| {
5499 found
5500 .get(base)
5501 .is_some_and(|(_, _, base_pack, ..)| !Arc::ptr_eq(base_pack, pack))
5502 };
5503 let starts = found.iter().filter(crosses_packs).map(|(t, _)| *t);
5504 for target in delta_cycle_breaks(starts, |oid| found.get(oid).map(|e| e.0)) {
5505 found.remove(&target);
5506 }
5507 Ok(found)
5508}
5509
5510pub fn packed_ref_delta_reuse_slice(
5519 objects_dir: &Path,
5520 oid: &ObjectId,
5521 packed_set: &HashSet<ObjectId>,
5522) -> Result<Option<(ObjectId, Vec<u8>)>> {
5523 for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Oldest)? {
5524 if idx.hash_bytes() != 20 {
5525 continue;
5526 }
5527 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5528 let Some((base, delta)) =
5529 parse_in_pack_delta_reuse_candidate(&idx, &pack_bytes, entry_offset, packed_set)?
5530 else {
5531 continue;
5532 };
5533 if reuse_delta_matches_pack_resolution(&idx, oid, base, &delta) {
5534 return Ok(Some((base, delta)));
5535 }
5536 }
5537 Ok(None)
5538}
5539
5540pub(crate) fn sort_pack_indexes_oldest_first(indexes: &mut [Arc<PackIndex>]) {
5545 if indexes.len() < 2 {
5546 return;
5547 }
5548 indexes.sort_by_cached_key(|idx| (pack_mtime(idx), idx.pack_path.clone()));
5549}
5550
5551fn pack_mtime(idx: &PackIndex) -> std::time::SystemTime {
5552 fs::metadata(&idx.pack_path)
5553 .and_then(|m| m.modified())
5554 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5555}
5556
5557#[derive(Clone, Copy)]
5559enum PackPreference {
5560 Oldest,
5562 Newest,
5564}
5565
5566fn packs_holding(
5572 objects_dir: &Path,
5573 oid: &ObjectId,
5574 preference: PackPreference,
5575) -> Result<Vec<(Arc<PackIndex>, u64)>> {
5576 let mut holding = Vec::new();
5577 for idx in read_local_pack_indexes_cached(objects_dir)? {
5578 if let Some(offset) = idx.find_offset(oid)? {
5579 holding.push((idx, offset));
5580 }
5581 }
5582 if holding.len() > 1 {
5583 let key = |idx: &PackIndex| (pack_mtime(idx), idx.pack_path.clone());
5584 match preference {
5585 PackPreference::Oldest => holding.sort_by_cached_key(|(idx, _)| key(idx)),
5586 PackPreference::Newest => {
5587 holding.sort_by_cached_key(|(idx, _)| std::cmp::Reverse(key(idx)));
5588 }
5589 }
5590 }
5591 Ok(holding)
5592}
5593
5594pub fn packed_delta_base_oid(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectId>> {
5596 for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
5597 if idx.hash_bytes() != 20 {
5598 continue;
5599 }
5600 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5601 let mut p = entry_offset as usize;
5602 let (packed_type, _) = parse_pack_object_header(&pack_bytes, &mut p)?;
5603 match packed_type {
5604 PackedType::RefDelta => {
5605 let hb = idx.hash_bytes();
5606 if p + hb > pack_bytes.len() {
5607 return Err(Error::CorruptObject("truncated ref-delta base".to_owned()));
5608 }
5609 return Ok(Some(ObjectId::from_bytes(&pack_bytes[p..p + hb])?));
5610 }
5611 PackedType::OfsDelta => {
5612 let base_off = parse_ofs_delta_base(&pack_bytes, &mut p, entry_offset)?;
5613 return Ok(idx
5614 .find_position_by_offset_sorted(base_off)?
5615 .and_then(|pos| ObjectId::from_bytes(idx.oid_at(pos)).ok()));
5616 }
5617 _ => continue,
5618 }
5619 }
5620 Ok(None)
5621}
5622
5623pub(crate) fn pack_entry_raw_end(
5627 idx: &PackIndex,
5628 pack_bytes: &[u8],
5629 entry_offset: u64,
5630) -> Result<Option<usize>> {
5631 let hb = idx.hash_bytes();
5632 let Some(pack_end) = pack_bytes.len().checked_sub(hb) else {
5633 return Ok(None);
5634 };
5635 let start = entry_offset as usize;
5636 if start >= pack_end {
5637 return Ok(None);
5638 }
5639 Ok(Some(
5640 idx.next_pack_offset_after(entry_offset, pack_end as u64)? as usize,
5641 ))
5642}
5643
5644pub(crate) struct PackedWholeObject {
5646 pub(crate) pack: Arc<PackData>,
5648 pub(crate) entry_offset: u64,
5650 pub(crate) entry_end: usize,
5652 pub(crate) zlib_offset: usize,
5654 pub(crate) data_end: usize,
5656 pub(crate) kind: ObjectKind,
5658 pub(crate) size: u64,
5660 pub(crate) index_crc: Option<u32>,
5662}
5663
5664pub(crate) fn count_local_packs_holding(objects_dir: &Path, oid: &ObjectId) -> Result<usize> {
5666 Ok(packs_holding(objects_dir, oid, PackPreference::Newest)?.len())
5667}
5668
5669pub(crate) enum PackedEntry {
5671 Whole(PackedWholeObject),
5673 NotWhole,
5676 Absent,
5678}
5679
5680pub(crate) fn locate_packed_whole_object(
5692 objects_dir: &Path,
5693 oid: &ObjectId,
5694 verify_crc: bool,
5695) -> Result<PackedEntry> {
5696 let indexes = read_local_pack_indexes_cached(objects_dir)?;
5697 let mut found = PackedEntry::Absent;
5698 for idx in &indexes {
5699 let Some(entry_offset) = idx.find_offset(oid)? else {
5700 continue;
5701 };
5702 found = PackedEntry::NotWhole;
5703 let pack = read_pack_bytes_cached(&idx.pack_path)?;
5704 let Some(data_end) = pack.len().checked_sub(idx.hash_bytes()) else {
5705 continue;
5706 };
5707 let Ok(mut pos) = usize::try_from(entry_offset) else {
5708 continue;
5709 };
5710 if pos >= data_end {
5711 continue;
5712 }
5713 let Ok((packed_type, size)) = parse_pack_object_header(&pack[..data_end], &mut pos) else {
5714 continue;
5715 };
5716 let Ok(kind) = packed_type_to_kind(packed_type) else {
5717 continue;
5718 };
5719 if verify_crc && !packed_entry_matches_index_crc(idx, &pack, entry_offset)? {
5720 continue;
5721 }
5722 let entry_end = pack_entry_raw_end(idx, &pack, entry_offset)?.unwrap_or(data_end);
5723 let index_crc = if verify_crc {
5724 None
5725 } else {
5726 idx.crc32_for_pack_offset(entry_offset).ok().flatten()
5727 };
5728 return Ok(PackedEntry::Whole(PackedWholeObject {
5729 pack,
5730 entry_offset,
5731 entry_end,
5732 zlib_offset: pos,
5733 data_end,
5734 kind,
5735 size,
5736 index_crc,
5737 }));
5738 }
5739 Ok(found)
5740}
5741
5742pub(crate) fn packed_entry_matches_index_crc(
5751 idx: &PackIndex,
5752 pack: &PackData,
5753 entry_offset: u64,
5754) -> Result<bool> {
5755 let Some(recorded) = idx.crc32_for_pack_offset(entry_offset)? else {
5756 return Ok(true);
5757 };
5758 let Some(end) = pack_entry_raw_end(idx, pack, entry_offset)? else {
5759 return Ok(false);
5760 };
5761 let start = usize::try_from(entry_offset)
5762 .map_err(|_| Error::CorruptObject(format!("pack offset {entry_offset} overflows")))?;
5763 if start >= end {
5764 return Ok(false);
5765 }
5766 let budget = ResidentBudget::new(pack, VERIFY_RESIDENT_PACK_BYTES);
5767 Ok(budget.crc32(&pack[start..end]) == recorded)
5768}
5769
5770const VERIFY_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
5773
5774pub fn packed_full_object_slice(objects_dir: &Path, oid: &ObjectId) -> Result<Option<Vec<u8>>> {
5793 Ok(packed_full_object_range(objects_dir, oid)?
5794 .map(|(bytes, start, end)| bytes[start..end].to_vec()))
5795}
5796
5797pub(crate) fn packed_full_object_range(
5800 objects_dir: &Path,
5801 oid: &ObjectId,
5802) -> Result<Option<(Arc<PackData>, usize, usize)>> {
5803 for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
5804 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5805 let start = entry_offset as usize;
5806 let mut p = start;
5807 let Ok((packed_type, _size)) = parse_pack_object_header(&pack_bytes, &mut p) else {
5808 continue;
5809 };
5810 if matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta) {
5811 continue;
5813 }
5814 let Some(end) = pack_entry_raw_end(&idx, &pack_bytes, entry_offset)? else {
5815 continue;
5816 };
5817 if end <= start {
5818 continue;
5819 }
5820 if !packed_entry_matches_index_crc(&idx, &pack_bytes, entry_offset)? {
5824 continue;
5825 }
5826 return Ok(Some((pack_bytes, start, end)));
5827 }
5828 Ok(None)
5829}
5830
5831pub(crate) fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
5832 let first = *bytes.get(*pos).ok_or_else(|| {
5833 Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
5834 })?;
5835 *pos += 1;
5836
5837 let type_code = (first >> 4) & 0x7;
5838 let mut size = (first & 0x0f) as u64;
5839 let mut shift = 4u32;
5840 let mut c = first;
5841 while (c & 0x80) != 0 {
5842 c = *bytes.get(*pos).ok_or_else(|| {
5843 Error::CorruptObject("unexpected end of variable size header".to_owned())
5844 })?;
5845 *pos += 1;
5846 size |= ((c & 0x7f) as u64) << shift;
5847 shift += 7;
5848 }
5849
5850 let packed_type = match type_code {
5851 1 => PackedType::Commit,
5852 2 => PackedType::Tree,
5853 3 => PackedType::Blob,
5854 4 => PackedType::Tag,
5855 6 => PackedType::OfsDelta,
5856 7 => PackedType::RefDelta,
5857 _ => {
5858 return Err(Error::CorruptObject(format!(
5859 "unsupported packed object type {}",
5860 type_code
5861 )))
5862 }
5863 };
5864 Ok((packed_type, size))
5865}
5866
5867#[derive(Debug, Clone, Copy)]
5869pub enum PackedDeltaDependency {
5870 OfsBase {
5872 base_offset: u64,
5874 },
5875 RefBase {
5877 base_oid: ObjectId,
5879 },
5880}
5881
5882pub fn read_packed_delta_dependency(
5886 pack_bytes: &[u8],
5887 object_offset: u64,
5888 hash_bytes: usize,
5889) -> Result<Option<PackedDeltaDependency>> {
5890 let mut pos = object_offset as usize;
5891 let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
5892 match ty {
5893 PackedType::OfsDelta => {
5894 let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
5895 Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
5896 }
5897 PackedType::RefDelta => {
5898 if pos + hash_bytes > pack_bytes.len() {
5899 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5900 }
5901 let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + hash_bytes])?;
5902 Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
5903 }
5904 _ => Ok(None),
5905 }
5906}
5907
5908pub(crate) fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
5909 let mut c = *bytes
5910 .get(*pos)
5911 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5912 *pos += 1;
5913 let mut value = (c & 0x7f) as u64;
5914 while (c & 0x80) != 0 {
5915 c = *bytes
5916 .get(*pos)
5917 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5918 *pos += 1;
5919 value = ((value + 1) << 7) | (c & 0x7f) as u64;
5920 }
5921 this_offset
5922 .checked_sub(value)
5923 .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
5924}
5925
5926pub fn slice_one_pack_object(
5934 bytes: &[u8],
5935 object_start_offset: u64,
5936 hash_bytes: usize,
5937) -> Result<&[u8]> {
5938 let start = object_start_offset as usize;
5939 let mut pos = start;
5940 skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
5941 Ok(&bytes[start..pos])
5942}
5943
5944pub fn skip_one_pack_object(
5946 bytes: &[u8],
5947 pos: &mut usize,
5948 object_start_offset: u64,
5949 hash_bytes: usize,
5950) -> Result<()> {
5951 let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
5952 let mut scratch = ZlibInflateScratch::default();
5953 match packed_type {
5954 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
5955 scratch.skip_zlib_stream(bytes, pos, size)?;
5956 }
5957 PackedType::RefDelta => {
5958 if *pos + hash_bytes > bytes.len() {
5959 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5960 }
5961 *pos += hash_bytes;
5962 scratch.skip_zlib_stream(bytes, pos, size)?;
5963 }
5964 PackedType::OfsDelta => {
5965 let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
5966 scratch.skip_zlib_stream(bytes, pos, size)?;
5967 }
5968 }
5969 Ok(())
5970}
5971
5972pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
5974 let index = read_pack_index(idx_path)?;
5975 let mut out = Vec::new();
5976 let hash_bytes = index.hash_bytes();
5977 for e in index.iter() {
5978 if e.oid().len() == hash_bytes {
5979 out.push(ObjectId::from_bytes(e.oid())?);
5980 }
5981 }
5982 Ok(out)
5983}
5984
5985mod pack_cache_test_sync {
5986 use std::cell::Cell;
5987 use std::sync::Mutex;
5988 use std::sync::MutexGuard;
5989
5990 struct PackCacheTestCoordinator {
5992 top_level_guards: u32,
5993 }
5994
5995 static COORD: Mutex<PackCacheTestCoordinator> = Mutex::new(PackCacheTestCoordinator {
5997 top_level_guards: 0,
5998 });
5999
6000 thread_local! {
6002 static DEPTH: Cell<u32> = const { Cell::new(0) };
6003 }
6004
6005 pub struct PackCacheTestGuard {
6007 _inner: Option<MutexGuard<'static, PackCacheTestCoordinator>>,
6008 }
6009
6010 pub fn acquire() -> PackCacheTestGuard {
6011 let depth = DEPTH.with(|d| {
6012 let n = d.get();
6013 d.set(n + 1);
6014 n
6015 });
6016 PackCacheTestGuard {
6017 _inner: if depth == 0 {
6018 let mut g = COORD.lock().unwrap_or_else(|e| e.into_inner());
6019 assert_eq!(
6020 g.top_level_guards, 0,
6021 "pack cache test coordinator already held"
6022 );
6023 g.top_level_guards = 1;
6024 Some(g)
6025 } else {
6026 None
6027 },
6028 }
6029 }
6030
6031 impl Drop for PackCacheTestGuard {
6032 fn drop(&mut self) {
6033 DEPTH.with(|d| {
6034 let n = d.get();
6035 assert!(n > 0, "pack cache test guard depth underflow");
6036 d.set(n - 1);
6037 if n == 1 {
6038 if let Some(mut g) = self._inner.take() {
6039 assert_eq!(g.top_level_guards, 1);
6040 g.top_level_guards = 0;
6041 }
6042 }
6043 });
6044 }
6045 }
6046}
6047
6048#[cfg(test)]
6049pub(crate) fn pack_cache_test_guard() -> pack_cache_test_sync::PackCacheTestGuard {
6050 pack_cache_test_sync::acquire()
6051}
6052
6053pub fn test_pack_cache_guard() -> pack_cache_test_sync::PackCacheTestGuard {
6055 pack_cache_test_sync::acquire()
6056}
6057
6058#[cfg(test)]
6059mod tests {
6060 use super::*;
6061 use crate::delta_encode::encode_lcp_delta;
6062 use crate::odb::Odb;
6063 use crate::pack_map::PackData;
6064 use crate::transfer::{build_pack, PackBuildOptions};
6065 use flate2::write::ZlibEncoder;
6066 use flate2::Compression;
6067 use std::io::Write;
6068 use std::process::Command;
6069
6070 fn git_try(dir: &std::path::Path, args: &[&str]) -> bool {
6071 Command::new("git") .current_dir(dir)
6073 .args(args)
6074 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6075 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6076 .output()
6077 .map(|o| o.status.success())
6078 .unwrap_or(false)
6079 }
6080
6081 fn git(dir: &std::path::Path, args: &[&str]) {
6082 let out = Command::new("git") .current_dir(dir)
6084 .args(args)
6085 .env("GIT_AUTHOR_NAME", "T")
6086 .env("GIT_AUTHOR_EMAIL", "t@example.com")
6087 .env("GIT_COMMITTER_NAME", "T")
6088 .env("GIT_COMMITTER_EMAIL", "t@example.com")
6089 .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
6090 .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
6091 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6092 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6093 .output()
6094 .expect("git");
6095 assert!(
6096 out.status.success(),
6097 "git {:?}: {}",
6098 args,
6099 String::from_utf8_lossy(&out.stderr)
6100 );
6101 }
6102
6103 fn append_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
6104 let mut size = payload_len;
6105 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
6106 size >>= 4;
6107 if size > 0 {
6108 buf.push(first | 0x80);
6109 while size > 0 {
6110 let b = (size & 0x7f) as u8;
6111 size >>= 7;
6112 buf.push(if size > 0 { b | 0x80 } else { b });
6113 }
6114 } else {
6115 buf.push(first);
6116 }
6117 }
6118
6119 fn zlib_pack(payload: &[u8]) -> Vec<u8> {
6120 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
6121 enc.write_all(payload).unwrap();
6122 enc.finish().unwrap()
6123 }
6124
6125 fn append_sha1_pack_trailer(buf: &mut Vec<u8>) {
6126 buf.extend_from_slice(HashAlgo::Sha1.checksum_digest(&*buf).as_slice());
6127 }
6128
6129 fn append_ref_delta(buf: &mut Vec<u8>, base_oid: &ObjectId, delta: &[u8]) {
6130 let compressed = zlib_pack(delta);
6131 append_pack_object_header(buf, 7, delta.len());
6132 buf.extend_from_slice(base_oid.as_bytes());
6133 buf.extend_from_slice(&compressed);
6134 }
6135
6136 fn append_ofs_delta(buf: &mut Vec<u8>, base_offset: u64, delta: &[u8]) -> u64 {
6137 let object_start = buf.len() as u64;
6138 let compressed = zlib_pack(delta);
6139 append_pack_object_header(buf, 6, delta.len());
6140 let mut ofs = object_start - base_offset;
6141 while ofs >= 0x80 {
6142 buf.push((ofs as u8 & 0x7f) | 0x80);
6143 ofs >>= 7;
6144 }
6145 buf.push(ofs as u8);
6146 buf.extend_from_slice(&compressed);
6147 object_start
6148 }
6149
6150 fn append_whole_blob(buf: &mut Vec<u8>, data: &[u8]) -> u64 {
6151 let off = buf.len() as u64;
6152 let compressed = zlib_pack(data);
6153 append_pack_object_header(buf, 3, data.len());
6154 buf.extend_from_slice(&compressed);
6155 off
6156 }
6157
6158 fn append_corrupt_whole_blob(buf: &mut Vec<u8>) -> u64 {
6159 let off = buf.len() as u64;
6160 append_pack_object_header(buf, 3, 8);
6161 buf.extend_from_slice(&[0xff, 0xfe, 0xfd]);
6162 off
6163 }
6164
6165 fn write_v2_idx_for_test(
6166 idx_path: &Path,
6167 pack_path: &Path,
6168 entries: &[(ObjectId, u64)],
6169 ) -> Result<()> {
6170 let pack_bytes = std::fs::read(pack_path)?;
6171 let mut with_crc = Vec::with_capacity(entries.len());
6172 for (oid, off) in entries {
6173 let start = *off as usize;
6174 let mut end = start;
6175 skip_one_pack_object(&pack_bytes, &mut end, *off, 20)
6176 .map_err(|e| Error::CorruptObject(format!("idx crc walk: {e}")))?;
6177 with_crc.push((*oid, *off, crc32fast::hash(&pack_bytes[start..end])));
6178 }
6179 write_v2_pack_index(idx_path, pack_path, &with_crc, 20)
6180 }
6181
6182 fn single_blob_pack(data: &[u8]) -> (Vec<u8>, ObjectId, u64) {
6183 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6184 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
6185 let mut pack = Vec::new();
6186 pack.extend_from_slice(b"PACK");
6187 pack.extend_from_slice(&2u32.to_be_bytes());
6188 pack.extend_from_slice(&1u32.to_be_bytes());
6189 let off = append_whole_blob(&mut pack, data);
6190 append_sha1_pack_trailer(&mut pack);
6191 (pack, oid, off)
6192 }
6193
6194 #[test]
6195 fn single_local_pack_reader_batch_check_caches_deltified_entries() {
6196 let _guard = pack_cache_test_guard();
6197 clear_pack_cache();
6198 let Some((tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(2) else {
6199 return;
6200 };
6201 let objects = tmp.path().join("objects");
6202 let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
6203 crate::pack_store::PackStore::with_context(store, || {
6204 let mut reader = SingleLocalPackReader::open(&objects)
6205 .expect("open")
6206 .expect("single pack");
6207 let info1 = reader.read_info(&tip).expect("first read");
6208 let info2 = reader.read_info(&tip).expect("cached read");
6209 assert_eq!(info1, info2);
6210 let full = read_object_from_pack(&idx, &tip).expect("full read");
6211 assert_eq!(info1.kind, full.kind);
6212 assert_eq!(info1.size, full.data.len() as u64);
6213 });
6214 let _ = odb;
6215 }
6216
6217 #[test]
6218 fn pack_read_info_cache_is_scoped_per_repository_and_pack_bytes() {
6219 let _guard = pack_cache_test_guard();
6220 clear_pack_cache();
6221 const ENTRY_OFFSET: u64 = 200;
6222 let dir_a = tempfile::tempdir().expect("tempdir a");
6223 let dir_b = tempfile::tempdir().expect("tempdir b");
6224 let objects_a = dir_a.path().join("objects");
6225 let objects_b = dir_b.path().join("objects");
6226 fs::create_dir_all(objects_a.join("pack")).expect("pack dir a");
6227 fs::create_dir_all(objects_b.join("pack")).expect("pack dir b");
6228 let pack_a = objects_a.join("pack/p-a.pack");
6229 let pack_b = objects_b.join("pack/p-b.pack");
6230 let (oid_a, _) =
6231 write_sparse_single_blob_pack(dir_a.path(), &pack_a, b"five!", ENTRY_OFFSET)
6232 .expect("pack a");
6233 let (oid_b, _) =
6234 write_sparse_single_blob_pack(dir_b.path(), &pack_b, b"1234567890", ENTRY_OFFSET)
6235 .expect("pack b");
6236 let idx_a = pack_a.with_extension("idx");
6237 let idx_b = pack_b.with_extension("idx");
6238 write_v2_idx_for_test(&idx_a, &pack_a, &[(oid_a, ENTRY_OFFSET)]).expect("idx a");
6239 write_v2_idx_for_test(&idx_b, &pack_b, &[(oid_b, ENTRY_OFFSET)]).expect("idx b");
6240 let idx_a = read_pack_index(&idx_a).expect("parse idx a");
6241 let idx_b = read_pack_index(&idx_b).expect("parse idx b");
6242
6243 let read_size = |objects: &Path, idx: &PackIndex| -> u64 {
6244 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects);
6245 crate::pack_store::PackStore::with_context(store, || {
6246 super::read_object_info_from_pack_at_offset(idx, ENTRY_OFFSET)
6247 .expect("read_info")
6248 .size
6249 })
6250 };
6251
6252 let size_a = read_size(&objects_a, &idx_a);
6253 let size_b = read_size(&objects_b, &idx_b);
6254 assert_eq!(size_a, 5);
6255 assert_eq!(size_b, 10);
6256 assert_eq!(read_size(&objects_a, &idx_a), 5);
6258 assert_eq!(read_size(&objects_b, &idx_b), 10);
6259 assert_eq!(read_size(&objects_a, &idx_a), 5);
6260 }
6261 fn write_sparse_single_blob_pack(
6264 repo_root: &Path,
6265 pack_path: &Path,
6266 data: &[u8],
6267 object_offset: u64,
6268 ) -> Result<(ObjectId, u64)> {
6269 use std::fs::OpenOptions;
6270 use std::io::{Seek, SeekFrom, Write};
6271
6272 let odb = Odb::new(repo_root);
6273 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
6274 let mut file = OpenOptions::new()
6275 .create(true)
6276 .write(true)
6277 .read(true)
6278 .truncate(true)
6279 .open(pack_path)
6280 .map_err(Error::Io)?;
6281 file.write_all(b"PACK").map_err(Error::Io)?;
6282 file.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
6283 file.write_all(&1u32.to_be_bytes()).map_err(Error::Io)?;
6284 file.seek(SeekFrom::Start(object_offset))
6285 .map_err(Error::Io)?;
6286 let mut tail = Vec::new();
6287 append_whole_blob(&mut tail, data);
6288 file.write_all(&tail).map_err(Error::Io)?;
6289 let trailer = sha1_trailer_for_open_file(&mut file)?;
6290 file.write_all(trailer.as_bytes()).map_err(Error::Io)?;
6291 file.flush().map_err(Error::Io)?;
6292 Ok((oid, object_offset))
6293 }
6294
6295 #[test]
6296 fn skip_one_pack_object_rejects_zero_size_with_zlib_payload() {
6297 let mut bytes = Vec::new();
6298 bytes.push(0x30);
6299 bytes.extend_from_slice(&zlib_pack(b"x"));
6300 let mut pos = 0usize;
6301 let err = skip_one_pack_object(&bytes, &mut pos, 0, 20).unwrap_err();
6302 assert!(matches!(err, Error::CorruptObject(_)), "got {err:?}");
6303 }
6304
6305 fn install_synthetic_pack(
6306 objects_dir: &Path,
6307 stem: &str,
6308 pack: &[u8],
6309 entries: &[(ObjectId, u64)],
6310 ) -> PackIndex {
6311 install_synthetic_pack_inner(objects_dir, stem, pack, entries, true)
6312 }
6313
6314 fn install_synthetic_pack_no_clear(
6315 objects_dir: &Path,
6316 stem: &str,
6317 pack: &[u8],
6318 entries: &[(ObjectId, u64)],
6319 ) -> PackIndex {
6320 install_synthetic_pack_inner(objects_dir, stem, pack, entries, false)
6321 }
6322
6323 fn install_synthetic_pack_inner(
6324 objects_dir: &Path,
6325 stem: &str,
6326 pack: &[u8],
6327 entries: &[(ObjectId, u64)],
6328 clear_cache: bool,
6329 ) -> PackIndex {
6330 let pack_dir = objects_dir.join("pack");
6331 std::fs::create_dir_all(&pack_dir).expect("pack dir");
6332 let pack_path = pack_dir.join(format!("{stem}.pack"));
6333 let idx_path = pack_dir.join(format!("{stem}.idx"));
6334 std::fs::write(&pack_path, pack).expect("write pack");
6335 write_v2_idx_for_test(&idx_path, &pack_path, entries).expect("write idx");
6336 if clear_cache {
6337 clear_pack_cache();
6338 }
6339 read_pack_index(&idx_path).expect("read idx")
6340 }
6341
6342 fn pack_index_for_objects(
6343 pack_path: std::path::PathBuf,
6344 objects: &[(ObjectId, u64)],
6345 ) -> PackIndex {
6346 let idx_path = pack_path.with_extension("idx");
6347 let pack_bytes = fs::read(&pack_path).expect("read pack for idx trailer");
6348 let trailer = &pack_bytes[pack_bytes.len().saturating_sub(20)..];
6349 let rows: Vec<(ObjectId, u64, u32)> = objects
6350 .iter()
6351 .map(|(oid, offset)| (*oid, *offset, 0))
6352 .collect();
6353 write_v2_pack_index_with_trailer(&idx_path, &rows, trailer, 20, None).expect("write idx");
6354 read_pack_index(&idx_path).expect("read idx")
6355 }
6356
6357 fn index_pack_in_scratch(pack: &[u8]) -> (tempfile::TempDir, PackIndex) {
6358 let dir = tempfile::tempdir().expect("tempdir");
6359 let pack_path = dir.path().join("chain.pack");
6360 std::fs::write(&pack_path, pack).expect("write pack");
6361 let out = Command::new("git") .current_dir(dir.path())
6363 .args(["index-pack", "chain.pack"])
6364 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6365 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6366 .output()
6367 .expect("index-pack");
6368 assert!(
6369 out.status.success(),
6370 "git index-pack failed: {}",
6371 String::from_utf8_lossy(&out.stderr)
6372 );
6373 let idx_path = dir.path().join("chain.idx");
6374 let idx = read_pack_index(&idx_path).expect("read idx");
6375 (dir, idx)
6376 }
6377
6378 fn build_deep_delta_repo(commit_count: usize) -> Option<(tempfile::TempDir, ObjectId, Odb)> {
6380 let tmp = tempfile::tempdir().ok()?;
6381 let dir = tmp.path();
6382 if !git_try(dir, &["init", "-q", "-b", "main", "."]) {
6383 return None;
6384 }
6385 let mut body = String::new();
6386 for i in 0..200 {
6387 body.push_str(&format!("seed line {i:04}\n"));
6388 }
6389 for rev in 0..commit_count {
6390 body.push_str(&format!("edit-{rev}\n"));
6391 std::fs::write(dir.join("blob.txt"), body.as_bytes()).ok()?;
6392 git(dir, &["add", "blob.txt"]);
6393 git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
6394 }
6395 let tip_hex = Command::new("git") .current_dir(dir)
6397 .args(["rev-parse", "HEAD"])
6398 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6399 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6400 .output()
6401 .ok()?;
6402 if !tip_hex.status.success() {
6403 return None;
6404 }
6405 let tip = ObjectId::from_hex(std::str::from_utf8(&tip_hex.stdout).ok()?.trim()).ok()?;
6406 let git_dir = dir.join(".git");
6407 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
6408 Some((tmp, tip, odb))
6409 }
6410
6411 #[test]
6412 fn verify_packed_object_hash_sha256_detects_corruption() {
6413 use crate::hash::hash_object;
6414 use std::fs;
6415
6416 let data = b"sha256 packed blob for hash verify\n";
6417 let oid = hash_object(HashAlgo::Sha256, ObjectKind::Blob, data).expect("hash");
6418 let wrong_oid =
6419 hash_object(HashAlgo::Sha256, ObjectKind::Blob, b"different payload\n").expect("hash");
6420 let mut pack = Vec::new();
6421 pack.extend_from_slice(b"PACK");
6422 pack.extend_from_slice(&2u32.to_be_bytes());
6423 pack.extend_from_slice(&1u32.to_be_bytes());
6424 let off = append_whole_blob(&mut pack, data);
6425 pack.extend_from_slice(HashAlgo::Sha256.checksum_digest(&pack).as_slice());
6426
6427 let dir = tempfile::tempdir().expect("tempdir");
6428 let pack_path = dir.path().join("sha256-corrupt.pack");
6429 fs::write(&pack_path, &pack).expect("write pack");
6430
6431 let idx_path = pack_path.with_extension("idx");
6432 let trailer = &pack[pack.len() - 32..];
6433 write_v2_pack_index_with_trailer(&idx_path, &[(wrong_oid, off, 0)], trailer, 32, None)
6434 .expect("write idx");
6435 let idx = read_pack_index(&idx_path).expect("read idx");
6436
6437 let err = read_object_from_pack_bytes(&pack, &idx, wrong_oid.as_bytes()).unwrap_err();
6438 match err {
6439 Error::CorruptObject(msg) => {
6440 assert!(
6441 msg.contains("hashes to"),
6442 "expected hash mismatch message, got: {msg}"
6443 );
6444 assert!(
6445 msg.contains(&oid.to_hex()),
6446 "expected recomputed oid in message, got: {msg}"
6447 );
6448 }
6449 other => panic!("expected CorruptObject, got {other:?}"),
6450 }
6451 }
6452
6453 #[cfg_attr(
6454 not(feature = "full-tests"),
6455 ignore = "starts system git or is heavy; run with --features full-tests"
6456 )]
6457 #[test]
6458 fn reads_delta_chain_deeper_than_50() {
6459 let _guard = pack_cache_test_guard();
6460 let Some((_repo, tip, odb)) = build_deep_delta_repo(120) else {
6461 let _ = "SKIP: git unavailable for deep delta fixture";
6462 return;
6463 };
6464 let pack = build_pack(
6465 &odb,
6466 &[tip],
6467 &[],
6468 &PackBuildOptions {
6469 delta: true,
6470 max_depth: 120,
6471 use_ofs_delta: true,
6472 ..PackBuildOptions::default()
6473 },
6474 )
6475 .expect("build deep delta pack");
6476
6477 let (_scratch, idx) = index_pack_in_scratch(&pack);
6478 assert!(
6479 idx.len() > 1,
6480 "expected a multi-object pack, got {}",
6481 idx.len()
6482 );
6483
6484 for entry in idx.iter() {
6485 let oid = ObjectId::from_bytes(entry.oid()).expect("oid in idx");
6486 let from_pack = read_object_from_pack(&idx, &oid).expect("read packed object");
6487 let expected = odb.read(&oid).expect("loose/alt copy for oid");
6488 assert_eq!(from_pack.kind, expected.kind);
6489 assert_eq!(from_pack.data, expected.data);
6490 assert_eq!(
6491 odb.hash(from_pack.kind, &from_pack.data).expect("hash"),
6492 oid
6493 );
6494 }
6495 }
6496
6497 #[test]
6498 fn cyclic_ref_delta_chain_errors() {
6499 let _guard = pack_cache_test_guard();
6500 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6501 let content_a = b"aaa".as_slice();
6502 let content_b = b"bbb".as_slice();
6503 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6504 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6505 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6506 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6507
6508 let mut pack = Vec::new();
6509 pack.extend_from_slice(b"PACK");
6510 pack.extend_from_slice(&2u32.to_be_bytes());
6511 pack.extend_from_slice(&2u32.to_be_bytes());
6512 let off_a = pack.len() as u64;
6513 append_ref_delta(&mut pack, &oid_b, &delta_a);
6514 let off_b = pack.len() as u64;
6515 append_ref_delta(&mut pack, &oid_a, &delta_b);
6516 append_sha1_pack_trailer(&mut pack);
6517
6518 let dir = tempfile::tempdir().expect("tempdir");
6519 let pack_path = dir.path().join("cycle.pack");
6520 std::fs::write(&pack_path, &pack).expect("write cyclic pack");
6521 let idx = pack_index_for_objects(pack_path, &[(oid_a, off_a), (oid_b, off_b)]);
6522 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
6523 for err in [
6524 read_object_from_pack(&idx, &oid_a).expect_err("read_object_from_pack"),
6525 read_object_from_pack_bytes(&pack_bytes, &idx, oid_a.as_bytes())
6526 .expect_err("read_object_from_pack_bytes"),
6527 ] {
6528 assert!(
6529 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6530 "expected typed delta chain limit, got {err:?}"
6531 );
6532 }
6533 }
6534
6535 #[test]
6536 fn resolve_cross_pack_delta_without_index_clone() {
6537 let _guard = pack_cache_test_guard();
6538 clear_pack_cache();
6539 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6540 let base = b"cross-pack-base-payload".as_slice();
6541 let tip = b"cross-pack-base-payload!".as_slice();
6542 let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
6543 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6544 let delta = encode_lcp_delta(base, tip).expect("delta");
6545
6546 let mut pack_base = Vec::new();
6547 pack_base.extend_from_slice(b"PACK");
6548 pack_base.extend_from_slice(&2u32.to_be_bytes());
6549 pack_base.extend_from_slice(&1u32.to_be_bytes());
6550 let off_base = append_whole_blob(&mut pack_base, base);
6551 append_sha1_pack_trailer(&mut pack_base);
6552
6553 let mut pack_delta = Vec::new();
6554 pack_delta.extend_from_slice(b"PACK");
6555 pack_delta.extend_from_slice(&2u32.to_be_bytes());
6556 pack_delta.extend_from_slice(&1u32.to_be_bytes());
6557 let off_tip = pack_delta.len() as u64;
6558 append_ref_delta(&mut pack_delta, &oid_base, &delta);
6559 append_sha1_pack_trailer(&mut pack_delta);
6560
6561 let dir = tempfile::tempdir().expect("tempdir");
6562 let objects = dir.path().join("objects");
6563 let _idx_base =
6564 install_synthetic_pack(&objects, "cross-base", &pack_base, &[(oid_base, off_base)]);
6565 let idx_delta =
6566 install_synthetic_pack(&objects, "cross-delta", &pack_delta, &[(oid_tip, off_tip)]);
6567
6568 let from_delta_pack = read_object_from_pack(&idx_delta, &oid_tip).expect("delta pack read");
6569 assert_eq!(from_delta_pack.kind, ObjectKind::Blob);
6570 assert_eq!(from_delta_pack.data, tip);
6571 assert_eq!(
6572 odb.hash(from_delta_pack.kind, &from_delta_pack.data)
6573 .expect("hash"),
6574 oid_tip
6575 );
6576
6577 let from_packs = read_object_from_packs(&objects, &oid_tip).expect("packs dir read");
6578 assert_eq!(from_packs.kind, ObjectKind::Blob);
6579 assert_eq!(from_packs.data, tip);
6580 assert_eq!(
6581 odb.hash(from_packs.kind, &from_packs.data).expect("hash"),
6582 oid_tip
6583 );
6584 }
6585
6586 #[cfg_attr(
6587 not(feature = "full-tests"),
6588 ignore = "starts system git or is heavy; run with --features full-tests"
6589 )]
6590 #[test]
6591 fn read_object_from_pack_bytes_uses_in_memory_index_without_on_disk_idx() {
6592 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6593 let content = b"in-memory-index-blob";
6594 let oid = odb.hash(ObjectKind::Blob, content).expect("hash");
6595
6596 let mut pack = Vec::new();
6597 pack.extend_from_slice(b"PACK");
6598 pack.extend_from_slice(&2u32.to_be_bytes());
6599 pack.extend_from_slice(&1u32.to_be_bytes());
6600 let off = append_whole_blob(&mut pack, content);
6601 append_sha1_pack_trailer(&mut pack);
6602
6603 let dir = tempfile::tempdir().expect("tempdir");
6604 let pack_path = dir.path().join("mem.pack");
6605 std::fs::write(&pack_path, &pack).expect("write pack");
6606 let idx = pack_index_for_objects(pack_path, &[(oid, off)]);
6607 std::fs::remove_file(&idx.idx_path).expect("drop on-disk idx");
6608 assert!(
6609 !idx.idx_path.exists(),
6610 "fixture must not rely on an on-disk .idx"
6611 );
6612
6613 let got = read_object_from_pack_bytes(&pack, &idx, oid.as_bytes()).expect("read bytes");
6614 assert_eq!(got.kind, ObjectKind::Blob);
6615 assert_eq!(got.data, content);
6616 }
6617
6618 fn build_ofs_delta_chain_pack(
6619 depth: usize,
6620 ) -> Option<(tempfile::TempDir, PackIndex, ObjectId, Odb)> {
6621 if depth == 0 {
6622 return None;
6623 }
6624 let tmp = tempfile::tempdir().ok()?;
6625 let odb = Odb::new(&tmp.path().join("objects"));
6626 let mut content = b"ofs-delta-chain-base\n".to_vec();
6627 let mut pack = Vec::new();
6628 pack.extend_from_slice(b"PACK");
6629 pack.extend_from_slice(&2u32.to_be_bytes());
6630 pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
6631 let mut base_off = append_whole_blob(&mut pack, &content);
6632 let mut entries = vec![(
6633 odb.hash(ObjectKind::Blob, &content).expect("hash"),
6634 base_off,
6635 )];
6636 for _ in 0..depth {
6637 let mut next = content.clone();
6638 next.push(b'!');
6639 let delta = encode_lcp_delta(&content, &next).ok()?;
6640 let off = append_ofs_delta(&mut pack, base_off, &delta);
6641 let oid = odb.hash(ObjectKind::Blob, &next).expect("hash");
6642 entries.push((oid, off));
6643 content = next;
6644 base_off = off;
6645 }
6646 append_sha1_pack_trailer(&mut pack);
6647 let tip = entries.last()?.0;
6648 let dir = tmp.path();
6649 std::fs::create_dir_all(dir.join("objects/pack")).ok()?;
6650 let pack_path = dir.join("objects/pack/chain.pack");
6651 std::fs::write(&pack_path, &pack).ok()?;
6652 write_v2_idx_for_test(&pack_path.with_extension("idx"), &pack_path, &entries).ok()?;
6653 clear_pack_cache();
6654 let idx = read_pack_index(&pack_path.with_extension("idx")).ok()?;
6655 Some((tmp, idx, tip, odb))
6656 }
6657
6658 #[test]
6659 fn warm_hinted_pack_damage_returns_corrupt_packed_object_via_full_lookup() {
6660 let _guard = pack_cache_test_guard();
6661 clear_pack_cache();
6662 let tmp = tempfile::tempdir().expect("tempdir");
6663 let dir = tmp.path();
6664 if !git_try(dir, &["init", "-q"]) {
6665 return;
6666 }
6667 std::fs::write(dir.join("big.txt"), vec![0u8; 20_000]).expect("big");
6668 git(dir, &["add", "big.txt"]);
6669 git(dir, &["commit", "-qm", "one"]);
6670 git(dir, &["repack", "-adq"]);
6671 std::fs::write(dir.join("more.txt"), b"second pack\n").expect("more");
6672 git(dir, &["add", "more.txt"]);
6673 git(dir, &["commit", "-qm", "two"]);
6674 git(dir, &["repack"]);
6675 let objects = dir.join(".git/objects");
6676 let pack_dir = objects.join("pack");
6677 let pack_count = std::fs::read_dir(&pack_dir)
6678 .expect("pack dir")
6679 .filter_map(|e| e.ok())
6680 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
6681 .count();
6682 assert!(
6683 pack_count >= 2,
6684 "expected multiple packs for warm-hint regression, got {pack_count}"
6685 );
6686 let odb = Odb::new(&objects);
6687 let oid_hex = Command::new("git")
6688 .current_dir(dir)
6689 .args(["rev-parse", "HEAD~1:big.txt"])
6690 .env("GIT_CONFIG_GLOBAL", "/dev/null")
6691 .env("GIT_CONFIG_SYSTEM", "/dev/null")
6692 .output()
6693 .expect("rev-parse")
6694 .stdout;
6695 let oid =
6696 ObjectId::from_hex(std::str::from_utf8(&oid_hex).unwrap().trim()).expect("parse oid");
6697 let indexes = read_local_pack_indexes(&objects).expect("indexes");
6698 let (idx_arc, offset) = indexes
6699 .iter()
6700 .find_map(|idx| {
6701 idx.find_offset(&oid)
6702 .ok()
6703 .flatten()
6704 .map(|offset| (Arc::new(idx.clone()), offset))
6705 })
6706 .expect("oid in a pack");
6707 let corrupt_pack = |pack_path: &Path| {
6708 let mut bytes = std::fs::read(pack_path).expect("read pack");
6709 let flip_at = (offset as usize + 8).min(bytes.len().saturating_sub(1));
6710 bytes[flip_at] ^= 0x55;
6711 #[cfg(unix)]
6712 {
6713 use std::os::unix::fs::PermissionsExt;
6714 std::fs::set_permissions(pack_path, std::fs::Permissions::from_mode(0o644))
6715 .expect("chmod pack");
6716 }
6717 std::fs::write(pack_path, &bytes).expect("corrupt pack");
6718 };
6719 let assert_warm_hint = |label: &str| {
6720 let hint = pack_cache::hinted_pack_index(&objects)
6721 .unwrap_or_else(|| panic!("{label}: pack hint must be warm"));
6722 assert_eq!(
6723 hint.pack_path, idx_arc.pack_path,
6724 "{label}: hint must target the corrupted pack"
6725 );
6726 };
6727 let reseed_hint = || pack_cache::remember_pack_hit(&objects, &idx_arc);
6728
6729 odb.with_pack_read_context(|| {
6730 odb.read(&oid).expect("warm pack hint");
6731 assert_warm_hint("after warm read");
6732
6733 corrupt_pack(&idx_arc.pack_path);
6734 assert!(
6735 pack_cache::revalidate_stale_pack_bytes(&idx_arc.pack_path).expect("revalidate"),
6736 "corrupt pack bytes must be observable in cache"
6737 );
6738 reseed_hint();
6739 assert_warm_hint("before try_read_from_hinted_pack");
6740
6741 let hinted = pack_cache::try_read_from_hinted_pack(&objects, &oid)
6742 .expect("hinted pack read must not propagate raw zlib");
6743 assert!(
6744 hinted.is_none(),
6745 "damaged hinted read must fall through to full lookup"
6746 );
6747 assert_warm_hint("before odb.read after hinted shortcut");
6748
6749 let err = odb.read(&oid).expect_err("read after corrupt hinted miss");
6750 assert!(
6751 matches!(err, Error::CorruptPackedObject { .. }),
6752 "read: {err:?}"
6753 );
6754
6755 reseed_hint();
6756 assert_warm_hint("before try_read_from_hinted_pack_into");
6757
6758 let mut buf = Vec::new();
6759 let hinted_into =
6760 pack_cache::try_read_from_hinted_pack_into(&objects, &oid, true, &mut buf)
6761 .expect("hinted pack read_into must not propagate raw zlib");
6762 assert!(
6763 hinted_into.is_none(),
6764 "damaged hinted read_into must fall through to full lookup"
6765 );
6766 assert_warm_hint("before odb.read_into after hinted shortcut");
6767
6768 let err = odb
6769 .read_into(&oid, &mut Vec::new())
6770 .expect_err("read_into after corrupt hinted miss");
6771 assert!(
6772 matches!(err, Error::CorruptPackedObject { .. }),
6773 "read_into: {err:?}"
6774 );
6775 });
6776 }
6777
6778 #[test]
6779 fn bounded_pack_cache_resolves_deltas_without_changing_shared_cache() {
6780 let _guard = pack_cache_test_guard();
6781 let (_tmp, idx, tip, odb) = build_ofs_delta_chain_pack(800).expect("chain");
6782 let normal = odb.read(&tip).expect("normal");
6783 let shared_bytes = odb.pack_store().expect("store").delta_bases().bytes_used();
6784 let bounded = odb.with_bounded_pack_cache();
6785 assert!(!Arc::ptr_eq(
6786 odb.pack_store().expect("store"),
6787 bounded.pack_store().expect("store")
6788 ));
6789 bounded.with_pack_read_context(|| {
6790 let got = bounded.read(&tip).expect("bounded");
6791 assert_eq!(got.data, normal.data);
6792 let pack = read_pack_bytes_cached(&idx.pack_path).expect("pack");
6793 assert!(pack.uses_positioned_reads());
6794 let cached = bounded
6795 .pack_store()
6796 .expect("store")
6797 .delta_bases()
6798 .bytes_used();
6799 assert!(cached > 0 && cached <= crate::pack_store::BOUNDED_DELTA_BASE_BYTES);
6800 });
6801 assert_eq!(
6802 odb.pack_store().expect("store").delta_bases().bytes_used(),
6803 shared_bytes
6804 );
6805 }
6806
6807 #[test]
6808 fn ofs_delta_chain_depth_4096_resolves() {
6809 let _guard = pack_cache_test_guard();
6810 clear_pack_cache();
6811 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(4096) else {
6812 return;
6813 };
6814 let got = read_object_from_pack(&idx, &tip).expect("4096-deep chain");
6815 let expect = odb.hash(ObjectKind::Blob, &got.data).expect("hash");
6816 assert_eq!(expect, tip);
6817 }
6818
6819 #[test]
6820 fn ofs_delta_chain_depth_4097_errors() {
6821 let _guard = pack_cache_test_guard();
6822 clear_pack_cache();
6823 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(4097) else {
6824 return;
6825 };
6826 let err = read_object_from_pack(&idx, &tip).expect_err("4097-deep chain");
6827 assert!(
6828 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6829 "got {err:?}"
6830 );
6831 }
6832
6833 #[test]
6834 fn delta_base_cache_limit_zero_disables_caching() {
6835 let _guard = pack_cache_test_guard();
6836 clear_pack_cache();
6837 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6838 return;
6839 };
6840 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 0);
6841 let _ = read_object_from_pack(&idx, &tip).expect("read chain");
6842 assert_eq!(
6843 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6844 0,
6845 "limit 0 must not retain delta bases"
6846 );
6847 pack_cache::test_set_delta_base_cache_byte_limit(
6848 &idx.pack_path,
6849 pack_cache::DELTA_BASE_CACHE_DEFAULT,
6850 );
6851 }
6852
6853 #[test]
6854 fn delta_base_cache_eviction_respects_byte_limit() {
6855 let _guard = pack_cache_test_guard();
6856 clear_pack_cache();
6857 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(32) else {
6858 return;
6859 };
6860 let expected = read_object_from_pack(&idx, &tip)
6861 .expect("baseline read")
6862 .data;
6863 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 4096);
6864 let _ = read_object_from_pack(&idx, &tip).expect("prime cache");
6865 assert!(
6866 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) <= 4096,
6867 "cache bytes {} exceeded cap",
6868 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path)
6869 );
6870 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 512);
6871 for round in 0..8 {
6872 let got = read_object_from_pack(&idx, &tip).expect("read after eviction");
6873 assert_eq!(got.data, expected, "round {round} must stay byte-identical");
6874 }
6875 pack_cache::test_set_delta_base_cache_byte_limit(
6876 &idx.pack_path,
6877 pack_cache::DELTA_BASE_CACHE_DEFAULT,
6878 );
6879 }
6880
6881 #[test]
6882 fn odb_read_into_caching_delta_bases_fills_cache_read_into_does_not() {
6883 let _guard = pack_cache_test_guard();
6884 clear_pack_cache();
6885 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(12) else {
6886 return;
6887 };
6888 let expected = read_object_from_pack(&idx, &tip)
6889 .expect("baseline read")
6890 .data;
6891 clear_pack_cache();
6892 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
6893 crate::pack_store::PackStore::with_context(store, || {
6894 let mut buf = Vec::new();
6895 assert_eq!(
6896 odb.read_into(&tip, &mut buf).expect("read_into"),
6897 ObjectKind::Blob
6898 );
6899 assert_eq!(buf, expected);
6900 assert_eq!(
6901 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6902 0,
6903 "read_into must not retain delta bases"
6904 );
6905
6906 let mut cached = Vec::new();
6907 let kind = odb
6908 .read_into_caching_delta_bases(&tip, &mut cached)
6909 .expect("caching read");
6910 assert_eq!(kind, ObjectKind::Blob);
6911 assert_eq!(cached, expected);
6912 assert!(
6913 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) > 0,
6914 "caching read must retain the chain's delta bases"
6915 );
6916
6917 let again = odb
6918 .read_into_caching_delta_bases(&tip, &mut cached)
6919 .expect("second caching read");
6920 assert_eq!(again, ObjectKind::Blob);
6921 assert_eq!(cached, expected);
6922 });
6923 }
6924
6925 #[test]
6926 fn repository_odb_read_into_caching_delta_bases_fills_cache_through_files_primary() {
6927 use crate::repo::{InitOptions, Repository};
6928 let _guard = pack_cache_test_guard();
6929 clear_pack_cache();
6930 let Some((tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6931 return;
6932 };
6933 let expected = read_object_from_pack(&idx, &tip)
6934 .expect("baseline read")
6935 .data;
6936 Repository::init(
6937 tmp.path(),
6938 &InitOptions {
6939 bare: true,
6940 ..InitOptions::default()
6941 },
6942 )
6943 .expect("init over packed objects");
6944 let repo = Repository::open(tmp.path(), &crate::environment::RepositoryOptions::empty())
6945 .expect("open");
6946 let odb = repo.odb();
6947 let store = Arc::clone(odb.pack_store().expect("pack store"));
6948 store.invalidate_packs();
6949
6950 let mut buf = Vec::new();
6951 assert_eq!(
6952 odb.read_into(&tip, &mut buf).expect("read_into"),
6953 ObjectKind::Blob
6954 );
6955 assert_eq!(buf, expected);
6956 assert_eq!(store.delta_bases().bytes_used(), 0);
6957
6958 let mut cached = Vec::new();
6959 assert_eq!(
6960 odb.read_into_caching_delta_bases(&tip, &mut cached)
6961 .expect("caching read"),
6962 ObjectKind::Blob
6963 );
6964 assert_eq!(cached, expected);
6965 assert!(store.delta_bases().bytes_used() > 0);
6966 }
6967
6968 #[test]
6969 fn odb_read_into_caching_delta_bases_falls_back_outside_packs() {
6970 let tmp = tempfile::tempdir().expect("tempdir");
6971 let odb = Odb::new(&tmp.path().join("objects"));
6972 let oid = odb
6973 .write(ObjectKind::Blob, b"loose only\n")
6974 .expect("write loose");
6975 let mut buf = vec![0u8; 4];
6976 let kind = odb
6977 .read_into_caching_delta_bases(&oid, &mut buf)
6978 .expect("loose read");
6979 assert_eq!(kind, ObjectKind::Blob);
6980 assert_eq!(buf, b"loose only\n");
6981 }
6982
6983 #[test]
6984 fn cached_delta_reads_support_overlay_alternates_and_custom_stores() {
6985 use crate::odb::store::MemoryStore;
6986 use crate::odb::OdbBuilder;
6987
6988 let tmp = tempfile::tempdir().expect("tempdir");
6989 let local_objects = tmp.path().join("local/objects");
6990 let alternate_objects = tmp.path().join("alternate/objects");
6991 let alternate = Odb::new(&alternate_objects);
6992 let alternate_oid = alternate
6993 .write(ObjectKind::Blob, b"alternate\n")
6994 .expect("alternate object");
6995 fs::create_dir_all(local_objects.join("info")).expect("info");
6996 fs::write(
6997 local_objects.join("info/alternates"),
6998 format!("{}\n", alternate_objects.display()),
6999 )
7000 .expect("alternates");
7001
7002 let odb = Odb::new(&local_objects);
7003 let mut buf = Vec::with_capacity(1024);
7004 assert_eq!(
7005 odb.read_into_caching_delta_bases(&alternate_oid, &mut buf)
7006 .expect("alternate read"),
7007 ObjectKind::Blob
7008 );
7009 assert_eq!(buf, b"alternate\n");
7010
7011 odb.enable_mem_overlay();
7012 let overlay_oid = odb
7013 .write(ObjectKind::Blob, b"overlay\n")
7014 .expect("overlay object");
7015 odb.read_into_caching_delta_bases(&overlay_oid, &mut buf)
7016 .expect("overlay read");
7017 assert_eq!(buf, b"overlay\n");
7018
7019 let custom = OdbBuilder::files(tmp.path().join("custom/objects"))
7020 .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
7021 .build();
7022 let custom_oid = custom
7023 .write(ObjectKind::Blob, b"custom\n")
7024 .expect("custom object");
7025 custom
7026 .read_into_caching_delta_bases(&custom_oid, &mut buf)
7027 .expect("custom read");
7028 assert_eq!(buf, b"custom\n");
7029
7030 let missing = HashAlgo::Sha1
7031 .hash_object(ObjectKind::Blob, b"missing")
7032 .expect("missing id");
7033 assert!(matches!(
7034 custom.read_into_caching_delta_bases(&missing, &mut buf),
7035 Err(Error::ObjectNotFound(_))
7036 ));
7037 }
7038
7039 #[test]
7040 fn delta_base_cache_invalidated_when_pack_bytes_reloaded() {
7041 use filetime::{set_file_mtime, FileTime};
7042 let _guard = pack_cache_test_guard();
7043 clear_pack_cache();
7044 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(8) else {
7045 return;
7046 };
7047 let pack_path = idx.pack_path.clone();
7048 let pack_id = pack_cache::pack_id_for(&pack_path);
7049 let mid_off = idx
7050 .iter()
7051 .nth(4)
7052 .map(|e| e.offset())
7053 .expect("mid hop")
7054 .expect("mid off");
7055 let _ = read_object_from_pack(&idx, &tip).expect("warm cache");
7056 assert!(
7057 pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
7058 "expected intermediate cached at offset {mid_off}"
7059 );
7060 set_file_mtime(&pack_path, FileTime::now()).expect("touch pack mtime");
7061 assert!(
7062 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
7063 "expected pack bytes reload"
7064 );
7065 assert!(
7066 !pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
7067 "cache must drop when pack bytes reload"
7068 );
7069 }
7070
7071 #[cfg_attr(
7072 not(feature = "full-tests"),
7073 ignore = "starts system git or is heavy; run with --features full-tests"
7074 )]
7075 #[test]
7076 fn git_repack_depth_250_matches_cat_file_batch() {
7077 let _guard = pack_cache_test_guard();
7078 clear_pack_cache();
7079 let tmp = tempfile::tempdir().expect("tempdir");
7080 let dir = tmp.path();
7081 if Command::new("git") .current_dir(dir)
7083 .args(["init", "-q"])
7084 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7085 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7086 .status()
7087 .is_err()
7088 {
7089 let _ = "SKIP: git unavailable";
7090 return;
7091 }
7092 for i in 0..64 {
7093 std::fs::write(dir.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
7094 git(dir, &["add", &format!("f{i}.txt")]);
7095 git(dir, &["commit", "-qm", &format!("c{i}")]);
7096 }
7097 git(dir, &["repack", "-adf", "--depth=250", "--window=250"]);
7098 let objects = dir.join(".git/objects");
7099 let indexes = read_local_pack_indexes_cached(&objects).expect("pack indexes");
7100 let idx = indexes
7101 .into_iter()
7102 .max_by_key(|i| i.len())
7103 .expect("repack pack");
7104 for entry in idx.iter() {
7105 let oid = ObjectId::from_bytes(entry.oid()).expect("oid");
7106 let grit_obj = read_object_from_pack(&idx, &oid).expect("grit read");
7107 let git_type = Command::new("git") .current_dir(dir)
7109 .args(["cat-file", "-t", &oid.to_hex()])
7110 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7111 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7112 .output()
7113 .expect("git cat-file -t");
7114 assert!(git_type.status.success());
7115 let git_kind = ObjectKind::from_bytes(
7116 std::str::from_utf8(git_type.stdout.trim_ascii_end())
7117 .expect("utf8 type")
7118 .as_bytes(),
7119 )
7120 .expect("git type");
7121 assert_eq!(git_kind, grit_obj.kind);
7122 let type_arg = match grit_obj.kind {
7123 ObjectKind::Blob => "blob",
7124 ObjectKind::Tree => "tree",
7125 ObjectKind::Commit => "commit",
7126 ObjectKind::Tag => "tag",
7127 };
7128 let git_payload = Command::new("git") .current_dir(dir)
7130 .args(["cat-file", type_arg, &oid.to_hex()])
7131 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7132 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7133 .output()
7134 .expect("git cat-file raw");
7135 assert!(git_payload.status.success());
7136 assert_eq!(git_payload.stdout, grit_obj.data);
7137 }
7138 }
7139
7140 #[test]
7141 fn cyclic_ref_delta_chain_across_two_packs_errors() {
7142 let _guard = pack_cache_test_guard();
7143 clear_pack_cache();
7144 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7145 let content_a = b"aaa".as_slice();
7146 let content_b = b"bbb".as_slice();
7147 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
7148 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
7149 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
7150 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
7151
7152 let mut pack_a = Vec::new();
7153 pack_a.extend_from_slice(b"PACK");
7154 pack_a.extend_from_slice(&2u32.to_be_bytes());
7155 pack_a.extend_from_slice(&1u32.to_be_bytes());
7156 let off_a = pack_a.len() as u64;
7157 append_ref_delta(&mut pack_a, &oid_b, &delta_a);
7158 append_sha1_pack_trailer(&mut pack_a);
7159
7160 let mut pack_b = Vec::new();
7161 pack_b.extend_from_slice(b"PACK");
7162 pack_b.extend_from_slice(&2u32.to_be_bytes());
7163 pack_b.extend_from_slice(&1u32.to_be_bytes());
7164 let off_b = pack_b.len() as u64;
7165 append_ref_delta(&mut pack_b, &oid_a, &delta_b);
7166 append_sha1_pack_trailer(&mut pack_b);
7167
7168 let dir = tempfile::tempdir().expect("tempdir");
7169 let objects = dir.path().join("objects");
7170 let idx_b = install_synthetic_pack(&objects, "cycle-b", &pack_b, &[(oid_b, off_b)]);
7171 let idx_a = install_synthetic_pack(&objects, "cycle-a", &pack_a, &[(oid_a, off_a)]);
7172 assert_eq!(idx_a.len(), 1);
7173 assert_eq!(idx_b.len(), 1);
7174 assert!(idx_b.contains(&oid_b).expect("contains"));
7175 let _ = idx_b;
7176
7177 let err = read_object_from_pack(&idx_a, &oid_a).expect_err("cross-pack cycle");
7178 assert!(
7179 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
7180 "expected typed delta chain limit, got {err:?}"
7181 );
7182 }
7183
7184 #[test]
7185 fn cross_pack_ref_delta_resolves_through_odb_read() {
7186 let _guard = pack_cache_test_guard();
7187 clear_pack_cache();
7188 let tmp = tempfile::tempdir().expect("tempdir");
7189 let objects = tmp.path().join("objects");
7190 let odb = Odb::new(&objects);
7191 let mut random = 17u64;
7192 let base: Vec<u8> = (0..20000)
7193 .map(|_| {
7194 random ^= random << 13;
7195 random ^= random >> 7;
7196 random ^= random << 17;
7197 random as u8
7198 })
7199 .collect();
7200 let mut target = base.clone();
7201 target.extend_from_slice(b"target suffix");
7202 let oid_base = odb.hash(ObjectKind::Blob, &base).expect("hash");
7203 let delta = encode_lcp_delta(&base, &target).expect("delta");
7204 let oid_target = odb.hash(ObjectKind::Blob, &target).expect("hash");
7205
7206 let mut pack_base = Vec::new();
7207 pack_base.extend_from_slice(b"PACK");
7208 pack_base.extend_from_slice(&2u32.to_be_bytes());
7209 pack_base.extend_from_slice(&1u32.to_be_bytes());
7210 let off_base = append_whole_blob(&mut pack_base, &base);
7211 append_sha1_pack_trailer(&mut pack_base);
7212
7213 let mut pack_tip = Vec::new();
7214 pack_tip.extend_from_slice(b"PACK");
7215 pack_tip.extend_from_slice(&2u32.to_be_bytes());
7216 pack_tip.extend_from_slice(&1u32.to_be_bytes());
7217 let off_tip = pack_tip.len() as u64;
7218 append_ref_delta(&mut pack_tip, &oid_base, &delta);
7219 append_sha1_pack_trailer(&mut pack_tip);
7220
7221 install_synthetic_pack(&objects, "base", &pack_base, &[(oid_base, off_base)]);
7222 install_synthetic_pack(&objects, "tip", &pack_tip, &[(oid_target, off_tip)]);
7223
7224 for reader in [&odb, &odb.with_bounded_pack_cache()] {
7225 let got = reader.read(&oid_target).expect("cross-pack ref-delta");
7226 assert_eq!(got.kind, ObjectKind::Blob);
7227 assert_eq!(got.data, target);
7228 assert_eq!(reader.hash(got.kind, &got.data).expect("hash"), oid_target);
7229 }
7230 }
7231
7232 #[test]
7233 fn corrupt_in_pack_base_rescued_from_loose() {
7234 let _guard = pack_cache_test_guard();
7235 let tmp = tempfile::tempdir().expect("tempdir");
7236 let objects = tmp.path().join("objects");
7237 std::fs::create_dir_all(objects.join("pack")).expect("dirs");
7238 let odb = Odb::new(&objects);
7239 let base = b"good-base-bytes";
7240 let tip = b"good-base-bytes!";
7241 let oid_base = odb.write_local(ObjectKind::Blob, base).expect("loose base");
7242 let delta = encode_lcp_delta(base, tip).expect("delta");
7243 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
7244
7245 let mut pack = Vec::new();
7246 pack.extend_from_slice(b"PACK");
7247 pack.extend_from_slice(&2u32.to_be_bytes());
7248 pack.extend_from_slice(&2u32.to_be_bytes());
7249 let off_base = append_corrupt_whole_blob(&mut pack);
7250 let off_tip = pack.len() as u64;
7251 append_ref_delta(&mut pack, &oid_base, &delta);
7252 append_sha1_pack_trailer(&mut pack);
7253
7254 let pack_path = objects.join("pack/rescue.pack");
7255 std::fs::write(&pack_path, &pack).expect("write pack");
7256 let idx = pack_index_for_objects(pack_path, &[(oid_base, off_base), (oid_tip, off_tip)]);
7257
7258 let got = read_object_from_pack(&idx, &oid_tip).expect("rescue read");
7259 assert_eq!(got.kind, ObjectKind::Blob);
7260 assert_eq!(got.data, tip);
7261 }
7262
7263 #[test]
7264 fn pack_added_after_first_lookup_found_on_miss() {
7265 let _guard = pack_cache_test_guard();
7266 clear_pack_cache();
7267 let tmp = tempfile::tempdir().expect("tempdir");
7268 let objects = tmp.path().join("objects");
7269 let pack_dir = objects.join("pack");
7270 let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
7271 install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
7272 pack_cache::test_reset_dir_rescan_count(&pack_dir);
7273
7274 read_object_from_packs(&objects, &oid_a).expect("warm cache");
7275 assert_eq!(
7276 pack_cache::test_dir_rescan_count(&pack_dir),
7277 1,
7278 "initial lookup should scan the pack directory once"
7279 );
7280
7281 std::thread::sleep(std::time::Duration::from_secs(1));
7283
7284 let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b");
7285 install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
7286 assert_eq!(
7287 pack_cache::test_dir_rescan_count(&pack_dir),
7288 1,
7289 "adding a pack must not rescan until a miss"
7290 );
7291
7292 read_object_from_packs(&objects, &oid_b).expect("new pack via miss path");
7293 assert_eq!(
7294 pack_cache::test_dir_rescan_count(&pack_dir),
7295 2,
7296 "miss path should re-scan when pack dir mtime changed"
7297 );
7298 }
7299
7300 #[test]
7301 fn pack_added_same_pack_dir_mtime_found_on_miss() {
7302 use crate::odb::Odb;
7303
7304 let _guard = pack_cache_test_guard();
7305 clear_pack_cache();
7306 let tmp = tempfile::tempdir().expect("tempdir");
7307 let objects = tmp.path().join("objects");
7308 let pack_dir = objects.join("pack");
7309 let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
7310 install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
7311
7312 let odb = Odb::new(&objects);
7313 odb.read(&oid_a).expect("warm odb cache");
7314
7315 let saved_mt = filetime::FileTime::from_last_modification_time(
7316 &std::fs::metadata(&pack_dir).expect("pack dir meta"),
7317 );
7318 let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b-same-second");
7319 install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
7320 filetime::set_file_mtime(&pack_dir, saved_mt).expect("restore pack dir mtime");
7321
7322 odb.read(&oid_b)
7323 .expect("new pack visible after idx-set compare on miss");
7324 }
7325
7326 #[test]
7327 fn absent_object_misses_do_not_rescan_pack_dir() {
7328 let _guard = pack_cache_test_guard();
7329 clear_pack_cache();
7330 let tmp = tempfile::tempdir().expect("tempdir");
7331 let objects = tmp.path().join("objects");
7332 let pack_dir = objects.join("pack");
7333 let (pack, oid, off) = single_blob_pack(b"cached-blob");
7334 install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
7335 pack_cache::test_reset_dir_rescan_count(&pack_dir);
7336
7337 read_object_from_packs(&objects, &oid).expect("warm cache");
7338 let after_warm = pack_cache::test_dir_rescan_count(&pack_dir);
7339 let missing = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
7340 for _ in 0..128 {
7341 assert!(matches!(
7342 read_object_from_packs(&objects, &missing),
7343 Err(Error::ObjectNotFound(_))
7344 ));
7345 }
7346 assert_eq!(
7347 pack_cache::test_dir_rescan_count(&pack_dir),
7348 after_warm,
7349 "absent oid misses must not re-scan the pack directory"
7350 );
7351 }
7352
7353 #[test]
7354 fn known_object_lookup_does_not_rescan() {
7355 let _guard = pack_cache_test_guard();
7356 clear_pack_cache();
7357 let tmp = tempfile::tempdir().expect("tempdir");
7358 let objects = tmp.path().join("objects");
7359 let pack_dir = objects.join("pack");
7360 pack_cache::test_reset_dir_rescan_count(&pack_dir);
7361 let (pack, oid, off) = single_blob_pack(b"cached-blob");
7362 install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
7363
7364 read_object_from_packs(&objects, &oid).expect("prime cache");
7365 let after_first = pack_cache::test_dir_rescan_count(&pack_dir);
7366 for _ in 0..64 {
7367 read_object_from_packs(&objects, &oid).expect("repeat read");
7368 }
7369 assert_eq!(
7370 pack_cache::test_dir_rescan_count(&pack_dir),
7371 after_first,
7372 "repeated hits must not re-scan the pack directory"
7373 );
7374 }
7375
7376 #[test]
7377 fn two_repositories_concurrent_reads() {
7378 use crate::odb::Odb;
7379 use std::sync::Arc;
7380
7381 let _guard = pack_cache_test_guard();
7382 clear_pack_cache();
7383 let tmp_a = tempfile::tempdir().expect("tempdir a");
7384 let tmp_b = tempfile::tempdir().expect("tempdir b");
7385 let objects_a = tmp_a.path().join("objects");
7386 let objects_b = tmp_b.path().join("objects");
7387 let pack_a_dir = objects_a.join("pack");
7388 let pack_b_dir = objects_b.join("pack");
7389 let (pack_a, oid_a, off_a) = single_blob_pack(b"parallel-a");
7390 let (pack_b, oid_b, off_b) = single_blob_pack(b"parallel-b");
7391 install_synthetic_pack(&objects_a, "a", &pack_a, &[(oid_a, off_a)]);
7392 install_synthetic_pack(&objects_b, "b", &pack_b, &[(oid_b, off_b)]);
7393 let odb_a = Odb::new(&objects_a);
7394 let odb_b = Odb::new(&objects_b);
7395 assert!(
7396 !Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
7397 "independent Odb opens must not share pack stores"
7398 );
7399 pack_cache::test_reset_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir);
7400 pack_cache::test_reset_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir);
7401
7402 let barrier = Arc::new(std::sync::Barrier::new(2));
7403 let b0 = Arc::clone(&barrier);
7404 let b1 = Arc::clone(&barrier);
7405 let odb_a_t = odb_a.clone();
7406 let odb_b_t = odb_b.clone();
7407 let id_a = oid_a;
7408 let id_b = oid_b;
7409
7410 let h1 = std::thread::spawn(move || {
7411 b0.wait();
7412 odb_a_t.read(&id_a).expect("read a");
7413 });
7414 let h2 = std::thread::spawn(move || {
7415 b1.wait();
7416 odb_b_t.read(&id_b).expect("read b");
7417 });
7418 h1.join().expect("thread a");
7419 h2.join().expect("thread b");
7420
7421 assert_eq!(
7422 pack_cache::test_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir),
7423 1
7424 );
7425 assert_eq!(
7426 pack_cache::test_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir),
7427 1
7428 );
7429 }
7430
7431 #[test]
7432 fn stale_cached_pack_bytes_revalidated_before_corruption_error() {
7433 let _guard = pack_cache_test_guard();
7434 clear_pack_cache();
7435 let tmp = tempfile::tempdir().expect("tempdir");
7436 let objects = tmp.path().join("objects");
7437 let (pack, oid, off) = single_blob_pack(b"revalidate-me");
7438 let idx = install_synthetic_pack(&objects, "good", &pack, &[(oid, off)]);
7439 read_object_from_pack(&idx, &oid).expect("prime pack bytes cache");
7440
7441 let good_on_disk = std::fs::read(&idx.pack_path).expect("read pack");
7442 pack_cache::test_inject_stale_pack_bytes(
7443 &idx.pack_path,
7444 PackData::from_owned(b"BAD".to_vec()),
7445 );
7446 std::fs::write(&idx.pack_path, &good_on_disk).expect("refresh on-disk pack");
7447 let got = read_object_from_pack(&idx, &oid).expect("revalidate from disk");
7448 assert_eq!(got.data, b"revalidate-me");
7449 }
7450
7451 #[test]
7452 fn replaced_pack_same_path_reread_after_invalidation() {
7453 let _guard = pack_cache_test_guard();
7454 clear_pack_cache();
7455 let tmp = tempfile::tempdir().expect("tempdir");
7456 let objects = tmp.path().join("objects");
7457 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
7458 let pack_path = objects.join("pack/repack-me.pack");
7459 let idx_path = objects.join("pack/repack-me.idx");
7460 const OBJECT_OFFSET: u64 = 9000;
7461 let (oid_a, off_a) =
7462 write_sparse_single_blob_pack(tmp.path(), &pack_path, b"before-repack", OBJECT_OFFSET)
7463 .expect("sparse pack a");
7464 assert!(
7465 std::fs::metadata(&pack_path).expect("stat").len() > 8192,
7466 "fixture pack must exceed mmap owned-buffer threshold"
7467 );
7468 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_a, off_a)]).expect("idx a");
7469 let idx = read_pack_index(&idx_path).expect("parse idx a");
7470 read_object_from_pack(&idx, &oid_a).expect("prime cache");
7471
7472 let (oid_b, off_b) = write_sparse_single_blob_pack(
7473 tmp.path(),
7474 &pack_path,
7475 b"after-repack-content",
7476 OBJECT_OFFSET,
7477 )
7478 .expect("sparse pack b");
7479 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_b, off_b)]).expect("idx b");
7480
7481 assert!(
7482 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
7483 "replacement must invalidate cached bytes"
7484 );
7485 let idx = read_pack_index(&idx_path).expect("reload idx after pack swap");
7486 let got = read_object_from_pack(&idx, &oid_b).expect("read from replaced pack");
7487 assert_eq!(got.data, b"after-repack-content");
7488 }
7489
7490 #[test]
7491 fn read_object_at_large_pack_offset_via_mmap() {
7492 use std::fs::OpenOptions;
7493 use std::io::{Seek, SeekFrom, Write};
7494
7495 let _guard = pack_cache_test_guard();
7496 clear_pack_cache();
7497 let tmp = tempfile::tempdir().expect("tempdir");
7498 let objects = tmp.path().join("objects");
7499 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
7500
7501 let data = b"large-offset-blob";
7502 let odb = Odb::new(tmp.path());
7503 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
7504
7505 let pack_path = objects.join("pack/large-off.pack");
7506 let mut file = OpenOptions::new()
7507 .create(true)
7508 .write(true)
7509 .read(true)
7510 .open(&pack_path)
7511 .expect("create sparse pack");
7512 file.write_all(b"PACK").expect("sig");
7513 file.write_all(&2u32.to_be_bytes()).expect("ver");
7514 file.write_all(&1u32.to_be_bytes()).expect("count");
7515 let object_offset: u64 = 1 << 32;
7516 file.seek(SeekFrom::Start(object_offset))
7517 .expect("seek past 4GiB");
7518 let mut tail = Vec::new();
7519 append_whole_blob(&mut tail, data);
7520 file.write_all(&tail).expect("object at huge offset");
7521 let trailer = sha1_trailer_for_open_file(&mut file).expect("trailer hash");
7522 file.write_all(trailer.as_bytes()).expect("append trailer");
7523 file.flush().expect("flush");
7524 let file_len = file.metadata().expect("meta").len();
7525 drop(file);
7526 assert!(file_len > object_offset);
7527
7528 let idx_path = pack_path.with_extension("idx");
7529 write_v2_pack_index_with_trailer(
7530 &idx_path,
7531 &[(oid, object_offset, 0)],
7532 trailer.as_bytes(),
7533 20,
7534 None,
7535 )
7536 .expect("idx");
7537
7538 let idx = read_pack_index(&idx_path).expect("parse idx");
7539 assert_eq!(idx.offset_at(0).expect("off"), object_offset);
7540 let got = read_object_from_pack(&idx, &oid).expect("read at large offset");
7541 assert_eq!(got.data, data);
7542 }
7543
7544 fn synthetic_index_entries(n: u32) -> Vec<(ObjectId, u64, u32)> {
7545 (0..n)
7546 .map(|i| {
7547 let digest = HashAlgo::Sha1.checksum_digest(&i.to_be_bytes());
7548 let oid = ObjectId::from_bytes(&digest).expect("oid");
7549 let offset = if i % 7 == 0 {
7550 (1u64 << 31) + u64::from(i)
7551 } else {
7552 12 + u64::from(i) * 40
7553 };
7554 (oid, offset, i.wrapping_mul(2_654_435_761))
7555 })
7556 .collect()
7557 }
7558
7559 #[test]
7560 fn index_entries_sort_in_place_on_any_thread_count() {
7561 for n in [0_u32, 1, 300, 20_000] {
7562 let mut reference = synthetic_index_entries(n);
7563 reference.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
7564 let mut fanout_ref = [0u32; 256];
7565 for (oid, _, _) in &reference {
7566 for slot in &mut fanout_ref[usize::from(oid.as_bytes()[0])..] {
7567 *slot += 1;
7568 }
7569 }
7570 for threads in [None, Some(1), Some(2), Some(3), Some(8)] {
7571 let mut entries = synthetic_index_entries(n);
7572 let fanout = sort_index_entries(&mut entries, threads);
7573 assert_eq!(entries, reference, "n={n} threads={threads:?}");
7574 assert_eq!(fanout, fanout_ref, "n={n} threads={threads:?}");
7575 }
7576 }
7577 }
7578
7579 #[test]
7580 fn index_written_in_place_reads_back_every_row_with_large_offsets() {
7581 let tmp = tempfile::tempdir().expect("tempdir");
7582 let idx_path = tmp.path().join("p.idx");
7583 let rows = synthetic_index_entries(20_000);
7584 let trailer = [0x5a_u8; 20];
7585 let mut entries = rows.clone();
7586 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer, 20, Some(4))
7587 .expect("write idx");
7588 let idx = read_pack_index(&idx_path).expect("read idx");
7589 assert_eq!(idx.len(), rows.len());
7590 let mut want = rows;
7591 want.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
7592 for (pos, (oid, offset, crc)) in want.iter().enumerate() {
7593 assert_eq!(idx.oid_at(pos), oid.as_bytes(), "row {pos}");
7594 assert_eq!(idx.offset_at(pos).expect("offset"), *offset, "row {pos}");
7595 assert_eq!(idx.crc32_at(pos).expect("crc"), *crc, "row {pos}");
7596 }
7597
7598 let mut bad = vec![(ObjectId::from_bytes(&[1; 32]).expect("oid"), 12, 0)];
7599 assert!(matches!(
7600 write_v2_pack_index_sorting(&idx_path, &mut bad, &trailer, 20, None),
7601 Err(Error::CorruptObject(_))
7602 ));
7603 assert!(matches!(
7604 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer[..19], 20, None),
7605 Err(Error::CorruptObject(_))
7606 ));
7607 }
7608
7609 fn sha1_trailer_for_open_file(file: &mut std::fs::File) -> Result<ObjectId> {
7611 use std::io::{Read, Seek, SeekFrom};
7612 let len = file.metadata().map_err(Error::Io)?.len();
7613 file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
7614 let mut hasher = HashAlgo::Sha1.checksum_hasher();
7615 let mut buf = [0u8; 64 * 1024];
7616 let mut left = len;
7617 while left > 0 {
7618 let chunk = left.min(buf.len() as u64) as usize;
7619 let n = file.read(&mut buf[..chunk]).map_err(Error::Io)?;
7620 if n == 0 {
7621 break;
7622 }
7623 hasher.update(&buf[..n]);
7624 left -= n as u64;
7625 }
7626 let digest = hasher.finalize();
7627 ObjectId::from_bytes(&digest).map_err(|e| Error::InvalidObjectId(format!("{e}")))
7628 }
7629
7630 #[test]
7631 fn cleared_cache_sees_removed_packs_disappear() {
7632 let _guard = pack_cache_test_guard();
7633 clear_pack_cache();
7634 let tmp = tempfile::tempdir().expect("tempdir");
7635 let objects = tmp.path().join("objects");
7636 let (pack, oid, off) = single_blob_pack(b"ephemeral");
7637 install_synthetic_pack(&objects, "gone", &pack, &[(oid, off)]);
7638 read_object_from_packs(&objects, &oid).expect("warm cache");
7639
7640 let pack_path = objects.join("pack/gone.pack");
7641 let idx_path = objects.join("pack/gone.idx");
7642 std::fs::remove_file(&pack_path).expect("remove pack");
7643 std::fs::remove_file(&idx_path).expect("remove idx");
7644 clear_pack_cache();
7645
7646 let indexes = read_local_pack_indexes_cached(&objects).expect("rescan");
7647 assert!(
7648 indexes.is_empty(),
7649 "after clear, listing should not retain removed packs"
7650 );
7651 }
7652
7653 #[test]
7654 fn repack_while_sibling_odb_holds_store_reprepares_on_miss() {
7655 use crate::odb::Odb;
7656
7657 let _guard = pack_cache_test_guard();
7658 clear_pack_cache();
7659 let tmp = tempfile::tempdir().expect("tempdir");
7660 let objects = tmp.path().join("objects");
7661 let (pack_old, oid_old, off_old) = single_blob_pack(b"before");
7662 install_synthetic_pack(&objects, "old", &pack_old, &[(oid_old, off_old)]);
7663 let odb_a = Odb::new(&objects);
7664 let odb_b = Odb::new(&objects);
7665 assert!(
7666 !std::sync::Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
7667 "sibling Odbs must have distinct pack stores"
7668 );
7669 odb_b.read(&oid_old).expect("warm b store");
7670
7671 std::fs::remove_file(objects.join("pack/old.pack")).expect("rm pack");
7672 std::fs::remove_file(objects.join("pack/old.idx")).expect("rm idx");
7673 let (pack_new, oid_new, off_new) = single_blob_pack(b"after");
7674 install_synthetic_pack(&objects, "new", &pack_new, &[(oid_new, off_new)]);
7675 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now())
7676 .expect("bump pack dir mtime for reprepare");
7677 odb_a.invalidate_packs();
7678
7679 let got = odb_b.read(&oid_new).expect("b reprepares after miss");
7681 assert_eq!(got.data.as_slice(), b"after");
7682 }
7683
7684 #[test]
7685 fn fresh_store_does_not_trust_a_listing_left_by_another_store() {
7686 let dir = tempfile::tempdir().expect("tempdir");
7687 let dir = dir.path();
7688 git(dir, &["init", "-q"]);
7689 git(dir, &["commit", "-q", "--allow-empty", "-m", "a"]);
7690 git(dir, &["repack", "-adq"]);
7691 let objects = dir.join(".git/objects");
7692 let pack_names = |indexes: Vec<Arc<PackIndex>>| -> Vec<PathBuf> {
7693 indexes
7694 .into_iter()
7695 .map(|idx| idx.pack_path.clone())
7696 .collect()
7697 };
7698
7699 let first = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7700 let before = crate::pack_store::PackStore::with_context(first, || {
7701 read_local_pack_indexes_cached(&objects)
7702 })
7703 .expect("first listing");
7704
7705 git(dir, &["commit", "-q", "--allow-empty", "-m", "b"]);
7707 git(dir, &["repack", "-adq"]);
7708
7709 let second = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7710 let after = crate::pack_store::PackStore::with_context(second, || {
7711 read_local_pack_indexes_cached(&objects)
7712 })
7713 .expect("second listing");
7714 let (before, after) = (pack_names(before), pack_names(after));
7715 assert_ne!(before, after);
7716 assert!(after.iter().all(|p| p.is_file()), "{after:?}");
7717 }
7718}
7719#[cfg(test)]
7720mod cached_lookup_tests {
7721 use super::*;
7722 use std::fs;
7723 use std::process::Command;
7724 use tempfile::TempDir;
7725
7726 fn git(dir: &Path, args: &[&str]) {
7727 let out = Command::new("git") .current_dir(dir)
7729 .args(args)
7730 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7731 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7732 .env("GIT_AUTHOR_NAME", "t")
7733 .env("GIT_AUTHOR_EMAIL", "t@example.com")
7734 .env("GIT_COMMITTER_NAME", "t")
7735 .env("GIT_COMMITTER_EMAIL", "t@example.com")
7736 .output()
7737 .expect("spawn git");
7738 assert!(
7739 out.status.success(),
7740 "git {:?}: {}",
7741 args,
7742 String::from_utf8_lossy(&out.stderr)
7743 );
7744 }
7745
7746 fn repack_all(dir: &Path) {
7747 git(dir, &["repack", "-a", "-d"]);
7748 }
7749
7750 fn git_stdout(dir: &Path, args: &[&str], stdin: Option<&str>) -> String {
7751 use std::io::Write;
7752 let mut child = Command::new("git") .current_dir(dir)
7754 .args(args)
7755 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7756 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7757 .stdin(std::process::Stdio::piped())
7758 .stdout(std::process::Stdio::piped())
7759 .spawn()
7760 .expect("spawn git");
7761 if let Some(input) = stdin {
7762 child
7763 .stdin
7764 .as_mut()
7765 .expect("stdin")
7766 .write_all(input.as_bytes())
7767 .expect("write stdin");
7768 }
7769 let out = child.wait_with_output().expect("git output");
7770 assert!(out.status.success(), "git {args:?}");
7771 String::from_utf8(out.stdout)
7772 .expect("utf-8")
7773 .trim()
7774 .to_owned()
7775 }
7776
7777 fn oid_of(dir: &Path, rev: &str) -> ObjectId {
7778 ObjectId::from_hex(&git_stdout(dir, &["rev-parse", rev], None)).expect("oid")
7779 }
7780
7781 #[cfg_attr(
7782 not(feature = "full-tests"),
7783 ignore = "starts system git or is heavy; run with --features full-tests"
7784 )]
7785 #[test]
7786 fn packed_commit_header_block_parses_like_the_whole_commit() {
7787 let _guard = pack_cache_test_guard();
7788 clear_pack_cache();
7789 let dir = TempDir::new().unwrap();
7790 let root = dir.path();
7791 git(root, &["init", "-q"]);
7792 fs::write(root.join("f"), "one\n").unwrap();
7793 git(root, &["add", "f"]);
7794 git(root, &["commit", "-qm", "first"]);
7795 fs::write(root.join("f"), "two\n").unwrap();
7796 let long_message = format!("second\n\n{}", "a long body line\n".repeat(200));
7797 git(root, &["commit", "-qam", &long_message]);
7798 git(root, &["tag", "-a", "-m", "annotated", "v1"]);
7799 let signature: String = (0..60)
7800 .map(|i| format!(" signature line {i:03} with some padding text\n"))
7801 .collect();
7802 let signed_body = format!(
7803 "tree {}\nparent {}\nauthor t <t@example.com> 1 +0000\n\
7804 committer t <t@example.com> 2 +0000\ngpgsig -----BEGIN-----\n{signature} \
7805 -----END-----\n\nsigned\n",
7806 oid_of(root, "HEAD^{tree}"),
7807 oid_of(root, "HEAD"),
7808 );
7809 let signed = git_stdout(
7810 root,
7811 &["hash-object", "-t", "commit", "-w", "--stdin"],
7812 Some(&signed_body),
7813 );
7814 git(root, &["update-ref", "refs/heads/signed", &signed]);
7815 git(root, &["repack", "-a", "-d", "--window=0"]);
7816 fs::write(root.join("f"), "three\n").unwrap();
7817 git(root, &["commit", "-qam", "second pack"]);
7818 git(root, &["repack", "-d", "--window=0"]);
7819 fs::write(root.join("f"), "four\n").unwrap();
7820 git(root, &["commit", "-qam", "loose"]);
7821
7822 let objects = root.join(".git/objects");
7823 let mut buf = Vec::new();
7824 for rev in ["HEAD~3", "HEAD~2", "HEAD~1"] {
7825 let oid = oid_of(root, rev);
7826 assert!(
7827 read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7828 "{rev} is a whole packed commit"
7829 );
7830 assert!(buf.ends_with(b"\n\n"), "{rev} ends at the blank line");
7831 let whole = git_stdout(root, &["cat-file", "commit", rev], None);
7832 let whole = crate::objects::CommitRef::parse(whole.as_bytes()).unwrap();
7833 let header = crate::objects::CommitRef::parse(&buf).unwrap();
7834 assert_eq!(header.tree(), whole.tree(), "{rev}");
7835 assert_eq!(header.parents(), whole.parents(), "{rev}");
7836 assert_eq!(header.committer_raw(), whole.committer_raw(), "{rev}");
7837 assert!(header.message_bytes().is_empty(), "{rev}");
7838 }
7839 let signed = ObjectId::from_hex(&signed).unwrap();
7840 for (oid, why) in [
7841 (signed, "headers longer than the inflate budget"),
7842 (oid_of(root, "v1"), "an annotated tag"),
7843 (oid_of(root, "HEAD~1^{tree}"), "a packed tree"),
7844 (oid_of(root, "HEAD"), "a loose commit"),
7845 ] {
7846 assert!(
7847 !read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7848 "{why}"
7849 );
7850 }
7851 }
7852
7853 fn init_repo_with_pack(dir: &Path) -> ObjectId {
7854 git(dir, &["init", "-q"]);
7855 std::fs::write(dir.join("blob.txt"), b"pack cached lookup fixture\n").unwrap();
7856 git(dir, &["add", "blob.txt"]);
7857 git(dir, &["commit", "-qm", "c"]);
7858 repack_all(dir);
7859 let hex = Command::new("git") .current_dir(dir)
7861 .args(["rev-parse", "HEAD^{tree}"])
7862 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7863 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7864 .output()
7865 .unwrap();
7866 let tree_hex = String::from_utf8(hex.stdout).unwrap();
7867 ObjectId::from_hex(tree_hex.trim()).unwrap()
7868 }
7869
7870 fn pack_dir(objects: &Path) -> PathBuf {
7871 objects.join("pack")
7872 }
7873
7874 fn v2_idx_path(objects: &Path) -> PathBuf {
7875 pack_dir(objects)
7876 .read_dir()
7877 .unwrap()
7878 .filter_map(|e| e.ok())
7879 .map(|e| e.path())
7880 .find(|p| p.extension().is_some_and(|x| x == "idx"))
7881 .expect("pack idx")
7882 }
7883
7884 fn convert_pack_idx_to_v1(objects: &Path) -> PathBuf {
7886 let idx_path = v2_idx_path(objects);
7887 let parsed = read_pack_index(&idx_path).expect("v2 idx");
7888 let mut body = Vec::new();
7889 for slot in parsed.fanout {
7890 body.extend_from_slice(&slot.to_be_bytes());
7891 }
7892 for entry in parsed.iter() {
7893 body.extend_from_slice(&(entry.offset().expect("off") as u32).to_be_bytes());
7894 body.extend_from_slice(entry.oid());
7895 }
7896 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7897 rewrite_test_file(&idx_path, &body);
7898 clear_pack_cache();
7899 idx_path
7900 }
7901
7902 fn rewrite_test_file(path: &Path, body: &[u8]) {
7903 let _ = fs::remove_file(path);
7904 fs::write(path, body).unwrap();
7905 }
7906
7907 fn synthetic_fanout_index(entries: &[(Vec<u8>, u64)]) -> PackIndex {
7908 let mut sorted = entries.to_vec();
7909 sorted.sort_by(|a, b| a.0.cmp(&b.0));
7910 let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
7911 let fanout = compute_fanout_from_oid_slices(&slices);
7912 let mut body = Vec::new();
7913 for f in fanout {
7914 body.extend_from_slice(&f.to_be_bytes());
7915 }
7916 for (oid, off) in sorted {
7917 body.extend_from_slice(&(off as u32).to_be_bytes());
7918 body.extend_from_slice(&oid);
7919 }
7920 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7921 parse_pack_index_bytes(Path::new("synthetic.idx"), body, false).expect("v1 idx")
7922 }
7923
7924 #[test]
7925 fn find_offset_fanout_bucket_first_last_and_miss() {
7926 let first = vec![0x05u8; 20];
7927 let mut mid = vec![0x05u8; 20];
7928 mid[19] = 1;
7929 let mut last = vec![0x05u8; 20];
7930 last[19] = 2;
7931 let miss = vec![0x06u8; 20];
7932 let idx =
7933 synthetic_fanout_index(&[(first.clone(), 12), (mid.clone(), 24), (last.clone(), 36)]);
7934
7935 let oid_first = ObjectId::from_bytes(&first).unwrap();
7936 let oid_mid = ObjectId::from_bytes(&mid).unwrap();
7937 let oid_last = ObjectId::from_bytes(&last).unwrap();
7938 let oid_miss = ObjectId::from_bytes(&miss).unwrap();
7939
7940 assert_eq!(idx.find_offset(&oid_first).expect("f"), Some(12));
7941 assert_eq!(idx.find_offset(&oid_mid).expect("m"), Some(24));
7942 assert_eq!(idx.find_offset(&oid_last).expect("l"), Some(36));
7943 assert_eq!(idx.find_offset(&oid_miss).expect("miss"), None);
7944 }
7945
7946 #[cfg_attr(
7947 not(feature = "full-tests"),
7948 ignore = "starts system git or is heavy; run with --features full-tests"
7949 )]
7950 #[test]
7951 fn cached_lookup_hit_v2_idx() {
7952 let _guard = pack_cache_test_guard();
7953 clear_pack_cache();
7954 let dir = TempDir::new().unwrap();
7955 let tree = init_repo_with_pack(dir.path());
7956 let objects = dir.path().join(".git").join("objects");
7957 let got = read_object_from_packs(&objects, &tree).expect("read tree from v2 pack");
7958 assert_eq!(got.kind, ObjectKind::Tree);
7959 let again = read_object_from_packs(&objects, &tree).expect("cached read");
7961 assert_eq!(again.data, got.data);
7962 }
7963
7964 #[cfg_attr(
7965 not(feature = "full-tests"),
7966 ignore = "starts system git or is heavy; run with --features full-tests"
7967 )]
7968 #[test]
7969 fn cached_lookup_hit_v1_idx() {
7970 let _guard = pack_cache_test_guard();
7971 clear_pack_cache();
7972 let dir = TempDir::new().unwrap();
7973 let tree = init_repo_with_pack(dir.path());
7974 let objects = dir.path().join(".git").join("objects");
7975 let v1 = convert_pack_idx_to_v1(&objects);
7976 assert!(pack_index_is_v1(&v1));
7977 let idx = read_pack_index_cached(&v1).expect("parse v1 idx");
7978 let off = idx
7979 .find_offset(&tree)
7980 .expect("fanout lookup in v1 idx")
7981 .expect("off");
7982 assert!(off > 0);
7983 let obj = read_object_from_pack(&idx, &tree).expect("read via v1 index");
7984 assert_eq!(obj.kind, ObjectKind::Tree);
7985 }
7986
7987 #[cfg_attr(
7988 not(feature = "full-tests"),
7989 ignore = "starts system git or is heavy; run with --features full-tests"
7990 )]
7991 #[test]
7992 fn cached_lookup_miss() {
7993 let _guard = pack_cache_test_guard();
7994 clear_pack_cache();
7995 let dir = TempDir::new().unwrap();
7996 init_repo_with_pack(dir.path());
7997 let objects = dir.path().join(".git").join("objects");
7998 let miss = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
7999 assert!(read_object_from_packs(&objects, &miss).is_err());
8000 assert!(
8001 try_read_object_from_local_packs_cached(&objects, &miss, || {
8002 Err(Error::ObjectNotFound(miss.to_hex()))
8003 })
8004 .unwrap()
8005 .is_none()
8006 );
8007 }
8008
8009 #[cfg_attr(
8010 not(feature = "full-tests"),
8011 ignore = "starts system git or is heavy; run with --features full-tests"
8012 )]
8013 #[test]
8014 fn cached_lookup_loose_only() {
8015 let _guard = pack_cache_test_guard();
8016 clear_pack_cache();
8017 let dir = TempDir::new().unwrap();
8018 git(dir.path(), &["init", "-q"]);
8019 let out = Command::new("git") .current_dir(dir.path())
8021 .args(["hash-object", "-w", "--stdin"])
8022 .env("GIT_CONFIG_GLOBAL", "/dev/null")
8023 .env("GIT_CONFIG_SYSTEM", "/dev/null")
8024 .stdin(std::process::Stdio::piped())
8025 .stdout(std::process::Stdio::piped())
8026 .spawn()
8027 .and_then(|mut c| {
8028 use std::io::Write;
8029 c.stdin
8030 .as_mut()
8031 .unwrap()
8032 .write_all(b"loose-only\n")
8033 .unwrap();
8034 c.wait_with_output()
8035 })
8036 .unwrap();
8037 assert!(out.status.success());
8038 let oid = ObjectId::from_hex(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
8039 let objects = dir.path().join(".git").join("objects");
8040 assert!(try_read_object_from_local_packs_cached(&objects, &oid, || {
8041 Err(Error::ObjectNotFound(oid.to_hex()))
8042 })
8043 .unwrap()
8044 .is_none());
8045 }
8046
8047 #[cfg_attr(
8048 not(feature = "full-tests"),
8049 ignore = "starts system git or is heavy; run with --features full-tests"
8050 )]
8051 #[test]
8052 fn cached_lookup_multiple_packs() {
8053 let _guard = pack_cache_test_guard();
8054 clear_pack_cache();
8055 let dir = TempDir::new().unwrap();
8056 git(dir.path(), &["init", "-q"]);
8057 std::fs::write(dir.path().join("a.txt"), b"a").unwrap();
8058 git(dir.path(), &["add", "a.txt"]);
8059 git(dir.path(), &["commit", "-qm", "a"]);
8060 repack_all(dir.path());
8061 let oid_a = ObjectId::from_hex(
8062 String::from_utf8(
8063 Command::new("git") .current_dir(dir.path())
8065 .args(["rev-parse", "HEAD:a.txt"])
8066 .env("GIT_CONFIG_GLOBAL", "/dev/null")
8067 .env("GIT_CONFIG_SYSTEM", "/dev/null")
8068 .output()
8069 .unwrap()
8070 .stdout,
8071 )
8072 .unwrap()
8073 .trim(),
8074 )
8075 .unwrap();
8076 std::fs::write(dir.path().join("b.txt"), b"b").unwrap();
8077 git(dir.path(), &["add", "b.txt"]);
8078 git(dir.path(), &["commit", "-qm", "b"]);
8079 git(dir.path(), &["repack", "-d"]);
8080 let oid_b = ObjectId::from_hex(
8081 String::from_utf8(
8082 Command::new("git") .current_dir(dir.path())
8084 .args(["rev-parse", "HEAD:b.txt"])
8085 .env("GIT_CONFIG_GLOBAL", "/dev/null")
8086 .env("GIT_CONFIG_SYSTEM", "/dev/null")
8087 .output()
8088 .unwrap()
8089 .stdout,
8090 )
8091 .unwrap()
8092 .trim(),
8093 )
8094 .unwrap();
8095 let objects = dir.path().join(".git").join("objects");
8096 let pack_count = pack_dir(&objects)
8097 .read_dir()
8098 .unwrap()
8099 .filter_map(|e| e.ok())
8100 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
8101 .count();
8102 assert!(
8103 pack_count >= 2,
8104 "expected multiple pack files, got {pack_count}"
8105 );
8106 let obj_a = read_object_from_packs(&objects, &oid_a).expect("pack a");
8107 let obj_b = read_object_from_packs(&objects, &oid_b).expect("pack b");
8108 assert_eq!(obj_a.kind, ObjectKind::Blob);
8109 assert_eq!(obj_b.kind, ObjectKind::Blob);
8110 }
8111
8112 #[cfg_attr(
8113 not(feature = "full-tests"),
8114 ignore = "starts system git or is heavy; run with --features full-tests"
8115 )]
8116 #[test]
8117 fn packed_delta_base_oid_uses_cached_index() {
8118 let _guard = pack_cache_test_guard();
8119 clear_pack_cache();
8120 let dir = TempDir::new().unwrap();
8121 init_repo_with_pack(dir.path());
8122 let objects = dir.path().join(".git").join("objects");
8123 let miss = ObjectId::from_hex("0101010101010101010101010101010101010101").unwrap();
8124 assert!(packed_delta_base_oid(&objects, &miss).unwrap().is_none());
8125 }
8126
8127 #[test]
8128 fn content_addressed_pack_bytes_skip_stat_revalidation() {
8129 let _guard = pack_cache_test_guard();
8130 clear_pack_cache();
8131 let dir = tempfile::tempdir().expect("tempdir");
8132 let pack_path = dir
8133 .path()
8134 .join("pack-1111111111111111111111111111111111111111.pack");
8135 let v1 = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
8136 std::fs::write(&pack_path, v1).expect("write v1");
8137 let cached = read_pack_bytes_cached(&pack_path).expect("prime cache");
8138 assert_eq!(&cached[..], v1);
8139
8140 let v2 = b"PACK-replaced-by-repack";
8141 std::fs::write(&pack_path, v2).expect("overwrite on disk");
8142 let again = read_pack_bytes_cached(&pack_path).expect("cached without stat");
8143 assert_eq!(
8144 &again[..],
8145 v1,
8146 "content-addressed name pins cached bytes in-process"
8147 );
8148 }
8149
8150 #[test]
8151 fn temporary_pack_bytes_served_from_cache_until_cleared() {
8152 let _guard = pack_cache_test_guard();
8153 clear_pack_cache();
8154 let dir = tempfile::tempdir().expect("tempdir");
8155 for pack_path in [
8156 dir.path().join("tmp_pack_abc123.pack"),
8157 dir.path().join("pack-custom.pack"),
8158 ] {
8159 clear_pack_cache();
8160 let v1 = b"PACK-temp-v1";
8161 std::fs::write(&pack_path, v1).expect("write v1");
8162 let _ = read_pack_bytes_cached(&pack_path).expect("prime cache");
8163
8164 let v2 = b"PACK-temp-v2-longer-body";
8165 std::fs::write(&pack_path, v2).expect("overwrite pack");
8166 let stale = read_pack_bytes_cached(&pack_path).expect("cache hit");
8167 assert_eq!(
8168 &stale[..],
8169 v1,
8170 "{} serves cached bytes without stat on hit",
8171 pack_path.display()
8172 );
8173 clear_pack_cache();
8174 let fresh = read_pack_bytes_cached(&pack_path).expect("after clear");
8175 assert_eq!(
8176 &fresh[..],
8177 v2,
8178 "{} reloads from disk after clear",
8179 pack_path.display()
8180 );
8181 }
8182 }
8183 #[cfg_attr(
8184 not(feature = "full-tests"),
8185 ignore = "starts system git or is heavy; run with --features full-tests"
8186 )]
8187 #[test]
8188 fn system_git_repack_requires_clear_pack_cache_for_fresh_listing() {
8189 let _guard = pack_cache_test_guard();
8190 clear_pack_cache();
8191 let dir = TempDir::new().unwrap();
8192 init_repo_with_pack(dir.path());
8193 let objects = dir.path().join(".git").join("objects");
8194 read_local_pack_indexes_cached(&objects).expect("warm listing");
8195 for entry in pack_dir(&objects).read_dir().unwrap().flatten() {
8196 let path = entry.path();
8197 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
8198 fs::remove_file(path).unwrap();
8199 }
8200 }
8201 clear_pack_cache();
8202 let indexes =
8203 read_local_pack_indexes_cached(&objects).expect("list after repack-like delete");
8204 assert!(
8205 indexes.is_empty(),
8206 "callers must clear_pack_cache after removing pack files"
8207 );
8208 }
8209
8210 fn test_oid(n: u8) -> ObjectId {
8211 ObjectId::from_hex(&format!("{n:040x}")).unwrap()
8212 }
8213
8214 fn max_delta_map_chain_edges(map: &HashMap<ObjectId, ObjectId>) -> usize {
8215 let value_set: HashSet<ObjectId> = map.values().copied().collect();
8216 let tips: Vec<ObjectId> = map
8217 .keys()
8218 .copied()
8219 .filter(|k| !value_set.contains(k))
8220 .collect();
8221 let mut max = 0usize;
8222 for tip in tips {
8223 let mut edges = 0usize;
8224 let mut cur = tip;
8225 let mut seen = HashSet::new();
8226 while seen.insert(cur) {
8227 let Some(&b) = map.get(&cur) else {
8228 break;
8229 };
8230 edges += 1;
8231 cur = b;
8232 }
8233 max = max.max(edges);
8234 }
8235 max
8236 }
8237
8238 #[test]
8239 fn apply_delta_depth_limit_hits_exact_cap_on_seventy_edge_chain() {
8240 let n = 70usize;
8241 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
8242 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
8243 for i in 1..n {
8244 map.insert(oids[i], oids[i - 1]);
8245 }
8246 let original = map.clone();
8247 apply_delta_depth_limit(&mut map, 50);
8248 assert_eq!(max_delta_map_chain_edges(&map), 50);
8249 let preserved = map
8250 .iter()
8251 .filter(|(t, b)| original.get(t) == Some(b))
8252 .count();
8253 assert!(
8254 preserved > 40,
8255 "non-cut edges should keep original bases (preserved {preserved})"
8256 );
8257 }
8258
8259 #[test]
8260 fn delta_cycle_breaks_drops_smallest_oid_of_each_cycle_only() {
8261 let o = |n: u8| test_oid(n);
8262 let map = HashMap::from([
8265 (o(1), o(2)),
8266 (o(2), o(1)),
8267 (o(3), o(5)),
8268 (o(4), o(3)),
8269 (o(5), o(4)),
8270 (o(6), o(4)),
8271 (o(8), o(7)),
8272 ]);
8273 let mut keys: Vec<ObjectId> = map.keys().copied().collect();
8274 for order in [false, true] {
8275 keys.sort();
8276 if order {
8277 keys.reverse();
8278 }
8279 let mut breaks = delta_cycle_breaks(keys.iter().copied(), |oid| map.get(oid).copied());
8280 breaks.sort();
8281 assert_eq!(breaks, vec![o(1), o(3)], "independent of walk order");
8282 }
8283 }
8284
8285 #[test]
8286 fn delta_cycle_breaks_handles_self_delta_and_acyclic_maps() {
8287 let a = test_oid(9);
8288 assert_eq!(delta_cycle_breaks([a], |_| Some(a)), vec![a]);
8289 let chain: HashMap<ObjectId, ObjectId> =
8290 (1..60u8).map(|i| (test_oid(i), test_oid(i - 1))).collect();
8291 assert!(delta_cycle_breaks(chain.keys().copied(), |o| chain.get(o).copied()).is_empty());
8292 }
8293
8294 #[test]
8295 fn apply_delta_depth_limit_breaks_cycles_shorter_than_depth() {
8296 let (a, b, c, d) = (test_oid(1), test_oid(2), test_oid(3), test_oid(4));
8297 let mut map = HashMap::from([(a, b), (b, a), (c, b), (d, c)]);
8299 apply_delta_depth_limit(&mut map, 50);
8300 assert_eq!(map, HashMap::from([(b, a), (c, b), (d, c)]));
8301 assert_eq!(max_delta_map_chain_edges(&map), 3);
8302 }
8303
8304 #[test]
8305 fn apply_delta_depth_limit_leaves_short_chains_untouched() {
8306 let a = test_oid(1);
8307 let b = test_oid(2);
8308 let c = test_oid(3);
8309 let mut map = HashMap::from([(b, a), (c, b)]);
8310 apply_delta_depth_limit(&mut map, 50);
8311 assert_eq!(map.get(&b), Some(&a));
8312 assert_eq!(map.get(&c), Some(&b));
8313 assert_eq!(max_delta_map_chain_edges(&map), 2);
8314 }
8315
8316 #[test]
8317 fn apply_delta_depth_limit_caps_long_chain_and_preserves_blobs() {
8318 let n = 120usize;
8319 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
8320 let mut blobs: HashMap<ObjectId, Vec<u8>> = HashMap::new();
8321 blobs.insert(oids[0], b"base".to_vec());
8322 for i in 1..n {
8323 let mut next = blobs[&oids[i - 1]].clone();
8324 next.push(i as u8);
8325 blobs.insert(oids[i], next);
8326 }
8327 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
8328 for i in 1..n {
8329 map.insert(oids[i], oids[i - 1]);
8330 }
8331 let original = map.clone();
8332 apply_delta_depth_limit(&mut map, 50);
8333 assert_eq!(
8334 max_delta_map_chain_edges(&map),
8335 50,
8336 "long chain must cap at exactly max_depth edges"
8337 );
8338 let preserved = map
8339 .iter()
8340 .filter(|(target, base)| original.get(target) == Some(base))
8341 .count();
8342 assert!(
8343 preserved > 60,
8344 "segment links should stay on original bases (preserved {preserved} of {})",
8345 map.len()
8346 );
8347
8348 use crate::delta_encode::encode_prefix_extension_delta;
8349 use crate::unpack_objects::apply_delta;
8350
8351 for (&target, &base) in &map {
8352 let delta = encode_prefix_extension_delta(&blobs[&base], &blobs[&target]).unwrap();
8353 let got = apply_delta(&blobs[&base], &delta).unwrap();
8354 assert_eq!(got, blobs[&target]);
8355 }
8356 }
8357
8358 #[cfg_attr(
8359 not(feature = "full-tests"),
8360 ignore = "starts system git or is heavy; run with --features full-tests"
8361 )]
8362 #[test]
8363 fn v2_idx_crc_matches_crc32fast_over_entry_bytes() {
8364 let _guard = pack_cache_test_guard();
8365 clear_pack_cache();
8366 let dir = TempDir::new().unwrap();
8367 let tree = init_repo_with_pack(dir.path());
8368 let objects = dir.path().join(".git").join("objects");
8369 let idx_path = v2_idx_path(&objects);
8370 let idx = read_pack_index(&idx_path).expect("read idx");
8371 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
8372 for entry in idx.iter() {
8373 let crc = entry.crc32().expect("v2 idx entry crc");
8374 let off = entry.offset().expect("entry off");
8375 let start = off as usize;
8376 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
8377 .expect("entry end")
8378 .expect("end");
8379 assert_eq!(crc32fast::hash(&pack_bytes[start..end]), crc);
8380 }
8381 let _ = tree;
8382 }
8383
8384 #[cfg_attr(
8385 not(feature = "full-tests"),
8386 ignore = "starts system git or is heavy; run with --features full-tests"
8387 )]
8388 #[test]
8389 fn packed_full_object_slice_returns_on_disk_bytes() {
8390 let _guard = pack_cache_test_guard();
8391 clear_pack_cache();
8392 let dir = TempDir::new().unwrap();
8393 let tree = init_repo_with_pack(dir.path());
8394 let objects = dir.path().join(".git").join("objects");
8395 let idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
8396 let pack_bytes = std::fs::read(&idx.pack_path).expect("pack");
8397 let off = idx.find_offset(&tree).expect("tree in pack").expect("off");
8398 let start = off as usize;
8399 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
8400 .expect("entry end")
8401 .expect("end");
8402 let expected = &pack_bytes[start..end];
8403 let slice = packed_full_object_slice(&objects, &tree)
8404 .expect("lookup")
8405 .expect("full tree slice");
8406 assert_eq!(slice, expected);
8407 }
8408
8409 fn touch_mtime(path: &Path, when: filetime::FileTime) {
8410 filetime::set_file_mtime(path, when).expect("set mtime");
8411 }
8412
8413 #[cfg_attr(
8414 not(feature = "full-tests"),
8415 ignore = "starts system git or is heavy; run with --features full-tests"
8416 )]
8417 #[test]
8418 fn packed_full_object_slice_crc_mismatch_falls_back_to_redundant_pack() {
8419 let _guard = pack_cache_test_guard();
8420 clear_pack_cache();
8421 let dir = TempDir::new().unwrap();
8422 let tree = init_repo_with_pack(dir.path());
8423 let objects = dir.path().join(".git").join("objects");
8424 let pack_dir = pack_dir(&objects);
8425 let primary_idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
8426 let primary_pack = primary_idx.pack_path.clone();
8427 let primary_idx_path = primary_idx.idx_path.clone();
8428
8429 let redundant_pack = pack_dir.join("pack-redundant.pack");
8430 let redundant_idx = pack_dir.join("pack-redundant.idx");
8431 fs::copy(&primary_pack, &redundant_pack).expect("copy pack");
8432 fs::copy(&primary_idx_path, &redundant_idx).expect("copy idx");
8433
8434 let older = filetime::FileTime::from_unix_time(1_700_000_000, 0);
8435 let newer = filetime::FileTime::from_unix_time(1_700_000_100, 0);
8436 touch_mtime(&primary_pack, older);
8437 touch_mtime(&primary_idx_path, older);
8438 touch_mtime(&redundant_pack, newer);
8439 touch_mtime(&redundant_idx, newer);
8440 clear_pack_cache();
8441
8442 let off = primary_idx
8443 .find_offset(&tree)
8444 .expect("tree offset")
8445 .expect("off");
8446 let start = off as usize;
8447 let end = super::pack_entry_raw_end(&primary_idx, &fs::read(&primary_pack).unwrap(), off)
8448 .expect("entry end")
8449 .expect("end");
8450 let flip = start + (end - start) / 2;
8451 let mut corrupt = fs::read(&redundant_pack).expect("read redundant");
8452 corrupt[flip] ^= 0x01;
8453 rewrite_test_file(&redundant_pack, &corrupt);
8454 clear_pack_cache();
8455
8456 let recorded_crc = read_pack_index(&redundant_idx)
8457 .expect("redundant idx")
8458 .crc32_for_pack_offset(off)
8459 .expect("crc")
8460 .expect("crc value");
8461 let corrupt_slice = &corrupt[start..end];
8462 assert_ne!(
8463 crc32fast::hash(corrupt_slice),
8464 recorded_crc,
8465 "test must flip bytes without updating idx CRC"
8466 );
8467 assert!(
8468 packed_full_object_slice(&objects, &tree)
8469 .expect("lookup")
8470 .is_some(),
8471 "valid redundant pack should serve the slice after CRC rejects corrupt copy"
8472 );
8473
8474 let obj = read_object_from_packs(&objects, &tree).expect("read tree");
8475 assert_eq!(obj.kind, ObjectKind::Tree);
8476 let slice = packed_full_object_slice(&objects, &tree)
8477 .expect("lookup")
8478 .expect("slice");
8479 let good_pack = fs::read(&primary_pack).expect("primary pack");
8480 assert_eq!(&slice, &good_pack[start..end]);
8481 }
8482
8483 #[test]
8484 fn wanted_index_entries_lookups_match_a_full_scan() {
8485 let tmp = TempDir::new().unwrap();
8486 let bare = grit_test_support::git_packed_history(tmp.path(), 30);
8487 let objects = bare.join("objects");
8488 let indexes = read_local_pack_indexes_cached(&objects).unwrap();
8489 assert_eq!(indexes.len(), 1);
8490 let idx = &indexes[0];
8491 let all: Vec<(ObjectId, u64)> = idx
8492 .iter()
8493 .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
8494 .collect();
8495 assert!(all.len() > 64);
8496 let absent = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
8497 let sorted = |mut v: Vec<(ObjectId, u64)>| {
8498 v.sort();
8499 v
8500 };
8501
8502 let few: HashSet<ObjectId> = [all[3].0, all[all.len() - 1].0, absent].into();
8503 let looked_up = wanted_index_entries(idx, few.iter(), |oid| few.contains(oid)).unwrap();
8504 assert_eq!(sorted(looked_up), sorted(vec![all[3], all[all.len() - 1]]));
8505
8506 let many: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).chain([absent]).collect();
8507 let scanned = wanted_index_entries(idx, many.iter(), |oid| many.contains(oid)).unwrap();
8508 assert_eq!(sorted(scanned), sorted(all));
8509
8510 let slots = locate_packed_objects(&objects, &few.iter().copied().collect()).unwrap();
8511 assert_eq!(slots.len(), 2);
8512 assert!(!slots.contains_key(&absent));
8513 }
8514}