1#![allow(dead_code)]
7
8pub(crate) use crate::pack_index::parse_pack_index_bytes;
9pub use crate::pack_index::{PackIndex, PackIndexEntryRef};
10
11#[cfg(test)]
12use crate::pack_index::compute_fanout_from_oid_slices;
13
14use crate::error::{Error, Result};
15use crate::git_path::CanonicalizeExt;
16use crate::hash::{hash_object, verify_trailer};
17use crate::objects::{HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind};
18use crate::odb::BoundedRead;
19pub use crate::pack_map::PackData;
20use crate::pack_map::ResidentBudget;
21use crate::unpack_objects::{apply_delta_into, apply_delta_prefix_into};
22use crate::zlib_inflate::ZlibInflateScratch;
23use std::cell::RefCell;
24use std::collections::{BTreeMap, HashMap, HashSet};
25use std::fs;
26use std::io;
27use std::path::{Path, PathBuf};
28use std::sync::Arc;
29use std::time::SystemTime;
30
31#[derive(Debug, Clone, Default)]
33pub struct LocalPackInfo {
34 pub pack_count: usize,
36 pub object_count: usize,
38 pub size_bytes: u64,
40 pub object_ids: HashSet<ObjectId>,
42}
43
44pub fn read_local_pack_indexes(objects_dir: &Path) -> Result<Vec<PackIndex>> {
51 let pack_dir = objects_dir.join("pack");
52 let rd = match fs::read_dir(&pack_dir) {
53 Ok(rd) => rd,
54 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
55 Err(err) => return Err(Error::Io(err)),
56 };
57
58 let mut out = Vec::new();
59 for entry in rd {
60 let entry = entry.map_err(Error::Io)?;
61 let path = entry.path();
62 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
63 continue;
64 }
65 if let Ok(idx) = read_pack_index(&path) {
66 if !idx.pack_path.is_file() {
69 continue;
70 }
71 out.push(idx);
72 }
73 }
74 Ok(out)
75}
76
77struct PackReadDamage {
88 pack: PathBuf,
89 offset: u64,
90 err: Error,
91}
92
93fn finish_pack_object_lookup(oid: &ObjectId, last: Option<PackReadDamage>) -> Error {
94 match last {
95 Some(d) => Error::wrap_packed_read_failure(*oid, d.pack, d.offset, d.err),
96 None => Error::ObjectNotFound(oid.to_hex()),
97 }
98}
99
100fn wrap_pack_cache_read_err(
101 objects_dir: &Path,
102 idx: &PackIndex,
103 oid: &ObjectId,
104 err: Error,
105) -> Error {
106 let offset = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)
107 .ok()
108 .flatten()
109 .unwrap_or(0);
110 Error::wrap_packed_read_failure(*oid, idx.pack_path.clone(), offset, err)
111}
112
113pub(crate) mod pack_cache {
114 use super::{read_pack_index_no_verify, Error, ObjectKind, PackIndex, Result};
115 use crate::pack_map::{fingerprint_from_file, PackData, PackFingerprint};
116 use std::collections::{HashMap, HashSet, VecDeque};
117 use std::fs;
118 use std::hash::{Hash, Hasher};
119 use std::io;
120 use std::path::{Path, PathBuf};
121 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
122 use std::sync::{Arc, Mutex, Weak};
123 use std::time::SystemTime;
124
125 const DELTA_LRU_SHARD_COUNT: usize = 32;
126 const BOUNDED_PACK_READERS: usize = 32;
129
130 type ThreadLocalDirListing = Option<(PathBuf, SystemTime, Vec<Arc<PackIndex>>)>;
131
132 #[derive(Clone)]
133 enum CachedPackBytes {
134 Retained(Arc<PackData>),
135 Positioned(Weak<PackData>),
136 }
137
138 impl CachedPackBytes {
139 fn get(&self) -> Option<Arc<PackData>> {
140 match self {
141 Self::Retained(bytes) => Some(Arc::clone(bytes)),
142 Self::Positioned(bytes) => bytes.upgrade(),
143 }
144 }
145 }
146
147 thread_local! {
149 static PACK_DIR_INDEXES: std::cell::RefCell<ThreadLocalDirListing> =
150 const { std::cell::RefCell::new(None) };
151 static PACK_HIT_INDEX: std::cell::RefCell<Option<(PathBuf, Arc<PackIndex>)>> =
152 const { std::cell::RefCell::new(None) };
153 static PACK_BYTES: std::cell::RefCell<HashMap<PathBuf, CachedPackBytes>> =
154 std::cell::RefCell::new(HashMap::new());
155 static PACK_INDEXES: std::cell::RefCell<HashMap<PathBuf, Arc<PackIndex>>> =
156 std::cell::RefCell::new(HashMap::new());
157 static PACK_IDS: std::cell::RefCell<hashbrown::HashMap<std::ffi::OsString, u32>> =
160 std::cell::RefCell::new(hashbrown::HashMap::new());
161 static PACK_COUNT_VALIDATED: std::cell::RefCell<HashSet<PathBuf>> =
162 std::cell::RefCell::new(HashSet::new());
163 static PACK_DIR_LISTING_TRUSTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
164 static THREAD_WARM_EPOCH: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
166 static THREAD_WARM_STORE: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
168 static PACK_MONOTONIC_CURSOR: std::cell::RefCell<Option<(PathBuf, usize)>> =
169 const { std::cell::RefCell::new(None) };
170 static SINGLE_READER_MEMO: std::cell::RefCell<Option<SingleReaderMemo>> =
171 const { std::cell::RefCell::new(None) };
172 static STORE_TLS_SNAPSHOTS: std::cell::RefCell<HashMap<u64, PackThreadLocalSnapshot>> =
173 std::cell::RefCell::new(HashMap::new());
174 static ACTIVE_TLS_STORE_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
175 static ACTIVE_STORE_LIFETIME: std::cell::RefCell<Option<Weak<()>>> =
176 const { std::cell::RefCell::new(None) };
177 static SKIP_STORE_TLS_RELEASE: std::cell::Cell<bool> =
179 const { std::cell::Cell::new(false) };
180 static ALTERNATE_PACK_READ_STORE: std::cell::RefCell<
183 Option<std::sync::Arc<crate::pack_store::PackStore>>,
184 > = const { std::cell::RefCell::new(None) };
185 static PRIMARY_EMPTY: std::cell::Cell<Option<bool>> =
187 const { std::cell::Cell::new(None) };
188 }
189
190 struct AlternatePackReadStoreGuard {
191 previous: Option<std::sync::Arc<crate::pack_store::PackStore>>,
192 }
193
194 impl AlternatePackReadStoreGuard {
195 fn install(store: std::sync::Arc<crate::pack_store::PackStore>) -> Option<Self> {
196 ALTERNATE_PACK_READ_STORE.with(|cell| {
197 if cell
198 .borrow()
199 .as_ref()
200 .is_some_and(|active| std::sync::Arc::ptr_eq(active, &store))
201 {
202 return None;
203 }
204 let previous = cell.borrow_mut().replace(store);
205 Some(Self { previous })
206 })
207 }
208 }
209
210 impl Drop for AlternatePackReadStoreGuard {
211 fn drop(&mut self) {
212 ALTERNATE_PACK_READ_STORE.with(|cell| {
213 *cell.borrow_mut() = self.previous.take();
214 });
215 }
216 }
217
218 pub(crate) fn clear_alternate_pack_read_store() {
219 ALTERNATE_PACK_READ_STORE.with(|cell| *cell.borrow_mut() = None);
220 }
221
222 pub(crate) fn with_alternate_pack_store<R>(
223 store: std::sync::Arc<crate::pack_store::PackStore>,
224 f: impl FnOnce() -> R,
225 ) -> R {
226 if crate::pack_store::PackStore::has_context()
227 && !crate::pack_store::PackStore::read_context_matches_objects_dir(store.objects_dir())
228 {
229 ALTERNATE_PACK_READ_STORE.with(|cell| {
230 let mut slot = cell.borrow_mut();
231 if slot
232 .as_ref()
233 .is_none_or(|active| !std::sync::Arc::ptr_eq(active, &store))
234 {
235 *slot = Some(store);
236 }
237 });
238 return f();
239 }
240 let _guard = AlternatePackReadStoreGuard::install(store);
241 f()
242 }
243
244 pub(crate) fn alternate_read_store() -> Option<std::sync::Arc<crate::pack_store::PackStore>> {
245 ALTERNATE_PACK_READ_STORE.with(|cell| cell.borrow().clone())
246 }
247
248 pub(crate) fn catalog_store() -> std::sync::Arc<crate::pack_store::PackStore> {
249 alternate_read_store().unwrap_or_else(crate::pack_store::PackStore::active_for_reads)
250 }
251
252 pub(crate) fn sync_thread_local_for_objects_dir(objects_dir: &Path) {
253 if pack_thread_locals_apply_to(objects_dir) {
254 sync_thread_local_with_store(&catalog_store());
255 }
256 }
257
258 pub(crate) fn pack_thread_locals_apply_to(objects_dir: &Path) -> bool {
261 if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
262 return true;
263 }
264 alternate_read_store().is_some_and(|store| store.objects_dir() == objects_dir)
265 }
266
267 pub(crate) fn primary_has_verified_empty_listing(objects_dir: &Path) -> bool {
269 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
270 return false;
271 }
272 PRIMARY_EMPTY.with(|cell| {
273 if let Some(hit) = cell.get() {
274 return hit;
275 }
276 let hit = trusted_verified_empty_pack_listing(objects_dir);
277 cell.set(Some(hit));
278 hit
279 })
280 }
281
282 pub(crate) fn set_skip_store_tls_release(skip: bool) {
283 SKIP_STORE_TLS_RELEASE.set(skip);
284 }
285
286 #[derive(Clone)]
287 struct PackThreadLocalSnapshot {
288 dir_indexes: ThreadLocalDirListing,
289 hit_index: Option<(PathBuf, Arc<PackIndex>)>,
290 pack_bytes: HashMap<PathBuf, CachedPackBytes>,
291 pack_indexes: HashMap<PathBuf, Arc<PackIndex>>,
292 pack_ids: hashbrown::HashMap<std::ffi::OsString, u32>,
293 count_validated: HashSet<PathBuf>,
294 listing_trusted: bool,
295 warm_epoch: u64,
296 warm_store: u64,
297 monotonic_cursor: Option<(PathBuf, usize)>,
298 single_reader_memo: Option<SingleReaderMemo>,
299 store_lifetime: Weak<()>,
300 }
301
302 fn capture_tls_snapshot(store_lifetime: Weak<()>) -> PackThreadLocalSnapshot {
303 PackThreadLocalSnapshot {
304 dir_indexes: PACK_DIR_INDEXES.with(|cell| cell.borrow().clone()),
305 hit_index: PACK_HIT_INDEX.with(|cell| cell.borrow().clone()),
306 pack_bytes: PACK_BYTES.with(|cell| cell.borrow().clone()),
307 pack_indexes: PACK_INDEXES.with(|cell| cell.borrow().clone()),
308 pack_ids: PACK_IDS.with(|cell| cell.borrow().clone()),
309 count_validated: PACK_COUNT_VALIDATED.with(|cell| cell.borrow().clone()),
310 listing_trusted: PACK_DIR_LISTING_TRUSTED.get(),
311 warm_epoch: THREAD_WARM_EPOCH.get(),
312 warm_store: THREAD_WARM_STORE.get(),
313 monotonic_cursor: PACK_MONOTONIC_CURSOR.with(|cell| cell.borrow().clone()),
314 single_reader_memo: SINGLE_READER_MEMO.with(|cell| cell.borrow().clone()),
315 store_lifetime,
316 }
317 }
318
319 fn apply_tls_snapshot(snap: PackThreadLocalSnapshot) {
320 PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = snap.dir_indexes);
321 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = snap.hit_index);
322 PACK_BYTES.with(|cell| *cell.borrow_mut() = snap.pack_bytes);
323 PACK_INDEXES.with(|cell| *cell.borrow_mut() = snap.pack_indexes);
324 PACK_IDS.with(|cell| *cell.borrow_mut() = snap.pack_ids);
325 PACK_COUNT_VALIDATED.with(|cell| *cell.borrow_mut() = snap.count_validated);
326 PACK_DIR_LISTING_TRUSTED.set(snap.listing_trusted);
327 THREAD_WARM_EPOCH.set(snap.warm_epoch);
328 THREAD_WARM_STORE.set(snap.warm_store);
329 PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = snap.monotonic_cursor);
330 SINGLE_READER_MEMO.with(|cell| *cell.borrow_mut() = snap.single_reader_memo);
331 ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = Some(snap.store_lifetime));
332 }
333
334 fn clear_live_tls_pack_mappings() {
335 PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = None);
336 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
337 PACK_BYTES.with(|cell| cell.borrow_mut().clear());
338 PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
339 PACK_IDS.with(|cell| cell.borrow_mut().clear());
340 PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
341 PACK_DIR_LISTING_TRUSTED.set(false);
342 PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = None);
343 forget_single_reader_memo();
344 super::clear_pack_read_info_cache();
345 }
346
347 fn seed_tls_from_store_catalog(store: &crate::pack_store::PackStore) {
348 PACK_BYTES.with(|cell| cell.borrow_mut().clear());
349 PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
350 PACK_IDS.with(|cell| cell.borrow_mut().clear());
351 PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
352 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
353 forget_single_reader_memo();
354 super::clear_pack_read_info_cache();
355 let objects_dir = store.objects_dir();
356 let pack_dir = objects_dir.join("pack");
357 store.with_catalog_read(|g| {
358 let listing = g
359 .by_dir
360 .get(&pack_dir)
361 .map(|cached| (pack_dir.clone(), cached.dir_mtime, cached.indexes.clone()));
362 PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = listing);
363 for (path, entry) in &g.by_pack {
364 remember_pack_bytes(path, &entry.bytes);
365 }
366 for (path, entry) in &g.by_idx {
367 remember_index(path, &entry.idx);
368 }
369 PACK_IDS.with(|cell| {
370 cell.borrow_mut().extend(
371 g.pack_ids
372 .iter()
373 .map(|(k, v)| (k.as_os_str().to_owned(), *v)),
374 );
375 });
376 PACK_COUNT_VALIDATED.with(|cell| {
377 cell.borrow_mut()
378 .extend(g.validated_pack_counts.iter().cloned());
379 });
380 PACK_DIR_LISTING_TRUSTED.set(true);
381 THREAD_WARM_EPOCH.set(store.cache_epoch());
382 THREAD_WARM_STORE.set(store_thread_local_key(store));
383 ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = Some(store.lifetime_weak()));
384 });
385 }
386
387 pub(crate) fn drop_store_tls_snapshot(store_id: u64) {
388 STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().remove(&store_id));
389 }
390
391 fn release_store_thread_local_inner(store_id: u64) {
392 if SKIP_STORE_TLS_RELEASE.get() {
393 return;
394 }
395 drop_store_tls_snapshot(store_id);
396 if ACTIVE_TLS_STORE_ID.get() != store_id && THREAD_WARM_STORE.get() != store_id {
397 return;
398 }
399 clear_live_tls_pack_mappings();
400 if ACTIVE_TLS_STORE_ID.get() == store_id {
401 ACTIVE_TLS_STORE_ID.set(0);
402 }
403 if THREAD_WARM_STORE.get() == store_id {
404 THREAD_WARM_STORE.set(0);
405 THREAD_WARM_EPOCH.set(0);
406 }
407 ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = None);
408 }
409
410 pub(crate) fn release_store_thread_local(store_id: u64) {
412 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
413 release_store_thread_local_inner(store_id);
414 }));
415 }
416
417 pub(crate) fn switch_tls_for_store(store: &crate::pack_store::PackStore) {
418 if !crate::pack_store::PackStore::has_context() {
419 return;
420 }
421 let new_id = store.id();
422 let epoch = store.cache_epoch();
423 if ACTIVE_TLS_STORE_ID.get() == new_id
428 && THREAD_WARM_EPOCH.get() == epoch
429 && THREAD_WARM_STORE.get() == new_id
430 {
431 return;
432 }
433 let old_id = ACTIVE_TLS_STORE_ID.get();
434 if old_id != 0 && old_id != new_id {
435 let outgoing_alive = ACTIVE_STORE_LIFETIME.with(|cell| {
436 cell.borrow()
437 .as_ref()
438 .and_then(|weak| weak.upgrade())
439 .is_some()
440 });
441 if outgoing_alive {
442 if let Some(lifetime) = ACTIVE_STORE_LIFETIME.with(|cell| cell.borrow().clone()) {
443 let snap = capture_tls_snapshot(lifetime);
444 STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().insert(old_id, snap));
445 } else {
446 clear_live_tls_pack_mappings();
447 }
448 } else {
449 clear_live_tls_pack_mappings();
450 }
451 }
452 ACTIVE_TLS_STORE_ID.set(new_id);
453 ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = Some(store.lifetime_weak()));
454 if let Some(snap) = STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().remove(&new_id)) {
455 if snap.warm_epoch == epoch && snap.store_lifetime.upgrade().is_some() {
456 apply_tls_snapshot(snap);
457 return;
458 }
459 }
460 seed_tls_from_store_catalog(store);
461 STORE_TLS_SNAPSHOTS.with(|map| {
462 map.borrow_mut()
463 .insert(new_id, capture_tls_snapshot(store.lifetime_weak()));
464 });
465 }
466
467 #[derive(Clone)]
472 struct SingleReaderMemo {
473 objects_dir: PathBuf,
474 pack_dir: PathBuf,
475 idx: Arc<PackIndex>,
476 pack_bytes: Arc<PackData>,
477 pack_id: u32,
478 }
479
480 fn forget_single_reader_memo() {
481 SINGLE_READER_MEMO.with(|cell| *cell.borrow_mut() = None);
482 }
483
484 pub(crate) fn remember_single_reader(
486 objects_dir: &Path,
487 idx: &Arc<PackIndex>,
488 pack_bytes: &Arc<PackData>,
489 pack_id: u32,
490 ) {
491 SINGLE_READER_MEMO.with(|cell| {
492 *cell.borrow_mut() = Some(SingleReaderMemo {
493 objects_dir: objects_dir.to_path_buf(),
494 pack_dir: objects_dir.join("pack"),
495 idx: Arc::clone(idx),
496 pack_bytes: Arc::clone(pack_bytes),
497 pack_id,
498 });
499 });
500 }
501
502 pub(crate) fn single_reader_for(
508 objects_dir: &Path,
509 ) -> Option<(Arc<PackIndex>, Arc<PackData>, u32)> {
510 if !PACK_DIR_LISTING_TRUSTED.get() {
511 return None;
512 }
513 SINGLE_READER_MEMO.with(|memo| {
514 let memo = memo.borrow();
515 let memo = memo.as_ref()?;
516 if memo.objects_dir.as_os_str() != objects_dir.as_os_str() {
517 return None;
518 }
519 let listing_unchanged = PACK_DIR_INDEXES.with(|cell| {
520 cell.borrow().as_ref().is_some_and(|(dir, _, indexes)| {
521 dir.as_os_str() == memo.pack_dir.as_os_str()
522 && indexes.len() == 1
523 && Arc::ptr_eq(&indexes[0], &memo.idx)
524 })
525 });
526 if !listing_unchanged {
527 return None;
528 }
529 let hit_current = PACK_HIT_INDEX.with(|cell| {
530 cell.borrow().as_ref().is_some_and(|(dir, idx)| {
531 dir.as_os_str() == objects_dir.as_os_str() && Arc::ptr_eq(idx, &memo.idx)
532 })
533 });
534 if !hit_current {
535 return None;
536 }
537 Some((
538 Arc::clone(&memo.idx),
539 Arc::clone(&memo.pack_bytes),
540 memo.pack_id,
541 ))
542 })
543 }
544
545 fn store_thread_local_key(store: &crate::pack_store::PackStore) -> u64 {
546 store.id()
547 }
548
549 pub(crate) fn clear_thread_local_dir_indexes() {
550 PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = None);
551 PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
552 PACK_BYTES.with(|cell| cell.borrow_mut().clear());
553 PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
554 PACK_IDS.with(|cell| cell.borrow_mut().clear());
555 PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
556 PACK_DIR_LISTING_TRUSTED.set(false);
557 THREAD_WARM_EPOCH.set(0);
558 THREAD_WARM_STORE.set(0);
559 STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().clear());
560 ACTIVE_TLS_STORE_ID.set(0);
561 ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = None);
562 super::clear_pack_read_info_cache();
563 PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = None);
564 forget_single_reader_memo();
565 }
566
567 pub(crate) fn try_read_object_via_single_pack_reader(
568 objects_dir: &Path,
569 oid: &crate::objects::ObjectId,
570 ) -> Result<Option<crate::objects::Object>> {
571 if !pack_thread_locals_apply_to(objects_dir) {
572 return Ok(None);
573 }
574 sync_thread_local_for_objects_dir(objects_dir);
575 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
576 return Ok(None);
577 };
578 match reader.read_object(oid) {
579 Ok(obj) => Ok(Some(obj)),
580 Err(Error::ObjectNotFound(_)) => Ok(None),
581 Err(err) => Err(super::wrap_pack_cache_read_err(
582 objects_dir,
583 reader.idx.as_ref(),
584 oid,
585 err,
586 )),
587 }
588 }
589
590 pub(crate) fn try_read_object_via_single_pack_reader_unless_whole_larger(
594 objects_dir: &Path,
595 oid: &crate::objects::ObjectId,
596 limit: u64,
597 ) -> Result<Option<super::BoundedRead>> {
598 if !pack_thread_locals_apply_to(objects_dir) {
599 return Ok(None);
600 }
601 sync_thread_local_for_objects_dir(objects_dir);
602 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
603 return Ok(None);
604 };
605 match reader.read_object_unless_whole_larger(oid, limit) {
606 Ok(read) => Ok(Some(read)),
607 Err(Error::ObjectNotFound(_)) => Ok(None),
608 Err(err) => Err(super::wrap_pack_cache_read_err(
609 objects_dir,
610 reader.idx.as_ref(),
611 oid,
612 err,
613 )),
614 }
615 }
616
617 pub(crate) fn try_read_commit_through_subject_via_single_pack_reader(
621 objects_dir: &Path,
622 oid: &crate::objects::ObjectId,
623 ) -> Result<Option<Vec<u8>>> {
624 if !pack_thread_locals_apply_to(objects_dir) {
625 return Ok(None);
626 }
627 sync_thread_local_for_objects_dir(objects_dir);
628 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
629 return Ok(None);
630 };
631 reader.read_commit_through_subject(oid)
632 }
633
634 pub(crate) fn try_read_object_via_single_pack_reader_into(
635 objects_dir: &Path,
636 oid: &crate::objects::ObjectId,
637 populate_delta_lru: bool,
638 buf: &mut Vec<u8>,
639 ) -> Result<Option<crate::objects::ObjectKind>> {
640 if !pack_thread_locals_apply_to(objects_dir) {
641 return Ok(None);
642 }
643 sync_thread_local_for_objects_dir(objects_dir);
644 let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
645 return Ok(None);
646 };
647 match reader.read_object_into_with_delta_policy(oid, populate_delta_lru, buf) {
648 Ok(kind) => Ok(Some(kind)),
649 Err(Error::ObjectNotFound(_)) => Ok(None),
650 Err(err) => Err(super::wrap_pack_cache_read_err(
651 objects_dir,
652 reader.idx.as_ref(),
653 oid,
654 err,
655 )),
656 }
657 }
658
659 pub(crate) fn clear_thread_local_after_invalidation(new_epoch: u64) {
661 clear_thread_local_dir_indexes();
662 THREAD_WARM_EPOCH.set(new_epoch);
663 }
664
665 pub(crate) fn sync_thread_local_with_store(store: &crate::pack_store::PackStore) {
666 switch_tls_for_store(store);
667 }
668
669 fn drop_thread_local_dir_listing(pack_dir: &Path) {
670 PACK_DIR_INDEXES.with(|cell| {
671 if cell
672 .borrow()
673 .as_ref()
674 .is_some_and(|(dir, _, _)| dir == pack_dir)
675 {
676 *cell.borrow_mut() = None;
677 PACK_DIR_LISTING_TRUSTED.set(false);
678 }
679 });
680 }
681
682 fn drop_thread_local_pack_bytes(pack_path: &Path) {
683 PACK_BYTES.with(|cell| {
684 cell.borrow_mut().remove(pack_path);
685 });
686 forget_single_reader_memo();
687 }
688
689 fn invalidate_thread_local_for_pack_path_change(pack_path: &Path) {
691 let idx_path = pack_path.with_extension("idx");
692 drop_thread_local_pack_bytes(pack_path);
693 PACK_INDEXES.with(|cell| {
694 cell.borrow_mut().remove(&idx_path);
695 });
696 PACK_IDS.with(|cell| {
697 cell.borrow_mut().remove(pack_path.as_os_str());
698 });
699 PACK_COUNT_VALIDATED.with(|cell| {
700 cell.borrow_mut().remove(pack_path);
701 });
702 PACK_HIT_INDEX.with(|cell| {
703 let drop_hint = cell
704 .borrow()
705 .as_ref()
706 .is_some_and(|(_, idx)| idx.pack_path == pack_path || idx.idx_path == idx_path);
707 if drop_hint {
708 *cell.borrow_mut() = None;
709 }
710 });
711 if let Some(pack_dir) = pack_path.parent() {
712 drop_thread_local_dir_listing(pack_dir);
713 }
714 super::clear_pack_read_info_cache();
715 }
716
717 pub(crate) fn warm_thread_local_from_store(store: &crate::pack_store::PackStore) {
723 if crate::pack_store::PackStore::has_context()
724 && !PACK_DIR_LISTING_TRUSTED.get()
725 && ACTIVE_TLS_STORE_ID.get() == store.id()
726 && THREAD_WARM_STORE.get() == store.id()
727 && THREAD_WARM_EPOCH.get() == store.cache_epoch()
728 {
729 seed_tls_from_store_catalog(store);
730 return;
731 }
732 switch_tls_for_store(store);
733 }
734
735 pub(crate) fn trusted_verified_empty_pack_listing(objects_dir: &Path) -> bool {
737 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
738 return false;
739 }
740 if !PACK_DIR_LISTING_TRUSTED.get() {
741 return false;
742 }
743 let pack_dir = objects_dir.join("pack");
744 crate::pack_store::PackStore::active_for_reads().with_catalog_read(|g| {
745 g.by_dir
746 .get(&pack_dir)
747 .is_some_and(|c| c.idx_listing_verified && c.indexes.is_empty())
748 })
749 }
750
751 fn cached_pack_bytes(pack_path: &Path) -> Option<Arc<PackData>> {
752 if !crate::pack_store::PackStore::has_context() {
753 return None;
754 }
755 PACK_BYTES.with(|cell| cell.borrow().get(pack_path).and_then(CachedPackBytes::get))
756 }
757
758 fn remember_pack_bytes(pack_path: &Path, bytes: &Arc<PackData>) {
759 if !crate::pack_store::PackStore::has_context() {
762 return;
763 }
764 let cached = if bytes.uses_positioned_reads() {
765 CachedPackBytes::Positioned(Arc::downgrade(bytes))
766 } else {
767 CachedPackBytes::Retained(Arc::clone(bytes))
768 };
769 PACK_BYTES.with(|cell| {
770 cell.borrow_mut().insert(pack_path.to_path_buf(), cached);
771 });
772 forget_single_reader_memo();
773 }
774
775 pub(crate) fn remember_pack_hit(objects_dir: &Path, idx: &Arc<PackIndex>) {
776 if !pack_thread_locals_apply_to(objects_dir) {
777 return;
778 }
779 promote_pack_index(objects_dir, &idx.idx_path);
780 PACK_HIT_INDEX.with(|cell| {
781 *cell.borrow_mut() = Some((objects_dir.to_path_buf(), Arc::clone(idx)));
782 });
783 }
784
785 pub(crate) fn find_offset_in_pack_read_context(
786 objects_dir: &Path,
787 idx: &PackIndex,
788 oid: &crate::objects::ObjectId,
789 ) -> Result<Option<u64>> {
790 if !pack_thread_locals_apply_to(objects_dir) {
791 return idx.find_offset(oid);
792 }
793 PACK_MONOTONIC_CURSOR.with(|cell| {
794 let mut slot = cell.borrow_mut();
795 if slot.as_ref().is_none_or(|(dir, _)| dir != objects_dir) {
796 *slot = Some((objects_dir.to_path_buf(), 0));
797 }
798 let Some((_, cursor)) = slot.as_mut() else {
799 return idx.find_offset(oid);
800 };
801 idx.find_offset_monotonic(oid, cursor)
802 })
803 }
804
805 pub(crate) fn hinted_pack_index(objects_dir: &Path) -> Option<Arc<PackIndex>> {
808 if !pack_thread_locals_apply_to(objects_dir) {
809 return None;
810 }
811 PACK_HIT_INDEX.with(|cell| {
812 cell.borrow()
813 .as_ref()
814 .and_then(|(dir, idx)| (dir == objects_dir).then(|| Arc::clone(idx)))
815 })
816 }
817
818 pub(crate) fn try_read_from_hinted_pack(
819 objects_dir: &Path,
820 oid: &crate::objects::ObjectId,
821 ) -> Result<Option<crate::objects::Object>> {
822 let Some(idx) = hinted_pack_index(objects_dir) else {
823 return Ok(None);
824 };
825 let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
826 return Ok(None);
827 };
828 match super::read_object_from_pack_at_offset(&idx, offset) {
829 Ok(obj) => Ok(Some(obj)),
830 Err(Error::ObjectNotFound(_)) => Ok(None),
831 Err(err) if err.is_object_read_wrap_source() => Ok(None),
832 Err(err) => Err(err),
833 }
834 }
835
836 pub(crate) fn try_read_from_hinted_pack_into(
837 objects_dir: &Path,
838 oid: &crate::objects::ObjectId,
839 populate_delta_lru: bool,
840 buf: &mut Vec<u8>,
841 ) -> Result<Option<crate::objects::ObjectKind>> {
842 let Some(idx) = hinted_pack_index(objects_dir) else {
843 return Ok(None);
844 };
845 let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
846 return Ok(None);
847 };
848 match super::read_object_at_depth_into(&idx, offset, 0, populate_delta_lru, buf) {
849 Ok(kind) => Ok(Some(kind)),
850 Err(Error::ObjectNotFound(_)) => Ok(None),
851 Err(err) if err.is_object_read_wrap_source() => Ok(None),
852 Err(err) => Err(err),
853 }
854 }
855
856 pub(crate) fn try_read_info_from_hinted_pack(
857 objects_dir: &Path,
858 oid: &crate::objects::ObjectId,
859 ) -> Result<Option<crate::objects::ObjectInfo>> {
860 let Some(idx) = hinted_pack_index(objects_dir) else {
861 return Ok(None);
862 };
863 let Some(offset) = idx.find_offset(oid)? else {
864 return Ok(None);
865 };
866 match super::read_object_info_from_pack_at_offset(&idx, offset) {
867 Ok(info) => Ok(Some(info)),
868 Err(Error::ObjectNotFound(_)) => Ok(None),
869 Err(err) => Err(err),
870 }
871 }
872 fn cached_dir_indexes(objects_dir: &Path, pack_dir: &Path) -> Option<Vec<Arc<PackIndex>>> {
873 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
874 return None;
875 }
876 let trust_listing = PACK_DIR_LISTING_TRUSTED.get();
877 let dir_mt = if trust_listing {
878 None
879 } else {
880 Some(dir_mtime(pack_dir))
881 };
882 PACK_DIR_INDEXES.with(|cell| {
883 cell.borrow()
884 .as_ref()
885 .and_then(|(dir, cached_mt, indexes)| {
886 if dir != pack_dir {
887 return None;
888 }
889 if trust_listing || dir_mt == Some(*cached_mt) {
890 Some(indexes.clone())
891 } else {
892 None
893 }
894 })
895 })
896 }
897
898 fn remember_dir_indexes(objects_dir: &Path, pack_dir: &Path, indexes: &[Arc<PackIndex>]) {
899 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
900 return;
901 }
902 let dir_mt = dir_mtime(pack_dir);
903 PACK_DIR_INDEXES.with(|cell| {
904 *cell.borrow_mut() = Some((pack_dir.to_path_buf(), dir_mt, indexes.to_vec()));
905 });
906 }
907
908 struct CachedDir {
909 dir_mtime: SystemTime,
910 indexes: Vec<Arc<PackIndex>>,
911 midx_mtime: SystemTime,
912 midx_pack_names: HashSet<String>,
913 idx_listing_verified: bool,
915 }
916
917 struct CachedIdx {
918 idx: Arc<PackIndex>,
919 }
920
921 struct CachedPack {
922 mtime: SystemTime,
923 size: u64,
924 bytes: Arc<PackData>,
925 }
926
927 pub(super) const DELTA_BASE_CACHE_DEFAULT: usize = 96 * 1024 * 1024;
929
930 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
931 pub(crate) struct DeltaCacheKey {
932 pack_id: u32,
933 offset: u64,
934 }
935
936 struct DeltaBaseLru {
938 byte_limit: usize,
939 bytes_used: usize,
940 entries: HashMap<DeltaCacheKey, (ObjectKind, Arc<[u8]>)>,
941 head: Option<DeltaCacheKey>,
942 tail: Option<DeltaCacheKey>,
943 links: HashMap<DeltaCacheKey, (Option<DeltaCacheKey>, Option<DeltaCacheKey>)>,
944 }
945
946 impl Default for DeltaBaseLru {
947 fn default() -> Self {
948 Self {
949 byte_limit: DELTA_BASE_CACHE_DEFAULT,
950 bytes_used: 0,
951 entries: HashMap::new(),
952 head: None,
953 tail: None,
954 links: HashMap::new(),
955 }
956 }
957 }
958
959 impl DeltaBaseLru {
960 fn set_byte_limit(&mut self, limit: usize) {
961 if limit == 0 {
962 self.clear();
963 self.byte_limit = 0;
964 return;
965 }
966 self.byte_limit = limit;
967 while self.bytes_used > self.byte_limit {
968 let Some(key) = self.head else {
969 break;
970 };
971 self.evict_key(key);
972 }
973 }
974
975 fn unlink(&mut self, key: DeltaCacheKey) {
976 let Some((prev, next)) = self.links.remove(&key) else {
977 return;
978 };
979 match prev {
980 Some(p) => {
981 if let Some(link) = self.links.get_mut(&p) {
982 link.1 = next;
983 }
984 }
985 None => self.head = next,
986 }
987 match next {
988 Some(n) => {
989 if let Some(link) = self.links.get_mut(&n) {
990 link.0 = prev;
991 }
992 }
993 None => self.tail = prev,
994 }
995 }
996
997 fn append_tail(&mut self, key: DeltaCacheKey) {
998 let prev = self.tail;
999 self.links.insert(key, (prev, None));
1000 if let Some(p) = prev {
1001 if let Some(link) = self.links.get_mut(&p) {
1002 link.1 = Some(key);
1003 }
1004 } else {
1005 self.head = Some(key);
1006 }
1007 self.tail = Some(key);
1008 }
1009
1010 fn touch(&mut self, key: DeltaCacheKey) {
1011 self.unlink(key);
1012 self.append_tail(key);
1013 }
1014
1015 fn evict_key(&mut self, key: DeltaCacheKey) {
1016 self.unlink(key);
1017 if let Some((_, data)) = self.entries.remove(&key) {
1018 self.bytes_used = self.bytes_used.saturating_sub(data.len());
1019 }
1020 }
1021
1022 fn get(&mut self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
1023 let (kind, data) = self.entries.get(&key).map(|(k, d)| (*k, Arc::clone(d)))?;
1024 self.touch(key);
1025 Some((kind, data))
1026 }
1027
1028 fn put(&mut self, key: DeltaCacheKey, kind: ObjectKind, data: Arc<[u8]>) {
1029 if self.byte_limit == 0 {
1030 return;
1031 }
1032 let sz = data.len();
1033 if sz > self.byte_limit {
1034 return;
1035 }
1036 use std::collections::hash_map::Entry;
1037 match self.entries.entry(key) {
1038 Entry::Occupied(mut slot) => {
1039 let old_len = slot.get().1.len();
1040 slot.insert((kind, Arc::clone(&data)));
1041 self.bytes_used = self.bytes_used.saturating_sub(old_len).saturating_add(sz);
1042 self.touch(key);
1043 }
1044 Entry::Vacant(slot) => {
1045 slot.insert((kind, Arc::clone(&data)));
1046 self.bytes_used = self.bytes_used.saturating_add(sz);
1047 self.append_tail(key);
1048 }
1049 }
1050 while self.bytes_used > self.byte_limit {
1051 let Some(evict) = self.head else {
1052 break;
1053 };
1054 self.evict_key(evict);
1055 }
1056 }
1057
1058 fn drop_pack(&mut self, pack_id: u32) {
1059 let keys: Vec<DeltaCacheKey> = self
1060 .entries
1061 .keys()
1062 .copied()
1063 .filter(|k| k.pack_id == pack_id)
1064 .collect();
1065 for key in keys {
1066 self.evict_key(key);
1067 }
1068 }
1069
1070 fn clear(&mut self) {
1071 self.entries.clear();
1072 self.links.clear();
1073 self.head = None;
1074 self.tail = None;
1075 self.bytes_used = 0;
1076 }
1077 }
1078
1079 fn shard_index(key: DeltaCacheKey) -> usize {
1080 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1081 key.hash(&mut hasher);
1082 (hasher.finish() as usize) % DELTA_LRU_SHARD_COUNT
1083 }
1084
1085 pub(crate) struct ShardedDeltaBaseLru {
1087 cache_enabled: AtomicBool,
1088 lru_populated: AtomicBool,
1089 total_byte_limit: AtomicUsize,
1090 shards: Box<[Mutex<DeltaBaseLru>; DELTA_LRU_SHARD_COUNT]>,
1091 }
1092
1093 impl ShardedDeltaBaseLru {
1094 pub fn new() -> Self {
1095 let per_shard = DELTA_BASE_CACHE_DEFAULT / DELTA_LRU_SHARD_COUNT;
1096 Self {
1097 cache_enabled: AtomicBool::new(true),
1098 lru_populated: AtomicBool::new(false),
1099 total_byte_limit: AtomicUsize::new(DELTA_BASE_CACHE_DEFAULT),
1100 shards: Box::new(std::array::from_fn(|_| {
1101 Mutex::new(DeltaBaseLru {
1102 byte_limit: per_shard,
1103 ..DeltaBaseLru::default()
1104 })
1105 })),
1106 }
1107 }
1108
1109 pub(crate) fn set_byte_limit(&self, limit: usize) {
1110 self.total_byte_limit.store(limit, Ordering::Release);
1111 if limit == 0 {
1112 self.cache_enabled.store(false, Ordering::Release);
1113 self.clear();
1114 return;
1115 }
1116 self.cache_enabled.store(true, Ordering::Release);
1117 let base = limit / DELTA_LRU_SHARD_COUNT;
1118 let rem = limit % DELTA_LRU_SHARD_COUNT;
1119 let mut any = false;
1120 for (i, shard) in self.shards.iter().enumerate() {
1121 let cap = base + usize::from(i < rem);
1122 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1123 g.set_byte_limit(cap);
1124 any |= !g.entries.is_empty();
1125 }
1126 self.lru_populated.store(any, Ordering::Release);
1127 }
1128
1129 pub(crate) fn get(&self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
1130 if !self.cache_enabled.load(Ordering::Acquire)
1131 || !self.lru_populated.load(Ordering::Acquire)
1132 {
1133 return None;
1134 }
1135 let shard = &self.shards[shard_index(key)];
1136 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1137 g.get(key)
1138 }
1139
1140 pub(crate) fn put_many(&self, kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
1141 if items.is_empty() || !self.cache_enabled.load(Ordering::Acquire) {
1142 return;
1143 }
1144 for &(pack_id, offset, ref data) in items {
1145 let key = DeltaCacheKey { pack_id, offset };
1146 let shard = &self.shards[shard_index(key)];
1147 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1148 if g.byte_limit == 0 {
1149 continue;
1150 }
1151 g.put(key, kind, Arc::clone(data));
1152 }
1153 self.lru_populated.store(true, Ordering::Release);
1154 }
1155
1156 pub(crate) fn clear(&self) {
1157 for shard in self.shards.iter() {
1158 shard.lock().unwrap_or_else(|e| e.into_inner()).clear();
1159 }
1160 self.lru_populated.store(false, Ordering::Release);
1161 }
1162
1163 pub(crate) fn drop_pack(&self, pack_id: u32) {
1164 let mut any = false;
1165 for shard in self.shards.iter() {
1166 let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1167 g.drop_pack(pack_id);
1168 any |= !g.entries.is_empty();
1169 }
1170 self.lru_populated.store(any, Ordering::Release);
1171 }
1172
1173 pub(crate) fn caching_enabled(&self) -> bool {
1174 self.cache_enabled.load(Ordering::Acquire)
1175 }
1176
1177 #[cfg(test)]
1178 pub(crate) fn bytes_used(&self) -> usize {
1179 self.shards
1180 .iter()
1181 .map(|s| s.lock().unwrap_or_else(|e| e.into_inner()).bytes_used)
1182 .sum()
1183 }
1184
1185 #[cfg(test)]
1186 pub(crate) fn contains(&self, pack_id: u32, offset: u64) -> bool {
1187 let key = DeltaCacheKey { pack_id, offset };
1188 self.shards[shard_index(key)]
1189 .lock()
1190 .unwrap_or_else(|e| e.into_inner())
1191 .entries
1192 .contains_key(&key)
1193 }
1194 }
1195
1196 #[derive(Default)]
1197 pub(crate) struct CatalogState {
1198 by_dir: HashMap<PathBuf, CachedDir>,
1199 by_idx: HashMap<PathBuf, CachedIdx>,
1200 by_pack: HashMap<PathBuf, CachedPack>,
1201 bounded_pack_order: VecDeque<PathBuf>,
1202 pack_ids: HashMap<PathBuf, u32>,
1204 next_pack_id: u32,
1205 validated_pack_counts: HashSet<PathBuf>,
1207 #[cfg(any(test, feature = "test-support"))]
1209 test_dir_rescan_counts: HashMap<PathBuf, u64>,
1210 }
1211
1212 impl CatalogState {
1213 fn retain_bounded_pack(&mut self, pack_path: &Path) {
1214 if let Some(pos) = self.bounded_pack_order.iter().position(|p| p == pack_path) {
1215 self.bounded_pack_order.remove(pos);
1216 }
1217 while self.bounded_pack_order.len() >= BOUNDED_PACK_READERS {
1218 if let Some(path) = self.bounded_pack_order.pop_front() {
1219 self.by_pack.remove(&path);
1220 }
1221 }
1222 self.bounded_pack_order.push_back(pack_path.to_path_buf());
1223 }
1224 }
1225
1226 pub(crate) fn with_current<R>(f: impl FnOnce(&mut CatalogState) -> R) -> R {
1227 crate::pack_store::PackStore::with_current_or_implicit(f)
1228 }
1229
1230 pub(crate) fn clear_catalog_on(m: &std::sync::RwLock<CatalogState>) {
1231 let mut g = m.write().unwrap_or_else(|e| e.into_inner());
1232 clear_inner(&mut g);
1233 }
1234
1235 pub(crate) fn configure_delta_base_on(
1236 delta: &ShardedDeltaBaseLru,
1237 cfg: Option<&crate::config::ConfigSet>,
1238 ) {
1239 let bytes = cfg
1240 .and_then(|config| config.get("core.deltaBaseCacheLimit"))
1241 .as_ref()
1242 .and_then(|value| crate::config::parse_i64(value).ok())
1243 .and_then(|value| usize::try_from(value).ok())
1244 .unwrap_or(DELTA_BASE_CACHE_DEFAULT);
1245 delta.set_byte_limit(bytes);
1246 }
1247
1248 #[cfg(any(test, feature = "test-support"))]
1249 thread_local! {
1250 static TEST_MARKER_STAT_COUNT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1251 }
1252
1253 fn marker_sidecar_is_file(path: &Path) -> bool {
1254 #[cfg(any(test, feature = "test-support"))]
1255 TEST_MARKER_STAT_COUNT.with(|count| count.set(count.get().saturating_add(1)));
1256 path.is_file()
1257 }
1258
1259 pub fn sidecar_flags(pack_path: &Path) -> (bool, bool) {
1261 (
1262 marker_sidecar_is_file(&pack_path.with_extension("promisor")),
1263 marker_sidecar_is_file(&pack_path.with_extension("mtimes")),
1264 )
1265 }
1266
1267 fn refresh_pack_sidecar_flags(idx: Arc<PackIndex>) -> Arc<PackIndex> {
1268 let (is_promisor, is_cruft) = sidecar_flags(&idx.pack_path);
1269 if idx.is_promisor == is_promisor && idx.is_cruft == is_cruft {
1270 return idx;
1271 }
1272 let updated = Arc::new(idx.with_sidecar_flags(is_promisor, is_cruft));
1273 with_current(|g| {
1274 g.by_idx.insert(
1275 idx.idx_path.clone(),
1276 CachedIdx {
1277 idx: Arc::clone(&updated),
1278 },
1279 );
1280 });
1281 updated
1282 }
1283
1284 fn dir_mtime(path: &Path) -> SystemTime {
1285 #[cfg(test)]
1286 crate::hot_path_test_metrics::record_pack_dir_stat_for_active_scope();
1287 #[cfg(test)]
1288 {
1289 if let Some(store) = alternate_read_store() {
1290 if path.starts_with(store.objects_dir()) {
1291 crate::hot_path_test_metrics::record_pack_dir_metadata_for_active_scope();
1292 }
1293 }
1294 crate::hot_path_test_metrics::record_pack_dir_stat_for_active_scope();
1295 }
1296 fs::metadata(path)
1297 .and_then(|m| m.modified())
1298 .unwrap_or(SystemTime::UNIX_EPOCH)
1299 }
1300
1301 fn file_signature(path: &Path) -> Option<(SystemTime, u64)> {
1302 #[cfg(test)]
1303 crate::hot_path_test_metrics::record_pack_signature_stat_for_active_scope();
1304 let m = fs::metadata(path).ok()?;
1305 let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
1306 Some((mtime, m.len()))
1307 }
1308
1309 pub fn pack_object_count_validated(pack_path: &Path) -> bool {
1310 if crate::pack_store::PackStore::has_context()
1311 && PACK_COUNT_VALIDATED.with(|cell| cell.borrow().contains(pack_path))
1312 {
1313 return true;
1314 }
1315 crate::pack_store::PackStore::active_for_reads()
1316 .with_catalog_read(|g| g.validated_pack_counts.contains(pack_path))
1317 }
1318
1319 pub fn mark_pack_object_count_validated(pack_path: &Path) {
1320 if crate::pack_store::PackStore::has_context() {
1321 PACK_COUNT_VALIDATED.with(|cell| {
1322 cell.borrow_mut().insert(pack_path.to_path_buf());
1323 });
1324 }
1325 let store = crate::pack_store::PackStore::active_for_reads();
1326 if store.with_catalog_read(|g| g.validated_pack_counts.contains(pack_path)) {
1327 return;
1328 }
1329 let _ = store.try_with_catalog(|g| {
1330 g.validated_pack_counts.insert(pack_path.to_path_buf());
1331 });
1332 }
1333
1334 fn cached_index(idx_path: &Path) -> Option<Arc<PackIndex>> {
1335 if !crate::pack_store::PackStore::has_context() {
1336 return None;
1337 }
1338 PACK_INDEXES.with(|cell| cell.borrow().get(idx_path).map(Arc::clone))
1339 }
1340
1341 fn remember_index(idx_path: &Path, idx: &Arc<PackIndex>) {
1342 if !crate::pack_store::PackStore::has_context() {
1343 return;
1344 }
1345 PACK_INDEXES.with(|cell| {
1346 cell.borrow_mut()
1347 .insert(idx_path.to_path_buf(), Arc::clone(idx));
1348 });
1349 }
1350
1351 fn index_cache_hit_stale(idx: &PackIndex) -> bool {
1352 !idx.pack_path.is_file()
1353 }
1354
1355 pub fn get_index(idx_path: &Path) -> Result<Arc<PackIndex>> {
1357 if let Some(hit) = cached_index(idx_path) {
1358 if index_cache_hit_stale(&hit) {
1359 PACK_INDEXES.with(|cell| {
1360 cell.borrow_mut().remove(idx_path);
1361 });
1362 } else {
1363 return Ok(hit);
1364 }
1365 }
1366 let store = catalog_store();
1367 if let Some(hit) =
1368 store.with_catalog_read(|g| g.by_idx.get(idx_path).map(|c| Arc::clone(&c.idx)))
1369 {
1370 if index_cache_hit_stale(&hit) {
1371 store.with_catalog(|g| {
1372 g.by_idx.remove(idx_path);
1373 });
1374 } else {
1375 remember_index(idx_path, &hit);
1376 return Ok(hit);
1377 }
1378 }
1379 crate::trace::trace_span!("grit.pack.index.open");
1380 let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
1381 store.with_catalog(|g| {
1382 g.by_idx.insert(
1383 idx_path.to_path_buf(),
1384 CachedIdx {
1385 idx: Arc::clone(&parsed),
1386 },
1387 );
1388 });
1389 remember_index(idx_path, &parsed);
1390 Ok(parsed)
1391 }
1392
1393 fn rescan_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1394 let pack_dir = objects_dir.join("pack");
1395 drop_thread_local_dir_listing(&pack_dir);
1396 let dir_mt = dir_mtime(&pack_dir);
1397
1398 let rd = match fs::read_dir(&pack_dir) {
1399 Ok(rd) => rd,
1400 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1401 with_current(|g| {
1402 g.by_dir.insert(
1403 pack_dir.clone(),
1404 CachedDir {
1405 dir_mtime: dir_mt,
1406 indexes: Vec::new(),
1407 midx_mtime: SystemTime::UNIX_EPOCH,
1408 midx_pack_names: HashSet::new(),
1409 idx_listing_verified: true,
1410 },
1411 );
1412 });
1413 return Ok(Vec::new());
1414 }
1415 Err(err) => return Err(Error::Io(err)),
1416 };
1417
1418 let mut out = Vec::new();
1419 for entry in rd {
1420 let entry = entry.map_err(Error::Io)?;
1421 let path = entry.path();
1422 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1423 continue;
1424 }
1425 let Ok(idx) = get_index(&path) else {
1426 continue;
1427 };
1428 if !idx.pack_path.is_file() {
1429 continue;
1430 }
1431 out.push(refresh_pack_sidecar_flags(idx));
1432 }
1433
1434 let midx_pack_names = load_midx_pack_names(&pack_dir);
1435 with_current(|g| {
1436 #[cfg(any(test, feature = "test-support"))]
1437 {
1438 *g.test_dir_rescan_counts
1439 .entry(pack_dir.clone())
1440 .or_insert(0) += 1;
1441 }
1442 g.by_dir.insert(
1443 pack_dir,
1444 CachedDir {
1445 dir_mtime: dir_mt,
1446 indexes: out.clone(),
1447 midx_mtime: midx_pack_names.0,
1448 midx_pack_names: midx_pack_names.1,
1449 idx_listing_verified: false,
1450 },
1451 );
1452 });
1453 STORE_TLS_SNAPSHOTS.with(|map| {
1454 map.borrow_mut()
1455 .remove(&crate::pack_store::PackStore::active_for_reads().id());
1456 });
1457 Ok(out)
1458 }
1459
1460 fn load_midx_pack_names(pack_dir: &Path) -> (SystemTime, HashSet<String>) {
1461 let Some(midx_path) = crate::midx::resolve_tip_midx_path(pack_dir) else {
1462 return (SystemTime::UNIX_EPOCH, HashSet::new());
1463 };
1464 let metadata = fs::metadata(&midx_path).ok();
1465 let mtime = metadata
1466 .as_ref()
1467 .and_then(|m| m.modified().ok())
1468 .unwrap_or(SystemTime::UNIX_EPOCH);
1469 let names = metadata
1470 .as_ref()
1471 .and_then(|metadata| {
1472 crate::midx::read_midx_pack_idx_names_from_path(&midx_path, metadata).ok()
1473 })
1474 .unwrap_or_default()
1475 .into_iter()
1476 .collect();
1477 (mtime, names)
1478 }
1479
1480 fn refresh_midx_pack_names_locked(pack_dir: &Path, cached: &mut CachedDir) {
1481 let (mtime, names) = load_midx_pack_names(pack_dir);
1482 if cached.midx_mtime != mtime {
1483 cached.midx_mtime = mtime;
1484 cached.midx_pack_names = names;
1485 }
1486 }
1487
1488 pub fn promote_pack_index(objects_dir: &Path, idx_path: &Path) {
1490 let pack_dir = objects_dir.join("pack");
1491 let store = crate::pack_store::PackStore::active_for_reads();
1492 let _ = store.try_with_catalog(|g| {
1493 let Some(cached) = g.by_dir.get_mut(&pack_dir) else {
1494 return;
1495 };
1496 let Some(pos) = cached
1497 .indexes
1498 .iter()
1499 .position(|idx| idx.idx_path == idx_path)
1500 else {
1501 return;
1502 };
1503 if pos == 0 {
1504 return;
1505 }
1506 let hit = cached.indexes.remove(pos);
1507 cached.indexes.insert(0, hit);
1508 });
1509 }
1510
1511 pub fn midx_covered_pack_names(objects_dir: &Path) -> HashSet<String> {
1513 let pack_dir = objects_dir.join("pack");
1514 let cached = with_current(|g| {
1515 let cached = g.by_dir.get_mut(&pack_dir)?;
1516 refresh_midx_pack_names_locked(&pack_dir, cached);
1517 Some(cached.midx_pack_names.clone())
1518 });
1519 if let Some(names) = cached {
1520 return names;
1521 }
1522 load_midx_pack_names(&pack_dir).1
1523 }
1524
1525 pub fn get_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1527 if let Some(store) = alternate_read_store() {
1528 if store.objects_dir() == objects_dir
1529 && !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir)
1530 {
1531 return get_dir_indexes_with_store(&store, objects_dir);
1532 }
1533 }
1534 let pack_dir = objects_dir.join("pack");
1535 let store = crate::pack_store::PackStore::active_for_reads();
1536 if crate::pack_store::PackStore::has_context() {
1537 sync_thread_local_with_store(&store);
1538 }
1539 if let Some(hit) = cached_dir_indexes(objects_dir, &pack_dir) {
1540 return Ok(hit);
1541 }
1542 let dir_mt = dir_mtime(&pack_dir);
1543 if let Some(hit) = store.with_catalog_read(|g| {
1544 g.by_dir
1545 .get(&pack_dir)
1546 .and_then(|c| (c.dir_mtime == dir_mt).then(|| c.indexes.clone()))
1547 }) {
1548 remember_dir_indexes(objects_dir, &pack_dir, &hit);
1549 return Ok(hit);
1550 }
1551 let fresh = rescan_dir_indexes(objects_dir)?;
1552 remember_dir_indexes(objects_dir, &pack_dir, &fresh);
1553 Ok(fresh)
1554 }
1555
1556 pub(crate) fn get_dir_indexes_with_store(
1559 store: &crate::pack_store::PackStore,
1560 objects_dir: &Path,
1561 ) -> Result<Vec<Arc<PackIndex>>> {
1562 debug_assert_eq!(store.objects_dir(), objects_dir);
1563 let pack_dir = objects_dir.join("pack");
1564 if let Some(hit) = store.with_catalog_read(|g| {
1565 g.by_dir.get(&pack_dir).and_then(|c| {
1566 (c.idx_listing_verified || !c.indexes.is_empty()).then(|| c.indexes.clone())
1567 })
1568 }) {
1569 return Ok(hit);
1570 }
1571 rescan_dir_indexes_for_store(store, objects_dir)
1572 }
1573
1574 fn get_index_for_store(
1575 store: &crate::pack_store::PackStore,
1576 idx_path: &Path,
1577 ) -> Result<Arc<PackIndex>> {
1578 if let Some(hit) =
1579 store.with_catalog_read(|g| g.by_idx.get(idx_path).map(|c| Arc::clone(&c.idx)))
1580 {
1581 if !index_cache_hit_stale(&hit) {
1582 return Ok(hit);
1583 }
1584 store.with_catalog(|g| {
1585 g.by_idx.remove(idx_path);
1586 });
1587 }
1588 crate::trace::trace_span!("grit.pack.index.open");
1589 let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
1590 store.with_catalog(|g| {
1591 g.by_idx.insert(
1592 idx_path.to_path_buf(),
1593 CachedIdx {
1594 idx: Arc::clone(&parsed),
1595 },
1596 );
1597 });
1598 Ok(parsed)
1599 }
1600
1601 fn rescan_dir_indexes_for_store(
1602 store: &crate::pack_store::PackStore,
1603 objects_dir: &Path,
1604 ) -> Result<Vec<Arc<PackIndex>>> {
1605 let pack_dir = objects_dir.join("pack");
1606 let dir_mt = dir_mtime(&pack_dir);
1607 let rd = match fs::read_dir(&pack_dir) {
1608 Ok(rd) => rd,
1609 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1610 store.with_catalog(|g| {
1611 g.by_dir.insert(
1612 pack_dir.clone(),
1613 CachedDir {
1614 dir_mtime: dir_mt,
1615 indexes: Vec::new(),
1616 midx_mtime: SystemTime::UNIX_EPOCH,
1617 midx_pack_names: HashSet::new(),
1618 idx_listing_verified: true,
1619 },
1620 );
1621 });
1622 return Ok(Vec::new());
1623 }
1624 Err(err) => return Err(Error::Io(err)),
1625 };
1626 let mut out = Vec::new();
1627 for entry in rd {
1628 let entry = entry.map_err(Error::Io)?;
1629 let path = entry.path();
1630 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1631 continue;
1632 }
1633 let Ok(idx) = get_index_for_store(store, &path) else {
1634 continue;
1635 };
1636 if !idx.pack_path.is_file() {
1637 continue;
1638 }
1639 out.push(refresh_pack_sidecar_flags(idx));
1640 }
1641 let midx_pack_names = load_midx_pack_names(&pack_dir);
1642 store.with_catalog(|g| {
1643 #[cfg(any(test, feature = "test-support"))]
1644 {
1645 *g.test_dir_rescan_counts
1646 .entry(pack_dir.clone())
1647 .or_insert(0) += 1;
1648 }
1649 g.by_dir.insert(
1650 pack_dir,
1651 CachedDir {
1652 dir_mtime: dir_mt,
1653 indexes: out.clone(),
1654 midx_mtime: midx_pack_names.0,
1655 midx_pack_names: midx_pack_names.1,
1656 idx_listing_verified: false,
1657 },
1658 );
1659 });
1660 Ok(out)
1661 }
1662
1663 pub(crate) fn get_pack_bytes_for_store(
1664 store: &crate::pack_store::PackStore,
1665 pack_path: &Path,
1666 ) -> Result<Arc<PackData>> {
1667 if let Some(hit) =
1668 store.with_catalog_read(|g| g.by_pack.get(pack_path).map(|c| Arc::clone(&c.bytes)))
1669 {
1670 return Ok(hit);
1671 }
1672 let (mtime, size) = file_signature(pack_path).ok_or_else(|| {
1673 Error::Io(io::Error::new(
1674 io::ErrorKind::NotFound,
1675 format!("pack not found: {}", pack_path.display()),
1676 ))
1677 })?;
1678 load_pack_bytes_from_disk_for_store(store, pack_path, mtime, size)
1679 }
1680
1681 fn load_pack_bytes_from_disk_for_store(
1682 store: &crate::pack_store::PackStore,
1683 pack_path: &Path,
1684 mtime: SystemTime,
1685 size: u64,
1686 ) -> Result<Arc<PackData>> {
1687 let bytes = if store.bounded_reads() {
1688 PackData::open_random_access(pack_path)?
1689 } else {
1690 PackData::open_for_sparse_reads(pack_path, &store.sparse_read_files)?
1691 };
1692 store.with_catalog(|g| {
1693 drop_delta_entries_locked(g, pack_path, store.delta_bases());
1694 g.by_idx.remove(&pack_path.with_extension("idx"));
1695 g.validated_pack_counts.remove(pack_path);
1696 if store.bounded_reads() {
1697 g.retain_bounded_pack(pack_path);
1698 }
1699 g.by_pack.insert(
1700 pack_path.to_path_buf(),
1701 CachedPack {
1702 mtime,
1703 size,
1704 bytes: Arc::clone(&bytes),
1705 },
1706 );
1707 });
1708 Ok(bytes)
1709 }
1710
1711 pub(crate) fn reprepare_dir_on_miss_for_store(
1712 store: &crate::pack_store::PackStore,
1713 objects_dir: &Path,
1714 ) -> Result<bool> {
1715 let pack_dir = objects_dir.join("pack");
1716 let dir_mt = dir_mtime(&pack_dir);
1717 if store.with_catalog_read(|g| {
1718 g.by_dir.get(&pack_dir).is_some_and(|c| {
1719 c.idx_listing_verified && c.indexes.is_empty() && c.dir_mtime == dir_mt
1720 })
1721 }) {
1722 return Ok(false);
1723 }
1724 enum ReprepareDecision {
1725 Fresh,
1726 Rescan,
1727 VerifyIdxSet,
1728 }
1729 let decision = store.with_catalog_read(|g| match g.by_dir.get(&pack_dir) {
1730 None => ReprepareDecision::Rescan,
1731 Some(c) if c.dir_mtime != dir_mt => ReprepareDecision::Rescan,
1732 Some(c) if c.idx_listing_verified && !c.indexes.is_empty() => ReprepareDecision::Fresh,
1733 Some(_) => ReprepareDecision::VerifyIdxSet,
1734 });
1735 match decision {
1736 ReprepareDecision::Fresh => return Ok(false),
1737 ReprepareDecision::Rescan => {
1738 rescan_dir_indexes_for_store(store, objects_dir)?;
1739 return Ok(true);
1740 }
1741 ReprepareDecision::VerifyIdxSet => {}
1742 }
1743 let cached_paths = store.with_catalog_read(|g| {
1744 g.by_dir.get(&pack_dir).map(|c| {
1745 c.indexes
1746 .iter()
1747 .map(|idx| idx.idx_path.clone())
1748 .collect::<HashSet<_>>()
1749 })
1750 });
1751 let Some(cached_paths) = cached_paths else {
1752 rescan_dir_indexes_for_store(store, objects_dir)?;
1753 return Ok(true);
1754 };
1755 let on_disk = idx_paths_on_disk(&pack_dir)?;
1756 if on_disk == cached_paths {
1757 store.with_catalog(|g| {
1758 if let Some(c) = g.by_dir.get_mut(&pack_dir) {
1759 c.idx_listing_verified = true;
1760 }
1761 });
1762 return Ok(false);
1763 }
1764 rescan_dir_indexes_for_store(store, objects_dir)?;
1765 Ok(true)
1766 }
1767
1768 fn idx_paths_on_disk(pack_dir: &Path) -> Result<HashSet<PathBuf>> {
1769 let mut out = HashSet::new();
1770 let rd = match fs::read_dir(pack_dir) {
1771 Ok(rd) => rd,
1772 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(out),
1773 Err(err) => return Err(Error::Io(err)),
1774 };
1775 for entry in rd {
1776 let entry = entry.map_err(Error::Io)?;
1777 let path = entry.path();
1778 if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1779 continue;
1780 }
1781 if path.with_extension("pack").is_file() {
1782 out.insert(path);
1783 }
1784 }
1785 Ok(out)
1786 }
1787
1788 pub fn reprepare_dir_on_miss(objects_dir: &Path) -> Result<bool> {
1792 if let Some(store) = alternate_read_store() {
1793 if store.objects_dir() == objects_dir
1794 && !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir)
1795 {
1796 return reprepare_dir_on_miss_for_store(&store, objects_dir);
1797 }
1798 }
1799 let pack_dir = objects_dir.join("pack");
1800 let dir_mt = dir_mtime(&pack_dir);
1801 let store = crate::pack_store::PackStore::active_for_reads();
1802 if store.with_catalog_read(|g| {
1803 g.by_dir.get(&pack_dir).is_some_and(|c| {
1804 c.idx_listing_verified && c.indexes.is_empty() && c.dir_mtime == dir_mt
1805 })
1806 }) && crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir)
1807 && PACK_DIR_LISTING_TRUSTED.get()
1808 {
1809 return Ok(false);
1810 }
1811 enum ReprepareDecision {
1812 Fresh,
1813 Rescan,
1814 VerifyIdxSet,
1815 }
1816 let decision = store.with_catalog_read(|g| match g.by_dir.get(&pack_dir) {
1817 None => ReprepareDecision::Rescan,
1818 Some(c) if c.dir_mtime != dir_mt => ReprepareDecision::Rescan,
1819 Some(c) if c.idx_listing_verified && !c.indexes.is_empty() => ReprepareDecision::Fresh,
1820 Some(_) => ReprepareDecision::VerifyIdxSet,
1821 });
1822 match decision {
1823 ReprepareDecision::Fresh => return Ok(false),
1824 ReprepareDecision::Rescan => {
1825 rescan_dir_indexes(objects_dir)?;
1826 return Ok(true);
1827 }
1828 ReprepareDecision::VerifyIdxSet => {}
1829 }
1830 let cached_paths = store.with_catalog_read(|g| {
1831 g.by_dir.get(&pack_dir).map(|c| {
1832 c.indexes
1833 .iter()
1834 .map(|idx| idx.idx_path.clone())
1835 .collect::<HashSet<_>>()
1836 })
1837 });
1838 let Some(cached_paths) = cached_paths else {
1839 rescan_dir_indexes(objects_dir)?;
1840 return Ok(true);
1841 };
1842 let on_disk = idx_paths_on_disk(&pack_dir)?;
1843 if on_disk == cached_paths {
1844 store.with_catalog(|g| {
1845 if let Some(c) = g.by_dir.get_mut(&pack_dir) {
1846 c.idx_listing_verified = true;
1847 }
1848 });
1849 return Ok(false);
1850 }
1851 rescan_dir_indexes(objects_dir)?;
1852 Ok(true)
1853 }
1854
1855 fn load_pack_bytes_from_disk(
1856 pack_path: &Path,
1857 mtime: SystemTime,
1858 size: u64,
1859 ) -> Result<Arc<PackData>> {
1860 let store = crate::pack_store::PackStore::active_for_reads();
1861 let replaces_cached = store.with_catalog_read(|g| g.by_pack.contains_key(pack_path))
1865 || PACK_BYTES.with(|cell| cell.borrow().contains_key(pack_path));
1866 if replaces_cached {
1867 invalidate_thread_local_for_pack_path_change(pack_path);
1868 }
1869 let bytes = if store.bounded_reads() {
1870 PackData::open_random_access(pack_path)?
1871 } else {
1872 PackData::open_for_sparse_reads(pack_path, &store.sparse_read_files)?
1873 };
1874 store.with_catalog(|g| {
1875 drop_delta_entries_locked(g, pack_path, store.delta_bases());
1876 if replaces_cached {
1877 g.by_idx.remove(&pack_path.with_extension("idx"));
1878 g.validated_pack_counts.remove(pack_path);
1879 }
1880 if store.bounded_reads() {
1881 g.retain_bounded_pack(pack_path);
1882 }
1883 g.by_pack.insert(
1884 pack_path.to_path_buf(),
1885 CachedPack {
1886 mtime,
1887 size,
1888 bytes: Arc::clone(&bytes),
1889 },
1890 );
1891 });
1892 remember_pack_bytes(pack_path, &bytes);
1893 Ok(bytes)
1894 }
1895
1896 pub fn get_pack_bytes(pack_path: &Path) -> Result<Arc<PackData>> {
1901 if let Some(store) = alternate_read_store() {
1902 if pack_path.starts_with(store.objects_dir()) {
1903 return get_pack_bytes_for_store(&store, pack_path);
1904 }
1905 }
1906 if let Some(hit) = cached_pack_bytes(pack_path) {
1907 return Ok(hit);
1908 }
1909 let store = crate::pack_store::PackStore::active_for_reads();
1910 let hit = if store.bounded_reads() {
1911 store.with_catalog(|g| {
1912 let bytes = Arc::clone(&g.by_pack.get(pack_path)?.bytes);
1913 g.retain_bounded_pack(pack_path);
1914 Some(bytes)
1915 })
1916 } else {
1917 store.with_catalog_read(|g| g.by_pack.get(pack_path).map(|c| Arc::clone(&c.bytes)))
1918 };
1919 if let Some(hit) = hit {
1920 remember_pack_bytes(pack_path, &hit);
1921 return Ok(hit);
1922 }
1923 let (mtime, size) = file_signature(pack_path).ok_or_else(|| {
1924 Error::Io(io::Error::new(
1925 io::ErrorKind::NotFound,
1926 format!("pack not found: {}", pack_path.display()),
1927 ))
1928 })?;
1929 load_pack_bytes_from_disk(pack_path, mtime, size)
1930 }
1931
1932 fn revalidate_stale_pack_bytes_in_context(pack_path: &Path) -> Result<bool> {
1933 let Some((mtime, size)) = file_signature(pack_path) else {
1934 let store = crate::pack_store::PackStore::active_for_reads();
1935 let removed = store.with_catalog(|g| {
1936 let removed = g.by_pack.remove(pack_path).is_some();
1937 if removed {
1938 drop_delta_entries_locked(g, pack_path, store.delta_bases());
1939 }
1940 removed
1941 });
1942 invalidate_thread_local_for_pack_path_change(pack_path);
1943 super::drop_pack_read_info_cache_for(pack_path);
1944 return Ok(removed);
1945 };
1946 let store = crate::pack_store::PackStore::active_for_reads();
1947 let stale = store.with_catalog_read(|g| match g.by_pack.get(pack_path) {
1948 Some(c) => c.mtime != mtime || c.size != size,
1949 None => true,
1950 });
1951 if !stale {
1952 return Ok(false);
1953 }
1954 load_pack_bytes_from_disk(pack_path, mtime, size)?;
1955 super::drop_pack_read_info_cache_for(pack_path);
1956 Ok(true)
1957 }
1958
1959 pub fn revalidate_stale_pack_bytes(pack_path: &Path) -> Result<bool> {
1963 if crate::pack_store::PackStore::has_context() {
1964 return revalidate_stale_pack_bytes_in_context(pack_path);
1965 }
1966 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1967 crate::pack_store::PackStore::with_context(store, || {
1968 revalidate_stale_pack_bytes_in_context(pack_path)
1969 })
1970 }
1971
1972 pub fn reload_pack_bytes_after_parse_failure(pack_path: &Path) -> Result<bool> {
1980 if revalidate_stale_pack_bytes(pack_path)? {
1981 return Ok(true);
1982 }
1983 let Some((mtime, size)) = file_signature(pack_path) else {
1984 return Ok(false);
1985 };
1986 let hash_bytes = hash_bytes_for_pack_file(pack_path)?;
1987 let disk_fp = fingerprint_from_file(pack_path, hash_bytes)?;
1988 let needs_reload = with_current(|g| {
1989 g.by_pack.get(pack_path).is_some_and(|c| {
1990 pack_fingerprint_mismatch(&c.bytes, hash_bytes, &disk_fp).unwrap_or(true)
1991 })
1992 });
1993 if !needs_reload {
1994 return Ok(false);
1995 }
1996 load_pack_bytes_from_disk(pack_path, mtime, size)?;
1997 super::drop_pack_read_info_cache_for(pack_path);
1998 Ok(true)
1999 }
2000
2001 fn hash_bytes_for_pack_file(pack_path: &Path) -> Result<usize> {
2002 let idx_path = pack_path.with_extension("idx");
2003 if let Some(hash_bytes) =
2004 with_current(|g| g.by_idx.get(&idx_path).map(|c| c.idx.hash_bytes()))
2005 {
2006 return Ok(hash_bytes);
2007 }
2008 Ok(read_pack_index_no_verify(&idx_path)?.hash_bytes())
2009 }
2010
2011 fn pack_fingerprint_mismatch(
2012 cached: &PackData,
2013 hash_bytes: usize,
2014 disk: &PackFingerprint,
2015 ) -> Result<bool> {
2016 Ok(cached.fingerprint(hash_bytes)? != *disk)
2017 }
2018
2019 #[cfg(test)]
2020 pub fn test_reset_dir_rescan_count(pack_dir: &Path) {
2021 let objects_dir = pack_dir.parent().expect("pack dir under objects/");
2022 crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir).with_pack(|g| {
2023 g.by_dir.remove(pack_dir);
2024 g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
2025 });
2026 }
2027
2028 #[cfg(test)]
2029 pub fn test_reset_marker_stat_count() {
2030 TEST_MARKER_STAT_COUNT.with(|count| count.set(0));
2031 }
2032
2033 #[cfg(test)]
2034 #[must_use]
2035 pub fn test_marker_stat_count() -> u64 {
2036 TEST_MARKER_STAT_COUNT.with(std::cell::Cell::get)
2037 }
2038
2039 #[cfg(test)]
2040 #[must_use]
2041 pub fn test_dir_rescan_count(pack_dir: &Path) -> u64 {
2042 let objects_dir = pack_dir.parent().expect("pack dir under objects/");
2043 test_dir_rescan_count_on_store(
2044 &crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir),
2045 pack_dir,
2046 )
2047 }
2048
2049 #[cfg(test)]
2050 #[must_use]
2051 pub fn test_dir_rescan_count_on_store(
2052 store: &std::sync::Arc<crate::pack_store::PackStore>,
2053 pack_dir: &Path,
2054 ) -> u64 {
2055 store.with_pack(|g| g.test_dir_rescan_counts.get(pack_dir).copied().unwrap_or(0))
2056 }
2057
2058 #[cfg(test)]
2059 pub fn test_reset_dir_rescan_count_on_store(
2060 store: &std::sync::Arc<crate::pack_store::PackStore>,
2061 pack_dir: &Path,
2062 ) {
2063 store.with_pack(|g| {
2064 g.by_dir.remove(pack_dir);
2065 g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
2066 });
2067 }
2068
2069 #[cfg(test)]
2071 pub fn test_inject_stale_pack_bytes(pack_path: &Path, stale: Arc<PackData>) {
2072 crate::pack_store::PackStore::legacy_for_pack_path(pack_path).with_pack(|g| {
2073 let stamp = g
2074 .by_pack
2075 .get(pack_path)
2076 .map(|c| (c.mtime, c.size))
2077 .unwrap_or((SystemTime::UNIX_EPOCH, stale.len() as u64));
2078 g.by_pack.insert(
2079 pack_path.to_path_buf(),
2080 CachedPack {
2081 mtime: stamp.0,
2082 size: stamp.1,
2083 bytes: stale,
2084 },
2085 );
2086 });
2087 }
2088
2089 fn clear_inner(g: &mut CatalogState) {
2090 g.by_dir.clear();
2091 g.by_idx.clear();
2092 g.by_pack.clear();
2093 g.bounded_pack_order.clear();
2094 g.pack_ids.clear();
2095 }
2096
2097 pub fn set_delta_base_cache_byte_limit(limit: usize) {
2099 crate::pack_store::PackStore::active_for_reads()
2100 .delta_bases()
2101 .set_byte_limit(limit);
2102 }
2103
2104 #[cfg(test)]
2105 pub fn test_delta_base_cache_bytes_used(pack_path: &Path) -> usize {
2106 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
2107 .delta_bases()
2108 .bytes_used()
2109 }
2110
2111 #[cfg(test)]
2112 pub fn test_set_delta_base_cache_byte_limit(pack_path: &Path, limit: usize) {
2113 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
2114 .delta_bases()
2115 .set_byte_limit(limit);
2116 }
2117
2118 #[cfg(test)]
2119 pub fn test_delta_base_cached(pack_path: &Path, pack_id: u32, offset: u64) -> bool {
2120 crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
2121 .delta_bases()
2122 .contains(pack_id, offset)
2123 }
2124
2125 fn pack_id_for_in_context(pack_path: &Path) -> u32 {
2126 if let Some(id) = PACK_IDS.with(|cell| cell.borrow().get(pack_path.as_os_str()).copied()) {
2127 return id;
2128 }
2129 let store = crate::pack_store::PackStore::active_for_reads();
2130 if let Some(id) = store.with_catalog_read(|g| g.pack_ids.get(pack_path).copied()) {
2131 PACK_IDS.with(|cell| {
2132 cell.borrow_mut()
2133 .insert(pack_path.as_os_str().to_owned(), id);
2134 });
2135 return id;
2136 }
2137 store.with_catalog(|g| {
2138 if let Some(&id) = g.pack_ids.get(pack_path) {
2139 return id;
2140 }
2141 let id = g.next_pack_id;
2142 g.next_pack_id = g.next_pack_id.saturating_add(1);
2143 g.pack_ids.insert(pack_path.to_path_buf(), id);
2144 PACK_IDS.with(|cell| {
2145 cell.borrow_mut()
2146 .insert(pack_path.as_os_str().to_owned(), id);
2147 });
2148 id
2149 })
2150 }
2151
2152 pub fn pack_id_for(pack_path: &Path) -> u32 {
2154 if crate::pack_store::PackStore::has_context() {
2155 return pack_id_for_in_context(pack_path);
2156 }
2157 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
2158 crate::pack_store::PackStore::with_context(store, || pack_id_for_in_context(pack_path))
2159 }
2160
2161 fn drop_delta_entries_locked(
2163 g: &mut CatalogState,
2164 pack_path: &Path,
2165 delta: &ShardedDeltaBaseLru,
2166 ) {
2167 if let Some(&pack_id) = g.pack_ids.get(pack_path) {
2168 delta.drop_pack(pack_id);
2169 }
2170 }
2171
2172 pub fn get_delta_base(pack_id: u32, offset: u64) -> Option<(ObjectKind, Arc<[u8]>)> {
2174 crate::pack_store::PackStore::active_for_reads()
2175 .delta_bases()
2176 .get(DeltaCacheKey { pack_id, offset })
2177 }
2178
2179 #[must_use]
2180 pub fn delta_base_caching_enabled() -> bool {
2181 crate::pack_store::PackStore::active_for_reads()
2182 .delta_bases()
2183 .caching_enabled()
2184 }
2185
2186 pub fn put_delta_bases(kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
2188 if items.is_empty() {
2189 return;
2190 }
2191 crate::pack_store::PackStore::active_for_reads()
2192 .delta_bases()
2193 .put_many(kind, items);
2194 }
2195
2196 pub fn clear_delta_bases() {
2198 crate::pack_store::PackStore::active_for_reads()
2199 .delta_bases()
2200 .clear();
2201 }
2202
2203 pub fn refresh_pack_signature(pack_path: &Path, touched_at: SystemTime) {
2210 with_current(|g| {
2211 if let Some(c) = g.by_pack.get_mut(pack_path) {
2212 c.mtime = touched_at;
2213 }
2214 });
2215 }
2216
2217 #[cfg(test)]
2218 #[test]
2219 fn bounded_pack_cache_releases_inactive_readers() {
2220 let dir = tempfile::tempdir().expect("dir");
2221 let store = Arc::new(crate::pack_store::PackStore::for_bounded_reads(
2222 dir.path().to_path_buf(),
2223 ));
2224 crate::pack_store::PackStore::with_context(Arc::clone(&store), || {
2225 let mut previous = Vec::new();
2226 let hot_path = dir.path().join("0.pack");
2227 let mut hot = None;
2228 for i in 0..70 {
2229 let path = dir.path().join(format!("{i}.pack"));
2230 let mut bytes = vec![i as u8; 32768];
2231 bytes[..12].copy_from_slice(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00");
2232 fs::write(&path, bytes).expect("write");
2233 let pack = get_pack_bytes(&path).expect("pack");
2234 assert!(pack.uses_positioned_reads());
2235 previous.push(Arc::downgrade(&pack));
2236 hot.get_or_insert_with(|| Arc::clone(&pack));
2237 assert!(Arc::ptr_eq(
2238 &get_pack_bytes(&hot_path).expect("hot pack"),
2239 hot.as_ref().expect("hot")
2240 ));
2241 }
2242 warm_thread_local_from_store(&store);
2243 assert!(store.with_catalog_read(|g| g.by_pack.len()) <= 32);
2244 assert!(previous[1..32].iter().all(|pack| pack.upgrade().is_none()));
2245 assert!(previous[69].upgrade().is_some());
2246 let reloaded = get_pack_bytes(&dir.path().join("1.pack")).expect("reopen");
2247 assert_eq!(
2248 reloaded
2249 .read_range(100, 200, &mut Vec::new())
2250 .expect("read"),
2251 &[1; 100]
2252 );
2253 });
2254 }
2255}
2256
2257pub(crate) fn with_alternate_pack_store<R>(
2259 store: std::sync::Arc<crate::pack_store::PackStore>,
2260 f: impl FnOnce() -> R,
2261) -> R {
2262 pack_cache::with_alternate_pack_store(store, f)
2263}
2264
2265pub fn read_local_pack_indexes_cached(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
2277 if crate::pack_store::PackStore::has_context() {
2278 return pack_cache::get_dir_indexes(objects_dir);
2279 }
2280 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
2281 crate::pack_store::PackStore::with_context(store, || pack_cache::get_dir_indexes(objects_dir))
2282}
2283
2284pub fn reprepare_pack_directory_on_miss(objects_dir: &Path) -> Result<bool> {
2293 if crate::pack_store::PackStore::has_context() {
2294 return reprepare_pack_directory_on_miss_in_context(objects_dir);
2295 }
2296 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
2297 crate::pack_store::PackStore::with_context(store, || {
2298 reprepare_pack_directory_on_miss_in_context(objects_dir)
2299 })
2300}
2301
2302fn reprepare_pack_directory_on_miss_in_context(objects_dir: &Path) -> Result<bool> {
2303 let refreshed = pack_cache::reprepare_dir_on_miss(objects_dir)?;
2304 if refreshed {
2305 crate::midx::evict_midx_read_cache_for_pack_dir(&objects_dir.join("pack"));
2306 }
2307 Ok(refreshed)
2308}
2309
2310pub fn read_pack_index_cached(idx_path: &Path) -> Result<Arc<PackIndex>> {
2318 if crate::pack_store::PackStore::has_context() {
2319 return pack_cache::get_index(idx_path);
2320 }
2321 let pack_path = idx_path.with_extension("pack");
2322 let store = crate::pack_store::PackStore::legacy_for_pack_path(&pack_path);
2323 crate::pack_store::PackStore::with_context(store, || pack_cache::get_index(idx_path))
2324}
2325
2326pub fn read_pack_bytes_cached(pack_path: &Path) -> Result<Arc<PackData>> {
2332 if crate::pack_store::PackStore::has_context() {
2333 return pack_cache::get_pack_bytes(pack_path);
2334 }
2335 let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
2336 crate::pack_store::PackStore::with_context(store, || pack_cache::get_pack_bytes(pack_path))
2337}
2338
2339pub fn clear_pack_cache() {
2344 #[cfg(any(test, feature = "test-support"))]
2345 let _guard = pack_cache_test_guard();
2346 clear_pack_read_info_cache();
2347 pack_cache::clear_thread_local_dir_indexes();
2348 crate::pack_store::PackStore::invalidate_all_legacy();
2349}
2350
2351#[cfg(test)]
2353pub fn test_reset_pack_marker_stat_count() {
2354 pack_cache::test_reset_marker_stat_count();
2355}
2356
2357#[cfg(test)]
2359#[must_use]
2360pub fn test_pack_marker_stat_count() -> u64 {
2361 pack_cache::test_marker_stat_count()
2362}
2363
2364pub fn clear_pack_delta_base_cache() {
2366 pack_cache::clear_delta_bases();
2367}
2368
2369pub fn configure_delta_base_cache_from_config(cfg: Option<&crate::config::ConfigSet>) {
2371 let bytes = cfg
2372 .and_then(|c| c.get("core.deltaBaseCacheLimit"))
2373 .as_ref()
2374 .and_then(|v| crate::config::parse_i64(v).ok())
2375 .and_then(|n| usize::try_from(n).ok())
2376 .unwrap_or(pack_cache::DELTA_BASE_CACHE_DEFAULT);
2377 pack_cache::set_delta_base_cache_byte_limit(bytes);
2378}
2379
2380pub fn refresh_pack_bytes_signature(pack_path: &Path, touched_at: SystemTime) {
2383 pack_cache::refresh_pack_signature(pack_path, touched_at);
2384}
2385
2386pub fn collect_local_pack_info(objects_dir: &Path) -> Result<LocalPackInfo> {
2392 let indexes = read_local_pack_indexes(objects_dir)?;
2393 let mut info = LocalPackInfo::default();
2394 for idx in indexes {
2395 let pack_meta = fs::metadata(&idx.pack_path).map_err(Error::Io)?;
2396 let idx_meta = fs::metadata(&idx.idx_path).map_err(Error::Io)?;
2397 info.pack_count += 1;
2398 info.object_count += idx.len();
2399 info.size_bytes += pack_meta.len() + idx_meta.len();
2400 let hash_bytes = idx.hash_bytes();
2401 for entry in idx.iter() {
2402 if entry.oid().len() == hash_bytes {
2403 if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
2404 info.object_ids.insert(oid);
2405 }
2406 }
2407 }
2408 }
2409 Ok(info)
2410}
2411
2412pub fn write_v2_pack_index_with_trailer(
2427 idx_path: &Path,
2428 entries: &[(ObjectId, u64, u32)],
2429 pack_trailer: &[u8],
2430 hash_bytes: usize,
2431 sort_threads: Option<usize>,
2432) -> Result<()> {
2433 write_v2_pack_index_sorting(
2434 idx_path,
2435 &mut entries.to_vec(),
2436 pack_trailer,
2437 hash_bytes,
2438 sort_threads,
2439 )
2440}
2441
2442pub fn write_v2_pack_index(
2444 idx_path: &Path,
2445 pack_path: &Path,
2446 entries: &[(ObjectId, u64, u32)],
2447 hash_bytes: usize,
2448) -> Result<()> {
2449 let pack_bytes = fs::read(pack_path).map_err(Error::Io)?;
2450 if pack_bytes.len() < hash_bytes {
2451 return Err(Error::CorruptObject(
2452 "pack too small for idx trailer".into(),
2453 ));
2454 }
2455 let trailer = &pack_bytes[pack_bytes.len() - hash_bytes..];
2456 write_v2_pack_index_with_trailer(idx_path, entries, trailer, hash_bytes, None)
2457}
2458
2459const PARALLEL_INDEX_SORT_MIN: usize = 8_192;
2460
2461fn sort_index_entries(
2467 entries: &mut [(ObjectId, u64, u32)],
2468 sort_threads: Option<usize>,
2469) -> [u32; 256] {
2470 let lead = |entry: &(ObjectId, u64, u32)| usize::from(entry.0.as_bytes()[0]);
2471 let mut counts = [0usize; 256];
2472 for entry in entries.iter() {
2473 counts[lead(entry)] += 1;
2474 }
2475 let mut ends = [0usize; 256];
2476 let mut total = 0usize;
2477 for (end, count) in ends.iter_mut().zip(counts) {
2478 total += count;
2479 *end = total;
2480 }
2481 let mut next = [0usize; 256];
2482 next[1..].copy_from_slice(&ends[..255]);
2483 for bucket in 0..256 {
2484 while next[bucket] < ends[bucket] {
2485 let at = next[bucket];
2486 let home = lead(&entries[at]);
2487 if home == bucket {
2488 next[bucket] += 1;
2489 } else {
2490 entries.swap(at, next[home]);
2491 next[home] += 1;
2492 }
2493 }
2494 }
2495
2496 let by_oid =
2497 |a: &(ObjectId, u64, u32), b: &(ObjectId, u64, u32)| a.0.as_bytes().cmp(b.0.as_bytes());
2498 let threads = sort_threads.unwrap_or(1).max(1);
2499 if threads <= 1 || entries.len() < PARALLEL_INDEX_SORT_MIN {
2500 let mut start = 0;
2501 for end in ends {
2502 entries[start..end].sort_unstable_by(by_oid);
2503 start = end;
2504 }
2505 } else {
2506 let share = entries.len().div_ceil(threads);
2507 std::thread::scope(|scope| {
2508 let mut rest = &mut *entries;
2509 let mut taken = 0usize;
2510 for end in ends {
2511 if end - taken >= share || end == total {
2512 let (run, tail) = std::mem::take(&mut rest).split_at_mut(end - taken);
2513 rest = tail;
2514 taken = end;
2515 if run.len() > 1 {
2516 scope.spawn(move || run.sort_unstable_by(by_oid));
2517 }
2518 }
2519 }
2520 });
2521 }
2522 ends.map(|end| u32::try_from(end).unwrap_or(u32::MAX))
2523}
2524
2525pub(crate) fn write_v2_pack_index_sorting(
2536 idx_path: &Path,
2537 entries: &mut [(ObjectId, u64, u32)],
2538 pack_trailer: &[u8],
2539 hash_bytes: usize,
2540 sort_threads: Option<usize>,
2541) -> Result<()> {
2542 use std::io::Write;
2543
2544 let Some(algo) = HashAlgo::from_len(hash_bytes) else {
2545 return Err(Error::CorruptObject(format!(
2546 "unsupported index hash width {hash_bytes}"
2547 )));
2548 };
2549 if pack_trailer.len() != hash_bytes {
2550 return Err(Error::CorruptObject(format!(
2551 "pack trailer length {} does not match hash width {hash_bytes}",
2552 pack_trailer.len()
2553 )));
2554 }
2555 if let Some((oid, _, _)) = entries
2556 .iter()
2557 .find(|(oid, _, _)| oid.as_bytes().len() != hash_bytes)
2558 {
2559 return Err(Error::CorruptObject(format!(
2560 "OID width {} does not match index hash width {hash_bytes}",
2561 oid.as_bytes().len()
2562 )));
2563 }
2564 let fanout = sort_index_entries(entries, sort_threads);
2565
2566 let file = fs::File::create(idx_path).map_err(Error::Io)?;
2567 let mut out = std::io::BufWriter::with_capacity(256 * 1024, file);
2568 let mut hasher = algo.checksum_hasher();
2569 let mut put = |bytes: &[u8]| -> Result<()> {
2570 hasher.update(bytes);
2571 out.write_all(bytes).map_err(Error::Io)
2572 };
2573 put(b"\xfftOc")?;
2574 put(&2u32.to_be_bytes())?;
2575 for f in fanout {
2576 put(&f.to_be_bytes())?;
2577 }
2578 for (oid, _, _) in entries.iter() {
2579 put(oid.as_bytes())?;
2580 }
2581 for (_, _, crc) in entries.iter() {
2582 put(&crc.to_be_bytes())?;
2583 }
2584 let mut large = 0u32;
2585 for (_, off, _) in entries.iter() {
2586 match u32::try_from(*off) {
2587 Ok(small) if small < 0x8000_0000 => put(&small.to_be_bytes())?,
2588 _ => {
2589 if large >= 0x8000_0000 {
2590 return Err(Error::CorruptObject(
2591 "pack index large-offset table overflow".to_owned(),
2592 ));
2593 }
2594 put(&(0x8000_0000u32 | large).to_be_bytes())?;
2595 large += 1;
2596 }
2597 }
2598 }
2599 for (_, off, _) in entries.iter() {
2600 if *off >= 0x8000_0000 {
2601 put(&off.to_be_bytes())?;
2602 }
2603 }
2604 put(pack_trailer)?;
2605 let digest = hasher.finalize();
2606 out.write_all(&digest).map_err(Error::Io)?;
2607 out.flush().map_err(Error::Io)?;
2608 Ok(())
2609}
2610
2611#[must_use]
2612pub fn oid_bytes_to_hex(oid: &[u8]) -> String {
2614 hex::encode(oid)
2615}
2616
2617pub fn hash_object_bytes(kind: ObjectKind, data: &[u8], hash_bytes: usize) -> Result<Vec<u8>> {
2619 let Some(algo) = HashAlgo::from_len(hash_bytes) else {
2620 return Err(Error::CorruptObject(format!(
2621 "unsupported object hash width: {hash_bytes}"
2622 )));
2623 };
2624 Ok(crate::hash::hash_object(algo, kind, data)?
2625 .as_bytes()
2626 .to_vec())
2627}
2628
2629pub fn read_pack_index(idx_path: &Path) -> Result<PackIndex> {
2641 crate::pack_index::open_pack_index_file(idx_path, true)
2642}
2643
2644pub fn read_pack_index_no_verify(idx_path: &Path) -> Result<PackIndex> {
2650 crate::pack_index::open_pack_index_file(idx_path, false)
2651}
2652
2653#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2655pub enum PackedType {
2656 Commit,
2658 Tree,
2660 Blob,
2662 Tag,
2664 OfsDelta,
2666 RefDelta,
2668}
2669
2670impl PackedType {
2671 #[must_use]
2673 pub fn as_str(self) -> &'static str {
2674 match self {
2675 Self::Commit => "commit",
2676 Self::Tree => "tree",
2677 Self::Blob => "blob",
2678 Self::Tag => "tag",
2679 Self::OfsDelta => "ofs-delta",
2680 Self::RefDelta => "ref-delta",
2681 }
2682 }
2683}
2684
2685#[derive(Debug, Clone)]
2687pub struct VerifyObjectRecord {
2688 pub oid: Vec<u8>,
2690 pub packed_type: PackedType,
2692 pub size: u64,
2694 pub size_in_pack: u64,
2696 pub offset: u64,
2698 pub depth: Option<u64>,
2700 pub base_oid: Option<Vec<u8>>,
2702 pub base_pack_offset: Option<u64>,
2704}
2705
2706enum DeltaBaseLink {
2708 Oid(Vec<u8>),
2710 Offset(u64),
2712}
2713
2714fn resolve_delta_depth(
2725 i: usize,
2726 base_links: &[Option<DeltaBaseLink>],
2727 by_oid: &HashMap<Vec<u8>, usize>,
2728 by_offset_idx: &HashMap<u64, usize>,
2729 records: &mut [VerifyObjectRecord],
2730) -> Result<u64> {
2731 if let Some(d) = records[i].depth {
2732 return Ok(d);
2733 }
2734 let Some(link) = &base_links[i] else {
2735 return Ok(0);
2736 };
2737 let base_idx = match link {
2738 DeltaBaseLink::Oid(oid) => by_oid.get(oid).copied(),
2739 DeltaBaseLink::Offset(off) => by_offset_idx.get(off).copied(),
2740 };
2741 records[i].depth = Some(1);
2743 let depth = match base_idx {
2744 Some(b) if b != i => {
2745 resolve_delta_depth(b, base_links, by_oid, by_offset_idx, records)?.saturating_add(1)
2746 }
2747 _ => 1,
2749 };
2750 records[i].depth = Some(depth);
2751 Ok(depth)
2752}
2753
2754pub const DEFAULT_PACK_DEPTH: usize = 50;
2756
2757#[must_use]
2759pub fn max_verify_pack_delta_depth(records: &[VerifyObjectRecord]) -> u64 {
2760 records.iter().filter_map(|r| r.depth).max().unwrap_or(0)
2761}
2762
2763pub(crate) fn delta_cycle_breaks(
2773 starts: impl IntoIterator<Item = ObjectId>,
2774 base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2775) -> Vec<ObjectId> {
2776 let mut seen: crate::objects::OidHashMap<Option<usize>> = crate::objects::oid_hash_map();
2778 let mut path: Vec<ObjectId> = Vec::new();
2779 let mut breaks = Vec::new();
2780 for start in starts {
2781 let mut cur = Some(start);
2782 while let Some(oid) = cur {
2783 match seen.get(&oid) {
2784 Some(None) => break,
2785 Some(Some(pos)) => {
2786 breaks.extend(path[*pos..].iter().min().copied());
2787 break;
2788 }
2789 None => {
2790 seen.insert(oid, Some(path.len()));
2791 path.push(oid);
2792 cur = base_of(&oid);
2793 }
2794 }
2795 }
2796 for oid in path.drain(..) {
2797 seen.insert(oid, None);
2798 }
2799 }
2800 breaks
2801}
2802
2803pub fn apply_delta_depth_limit(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2813 for target in delta_cycle_breaks(map.keys().copied(), |oid| map.get(oid).copied()) {
2814 map.remove(&target);
2815 }
2816 cap_delta_chain_depth(map, max_depth);
2817}
2818
2819pub(crate) fn cap_delta_chain_depth(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2822 match delta_chain_cuts(
2823 map.keys().copied(),
2824 map.len(),
2825 |oid| map.get(oid).copied(),
2826 max_depth,
2827 ) {
2828 Some(cuts) => {
2829 for target in cuts {
2830 map.remove(&target);
2831 }
2832 }
2833 None => cap_delta_chain_depth_per_tip(map, max_depth),
2834 }
2835}
2836
2837pub(crate) fn delta_chain_cuts(
2845 targets: impl IntoIterator<Item = ObjectId>,
2846 len: usize,
2847 base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2848 max_depth: usize,
2849) -> Option<Vec<ObjectId>> {
2850 let modulus = max_depth.saturating_add(1);
2851 let ends = delta_chain_ends(targets, len, base_of)?;
2852 Some(
2853 ends.by_target
2854 .into_iter()
2855 .filter(|(_, (_, depth))| (*depth as usize).is_multiple_of(modulus))
2856 .map(|(target, _)| target)
2857 .collect(),
2858 )
2859}
2860
2861pub(crate) struct DeltaChainEnds {
2864 by_target: crate::objects::OidHashMap<(u32, u32)>,
2866 ends: Vec<ObjectId>,
2868}
2869
2870impl DeltaChainEnds {
2871 pub(crate) fn get(&self, target: &ObjectId) -> Option<(ObjectId, u32)> {
2873 let &(end, depth) = self.by_target.get(target)?;
2874 Some((self.ends[end as usize], depth))
2875 }
2876}
2877
2878pub(crate) fn delta_chain_ends(
2884 targets: impl IntoIterator<Item = ObjectId>,
2885 len: usize,
2886 base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2887) -> Option<DeltaChainEnds> {
2888 let mut by_target: crate::objects::OidHashMap<(u32, u32)> =
2889 crate::objects::oid_hash_map_with_capacity(len);
2890 let mut ends: Vec<ObjectId> = Vec::new();
2891 let mut path: Vec<ObjectId> = Vec::new();
2892 for start in targets {
2893 if by_target.contains_key(&start) {
2894 continue;
2895 }
2896 path.clear();
2897 let mut cur = start;
2898 let (end, mut depth) = loop {
2899 if let Some(&known) = by_target.get(&cur) {
2900 break known;
2901 }
2902 let Some(base) = base_of(&cur) else {
2903 let end = u32::try_from(ends.len()).ok()?;
2904 ends.push(cur);
2905 break (end, 0);
2906 };
2907 if path.len() >= len {
2909 return None;
2910 }
2911 path.push(cur);
2912 cur = base;
2913 };
2914 for &target in path.iter().rev() {
2915 depth += 1;
2916 by_target.insert(target, (end, depth));
2917 }
2918 }
2919 Some(DeltaChainEnds { by_target, ends })
2920}
2921
2922fn cap_delta_chain_depth_per_tip(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2925 let keys: Vec<ObjectId> = map.keys().copied().collect();
2926 let value_set: HashSet<ObjectId> = map.values().copied().collect();
2927 let mut tips: Vec<ObjectId> = keys
2928 .into_iter()
2929 .filter(|k| !value_set.contains(k))
2930 .collect();
2931 tips.sort();
2932
2933 let modulus = max_depth.saturating_add(1);
2934 let mut snip: HashSet<ObjectId> = HashSet::new();
2935
2936 for tip in tips {
2937 let mut chain: Vec<ObjectId> = Vec::new();
2938 let mut cur = tip;
2939 let mut seen = HashSet::new();
2940 while seen.insert(cur) {
2941 chain.push(cur);
2942 let Some(&b) = map.get(&cur) else {
2943 break;
2944 };
2945 cur = b;
2946 }
2947
2948 let n = chain.len();
2949 if n < 2 || n - 1 <= max_depth {
2950 continue;
2951 }
2952
2953 let mut total_depth = (n - 1) as u32;
2954 for &oid in &chain {
2955 let assigned = (total_depth as usize) % modulus;
2956 total_depth = total_depth.saturating_sub(1);
2957 if assigned == 0 {
2958 snip.insert(oid);
2959 }
2960 }
2961 }
2962
2963 for oid in snip {
2964 map.remove(&oid);
2965 }
2966}
2967
2968#[derive(Debug)]
2973pub(crate) struct VerifiedPackForFsck {
2974 pub idx: PackIndex,
2975 pub pack_bytes: Arc<PackData>,
2977 pub records: Vec<VerifyObjectRecord>,
2978}
2979
2980pub(crate) fn verify_pack_for_fsck(idx_path: &Path) -> Result<VerifiedPackForFsck> {
2988 let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, true)?;
2989 Ok(VerifiedPackForFsck {
2990 idx,
2991 pack_bytes,
2992 records,
2993 })
2994}
2995
2996#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2998pub(crate) enum PackFsckTrailerIssue {
2999 PackChecksum,
3001 IdxMismatch,
3003}
3004
3005pub(crate) fn prepare_pack_for_object_fsck(
3009 idx_path: &Path,
3010) -> Result<(VerifiedPackForFsck, Option<PackFsckTrailerIssue>)> {
3011 let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, false)?;
3012 let trailer = pack_trailer_fsck_issue(idx_path, &idx, &pack_bytes);
3013 Ok((
3014 VerifiedPackForFsck {
3015 idx,
3016 pack_bytes,
3017 records,
3018 },
3019 trailer,
3020 ))
3021}
3022
3023fn pack_trailer_fsck_issue(
3024 idx_path: &Path,
3025 idx: &PackIndex,
3026 pack_bytes: &[u8],
3027) -> Option<PackFsckTrailerIssue> {
3028 let hb = idx.hash_bytes();
3029 let algo = HashAlgo::from_len(hb)?;
3030 if verify_trailer(algo, pack_bytes).is_err() {
3031 return Some(PackFsckTrailerIssue::PackChecksum);
3032 }
3033 let idx_file_bytes = fs::read(idx_path).ok()?;
3034 if idx_file_bytes.len() >= 2 * hb {
3035 let pack_end = pack_bytes.len().checked_sub(hb)?;
3036 let n = idx_file_bytes.len();
3037 let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
3038 if embedded != &pack_bytes[pack_end..] {
3039 return Some(PackFsckTrailerIssue::IdxMismatch);
3040 }
3041 }
3042 None
3043}
3044
3045pub fn verify_pack_and_collect(idx_path: &Path) -> Result<Vec<VerifyObjectRecord>> {
3051 let VerifiedPackForFsck {
3052 idx,
3053 pack_bytes,
3054 records,
3055 } = verify_pack_for_fsck(idx_path)?;
3056 let hb = idx.hash_bytes();
3057 for entry in idx.iter() {
3058 let obj = read_object_from_pack_bytes(&pack_bytes, &idx, entry.oid())?;
3059 let computed = hash_object_bytes(obj.kind, &obj.data, hb)?;
3060 if computed.as_slice() != entry.oid() {
3061 return Err(Error::CorruptObject(format!(
3062 "pack object hash mismatch at offset {} (index says {})",
3063 entry.offset()?,
3064 oid_bytes_to_hex(entry.oid())
3065 )));
3066 }
3067 }
3068 Ok(records)
3069}
3070
3071fn verify_pack_structure_and_records(
3072 idx_path: &Path,
3073 verify_pack_trailer: bool,
3074) -> Result<(PackIndex, Arc<PackData>, Vec<VerifyObjectRecord>)> {
3075 let idx = read_pack_index(idx_path)?;
3076 let idx_file_bytes = fs::read(idx_path).map_err(Error::Io)?;
3077 let pack_bytes = PackData::open_with_min_len(&idx.pack_path, 0)?;
3078 let hb = idx.hash_bytes();
3079 if pack_bytes.len() < 12 + hb {
3080 return Err(Error::CorruptObject(format!(
3081 "pack file {} is too small",
3082 idx.pack_path.display()
3083 )));
3084 }
3085 let pack_end = pack_bytes.len() - hb;
3086 let Some(algo) = HashAlgo::from_len(hb) else {
3087 return Err(Error::CorruptObject(format!(
3088 "unsupported OID width {hb} for pack {}",
3089 idx.pack_path.display()
3090 )));
3091 };
3092 if verify_pack_trailer {
3093 verify_trailer(algo, &pack_bytes).map_err(|e| {
3094 Error::CorruptObject(format!(
3095 "pack trailing checksum mismatch for {}: {e}",
3096 idx.pack_path.display()
3097 ))
3098 })?;
3099 if idx_file_bytes.len() >= 2 * hb {
3102 let n = idx_file_bytes.len();
3103 let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
3104 if embedded != &pack_bytes[pack_end..] {
3105 return Err(Error::CorruptObject(format!(
3106 "pack checksum in index does not match {}",
3107 idx.pack_path.display()
3108 )));
3109 }
3110 }
3111 }
3112 if &pack_bytes[0..4] != b"PACK" {
3113 return Err(Error::CorruptObject(format!(
3114 "pack file {} has invalid signature",
3115 idx.pack_path.display()
3116 )));
3117 }
3118 let version = u32::from_be_bytes(pack_bytes[4..8].try_into().unwrap_or([0, 0, 0, 0]));
3119 if version != 2 && version != 3 {
3120 return Err(Error::CorruptObject(format!(
3121 "unsupported pack version {} in {}",
3122 version,
3123 idx.pack_path.display()
3124 )));
3125 }
3126 let count = u32::from_be_bytes(pack_bytes[8..12].try_into().unwrap_or([0, 0, 0, 0])) as usize;
3127 if count != idx.len() {
3128 return Err(Error::CorruptObject(format!(
3129 "pack/index object count mismatch for {}",
3130 idx.pack_path.display()
3131 )));
3132 }
3133
3134 let mut by_offset: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
3135 for entry in idx.iter() {
3136 by_offset.insert(entry.offset()?, entry.oid().to_vec());
3137 }
3138 let offsets: Vec<u64> = by_offset.keys().copied().collect();
3139 if offsets.is_empty() {
3140 return Ok((idx, pack_bytes, Vec::new()));
3141 }
3142
3143 let mut by_oid: HashMap<Vec<u8>, usize> = HashMap::new();
3144 let mut by_offset_idx: HashMap<u64, usize> = HashMap::new();
3145 let mut records: Vec<VerifyObjectRecord> = Vec::with_capacity(offsets.len());
3146 let mut base_links: Vec<Option<DeltaBaseLink>> = Vec::with_capacity(offsets.len());
3150 for (i, offset) in offsets.iter().copied().enumerate() {
3151 let oid = by_offset.get(&offset).cloned().ok_or_else(|| {
3152 Error::CorruptObject(format!("missing object id for offset {}", offset))
3153 })?;
3154 let next_off = offsets
3155 .get(i + 1)
3156 .copied()
3157 .unwrap_or((pack_bytes.len() - hb) as u64);
3158 if next_off <= offset || next_off > (pack_bytes.len() - hb) as u64 {
3159 return Err(Error::CorruptObject(format!(
3160 "invalid object boundaries at offset {} in {}",
3161 offset,
3162 idx.pack_path.display()
3163 )));
3164 }
3165 let mut p = offset as usize;
3166 let (packed_type, size) = parse_pack_object_header(&pack_bytes, &mut p)?;
3167 let mut base_oid: Option<Vec<u8>> = None;
3168 let mut base_link: Option<DeltaBaseLink> = None;
3169
3170 match packed_type {
3171 PackedType::RefDelta => {
3172 if p + hb > pack_bytes.len() {
3173 return Err(Error::CorruptObject(format!(
3174 "truncated ref-delta base at offset {}",
3175 offset
3176 )));
3177 }
3178 let raw = pack_bytes[p..p + hb].to_vec();
3179 base_oid = Some(raw.clone());
3180 base_link = Some(DeltaBaseLink::Oid(raw));
3181 }
3182 PackedType::OfsDelta => {
3183 let base_offset = parse_ofs_delta_base(&pack_bytes, &mut p, offset)?;
3184 base_link = Some(DeltaBaseLink::Offset(base_offset));
3185 }
3186 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {}
3187 }
3188
3189 let size_in_pack = next_off - offset;
3190 let base_pack_offset = match &base_link {
3191 Some(DeltaBaseLink::Offset(off)) => Some(*off),
3192 _ => None,
3193 };
3194 records.push(VerifyObjectRecord {
3195 oid: oid.clone(),
3196 packed_type,
3197 size,
3198 size_in_pack,
3199 offset,
3200 depth: None,
3201 base_oid,
3202 base_pack_offset,
3203 });
3204 base_links.push(base_link);
3205 by_oid.insert(oid, i);
3206 by_offset_idx.insert(offset, i);
3207 }
3208
3209 for i in 0..records.len() {
3213 if base_links[i].is_some() {
3214 let _ = resolve_delta_depth(i, &base_links, &by_oid, &by_offset_idx, &mut records)?;
3215 }
3216 }
3217
3218 Ok((idx, pack_bytes, records))
3219}
3220
3221pub fn read_alternates_recursive(objects_dir: &Path) -> Result<Vec<PathBuf>> {
3227 let mut visited = HashSet::new();
3228 let mut out = Vec::new();
3229 read_alternates_inner(objects_dir, &mut visited, &mut out, 0)?;
3230 Ok(out)
3231}
3232
3233const MAX_ALTERNATE_DEPTH: usize = 5;
3235
3236fn parse_alternates_file_line(raw: &str) -> Option<PathBuf> {
3241 let line = raw.trim();
3242 if line.is_empty() || line.starts_with('#') {
3243 return None;
3244 }
3245 if line.starts_with('"') {
3246 let path = unquote_alternates_c_style(line)?;
3247 if path.is_empty() {
3248 return None;
3249 }
3250 return Some(PathBuf::from(path));
3251 }
3252 Some(PathBuf::from(line))
3253}
3254
3255fn unquote_alternates_c_style(line: &str) -> Option<String> {
3257 let bytes = line.as_bytes();
3258 if bytes.first() != Some(&b'"') {
3259 return None;
3260 }
3261 let mut out = Vec::new();
3262 let mut i = 1usize;
3263 while i < bytes.len() {
3264 if bytes[i] == b'"' {
3265 return String::from_utf8(out).ok();
3266 }
3267 if bytes[i] == b'\\' {
3268 i += 1;
3269 if i >= bytes.len() {
3270 return None;
3271 }
3272 match bytes[i] {
3273 b'\\' => out.push(b'\\'),
3274 b'"' => out.push(b'"'),
3275 b'a' => out.push(7),
3276 b'b' => out.push(8),
3277 b'f' => out.push(12),
3278 b'n' => out.push(b'\n'),
3279 b'r' => out.push(b'\r'),
3280 b't' => out.push(b'\t'),
3281 b'v' => out.push(11),
3282 c if c.is_ascii_digit() => {
3283 if i + 2 >= bytes.len() {
3284 return None;
3285 }
3286 let oct = std::str::from_utf8(&bytes[i..i + 3]).ok()?;
3287 out.push(u8::from_str_radix(oct, 8).ok()?);
3288 i += 2;
3289 }
3290 other => out.push(other),
3291 }
3292 i += 1;
3293 continue;
3294 }
3295 out.push(bytes[i]);
3296 i += 1;
3297 }
3298 None
3299}
3300
3301fn read_alternates_inner(
3302 objects_dir: &Path,
3303 visited: &mut HashSet<PathBuf>,
3304 out: &mut Vec<PathBuf>,
3305 depth: usize,
3306) -> Result<()> {
3307 if depth > MAX_ALTERNATE_DEPTH {
3308 return Ok(());
3309 }
3310 let alt_file = objects_dir.join("info").join("alternates");
3314 let text = match fs::read_to_string(&alt_file) {
3315 Ok(text) => text,
3316 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
3317 Err(err) => return Err(Error::Io(err)),
3318 };
3319 let canonical = canonical_or_self(objects_dir);
3320
3321 for raw in text.lines() {
3322 let Some(path) = parse_alternates_file_line(raw) else {
3323 continue;
3324 };
3325 let candidate = if path.is_absolute() {
3326 path
3327 } else {
3328 canonical.join(path)
3329 };
3330 let candidate = canonical_or_self(&candidate);
3331 if visited.insert(candidate.clone()) {
3332 out.push(candidate.clone());
3333 read_alternates_inner(&candidate, visited, out, depth + 1)?;
3334 }
3335 }
3336 Ok(())
3337}
3338
3339fn canonical_or_self(path: &Path) -> PathBuf {
3340 path.canonicalize_simplified()
3341 .unwrap_or_else(|_| path.to_path_buf())
3342}
3343
3344fn packed_type_to_kind(pt: PackedType) -> Result<ObjectKind> {
3346 match pt {
3347 PackedType::Commit => Ok(ObjectKind::Commit),
3348 PackedType::Tree => Ok(ObjectKind::Tree),
3349 PackedType::Blob => Ok(ObjectKind::Blob),
3350 PackedType::Tag => Ok(ObjectKind::Tag),
3351 PackedType::OfsDelta | PackedType::RefDelta => Err(Error::CorruptObject(
3352 "cannot convert delta type to object kind directly".to_owned(),
3353 )),
3354 }
3355}
3356
3357thread_local! {
3359 static DELTA_SIZE_ZLIB: std::cell::RefCell<crate::zlib_inflate::ZlibInflateScratch> =
3360 std::cell::RefCell::new(crate::zlib_inflate::ZlibInflateScratch::default());
3361}
3362thread_local! {
3364 static PACK_READ_INFO_CACHE: std::cell::RefCell<HashMap<PackReadInfoCacheKey, ObjectInfo>> =
3365 std::cell::RefCell::new(HashMap::new());
3366}
3367
3368const PACK_READ_INFO_CACHE_CAP: usize = 600_000;
3369
3370#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3371struct PackReadInfoCacheKey {
3372 pack_id: u32,
3373 offset: u64,
3374}
3375
3376fn pack_read_info_cache_key(pack_path: &Path, offset: u64) -> Option<PackReadInfoCacheKey> {
3377 if !crate::pack_store::PackStore::has_context() {
3378 return None;
3379 }
3380 Some(PackReadInfoCacheKey {
3381 pack_id: pack_cache::pack_id_for(pack_path),
3382 offset,
3383 })
3384}
3385
3386pub(crate) fn clear_pack_read_info_cache() {
3388 PACK_READ_INFO_CACHE.with(|c| c.borrow_mut().clear());
3389}
3390
3391fn drop_pack_read_info_cache_for(pack_path: &Path) {
3392 let pack_id = pack_cache::pack_id_for(pack_path);
3393 PACK_READ_INFO_CACHE.with(|c| {
3394 c.borrow_mut().retain(|k, _| k.pack_id != pack_id);
3395 });
3396}
3397
3398fn cached_pack_read_info(pack_path: &Path, offset: u64) -> Option<ObjectInfo> {
3399 let key = pack_read_info_cache_key(pack_path, offset)?;
3400 PACK_READ_INFO_CACHE.with(|c| c.borrow().get(&key).cloned())
3401}
3402
3403fn remember_pack_read_info(pack_path: &Path, offset: u64, info: ObjectInfo) {
3404 let Some(key) = pack_read_info_cache_key(pack_path, offset) else {
3405 return;
3406 };
3407 PACK_READ_INFO_CACHE.with(|c| {
3408 let mut g = c.borrow_mut();
3409 if g.len() >= PACK_READ_INFO_CACHE_CAP {
3410 g.clear();
3411 }
3412 g.insert(key, info);
3413 });
3414}
3415pub(crate) fn read_delta_result_size_from_pack_zlib(bytes: &[u8], pos: &mut usize) -> Result<u64> {
3419 DELTA_SIZE_ZLIB.with(|slot| slot.borrow_mut().read_pack_delta_result_size(bytes, pos))
3420}
3421
3422fn decompress_pack_data_into(
3423 bytes: &[u8],
3424 pos: &mut usize,
3425 expected_size: u64,
3426 scratch: &mut ZlibInflateScratch,
3427 out: &mut Vec<u8>,
3428) -> Result<()> {
3429 scratch.decompress_fixed_into(bytes, pos, expected_size, out)
3430}
3431
3432const MAX_DELTA_CHAIN_READ_DEPTH: usize = 4096;
3441
3442#[derive(Clone)]
3444struct PendingDeltaFrame {
3445 pack_id: u32,
3446 offset: u64,
3447 delta: Vec<u8>,
3448}
3449
3450#[derive(Default)]
3454struct DeltaChainState {
3455 depth: usize,
3456 visited: HashSet<(u32, u64)>,
3457 pending: Vec<PendingDeltaFrame>,
3458 inflate: ZlibInflateScratch,
3459 whole_object: Vec<u8>,
3460 delta_decompress: Vec<u8>,
3461 chain_a: Vec<u8>,
3462 chain_b: Vec<u8>,
3463}
3464
3465thread_local! {
3467 static PACK_READ_DELTA_STATE: std::cell::RefCell<DeltaChainState> =
3468 std::cell::RefCell::new(DeltaChainState::default());
3469}
3470
3471fn with_pack_read_delta_state<R>(
3472 start_depth: usize,
3473 f: impl FnOnce(&mut DeltaChainState) -> R,
3474) -> R {
3475 PACK_READ_DELTA_STATE.with(|slot| {
3476 let mut state = slot.borrow_mut();
3477 state.depth = start_depth;
3478 state.visited.clear();
3479 f(&mut state)
3480 })
3481}
3482
3483impl DeltaChainState {
3484 fn visit(&mut self, pack_id: u32, offset: u64) -> Result<()> {
3485 if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
3486 return Err(Error::DeltaChainTooDeep {
3487 limit: MAX_DELTA_CHAIN_READ_DEPTH,
3488 });
3489 }
3490 if !self.visited.insert((pack_id, offset)) {
3491 return Err(Error::DeltaChainTooDeep {
3492 limit: MAX_DELTA_CHAIN_READ_DEPTH,
3493 });
3494 }
3495 Ok(())
3496 }
3497
3498 fn next_hop(&mut self) -> Result<()> {
3499 self.depth += 1;
3500 if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
3501 return Err(Error::DeltaChainTooDeep {
3502 limit: MAX_DELTA_CHAIN_READ_DEPTH,
3503 });
3504 }
3505 Ok(())
3506 }
3507}
3508
3509fn copy_payload_preserve_capacity(out: &mut Vec<u8>, payload: &[u8]) {
3511 out.clear();
3512 out.extend_from_slice(payload);
3513}
3514
3515#[allow(clippy::too_many_arguments)]
3518fn apply_delta_chain_forward_into(
3519 out: &mut Vec<u8>,
3520 kind: ObjectKind,
3521 base_data: &[u8],
3522 whole_base: Option<(u32, u64)>,
3523 pending: &[PendingDeltaFrame],
3524 result_pack_id: u32,
3525 result_offset: u64,
3526 populate_delta_lru: bool,
3527 state: &mut DeltaChainState,
3528) -> Result<()> {
3529 if pending.is_empty() {
3530 copy_payload_preserve_capacity(out, base_data);
3531 return Ok(());
3532 }
3533
3534 let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
3535 let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
3536 Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
3537 if caching {
3538 if let Some((pid, off)) = whole_base {
3539 cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
3540 }
3541 }
3542
3543 let chain_a = &mut state.chain_a;
3544 let chain_b = &mut state.chain_b;
3545 let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
3546 let last = rev
3547 .pop()
3548 .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
3549 if rev.is_empty() {
3550 apply_delta_into(out, base_data, &last.delta)?;
3551 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3552 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3553 }
3554 if caching {
3555 pack_cache::put_delta_bases(kind, &cache_inserts);
3556 }
3557 return Ok(());
3558 }
3559
3560 let Some(first) = rev.first().copied() else {
3561 return Err(Error::CorruptObject(
3562 "delta chain missing base-adjacent frame".to_owned(),
3563 ));
3564 };
3565 apply_delta_into(chain_a, base_data, &first.delta)?;
3566 if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
3567 cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
3568 }
3569 let mut src_is_a = true;
3570 for frame in rev.iter().skip(1) {
3571 if src_is_a {
3572 apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
3573 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3574 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
3575 }
3576 } else {
3577 apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
3578 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3579 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
3580 }
3581 }
3582 src_is_a = !src_is_a;
3583 }
3584 let penultimate_base = if src_is_a {
3585 chain_a.as_slice()
3586 } else {
3587 chain_b.as_slice()
3588 };
3589 apply_delta_into(out, penultimate_base, &last.delta)?;
3590 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3591 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3592 }
3593 if caching {
3594 pack_cache::put_delta_bases(kind, &cache_inserts);
3595 }
3596 Ok(())
3597}
3598
3599fn commit_prefix_has_author(data: &[u8]) -> bool {
3600 let mut pos = 0usize;
3601 while pos < data.len() {
3602 let line_start = pos;
3603 let mut line_end = pos;
3604 while line_end < data.len() && data[line_end] != b'\n' {
3605 line_end += 1;
3606 }
3607 let line = &data[line_start..line_end];
3608 let after_nl = line_end.saturating_add(1);
3609 if line.is_empty() {
3610 return false;
3611 }
3612 if line.first() == Some(&b' ') {
3613 return false;
3614 }
3615 let Some(key_end) = line.iter().position(|&b| b == b' ') else {
3616 return false;
3617 };
3618 if &line[..key_end] == b"author" {
3619 return true;
3620 }
3621 pos = after_nl;
3622 }
3623 false
3624}
3625
3626#[allow(clippy::too_many_arguments)]
3627fn apply_delta_chain_forward_prefix_into(
3628 out: &mut Vec<u8>,
3629 kind: ObjectKind,
3630 base_data: &[u8],
3631 whole_base: Option<(u32, u64)>,
3632 pending: &[PendingDeltaFrame],
3633 result_pack_id: u32,
3634 result_offset: u64,
3635 populate_delta_lru: bool,
3636 state: &mut DeltaChainState,
3637 max_out: usize,
3638) -> Result<()> {
3639 if pending.is_empty() {
3640 out.clear();
3641 let take = base_data.len().min(max_out);
3642 out.extend_from_slice(&base_data[..take]);
3643 return Ok(());
3644 }
3645
3646 let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
3647 let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
3648 Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
3649 if caching {
3650 if let Some((pid, off)) = whole_base {
3651 cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
3652 }
3653 }
3654
3655 let chain_a = &mut state.chain_a;
3656 let chain_b = &mut state.chain_b;
3657 let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
3658 let last = rev
3659 .pop()
3660 .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
3661 let prefix_ok = |buf: &[u8]| commit_prefix_has_author(buf);
3662 if rev.is_empty() {
3663 apply_delta_prefix_into(out, base_data, &last.delta, max_out, prefix_ok)?;
3664 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3665 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3666 }
3667 if caching {
3668 pack_cache::put_delta_bases(kind, &cache_inserts);
3669 }
3670 return Ok(());
3671 }
3672
3673 let Some(first) = rev.first().copied() else {
3674 return Err(Error::CorruptObject(
3675 "delta chain missing base-adjacent frame".to_owned(),
3676 ));
3677 };
3678 apply_delta_into(chain_a, base_data, &first.delta)?;
3679 if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
3680 cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
3681 }
3682 let mut src_is_a = true;
3683 for frame in rev.iter().skip(1) {
3684 if src_is_a {
3685 apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
3686 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3687 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
3688 }
3689 } else {
3690 apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
3691 if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3692 cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
3693 }
3694 }
3695 src_is_a = !src_is_a;
3696 }
3697 let penultimate_base = if src_is_a {
3698 chain_a.as_slice()
3699 } else {
3700 chain_b.as_slice()
3701 };
3702 apply_delta_prefix_into(out, penultimate_base, &last.delta, max_out, prefix_ok)?;
3703 if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3704 cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3705 }
3706 if caching {
3707 pack_cache::put_delta_bases(kind, &cache_inserts);
3708 }
3709 Ok(())
3710}
3711
3712#[allow(clippy::too_many_arguments)]
3713fn finish_pack_read_prefix_with_pending(
3714 out: &mut Vec<u8>,
3715 kind: ObjectKind,
3716 base: &[u8],
3717 whole_base: Option<(u32, u64)>,
3718 result_pack_id: u32,
3719 result_offset: u64,
3720 populate_delta_lru: bool,
3721 state: &mut DeltaChainState,
3722) -> Result<ObjectKind> {
3723 let pending = std::mem::take(&mut state.pending);
3724 apply_delta_chain_forward_prefix_into(
3725 out,
3726 kind,
3727 base,
3728 whole_base,
3729 &pending,
3730 result_pack_id,
3731 result_offset,
3732 populate_delta_lru,
3733 state,
3734 COMMIT_HEADER_READ_PREFIX,
3735 )?;
3736 Ok(kind)
3737}
3738
3739#[allow(clippy::too_many_arguments)]
3740fn finish_pack_read_with_pending(
3741 out: &mut Vec<u8>,
3742 kind: ObjectKind,
3743 base: &[u8],
3744 whole_base: Option<(u32, u64)>,
3745 result_pack_id: u32,
3746 result_offset: u64,
3747 populate_delta_lru: bool,
3748 state: &mut DeltaChainState,
3749) -> Result<ObjectKind> {
3750 let pending = std::mem::take(&mut state.pending);
3751 apply_delta_chain_forward_into(
3752 out,
3753 kind,
3754 base,
3755 whole_base,
3756 &pending,
3757 result_pack_id,
3758 result_offset,
3759 populate_delta_lru,
3760 state,
3761 )?;
3762 Ok(kind)
3763}
3764
3765enum PackIndexHandle<'a> {
3768 Borrowed(&'a PackIndex),
3769 Shared(Arc<PackIndex>),
3770}
3771
3772impl<'a> Clone for PackIndexHandle<'a> {
3773 fn clone(&self) -> Self {
3774 match self {
3775 Self::Borrowed(idx) => Self::Borrowed(idx),
3776 Self::Shared(arc) => Self::Shared(Arc::clone(arc)),
3777 }
3778 }
3779}
3780
3781impl PackIndexHandle<'_> {
3782 fn get(&self) -> &PackIndex {
3783 match self {
3784 Self::Borrowed(idx) => idx,
3785 Self::Shared(idx) => idx.as_ref(),
3786 }
3787 }
3788}
3789
3790fn find_other_pack_index(
3791 objects_dir: &Path,
3792 current: &PackIndex,
3793 oid: &ObjectId,
3794) -> Result<Option<Arc<PackIndex>>> {
3795 for idx in read_local_pack_indexes_cached(objects_dir)? {
3796 if idx.idx_path == current.idx_path {
3797 continue;
3798 }
3799 if idx.contains(oid)? {
3800 return Ok(Some(Arc::clone(&idx)));
3801 }
3802 }
3803 Ok(None)
3804}
3805
3806fn rescue_in_pack_base(
3809 idx: &PackIndex,
3810 base_offset: u64,
3811 objects_dir: Option<&Path>,
3812 state: &mut DeltaChainState,
3813) -> Result<Option<(ObjectKind, Vec<u8>)>> {
3814 let Some(pos) = idx.find_position_by_offset_sorted(base_offset)? else {
3815 return Ok(None);
3816 };
3817 let entry_oid = idx.oid_at(pos);
3818 if entry_oid.len() != idx.hash_bytes() {
3819 return Ok(None);
3820 }
3821 let base_oid = ObjectId::from_bytes(entry_oid)?;
3822 let Some(dir) = objects_dir else {
3823 return Ok(None);
3824 };
3825 let loose = dir
3826 .join(base_oid.loose_prefix())
3827 .join(base_oid.loose_suffix());
3828 if loose.is_file() {
3829 if let Ok(obj) = crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid) {
3830 return Ok(Some((obj.kind, obj.data)));
3831 }
3832 }
3833 if let Some(other_idx) = find_other_pack_index(dir, idx, &base_oid)? {
3834 let Some(off) = other_idx.find_offset(&base_oid)? else {
3835 return Ok(None);
3836 };
3837 let caller_pending = std::mem::take(&mut state.pending);
3840 let rescued =
3841 resolve_pack_object_at(PackIndexHandle::Shared(other_idx), off, Some(dir), state);
3842 state.pending = caller_pending;
3843 if let Ok((kind, data)) = rescued {
3844 return Ok(Some((kind, data)));
3845 }
3846 }
3847 Ok(None)
3848}
3849
3850fn pack_bytes_parse_may_be_stale(err: &Error) -> bool {
3852 matches!(err, Error::CorruptObject(_) | Error::Zlib(_))
3853}
3854
3855fn resolve_pack_object_at(
3861 start_idx: PackIndexHandle<'_>,
3862 start_offset: u64,
3863 objects_dir: Option<&Path>,
3864 state: &mut DeltaChainState,
3865) -> Result<(ObjectKind, Vec<u8>)> {
3866 resolve_pack_object_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
3867}
3868
3869fn resolve_pack_object_at_into(
3870 start_idx: PackIndexHandle<'_>,
3871 start_offset: u64,
3872 objects_dir: Option<&Path>,
3873 state: &mut DeltaChainState,
3874 populate_delta_lru: bool,
3875 out: &mut Vec<u8>,
3876) -> Result<ObjectKind> {
3877 resolve_pack_object_at_with_bytes_into(
3878 start_idx,
3879 start_offset,
3880 objects_dir,
3881 state,
3882 None,
3883 populate_delta_lru,
3884 out,
3885 )
3886}
3887
3888fn resolve_pack_object_at_with_bytes(
3889 start_idx: PackIndexHandle<'_>,
3890 start_offset: u64,
3891 objects_dir: Option<&Path>,
3892 state: &mut DeltaChainState,
3893 pack_bytes_override: Option<&[u8]>,
3894) -> Result<(ObjectKind, Vec<u8>)> {
3895 let mut out = Vec::new();
3896 let kind = resolve_pack_object_at_with_bytes_into(
3897 start_idx,
3898 start_offset,
3899 objects_dir,
3900 state,
3901 pack_bytes_override,
3902 true,
3903 &mut out,
3904 )?;
3905 Ok((kind, out))
3906}
3907
3908fn resolve_pack_object_at_with_bytes_into(
3909 start_idx: PackIndexHandle<'_>,
3910 start_offset: u64,
3911 objects_dir: Option<&Path>,
3912 state: &mut DeltaChainState,
3913 pack_bytes_override: Option<&[u8]>,
3914 populate_delta_lru: bool,
3915 out: &mut Vec<u8>,
3916) -> Result<ObjectKind> {
3917 let pack_path = start_idx.get().pack_path.clone();
3918 let start_pack_id = pack_cache::pack_id_for(&pack_path);
3919 for attempt in 0..2 {
3920 match resolve_pack_object_at_body(
3921 start_idx.clone(),
3922 start_offset,
3923 start_pack_id,
3924 objects_dir,
3925 state,
3926 pack_bytes_override,
3927 populate_delta_lru,
3928 out,
3929 ) {
3930 Ok(kind) => return Ok(kind),
3931 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3932 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3933 state.visited.retain(|(id, _)| *id != start_pack_id);
3934 continue;
3935 }
3936 return Err(e);
3937 }
3938 Err(e) => return Err(e),
3939 }
3940 }
3941 unreachable!("at most two resolution attempts")
3942}
3943
3944#[allow(clippy::too_many_arguments)]
3945fn resolve_pack_object_at_body(
3946 mut cur_idx: PackIndexHandle<'_>,
3947 start_offset: u64,
3948 result_pack_id: u32,
3949 objects_dir: Option<&Path>,
3950 state: &mut DeltaChainState,
3951 pack_bytes_override: Option<&[u8]>,
3952 populate_delta_lru: bool,
3953 out: &mut Vec<u8>,
3954) -> Result<ObjectKind> {
3955 let result_offset = start_offset;
3956 let mut cur_offset = start_offset;
3957 state.pending.clear();
3958
3959 let mut cur_pack_path = cur_idx.get().pack_path.clone();
3960 let mut cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3961 let mut validated_pack_count = false;
3962
3963 enum PackBytesSource<'a> {
3964 Borrowed(&'a [u8]),
3965 Cached(Arc<PackData>),
3966 }
3967
3968 let mut pack_bytes = match pack_bytes_override {
3969 Some(b) => PackBytesSource::Borrowed(b),
3970 None => PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?),
3971 };
3972
3973 let mut compressed = Vec::new();
3974 loop {
3975 let idx_ref = cur_idx.get();
3976 if idx_ref.pack_path != cur_pack_path {
3977 cur_pack_path = idx_ref.pack_path.clone();
3978 pack_bytes = PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?);
3979 cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3980 validated_pack_count = false;
3981 }
3982 let pack_id = cur_pack_id;
3983 state.visit(pack_id, cur_offset)?;
3984
3985 if !validated_pack_count {
3986 let header: &[u8] = match &pack_bytes {
3987 PackBytesSource::Borrowed(b) => b,
3988 PackBytesSource::Cached(c) => c,
3989 };
3990 validate_pack_index_object_count(header, idx_ref)?;
3991 validated_pack_count = true;
3992 }
3993 let (bytes, mut pos) = match &pack_bytes {
3994 PackBytesSource::Borrowed(b) => (*b, cur_offset as usize),
3995 PackBytesSource::Cached(c) => {
3996 pack_entry_bytes(idx_ref, cur_offset, c, &mut compressed)?
3997 }
3998 };
3999 let object_start = cur_offset;
4000 let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
4001 Ok(v) => v,
4002 Err(err) => {
4003 if let Some((kind, data)) =
4004 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
4005 {
4006 return finish_pack_read_with_pending(
4007 out,
4008 kind,
4009 &data,
4010 None,
4011 result_pack_id,
4012 result_offset,
4013 populate_delta_lru,
4014 state,
4015 );
4016 }
4017 return Err(err);
4018 }
4019 };
4020
4021 match packed_type {
4022 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4023 let kind = packed_type_to_kind(packed_type)?;
4024 if state.pending.is_empty() {
4025 if let Err(err) =
4026 decompress_pack_data_into(bytes, &mut pos, size, &mut state.inflate, out)
4027 {
4028 if let Some((rescue_kind, rescued)) =
4029 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
4030 {
4031 return finish_pack_read_with_pending(
4032 out,
4033 rescue_kind,
4034 &rescued,
4035 None,
4036 result_pack_id,
4037 result_offset,
4038 populate_delta_lru,
4039 state,
4040 );
4041 }
4042 return Err(err);
4043 }
4044 return Ok(kind);
4045 }
4046 state.whole_object.clear();
4047 if let Err(err) = decompress_pack_data_into(
4048 bytes,
4049 &mut pos,
4050 size,
4051 &mut state.inflate,
4052 &mut state.whole_object,
4053 ) {
4054 if let Some((rescue_kind, rescued)) =
4055 rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
4056 {
4057 return finish_pack_read_with_pending(
4058 out,
4059 rescue_kind,
4060 &rescued,
4061 None,
4062 result_pack_id,
4063 result_offset,
4064 populate_delta_lru,
4065 state,
4066 );
4067 }
4068 return Err(err);
4069 }
4070 let whole = std::mem::take(&mut state.whole_object);
4071 return finish_pack_read_with_pending(
4072 out,
4073 kind,
4074 &whole,
4075 Some((pack_id, cur_offset)),
4076 result_pack_id,
4077 result_offset,
4078 populate_delta_lru,
4079 state,
4080 );
4081 }
4082 PackedType::OfsDelta => {
4083 state.next_hop()?;
4084 let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
4085 state.delta_decompress.clear();
4086 decompress_pack_data_into(
4087 bytes,
4088 &mut pos,
4089 size,
4090 &mut state.inflate,
4091 &mut state.delta_decompress,
4092 )?;
4093 state.pending.push(PendingDeltaFrame {
4094 pack_id,
4095 offset: object_start,
4096 delta: std::mem::take(&mut state.delta_decompress),
4097 });
4098 if let Some((base_kind, base_data)) =
4099 pack_cache::get_delta_base(pack_id, base_offset)
4100 {
4101 return finish_pack_read_with_pending(
4102 out,
4103 base_kind,
4104 base_data.as_ref(),
4105 None,
4106 result_pack_id,
4107 result_offset,
4108 populate_delta_lru,
4109 state,
4110 );
4111 }
4112 cur_offset = base_offset;
4113 }
4114 PackedType::RefDelta => {
4115 state.next_hop()?;
4116 let hb = idx_ref.hash_bytes();
4117 if pos + hb > bytes.len() {
4118 return Err(Error::CorruptObject(
4119 "truncated ref-delta base OID".to_owned(),
4120 ));
4121 }
4122 let base_raw = bytes[pos..pos + hb].to_vec();
4123 pos += hb;
4124 state.delta_decompress.clear();
4125 decompress_pack_data_into(
4126 bytes,
4127 &mut pos,
4128 size,
4129 &mut state.inflate,
4130 &mut state.delta_decompress,
4131 )?;
4132 state.pending.push(PendingDeltaFrame {
4133 pack_id,
4134 offset: object_start,
4135 delta: std::mem::take(&mut state.delta_decompress),
4136 });
4137
4138 if let Ok(base_oid) = ObjectId::from_bytes(base_raw.as_slice()) {
4139 if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
4140 if let Some((base_kind, base_data)) =
4141 pack_cache::get_delta_base(pack_id, base_offset)
4142 {
4143 return finish_pack_read_with_pending(
4144 out,
4145 base_kind,
4146 base_data.as_ref(),
4147 None,
4148 result_pack_id,
4149 result_offset,
4150 populate_delta_lru,
4151 state,
4152 );
4153 }
4154 cur_offset = base_offset;
4155 continue;
4156 }
4157 }
4158
4159 if hb == 20 {
4160 if let (Some(dir), Ok(base_oid)) =
4161 (objects_dir, ObjectId::from_bytes(base_raw.as_slice()))
4162 {
4163 let loose = dir
4164 .join(base_oid.loose_prefix())
4165 .join(base_oid.loose_suffix());
4166 if loose.is_file() {
4167 if let Ok(obj) =
4168 crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid)
4169 {
4170 return finish_pack_read_with_pending(
4171 out,
4172 obj.kind,
4173 &obj.data,
4174 None,
4175 result_pack_id,
4176 result_offset,
4177 populate_delta_lru,
4178 state,
4179 );
4180 }
4181 }
4182 if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
4183 let Some(off) = other_idx.find_offset(&base_oid)? else {
4184 return Err(Error::ObjectNotFound(base_oid.to_hex()));
4185 };
4186 cur_idx = PackIndexHandle::Shared(other_idx);
4187 cur_offset = off;
4188 continue;
4189 }
4190 }
4191 }
4192 return Err(Error::CorruptObject(format!(
4193 "ref-delta base {} not found in pack",
4194 oid_bytes_to_hex(&base_raw)
4195 )));
4196 }
4197 }
4198 }
4199}
4200
4201fn resolve_pack_object_info_at(
4203 start_idx: PackIndexHandle<'_>,
4204 start_offset: u64,
4205 objects_dir: Option<&Path>,
4206 state: &mut DeltaChainState,
4207) -> Result<ObjectInfo> {
4208 resolve_pack_object_info_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
4209}
4210
4211fn resolve_pack_object_info_at_body(
4212 cur_idx: PackIndexHandle<'_>,
4213 start_offset: u64,
4214 objects_dir: Option<&Path>,
4215 state: &mut DeltaChainState,
4216 pack_bytes_override: Option<&[u8]>,
4217) -> Result<ObjectInfo> {
4218 let cached_pack;
4219 let cur_bytes: &[u8] = if let Some(b) = pack_bytes_override {
4220 b
4221 } else {
4222 cached_pack = read_pack_bytes_cached(&cur_idx.get().pack_path)?;
4223 cached_pack.as_ref()
4224 };
4225 let mut cur_offset = start_offset;
4226 let mut result_size: Option<u64> = None;
4227
4228 loop {
4229 let idx_ref = cur_idx.get();
4230 let pack_id = pack_cache::pack_id_for(&idx_ref.pack_path);
4231 state.visit(pack_id, cur_offset)?;
4232
4233 if let Some(base) = cached_pack_read_info(&idx_ref.pack_path, cur_offset) {
4234 if let Some(size) = result_size {
4235 return Ok(ObjectInfo {
4236 kind: base.kind,
4237 size,
4238 });
4239 }
4240 return Ok(base);
4241 }
4242 let object_start = cur_offset;
4243 let mut pos = cur_offset as usize;
4244 let (packed_type, header_size) = parse_pack_object_header(cur_bytes, &mut pos)?;
4245
4246 match packed_type {
4247 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4248 let kind = packed_type_to_kind(packed_type)?;
4249 remember_pack_read_info(
4250 &idx_ref.pack_path,
4251 cur_offset,
4252 ObjectInfo {
4253 kind,
4254 size: header_size,
4255 },
4256 );
4257 let size = result_size.unwrap_or(header_size);
4258 return Ok(ObjectInfo { kind, size });
4259 }
4260 PackedType::OfsDelta => {
4261 state.next_hop()?;
4262 let base_offset = parse_ofs_delta_base(cur_bytes, &mut pos, object_start)?;
4263 if result_size.is_none() {
4264 result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
4265 }
4266 cur_offset = base_offset;
4267 }
4268 PackedType::RefDelta => {
4269 state.next_hop()?;
4270 let hb = idx_ref.hash_bytes();
4271 if pos + hb > cur_bytes.len() {
4272 return Err(Error::CorruptObject(
4273 "truncated ref-delta base OID".to_owned(),
4274 ));
4275 }
4276 let base_raw = &cur_bytes[pos..pos + hb];
4277 pos += hb;
4278 if result_size.is_none() {
4279 result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
4280 }
4281
4282 let base_oid = ObjectId::from_bytes(base_raw)?;
4283 if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
4284 cur_offset = base_offset;
4285 continue;
4286 }
4287
4288 if let Some(dir) = objects_dir {
4289 let loose = dir
4290 .join(base_oid.loose_prefix())
4291 .join(base_oid.loose_suffix());
4292 if loose.is_file() {
4293 let info = crate::odb::read_loose_object_info(&loose)?;
4294 let size = result_size.unwrap_or(info.size);
4295 return Ok(ObjectInfo {
4296 kind: info.kind,
4297 size,
4298 });
4299 }
4300 if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
4301 let Some(off) = other_idx.find_offset(&base_oid)? else {
4302 return Err(Error::ObjectNotFound(base_oid.to_hex()));
4303 };
4304 let base_info = resolve_pack_object_info_at(
4305 PackIndexHandle::Shared(other_idx),
4306 off,
4307 objects_dir,
4308 state,
4309 )?;
4310 let size = result_size.unwrap_or(base_info.size);
4311 return Ok(ObjectInfo {
4312 kind: base_info.kind,
4313 size,
4314 });
4315 }
4316 }
4317 return Err(Error::CorruptObject(format!(
4318 "ref-delta base {} not found in pack",
4319 oid_bytes_to_hex(base_raw)
4320 )));
4321 }
4322 }
4323 }
4324}
4325
4326fn read_pack_object_at(
4327 pack_bytes: &[u8],
4328 start_offset: u64,
4329 idx: &PackIndex,
4330 objects_dir: Option<&Path>,
4331 start_depth: usize,
4332) -> Result<(ObjectKind, Vec<u8>)> {
4333 let mut state = DeltaChainState {
4334 depth: start_depth,
4335 visited: HashSet::new(),
4336 ..Default::default()
4337 };
4338 resolve_pack_object_at_with_bytes(
4339 PackIndexHandle::Borrowed(idx),
4340 start_offset,
4341 objects_dir,
4342 &mut state,
4343 Some(pack_bytes),
4344 )
4345}
4346
4347pub fn read_object_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<Object> {
4356 if crate::pack_store::PackStore::has_context() {
4357 return read_object_from_pack_at_depth(idx, oid, 0);
4358 }
4359 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
4360 crate::pack_store::PackStore::with_context(store, || {
4361 read_object_from_pack_at_depth(idx, oid, 0)
4362 })
4363}
4364
4365pub fn read_object_from_pack_into(
4371 idx: &PackIndex,
4372 oid: &ObjectId,
4373 buf: &mut Vec<u8>,
4374) -> Result<ObjectKind> {
4375 let Some(offset) = idx.find_offset(oid)? else {
4376 return Err(Error::ObjectNotFound(oid.to_hex()));
4377 };
4378 if crate::pack_store::PackStore::has_context() {
4379 return read_object_at_depth_into(idx, offset, 0, false, buf);
4380 }
4381 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
4382 crate::pack_store::PackStore::with_context(store, || {
4383 read_object_at_depth_into(idx, offset, 0, false, buf)
4384 })
4385}
4386
4387pub fn read_object_info_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<ObjectInfo> {
4393 let Some(offset) = idx.find_offset(oid)? else {
4394 return Err(Error::ObjectNotFound(oid.to_hex()));
4395 };
4396 read_object_info_from_pack_at_offset(idx, offset)
4397}
4398
4399pub fn read_object_info_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<ObjectInfo> {
4405 let pack_path = idx.pack_path.clone();
4406 for attempt in 0..2 {
4407 let pack_bytes = read_pack_bytes_cached(&pack_path)?;
4408 if !pack_cache::pack_object_count_validated(&pack_path) {
4409 match validate_pack_index_object_count(&pack_bytes, idx) {
4410 Ok(()) => pack_cache::mark_pack_object_count_validated(&pack_path),
4411 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4412 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
4413 continue;
4414 }
4415 return Err(e);
4416 }
4417 Err(e) => return Err(e),
4418 }
4419 }
4420 let mut header = [0u8; 16];
4423 if let Some(len) = pack_bytes.read_sparse(offset, &mut header) {
4424 if let Some(info) = read_whole_pack_entry_info_fast(idx, 0, &header[..len]) {
4425 if info.kind == ObjectKind::Tag {
4426 remember_pack_read_info(&pack_path, offset, info);
4427 }
4428 return Ok(info);
4429 }
4430 }
4431 match read_object_info_from_pack_at_offset_with_bytes(idx, offset, pack_bytes.as_ref()) {
4432 Ok(info) => return Ok(info),
4433 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4434 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
4435 continue;
4436 }
4437 return Err(e);
4438 }
4439 Err(e) => return Err(e),
4440 }
4441 }
4442 unreachable!("at most two read attempts")
4443}
4444
4445const COMMIT_HEADER_BLOCK_WINDOW: usize = 4096;
4447
4448const COMMIT_HEADER_BLOCK_MAX: usize = 1024;
4451
4452const INFLATE_FAST_OUTPUT_SLACK: usize = 258;
4456
4457const COMMIT_HEADER_BLOCK_FIRST_READ: usize = 1024;
4460
4461const COMMIT_HEADER_BLOCK_INPUT_STEP: usize = 256;
4464
4465pub(crate) fn read_packed_commit_header_block_into(
4481 objects_dir: &Path,
4482 oid: &ObjectId,
4483 out: &mut Vec<u8>,
4484) -> Result<bool> {
4485 pack_cache::sync_thread_local_for_objects_dir(objects_dir);
4486 if let Some(idx) = pack_cache::hinted_pack_index(objects_dir) {
4487 if let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, &idx, oid)?
4488 {
4489 return Ok(read_commit_header_block_at(&idx, offset, out));
4490 }
4491 }
4492 for idx in pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)? {
4493 if let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, &idx, oid)?
4494 {
4495 pack_cache::remember_pack_hit(objects_dir, &idx);
4496 return Ok(read_commit_header_block_at(&idx, offset, out));
4497 }
4498 }
4499 Ok(false)
4500}
4501
4502pub(crate) fn read_packed_commit_header_blocks(
4514 objects_dir: &Path,
4515 oids: &[ObjectId],
4516 mut found: impl FnMut(usize, &[u8]),
4517) -> Result<()> {
4518 if oids.is_empty() {
4519 return Ok(());
4520 }
4521 pack_cache::sync_thread_local_for_objects_dir(objects_dir);
4522 let indexes = pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)?;
4523 let mut packs: Vec<Option<Option<Arc<PackData>>>> = vec![None; indexes.len()];
4524 let mut readers: Vec<Option<Option<crate::pack_index::PackIndexRowReader<'_>>>> =
4525 std::iter::repeat_with(|| None)
4526 .take(indexes.len())
4527 .collect();
4528 let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
4529 let mut out = Vec::with_capacity(COMMIT_HEADER_BLOCK_MAX + INFLATE_FAST_OUTPUT_SLACK);
4530 let mut last_hit = 0usize;
4531 WHOLE_OBJECT_ZLIB.with(|slot| {
4532 let mut zlib = slot.borrow_mut();
4533 for (i, oid) in oids.iter().enumerate() {
4534 let order =
4535 std::iter::once(last_hit).chain((0..indexes.len()).filter(|&p| p != last_hit));
4536 for p in order {
4537 let (Some(idx), Some(reader)) = (indexes.get(p), readers.get_mut(p)) else {
4538 continue;
4539 };
4540 let offset = match reader.get_or_insert_with(|| idx.row_reader()) {
4541 Some(reader) => reader.find_offset(oid)?,
4542 None => idx.find_offset(oid)?,
4543 };
4544 let Some(offset) = offset else {
4545 continue;
4546 };
4547 last_hit = p;
4548 let pack = packs[p].get_or_insert_with(|| validated_pack_bytes(idx));
4549 if let Some(pack) = pack {
4550 if read_commit_header_block_from(pack, offset, &mut window, &mut zlib, &mut out)
4551 {
4552 found(i, &out);
4553 }
4554 }
4555 break;
4556 }
4557 }
4558 Ok(())
4559 })
4560}
4561
4562fn validated_pack_bytes(idx: &PackIndex) -> Option<Arc<PackData>> {
4564 let pack_bytes = read_pack_bytes_cached(&idx.pack_path).ok()?;
4565 if !pack_cache::pack_object_count_validated(&idx.pack_path) {
4566 validate_pack_index_object_count(&pack_bytes, idx).ok()?;
4567 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
4568 }
4569 Some(pack_bytes)
4570}
4571
4572fn read_commit_header_block_at(idx: &PackIndex, offset: u64, out: &mut Vec<u8>) -> bool {
4573 let Some(pack_bytes) = validated_pack_bytes(idx) else {
4574 return false;
4575 };
4576 let mut window = [0u8; COMMIT_HEADER_BLOCK_WINDOW];
4577 WHOLE_OBJECT_ZLIB.with(|slot| {
4578 read_commit_header_block_from(
4579 &pack_bytes,
4580 offset,
4581 &mut window,
4582 &mut slot.borrow_mut(),
4583 out,
4584 )
4585 })
4586}
4587
4588fn read_commit_header_block_from(
4595 pack_bytes: &PackData,
4596 offset: u64,
4597 window: &mut [u8],
4598 zlib: &mut ZlibInflateScratch,
4599 out: &mut Vec<u8>,
4600) -> bool {
4601 let first_read = COMMIT_HEADER_BLOCK_FIRST_READ.min(window.len());
4602 let Some(len) = pack_bytes.read_sparse(offset, &mut window[..first_read]) else {
4603 return false;
4604 };
4605 let mut pos = 0usize;
4606 let Ok((PackedType::Commit, size)) = parse_pack_object_header(&window[..len], &mut pos) else {
4607 return false;
4608 };
4609 let want = usize::try_from(size).map_or(COMMIT_HEADER_BLOCK_MAX, |size| {
4610 size.min(COMMIT_HEADER_BLOCK_MAX)
4611 });
4612 if inflate_commit_header_block(&window[pos..len], size, want, zlib, out) {
4613 return true;
4614 }
4615 if len < first_read || first_read == window.len() {
4616 return false;
4617 }
4618 let Some(len) = pack_bytes.read_sparse(offset, window) else {
4619 return false;
4620 };
4621 inflate_commit_header_block(&window[pos..len], size, want, zlib, out)
4622}
4623
4624fn inflate_commit_header_block(
4631 compressed: &[u8],
4632 size: u64,
4633 want: usize,
4634 zlib: &mut ZlibInflateScratch,
4635 out: &mut Vec<u8>,
4636) -> bool {
4637 let inflated = zlib.decompress_prefix_until(
4638 compressed,
4639 size,
4640 COMMIT_HEADER_BLOCK_INPUT_STEP,
4641 out,
4642 |so_far| memchr::memmem::find(so_far, b"\n\n").map(|blank| blank + 2),
4643 );
4644 if inflated.is_err() {
4645 return false;
4646 }
4647 match memchr::memmem::find(out, b"\n\n") {
4648 Some(blank) if blank + 2 <= want => {
4649 out.truncate(blank + 2);
4650 true
4651 }
4652 _ => false,
4653 }
4654}
4655
4656fn read_object_info_from_pack_at_offset_with_bytes(
4657 idx: &PackIndex,
4658 offset: u64,
4659 pack_bytes: &[u8],
4660) -> Result<ObjectInfo> {
4661 if let Some(info) = read_whole_pack_entry_info_fast(idx, offset, pack_bytes) {
4662 return Ok(info);
4663 }
4664 let objects_dir = idx.pack_path.parent().and_then(Path::parent);
4665 with_pack_read_delta_state(0, |state| {
4666 resolve_pack_object_info_at_with_bytes(
4667 PackIndexHandle::Borrowed(idx),
4668 offset,
4669 objects_dir,
4670 state,
4671 Some(pack_bytes),
4672 )
4673 })
4674}
4675
4676fn resolve_pack_object_info_at_with_bytes(
4677 start_idx: PackIndexHandle<'_>,
4678 start_offset: u64,
4679 objects_dir: Option<&Path>,
4680 state: &mut DeltaChainState,
4681 pack_bytes_override: Option<&[u8]>,
4682) -> Result<ObjectInfo> {
4683 let idx_ref = start_idx.get();
4684 let pack_path = idx_ref.pack_path.clone();
4685 let start_pack_id = pack_cache::pack_id_for(&pack_path);
4686 if let Some(info) = cached_pack_read_info(&pack_path, start_offset) {
4687 return Ok(info);
4688 }
4689 for attempt in 0..2 {
4690 match resolve_pack_object_info_at_body(
4691 start_idx.clone(),
4692 start_offset,
4693 objects_dir,
4694 state,
4695 pack_bytes_override,
4696 ) {
4697 Ok(v) => {
4698 remember_pack_read_info(&pack_path, start_offset, v);
4699 return Ok(v);
4700 }
4701 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4702 if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
4703 state.visited.retain(|(id, _)| *id != start_pack_id);
4704 continue;
4705 }
4706 return Err(e);
4707 }
4708 Err(e) => return Err(e),
4709 }
4710 }
4711 unreachable!("at most two resolution attempts")
4712}
4713
4714#[inline]
4716fn read_whole_pack_entry_info_fast(
4717 _idx: &PackIndex,
4718 offset: u64,
4719 pack_bytes: &[u8],
4720) -> Option<ObjectInfo> {
4721 let mut pos = offset as usize;
4722 if pos >= pack_bytes.len() {
4723 return None;
4724 }
4725 let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
4726 match packed_type {
4727 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4728 let kind = packed_type_to_kind(packed_type).ok()?;
4729 Some(ObjectInfo {
4730 kind,
4731 size: header_size,
4732 })
4733 }
4734 PackedType::OfsDelta | PackedType::RefDelta => None,
4735 }
4736}
4737
4738thread_local! {
4740 static WHOLE_OBJECT_ZLIB: RefCell<ZlibInflateScratch> =
4741 RefCell::new(ZlibInflateScratch::default());
4742}
4743
4744pub(crate) const COMMIT_HEADER_READ_PREFIX: usize = 512;
4746
4747const COMMIT_PARENTS_WHOLE_READ_MAX: u64 = 1 << 20;
4750
4751const COMMIT_SUBJECT_READ_STEP: usize = 128;
4756
4757fn pack_entry_bytes<'a>(
4759 idx: &PackIndex,
4760 offset: u64,
4761 pack: &'a PackData,
4762 buf: &'a mut Vec<u8>,
4763) -> Result<(&'a [u8], usize)> {
4764 let start = usize::try_from(offset)
4765 .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
4766 if !pack.uses_positioned_reads() {
4767 return Ok((pack, start));
4768 }
4769 let pack_end = pack
4770 .len()
4771 .checked_sub(idx.hash_bytes())
4772 .ok_or_else(|| Error::CorruptObject("truncated pack trailer".to_owned()))?;
4773 let end = idx.next_pack_offset_after(offset, pack_end as u64)? as usize;
4774 Ok((pack.read_range(start, end, buf)?, 0))
4775}
4776
4777fn read_whole_pack_entry_object_fast(
4778 idx: &PackIndex,
4779 offset: u64,
4780 pack_bytes: &PackData,
4781) -> Option<Object> {
4782 let mut data = Vec::new();
4783 read_whole_pack_entry_object_into(idx, offset, pack_bytes, &mut data)
4784 .map(|kind| Object::new(kind, data))
4785}
4786
4787fn read_whole_pack_entry_object_into(
4788 idx: &PackIndex,
4789 offset: u64,
4790 pack_bytes: &PackData,
4791 out: &mut Vec<u8>,
4792) -> Option<ObjectKind> {
4793 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4794 if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
4795 out.clear();
4796 out.extend_from_slice(data.as_ref());
4797 return Some(kind);
4798 }
4799 if cached_pack_read_info(&idx.pack_path, offset)
4802 .is_some_and(|info| info.kind == ObjectKind::Tag)
4803 {
4804 let mut bytes = [0u8; 4096];
4805 if let Some(len) = pack_bytes.read_sparse(offset, &mut bytes) {
4806 if let Some(kind) = inflate_whole_pack_entry_into(&bytes[..len], 0, out) {
4807 return Some(kind);
4808 }
4809 }
4810 }
4811 let mut compressed = Vec::new();
4812 let (pack_bytes, pos) = pack_entry_bytes(idx, offset, pack_bytes, &mut compressed).ok()?;
4813 inflate_whole_pack_entry_into(pack_bytes, pos as u64, out)
4814}
4815
4816fn inflate_whole_pack_entry_into(
4817 pack_bytes: &[u8],
4818 offset: u64,
4819 out: &mut Vec<u8>,
4820) -> Option<ObjectKind> {
4821 let mut pos = offset as usize;
4822 if pos >= pack_bytes.len() {
4823 return None;
4824 }
4825 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
4826 match packed_type {
4827 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4828 let kind = packed_type_to_kind(packed_type).ok()?;
4829 WHOLE_OBJECT_ZLIB.with(|slot| {
4830 let mut inflate = slot.borrow_mut();
4831 decompress_pack_data_into(pack_bytes, &mut pos, size, &mut inflate, out)
4832 .ok()
4833 .map(|()| kind)
4834 })
4835 }
4836 PackedType::OfsDelta | PackedType::RefDelta => None,
4837 }
4838}
4839
4840fn read_object_from_packs_in_context_into(
4841 objects_dir: &Path,
4842 oid: &ObjectId,
4843 buf: &mut Vec<u8>,
4844) -> Result<ObjectKind> {
4845 match try_read_object_from_packs_with_options_into(
4846 objects_dir,
4847 oid,
4848 PackLookupOptions::ALL_LOCAL_PACKS,
4849 false,
4850 buf,
4851 ) {
4852 Ok(kind) => Ok(kind),
4853 Err(err @ Error::ObjectNotFound(_)) => {
4854 if reprepare_pack_directory_on_miss(objects_dir)? {
4855 try_read_object_from_packs_with_options_into(
4856 objects_dir,
4857 oid,
4858 PackLookupOptions::ALL_LOCAL_PACKS,
4859 false,
4860 buf,
4861 )
4862 } else {
4863 Err(err)
4864 }
4865 }
4866 Err(err) => Err(err),
4867 }
4868}
4869
4870fn read_object_from_packs_in_context(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
4871 match try_read_object_from_packs_with_options(
4872 objects_dir,
4873 oid,
4874 PackLookupOptions::ALL_LOCAL_PACKS,
4875 ) {
4876 Ok(obj) => Ok(obj),
4877 Err(err @ Error::ObjectNotFound(_)) => {
4878 if reprepare_pack_directory_on_miss_in_context(objects_dir)? {
4879 try_read_object_from_packs_with_options(
4880 objects_dir,
4881 oid,
4882 PackLookupOptions::ALL_LOCAL_PACKS,
4883 )
4884 } else {
4885 Err(err)
4886 }
4887 }
4888 Err(err) => Err(err),
4889 }
4890}
4891
4892pub(crate) fn read_object_at(idx: &PackIndex, offset: u64) -> Result<Object> {
4897 read_object_at_depth(idx, offset, 0)
4898}
4899
4900pub fn read_object_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<Object> {
4904 read_object_at(idx, offset)
4905}
4906
4907fn read_object_from_pack_at_depth(idx: &PackIndex, oid: &ObjectId, depth: usize) -> Result<Object> {
4910 let Some(offset) = idx.find_offset(oid)? else {
4911 return Err(Error::ObjectNotFound(oid.to_hex()));
4912 };
4913 read_object_at_depth(idx, offset, depth)
4914}
4915
4916fn read_object_at_depth(idx: &PackIndex, offset: u64, depth: usize) -> Result<Object> {
4917 let mut data = Vec::new();
4918 let kind = read_object_at_depth_into(idx, offset, depth, true, &mut data)?;
4919 Ok(Object::new(kind, data))
4920}
4921
4922fn read_object_at_depth_into(
4923 idx: &PackIndex,
4924 offset: u64,
4925 depth: usize,
4926 populate_delta_lru: bool,
4927 out: &mut Vec<u8>,
4928) -> Result<ObjectKind> {
4929 if depth == 0 {
4930 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4931 if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
4932 out.clear();
4933 out.extend_from_slice(data.as_ref());
4934 return Ok(kind);
4935 }
4936 }
4937 let pack_path = &idx.pack_path;
4938 let objects_dir = idx.pack_path.parent().and_then(Path::parent);
4939 for attempt in 0..2 {
4940 let pack_bytes = read_pack_bytes_cached(pack_path)?;
4941 if !pack_cache::pack_object_count_validated(pack_path) {
4942 match validate_pack_index_object_count(&pack_bytes, idx) {
4943 Ok(()) => pack_cache::mark_pack_object_count_validated(pack_path),
4944 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4945 if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
4946 continue;
4947 }
4948 return Err(e);
4949 }
4950 Err(e) => return Err(e),
4951 }
4952 }
4953 if depth == 0 && !pack_bytes.uses_positioned_reads() {
4954 if let Some(kind) = read_whole_pack_entry_object_into(idx, offset, &pack_bytes, out) {
4955 return Ok(kind);
4956 }
4957 }
4958 let resolved = with_pack_read_delta_state(depth, |state| {
4959 resolve_pack_object_at_into(
4960 PackIndexHandle::Borrowed(idx),
4961 offset,
4962 objects_dir,
4963 state,
4964 populate_delta_lru,
4965 out,
4966 )
4967 });
4968 match resolved {
4969 Ok(kind) => return Ok(kind),
4970 Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4971 if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
4972 continue;
4973 }
4974 return Err(e);
4975 }
4976 Err(e) => return Err(e),
4977 }
4978 }
4979 unreachable!("at most two read attempts")
4980}
4981
4982pub fn read_object_from_pack_bytes(
4984 pack_bytes: &[u8],
4985 idx: &PackIndex,
4986 oid: &[u8],
4987) -> Result<Object> {
4988 validate_pack_index_object_count(pack_bytes, idx)?;
4989 let id = ObjectId::from_bytes(oid).map_err(|_| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
4990 let entry_offset = idx
4991 .find_offset(&id)?
4992 .ok_or_else(|| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
4993 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
4994 crate::pack_store::PackStore::with_context(store, || {
4995 let (kind, data) = read_pack_object_at(pack_bytes, entry_offset, idx, None, 0)?;
4996 verify_packed_object_hash(kind, &data, oid)?;
4997 Ok(Object::new(kind, data))
4998 })
4999}
5000
5001fn validate_pack_index_object_count(pack_bytes: &[u8], idx: &PackIndex) -> Result<()> {
5002 if pack_bytes.len() < 12 || &pack_bytes[0..4] != b"PACK" {
5003 return Err(Error::CorruptObject("bad pack header".to_owned()));
5004 }
5005 let count =
5006 u32::from_be_bytes([pack_bytes[8], pack_bytes[9], pack_bytes[10], pack_bytes[11]]) as usize;
5007 if count != idx.len() {
5008 return Err(Error::CorruptObject(format!(
5009 "pack object count mismatch: pack has {count}, index has {}",
5010 idx.len()
5011 )));
5012 }
5013 Ok(())
5014}
5015
5016fn verify_packed_object_hash(kind: ObjectKind, data: &[u8], expected_oid: &[u8]) -> Result<()> {
5017 let algo = HashAlgo::from_len(expected_oid.len()).ok_or_else(|| {
5018 Error::CorruptObject(format!(
5019 "unsupported packed object id width: {}",
5020 expected_oid.len()
5021 ))
5022 })?;
5023 let actual = hash_object(algo, kind, data)?;
5024 if actual.as_bytes() != expected_oid {
5025 return Err(Error::CorruptObject(format!(
5026 "packed object {} hashes to {}",
5027 oid_bytes_to_hex(expected_oid),
5028 actual.to_hex()
5029 )));
5030 }
5031 Ok(())
5032}
5033
5034#[derive(Clone, Copy, Debug, Default)]
5036pub struct PackLookupOptions {
5037 pub skip_midx_covered_packs: bool,
5039 pub only_midx_covered_packs: bool,
5041}
5042
5043impl PackLookupOptions {
5044 pub const ALL_LOCAL_PACKS: Self = Self {
5046 skip_midx_covered_packs: false,
5047 only_midx_covered_packs: false,
5048 };
5049
5050 pub const MIDX_COVERED_PACKS: Self = Self {
5052 skip_midx_covered_packs: false,
5053 only_midx_covered_packs: true,
5054 };
5055
5056 fn allows_pack_shortcuts(self) -> bool {
5061 !self.skip_midx_covered_packs && !self.only_midx_covered_packs
5062 }
5063}
5064
5065fn select_single_local_pack_index(
5067 indexes: &[Arc<PackIndex>],
5068 commit_hint: Option<&crate::objects::ObjectId>,
5069) -> Result<Option<Arc<PackIndex>>> {
5070 if indexes.is_empty() {
5071 return Ok(None);
5072 }
5073 if indexes.len() == 1 {
5074 return Ok(Some(Arc::clone(&indexes[0])));
5075 }
5076 if let Some(oid) = commit_hint {
5077 let mut found: Option<Arc<PackIndex>> = None;
5078 for idx in indexes {
5079 if idx.find_offset(oid)?.is_some() {
5080 if found.is_some() {
5081 return Ok(None);
5082 }
5083 found = Some(Arc::clone(idx));
5084 }
5085 }
5086 if found.is_some() {
5087 return Ok(found);
5088 }
5089 }
5090 Ok(None)
5091}
5092
5093pub struct SingleLocalPackReader {
5097 idx: Arc<PackIndex>,
5098 pack_bytes: Arc<PackData>,
5099 pack_id: u32,
5100 cursor: usize,
5101 info_cache: HashMap<u64, ObjectInfo>,
5102 delta_scratch: Option<ZlibInflateScratch>,
5105 commit_header_buf: Vec<u8>,
5106}
5107
5108pub(crate) fn objects_dir_has_loose_objects(objects_dir: &Path) -> bool {
5109 let Ok(entries) = std::fs::read_dir(objects_dir) else {
5110 return false;
5111 };
5112 for entry in entries.flatten() {
5113 let name = entry.file_name();
5114 let name = name.to_string_lossy();
5115 if name.len() != 2 || !name.bytes().all(|b| b.is_ascii_hexdigit()) {
5116 continue;
5117 }
5118 let Ok(hex_entries) = std::fs::read_dir(entry.path()) else {
5119 continue;
5120 };
5121 if hex_entries.flatten().any(|e| e.path().is_file()) {
5122 return true;
5123 }
5124 }
5125 false
5126}
5127
5128impl SingleLocalPackReader {
5129 pub fn open(objects_dir: &Path) -> Result<Option<Self>> {
5135 Self::open_with_options(objects_dir, false, None)
5136 }
5137
5138 pub fn open_allow_loose(objects_dir: &Path) -> Result<Option<Self>> {
5140 Self::open_with_options(objects_dir, true, None)
5141 }
5142
5143 pub(crate) fn open_allow_loose_for_commits(
5146 objects_dir: &Path,
5147 commit_hint: &crate::objects::ObjectId,
5148 ) -> Result<Option<Self>> {
5149 Self::open_with_options(objects_dir, true, Some(commit_hint))
5150 }
5151
5152 pub(crate) fn open_for_pack_index(
5154 objects_dir: &Path,
5155 idx: Arc<PackIndex>,
5156 ) -> Result<Option<Self>> {
5157 if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
5158 return Ok(None);
5159 }
5160 pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
5161 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5162 validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
5163 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
5164 pack_cache::remember_pack_hit(objects_dir, &idx);
5165 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
5166 Ok(Some(Self {
5167 idx,
5168 pack_bytes,
5169 pack_id,
5170 cursor: 0,
5171 info_cache: HashMap::new(),
5172 delta_scratch: None,
5173 commit_header_buf: Vec::new(),
5174 }))
5175 }
5176
5177 pub(crate) fn object_kinds_at_sorted_offsets(
5188 &self,
5189 offsets: &[u64],
5190 ) -> Result<Vec<Option<ObjectKind>>> {
5191 let pack_bytes = self.pack_bytes.as_ref();
5192 let hash_bytes = self.idx.hash_bytes();
5193 let mut kinds: Vec<Option<ObjectKind>> = Vec::with_capacity(offsets.len());
5194 for (rank, &offset) in offsets.iter().enumerate() {
5195 let mut pos = usize::try_from(offset)
5196 .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
5197 let (packed_type, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
5198 let base_offset = match packed_type {
5199 PackedType::OfsDelta => Some(parse_ofs_delta_base(pack_bytes, &mut pos, offset)?),
5200 PackedType::RefDelta => {
5201 let raw = pack_bytes.get(pos..pos + hash_bytes).ok_or_else(|| {
5202 Error::CorruptObject("truncated ref-delta base OID".to_owned())
5203 })?;
5204 self.idx.find_offset(&ObjectId::from_bytes(raw)?)?
5205 }
5206 _ => {
5207 kinds.push(Some(packed_type_to_kind(packed_type)?));
5208 continue;
5209 }
5210 };
5211 let base_kind = base_offset
5212 .and_then(|base| offsets.binary_search(&base).ok())
5213 .filter(|&base_rank| base_rank < rank)
5214 .and_then(|base_rank| kinds[base_rank]);
5215 kinds.push(base_kind);
5216 }
5217 Ok(kinds)
5218 }
5219
5220 pub(crate) fn contains_oid(&self, oid: &crate::objects::ObjectId) -> Result<bool> {
5222 Ok(self.idx.find_offset(oid)?.is_some())
5223 }
5224
5225 fn open_with_options(
5226 objects_dir: &Path,
5227 allow_loose: bool,
5228 commit_hint: Option<&crate::objects::ObjectId>,
5229 ) -> Result<Option<Self>> {
5230 if !pack_cache::pack_thread_locals_apply_to(objects_dir) {
5231 return Ok(None);
5232 }
5233 pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5234 let memo_eligible = allow_loose && commit_hint.is_none();
5235 if memo_eligible {
5236 if let Some((idx, pack_bytes, pack_id)) = pack_cache::single_reader_for(objects_dir) {
5237 return Ok(Some(Self {
5238 idx,
5239 pack_bytes,
5240 pack_id,
5241 cursor: 0,
5242 info_cache: HashMap::new(),
5243 delta_scratch: None,
5244 commit_header_buf: Vec::new(),
5245 }));
5246 }
5247 }
5248 let indexes = pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)?;
5249 let idx = match select_single_local_pack_index(&indexes, commit_hint)? {
5250 Some(idx) => idx,
5251 None => return Ok(None),
5252 };
5253 if !allow_loose && objects_dir_has_loose_objects(objects_dir) {
5254 return Ok(None);
5255 }
5256 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5257 validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
5258 pack_cache::mark_pack_object_count_validated(&idx.pack_path);
5259 pack_cache::remember_pack_hit(objects_dir, &idx);
5260 let pack_id = pack_cache::pack_id_for(&idx.pack_path);
5261 if memo_eligible && indexes.len() == 1 {
5262 pack_cache::remember_single_reader(objects_dir, &idx, &pack_bytes, pack_id);
5263 }
5264 Ok(Some(Self {
5265 idx,
5266 pack_bytes,
5267 pack_id,
5268 cursor: 0,
5269 info_cache: HashMap::new(),
5270 delta_scratch: None,
5271 commit_header_buf: Vec::new(),
5272 }))
5273 }
5274
5275 pub fn read_commit_author_prefix(&mut self, oid: &ObjectId) -> Result<Option<&[u8]>> {
5284 self.read_commit_author_prefix_at(oid, true)
5285 }
5286
5287 pub(crate) fn read_commit_author_prefix_unordered(
5289 &mut self,
5290 oid: &ObjectId,
5291 ) -> Result<Option<&[u8]>> {
5292 self.read_commit_author_prefix_at(oid, false)
5293 }
5294
5295 pub(crate) fn read_commit_author_prefix_at_pack_row(
5297 &mut self,
5298 row: usize,
5299 ) -> Result<Option<&[u8]>> {
5300 self.cursor = row;
5301 let offset = self.idx.offset_at(row)?;
5302 self.read_commit_author_prefix_at_pack_offset(offset)
5303 }
5304
5305 fn read_commit_author_prefix_at(
5306 &mut self,
5307 oid: &ObjectId,
5308 monotonic: bool,
5309 ) -> Result<Option<&[u8]>> {
5310 let offset = if monotonic {
5311 self.idx.find_offset_monotonic(oid, &mut self.cursor)?
5312 } else {
5313 self.idx.find_offset(oid)?
5314 };
5315 let Some(offset) = offset else {
5316 return Ok(None);
5317 };
5318 self.read_commit_author_prefix_at_pack_offset(offset)
5319 }
5320
5321 fn read_commit_author_prefix_at_pack_offset(&mut self, offset: u64) -> Result<Option<&[u8]>> {
5322 self.commit_header_buf.clear();
5323 let filled = with_pack_read_delta_state(0, |state| {
5324 self.resolve_commit_author_prefix_at_pack_offset(offset, state)
5325 })?;
5326 if !filled || self.commit_header_buf.is_empty() {
5327 return Ok(None);
5328 }
5329 Ok(Some(&self.commit_header_buf))
5330 }
5331
5332 fn resolve_commit_author_prefix_at_pack_offset(
5333 &mut self,
5334 start_offset: u64,
5335 state: &mut DeltaChainState,
5336 ) -> Result<bool> {
5337 let pack_bytes = self.pack_bytes.as_ref();
5338 let mut cur_offset = start_offset;
5339 let result_offset = start_offset;
5340 let result_pack_id = self.pack_id;
5341 state.pending.clear();
5342
5343 let mut compressed = Vec::new();
5344 loop {
5345 state.visit(self.pack_id, cur_offset)?;
5346 let (bytes, mut pos) =
5347 pack_entry_bytes(&self.idx, cur_offset, pack_bytes, &mut compressed)?;
5348 let object_start = cur_offset;
5349 let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
5350 Ok(v) => v,
5351 Err(_) => return Ok(false),
5352 };
5353 match packed_type {
5354 PackedType::Commit => {
5355 let kind = ObjectKind::Commit;
5356 if state.pending.is_empty() {
5357 self.delta_scratch
5358 .get_or_insert_with(ZlibInflateScratch::default)
5359 .decompress_prefix_into(
5360 &bytes[pos..],
5361 COMMIT_HEADER_READ_PREFIX,
5362 &mut self.commit_header_buf,
5363 )?;
5364 return Ok(!self.commit_header_buf.is_empty());
5365 }
5366 if let Some((base_kind, base_data)) =
5367 pack_cache::get_delta_base(self.pack_id, cur_offset)
5368 {
5369 if base_kind != ObjectKind::Commit {
5370 return Ok(false);
5371 }
5372 finish_pack_read_prefix_with_pending(
5373 &mut self.commit_header_buf,
5374 kind,
5375 base_data.as_ref(),
5376 None,
5377 result_pack_id,
5378 result_offset,
5379 true,
5380 state,
5381 )?;
5382 return Ok(commit_prefix_has_author(&self.commit_header_buf));
5383 }
5384 state.whole_object.clear();
5385 decompress_pack_data_into(
5386 bytes,
5387 &mut pos,
5388 size,
5389 &mut state.inflate,
5390 &mut state.whole_object,
5391 )?;
5392 let whole = std::mem::take(&mut state.whole_object);
5393 finish_pack_read_prefix_with_pending(
5394 &mut self.commit_header_buf,
5395 kind,
5396 &whole,
5397 Some((self.pack_id, cur_offset)),
5398 result_pack_id,
5399 result_offset,
5400 true,
5401 state,
5402 )?;
5403 return Ok(commit_prefix_has_author(&self.commit_header_buf));
5404 }
5405 PackedType::Tree | PackedType::Blob | PackedType::Tag => return Ok(false),
5406 PackedType::OfsDelta => {
5407 state.next_hop()?;
5408 let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
5409 state.delta_decompress.clear();
5410 decompress_pack_data_into(
5411 bytes,
5412 &mut pos,
5413 size,
5414 &mut state.inflate,
5415 &mut state.delta_decompress,
5416 )?;
5417 state.pending.push(PendingDeltaFrame {
5418 pack_id: self.pack_id,
5419 offset: object_start,
5420 delta: std::mem::take(&mut state.delta_decompress),
5421 });
5422 if let Some((base_kind, base_data)) =
5423 pack_cache::get_delta_base(self.pack_id, base_offset)
5424 {
5425 if base_kind != ObjectKind::Commit {
5426 return Ok(false);
5427 }
5428 finish_pack_read_prefix_with_pending(
5429 &mut self.commit_header_buf,
5430 base_kind,
5431 base_data.as_ref(),
5432 None,
5433 result_pack_id,
5434 result_offset,
5435 true,
5436 state,
5437 )?;
5438 return Ok(commit_prefix_has_author(&self.commit_header_buf));
5439 }
5440 cur_offset = base_offset;
5441 }
5442 PackedType::RefDelta => {
5443 state.next_hop()?;
5444 let hb = self.idx.hash_bytes();
5445 if pos + hb > bytes.len() {
5446 return Ok(false);
5447 }
5448 let base_raw = &bytes[pos..pos + hb];
5449 pos += hb;
5450 state.delta_decompress.clear();
5451 decompress_pack_data_into(
5452 bytes,
5453 &mut pos,
5454 size,
5455 &mut state.inflate,
5456 &mut state.delta_decompress,
5457 )?;
5458 state.pending.push(PendingDeltaFrame {
5459 pack_id: self.pack_id,
5460 offset: object_start,
5461 delta: std::mem::take(&mut state.delta_decompress),
5462 });
5463 let base_oid = ObjectId::from_bytes(base_raw)?;
5464 let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
5465 return Ok(false);
5466 };
5467 if let Some((base_kind, base_data)) =
5468 pack_cache::get_delta_base(self.pack_id, base_offset)
5469 {
5470 if base_kind != ObjectKind::Commit {
5471 return Ok(false);
5472 }
5473 finish_pack_read_prefix_with_pending(
5474 &mut self.commit_header_buf,
5475 base_kind,
5476 base_data.as_ref(),
5477 None,
5478 result_pack_id,
5479 result_offset,
5480 true,
5481 state,
5482 )?;
5483 return Ok(commit_prefix_has_author(&self.commit_header_buf));
5484 }
5485 cur_offset = base_offset;
5486 }
5487 }
5488 }
5489 }
5490
5491 pub(crate) fn clear_commit_header_prefix(&mut self) {
5493 self.commit_header_buf.clear();
5494 }
5495
5496 pub(crate) fn commit_header_prefix(&self) -> &[u8] {
5498 &self.commit_header_buf
5499 }
5500
5501 fn read_info_at_pack_offset(&mut self, start_offset: u64) -> Result<ObjectInfo> {
5502 if let Some(info) = self.info_cache.get(&start_offset) {
5503 return Ok(*info);
5504 }
5505 let pack_path = self.idx.pack_path.clone();
5506 let info = with_pack_read_delta_state(0, |state| {
5507 self.resolve_info_at_pack_offset_body(start_offset, state)
5508 })?;
5509 self.info_cache.insert(start_offset, info);
5510 remember_pack_read_info(&pack_path, start_offset, info);
5511 Ok(info)
5512 }
5513
5514 fn resolve_info_at_pack_offset_body(
5515 &mut self,
5516 start_offset: u64,
5517 state: &mut DeltaChainState,
5518 ) -> Result<ObjectInfo> {
5519 let pack_bytes = self.pack_bytes.as_ref();
5520 let mut cur_offset = start_offset;
5521 let mut result_size: Option<u64> = None;
5522 loop {
5523 state.visit(self.pack_id, cur_offset)?;
5524 if let Some(base) = self.info_cache.get(&cur_offset) {
5525 if let Some(size) = result_size {
5526 return Ok(ObjectInfo {
5527 kind: base.kind,
5528 size,
5529 });
5530 }
5531 return Ok(*base);
5532 }
5533
5534 let object_start = cur_offset;
5535 let mut pos = cur_offset as usize;
5536 let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos)?;
5537 match packed_type {
5538 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
5539 let kind = packed_type_to_kind(packed_type)?;
5540 let undeltified = ObjectInfo {
5541 kind,
5542 size: header_size,
5543 };
5544 self.info_cache.insert(cur_offset, undeltified);
5545 let size = result_size.unwrap_or(header_size);
5546 return Ok(ObjectInfo { kind, size });
5547 }
5548 PackedType::OfsDelta => {
5549 state.next_hop()?;
5550 let base_offset = parse_ofs_delta_base(pack_bytes, &mut pos, object_start)?;
5551 if result_size.is_none() {
5552 result_size = Some(
5553 self.delta_scratch
5554 .get_or_insert_with(ZlibInflateScratch::default)
5555 .read_pack_delta_result_size(pack_bytes, &mut pos)?,
5556 );
5557 }
5558 cur_offset = base_offset;
5559 }
5560 PackedType::RefDelta => {
5561 state.next_hop()?;
5562 let hb = self.idx.hash_bytes();
5563 if pos + hb > pack_bytes.len() {
5564 return Err(Error::CorruptObject(
5565 "truncated ref-delta base OID".to_owned(),
5566 ));
5567 }
5568 let base_raw = &pack_bytes[pos..pos + hb];
5569 pos += hb;
5570 if result_size.is_none() {
5571 result_size = Some(
5572 self.delta_scratch
5573 .get_or_insert_with(ZlibInflateScratch::default)
5574 .read_pack_delta_result_size(pack_bytes, &mut pos)?,
5575 );
5576 }
5577 let base_oid = ObjectId::from_bytes(base_raw)?;
5578 let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
5579 return Err(Error::ObjectNotFound(base_oid.to_hex()));
5580 };
5581 cur_offset = base_offset;
5582 }
5583 }
5584 }
5585 }
5586
5587 pub fn read_info(&mut self, oid: &ObjectId) -> Result<ObjectInfo> {
5593 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5594 return Err(Error::ObjectNotFound(oid.to_hex()));
5595 };
5596 if let Some(info) =
5597 read_whole_pack_entry_info_fast(&self.idx, offset, self.pack_bytes.as_ref())
5598 {
5599 self.info_cache.insert(offset, info);
5600 return Ok(info);
5601 }
5602 self.read_info_at_pack_offset(offset)
5603 }
5604
5605 pub fn read_object(&mut self, oid: &ObjectId) -> Result<Object> {
5611 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5612 return Err(Error::ObjectNotFound(oid.to_hex()));
5613 };
5614 if !self.pack_bytes.uses_positioned_reads() {
5615 if let Some(obj) =
5616 read_whole_pack_entry_object_fast(&self.idx, offset, &self.pack_bytes)
5617 {
5618 return Ok(obj);
5619 }
5620 }
5621 read_object_from_pack_at_offset(&self.idx, offset)
5622 }
5623
5624 pub(crate) fn read_commit_through_subject(
5641 &mut self,
5642 oid: &ObjectId,
5643 ) -> Result<Option<Vec<u8>>> {
5644 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5645 return Ok(None);
5646 };
5647 let mut compressed = Vec::new();
5648 let Ok((pack_bytes, mut pos)) =
5649 pack_entry_bytes(&self.idx, offset, &self.pack_bytes, &mut compressed)
5650 else {
5651 return Ok(None);
5652 };
5653 let Ok((PackedType::Commit, size)) = parse_pack_object_header(pack_bytes, &mut pos) else {
5654 return Ok(None);
5655 };
5656 let Some(input) = pack_bytes.get(pos..) else {
5657 return Ok(None);
5658 };
5659 let mut out = Vec::new();
5660 let inflated = WHOLE_OBJECT_ZLIB.with(|slot| {
5661 slot.borrow_mut().decompress_prefix_until(
5662 input,
5663 size,
5664 COMMIT_SUBJECT_READ_STEP,
5665 &mut out,
5666 crate::objects::commit_through_subject_len,
5667 )
5668 });
5669 Ok(inflated.is_ok().then_some(out))
5670 }
5671
5672 pub(crate) fn read_commit_parents(
5696 &mut self,
5697 oid: &ObjectId,
5698 hex_len: usize,
5699 ) -> Result<Option<Vec<ObjectId>>> {
5700 let Some(offset) = self.idx.find_offset(oid)? else {
5701 return Ok(None);
5702 };
5703 let mut compressed = Vec::new();
5704 let Ok((pack_bytes, mut pos)) =
5705 pack_entry_bytes(&self.idx, offset, &self.pack_bytes, &mut compressed)
5706 else {
5707 return Ok(None);
5708 };
5709 let Ok((PackedType::Commit, size)) = parse_pack_object_header(pack_bytes, &mut pos) else {
5710 return Ok(None);
5711 };
5712 if size > COMMIT_PARENTS_WHOLE_READ_MAX {
5713 return Ok(None);
5714 }
5715 let scratch = self
5716 .delta_scratch
5717 .get_or_insert_with(ZlibInflateScratch::default);
5718 if decompress_pack_data_into(
5719 pack_bytes,
5720 &mut pos,
5721 size,
5722 scratch,
5723 &mut self.commit_header_buf,
5724 )
5725 .is_err()
5726 {
5727 return Ok(None);
5728 }
5729 Ok(
5730 crate::objects::parse_commit_tree_and_parents_lenient(&self.commit_header_buf, hex_len)
5731 .map(|(_, parents)| parents),
5732 )
5733 }
5734
5735 pub(crate) fn read_object_unless_whole_larger(
5744 &mut self,
5745 oid: &ObjectId,
5746 limit: u64,
5747 ) -> Result<BoundedRead> {
5748 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5749 return Err(Error::ObjectNotFound(oid.to_hex()));
5750 };
5751 let mut pos = usize::try_from(offset)
5752 .map_err(|_| Error::CorruptObject(format!("pack offset {offset} overflows")))?;
5753 if let Ok((packed_type, size)) = parse_pack_object_header(&self.pack_bytes, &mut pos) {
5754 let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
5755 if whole && size > limit {
5756 return Ok(BoundedRead::Larger);
5757 }
5758 }
5759 if let Some(obj) =
5760 read_whole_pack_entry_object_fast(&self.idx, offset, self.pack_bytes.as_ref())
5761 {
5762 return Ok(BoundedRead::Object(obj));
5763 }
5764 read_object_from_pack_at_offset(&self.idx, offset).map(BoundedRead::Object)
5765 }
5766
5767 pub fn read_object_into(&mut self, oid: &ObjectId, out: &mut Vec<u8>) -> Result<ObjectKind> {
5771 self.read_object_into_with_delta_policy(oid, false, out)
5772 }
5773
5774 pub(crate) fn read_object_into_with_delta_policy(
5777 &mut self,
5778 oid: &ObjectId,
5779 populate_delta_lru: bool,
5780 out: &mut Vec<u8>,
5781 ) -> Result<ObjectKind> {
5782 let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5783 return Err(Error::ObjectNotFound(oid.to_hex()));
5784 };
5785 if !self.pack_bytes.uses_positioned_reads() {
5786 if let Some(kind) =
5787 read_whole_pack_entry_object_into(&self.idx, offset, &self.pack_bytes, out)
5788 {
5789 return Ok(kind);
5790 }
5791 }
5792 read_object_at_depth_into(&self.idx, offset, 0, populate_delta_lru, out)
5793 }
5794
5795 pub fn pack_offset(&self, oid: &ObjectId) -> Result<Option<u64>> {
5801 self.idx.find_offset(oid)
5802 }
5803}
5804fn pack_indexes_for_lookup(
5805 objects_dir: &Path,
5806 opts: PackLookupOptions,
5807) -> Result<Vec<Arc<PackIndex>>> {
5808 let indexes = read_local_pack_indexes_cached(objects_dir)?;
5809 if !opts.skip_midx_covered_packs && !opts.only_midx_covered_packs {
5810 return Ok(indexes);
5811 }
5812 let covered = pack_cache::midx_covered_pack_names(objects_dir);
5813 if covered.is_empty() {
5814 return if opts.only_midx_covered_packs {
5815 Ok(Vec::new())
5816 } else {
5817 Ok(indexes)
5818 };
5819 }
5820 Ok(indexes
5821 .into_iter()
5822 .filter(|idx| {
5823 idx.idx_path
5824 .file_name()
5825 .and_then(|n| n.to_str())
5826 .map(|name| {
5827 if opts.only_midx_covered_packs {
5828 covered.contains(name)
5829 } else {
5830 !covered.contains(name)
5831 }
5832 })
5833 .unwrap_or(!opts.only_midx_covered_packs)
5834 })
5835 .collect())
5836}
5837
5838pub fn read_object_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
5850 if crate::pack_store::PackStore::has_context() {
5851 return read_object_from_packs_in_context(objects_dir, oid);
5852 }
5853 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
5854 crate::pack_store::PackStore::with_context(store, || {
5855 read_object_from_packs_in_context(objects_dir, oid)
5856 })
5857}
5858
5859pub fn read_object_from_packs_into(
5865 objects_dir: &Path,
5866 oid: &ObjectId,
5867 buf: &mut Vec<u8>,
5868) -> Result<ObjectKind> {
5869 if crate::pack_store::PackStore::has_context() {
5870 return read_object_from_packs_in_context_into(objects_dir, oid, buf);
5871 }
5872 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
5873 crate::pack_store::PackStore::with_context(store, || {
5874 read_object_from_packs_in_context_into(objects_dir, oid, buf)
5875 })
5876}
5877
5878pub fn read_object_info_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<ObjectInfo> {
5884 match try_read_object_info_from_packs_with_options(
5885 objects_dir,
5886 oid,
5887 PackLookupOptions::ALL_LOCAL_PACKS,
5888 ) {
5889 Ok(info) => Ok(info),
5890 Err(err @ Error::ObjectNotFound(_)) => {
5891 if reprepare_pack_directory_on_miss(objects_dir)? {
5892 try_read_object_info_from_packs_with_options(
5893 objects_dir,
5894 oid,
5895 PackLookupOptions::ALL_LOCAL_PACKS,
5896 )
5897 } else {
5898 Err(err)
5899 }
5900 }
5901 Err(err) => Err(err),
5902 }
5903}
5904
5905pub(crate) fn try_read_object_info_from_packs_with_options(
5907 objects_dir: &Path,
5908 oid: &ObjectId,
5909 opts: PackLookupOptions,
5910) -> Result<ObjectInfo> {
5911 pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5912 if opts.allows_pack_shortcuts() {
5913 if let Some(info) = pack_cache::try_read_info_from_hinted_pack(objects_dir, oid)? {
5914 return Ok(info);
5915 }
5916 }
5917 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
5918 let mut last_err: Option<Error> = None;
5919 for idx in &indexes {
5920 let Some(offset) = idx.find_offset(oid)? else {
5921 continue;
5922 };
5923 match read_object_info_from_pack_at_offset(idx, offset) {
5924 Ok(info) => {
5925 pack_cache::remember_pack_hit(objects_dir, idx);
5926 return Ok(info);
5927 }
5928 Err(Error::ObjectNotFound(_)) => {}
5929 Err(err) => last_err = Some(err),
5930 }
5931 }
5932 Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
5933}
5934
5935pub(crate) fn try_read_object_from_packs_with_options(
5937 objects_dir: &Path,
5938 oid: &ObjectId,
5939 opts: PackLookupOptions,
5940) -> Result<Object> {
5941 pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5942 if opts.allows_pack_shortcuts() {
5943 if let Some(obj) = pack_cache::try_read_object_via_single_pack_reader(objects_dir, oid)? {
5944 return Ok(obj);
5945 }
5946 if let Some(obj) = pack_cache::try_read_from_hinted_pack(objects_dir, oid)? {
5947 return Ok(obj);
5948 }
5949 }
5950 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
5951 let mut last_damage: Option<PackReadDamage> = None;
5952 let mut last_err: Option<Error> = None;
5953 for idx in &indexes {
5954 let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
5955 else {
5956 continue;
5957 };
5958 match read_object_from_pack_at_offset(idx, offset) {
5959 Ok(obj) => {
5960 pack_cache::remember_pack_hit(objects_dir, idx);
5961 return Ok(obj);
5962 }
5963 Err(Error::ObjectNotFound(_)) => {}
5966 Err(err) if err.is_object_read_wrap_source() => {
5969 last_damage = Some(PackReadDamage {
5970 pack: idx.pack_path.clone(),
5971 offset,
5972 err,
5973 });
5974 }
5975 Err(err) => last_err = Some(err),
5976 }
5977 }
5978 if let Some(err) = last_err {
5979 return Err(err);
5980 }
5981 Err(finish_pack_object_lookup(oid, last_damage))
5982}
5983
5984pub(crate) fn try_read_object_from_packs_with_options_into(
5989 objects_dir: &Path,
5990 oid: &ObjectId,
5991 opts: PackLookupOptions,
5992 populate_delta_lru: bool,
5993 buf: &mut Vec<u8>,
5994) -> Result<ObjectKind> {
5995 pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5996 if opts.allows_pack_shortcuts() {
5997 if let Some(kind) = pack_cache::try_read_object_via_single_pack_reader_into(
5998 objects_dir,
5999 oid,
6000 populate_delta_lru,
6001 buf,
6002 )? {
6003 return Ok(kind);
6004 }
6005 if let Some(kind) =
6006 pack_cache::try_read_from_hinted_pack_into(objects_dir, oid, populate_delta_lru, buf)?
6007 {
6008 return Ok(kind);
6009 }
6010 }
6011 let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
6012 let mut last_damage: Option<PackReadDamage> = None;
6013 let mut last_err: Option<Error> = None;
6014 for idx in &indexes {
6015 let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
6016 else {
6017 continue;
6018 };
6019 match read_object_at_depth_into(idx, offset, 0, populate_delta_lru, buf) {
6020 Ok(kind) => {
6021 pack_cache::remember_pack_hit(objects_dir, idx);
6022 return Ok(kind);
6023 }
6024 Err(Error::ObjectNotFound(_)) => {}
6025 Err(err) if err.is_object_read_wrap_source() => {
6026 last_damage = Some(PackReadDamage {
6027 pack: idx.pack_path.clone(),
6028 offset,
6029 err,
6030 });
6031 }
6032 Err(err) => last_err = Some(err),
6033 }
6034 }
6035 if let Some(err) = last_err {
6036 return Err(err);
6037 }
6038 Err(finish_pack_object_lookup(oid, last_damage))
6039}
6040
6041#[must_use]
6043pub fn pack_index_is_v1(idx_path: &Path) -> bool {
6044 fs::read(idx_path)
6045 .ok()
6046 .is_some_and(|bytes| bytes.len() >= 4 && bytes[0..4] != [0xff, b't', b'O', b'c'])
6047}
6048
6049pub fn try_read_object_from_local_packs_cached(
6061 objects_dir: &Path,
6062 oid: &ObjectId,
6063 mut alternate_read: impl FnMut() -> Result<Object>,
6064) -> Result<Option<Object>> {
6065 let indexes = read_local_pack_indexes_cached(objects_dir)?;
6066 for idx in &indexes {
6067 if idx.find_offset(oid)?.is_none() {
6068 continue;
6069 }
6070 match read_object_from_pack(idx, oid) {
6071 Ok(obj) => return Ok(Some(obj)),
6072 Err(_) if pack_index_is_v1(&idx.idx_path) => {
6073 return Ok(Some(Object::new(ObjectKind::Blob, Vec::new())));
6074 }
6075 Err(_) => {
6076 if let Ok(obj) = alternate_read() {
6077 return Ok(Some(obj));
6078 }
6079 }
6080 }
6081 }
6082 Ok(None)
6083}
6084
6085fn parse_in_pack_delta_reuse_candidate(
6089 idx: &PackIndex,
6090 pack_bytes: &[u8],
6091 entry_offset: u64,
6092 in_pack: &HashSet<ObjectId>,
6093) -> Result<Option<(ObjectId, Vec<u8>)>> {
6094 let hb = idx.hash_bytes();
6095 let mut p = entry_offset as usize;
6096 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
6097 let base = match packed_type {
6098 PackedType::RefDelta => {
6099 if p + hb > pack_bytes.len() {
6100 return Err(Error::CorruptObject(
6101 "truncated ref-delta base oid while scanning for reuse".to_owned(),
6102 ));
6103 }
6104 let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
6105 p += hb;
6106 bo
6107 }
6108 PackedType::OfsDelta => {
6109 let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
6110 let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
6111 return Ok(None);
6112 };
6113 let base_oid_bytes = idx.oid_at(pos);
6114 if base_oid_bytes.len() != hb {
6115 return Ok(None);
6116 }
6117 ObjectId::from_bytes(base_oid_bytes)?
6118 }
6119 _ => return Ok(None),
6120 };
6121 if !in_pack.contains(&base) {
6122 return Ok(None);
6123 }
6124 let zlib_start = p;
6125 let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
6126 return Ok(None);
6127 };
6128 if zlib_start >= end_pos {
6129 return Ok(None);
6130 }
6131 let compressed = &pack_bytes[zlib_start..end_pos];
6132 let mut zpos = 0usize;
6133 let mut scratch = ZlibInflateScratch::default();
6134 let delta = match scratch.decompress_fixed(compressed, &mut zpos, size) {
6135 Ok(d) => d,
6136 Err(_) => return Ok(None),
6137 };
6138 Ok(Some((base, delta)))
6139}
6140
6141fn parse_in_pack_delta_reuse_zlib(
6144 idx: &PackIndex,
6145 pack_bytes: &[u8],
6146 entry_offset: u64,
6147 reusable_base: &dyn Fn(&ObjectId) -> bool,
6148) -> Result<Option<(ObjectId, u64, usize, usize)>> {
6149 parse_delta_reuse_zlib_at(
6150 idx,
6151 pack_bytes,
6152 entry_offset,
6153 reusable_base,
6154 DeltaReuseLookups {
6155 row_at_offset: &|offset| idx.find_position_by_offset_sorted(offset),
6156 entry_end: &|| pack_entry_raw_end(idx, pack_bytes, entry_offset),
6157 entry_crc: &|| idx.crc32_for_pack_offset(entry_offset),
6158 },
6159 )
6160}
6161
6162struct DeltaReuseLookups<'a> {
6166 row_at_offset: &'a dyn Fn(u64) -> Result<Option<usize>>,
6167 entry_end: &'a dyn Fn() -> Result<Option<usize>>,
6168 entry_crc: &'a dyn Fn() -> Result<Option<u32>>,
6169}
6170
6171fn parse_delta_reuse_zlib_at(
6173 idx: &PackIndex,
6174 pack_bytes: &[u8],
6175 entry_offset: u64,
6176 reusable_base: &dyn Fn(&ObjectId) -> bool,
6177 lookups: DeltaReuseLookups<'_>,
6178) -> Result<Option<(ObjectId, u64, usize, usize)>> {
6179 let start = entry_offset as usize;
6180 let hb = idx.hash_bytes();
6181 let mut p = start;
6182 let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
6183 let base = match packed_type {
6184 PackedType::RefDelta => {
6185 if p + hb > pack_bytes.len() {
6186 return Err(Error::CorruptObject(
6187 "truncated ref-delta base oid while scanning for reuse".to_owned(),
6188 ));
6189 }
6190 let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
6191 p += hb;
6192 bo
6193 }
6194 PackedType::OfsDelta => {
6195 let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
6196 let Some(pos) = (lookups.row_at_offset)(base_off)? else {
6197 return Ok(None);
6198 };
6199 let base_oid_bytes = idx.oid_at(pos);
6200 if base_oid_bytes.len() != hb {
6201 return Ok(None);
6202 }
6203 ObjectId::from_bytes(base_oid_bytes)?
6204 }
6205 _ => return Ok(None),
6206 };
6207 if !reusable_base(&base) {
6208 return Ok(None);
6209 }
6210 let zlib_start = p;
6211 let Some(end_pos) = (lookups.entry_end)()? else {
6212 return Ok(None);
6213 };
6214 if zlib_start >= end_pos {
6215 return Ok(None);
6216 }
6217 let entry_bytes = &pack_bytes[start..end_pos];
6218 if let Some(crc) = (lookups.entry_crc)()? {
6219 if crc32fast::hash(entry_bytes) != crc {
6220 return Ok(None);
6221 }
6222 }
6223 Ok(Some((base, size, zlib_start, end_pos)))
6224}
6225
6226fn reuse_delta_matches_pack_resolution(
6227 idx: &PackIndex,
6228 target: &ObjectId,
6229 base: ObjectId,
6230 delta: &[u8],
6231) -> bool {
6232 let Ok(base_obj) = read_object_from_pack(idx, &base) else {
6233 return false;
6234 };
6235 let Ok(target_obj) = read_object_from_pack(idx, target) else {
6236 return false;
6237 };
6238 crate::unpack_objects::apply_delta(&base_obj.data, delta)
6239 .ok()
6240 .is_some_and(|applied| applied == target_obj.data)
6241}
6242
6243fn index_scan_is_cheaper(idx: &PackIndex, wanted: usize) -> bool {
6250 let lookup_cost =
6251 wanted.saturating_mul(usize::BITS.saturating_sub(idx.len().leading_zeros()) as usize);
6252 lookup_cost >= idx.len()
6253}
6254
6255fn wanted_index_entries<'a>(
6256 idx: &PackIndex,
6257 wanted: impl ExactSizeIterator<Item = &'a ObjectId>,
6258 contains: impl Fn(&ObjectId) -> bool,
6259) -> Result<Vec<(ObjectId, u64)>> {
6260 if !index_scan_is_cheaper(idx, wanted.len()) {
6261 let mut out = Vec::new();
6262 for oid in wanted {
6263 if let Some(offset) = idx.find_offset(oid)? {
6264 out.push((*oid, offset));
6265 }
6266 }
6267 return Ok(out);
6268 }
6269 let mut out = Vec::new();
6270 for entry in idx.iter() {
6271 let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
6272 continue;
6273 };
6274 if contains(&oid) {
6275 out.push((oid, entry.offset()?));
6276 }
6277 }
6278 Ok(out)
6279}
6280
6281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6283pub(crate) struct PackedSlot {
6284 pub pack: u32,
6286 pub whole: bool,
6288}
6289
6290pub(crate) fn locate_packed_objects(
6300 objects_dir: &Path,
6301 wanted: &crate::objects::OidHashSet,
6302) -> Result<crate::objects::OidHashMap<PackedSlot>> {
6303 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
6304 sort_pack_indexes_oldest_first(&mut indexes);
6305 let mut slots = crate::objects::oid_hash_map_with_capacity(wanted.len());
6306 for (pack, idx) in (0u32..).zip(&indexes) {
6307 let bytes = read_pack_bytes_cached(&idx.pack_path)?;
6308 for (oid, offset) in wanted_index_entries(idx, wanted.iter(), |oid| wanted.contains(oid))? {
6309 if slots.contains_key(&oid) {
6310 continue;
6311 }
6312 let mut pos = usize::try_from(offset)
6313 .map_err(|_| Error::CorruptObject("pack offset exceeds memory".to_owned()))?;
6314 let (packed_type, _) = parse_pack_object_header(&bytes, &mut pos)?;
6315 let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
6316 slots.insert(oid, PackedSlot { pack, whole });
6317 }
6318 }
6319 Ok(slots)
6320}
6321
6322pub(crate) type OnDiskReuseZlibEntry = (ObjectId, u64, Arc<PackData>, usize, usize);
6325
6326pub(crate) fn collect_packed_delta_reuse_for_send(
6345 objects_dir: &Path,
6346 send: &HashSet<ObjectId>,
6347 reusable_base: &dyn Fn(&ObjectId) -> bool,
6348) -> Result<HashMap<ObjectId, OnDiskReuseZlibEntry>> {
6349 let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
6350 sort_pack_indexes_oldest_first(&mut indexes);
6351
6352 let mut found: HashMap<ObjectId, OnDiskReuseZlibEntry> = HashMap::new();
6353 for idx in indexes.iter().filter(|idx| idx.hash_bytes() == 20) {
6354 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6355 if index_scan_is_cheaper(idx, send.len()) {
6356 scan_packed_delta_reuse(idx, &pack_bytes, send, reusable_base, &mut found)?;
6357 continue;
6358 }
6359 for (oid, offset) in wanted_index_entries(idx, send.iter(), |oid| send.contains(oid))? {
6360 if found.contains_key(&oid) || !reusable_base(&oid) {
6361 continue;
6362 }
6363 let Some((base, size, zlib_start, zlib_end)) =
6364 parse_in_pack_delta_reuse_zlib(idx, &pack_bytes, offset, reusable_base)?
6365 else {
6366 continue;
6367 };
6368 found.insert(
6369 oid,
6370 (base, size, Arc::clone(&pack_bytes), zlib_start, zlib_end),
6371 );
6372 }
6373 }
6374 let crosses_packs = |(_, (base, _, pack, ..)): &(&ObjectId, &OnDiskReuseZlibEntry)| {
6377 found
6378 .get(base)
6379 .is_some_and(|(_, _, base_pack, ..)| !Arc::ptr_eq(base_pack, pack))
6380 };
6381 let starts = found.iter().filter(crosses_packs).map(|(t, _)| *t);
6382 for target in delta_cycle_breaks(starts, |oid| found.get(oid).map(|e| e.0)) {
6383 found.remove(&target);
6384 }
6385 Ok(found)
6386}
6387
6388fn scan_packed_delta_reuse(
6393 idx: &PackIndex,
6394 pack_bytes: &Arc<PackData>,
6395 send: &HashSet<ObjectId>,
6396 reusable_base: &dyn Fn(&ObjectId) -> bool,
6397 found: &mut HashMap<ObjectId, OnDiskReuseZlibEntry>,
6398) -> Result<()> {
6399 let pack_end = pack_bytes.len().checked_sub(idx.hash_bytes());
6400 idx.with_offset_order(|pairs| -> Result<()> {
6401 let row_at_offset = |offset: u64| {
6402 Ok(pairs
6403 .binary_search_by_key(&offset, |p| p.0)
6404 .ok()
6405 .map(|i| pairs[i].1 as usize))
6406 };
6407 for (i, &(offset, row)) in pairs.iter().enumerate() {
6408 let Ok(oid) = ObjectId::from_bytes(idx.oid_at(row as usize)) else {
6409 continue;
6410 };
6411 if !send.contains(&oid) || found.contains_key(&oid) || !reusable_base(&oid) {
6412 continue;
6413 }
6414 let entry_end = pack_end.filter(|&end| (offset as usize) < end).map(|end| {
6415 pairs[i + 1..]
6416 .iter()
6417 .find(|p| p.0 > offset)
6418 .map_or(end, |p| (p.0 as usize).min(end))
6419 });
6420 let Some((base, size, zlib_start, zlib_end)) = parse_delta_reuse_zlib_at(
6421 idx,
6422 pack_bytes,
6423 offset,
6424 reusable_base,
6425 DeltaReuseLookups {
6426 row_at_offset: &row_at_offset,
6427 entry_end: &|| Ok(entry_end),
6428 entry_crc: &|| Ok(idx.crc32_at(row as usize)),
6429 },
6430 )?
6431 else {
6432 continue;
6433 };
6434 found.insert(
6435 oid,
6436 (base, size, Arc::clone(pack_bytes), zlib_start, zlib_end),
6437 );
6438 }
6439 Ok(())
6440 })?
6441}
6442
6443pub fn packed_ref_delta_reuse_slice(
6452 objects_dir: &Path,
6453 oid: &ObjectId,
6454 packed_set: &HashSet<ObjectId>,
6455) -> Result<Option<(ObjectId, Vec<u8>)>> {
6456 for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Oldest)? {
6457 if idx.hash_bytes() != 20 {
6458 continue;
6459 }
6460 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6461 let Some((base, delta)) =
6462 parse_in_pack_delta_reuse_candidate(&idx, &pack_bytes, entry_offset, packed_set)?
6463 else {
6464 continue;
6465 };
6466 if reuse_delta_matches_pack_resolution(&idx, oid, base, &delta) {
6467 return Ok(Some((base, delta)));
6468 }
6469 }
6470 Ok(None)
6471}
6472
6473pub(crate) fn sort_pack_indexes_oldest_first(indexes: &mut [Arc<PackIndex>]) {
6478 if indexes.len() < 2 {
6479 return;
6480 }
6481 indexes.sort_by_cached_key(|idx| (pack_mtime(idx), idx.pack_path.clone()));
6482}
6483
6484fn pack_mtime(idx: &PackIndex) -> std::time::SystemTime {
6485 fs::metadata(&idx.pack_path)
6486 .and_then(|m| m.modified())
6487 .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
6488}
6489
6490#[derive(Clone, Copy)]
6492enum PackPreference {
6493 Oldest,
6495 Newest,
6497}
6498
6499fn packs_holding(
6505 objects_dir: &Path,
6506 oid: &ObjectId,
6507 preference: PackPreference,
6508) -> Result<Vec<(Arc<PackIndex>, u64)>> {
6509 let mut holding = Vec::new();
6510 for idx in read_local_pack_indexes_cached(objects_dir)? {
6511 if let Some(offset) = idx.find_offset(oid)? {
6512 holding.push((idx, offset));
6513 }
6514 }
6515 if holding.len() > 1 {
6516 let key = |idx: &PackIndex| (pack_mtime(idx), idx.pack_path.clone());
6517 match preference {
6518 PackPreference::Oldest => holding.sort_by_cached_key(|(idx, _)| key(idx)),
6519 PackPreference::Newest => {
6520 holding.sort_by_cached_key(|(idx, _)| std::cmp::Reverse(key(idx)));
6521 }
6522 }
6523 }
6524 Ok(holding)
6525}
6526
6527pub fn packed_delta_base_oid(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectId>> {
6529 for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
6530 if idx.hash_bytes() != 20 {
6531 continue;
6532 }
6533 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6534 let mut p = entry_offset as usize;
6535 let (packed_type, _) = parse_pack_object_header(&pack_bytes, &mut p)?;
6536 match packed_type {
6537 PackedType::RefDelta => {
6538 let hb = idx.hash_bytes();
6539 if p + hb > pack_bytes.len() {
6540 return Err(Error::CorruptObject("truncated ref-delta base".to_owned()));
6541 }
6542 return Ok(Some(ObjectId::from_bytes(&pack_bytes[p..p + hb])?));
6543 }
6544 PackedType::OfsDelta => {
6545 let base_off = parse_ofs_delta_base(&pack_bytes, &mut p, entry_offset)?;
6546 return Ok(idx
6547 .find_position_by_offset_sorted(base_off)?
6548 .and_then(|pos| ObjectId::from_bytes(idx.oid_at(pos)).ok()));
6549 }
6550 _ => continue,
6551 }
6552 }
6553 Ok(None)
6554}
6555
6556pub(crate) fn pack_entry_raw_end(
6560 idx: &PackIndex,
6561 pack_bytes: &[u8],
6562 entry_offset: u64,
6563) -> Result<Option<usize>> {
6564 let hb = idx.hash_bytes();
6565 let Some(pack_end) = pack_bytes.len().checked_sub(hb) else {
6566 return Ok(None);
6567 };
6568 let start = entry_offset as usize;
6569 if start >= pack_end {
6570 return Ok(None);
6571 }
6572 Ok(Some(
6573 idx.next_pack_offset_after(entry_offset, pack_end as u64)? as usize,
6574 ))
6575}
6576
6577pub(crate) struct PackedWholeObject {
6579 pub(crate) pack: Arc<PackData>,
6581 pub(crate) entry_offset: u64,
6583 pub(crate) entry_end: usize,
6585 pub(crate) zlib_offset: usize,
6587 pub(crate) data_end: usize,
6589 pub(crate) kind: ObjectKind,
6591 pub(crate) size: u64,
6593 pub(crate) index_crc: Option<u32>,
6595}
6596
6597pub(crate) fn count_local_packs_holding(objects_dir: &Path, oid: &ObjectId) -> Result<usize> {
6599 Ok(packs_holding(objects_dir, oid, PackPreference::Newest)?.len())
6600}
6601
6602pub(crate) enum PackedEntry {
6604 Whole(PackedWholeObject),
6606 NotWhole,
6609 Absent,
6611}
6612
6613pub(crate) fn locate_packed_whole_object(
6625 objects_dir: &Path,
6626 oid: &ObjectId,
6627 verify_crc: bool,
6628) -> Result<PackedEntry> {
6629 let indexes = read_local_pack_indexes_cached(objects_dir)?;
6630 let mut found = PackedEntry::Absent;
6631 for idx in &indexes {
6632 let Some(entry_offset) = idx.find_offset(oid)? else {
6633 continue;
6634 };
6635 found = PackedEntry::NotWhole;
6636 let pack = read_pack_bytes_cached(&idx.pack_path)?;
6637 let Some(data_end) = pack.len().checked_sub(idx.hash_bytes()) else {
6638 continue;
6639 };
6640 let Ok(mut pos) = usize::try_from(entry_offset) else {
6641 continue;
6642 };
6643 if pos >= data_end {
6644 continue;
6645 }
6646 let Ok((packed_type, size)) = parse_pack_object_header(&pack[..data_end], &mut pos) else {
6647 continue;
6648 };
6649 let Ok(kind) = packed_type_to_kind(packed_type) else {
6650 continue;
6651 };
6652 if verify_crc && !packed_entry_matches_index_crc(idx, &pack, entry_offset)? {
6653 continue;
6654 }
6655 let entry_end = pack_entry_raw_end(idx, &pack, entry_offset)?.unwrap_or(data_end);
6656 let index_crc = if verify_crc {
6657 None
6658 } else {
6659 idx.crc32_for_pack_offset(entry_offset).ok().flatten()
6660 };
6661 return Ok(PackedEntry::Whole(PackedWholeObject {
6662 pack,
6663 entry_offset,
6664 entry_end,
6665 zlib_offset: pos,
6666 data_end,
6667 kind,
6668 size,
6669 index_crc,
6670 }));
6671 }
6672 Ok(found)
6673}
6674
6675pub(crate) fn packed_entry_matches_index_crc(
6684 idx: &PackIndex,
6685 pack: &PackData,
6686 entry_offset: u64,
6687) -> Result<bool> {
6688 let Some(recorded) = idx.crc32_for_pack_offset(entry_offset)? else {
6689 return Ok(true);
6690 };
6691 let Some(end) = pack_entry_raw_end(idx, pack, entry_offset)? else {
6692 return Ok(false);
6693 };
6694 let start = usize::try_from(entry_offset)
6695 .map_err(|_| Error::CorruptObject(format!("pack offset {entry_offset} overflows")))?;
6696 if start >= end {
6697 return Ok(false);
6698 }
6699 let budget = ResidentBudget::new(pack, VERIFY_RESIDENT_PACK_BYTES);
6700 Ok(budget.crc32(&pack[start..end]) == recorded)
6701}
6702
6703const VERIFY_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
6706
6707pub fn packed_full_object_slice(objects_dir: &Path, oid: &ObjectId) -> Result<Option<Vec<u8>>> {
6726 Ok(packed_full_object_range(objects_dir, oid)?
6727 .map(|(bytes, start, end)| bytes[start..end].to_vec()))
6728}
6729
6730pub(crate) fn packed_full_object_range(
6733 objects_dir: &Path,
6734 oid: &ObjectId,
6735) -> Result<Option<(Arc<PackData>, usize, usize)>> {
6736 for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
6737 let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6738 let start = entry_offset as usize;
6739 let mut p = start;
6740 let Ok((packed_type, _size)) = parse_pack_object_header(&pack_bytes, &mut p) else {
6741 continue;
6742 };
6743 if matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta) {
6744 continue;
6746 }
6747 let Some(end) = pack_entry_raw_end(&idx, &pack_bytes, entry_offset)? else {
6748 continue;
6749 };
6750 if end <= start {
6751 continue;
6752 }
6753 if !packed_entry_matches_index_crc(&idx, &pack_bytes, entry_offset)? {
6757 continue;
6758 }
6759 return Ok(Some((pack_bytes, start, end)));
6760 }
6761 Ok(None)
6762}
6763
6764pub(crate) fn decode_pack_object_size_varint_from_first(
6766 first: u8,
6767 mut next: impl FnMut() -> Result<u8>,
6768) -> Result<u64> {
6769 let mut size = (first & 0x0f) as u64;
6770 let mut shift = 4u32;
6771 let mut c = first;
6772 while (c & 0x80) != 0 {
6773 if shift >= 64 {
6774 return Err(Error::CorruptObject(
6775 "pack object size varint overflow".to_owned(),
6776 ));
6777 }
6778 c = next()?;
6779 let digit = (c & 0x7f) as u64;
6780 size = size
6781 .checked_add(
6782 digit
6783 .checked_shl(shift)
6784 .ok_or_else(|| Error::CorruptObject("pack object size overflow".to_owned()))?,
6785 )
6786 .ok_or_else(|| Error::CorruptObject("pack object size overflow".to_owned()))?;
6787 shift += 7;
6788 }
6789 Ok(size)
6790}
6791
6792pub(crate) fn decode_pack_object_size_varint(
6794 first: u8,
6795 bytes: &[u8],
6796 pos: &mut usize,
6797) -> Result<u64> {
6798 decode_pack_object_size_varint_from_first(first, || {
6799 let b = *bytes.get(*pos).ok_or_else(|| {
6800 Error::CorruptObject("unexpected end of variable size header".to_owned())
6801 })?;
6802 *pos += 1;
6803 Ok(b)
6804 })
6805}
6806
6807pub(crate) fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
6808 let first = *bytes.get(*pos).ok_or_else(|| {
6809 Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
6810 })?;
6811 *pos += 1;
6812
6813 let type_code = (first >> 4) & 0x7;
6814 let size = decode_pack_object_size_varint(first, bytes, pos)?;
6815
6816 let packed_type = match type_code {
6817 1 => PackedType::Commit,
6818 2 => PackedType::Tree,
6819 3 => PackedType::Blob,
6820 4 => PackedType::Tag,
6821 6 => PackedType::OfsDelta,
6822 7 => PackedType::RefDelta,
6823 _ => {
6824 return Err(Error::CorruptObject(format!(
6825 "unsupported packed object type {}",
6826 type_code
6827 )))
6828 }
6829 };
6830 Ok((packed_type, size))
6831}
6832
6833#[derive(Debug, Clone, Copy)]
6835pub enum PackedDeltaDependency {
6836 OfsBase {
6838 base_offset: u64,
6840 },
6841 RefBase {
6843 base_oid: ObjectId,
6845 },
6846}
6847
6848pub fn read_packed_delta_dependency(
6852 pack_bytes: &[u8],
6853 object_offset: u64,
6854 hash_bytes: usize,
6855) -> Result<Option<PackedDeltaDependency>> {
6856 let mut pos = object_offset as usize;
6857 let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
6858 match ty {
6859 PackedType::OfsDelta => {
6860 let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
6861 Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
6862 }
6863 PackedType::RefDelta => {
6864 if pos + hash_bytes > pack_bytes.len() {
6865 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
6866 }
6867 let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + hash_bytes])?;
6868 Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
6869 }
6870 _ => Ok(None),
6871 }
6872}
6873
6874pub(crate) fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
6875 let mut c = *bytes
6876 .get(*pos)
6877 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
6878 *pos += 1;
6879 let mut value = (c & 0x7f) as u64;
6880 while (c & 0x80) != 0 {
6881 c = *bytes
6882 .get(*pos)
6883 .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
6884 *pos += 1;
6885 value = ((value + 1) << 7) | (c & 0x7f) as u64;
6886 }
6887 this_offset
6888 .checked_sub(value)
6889 .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
6890}
6891
6892pub fn slice_one_pack_object(
6900 bytes: &[u8],
6901 object_start_offset: u64,
6902 hash_bytes: usize,
6903) -> Result<&[u8]> {
6904 let start = object_start_offset as usize;
6905 let mut pos = start;
6906 skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
6907 Ok(&bytes[start..pos])
6908}
6909
6910pub fn skip_one_pack_object(
6912 bytes: &[u8],
6913 pos: &mut usize,
6914 object_start_offset: u64,
6915 hash_bytes: usize,
6916) -> Result<()> {
6917 let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
6918 let mut scratch = ZlibInflateScratch::default();
6919 match packed_type {
6920 PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
6921 scratch.skip_zlib_stream(bytes, pos, size)?;
6922 }
6923 PackedType::RefDelta => {
6924 if *pos + hash_bytes > bytes.len() {
6925 return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
6926 }
6927 *pos += hash_bytes;
6928 scratch.skip_zlib_stream(bytes, pos, size)?;
6929 }
6930 PackedType::OfsDelta => {
6931 let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
6932 scratch.skip_zlib_stream(bytes, pos, size)?;
6933 }
6934 }
6935 Ok(())
6936}
6937
6938pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
6940 let index = read_pack_index(idx_path)?;
6941 let mut out = Vec::new();
6942 let hash_bytes = index.hash_bytes();
6943 for e in index.iter() {
6944 if e.oid().len() == hash_bytes {
6945 out.push(ObjectId::from_bytes(e.oid())?);
6946 }
6947 }
6948 Ok(out)
6949}
6950
6951#[cfg(any(test, feature = "test-support"))]
6952pub(crate) mod pack_cache_test_sync {
6953 use std::cell::Cell;
6954 use std::sync::Mutex;
6955 use std::sync::MutexGuard;
6956
6957 struct PackCacheTestCoordinator {
6959 top_level_guards: u32,
6960 }
6961
6962 static COORD: Mutex<PackCacheTestCoordinator> = Mutex::new(PackCacheTestCoordinator {
6964 top_level_guards: 0,
6965 });
6966
6967 thread_local! {
6969 static DEPTH: Cell<u32> = const { Cell::new(0) };
6970 }
6971
6972 pub struct PackCacheTestGuard {
6974 _inner: Option<MutexGuard<'static, PackCacheTestCoordinator>>,
6975 }
6976
6977 pub fn acquire() -> PackCacheTestGuard {
6978 let depth = DEPTH.with(|d| {
6979 let n = d.get();
6980 d.set(n + 1);
6981 n
6982 });
6983 PackCacheTestGuard {
6984 _inner: if depth == 0 {
6985 let mut g = COORD.lock().unwrap_or_else(|e| e.into_inner());
6986 assert_eq!(
6987 g.top_level_guards, 0,
6988 "pack cache test coordinator already held"
6989 );
6990 g.top_level_guards = 1;
6991 Some(g)
6992 } else {
6993 None
6994 },
6995 }
6996 }
6997
6998 impl Drop for PackCacheTestGuard {
6999 fn drop(&mut self) {
7000 DEPTH.with(|d| {
7001 let n = d.get();
7002 assert!(n > 0, "pack cache test guard depth underflow");
7003 d.set(n - 1);
7004 if n == 1 {
7005 if let Some(mut g) = self._inner.take() {
7006 assert_eq!(g.top_level_guards, 1);
7007 g.top_level_guards = 0;
7008 }
7009 }
7010 });
7011 }
7012 }
7013}
7014
7015#[cfg(any(test, feature = "test-support"))]
7016pub(crate) fn pack_cache_test_guard() -> pack_cache_test_sync::PackCacheTestGuard {
7017 pack_cache_test_sync::acquire()
7018}
7019
7020#[cfg(test)]
7021mod tests {
7022 use super::*;
7023 use crate::delta_encode::encode_lcp_delta;
7024 use crate::odb::Odb;
7025 use crate::pack_map::PackData;
7026 use crate::transfer::{build_pack, PackBuildOptions};
7027 use flate2::write::ZlibEncoder;
7028 use flate2::Compression;
7029 use std::io::Write;
7030 use std::process::Command;
7031
7032 fn git_try(dir: &std::path::Path, args: &[&str]) -> bool {
7033 Command::new("git") .current_dir(dir)
7035 .args(args)
7036 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7037 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7038 .output()
7039 .map(|o| o.status.success())
7040 .unwrap_or(false)
7041 }
7042
7043 fn git(dir: &std::path::Path, args: &[&str]) {
7044 let out = Command::new("git") .current_dir(dir)
7046 .args(args)
7047 .env("GIT_AUTHOR_NAME", "T")
7048 .env("GIT_AUTHOR_EMAIL", "t@example.com")
7049 .env("GIT_COMMITTER_NAME", "T")
7050 .env("GIT_COMMITTER_EMAIL", "t@example.com")
7051 .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
7052 .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
7053 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7054 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7055 .output()
7056 .expect("git");
7057 assert!(
7058 out.status.success(),
7059 "git {:?}: {}",
7060 args,
7061 String::from_utf8_lossy(&out.stderr)
7062 );
7063 }
7064
7065 fn append_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
7066 let mut size = payload_len;
7067 let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
7068 size >>= 4;
7069 if size > 0 {
7070 buf.push(first | 0x80);
7071 while size > 0 {
7072 let b = (size & 0x7f) as u8;
7073 size >>= 7;
7074 buf.push(if size > 0 { b | 0x80 } else { b });
7075 }
7076 } else {
7077 buf.push(first);
7078 }
7079 }
7080
7081 fn zlib_pack(payload: &[u8]) -> Vec<u8> {
7082 let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
7083 enc.write_all(payload).unwrap();
7084 enc.finish().unwrap()
7085 }
7086
7087 fn append_sha1_pack_trailer(buf: &mut Vec<u8>) {
7088 buf.extend_from_slice(HashAlgo::Sha1.checksum_digest(&*buf).as_slice());
7089 }
7090
7091 fn append_ref_delta(buf: &mut Vec<u8>, base_oid: &ObjectId, delta: &[u8]) {
7092 let compressed = zlib_pack(delta);
7093 append_pack_object_header(buf, 7, delta.len());
7094 buf.extend_from_slice(base_oid.as_bytes());
7095 buf.extend_from_slice(&compressed);
7096 }
7097
7098 fn append_ofs_delta(buf: &mut Vec<u8>, base_offset: u64, delta: &[u8]) -> u64 {
7099 let object_start = buf.len() as u64;
7100 let compressed = zlib_pack(delta);
7101 append_pack_object_header(buf, 6, delta.len());
7102 let mut ofs = object_start - base_offset;
7103 while ofs >= 0x80 {
7104 buf.push((ofs as u8 & 0x7f) | 0x80);
7105 ofs >>= 7;
7106 }
7107 buf.push(ofs as u8);
7108 buf.extend_from_slice(&compressed);
7109 object_start
7110 }
7111
7112 #[test]
7113 fn commit_header_block_inflate_stops_at_the_headers() {
7114 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
7116 let mut next = || {
7117 state = state
7118 .wrapping_mul(6_364_136_223_846_793_005)
7119 .wrapping_add(1_442_695_040_888_963_407);
7120 state
7121 };
7122 let parents: String = (0..12)
7123 .map(|_| {
7124 format!(
7125 "parent {:016x}{:016x}{:08x}\n",
7126 next(),
7127 next(),
7128 next() as u32
7129 )
7130 })
7131 .collect();
7132 let headers = format!(
7133 "tree {:040x}\n{parents}author A <a@example.com> 1 +0000\n\
7134 committer C <c@example.com> 2 +0000\n\n",
7135 7
7136 );
7137 let mut zlib = ZlibInflateScratch::default();
7138 let mut out = Vec::new();
7139 for body in [String::new(), "subject\n".to_owned(), "x\n".repeat(5000)] {
7140 let commit = format!("{headers}{body}");
7141 let compressed = zlib_pack(commit.as_bytes());
7142 assert!(
7143 compressed.len() > COMMIT_HEADER_BLOCK_INPUT_STEP,
7144 "the headers alone should take more than one input step"
7145 );
7146 assert!(inflate_commit_header_block(
7147 &compressed,
7148 commit.len() as u64,
7149 COMMIT_HEADER_BLOCK_MAX,
7150 &mut zlib,
7151 &mut out,
7152 ));
7153 assert_eq!(out, headers.as_bytes(), "body of {} bytes", body.len());
7154 }
7155
7156 let commit = format!("{headers}subject\n");
7157 let compressed = zlib_pack(commit.as_bytes());
7158 let size = commit.len() as u64;
7159 assert!(
7160 !inflate_commit_header_block(&compressed, size, headers.len() - 3, &mut zlib, &mut out),
7161 "headers longer than the budget"
7162 );
7163 assert!(
7164 !inflate_commit_header_block(&compressed[..40], size, 1024, &mut zlib, &mut out),
7165 "a stream cut off before the end of the headers"
7166 );
7167 let no_blank = b"tree 0000000000000000000000000000000000000007\nauthor A";
7168 assert!(
7169 !inflate_commit_header_block(
7170 &zlib_pack(no_blank),
7171 no_blank.len() as u64,
7172 1024,
7173 &mut zlib,
7174 &mut out,
7175 ),
7176 "a payload with no blank line"
7177 );
7178 assert!(
7179 !inflate_commit_header_block(
7180 &compressed,
7181 headers.len() as u64 - 3,
7182 1024,
7183 &mut zlib,
7184 &mut out,
7185 ),
7186 "a stream that inflates past its declared size"
7187 );
7188 }
7189
7190 fn append_whole_blob(buf: &mut Vec<u8>, data: &[u8]) -> u64 {
7191 let off = buf.len() as u64;
7192 let compressed = zlib_pack(data);
7193 append_pack_object_header(buf, 3, data.len());
7194 buf.extend_from_slice(&compressed);
7195 off
7196 }
7197
7198 fn append_corrupt_whole_blob(buf: &mut Vec<u8>) -> u64 {
7199 let off = buf.len() as u64;
7200 append_pack_object_header(buf, 3, 8);
7201 buf.extend_from_slice(&[0xff, 0xfe, 0xfd]);
7202 off
7203 }
7204
7205 fn write_v2_idx_for_test(
7206 idx_path: &Path,
7207 pack_path: &Path,
7208 entries: &[(ObjectId, u64)],
7209 ) -> Result<()> {
7210 let pack_bytes = std::fs::read(pack_path)?;
7211 let mut with_crc = Vec::with_capacity(entries.len());
7212 for (oid, off) in entries {
7213 let start = *off as usize;
7214 let mut end = start;
7215 skip_one_pack_object(&pack_bytes, &mut end, *off, 20)
7216 .map_err(|e| Error::CorruptObject(format!("idx crc walk: {e}")))?;
7217 with_crc.push((*oid, *off, crc32fast::hash(&pack_bytes[start..end])));
7218 }
7219 write_v2_pack_index(idx_path, pack_path, &with_crc, 20)
7220 }
7221
7222 fn single_blob_pack(data: &[u8]) -> (Vec<u8>, ObjectId, u64) {
7223 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7224 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
7225 let mut pack = Vec::new();
7226 pack.extend_from_slice(b"PACK");
7227 pack.extend_from_slice(&2u32.to_be_bytes());
7228 pack.extend_from_slice(&1u32.to_be_bytes());
7229 let off = append_whole_blob(&mut pack, data);
7230 append_sha1_pack_trailer(&mut pack);
7231 (pack, oid, off)
7232 }
7233
7234 #[cfg_attr(
7235 not(feature = "full-tests"),
7236 ignore = "starts system git or is heavy; run with --features full-tests"
7237 )]
7238 #[test]
7239 fn read_commit_parents_reads_whole_commit_entries() {
7240 let _guard = pack_cache_test_guard();
7241 clear_pack_cache();
7242 let tmp = tempfile::tempdir().expect("tempdir");
7243 let wt = tmp.path();
7244 git(wt, &["init", "-q", "-b", "main"]);
7245 let commit = |file: &str| {
7246 fs::write(wt.join(file), file).expect("write");
7247 git(wt, &["add", file]);
7248 git(wt, &["commit", "-qm", file]);
7249 };
7250 commit("root");
7251 for leg in ["a", "b", "c"] {
7252 git(wt, &["switch", "-qc", leg, "main"]);
7253 commit(leg);
7254 }
7255 git(wt, &["switch", "-q", "main"]);
7256 commit("main");
7257 git(wt, &["merge", "-q", "--no-edit", "a"]);
7258 git(wt, &["merge", "-q", "--no-edit", "b", "c"]);
7259 git(wt, &["repack", "-q", "-a", "-d"]);
7260 let rev_parse = |spec: &str| {
7261 let out = Command::new("git") .current_dir(wt)
7263 .args(["rev-parse", spec])
7264 .output()
7265 .expect("git rev-parse");
7266 ObjectId::from_hex(String::from_utf8_lossy(&out.stdout).trim()).expect("oid")
7267 };
7268 let octopus = rev_parse("HEAD");
7269 let merge = rev_parse("HEAD^1");
7270 let main = rev_parse("HEAD^1^1");
7271 let root = rev_parse("HEAD^1^1^1");
7272 let objects = wt.join(".git/objects");
7273 let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
7274 crate::pack_store::PackStore::with_context(store, || {
7275 let mut reader = SingleLocalPackReader::open_allow_loose(&objects)
7276 .expect("open")
7277 .expect("single pack");
7278 assert_eq!(
7279 reader.read_commit_parents(&root, 40).expect("root"),
7280 Some(vec![])
7281 );
7282 assert_eq!(
7283 reader.read_commit_parents(&main, 40).expect("main"),
7284 Some(vec![root])
7285 );
7286 assert_eq!(
7287 reader.read_commit_parents(&merge, 40).expect("merge"),
7288 Some(vec![main, rev_parse("a")])
7289 );
7290 assert_eq!(
7291 reader.read_commit_parents(&octopus, 40).expect("octopus"),
7292 Some(vec![merge, rev_parse("b"), rev_parse("c")])
7293 );
7294 let tree = rev_parse("HEAD^{tree}");
7295 assert_eq!(reader.read_commit_parents(&tree, 40).expect("tree"), None);
7296 let absent =
7297 ObjectId::from_hex("1234567890123456789012345678901234567890").expect("absent oid");
7298 assert_eq!(
7299 reader.read_commit_parents(&absent, 40).expect("absent"),
7300 None
7301 );
7302 });
7303 }
7304
7305 #[cfg_attr(
7306 not(feature = "full-tests"),
7307 ignore = "starts system git or is heavy; run with --features full-tests"
7308 )]
7309 #[test]
7310 fn read_commit_parents_rejects_a_bad_zlib_checksum_after_the_headers() {
7311 let _guard = pack_cache_test_guard();
7312 clear_pack_cache();
7313 let tmp = tempfile::tempdir().expect("tempdir");
7314 let wt = tmp.path();
7315 git(wt, &["init", "-q", "-b", "main"]);
7316 fs::write(wt.join("f"), "f").expect("write");
7317 git(wt, &["add", "f"]);
7318 git(wt, &["commit", "-qm", "only commit"]);
7319 git(wt, &["repack", "-q", "-a", "-d", "-f"]);
7320 let git_out = |args: &[&str]| {
7321 let out = Command::new("git") .current_dir(wt)
7323 .args(args)
7324 .output()
7325 .expect("git");
7326 String::from_utf8_lossy(&out.stdout).into_owned()
7327 };
7328 let head = ObjectId::from_hex(git_out(&["rev-parse", "HEAD"]).trim()).expect("oid");
7329 let pack_dir = wt.join(".git/objects/pack");
7330 let idx_path = fs::read_dir(&pack_dir)
7331 .expect("pack dir")
7332 .filter_map(|e| e.ok().map(|e| e.path()))
7333 .find(|p| p.extension().is_some_and(|x| x == "idx"))
7334 .expect("pack index");
7335 let listing = git_out(&["verify-pack", "-v", idx_path.to_str().expect("utf-8")]);
7336 let (offset, in_pack) = listing
7337 .lines()
7338 .find_map(|line| {
7339 let f: Vec<&str> = line.split_whitespace().collect();
7340 (f.first() == Some(&head.to_hex().as_str())).then(|| {
7341 (
7342 f[4].parse::<usize>().expect("offset"),
7343 f[3].parse::<usize>().expect("size"),
7344 )
7345 })
7346 })
7347 .expect("commit entry");
7348 let pack_path = idx_path.with_extension("pack");
7351 let mut bytes = fs::read(&pack_path).expect("read pack");
7352 bytes[offset + in_pack - 1] ^= 0x01;
7353 let mut perms = fs::metadata(&pack_path).expect("stat pack").permissions();
7354 #[allow(clippy::permissions_set_readonly_false)]
7355 perms.set_readonly(false);
7356 fs::set_permissions(&pack_path, perms).expect("make pack writable");
7357 fs::write(&pack_path, &bytes).expect("write pack");
7358
7359 let objects = wt.join(".git/objects");
7360 let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
7361 crate::pack_store::PackStore::with_context(store, || {
7362 let mut reader = SingleLocalPackReader::open_allow_loose(&objects)
7363 .expect("open")
7364 .expect("single pack");
7365 assert_eq!(
7366 reader.read_commit_parents(&head, 40).expect("lookup"),
7367 None,
7368 "a corrupt stream must not yield parents"
7369 );
7370 });
7371 }
7372
7373 #[test]
7374 fn single_local_pack_reader_batch_check_caches_deltified_entries() {
7375 let _guard = pack_cache_test_guard();
7376 clear_pack_cache();
7377 let Some((tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(2) else {
7378 return;
7379 };
7380 let objects = tmp.path().join("objects");
7381 let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
7382 crate::pack_store::PackStore::with_context(store, || {
7383 let mut reader = SingleLocalPackReader::open(&objects)
7384 .expect("open")
7385 .expect("single pack");
7386 let info1 = reader.read_info(&tip).expect("first read");
7387 let info2 = reader.read_info(&tip).expect("cached read");
7388 assert_eq!(info1, info2);
7389 let full = read_object_from_pack(&idx, &tip).expect("full read");
7390 assert_eq!(info1.kind, full.kind);
7391 assert_eq!(info1.size, full.data.len() as u64);
7392 });
7393 let _ = odb;
7394 }
7395
7396 #[test]
7397 fn pack_read_info_cache_is_scoped_per_repository_and_pack_bytes() {
7398 let _guard = pack_cache_test_guard();
7399 clear_pack_cache();
7400 const ENTRY_OFFSET: u64 = 200;
7401 let dir_a = tempfile::tempdir().expect("tempdir a");
7402 let dir_b = tempfile::tempdir().expect("tempdir b");
7403 let objects_a = dir_a.path().join("objects");
7404 let objects_b = dir_b.path().join("objects");
7405 fs::create_dir_all(objects_a.join("pack")).expect("pack dir a");
7406 fs::create_dir_all(objects_b.join("pack")).expect("pack dir b");
7407 let pack_a = objects_a.join("pack/p-a.pack");
7408 let pack_b = objects_b.join("pack/p-b.pack");
7409 let (oid_a, _) =
7410 write_sparse_single_blob_pack(dir_a.path(), &pack_a, b"five!", ENTRY_OFFSET)
7411 .expect("pack a");
7412 let (oid_b, _) =
7413 write_sparse_single_blob_pack(dir_b.path(), &pack_b, b"1234567890", ENTRY_OFFSET)
7414 .expect("pack b");
7415 let idx_a = pack_a.with_extension("idx");
7416 let idx_b = pack_b.with_extension("idx");
7417 write_v2_idx_for_test(&idx_a, &pack_a, &[(oid_a, ENTRY_OFFSET)]).expect("idx a");
7418 write_v2_idx_for_test(&idx_b, &pack_b, &[(oid_b, ENTRY_OFFSET)]).expect("idx b");
7419 let idx_a = read_pack_index(&idx_a).expect("parse idx a");
7420 let idx_b = read_pack_index(&idx_b).expect("parse idx b");
7421
7422 let read_size = |objects: &Path, idx: &PackIndex| -> u64 {
7423 let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects);
7424 crate::pack_store::PackStore::with_context(store, || {
7425 super::read_object_info_from_pack_at_offset(idx, ENTRY_OFFSET)
7426 .expect("read_info")
7427 .size
7428 })
7429 };
7430
7431 let size_a = read_size(&objects_a, &idx_a);
7432 let size_b = read_size(&objects_b, &idx_b);
7433 assert_eq!(size_a, 5);
7434 assert_eq!(size_b, 10);
7435 assert_eq!(read_size(&objects_a, &idx_a), 5);
7437 assert_eq!(read_size(&objects_b, &idx_b), 10);
7438 assert_eq!(read_size(&objects_a, &idx_a), 5);
7439 }
7440 fn write_sparse_single_blob_pack(
7443 repo_root: &Path,
7444 pack_path: &Path,
7445 data: &[u8],
7446 object_offset: u64,
7447 ) -> Result<(ObjectId, u64)> {
7448 use std::fs::OpenOptions;
7449 use std::io::{Seek, SeekFrom, Write};
7450
7451 let odb = Odb::new(repo_root);
7452 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
7453 let mut file = OpenOptions::new()
7454 .create(true)
7455 .write(true)
7456 .read(true)
7457 .truncate(true)
7458 .open(pack_path)
7459 .map_err(Error::Io)?;
7460 file.write_all(b"PACK").map_err(Error::Io)?;
7461 file.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
7462 file.write_all(&1u32.to_be_bytes()).map_err(Error::Io)?;
7463 file.seek(SeekFrom::Start(object_offset))
7464 .map_err(Error::Io)?;
7465 let mut tail = Vec::new();
7466 append_whole_blob(&mut tail, data);
7467 file.write_all(&tail).map_err(Error::Io)?;
7468 let trailer = sha1_trailer_for_open_file(&mut file)?;
7469 file.write_all(trailer.as_bytes()).map_err(Error::Io)?;
7470 file.flush().map_err(Error::Io)?;
7471 Ok((oid, object_offset))
7472 }
7473
7474 #[test]
7475 fn skip_one_pack_object_rejects_zero_size_with_zlib_payload() {
7476 let mut bytes = Vec::new();
7477 bytes.push(0x30);
7478 bytes.extend_from_slice(&zlib_pack(b"x"));
7479 let mut pos = 0usize;
7480 let err = skip_one_pack_object(&bytes, &mut pos, 0, 20).unwrap_err();
7481 assert!(matches!(err, Error::CorruptObject(_)), "got {err:?}");
7482 }
7483
7484 #[test]
7485 fn parse_pack_object_header_rejects_all_ff_size_continuation() {
7486 let header = [
7487 0x80 | (3 << 4) | 0x0f,
7488 0xff,
7489 0xff,
7490 0xff,
7491 0xff,
7492 0xff,
7493 0xff,
7494 0xff,
7495 0xff,
7496 0xff,
7497 ];
7498 let mut pos = 0usize;
7499 let err = parse_pack_object_header(&header, &mut pos).unwrap_err();
7500 assert!(matches!(err, Error::CorruptObject(_)), "{err:?}");
7501 }
7502
7503 #[test]
7504 fn lying_huge_pack_blob_read_returns_corrupt_packed_object() {
7505 let tmp = tempfile::tempdir().expect("tempdir");
7506 let dir = tmp.path();
7507 if !git_try(dir, &["init", "-q"]) {
7508 return;
7509 }
7510 std::fs::write(dir.join("f"), b"hello\n").expect("write");
7511 git(dir, &["add", "f"]);
7512 git(
7513 dir,
7514 &[
7515 "-c",
7516 "user.name=t",
7517 "-c",
7518 "user.email=t@t",
7519 "commit",
7520 "-qm",
7521 "one",
7522 ],
7523 );
7524 let oid_hex = Command::new("git")
7525 .current_dir(dir)
7526 .args(["rev-parse", "HEAD:f"])
7527 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7528 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7529 .output()
7530 .expect("rev-parse")
7531 .stdout;
7532 let oid =
7533 ObjectId::from_hex(std::str::from_utf8(&oid_hex).unwrap().trim()).expect("parse oid");
7534 let objects = dir.join(".git/objects");
7535 let pack_dir = objects.join("pack");
7536 std::fs::create_dir_all(&pack_dir).expect("pack dir");
7537 let pack_stem = pack_dir.join("pack");
7538 let mut child = Command::new("git")
7539 .current_dir(dir)
7540 .args(["pack-objects", "-q"])
7541 .arg(&pack_stem)
7542 .stdin(std::process::Stdio::piped())
7543 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7544 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7545 .spawn()
7546 .expect("pack-objects spawn");
7547 use std::io::Write;
7548 child
7549 .stdin
7550 .take()
7551 .expect("stdin")
7552 .write_all(format!("{oid}\n").as_bytes())
7553 .expect("write oid");
7554 assert!(
7555 child.wait().expect("pack-objects wait").success(),
7556 "pack-objects failed"
7557 );
7558 let loose = objects.join(format!("{}/{}", &oid.to_hex()[..2], &oid.to_hex()[2..]));
7559 let _ = std::fs::remove_file(loose);
7560 let pack_path = std::fs::read_dir(&pack_dir)
7561 .expect("pack dir")
7562 .filter_map(|e| e.ok())
7563 .map(|e| e.path())
7564 .find(|p| p.extension().is_some_and(|x| x == "pack"))
7565 .expect("pack file");
7566 let idx_path = pack_path.with_extension("idx");
7567 let mut pack = std::fs::read(&pack_path).expect("read pack");
7568 assert_eq!(pack[12] & 0x80, 0, "expected single-byte size header");
7569 let tail = pack[13..].to_vec();
7570 pack.truncate(12);
7571 pack.extend_from_slice(&[0x80 | (3 << 4) | 0x0f, 0xff, 0xff, 0xff, 0xff, 0x7f]);
7572 pack.extend_from_slice(&tail);
7573 let pack_body_end = pack.len() - 20;
7574 let pack_hash = HashAlgo::Sha1.checksum_digest(&pack[..pack_body_end]);
7575 let pack_trailer = pack_hash.as_slice().to_vec();
7576 pack[pack_body_end..].copy_from_slice(&pack_trailer);
7577 let mut idx = std::fs::read(&idx_path).expect("read idx");
7578 let idx_len = idx.len();
7579 let crc_off = 8 + 1024 + 20;
7580 let crc = crc32fast::hash(&pack[12..pack_body_end]);
7581 idx[crc_off..crc_off + 4].copy_from_slice(&crc.to_be_bytes());
7582 idx[idx_len - 40..idx_len - 20].copy_from_slice(&pack_trailer);
7583 let idx_hash = HashAlgo::Sha1.checksum_digest(&idx[..idx_len - 20]);
7584 let idx_trailer = idx_hash.as_slice().to_vec();
7585 idx[idx_len - 20..].copy_from_slice(&idx_trailer);
7586 #[cfg(unix)]
7587 {
7588 use std::os::unix::fs::PermissionsExt;
7589 for path in [&pack_path, &idx_path] {
7590 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o644))
7591 .expect("chmod pack/index");
7592 }
7593 }
7594 std::fs::write(&pack_path, &pack).expect("write pack");
7595 std::fs::write(&idx_path, &idx).expect("write idx");
7596 clear_pack_cache();
7597 let odb = Odb::new(&objects);
7598 let err = odb.read(&oid).expect_err("lying header must not abort");
7599 assert!(matches!(err, Error::CorruptPackedObject { .. }), "{err:?}");
7600 }
7601
7602 fn install_synthetic_pack(
7603 objects_dir: &Path,
7604 stem: &str,
7605 pack: &[u8],
7606 entries: &[(ObjectId, u64)],
7607 ) -> PackIndex {
7608 install_synthetic_pack_inner(objects_dir, stem, pack, entries, true)
7609 }
7610
7611 fn install_synthetic_pack_no_clear(
7612 objects_dir: &Path,
7613 stem: &str,
7614 pack: &[u8],
7615 entries: &[(ObjectId, u64)],
7616 ) -> PackIndex {
7617 install_synthetic_pack_inner(objects_dir, stem, pack, entries, false)
7618 }
7619
7620 fn install_synthetic_pack_inner(
7621 objects_dir: &Path,
7622 stem: &str,
7623 pack: &[u8],
7624 entries: &[(ObjectId, u64)],
7625 clear_cache: bool,
7626 ) -> PackIndex {
7627 let pack_dir = objects_dir.join("pack");
7628 std::fs::create_dir_all(&pack_dir).expect("pack dir");
7629 let pack_path = pack_dir.join(format!("{stem}.pack"));
7630 let idx_path = pack_dir.join(format!("{stem}.idx"));
7631 std::fs::write(&pack_path, pack).expect("write pack");
7632 write_v2_idx_for_test(&idx_path, &pack_path, entries).expect("write idx");
7633 if clear_cache {
7634 clear_pack_cache();
7635 }
7636 read_pack_index(&idx_path).expect("read idx")
7637 }
7638
7639 fn pack_index_for_objects(
7640 pack_path: std::path::PathBuf,
7641 objects: &[(ObjectId, u64)],
7642 ) -> PackIndex {
7643 let idx_path = pack_path.with_extension("idx");
7644 let pack_bytes = fs::read(&pack_path).expect("read pack for idx trailer");
7645 let trailer = &pack_bytes[pack_bytes.len().saturating_sub(20)..];
7646 let rows: Vec<(ObjectId, u64, u32)> = objects
7647 .iter()
7648 .map(|(oid, offset)| (*oid, *offset, 0))
7649 .collect();
7650 write_v2_pack_index_with_trailer(&idx_path, &rows, trailer, 20, None).expect("write idx");
7651 read_pack_index(&idx_path).expect("read idx")
7652 }
7653
7654 fn index_pack_in_scratch(pack: &[u8]) -> (tempfile::TempDir, PackIndex) {
7655 let dir = tempfile::tempdir().expect("tempdir");
7656 let pack_path = dir.path().join("chain.pack");
7657 std::fs::write(&pack_path, pack).expect("write pack");
7658 let out = Command::new("git") .current_dir(dir.path())
7660 .args(["index-pack", "chain.pack"])
7661 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7662 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7663 .output()
7664 .expect("index-pack");
7665 assert!(
7666 out.status.success(),
7667 "git index-pack failed: {}",
7668 String::from_utf8_lossy(&out.stderr)
7669 );
7670 let idx_path = dir.path().join("chain.idx");
7671 let idx = read_pack_index(&idx_path).expect("read idx");
7672 (dir, idx)
7673 }
7674
7675 fn build_deep_delta_repo(commit_count: usize) -> Option<(tempfile::TempDir, ObjectId, Odb)> {
7677 let tmp = tempfile::tempdir().ok()?;
7678 let dir = tmp.path();
7679 if !git_try(dir, &["init", "-q", "-b", "main", "."]) {
7680 return None;
7681 }
7682 let mut body = String::new();
7683 for i in 0..200 {
7684 body.push_str(&format!("seed line {i:04}\n"));
7685 }
7686 for rev in 0..commit_count {
7687 body.push_str(&format!("edit-{rev}\n"));
7688 std::fs::write(dir.join("blob.txt"), body.as_bytes()).ok()?;
7689 git(dir, &["add", "blob.txt"]);
7690 git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
7691 }
7692 let tip_hex = Command::new("git") .current_dir(dir)
7694 .args(["rev-parse", "HEAD"])
7695 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7696 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7697 .output()
7698 .ok()?;
7699 if !tip_hex.status.success() {
7700 return None;
7701 }
7702 let tip = ObjectId::from_hex(std::str::from_utf8(&tip_hex.stdout).ok()?.trim()).ok()?;
7703 let git_dir = dir.join(".git");
7704 let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
7705 Some((tmp, tip, odb))
7706 }
7707
7708 #[test]
7709 fn verify_packed_object_hash_sha256_detects_corruption() {
7710 use crate::hash::hash_object;
7711 use std::fs;
7712
7713 let data = b"sha256 packed blob for hash verify\n";
7714 let oid = hash_object(HashAlgo::Sha256, ObjectKind::Blob, data).expect("hash");
7715 let wrong_oid =
7716 hash_object(HashAlgo::Sha256, ObjectKind::Blob, b"different payload\n").expect("hash");
7717 let mut pack = Vec::new();
7718 pack.extend_from_slice(b"PACK");
7719 pack.extend_from_slice(&2u32.to_be_bytes());
7720 pack.extend_from_slice(&1u32.to_be_bytes());
7721 let off = append_whole_blob(&mut pack, data);
7722 pack.extend_from_slice(HashAlgo::Sha256.checksum_digest(&pack).as_slice());
7723
7724 let dir = tempfile::tempdir().expect("tempdir");
7725 let pack_path = dir.path().join("sha256-corrupt.pack");
7726 fs::write(&pack_path, &pack).expect("write pack");
7727
7728 let idx_path = pack_path.with_extension("idx");
7729 let trailer = &pack[pack.len() - 32..];
7730 write_v2_pack_index_with_trailer(&idx_path, &[(wrong_oid, off, 0)], trailer, 32, None)
7731 .expect("write idx");
7732 let idx = read_pack_index(&idx_path).expect("read idx");
7733
7734 let err = read_object_from_pack_bytes(&pack, &idx, wrong_oid.as_bytes()).unwrap_err();
7735 match err {
7736 Error::CorruptObject(msg) => {
7737 assert!(
7738 msg.contains("hashes to"),
7739 "expected hash mismatch message, got: {msg}"
7740 );
7741 assert!(
7742 msg.contains(&oid.to_hex()),
7743 "expected recomputed oid in message, got: {msg}"
7744 );
7745 }
7746 other => panic!("expected CorruptObject, got {other:?}"),
7747 }
7748 }
7749
7750 #[cfg_attr(
7751 not(feature = "full-tests"),
7752 ignore = "starts system git or is heavy; run with --features full-tests"
7753 )]
7754 #[test]
7755 fn reads_delta_chain_deeper_than_50() {
7756 let _guard = pack_cache_test_guard();
7757 let Some((_repo, tip, odb)) = build_deep_delta_repo(120) else {
7758 let _ = "SKIP: git unavailable for deep delta fixture";
7759 return;
7760 };
7761 let pack = build_pack(
7762 &odb,
7763 &[tip],
7764 &[],
7765 &PackBuildOptions {
7766 delta: true,
7767 max_depth: 120,
7768 use_ofs_delta: true,
7769 ..PackBuildOptions::default()
7770 },
7771 )
7772 .expect("build deep delta pack");
7773
7774 let (_scratch, idx) = index_pack_in_scratch(&pack);
7775 assert!(
7776 idx.len() > 1,
7777 "expected a multi-object pack, got {}",
7778 idx.len()
7779 );
7780
7781 for entry in idx.iter() {
7782 let oid = ObjectId::from_bytes(entry.oid()).expect("oid in idx");
7783 let from_pack = read_object_from_pack(&idx, &oid).expect("read packed object");
7784 let expected = odb.read(&oid).expect("loose/alt copy for oid");
7785 assert_eq!(from_pack.kind, expected.kind);
7786 assert_eq!(from_pack.data, expected.data);
7787 assert_eq!(
7788 odb.hash(from_pack.kind, &from_pack.data).expect("hash"),
7789 oid
7790 );
7791 }
7792 }
7793
7794 #[test]
7795 fn cyclic_ref_delta_chain_errors() {
7796 let _guard = pack_cache_test_guard();
7797 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7798 let content_a = b"aaa".as_slice();
7799 let content_b = b"bbb".as_slice();
7800 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
7801 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
7802 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
7803 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
7804
7805 let mut pack = Vec::new();
7806 pack.extend_from_slice(b"PACK");
7807 pack.extend_from_slice(&2u32.to_be_bytes());
7808 pack.extend_from_slice(&2u32.to_be_bytes());
7809 let off_a = pack.len() as u64;
7810 append_ref_delta(&mut pack, &oid_b, &delta_a);
7811 let off_b = pack.len() as u64;
7812 append_ref_delta(&mut pack, &oid_a, &delta_b);
7813 append_sha1_pack_trailer(&mut pack);
7814
7815 let dir = tempfile::tempdir().expect("tempdir");
7816 let pack_path = dir.path().join("cycle.pack");
7817 std::fs::write(&pack_path, &pack).expect("write cyclic pack");
7818 let idx = pack_index_for_objects(pack_path, &[(oid_a, off_a), (oid_b, off_b)]);
7819 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
7820 for err in [
7821 read_object_from_pack(&idx, &oid_a).expect_err("read_object_from_pack"),
7822 read_object_from_pack_bytes(&pack_bytes, &idx, oid_a.as_bytes())
7823 .expect_err("read_object_from_pack_bytes"),
7824 ] {
7825 assert!(
7826 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
7827 "expected typed delta chain limit, got {err:?}"
7828 );
7829 }
7830 }
7831
7832 #[test]
7833 fn resolve_cross_pack_delta_without_index_clone() {
7834 let _guard = pack_cache_test_guard();
7835 clear_pack_cache();
7836 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7837 let base = b"cross-pack-base-payload".as_slice();
7838 let tip = b"cross-pack-base-payload!".as_slice();
7839 let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
7840 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
7841 let delta = encode_lcp_delta(base, tip).expect("delta");
7842
7843 let mut pack_base = Vec::new();
7844 pack_base.extend_from_slice(b"PACK");
7845 pack_base.extend_from_slice(&2u32.to_be_bytes());
7846 pack_base.extend_from_slice(&1u32.to_be_bytes());
7847 let off_base = append_whole_blob(&mut pack_base, base);
7848 append_sha1_pack_trailer(&mut pack_base);
7849
7850 let mut pack_delta = Vec::new();
7851 pack_delta.extend_from_slice(b"PACK");
7852 pack_delta.extend_from_slice(&2u32.to_be_bytes());
7853 pack_delta.extend_from_slice(&1u32.to_be_bytes());
7854 let off_tip = pack_delta.len() as u64;
7855 append_ref_delta(&mut pack_delta, &oid_base, &delta);
7856 append_sha1_pack_trailer(&mut pack_delta);
7857
7858 let dir = tempfile::tempdir().expect("tempdir");
7859 let objects = dir.path().join("objects");
7860 let _idx_base =
7861 install_synthetic_pack(&objects, "cross-base", &pack_base, &[(oid_base, off_base)]);
7862 let idx_delta =
7863 install_synthetic_pack(&objects, "cross-delta", &pack_delta, &[(oid_tip, off_tip)]);
7864
7865 let from_delta_pack = read_object_from_pack(&idx_delta, &oid_tip).expect("delta pack read");
7866 assert_eq!(from_delta_pack.kind, ObjectKind::Blob);
7867 assert_eq!(from_delta_pack.data, tip);
7868 assert_eq!(
7869 odb.hash(from_delta_pack.kind, &from_delta_pack.data)
7870 .expect("hash"),
7871 oid_tip
7872 );
7873
7874 let from_packs = read_object_from_packs(&objects, &oid_tip).expect("packs dir read");
7875 assert_eq!(from_packs.kind, ObjectKind::Blob);
7876 assert_eq!(from_packs.data, tip);
7877 assert_eq!(
7878 odb.hash(from_packs.kind, &from_packs.data).expect("hash"),
7879 oid_tip
7880 );
7881 }
7882
7883 #[cfg_attr(
7884 not(feature = "full-tests"),
7885 ignore = "starts system git or is heavy; run with --features full-tests"
7886 )]
7887 #[test]
7888 fn read_object_from_pack_bytes_uses_in_memory_index_without_on_disk_idx() {
7889 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7890 let content = b"in-memory-index-blob";
7891 let oid = odb.hash(ObjectKind::Blob, content).expect("hash");
7892
7893 let mut pack = Vec::new();
7894 pack.extend_from_slice(b"PACK");
7895 pack.extend_from_slice(&2u32.to_be_bytes());
7896 pack.extend_from_slice(&1u32.to_be_bytes());
7897 let off = append_whole_blob(&mut pack, content);
7898 append_sha1_pack_trailer(&mut pack);
7899
7900 let dir = tempfile::tempdir().expect("tempdir");
7901 let pack_path = dir.path().join("mem.pack");
7902 std::fs::write(&pack_path, &pack).expect("write pack");
7903 let idx = pack_index_for_objects(pack_path, &[(oid, off)]);
7904 std::fs::remove_file(&idx.idx_path).expect("drop on-disk idx");
7905 assert!(
7906 !idx.idx_path.exists(),
7907 "fixture must not rely on an on-disk .idx"
7908 );
7909
7910 let got = read_object_from_pack_bytes(&pack, &idx, oid.as_bytes()).expect("read bytes");
7911 assert_eq!(got.kind, ObjectKind::Blob);
7912 assert_eq!(got.data, content);
7913 }
7914
7915 fn build_ofs_delta_chain_pack(
7916 depth: usize,
7917 ) -> Option<(tempfile::TempDir, PackIndex, ObjectId, Odb)> {
7918 if depth == 0 {
7919 return None;
7920 }
7921 let tmp = tempfile::tempdir().ok()?;
7922 let odb = Odb::new(&tmp.path().join("objects"));
7923 let mut content = b"ofs-delta-chain-base\n".to_vec();
7924 let mut pack = Vec::new();
7925 pack.extend_from_slice(b"PACK");
7926 pack.extend_from_slice(&2u32.to_be_bytes());
7927 pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
7928 let mut base_off = append_whole_blob(&mut pack, &content);
7929 let mut entries = vec![(
7930 odb.hash(ObjectKind::Blob, &content).expect("hash"),
7931 base_off,
7932 )];
7933 for _ in 0..depth {
7934 let mut next = content.clone();
7935 next.push(b'!');
7936 let delta = encode_lcp_delta(&content, &next).ok()?;
7937 let off = append_ofs_delta(&mut pack, base_off, &delta);
7938 let oid = odb.hash(ObjectKind::Blob, &next).expect("hash");
7939 entries.push((oid, off));
7940 content = next;
7941 base_off = off;
7942 }
7943 append_sha1_pack_trailer(&mut pack);
7944 let tip = entries.last()?.0;
7945 let dir = tmp.path();
7946 std::fs::create_dir_all(dir.join("objects/pack")).ok()?;
7947 let pack_path = dir.join("objects/pack/chain.pack");
7948 std::fs::write(&pack_path, &pack).ok()?;
7949 write_v2_idx_for_test(&pack_path.with_extension("idx"), &pack_path, &entries).ok()?;
7950 clear_pack_cache();
7951 let idx = read_pack_index(&pack_path.with_extension("idx")).ok()?;
7952 Some((tmp, idx, tip, odb))
7953 }
7954
7955 #[test]
7956 fn warm_hinted_pack_damage_returns_corrupt_packed_object_via_full_lookup() {
7957 let _guard = pack_cache_test_guard();
7958 clear_pack_cache();
7959 let tmp = tempfile::tempdir().expect("tempdir");
7960 let dir = tmp.path();
7961 if !git_try(dir, &["init", "-q"]) {
7962 return;
7963 }
7964 std::fs::write(dir.join("big.txt"), vec![0u8; 20_000]).expect("big");
7965 git(dir, &["add", "big.txt"]);
7966 git(dir, &["commit", "-qm", "one"]);
7967 git(dir, &["repack", "-adq"]);
7968 std::fs::write(dir.join("more.txt"), b"second pack\n").expect("more");
7969 git(dir, &["add", "more.txt"]);
7970 git(dir, &["commit", "-qm", "two"]);
7971 git(dir, &["repack"]);
7972 let objects = dir.join(".git/objects");
7973 let pack_dir = objects.join("pack");
7974 let pack_count = std::fs::read_dir(&pack_dir)
7975 .expect("pack dir")
7976 .filter_map(|e| e.ok())
7977 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
7978 .count();
7979 assert!(
7980 pack_count >= 2,
7981 "expected multiple packs for warm-hint regression, got {pack_count}"
7982 );
7983 let odb = Odb::new(&objects);
7984 let oid_hex = Command::new("git")
7985 .current_dir(dir)
7986 .args(["rev-parse", "HEAD~1:big.txt"])
7987 .env("GIT_CONFIG_GLOBAL", "/dev/null")
7988 .env("GIT_CONFIG_SYSTEM", "/dev/null")
7989 .output()
7990 .expect("rev-parse")
7991 .stdout;
7992 let oid =
7993 ObjectId::from_hex(std::str::from_utf8(&oid_hex).unwrap().trim()).expect("parse oid");
7994 let indexes = read_local_pack_indexes(&objects).expect("indexes");
7995 let (idx_arc, offset) = indexes
7996 .iter()
7997 .find_map(|idx| {
7998 idx.find_offset(&oid)
7999 .ok()
8000 .flatten()
8001 .map(|offset| (Arc::new(idx.clone()), offset))
8002 })
8003 .expect("oid in a pack");
8004 let corrupt_pack = |pack_path: &Path| {
8005 let mut bytes = std::fs::read(pack_path).expect("read pack");
8006 let flip_at = (offset as usize + 8).min(bytes.len().saturating_sub(1));
8007 bytes[flip_at] ^= 0x55;
8008 #[cfg(unix)]
8009 {
8010 use std::os::unix::fs::PermissionsExt;
8011 std::fs::set_permissions(pack_path, std::fs::Permissions::from_mode(0o644))
8012 .expect("chmod pack");
8013 }
8014 std::fs::write(pack_path, &bytes).expect("corrupt pack");
8015 };
8016 let assert_warm_hint = |label: &str| {
8017 let hint = pack_cache::hinted_pack_index(&objects)
8018 .unwrap_or_else(|| panic!("{label}: pack hint must be warm"));
8019 assert_eq!(
8020 hint.pack_path, idx_arc.pack_path,
8021 "{label}: hint must target the corrupted pack"
8022 );
8023 };
8024 let reseed_hint = || pack_cache::remember_pack_hit(&objects, &idx_arc);
8025
8026 odb.with_pack_read_context(|| {
8027 odb.read(&oid).expect("warm pack hint");
8028 assert_warm_hint("after warm read");
8029
8030 corrupt_pack(&idx_arc.pack_path);
8031 assert!(
8032 pack_cache::revalidate_stale_pack_bytes(&idx_arc.pack_path).expect("revalidate"),
8033 "corrupt pack bytes must be observable in cache"
8034 );
8035 reseed_hint();
8036 assert_warm_hint("before try_read_from_hinted_pack");
8037
8038 let hinted = pack_cache::try_read_from_hinted_pack(&objects, &oid)
8039 .expect("hinted pack read must not propagate raw zlib");
8040 assert!(
8041 hinted.is_none(),
8042 "damaged hinted read must fall through to full lookup"
8043 );
8044 assert_warm_hint("before odb.read after hinted shortcut");
8045
8046 let err = odb.read(&oid).expect_err("read after corrupt hinted miss");
8047 assert!(
8048 matches!(err, Error::CorruptPackedObject { .. }),
8049 "read: {err:?}"
8050 );
8051
8052 reseed_hint();
8053 assert_warm_hint("before try_read_from_hinted_pack_into");
8054
8055 let mut buf = Vec::new();
8056 let hinted_into =
8057 pack_cache::try_read_from_hinted_pack_into(&objects, &oid, true, &mut buf)
8058 .expect("hinted pack read_into must not propagate raw zlib");
8059 assert!(
8060 hinted_into.is_none(),
8061 "damaged hinted read_into must fall through to full lookup"
8062 );
8063 assert_warm_hint("before odb.read_into after hinted shortcut");
8064
8065 let err = odb
8066 .read_into(&oid, &mut Vec::new())
8067 .expect_err("read_into after corrupt hinted miss");
8068 assert!(
8069 matches!(err, Error::CorruptPackedObject { .. }),
8070 "read_into: {err:?}"
8071 );
8072 });
8073 }
8074
8075 #[test]
8076 fn bounded_pack_cache_resolves_deltas_without_changing_shared_cache() {
8077 let _guard = pack_cache_test_guard();
8078 let (_tmp, idx, tip, odb) = build_ofs_delta_chain_pack(800).expect("chain");
8079 let normal = odb.read(&tip).expect("normal");
8080 let shared_bytes = odb.pack_store().expect("store").delta_bases().bytes_used();
8081 let bounded = odb.with_bounded_pack_cache();
8082 assert!(!Arc::ptr_eq(
8083 odb.pack_store().expect("store"),
8084 bounded.pack_store().expect("store")
8085 ));
8086 bounded.with_pack_read_context(|| {
8087 let got = bounded.read(&tip).expect("bounded");
8088 assert_eq!(got.data, normal.data);
8089 let pack = read_pack_bytes_cached(&idx.pack_path).expect("pack");
8090 assert!(pack.uses_positioned_reads());
8091 let cached = bounded
8092 .pack_store()
8093 .expect("store")
8094 .delta_bases()
8095 .bytes_used();
8096 assert!(cached > 0 && cached <= crate::pack_store::BOUNDED_DELTA_BASE_BYTES);
8097 });
8098 assert_eq!(
8099 odb.pack_store().expect("store").delta_bases().bytes_used(),
8100 shared_bytes
8101 );
8102 }
8103
8104 #[test]
8105 fn ofs_delta_chain_depth_4096_resolves() {
8106 let _guard = pack_cache_test_guard();
8107 clear_pack_cache();
8108 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(4096) else {
8109 return;
8110 };
8111 let got = read_object_from_pack(&idx, &tip).expect("4096-deep chain");
8112 let expect = odb.hash(ObjectKind::Blob, &got.data).expect("hash");
8113 assert_eq!(expect, tip);
8114 }
8115
8116 #[test]
8117 fn ofs_delta_chain_depth_4097_errors() {
8118 let _guard = pack_cache_test_guard();
8119 clear_pack_cache();
8120 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(4097) else {
8121 return;
8122 };
8123 let err = read_object_from_pack(&idx, &tip).expect_err("4097-deep chain");
8124 assert!(
8125 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
8126 "got {err:?}"
8127 );
8128 }
8129
8130 #[test]
8131 fn delta_base_cache_limit_zero_disables_caching() {
8132 let _guard = pack_cache_test_guard();
8133 clear_pack_cache();
8134 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
8135 return;
8136 };
8137 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 0);
8138 let _ = read_object_from_pack(&idx, &tip).expect("read chain");
8139 assert_eq!(
8140 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
8141 0,
8142 "limit 0 must not retain delta bases"
8143 );
8144 pack_cache::test_set_delta_base_cache_byte_limit(
8145 &idx.pack_path,
8146 pack_cache::DELTA_BASE_CACHE_DEFAULT,
8147 );
8148 }
8149
8150 #[test]
8151 fn delta_base_cache_eviction_respects_byte_limit() {
8152 let _guard = pack_cache_test_guard();
8153 clear_pack_cache();
8154 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(32) else {
8155 return;
8156 };
8157 let expected = read_object_from_pack(&idx, &tip)
8158 .expect("baseline read")
8159 .data;
8160 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 4096);
8161 let _ = read_object_from_pack(&idx, &tip).expect("prime cache");
8162 assert!(
8163 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) <= 4096,
8164 "cache bytes {} exceeded cap",
8165 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path)
8166 );
8167 pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 512);
8168 for round in 0..8 {
8169 let got = read_object_from_pack(&idx, &tip).expect("read after eviction");
8170 assert_eq!(got.data, expected, "round {round} must stay byte-identical");
8171 }
8172 pack_cache::test_set_delta_base_cache_byte_limit(
8173 &idx.pack_path,
8174 pack_cache::DELTA_BASE_CACHE_DEFAULT,
8175 );
8176 }
8177
8178 #[test]
8179 fn odb_read_into_caching_delta_bases_fills_cache_read_into_does_not() {
8180 let _guard = pack_cache_test_guard();
8181 clear_pack_cache();
8182 let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(12) else {
8183 return;
8184 };
8185 let expected = read_object_from_pack(&idx, &tip)
8186 .expect("baseline read")
8187 .data;
8188 clear_pack_cache();
8189 let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
8190 crate::pack_store::PackStore::with_context(store, || {
8191 let mut buf = Vec::new();
8192 assert_eq!(
8193 odb.read_into(&tip, &mut buf).expect("read_into"),
8194 ObjectKind::Blob
8195 );
8196 assert_eq!(buf, expected);
8197 assert_eq!(
8198 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
8199 0,
8200 "read_into must not retain delta bases"
8201 );
8202
8203 let mut cached = Vec::new();
8204 let kind = odb
8205 .read_into_caching_delta_bases(&tip, &mut cached)
8206 .expect("caching read");
8207 assert_eq!(kind, ObjectKind::Blob);
8208 assert_eq!(cached, expected);
8209 assert!(
8210 pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) > 0,
8211 "caching read must retain the chain's delta bases"
8212 );
8213
8214 let again = odb
8215 .read_into_caching_delta_bases(&tip, &mut cached)
8216 .expect("second caching read");
8217 assert_eq!(again, ObjectKind::Blob);
8218 assert_eq!(cached, expected);
8219 });
8220 }
8221
8222 #[test]
8223 fn repository_odb_read_into_caching_delta_bases_fills_cache_through_files_primary() {
8224 use crate::repo::{InitOptions, Repository};
8225 let _guard = pack_cache_test_guard();
8226 clear_pack_cache();
8227 let Some((tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
8228 return;
8229 };
8230 let expected = read_object_from_pack(&idx, &tip)
8231 .expect("baseline read")
8232 .data;
8233 Repository::init(
8234 tmp.path(),
8235 &InitOptions {
8236 bare: true,
8237 ..InitOptions::default()
8238 },
8239 )
8240 .expect("init over packed objects");
8241 let repo = Repository::open(tmp.path(), &crate::environment::RepositoryOptions::empty())
8242 .expect("open");
8243 let odb = repo.odb();
8244 let store = Arc::clone(odb.pack_store().expect("pack store"));
8245 store.invalidate_packs();
8246
8247 let mut buf = Vec::new();
8248 assert_eq!(
8249 odb.read_into(&tip, &mut buf).expect("read_into"),
8250 ObjectKind::Blob
8251 );
8252 assert_eq!(buf, expected);
8253 assert_eq!(store.delta_bases().bytes_used(), 0);
8254
8255 let mut cached = Vec::new();
8256 assert_eq!(
8257 odb.read_into_caching_delta_bases(&tip, &mut cached)
8258 .expect("caching read"),
8259 ObjectKind::Blob
8260 );
8261 assert_eq!(cached, expected);
8262 assert!(store.delta_bases().bytes_used() > 0);
8263 }
8264
8265 #[test]
8266 fn odb_read_into_caching_delta_bases_falls_back_outside_packs() {
8267 let tmp = tempfile::tempdir().expect("tempdir");
8268 let odb = Odb::new(&tmp.path().join("objects"));
8269 let oid = odb
8270 .write(ObjectKind::Blob, b"loose only\n")
8271 .expect("write loose");
8272 let mut buf = vec![0u8; 4];
8273 let kind = odb
8274 .read_into_caching_delta_bases(&oid, &mut buf)
8275 .expect("loose read");
8276 assert_eq!(kind, ObjectKind::Blob);
8277 assert_eq!(buf, b"loose only\n");
8278 }
8279
8280 #[test]
8281 fn cached_delta_reads_support_overlay_alternates_and_custom_stores() {
8282 use crate::odb::store::MemoryStore;
8283 use crate::odb::OdbBuilder;
8284
8285 let tmp = tempfile::tempdir().expect("tempdir");
8286 let local_objects = tmp.path().join("local/objects");
8287 let alternate_objects = tmp.path().join("alternate/objects");
8288 let alternate = Odb::new(&alternate_objects);
8289 let alternate_oid = alternate
8290 .write(ObjectKind::Blob, b"alternate\n")
8291 .expect("alternate object");
8292 fs::create_dir_all(local_objects.join("info")).expect("info");
8293 fs::write(
8294 local_objects.join("info/alternates"),
8295 format!("{}\n", alternate_objects.display()),
8296 )
8297 .expect("alternates");
8298
8299 let odb = Odb::new(&local_objects);
8300 let mut buf = Vec::with_capacity(1024);
8301 assert_eq!(
8302 odb.read_into_caching_delta_bases(&alternate_oid, &mut buf)
8303 .expect("alternate read"),
8304 ObjectKind::Blob
8305 );
8306 assert_eq!(buf, b"alternate\n");
8307
8308 odb.enable_mem_overlay();
8309 let overlay_oid = odb
8310 .write(ObjectKind::Blob, b"overlay\n")
8311 .expect("overlay object");
8312 odb.read_into_caching_delta_bases(&overlay_oid, &mut buf)
8313 .expect("overlay read");
8314 assert_eq!(buf, b"overlay\n");
8315
8316 let custom = OdbBuilder::files(tmp.path().join("custom/objects"))
8317 .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
8318 .build();
8319 let custom_oid = custom
8320 .write(ObjectKind::Blob, b"custom\n")
8321 .expect("custom object");
8322 custom
8323 .read_into_caching_delta_bases(&custom_oid, &mut buf)
8324 .expect("custom read");
8325 assert_eq!(buf, b"custom\n");
8326
8327 let missing = HashAlgo::Sha1
8328 .hash_object(ObjectKind::Blob, b"missing")
8329 .expect("missing id");
8330 assert!(matches!(
8331 custom.read_into_caching_delta_bases(&missing, &mut buf),
8332 Err(Error::ObjectNotFound(_))
8333 ));
8334 }
8335
8336 #[test]
8337 fn delta_base_cache_invalidated_when_pack_bytes_reloaded() {
8338 use filetime::{set_file_mtime, FileTime};
8339 let _guard = pack_cache_test_guard();
8340 clear_pack_cache();
8341 let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(8) else {
8342 return;
8343 };
8344 let pack_path = idx.pack_path.clone();
8345 let pack_id = pack_cache::pack_id_for(&pack_path);
8346 let mid_off = idx
8347 .iter()
8348 .nth(4)
8349 .map(|e| e.offset())
8350 .expect("mid hop")
8351 .expect("mid off");
8352 let _ = read_object_from_pack(&idx, &tip).expect("warm cache");
8353 assert!(
8354 pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
8355 "expected intermediate cached at offset {mid_off}"
8356 );
8357 set_file_mtime(&pack_path, FileTime::now()).expect("touch pack mtime");
8358 assert!(
8359 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
8360 "expected pack bytes reload"
8361 );
8362 assert!(
8363 !pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
8364 "cache must drop when pack bytes reload"
8365 );
8366 }
8367
8368 #[cfg_attr(
8369 not(feature = "full-tests"),
8370 ignore = "starts system git or is heavy; run with --features full-tests"
8371 )]
8372 #[test]
8373 fn git_repack_depth_250_matches_cat_file_batch() {
8374 let _guard = pack_cache_test_guard();
8375 clear_pack_cache();
8376 let tmp = tempfile::tempdir().expect("tempdir");
8377 let dir = tmp.path();
8378 if Command::new("git") .current_dir(dir)
8380 .args(["init", "-q"])
8381 .env("GIT_CONFIG_GLOBAL", "/dev/null")
8382 .env("GIT_CONFIG_SYSTEM", "/dev/null")
8383 .status()
8384 .is_err()
8385 {
8386 let _ = "SKIP: git unavailable";
8387 return;
8388 }
8389 for i in 0..64 {
8390 std::fs::write(dir.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
8391 git(dir, &["add", &format!("f{i}.txt")]);
8392 git(dir, &["commit", "-qm", &format!("c{i}")]);
8393 }
8394 git(dir, &["repack", "-adf", "--depth=250", "--window=250"]);
8395 let objects = dir.join(".git/objects");
8396 let indexes = read_local_pack_indexes_cached(&objects).expect("pack indexes");
8397 let idx = indexes
8398 .into_iter()
8399 .max_by_key(|i| i.len())
8400 .expect("repack pack");
8401 for entry in idx.iter() {
8402 let oid = ObjectId::from_bytes(entry.oid()).expect("oid");
8403 let grit_obj = read_object_from_pack(&idx, &oid).expect("grit read");
8404 let git_type = Command::new("git") .current_dir(dir)
8406 .args(["cat-file", "-t", &oid.to_hex()])
8407 .env("GIT_CONFIG_GLOBAL", "/dev/null")
8408 .env("GIT_CONFIG_SYSTEM", "/dev/null")
8409 .output()
8410 .expect("git cat-file -t");
8411 assert!(git_type.status.success());
8412 let git_kind = ObjectKind::from_bytes(
8413 std::str::from_utf8(git_type.stdout.trim_ascii_end())
8414 .expect("utf8 type")
8415 .as_bytes(),
8416 )
8417 .expect("git type");
8418 assert_eq!(git_kind, grit_obj.kind);
8419 let type_arg = match grit_obj.kind {
8420 ObjectKind::Blob => "blob",
8421 ObjectKind::Tree => "tree",
8422 ObjectKind::Commit => "commit",
8423 ObjectKind::Tag => "tag",
8424 };
8425 let git_payload = Command::new("git") .current_dir(dir)
8427 .args(["cat-file", type_arg, &oid.to_hex()])
8428 .env("GIT_CONFIG_GLOBAL", "/dev/null")
8429 .env("GIT_CONFIG_SYSTEM", "/dev/null")
8430 .output()
8431 .expect("git cat-file raw");
8432 assert!(git_payload.status.success());
8433 assert_eq!(git_payload.stdout, grit_obj.data);
8434 }
8435 }
8436
8437 #[test]
8438 fn cyclic_ref_delta_chain_across_two_packs_errors() {
8439 let _guard = pack_cache_test_guard();
8440 clear_pack_cache();
8441 let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
8442 let content_a = b"aaa".as_slice();
8443 let content_b = b"bbb".as_slice();
8444 let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
8445 let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
8446 let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
8447 let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
8448
8449 let mut pack_a = Vec::new();
8450 pack_a.extend_from_slice(b"PACK");
8451 pack_a.extend_from_slice(&2u32.to_be_bytes());
8452 pack_a.extend_from_slice(&1u32.to_be_bytes());
8453 let off_a = pack_a.len() as u64;
8454 append_ref_delta(&mut pack_a, &oid_b, &delta_a);
8455 append_sha1_pack_trailer(&mut pack_a);
8456
8457 let mut pack_b = Vec::new();
8458 pack_b.extend_from_slice(b"PACK");
8459 pack_b.extend_from_slice(&2u32.to_be_bytes());
8460 pack_b.extend_from_slice(&1u32.to_be_bytes());
8461 let off_b = pack_b.len() as u64;
8462 append_ref_delta(&mut pack_b, &oid_a, &delta_b);
8463 append_sha1_pack_trailer(&mut pack_b);
8464
8465 let dir = tempfile::tempdir().expect("tempdir");
8466 let objects = dir.path().join("objects");
8467 let idx_b = install_synthetic_pack(&objects, "cycle-b", &pack_b, &[(oid_b, off_b)]);
8468 let idx_a = install_synthetic_pack(&objects, "cycle-a", &pack_a, &[(oid_a, off_a)]);
8469 assert_eq!(idx_a.len(), 1);
8470 assert_eq!(idx_b.len(), 1);
8471 assert!(idx_b.contains(&oid_b).expect("contains"));
8472 let _ = idx_b;
8473
8474 let err = read_object_from_pack(&idx_a, &oid_a).expect_err("cross-pack cycle");
8475 assert!(
8476 matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
8477 "expected typed delta chain limit, got {err:?}"
8478 );
8479 }
8480
8481 #[test]
8482 fn cross_pack_ref_delta_resolves_through_odb_read() {
8483 let _guard = pack_cache_test_guard();
8484 clear_pack_cache();
8485 let tmp = tempfile::tempdir().expect("tempdir");
8486 let objects = tmp.path().join("objects");
8487 let odb = Odb::new(&objects);
8488 let mut random = 17u64;
8489 let base: Vec<u8> = (0..20000)
8490 .map(|_| {
8491 random ^= random << 13;
8492 random ^= random >> 7;
8493 random ^= random << 17;
8494 random as u8
8495 })
8496 .collect();
8497 let mut target = base.clone();
8498 target.extend_from_slice(b"target suffix");
8499 let oid_base = odb.hash(ObjectKind::Blob, &base).expect("hash");
8500 let delta = encode_lcp_delta(&base, &target).expect("delta");
8501 let oid_target = odb.hash(ObjectKind::Blob, &target).expect("hash");
8502
8503 let mut pack_base = Vec::new();
8504 pack_base.extend_from_slice(b"PACK");
8505 pack_base.extend_from_slice(&2u32.to_be_bytes());
8506 pack_base.extend_from_slice(&1u32.to_be_bytes());
8507 let off_base = append_whole_blob(&mut pack_base, &base);
8508 append_sha1_pack_trailer(&mut pack_base);
8509
8510 let mut pack_tip = Vec::new();
8511 pack_tip.extend_from_slice(b"PACK");
8512 pack_tip.extend_from_slice(&2u32.to_be_bytes());
8513 pack_tip.extend_from_slice(&1u32.to_be_bytes());
8514 let off_tip = pack_tip.len() as u64;
8515 append_ref_delta(&mut pack_tip, &oid_base, &delta);
8516 append_sha1_pack_trailer(&mut pack_tip);
8517
8518 install_synthetic_pack(&objects, "base", &pack_base, &[(oid_base, off_base)]);
8519 install_synthetic_pack(&objects, "tip", &pack_tip, &[(oid_target, off_tip)]);
8520
8521 for reader in [&odb, &odb.with_bounded_pack_cache()] {
8522 let got = reader.read(&oid_target).expect("cross-pack ref-delta");
8523 assert_eq!(got.kind, ObjectKind::Blob);
8524 assert_eq!(got.data, target);
8525 assert_eq!(reader.hash(got.kind, &got.data).expect("hash"), oid_target);
8526 }
8527 }
8528
8529 #[test]
8530 fn corrupt_in_pack_base_rescued_from_loose() {
8531 let _guard = pack_cache_test_guard();
8532 let tmp = tempfile::tempdir().expect("tempdir");
8533 let objects = tmp.path().join("objects");
8534 std::fs::create_dir_all(objects.join("pack")).expect("dirs");
8535 let odb = Odb::new(&objects);
8536 let base = b"good-base-bytes";
8537 let tip = b"good-base-bytes!";
8538 let oid_base = odb.write_local(ObjectKind::Blob, base).expect("loose base");
8539 let delta = encode_lcp_delta(base, tip).expect("delta");
8540 let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
8541
8542 let mut pack = Vec::new();
8543 pack.extend_from_slice(b"PACK");
8544 pack.extend_from_slice(&2u32.to_be_bytes());
8545 pack.extend_from_slice(&2u32.to_be_bytes());
8546 let off_base = append_corrupt_whole_blob(&mut pack);
8547 let off_tip = pack.len() as u64;
8548 append_ref_delta(&mut pack, &oid_base, &delta);
8549 append_sha1_pack_trailer(&mut pack);
8550
8551 let pack_path = objects.join("pack/rescue.pack");
8552 std::fs::write(&pack_path, &pack).expect("write pack");
8553 let idx = pack_index_for_objects(pack_path, &[(oid_base, off_base), (oid_tip, off_tip)]);
8554
8555 let got = read_object_from_pack(&idx, &oid_tip).expect("rescue read");
8556 assert_eq!(got.kind, ObjectKind::Blob);
8557 assert_eq!(got.data, tip);
8558 }
8559
8560 #[test]
8561 fn pack_added_after_first_lookup_found_on_miss() {
8562 let _guard = pack_cache_test_guard();
8563 clear_pack_cache();
8564 let tmp = tempfile::tempdir().expect("tempdir");
8565 let objects = tmp.path().join("objects");
8566 let pack_dir = objects.join("pack");
8567 let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
8568 install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
8569 pack_cache::test_reset_dir_rescan_count(&pack_dir);
8570
8571 read_object_from_packs(&objects, &oid_a).expect("warm cache");
8572 assert_eq!(
8573 pack_cache::test_dir_rescan_count(&pack_dir),
8574 1,
8575 "initial lookup should scan the pack directory once"
8576 );
8577
8578 std::thread::sleep(std::time::Duration::from_secs(1));
8580
8581 let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b");
8582 install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
8583 assert_eq!(
8584 pack_cache::test_dir_rescan_count(&pack_dir),
8585 1,
8586 "adding a pack must not rescan until a miss"
8587 );
8588
8589 read_object_from_packs(&objects, &oid_b).expect("new pack via miss path");
8590 assert_eq!(
8591 pack_cache::test_dir_rescan_count(&pack_dir),
8592 2,
8593 "miss path should re-scan when pack dir mtime changed"
8594 );
8595 }
8596
8597 #[test]
8598 fn pack_added_same_pack_dir_mtime_found_on_miss() {
8599 use crate::odb::Odb;
8600
8601 let _guard = pack_cache_test_guard();
8602 clear_pack_cache();
8603 let tmp = tempfile::tempdir().expect("tempdir");
8604 let objects = tmp.path().join("objects");
8605 let pack_dir = objects.join("pack");
8606 let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
8607 install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
8608
8609 let odb = Odb::new(&objects);
8610 odb.read(&oid_a).expect("warm odb cache");
8611
8612 let saved_mt = filetime::FileTime::from_last_modification_time(
8613 &std::fs::metadata(&pack_dir).expect("pack dir meta"),
8614 );
8615 let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b-same-second");
8616 install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
8617 filetime::set_file_mtime(&pack_dir, saved_mt).expect("restore pack dir mtime");
8618
8619 odb.read(&oid_b)
8620 .expect("new pack visible after idx-set compare on miss");
8621 }
8622
8623 #[test]
8624 fn absent_object_misses_do_not_rescan_pack_dir() {
8625 let _guard = pack_cache_test_guard();
8626 clear_pack_cache();
8627 let tmp = tempfile::tempdir().expect("tempdir");
8628 let objects = tmp.path().join("objects");
8629 let pack_dir = objects.join("pack");
8630 let (pack, oid, off) = single_blob_pack(b"cached-blob");
8631 install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
8632 pack_cache::test_reset_dir_rescan_count(&pack_dir);
8633
8634 read_object_from_packs(&objects, &oid).expect("warm cache");
8635 let after_warm = pack_cache::test_dir_rescan_count(&pack_dir);
8636 let missing = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
8637 for _ in 0..128 {
8638 assert!(matches!(
8639 read_object_from_packs(&objects, &missing),
8640 Err(Error::ObjectNotFound(_))
8641 ));
8642 }
8643 assert_eq!(
8644 pack_cache::test_dir_rescan_count(&pack_dir),
8645 after_warm,
8646 "absent oid misses must not re-scan the pack directory"
8647 );
8648 }
8649
8650 #[test]
8651 fn known_object_lookup_does_not_rescan() {
8652 let _guard = pack_cache_test_guard();
8653 clear_pack_cache();
8654 let tmp = tempfile::tempdir().expect("tempdir");
8655 let objects = tmp.path().join("objects");
8656 let pack_dir = objects.join("pack");
8657 pack_cache::test_reset_dir_rescan_count(&pack_dir);
8658 let (pack, oid, off) = single_blob_pack(b"cached-blob");
8659 install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
8660
8661 read_object_from_packs(&objects, &oid).expect("prime cache");
8662 let after_first = pack_cache::test_dir_rescan_count(&pack_dir);
8663 for _ in 0..64 {
8664 read_object_from_packs(&objects, &oid).expect("repeat read");
8665 }
8666 assert_eq!(
8667 pack_cache::test_dir_rescan_count(&pack_dir),
8668 after_first,
8669 "repeated hits must not re-scan the pack directory"
8670 );
8671 }
8672
8673 #[test]
8674 fn two_repositories_concurrent_reads() {
8675 use crate::odb::Odb;
8676 use std::sync::Arc;
8677
8678 let _guard = pack_cache_test_guard();
8679 clear_pack_cache();
8680 let tmp_a = tempfile::tempdir().expect("tempdir a");
8681 let tmp_b = tempfile::tempdir().expect("tempdir b");
8682 let objects_a = tmp_a.path().join("objects");
8683 let objects_b = tmp_b.path().join("objects");
8684 let pack_a_dir = objects_a.join("pack");
8685 let pack_b_dir = objects_b.join("pack");
8686 let (pack_a, oid_a, off_a) = single_blob_pack(b"parallel-a");
8687 let (pack_b, oid_b, off_b) = single_blob_pack(b"parallel-b");
8688 install_synthetic_pack(&objects_a, "a", &pack_a, &[(oid_a, off_a)]);
8689 install_synthetic_pack(&objects_b, "b", &pack_b, &[(oid_b, off_b)]);
8690 let odb_a = Odb::new(&objects_a);
8691 let odb_b = Odb::new(&objects_b);
8692 assert!(
8693 !Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
8694 "independent Odb opens must not share pack stores"
8695 );
8696 pack_cache::test_reset_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir);
8697 pack_cache::test_reset_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir);
8698
8699 let barrier = Arc::new(std::sync::Barrier::new(2));
8700 let b0 = Arc::clone(&barrier);
8701 let b1 = Arc::clone(&barrier);
8702 let odb_a_t = odb_a.clone();
8703 let odb_b_t = odb_b.clone();
8704 let id_a = oid_a;
8705 let id_b = oid_b;
8706
8707 let h1 = std::thread::spawn(move || {
8708 b0.wait();
8709 odb_a_t.read(&id_a).expect("read a");
8710 });
8711 let h2 = std::thread::spawn(move || {
8712 b1.wait();
8713 odb_b_t.read(&id_b).expect("read b");
8714 });
8715 h1.join().expect("thread a");
8716 h2.join().expect("thread b");
8717
8718 assert_eq!(
8719 pack_cache::test_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir),
8720 1
8721 );
8722 assert_eq!(
8723 pack_cache::test_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir),
8724 1
8725 );
8726 }
8727
8728 #[test]
8729 fn stale_cached_pack_bytes_revalidated_before_corruption_error() {
8730 let _guard = pack_cache_test_guard();
8731 clear_pack_cache();
8732 let tmp = tempfile::tempdir().expect("tempdir");
8733 let objects = tmp.path().join("objects");
8734 let (pack, oid, off) = single_blob_pack(b"revalidate-me");
8735 let idx = install_synthetic_pack(&objects, "good", &pack, &[(oid, off)]);
8736 read_object_from_pack(&idx, &oid).expect("prime pack bytes cache");
8737
8738 let good_on_disk = std::fs::read(&idx.pack_path).expect("read pack");
8739 pack_cache::test_inject_stale_pack_bytes(
8740 &idx.pack_path,
8741 PackData::from_owned(b"BAD".to_vec()),
8742 );
8743 std::fs::write(&idx.pack_path, &good_on_disk).expect("refresh on-disk pack");
8744 let got = read_object_from_pack(&idx, &oid).expect("revalidate from disk");
8745 assert_eq!(got.data, b"revalidate-me");
8746 }
8747
8748 #[test]
8749 fn replaced_pack_same_path_reread_after_invalidation() {
8750 let _guard = pack_cache_test_guard();
8751 clear_pack_cache();
8752 let tmp = tempfile::tempdir().expect("tempdir");
8753 let objects = tmp.path().join("objects");
8754 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
8755 let pack_path = objects.join("pack/repack-me.pack");
8756 let idx_path = objects.join("pack/repack-me.idx");
8757 const OBJECT_OFFSET: u64 = 9000;
8758 let (oid_a, off_a) =
8759 write_sparse_single_blob_pack(tmp.path(), &pack_path, b"before-repack", OBJECT_OFFSET)
8760 .expect("sparse pack a");
8761 assert!(
8762 std::fs::metadata(&pack_path).expect("stat").len() > 8192,
8763 "fixture pack must exceed mmap owned-buffer threshold"
8764 );
8765 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_a, off_a)]).expect("idx a");
8766 let idx = read_pack_index(&idx_path).expect("parse idx a");
8767 read_object_from_pack(&idx, &oid_a).expect("prime cache");
8768
8769 let (oid_b, off_b) = write_sparse_single_blob_pack(
8770 tmp.path(),
8771 &pack_path,
8772 b"after-repack-content",
8773 OBJECT_OFFSET,
8774 )
8775 .expect("sparse pack b");
8776 write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_b, off_b)]).expect("idx b");
8777
8778 assert!(
8779 pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
8780 "replacement must invalidate cached bytes"
8781 );
8782 let idx = read_pack_index(&idx_path).expect("reload idx after pack swap");
8783 let got = read_object_from_pack(&idx, &oid_b).expect("read from replaced pack");
8784 assert_eq!(got.data, b"after-repack-content");
8785 }
8786
8787 #[test]
8788 fn read_object_at_large_pack_offset_via_mmap() {
8789 use std::fs::OpenOptions;
8790 use std::io::{Seek, SeekFrom, Write};
8791
8792 let _guard = pack_cache_test_guard();
8793 clear_pack_cache();
8794 let tmp = tempfile::tempdir().expect("tempdir");
8795 let objects = tmp.path().join("objects");
8796 std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
8797
8798 let data = b"large-offset-blob";
8799 let odb = Odb::new(tmp.path());
8800 let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
8801
8802 let pack_path = objects.join("pack/large-off.pack");
8803 let mut file = OpenOptions::new()
8804 .create(true)
8805 .write(true)
8806 .read(true)
8807 .open(&pack_path)
8808 .expect("create sparse pack");
8809 file.write_all(b"PACK").expect("sig");
8810 file.write_all(&2u32.to_be_bytes()).expect("ver");
8811 file.write_all(&1u32.to_be_bytes()).expect("count");
8812 let object_offset: u64 = 1 << 32;
8813 file.seek(SeekFrom::Start(object_offset))
8814 .expect("seek past 4GiB");
8815 let mut tail = Vec::new();
8816 append_whole_blob(&mut tail, data);
8817 file.write_all(&tail).expect("object at huge offset");
8818 let trailer = sha1_trailer_for_open_file(&mut file).expect("trailer hash");
8819 file.write_all(trailer.as_bytes()).expect("append trailer");
8820 file.flush().expect("flush");
8821 let file_len = file.metadata().expect("meta").len();
8822 drop(file);
8823 assert!(file_len > object_offset);
8824
8825 let idx_path = pack_path.with_extension("idx");
8826 write_v2_pack_index_with_trailer(
8827 &idx_path,
8828 &[(oid, object_offset, 0)],
8829 trailer.as_bytes(),
8830 20,
8831 None,
8832 )
8833 .expect("idx");
8834
8835 let idx = read_pack_index(&idx_path).expect("parse idx");
8836 assert_eq!(idx.offset_at(0).expect("off"), object_offset);
8837 let got = read_object_from_pack(&idx, &oid).expect("read at large offset");
8838 assert_eq!(got.data, data);
8839 }
8840
8841 fn synthetic_index_entries(n: u32) -> Vec<(ObjectId, u64, u32)> {
8842 (0..n)
8843 .map(|i| {
8844 let digest = HashAlgo::Sha1.checksum_digest(&i.to_be_bytes());
8845 let oid = ObjectId::from_bytes(&digest).expect("oid");
8846 let offset = if i % 7 == 0 {
8847 (1u64 << 31) + u64::from(i)
8848 } else {
8849 12 + u64::from(i) * 40
8850 };
8851 (oid, offset, i.wrapping_mul(2_654_435_761))
8852 })
8853 .collect()
8854 }
8855
8856 #[test]
8857 fn index_entries_sort_in_place_on_any_thread_count() {
8858 for n in [0_u32, 1, 300, 20_000] {
8859 let mut reference = synthetic_index_entries(n);
8860 reference.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
8861 let mut fanout_ref = [0u32; 256];
8862 for (oid, _, _) in &reference {
8863 for slot in &mut fanout_ref[usize::from(oid.as_bytes()[0])..] {
8864 *slot += 1;
8865 }
8866 }
8867 for threads in [None, Some(1), Some(2), Some(3), Some(8)] {
8868 let mut entries = synthetic_index_entries(n);
8869 let fanout = sort_index_entries(&mut entries, threads);
8870 assert_eq!(entries, reference, "n={n} threads={threads:?}");
8871 assert_eq!(fanout, fanout_ref, "n={n} threads={threads:?}");
8872 }
8873 }
8874 }
8875
8876 #[test]
8877 fn index_written_in_place_reads_back_every_row_with_large_offsets() {
8878 let tmp = tempfile::tempdir().expect("tempdir");
8879 let idx_path = tmp.path().join("p.idx");
8880 let rows = synthetic_index_entries(20_000);
8881 let trailer = [0x5a_u8; 20];
8882 let mut entries = rows.clone();
8883 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer, 20, Some(4))
8884 .expect("write idx");
8885 let idx = read_pack_index(&idx_path).expect("read idx");
8886 assert_eq!(idx.len(), rows.len());
8887 let mut want = rows;
8888 want.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
8889 for (pos, (oid, offset, crc)) in want.iter().enumerate() {
8890 assert_eq!(idx.oid_at(pos), oid.as_bytes(), "row {pos}");
8891 assert_eq!(idx.offset_at(pos).expect("offset"), *offset, "row {pos}");
8892 assert_eq!(idx.crc32_at(pos).expect("crc"), *crc, "row {pos}");
8893 }
8894
8895 let mut bad = vec![(ObjectId::from_bytes(&[1; 32]).expect("oid"), 12, 0)];
8896 assert!(matches!(
8897 write_v2_pack_index_sorting(&idx_path, &mut bad, &trailer, 20, None),
8898 Err(Error::CorruptObject(_))
8899 ));
8900 assert!(matches!(
8901 write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer[..19], 20, None),
8902 Err(Error::CorruptObject(_))
8903 ));
8904 }
8905
8906 fn sha1_trailer_for_open_file(file: &mut std::fs::File) -> Result<ObjectId> {
8908 use std::io::{Read, Seek, SeekFrom};
8909 let len = file.metadata().map_err(Error::Io)?.len();
8910 file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
8911 let mut hasher = HashAlgo::Sha1.checksum_hasher();
8912 let mut buf = [0u8; 64 * 1024];
8913 let mut left = len;
8914 while left > 0 {
8915 let chunk = left.min(buf.len() as u64) as usize;
8916 let n = file.read(&mut buf[..chunk]).map_err(Error::Io)?;
8917 if n == 0 {
8918 break;
8919 }
8920 hasher.update(&buf[..n]);
8921 left -= n as u64;
8922 }
8923 let digest = hasher.finalize();
8924 ObjectId::from_bytes(&digest).map_err(|e| Error::InvalidObjectId(format!("{e}")))
8925 }
8926
8927 #[test]
8928 fn cleared_cache_sees_removed_packs_disappear() {
8929 let _guard = pack_cache_test_guard();
8930 clear_pack_cache();
8931 let tmp = tempfile::tempdir().expect("tempdir");
8932 let objects = tmp.path().join("objects");
8933 let (pack, oid, off) = single_blob_pack(b"ephemeral");
8934 install_synthetic_pack(&objects, "gone", &pack, &[(oid, off)]);
8935 read_object_from_packs(&objects, &oid).expect("warm cache");
8936
8937 let pack_path = objects.join("pack/gone.pack");
8938 let idx_path = objects.join("pack/gone.idx");
8939 std::fs::remove_file(&pack_path).expect("remove pack");
8940 std::fs::remove_file(&idx_path).expect("remove idx");
8941 clear_pack_cache();
8942
8943 let indexes = read_local_pack_indexes_cached(&objects).expect("rescan");
8944 assert!(
8945 indexes.is_empty(),
8946 "after clear, listing should not retain removed packs"
8947 );
8948 }
8949
8950 #[test]
8951 fn repack_while_sibling_odb_holds_store_reprepares_on_miss() {
8952 use crate::odb::Odb;
8953
8954 let _guard = pack_cache_test_guard();
8955 clear_pack_cache();
8956 let tmp = tempfile::tempdir().expect("tempdir");
8957 let objects = tmp.path().join("objects");
8958 let (pack_old, oid_old, off_old) = single_blob_pack(b"before");
8959 install_synthetic_pack(&objects, "old", &pack_old, &[(oid_old, off_old)]);
8960 let odb_a = Odb::new(&objects);
8961 let odb_b = Odb::new(&objects);
8962 assert!(
8963 !std::sync::Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
8964 "sibling Odbs must have distinct pack stores"
8965 );
8966 odb_b.read(&oid_old).expect("warm b store");
8967
8968 std::fs::remove_file(objects.join("pack/old.pack")).expect("rm pack");
8969 std::fs::remove_file(objects.join("pack/old.idx")).expect("rm idx");
8970 let (pack_new, oid_new, off_new) = single_blob_pack(b"after");
8971 install_synthetic_pack(&objects, "new", &pack_new, &[(oid_new, off_new)]);
8972 filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now())
8973 .expect("bump pack dir mtime for reprepare");
8974 odb_a.invalidate_packs();
8975
8976 let got = odb_b.read(&oid_new).expect("b reprepares after miss");
8978 assert_eq!(got.data.as_slice(), b"after");
8979 }
8980
8981 #[test]
8982 fn fresh_store_does_not_trust_a_listing_left_by_another_store() {
8983 let dir = tempfile::tempdir().expect("tempdir");
8984 let dir = dir.path();
8985 git(dir, &["init", "-q"]);
8986 git(dir, &["commit", "-q", "--allow-empty", "-m", "a"]);
8987 git(dir, &["repack", "-adq"]);
8988 let objects = dir.join(".git/objects");
8989 let pack_names = |indexes: Vec<Arc<PackIndex>>| -> Vec<PathBuf> {
8990 indexes
8991 .into_iter()
8992 .map(|idx| idx.pack_path.clone())
8993 .collect()
8994 };
8995
8996 let first = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
8997 let before = crate::pack_store::PackStore::with_context(first, || {
8998 read_local_pack_indexes_cached(&objects)
8999 })
9000 .expect("first listing");
9001
9002 git(dir, &["commit", "-q", "--allow-empty", "-m", "b"]);
9004 git(dir, &["repack", "-adq"]);
9005
9006 let second = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
9007 let after = crate::pack_store::PackStore::with_context(second, || {
9008 read_local_pack_indexes_cached(&objects)
9009 })
9010 .expect("second listing");
9011 let (before, after) = (pack_names(before), pack_names(after));
9012 assert_ne!(before, after);
9013 assert!(after.iter().all(|p| p.is_file()), "{after:?}");
9014 }
9015
9016 fn stored_commit_entry(body: &[u8]) -> Arc<PackData> {
9019 let mut entry = Vec::new();
9020 crate::pack_objects::encode_pack_object_header(&mut entry, 1, body.len());
9021 let mut zlib = ZlibEncoder::new(Vec::new(), Compression::none());
9022 zlib.write_all(body).unwrap();
9023 entry.extend(zlib.finish().unwrap());
9024 entry.extend_from_slice(&[0u8; 64]);
9025 PackData::from_owned(entry)
9026 }
9027
9028 fn commit_with_long_headers(header_padding: usize) -> (Vec<u8>, usize) {
9029 let mut body = format!(
9030 "tree {}\nauthor a <a@example.com> 1 +0000\ncommitter c <c@example.com> 2 +0000\n",
9031 "4b825dc642cb6eb9a060e54bf8d69288e0ee4904"
9032 )
9033 .into_bytes();
9034 body.extend_from_slice(b"x-pad ");
9035 body.extend(std::iter::repeat_n(b'p', header_padding));
9036 body.extend_from_slice(b"\n\n");
9037 let header_len = body.len();
9038 body.extend(std::iter::repeat_n(b'm', 2000));
9039 (body, header_len)
9040 }
9041
9042 #[test]
9043 fn commit_header_block_read_takes_a_second_read_when_the_first_is_short() {
9044 let (_, unpadded) = commit_with_long_headers(0);
9046 let (body, header_len) = commit_with_long_headers(COMMIT_HEADER_BLOCK_MAX - 4 - unpadded);
9047 assert!(header_len < COMMIT_HEADER_BLOCK_MAX);
9048 let pack = stored_commit_entry(&body);
9049 let mut zlib = ZlibInflateScratch::default();
9050 let mut out = Vec::new();
9051
9052 let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
9053 assert!(read_commit_header_block_from(
9054 &pack,
9055 0,
9056 &mut window,
9057 &mut zlib,
9058 &mut out
9059 ));
9060 assert_eq!(out, &body[..header_len]);
9061
9062 let mut first_read_only = vec![0u8; COMMIT_HEADER_BLOCK_FIRST_READ];
9063 assert!(
9064 !read_commit_header_block_from(&pack, 0, &mut first_read_only, &mut zlib, &mut out),
9065 "the stored stream does not fit the first read alone"
9066 );
9067 }
9068
9069 #[test]
9070 fn commit_header_block_read_rejects_headers_past_the_inflate_budget() {
9071 let (body, header_len) = commit_with_long_headers(COMMIT_HEADER_BLOCK_MAX);
9072 assert!(header_len > COMMIT_HEADER_BLOCK_MAX);
9073 let pack = stored_commit_entry(&body);
9074 let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
9075 let mut out = Vec::new();
9076 assert!(!read_commit_header_block_from(
9077 &pack,
9078 0,
9079 &mut window,
9080 &mut ZlibInflateScratch::default(),
9081 &mut out
9082 ));
9083 }
9084
9085 #[test]
9086 fn commit_header_block_read_rejects_a_stream_longer_than_the_declared_size() {
9087 let (body, header_len) = commit_with_long_headers(10);
9088 let mut entry = Vec::new();
9089 crate::pack_objects::encode_pack_object_header(&mut entry, 1, header_len + 5);
9090 let mut zlib = ZlibEncoder::new(Vec::new(), Compression::default());
9091 zlib.write_all(&body).unwrap();
9092 entry.extend(zlib.finish().unwrap());
9093 let pack = PackData::from_owned(entry);
9094 let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
9095 let mut out = Vec::new();
9096 assert!(!read_commit_header_block_from(
9097 &pack,
9098 0,
9099 &mut window,
9100 &mut ZlibInflateScratch::default(),
9101 &mut out
9102 ));
9103 }
9104}
9105#[cfg(test)]
9106mod cached_lookup_tests {
9107 use super::*;
9108 use std::fs;
9109 use std::process::Command;
9110 use tempfile::TempDir;
9111
9112 fn git(dir: &Path, args: &[&str]) {
9113 let out = Command::new("git") .current_dir(dir)
9115 .args(args)
9116 .env("GIT_CONFIG_GLOBAL", "/dev/null")
9117 .env("GIT_CONFIG_SYSTEM", "/dev/null")
9118 .env("GIT_AUTHOR_NAME", "t")
9119 .env("GIT_AUTHOR_EMAIL", "t@example.com")
9120 .env("GIT_COMMITTER_NAME", "t")
9121 .env("GIT_COMMITTER_EMAIL", "t@example.com")
9122 .output()
9123 .expect("spawn git");
9124 assert!(
9125 out.status.success(),
9126 "git {:?}: {}",
9127 args,
9128 String::from_utf8_lossy(&out.stderr)
9129 );
9130 }
9131
9132 fn repack_all(dir: &Path) {
9133 git(dir, &["repack", "-a", "-d"]);
9134 }
9135
9136 fn git_stdout(dir: &Path, args: &[&str], stdin: Option<&str>) -> String {
9137 use std::io::Write;
9138 let mut child = Command::new("git") .current_dir(dir)
9140 .args(args)
9141 .env("GIT_CONFIG_GLOBAL", "/dev/null")
9142 .env("GIT_CONFIG_SYSTEM", "/dev/null")
9143 .stdin(std::process::Stdio::piped())
9144 .stdout(std::process::Stdio::piped())
9145 .spawn()
9146 .expect("spawn git");
9147 if let Some(input) = stdin {
9148 child
9149 .stdin
9150 .as_mut()
9151 .expect("stdin")
9152 .write_all(input.as_bytes())
9153 .expect("write stdin");
9154 }
9155 let out = child.wait_with_output().expect("git output");
9156 assert!(out.status.success(), "git {args:?}");
9157 String::from_utf8(out.stdout)
9158 .expect("utf-8")
9159 .trim()
9160 .to_owned()
9161 }
9162
9163 fn oid_of(dir: &Path, rev: &str) -> ObjectId {
9164 ObjectId::from_hex(&git_stdout(dir, &["rev-parse", rev], None)).expect("oid")
9165 }
9166
9167 #[cfg_attr(
9168 not(feature = "full-tests"),
9169 ignore = "starts system git or is heavy; run with --features full-tests"
9170 )]
9171 #[test]
9172 fn cold_reads_trust_the_pack_listing_after_the_first_scan() {
9173 use crate::hot_path_test_metrics::HotPathMetricsScope;
9174
9175 let _guard = pack_cache_test_guard();
9176 clear_pack_cache();
9177 let dir = TempDir::new().unwrap();
9178 let root = dir.path();
9179 git(root, &["init", "-q"]);
9180 for n in 0..40 {
9181 fs::write(root.join("f"), format!("{n}\n")).unwrap();
9182 git(root, &["add", "f"]);
9183 git(root, &["commit", "-qm", &format!("c{n}")]);
9184 }
9185 git(root, &["gc", "-q"]);
9186 let oids: Vec<ObjectId> = git_stdout(root, &["rev-list", "--objects", "--all"], None)
9187 .lines()
9188 .map(|line| ObjectId::from_hex(&line[..40]).expect("oid"))
9189 .collect();
9190 assert!(oids.len() >= 120);
9191
9192 let repo = crate::repo::Repository::open(
9193 &root.join(".git"),
9194 &crate::environment::RepositoryOptions::empty(),
9195 )
9196 .expect("open");
9197 let metrics = repo.odb().hot_path_metrics_arc();
9198 let _scope = HotPathMetricsScope::install(Arc::clone(&metrics));
9199 metrics.reset_pack_dir_stats();
9200 metrics.set_stamp_counting(true);
9201 for oid in &oids {
9202 repo.odb().read(oid).expect("read");
9203 }
9204 metrics.set_stamp_counting(false);
9205 assert!(
9207 metrics.pack_dir_stat_calls() <= 8,
9208 "{} pack directory checks for {} reads",
9209 metrics.pack_dir_stat_calls(),
9210 oids.len()
9211 );
9212 }
9213
9214 #[cfg_attr(
9215 not(feature = "full-tests"),
9216 ignore = "starts system git or is heavy; run with --features full-tests"
9217 )]
9218 #[test]
9219 fn packed_commit_header_block_parses_like_the_whole_commit() {
9220 let _guard = pack_cache_test_guard();
9221 clear_pack_cache();
9222 let dir = TempDir::new().unwrap();
9223 let root = dir.path();
9224 git(root, &["init", "-q"]);
9225 fs::write(root.join("f"), "one\n").unwrap();
9226 git(root, &["add", "f"]);
9227 git(root, &["commit", "-qm", "first"]);
9228 fs::write(root.join("f"), "two\n").unwrap();
9229 let long_message = format!("second\n\n{}", "a long body line\n".repeat(200));
9230 git(root, &["commit", "-qam", &long_message]);
9231 git(root, &["tag", "-a", "-m", "annotated", "v1"]);
9232 let signature: String = (0..60)
9233 .map(|i| format!(" signature line {i:03} with some padding text\n"))
9234 .collect();
9235 let signed_body = format!(
9236 "tree {}\nparent {}\nauthor t <t@example.com> 1 +0000\n\
9237 committer t <t@example.com> 2 +0000\ngpgsig -----BEGIN-----\n{signature} \
9238 -----END-----\n\nsigned\n",
9239 oid_of(root, "HEAD^{tree}"),
9240 oid_of(root, "HEAD"),
9241 );
9242 let signed = git_stdout(
9243 root,
9244 &["hash-object", "-t", "commit", "-w", "--stdin"],
9245 Some(&signed_body),
9246 );
9247 git(root, &["update-ref", "refs/heads/signed", &signed]);
9248 git(root, &["repack", "-a", "-d", "--window=0"]);
9249 fs::write(root.join("f"), "three\n").unwrap();
9250 git(root, &["commit", "-qam", "second pack"]);
9251 git(root, &["repack", "-d", "--window=0"]);
9252 fs::write(root.join("f"), "four\n").unwrap();
9253 git(root, &["commit", "-qam", "loose"]);
9254
9255 let objects = root.join(".git/objects");
9256 let mut buf = Vec::new();
9257 for rev in ["HEAD~3", "HEAD~2", "HEAD~1"] {
9258 let oid = oid_of(root, rev);
9259 assert!(
9260 read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
9261 "{rev} is a whole packed commit"
9262 );
9263 assert!(buf.ends_with(b"\n\n"), "{rev} ends at the blank line");
9264 let whole = git_stdout(root, &["cat-file", "commit", rev], None);
9265 let whole = crate::objects::CommitRef::parse(whole.as_bytes()).unwrap();
9266 let header = crate::objects::CommitRef::parse(&buf).unwrap();
9267 assert_eq!(header.tree(), whole.tree(), "{rev}");
9268 assert_eq!(header.parents(), whole.parents(), "{rev}");
9269 assert_eq!(header.committer_raw(), whole.committer_raw(), "{rev}");
9270 assert!(header.message_bytes().is_empty(), "{rev}");
9271 }
9272 let signed = ObjectId::from_hex(&signed).unwrap();
9273 for (oid, why) in [
9274 (signed, "headers longer than the inflate budget"),
9275 (oid_of(root, "v1"), "an annotated tag"),
9276 (oid_of(root, "HEAD~1^{tree}"), "a packed tree"),
9277 (oid_of(root, "HEAD"), "a loose commit"),
9278 ] {
9279 assert!(
9280 !read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
9281 "{why}"
9282 );
9283 }
9284 }
9285
9286 #[cfg_attr(
9287 not(feature = "full-tests"),
9288 ignore = "starts system git or is heavy; run with --features full-tests"
9289 )]
9290 #[test]
9291 fn packed_commit_header_blocks_match_single_reads() {
9292 let _guard = pack_cache_test_guard();
9293 clear_pack_cache();
9294 let dir = TempDir::new().unwrap();
9295 let root = dir.path();
9296 git(root, &["init", "-q"]);
9297 let commits = 1500;
9299 let mut stream = String::new();
9300 for i in 1..=commits {
9301 let content = format!("revision {i}\n");
9302 let message = format!("commit {i}\n");
9303 stream.push_str(&format!(
9304 "commit refs/heads/main\nmark :{i}\n\
9305 committer T <t@example.com> {} +0000\ndata {}\n{message}\
9306 M 100644 inline f\ndata {}\n{content}\n",
9307 1_000_000 + i,
9308 message.len(),
9309 content.len()
9310 ));
9311 }
9312 git_stdout(root, &["fast-import", "--quiet"], Some(&stream));
9313 git(root, &["tag", "-a", "-m", "annotated", "v1", "main~10"]);
9314 git(root, &["repack", "-a", "-d", "--window=0"]);
9315 git(root, &["checkout", "-q", "main"]);
9316 fs::write(root.join("f"), "loose\n").unwrap();
9317 git(root, &["commit", "-qam", "loose"]);
9318
9319 let mut oids: Vec<ObjectId> = (1..commits)
9320 .step_by(7)
9321 .map(|n| oid_of(root, &format!("HEAD~{n}")))
9322 .collect();
9323 oids.extend([
9324 oid_of(root, "v1"),
9325 oid_of(root, "HEAD~1^{tree}"),
9326 oid_of(root, "HEAD"),
9327 ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
9328 ]);
9329 let objects = root.join(".git/objects");
9330 let mut batch: Vec<Option<Vec<u8>>> = vec![None; oids.len()];
9331 read_packed_commit_header_blocks(&objects, &oids, |i, header| {
9332 assert!(
9333 batch[i].replace(header.to_vec()).is_none(),
9334 "one call per commit"
9335 );
9336 })
9337 .unwrap();
9338 let mut single = Vec::new();
9339 for (oid, from_batch) in oids.iter().zip(&batch) {
9340 let read = read_packed_commit_header_block_into(&objects, oid, &mut single).unwrap();
9341 assert_eq!(
9342 from_batch.as_deref(),
9343 read.then_some(single.as_slice()),
9344 "{oid}"
9345 );
9346 }
9347 assert_eq!(
9348 batch.iter().filter(|header| header.is_some()).count(),
9349 oids.len() - 4,
9350 "every packed commit, and nothing else, is read"
9351 );
9352 }
9353
9354 fn init_repo_with_pack(dir: &Path) -> ObjectId {
9355 git(dir, &["init", "-q"]);
9356 std::fs::write(dir.join("blob.txt"), b"pack cached lookup fixture\n").unwrap();
9357 git(dir, &["add", "blob.txt"]);
9358 git(dir, &["commit", "-qm", "c"]);
9359 repack_all(dir);
9360 let hex = Command::new("git") .current_dir(dir)
9362 .args(["rev-parse", "HEAD^{tree}"])
9363 .env("GIT_CONFIG_GLOBAL", "/dev/null")
9364 .env("GIT_CONFIG_SYSTEM", "/dev/null")
9365 .output()
9366 .unwrap();
9367 let tree_hex = String::from_utf8(hex.stdout).unwrap();
9368 ObjectId::from_hex(tree_hex.trim()).unwrap()
9369 }
9370
9371 fn pack_dir(objects: &Path) -> PathBuf {
9372 objects.join("pack")
9373 }
9374
9375 fn v2_idx_path(objects: &Path) -> PathBuf {
9376 pack_dir(objects)
9377 .read_dir()
9378 .unwrap()
9379 .filter_map(|e| e.ok())
9380 .map(|e| e.path())
9381 .find(|p| p.extension().is_some_and(|x| x == "idx"))
9382 .expect("pack idx")
9383 }
9384
9385 fn convert_pack_idx_to_v1(objects: &Path) -> PathBuf {
9387 let idx_path = v2_idx_path(objects);
9388 let parsed = read_pack_index(&idx_path).expect("v2 idx");
9389 let mut body = Vec::new();
9390 for slot in parsed.fanout {
9391 body.extend_from_slice(&slot.to_be_bytes());
9392 }
9393 for entry in parsed.iter() {
9394 body.extend_from_slice(&(entry.offset().expect("off") as u32).to_be_bytes());
9395 body.extend_from_slice(entry.oid());
9396 }
9397 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
9398 rewrite_test_file(&idx_path, &body);
9399 clear_pack_cache();
9400 idx_path
9401 }
9402
9403 fn rewrite_test_file(path: &Path, body: &[u8]) {
9404 let _ = fs::remove_file(path);
9405 fs::write(path, body).unwrap();
9406 }
9407
9408 fn synthetic_fanout_index(entries: &[(Vec<u8>, u64)]) -> PackIndex {
9409 let mut sorted = entries.to_vec();
9410 sorted.sort_by(|a, b| a.0.cmp(&b.0));
9411 let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
9412 let fanout = compute_fanout_from_oid_slices(&slices);
9413 let mut body = Vec::new();
9414 for f in fanout {
9415 body.extend_from_slice(&f.to_be_bytes());
9416 }
9417 for (oid, off) in sorted {
9418 body.extend_from_slice(&(off as u32).to_be_bytes());
9419 body.extend_from_slice(&oid);
9420 }
9421 body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
9422 parse_pack_index_bytes(Path::new("synthetic.idx"), body, false).expect("v1 idx")
9423 }
9424
9425 #[test]
9426 fn find_offset_fanout_bucket_first_last_and_miss() {
9427 let first = vec![0x05u8; 20];
9428 let mut mid = vec![0x05u8; 20];
9429 mid[19] = 1;
9430 let mut last = vec![0x05u8; 20];
9431 last[19] = 2;
9432 let miss = vec![0x06u8; 20];
9433 let idx =
9434 synthetic_fanout_index(&[(first.clone(), 12), (mid.clone(), 24), (last.clone(), 36)]);
9435
9436 let oid_first = ObjectId::from_bytes(&first).unwrap();
9437 let oid_mid = ObjectId::from_bytes(&mid).unwrap();
9438 let oid_last = ObjectId::from_bytes(&last).unwrap();
9439 let oid_miss = ObjectId::from_bytes(&miss).unwrap();
9440
9441 assert_eq!(idx.find_offset(&oid_first).expect("f"), Some(12));
9442 assert_eq!(idx.find_offset(&oid_mid).expect("m"), Some(24));
9443 assert_eq!(idx.find_offset(&oid_last).expect("l"), Some(36));
9444 assert_eq!(idx.find_offset(&oid_miss).expect("miss"), None);
9445 }
9446
9447 #[cfg_attr(
9448 not(feature = "full-tests"),
9449 ignore = "starts system git or is heavy; run with --features full-tests"
9450 )]
9451 #[test]
9452 fn cached_lookup_hit_v2_idx() {
9453 let _guard = pack_cache_test_guard();
9454 clear_pack_cache();
9455 let dir = TempDir::new().unwrap();
9456 let tree = init_repo_with_pack(dir.path());
9457 let objects = dir.path().join(".git").join("objects");
9458 let got = read_object_from_packs(&objects, &tree).expect("read tree from v2 pack");
9459 assert_eq!(got.kind, ObjectKind::Tree);
9460 let again = read_object_from_packs(&objects, &tree).expect("cached read");
9462 assert_eq!(again.data, got.data);
9463 }
9464
9465 #[cfg_attr(
9466 not(feature = "full-tests"),
9467 ignore = "starts system git or is heavy; run with --features full-tests"
9468 )]
9469 #[test]
9470 fn cached_lookup_hit_v1_idx() {
9471 let _guard = pack_cache_test_guard();
9472 clear_pack_cache();
9473 let dir = TempDir::new().unwrap();
9474 let tree = init_repo_with_pack(dir.path());
9475 let objects = dir.path().join(".git").join("objects");
9476 let v1 = convert_pack_idx_to_v1(&objects);
9477 assert!(pack_index_is_v1(&v1));
9478 let idx = read_pack_index_cached(&v1).expect("parse v1 idx");
9479 let off = idx
9480 .find_offset(&tree)
9481 .expect("fanout lookup in v1 idx")
9482 .expect("off");
9483 assert!(off > 0);
9484 let obj = read_object_from_pack(&idx, &tree).expect("read via v1 index");
9485 assert_eq!(obj.kind, ObjectKind::Tree);
9486 }
9487
9488 #[cfg_attr(
9489 not(feature = "full-tests"),
9490 ignore = "starts system git or is heavy; run with --features full-tests"
9491 )]
9492 #[test]
9493 fn cached_lookup_miss() {
9494 let _guard = pack_cache_test_guard();
9495 clear_pack_cache();
9496 let dir = TempDir::new().unwrap();
9497 init_repo_with_pack(dir.path());
9498 let objects = dir.path().join(".git").join("objects");
9499 let miss = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
9500 assert!(read_object_from_packs(&objects, &miss).is_err());
9501 assert!(
9502 try_read_object_from_local_packs_cached(&objects, &miss, || {
9503 Err(Error::ObjectNotFound(miss.to_hex()))
9504 })
9505 .unwrap()
9506 .is_none()
9507 );
9508 }
9509
9510 #[cfg_attr(
9511 not(feature = "full-tests"),
9512 ignore = "starts system git or is heavy; run with --features full-tests"
9513 )]
9514 #[test]
9515 fn cached_lookup_loose_only() {
9516 let _guard = pack_cache_test_guard();
9517 clear_pack_cache();
9518 let dir = TempDir::new().unwrap();
9519 git(dir.path(), &["init", "-q"]);
9520 let out = Command::new("git") .current_dir(dir.path())
9522 .args(["hash-object", "-w", "--stdin"])
9523 .env("GIT_CONFIG_GLOBAL", "/dev/null")
9524 .env("GIT_CONFIG_SYSTEM", "/dev/null")
9525 .stdin(std::process::Stdio::piped())
9526 .stdout(std::process::Stdio::piped())
9527 .spawn()
9528 .and_then(|mut c| {
9529 use std::io::Write;
9530 c.stdin
9531 .as_mut()
9532 .unwrap()
9533 .write_all(b"loose-only\n")
9534 .unwrap();
9535 c.wait_with_output()
9536 })
9537 .unwrap();
9538 assert!(out.status.success());
9539 let oid = ObjectId::from_hex(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
9540 let objects = dir.path().join(".git").join("objects");
9541 assert!(try_read_object_from_local_packs_cached(&objects, &oid, || {
9542 Err(Error::ObjectNotFound(oid.to_hex()))
9543 })
9544 .unwrap()
9545 .is_none());
9546 }
9547
9548 #[cfg_attr(
9549 not(feature = "full-tests"),
9550 ignore = "starts system git or is heavy; run with --features full-tests"
9551 )]
9552 #[test]
9553 fn cached_lookup_multiple_packs() {
9554 let _guard = pack_cache_test_guard();
9555 clear_pack_cache();
9556 let dir = TempDir::new().unwrap();
9557 git(dir.path(), &["init", "-q"]);
9558 std::fs::write(dir.path().join("a.txt"), b"a").unwrap();
9559 git(dir.path(), &["add", "a.txt"]);
9560 git(dir.path(), &["commit", "-qm", "a"]);
9561 repack_all(dir.path());
9562 let oid_a = ObjectId::from_hex(
9563 String::from_utf8(
9564 Command::new("git") .current_dir(dir.path())
9566 .args(["rev-parse", "HEAD:a.txt"])
9567 .env("GIT_CONFIG_GLOBAL", "/dev/null")
9568 .env("GIT_CONFIG_SYSTEM", "/dev/null")
9569 .output()
9570 .unwrap()
9571 .stdout,
9572 )
9573 .unwrap()
9574 .trim(),
9575 )
9576 .unwrap();
9577 std::fs::write(dir.path().join("b.txt"), b"b").unwrap();
9578 git(dir.path(), &["add", "b.txt"]);
9579 git(dir.path(), &["commit", "-qm", "b"]);
9580 git(dir.path(), &["repack", "-d"]);
9581 let oid_b = ObjectId::from_hex(
9582 String::from_utf8(
9583 Command::new("git") .current_dir(dir.path())
9585 .args(["rev-parse", "HEAD:b.txt"])
9586 .env("GIT_CONFIG_GLOBAL", "/dev/null")
9587 .env("GIT_CONFIG_SYSTEM", "/dev/null")
9588 .output()
9589 .unwrap()
9590 .stdout,
9591 )
9592 .unwrap()
9593 .trim(),
9594 )
9595 .unwrap();
9596 let objects = dir.path().join(".git").join("objects");
9597 let pack_count = pack_dir(&objects)
9598 .read_dir()
9599 .unwrap()
9600 .filter_map(|e| e.ok())
9601 .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
9602 .count();
9603 assert!(
9604 pack_count >= 2,
9605 "expected multiple pack files, got {pack_count}"
9606 );
9607 let obj_a = read_object_from_packs(&objects, &oid_a).expect("pack a");
9608 let obj_b = read_object_from_packs(&objects, &oid_b).expect("pack b");
9609 assert_eq!(obj_a.kind, ObjectKind::Blob);
9610 assert_eq!(obj_b.kind, ObjectKind::Blob);
9611 }
9612
9613 #[cfg_attr(
9614 not(feature = "full-tests"),
9615 ignore = "starts system git or is heavy; run with --features full-tests"
9616 )]
9617 #[test]
9618 fn packed_delta_base_oid_uses_cached_index() {
9619 let _guard = pack_cache_test_guard();
9620 clear_pack_cache();
9621 let dir = TempDir::new().unwrap();
9622 init_repo_with_pack(dir.path());
9623 let objects = dir.path().join(".git").join("objects");
9624 let miss = ObjectId::from_hex("0101010101010101010101010101010101010101").unwrap();
9625 assert!(packed_delta_base_oid(&objects, &miss).unwrap().is_none());
9626 }
9627
9628 #[test]
9629 fn content_addressed_pack_bytes_skip_stat_revalidation() {
9630 let _guard = pack_cache_test_guard();
9631 clear_pack_cache();
9632 let dir = tempfile::tempdir().expect("tempdir");
9633 let pack_path = dir
9634 .path()
9635 .join("pack-1111111111111111111111111111111111111111.pack");
9636 let v1 = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
9637 std::fs::write(&pack_path, v1).expect("write v1");
9638 let cached = read_pack_bytes_cached(&pack_path).expect("prime cache");
9639 assert_eq!(&cached[..], v1);
9640
9641 let v2 = b"PACK-replaced-by-repack";
9642 std::fs::write(&pack_path, v2).expect("overwrite on disk");
9643 let again = read_pack_bytes_cached(&pack_path).expect("cached without stat");
9644 assert_eq!(
9645 &again[..],
9646 v1,
9647 "content-addressed name pins cached bytes in-process"
9648 );
9649 }
9650
9651 #[test]
9652 fn temporary_pack_bytes_served_from_cache_until_cleared() {
9653 let _guard = pack_cache_test_guard();
9654 clear_pack_cache();
9655 let dir = tempfile::tempdir().expect("tempdir");
9656 for pack_path in [
9657 dir.path().join("tmp_pack_abc123.pack"),
9658 dir.path().join("pack-custom.pack"),
9659 ] {
9660 clear_pack_cache();
9661 let v1 = b"PACK-temp-v1";
9662 std::fs::write(&pack_path, v1).expect("write v1");
9663 let _ = read_pack_bytes_cached(&pack_path).expect("prime cache");
9664
9665 let v2 = b"PACK-temp-v2-longer-body";
9666 std::fs::write(&pack_path, v2).expect("overwrite pack");
9667 let stale = read_pack_bytes_cached(&pack_path).expect("cache hit");
9668 assert_eq!(
9669 &stale[..],
9670 v1,
9671 "{} serves cached bytes without stat on hit",
9672 pack_path.display()
9673 );
9674 clear_pack_cache();
9675 let fresh = read_pack_bytes_cached(&pack_path).expect("after clear");
9676 assert_eq!(
9677 &fresh[..],
9678 v2,
9679 "{} reloads from disk after clear",
9680 pack_path.display()
9681 );
9682 }
9683 }
9684 #[cfg_attr(
9685 not(feature = "full-tests"),
9686 ignore = "starts system git or is heavy; run with --features full-tests"
9687 )]
9688 #[test]
9689 fn system_git_repack_requires_clear_pack_cache_for_fresh_listing() {
9690 let _guard = pack_cache_test_guard();
9691 clear_pack_cache();
9692 let dir = TempDir::new().unwrap();
9693 init_repo_with_pack(dir.path());
9694 let objects = dir.path().join(".git").join("objects");
9695 read_local_pack_indexes_cached(&objects).expect("warm listing");
9696 for entry in pack_dir(&objects).read_dir().unwrap().flatten() {
9697 let path = entry.path();
9698 if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
9699 fs::remove_file(path).unwrap();
9700 }
9701 }
9702 clear_pack_cache();
9703 let indexes =
9704 read_local_pack_indexes_cached(&objects).expect("list after repack-like delete");
9705 assert!(
9706 indexes.is_empty(),
9707 "callers must clear_pack_cache after removing pack files"
9708 );
9709 }
9710
9711 fn test_oid(n: u8) -> ObjectId {
9712 ObjectId::from_hex(&format!("{n:040x}")).unwrap()
9713 }
9714
9715 fn max_delta_map_chain_edges(map: &HashMap<ObjectId, ObjectId>) -> usize {
9716 let value_set: HashSet<ObjectId> = map.values().copied().collect();
9717 let tips: Vec<ObjectId> = map
9718 .keys()
9719 .copied()
9720 .filter(|k| !value_set.contains(k))
9721 .collect();
9722 let mut max = 0usize;
9723 for tip in tips {
9724 let mut edges = 0usize;
9725 let mut cur = tip;
9726 let mut seen = HashSet::new();
9727 while seen.insert(cur) {
9728 let Some(&b) = map.get(&cur) else {
9729 break;
9730 };
9731 edges += 1;
9732 cur = b;
9733 }
9734 max = max.max(edges);
9735 }
9736 max
9737 }
9738
9739 #[test]
9740 fn apply_delta_depth_limit_hits_exact_cap_on_seventy_edge_chain() {
9741 let n = 70usize;
9742 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
9743 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
9744 for i in 1..n {
9745 map.insert(oids[i], oids[i - 1]);
9746 }
9747 let original = map.clone();
9748 apply_delta_depth_limit(&mut map, 50);
9749 assert_eq!(max_delta_map_chain_edges(&map), 50);
9750 let preserved = map
9751 .iter()
9752 .filter(|(t, b)| original.get(t) == Some(b))
9753 .count();
9754 assert!(
9755 preserved > 40,
9756 "non-cut edges should keep original bases (preserved {preserved})"
9757 );
9758 }
9759
9760 #[test]
9761 fn delta_cycle_breaks_drops_smallest_oid_of_each_cycle_only() {
9762 let o = |n: u8| test_oid(n);
9763 let map = HashMap::from([
9766 (o(1), o(2)),
9767 (o(2), o(1)),
9768 (o(3), o(5)),
9769 (o(4), o(3)),
9770 (o(5), o(4)),
9771 (o(6), o(4)),
9772 (o(8), o(7)),
9773 ]);
9774 let mut keys: Vec<ObjectId> = map.keys().copied().collect();
9775 for order in [false, true] {
9776 keys.sort();
9777 if order {
9778 keys.reverse();
9779 }
9780 let mut breaks = delta_cycle_breaks(keys.iter().copied(), |oid| map.get(oid).copied());
9781 breaks.sort();
9782 assert_eq!(breaks, vec![o(1), o(3)], "independent of walk order");
9783 }
9784 }
9785
9786 #[test]
9787 fn delta_cycle_breaks_handles_self_delta_and_acyclic_maps() {
9788 let a = test_oid(9);
9789 assert_eq!(delta_cycle_breaks([a], |_| Some(a)), vec![a]);
9790 let chain: HashMap<ObjectId, ObjectId> =
9791 (1..60u8).map(|i| (test_oid(i), test_oid(i - 1))).collect();
9792 assert!(delta_cycle_breaks(chain.keys().copied(), |o| chain.get(o).copied()).is_empty());
9793 }
9794
9795 #[test]
9796 fn apply_delta_depth_limit_breaks_cycles_shorter_than_depth() {
9797 let (a, b, c, d) = (test_oid(1), test_oid(2), test_oid(3), test_oid(4));
9798 let mut map = HashMap::from([(a, b), (b, a), (c, b), (d, c)]);
9800 apply_delta_depth_limit(&mut map, 50);
9801 assert_eq!(map, HashMap::from([(b, a), (c, b), (d, c)]));
9802 assert_eq!(max_delta_map_chain_edges(&map), 3);
9803 }
9804
9805 #[test]
9806 fn cap_delta_chain_depth_matches_per_tip_walk_on_random_forests() {
9807 let oid = |i: u32| {
9808 let mut bytes = [0u8; 20];
9809 bytes[..4].copy_from_slice(&i.to_be_bytes());
9810 ObjectId::from_bytes(&bytes).unwrap()
9811 };
9812 let mut state = 0x2545_f491_4f6c_dd1du64;
9813 let mut next = |bound: u32| {
9814 state ^= state << 13;
9815 state ^= state >> 7;
9816 state ^= state << 17;
9817 (state % u64::from(bound)) as u32
9818 };
9819 for round in 0..200u32 {
9820 let n = 2 + next(400);
9821 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
9822 for i in 1..n {
9825 if next(5) == 0 {
9826 continue;
9827 }
9828 let base = if next(3) == 0 { next(i) } else { i - 1 };
9829 map.insert(oid(i), oid(base));
9830 }
9831 for max_depth in [0, 1, 3, 10, 50] {
9832 let mut fast = map.clone();
9833 let mut slow = map.clone();
9834 cap_delta_chain_depth(&mut fast, max_depth);
9835 cap_delta_chain_depth_per_tip(&mut slow, max_depth);
9836 assert_eq!(fast, slow, "round {round}, max_depth {max_depth}");
9837 assert!(max_delta_map_chain_edges(&fast) <= max_depth);
9838 }
9839 }
9840 }
9841
9842 #[test]
9843 fn delta_chain_ends_reports_cycles() {
9844 let (a, b, c) = (test_oid(1), test_oid(2), test_oid(3));
9845 let cyclic = HashMap::from([(a, b), (b, a), (c, a)]);
9846 assert!(
9847 delta_chain_ends(cyclic.keys().copied(), cyclic.len(), |o| cyclic
9848 .get(o)
9849 .copied())
9850 .is_none()
9851 );
9852 let chain = HashMap::from([(c, b), (b, a)]);
9853 let ends = delta_chain_ends(chain.keys().copied(), chain.len(), |o| {
9854 chain.get(o).copied()
9855 })
9856 .unwrap();
9857 assert_eq!(ends.get(&c), Some((a, 2)));
9858 assert_eq!(ends.get(&b), Some((a, 1)));
9859 assert_eq!(ends.get(&a), None);
9860 }
9861
9862 #[test]
9863 fn apply_delta_depth_limit_leaves_short_chains_untouched() {
9864 let a = test_oid(1);
9865 let b = test_oid(2);
9866 let c = test_oid(3);
9867 let mut map = HashMap::from([(b, a), (c, b)]);
9868 apply_delta_depth_limit(&mut map, 50);
9869 assert_eq!(map.get(&b), Some(&a));
9870 assert_eq!(map.get(&c), Some(&b));
9871 assert_eq!(max_delta_map_chain_edges(&map), 2);
9872 }
9873
9874 #[test]
9875 fn apply_delta_depth_limit_caps_long_chain_and_preserves_blobs() {
9876 let n = 120usize;
9877 let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
9878 let mut blobs: HashMap<ObjectId, Vec<u8>> = HashMap::new();
9879 blobs.insert(oids[0], b"base".to_vec());
9880 for i in 1..n {
9881 let mut next = blobs[&oids[i - 1]].clone();
9882 next.push(i as u8);
9883 blobs.insert(oids[i], next);
9884 }
9885 let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
9886 for i in 1..n {
9887 map.insert(oids[i], oids[i - 1]);
9888 }
9889 let original = map.clone();
9890 apply_delta_depth_limit(&mut map, 50);
9891 assert_eq!(
9892 max_delta_map_chain_edges(&map),
9893 50,
9894 "long chain must cap at exactly max_depth edges"
9895 );
9896 let preserved = map
9897 .iter()
9898 .filter(|(target, base)| original.get(target) == Some(base))
9899 .count();
9900 assert!(
9901 preserved > 60,
9902 "segment links should stay on original bases (preserved {preserved} of {})",
9903 map.len()
9904 );
9905
9906 use crate::delta_encode::encode_prefix_extension_delta;
9907 use crate::unpack_objects::apply_delta;
9908
9909 for (&target, &base) in &map {
9910 let delta = encode_prefix_extension_delta(&blobs[&base], &blobs[&target]).unwrap();
9911 let got = apply_delta(&blobs[&base], &delta).unwrap();
9912 assert_eq!(got, blobs[&target]);
9913 }
9914 }
9915
9916 #[cfg_attr(
9917 not(feature = "full-tests"),
9918 ignore = "starts system git or is heavy; run with --features full-tests"
9919 )]
9920 #[test]
9921 fn v2_idx_crc_matches_crc32fast_over_entry_bytes() {
9922 let _guard = pack_cache_test_guard();
9923 clear_pack_cache();
9924 let dir = TempDir::new().unwrap();
9925 let tree = init_repo_with_pack(dir.path());
9926 let objects = dir.path().join(".git").join("objects");
9927 let idx_path = v2_idx_path(&objects);
9928 let idx = read_pack_index(&idx_path).expect("read idx");
9929 let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
9930 for entry in idx.iter() {
9931 let crc = entry.crc32().expect("v2 idx entry crc");
9932 let off = entry.offset().expect("entry off");
9933 let start = off as usize;
9934 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
9935 .expect("entry end")
9936 .expect("end");
9937 assert_eq!(crc32fast::hash(&pack_bytes[start..end]), crc);
9938 }
9939 let _ = tree;
9940 }
9941
9942 #[cfg_attr(
9943 not(feature = "full-tests"),
9944 ignore = "starts system git or is heavy; run with --features full-tests"
9945 )]
9946 #[test]
9947 fn packed_full_object_slice_returns_on_disk_bytes() {
9948 let _guard = pack_cache_test_guard();
9949 clear_pack_cache();
9950 let dir = TempDir::new().unwrap();
9951 let tree = init_repo_with_pack(dir.path());
9952 let objects = dir.path().join(".git").join("objects");
9953 let idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
9954 let pack_bytes = std::fs::read(&idx.pack_path).expect("pack");
9955 let off = idx.find_offset(&tree).expect("tree in pack").expect("off");
9956 let start = off as usize;
9957 let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
9958 .expect("entry end")
9959 .expect("end");
9960 let expected = &pack_bytes[start..end];
9961 let slice = packed_full_object_slice(&objects, &tree)
9962 .expect("lookup")
9963 .expect("full tree slice");
9964 assert_eq!(slice, expected);
9965 }
9966
9967 fn touch_mtime(path: &Path, when: filetime::FileTime) {
9968 filetime::set_file_mtime(path, when).expect("set mtime");
9969 }
9970
9971 #[cfg_attr(
9972 not(feature = "full-tests"),
9973 ignore = "starts system git or is heavy; run with --features full-tests"
9974 )]
9975 #[test]
9976 fn packed_full_object_slice_crc_mismatch_falls_back_to_redundant_pack() {
9977 let _guard = pack_cache_test_guard();
9978 clear_pack_cache();
9979 let dir = TempDir::new().unwrap();
9980 let tree = init_repo_with_pack(dir.path());
9981 let objects = dir.path().join(".git").join("objects");
9982 let pack_dir = pack_dir(&objects);
9983 let primary_idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
9984 let primary_pack = primary_idx.pack_path.clone();
9985 let primary_idx_path = primary_idx.idx_path.clone();
9986
9987 let redundant_pack = pack_dir.join("pack-redundant.pack");
9988 let redundant_idx = pack_dir.join("pack-redundant.idx");
9989 fs::copy(&primary_pack, &redundant_pack).expect("copy pack");
9990 fs::copy(&primary_idx_path, &redundant_idx).expect("copy idx");
9991
9992 let older = filetime::FileTime::from_unix_time(1_700_000_000, 0);
9993 let newer = filetime::FileTime::from_unix_time(1_700_000_100, 0);
9994 touch_mtime(&primary_pack, older);
9995 touch_mtime(&primary_idx_path, older);
9996 touch_mtime(&redundant_pack, newer);
9997 touch_mtime(&redundant_idx, newer);
9998 clear_pack_cache();
9999
10000 let off = primary_idx
10001 .find_offset(&tree)
10002 .expect("tree offset")
10003 .expect("off");
10004 let start = off as usize;
10005 let end = super::pack_entry_raw_end(&primary_idx, &fs::read(&primary_pack).unwrap(), off)
10006 .expect("entry end")
10007 .expect("end");
10008 let flip = start + (end - start) / 2;
10009 let mut corrupt = fs::read(&redundant_pack).expect("read redundant");
10010 corrupt[flip] ^= 0x01;
10011 rewrite_test_file(&redundant_pack, &corrupt);
10012 clear_pack_cache();
10013
10014 let recorded_crc = read_pack_index(&redundant_idx)
10015 .expect("redundant idx")
10016 .crc32_for_pack_offset(off)
10017 .expect("crc")
10018 .expect("crc value");
10019 let corrupt_slice = &corrupt[start..end];
10020 assert_ne!(
10021 crc32fast::hash(corrupt_slice),
10022 recorded_crc,
10023 "test must flip bytes without updating idx CRC"
10024 );
10025 assert!(
10026 packed_full_object_slice(&objects, &tree)
10027 .expect("lookup")
10028 .is_some(),
10029 "valid redundant pack should serve the slice after CRC rejects corrupt copy"
10030 );
10031
10032 let obj = read_object_from_packs(&objects, &tree).expect("read tree");
10033 assert_eq!(obj.kind, ObjectKind::Tree);
10034 let slice = packed_full_object_slice(&objects, &tree)
10035 .expect("lookup")
10036 .expect("slice");
10037 let good_pack = fs::read(&primary_pack).expect("primary pack");
10038 assert_eq!(&slice, &good_pack[start..end]);
10039 }
10040
10041 #[test]
10042 fn delta_reuse_scan_in_storage_order_matches_per_object_lookups() {
10043 let tmp = TempDir::new().unwrap();
10044 let bare = grit_test_support::git_packed_history(tmp.path(), 40);
10045 let objects = bare.join("objects");
10046 let indexes = read_local_pack_indexes_cached(&objects).unwrap();
10047 let idx = &indexes[0];
10048 let pack_bytes = read_pack_bytes_cached(&idx.pack_path).unwrap();
10049 let all: Vec<(ObjectId, u64)> = idx
10050 .iter()
10051 .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
10052 .collect();
10053 let send: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).collect();
10054 let reusable = |oid: &ObjectId| send.contains(oid);
10055
10056 let mut looked_up = HashMap::new();
10057 for &(oid, offset) in &all {
10058 if let Some((base, size, start, end)) =
10059 parse_in_pack_delta_reuse_zlib(idx, &pack_bytes, offset, &reusable).unwrap()
10060 {
10061 looked_up.insert(oid, (base, size, start, end));
10062 }
10063 }
10064 assert!(looked_up.len() > 20, "the fixture pack stores deltas");
10065
10066 let mut scanned = HashMap::new();
10067 scan_packed_delta_reuse(idx, &pack_bytes, &send, &reusable, &mut scanned).unwrap();
10068 let scanned: HashMap<ObjectId, (ObjectId, u64, usize, usize)> = scanned
10069 .into_iter()
10070 .map(|(oid, (base, size, _, start, end))| (oid, (base, size, start, end)))
10071 .collect();
10072 assert_eq!(scanned, looked_up);
10073 assert!(index_scan_is_cheaper(idx, send.len()));
10074 let found = collect_packed_delta_reuse_for_send(&objects, &send, &reusable).unwrap();
10075 assert_eq!(found.len(), looked_up.len());
10076 }
10077
10078 #[test]
10079 fn wanted_index_entries_lookups_match_a_full_scan() {
10080 let tmp = TempDir::new().unwrap();
10081 let bare = grit_test_support::git_packed_history(tmp.path(), 30);
10082 let objects = bare.join("objects");
10083 let indexes = read_local_pack_indexes_cached(&objects).unwrap();
10084 assert_eq!(indexes.len(), 1);
10085 let idx = &indexes[0];
10086 let all: Vec<(ObjectId, u64)> = idx
10087 .iter()
10088 .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
10089 .collect();
10090 assert!(all.len() > 64);
10091 let absent = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
10092 let sorted = |mut v: Vec<(ObjectId, u64)>| {
10093 v.sort();
10094 v
10095 };
10096
10097 let few: HashSet<ObjectId> = [all[3].0, all[all.len() - 1].0, absent].into();
10098 let looked_up = wanted_index_entries(idx, few.iter(), |oid| few.contains(oid)).unwrap();
10099 assert_eq!(sorted(looked_up), sorted(vec![all[3], all[all.len() - 1]]));
10100
10101 let many: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).chain([absent]).collect();
10102 let scanned = wanted_index_entries(idx, many.iter(), |oid| many.contains(oid)).unwrap();
10103 assert_eq!(sorted(scanned), sorted(all));
10104
10105 let slots = locate_packed_objects(&objects, &few.iter().copied().collect()).unwrap();
10106 assert_eq!(slots.len(), 2);
10107 assert!(!slots.contains_key(&absent));
10108 }
10109}