1use std::collections::HashSet;
4use std::ops::ControlFlow;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use crate::diagnostics::DiagnosticsHandle;
9use crate::error::{Error, Result};
10use crate::hash;
11use crate::midx::midx_oid_listed_in_tip;
12use crate::objects::{HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind};
13use crate::pack::{self, PackLookupOptions};
14use crate::pack_store::PackStore;
15
16use super::{
17 LocalSizedRead, LooseStore, MidxObjects, MidxObjectsStatus, ObjectStore, ObjectStream,
18 PackedObjects, WritableObjectStore,
19};
20use crate::odb::{empty_tree_oid_matches, WriteOptions};
21
22pub struct FilesSource {
24 objects_dir: PathBuf,
25 loose: LooseStore,
26 packs: PackedObjects,
27 midx: MidxObjects,
28 use_midx: bool,
29 midx_tip_listing_for_exists: bool,
30 _diagnostics: DiagnosticsHandle,
31}
32
33impl std::fmt::Debug for FilesSource {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.debug_struct("FilesSource")
36 .field("objects_dir", &self.objects_dir)
37 .field("use_midx", &self.use_midx)
38 .finish()
39 }
40}
41
42impl FilesSource {
43 pub fn open(
52 pack_store: Arc<PackStore>,
53 loose: LooseStore,
54 use_midx: bool,
55 midx_tip_listing_for_exists: bool,
56 diagnostics: DiagnosticsHandle,
57 ) -> Result<Self> {
58 let midx = MidxObjects::new(Arc::clone(&pack_store), use_midx)?;
59 Ok(Self {
60 objects_dir: pack_store.objects_dir().to_path_buf(),
61 loose,
62 packs: PackedObjects::new(pack_store),
63 midx,
64 use_midx,
65 midx_tip_listing_for_exists,
66 _diagnostics: diagnostics,
67 })
68 }
69
70 #[must_use]
72 pub fn objects_dir(&self) -> &Path {
73 &self.objects_dir
74 }
75
76 #[must_use]
78 pub fn loose_store(&self) -> &LooseStore {
79 &self.loose
80 }
81
82 #[must_use]
84 pub fn packed_objects(&self) -> &PackedObjects {
85 &self.packs
86 }
87
88 #[must_use]
90 pub fn midx_objects(&self) -> &MidxObjects {
91 &self.midx
92 }
93
94 #[must_use]
96 pub fn exists_local_materialized(&self, oid: &ObjectId) -> bool {
97 super::local_object_materialized(&self.objects_dir, oid)
98 }
99
100 pub fn try_finish_duplicate_write(
106 &self,
107 oid: &ObjectId,
108 options: WriteOptions,
109 ) -> Result<Option<ObjectId>> {
110 if let Some(existing) = self.loose.try_finish_if_loose_present(oid, options)? {
111 return Ok(Some(existing));
112 }
113 if options.trust_new_loose {
114 return Ok(None);
115 }
116 if options.assume_loose_only_existence && self.exists_local_materialized(oid) {
117 if self.freshen(oid)? {
118 return Ok(Some(*oid));
119 }
120 return Ok(None);
123 }
124 Ok(None)
125 }
126
127 pub fn write_zlib_prehashed(
133 &self,
134 oid: &ObjectId,
135 zlib_store: &[u8],
136 options: WriteOptions,
137 ) -> Result<ObjectId> {
138 if let Some(existing) = self.try_finish_duplicate_write(oid, options)? {
139 return Ok(existing);
140 }
141 self.loose.write_zlib_prehashed(oid, zlib_store, options)
142 }
143
144 pub fn local_loose_exists_but_unreadable(&self, oid: &ObjectId, err: &Error) -> bool {
146 if !self.loose.object_path(oid).is_file() {
147 return false;
148 }
149 matches!(
150 err,
151 Error::CorruptObject(_) | Error::LooseHashMismatch { .. } | Error::Zlib(_)
152 )
153 }
154
155 fn midx_pack_filter_active(&self) -> bool {
156 self.use_midx && self.midx.status() == MidxObjectsStatus::Active
157 }
158
159 fn midx_read_should_fallback_to_packs(err: &Error) -> bool {
160 matches!(err, Error::CorruptObject(_) | Error::Zlib(_))
161 }
162
163 fn try_read_object_into(
164 &self,
165 oid: &ObjectId,
166 populate_delta_lru: bool,
167 buf: &mut Vec<u8>,
168 ) -> Result<Option<ObjectKind>> {
169 if empty_tree_oid_matches(self.hash_algo(), oid) {
170 buf.clear();
171 return Ok(Some(ObjectKind::Tree));
172 }
173
174 let midx_pack_filter = self.midx_pack_filter_active();
175 let pack_opts = PackLookupOptions {
176 skip_midx_covered_packs: midx_pack_filter,
177 only_midx_covered_packs: false,
178 };
179
180 if self.use_midx {
181 match self.midx.read_into(oid, buf) {
182 Ok(Some(kind)) => return Ok(Some(kind)),
183 Ok(None) => {}
184 Err(Error::Midx(_)) => {}
185 Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
186 Err(err) => return Err(err),
187 }
188 }
189
190 match pack::try_read_object_from_packs_with_options_into(
191 &self.objects_dir,
192 oid,
193 pack_opts,
194 populate_delta_lru,
195 buf,
196 ) {
197 Ok(kind) => return Ok(Some(kind)),
198 Err(Error::ObjectNotFound(_)) => {}
199 Err(err) => return Err(err),
200 }
201
202 if midx_pack_filter {
203 match pack::try_read_object_from_packs_with_options_into(
204 &self.objects_dir,
205 oid,
206 PackLookupOptions::MIDX_COVERED_PACKS,
207 populate_delta_lru,
208 buf,
209 ) {
210 Ok(kind) => return Ok(Some(kind)),
211 Err(Error::ObjectNotFound(_)) => {}
212 Err(err) => return Err(err),
213 }
214 }
215
216 #[cfg(test)]
217 crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
218 self.loose.read_into(oid, buf)
219 }
220
221 fn try_read_object(&self, oid: &ObjectId) -> Result<Option<Object>> {
222 if empty_tree_oid_matches(self.hash_algo(), oid) {
223 return Ok(Some(Object {
224 kind: ObjectKind::Tree,
225 data: Vec::new(),
226 }));
227 }
228
229 let midx_pack_filter = self.midx_pack_filter_active();
230 let pack_opts = PackLookupOptions {
231 skip_midx_covered_packs: midx_pack_filter,
232 only_midx_covered_packs: false,
233 };
234
235 if self.use_midx {
236 match self.midx.read(oid) {
237 Ok(Some(obj)) => return Ok(Some(obj)),
238 Ok(None) => {}
239 Err(Error::Midx(_)) => {}
240 Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
241 Err(err) => return Err(err),
242 }
243 }
244
245 match pack::try_read_object_from_packs_with_options(&self.objects_dir, oid, pack_opts) {
246 Ok(obj) => return Ok(Some(obj)),
247 Err(Error::ObjectNotFound(_)) => {}
248 Err(err) => return Err(err),
249 }
250
251 if midx_pack_filter {
252 match pack::try_read_object_from_packs_with_options(
253 &self.objects_dir,
254 oid,
255 PackLookupOptions::MIDX_COVERED_PACKS,
256 ) {
257 Ok(obj) => return Ok(Some(obj)),
258 Err(Error::ObjectNotFound(_)) => {}
259 Err(err) => return Err(err),
260 }
261 }
262
263 #[cfg(test)]
264 crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
265 self.loose.read(oid)
266 }
267
268 fn try_read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
269 if empty_tree_oid_matches(self.hash_algo(), oid) {
270 return Ok(Some(ObjectInfo {
271 kind: ObjectKind::Tree,
272 size: 0,
273 }));
274 }
275
276 let midx_pack_filter = self.midx_pack_filter_active();
277 let pack_opts = PackLookupOptions {
278 skip_midx_covered_packs: midx_pack_filter,
279 only_midx_covered_packs: false,
280 };
281
282 if self.use_midx {
283 match self.midx.read_info(oid) {
284 Ok(Some(info)) => return Ok(Some(info)),
285 Ok(None) => {}
286 Err(Error::Midx(_)) => {}
287 Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
288 Err(err) => return Err(err),
289 }
290 }
291
292 match pack::try_read_object_info_from_packs_with_options(&self.objects_dir, oid, pack_opts)
293 {
294 Ok(info) => return Ok(Some(info)),
295 Err(Error::ObjectNotFound(_)) => {}
296 Err(err) => return Err(err),
297 }
298
299 if midx_pack_filter {
300 match pack::try_read_object_info_from_packs_with_options(
301 &self.objects_dir,
302 oid,
303 PackLookupOptions::MIDX_COVERED_PACKS,
304 ) {
305 Ok(info) => return Ok(Some(info)),
306 Err(Error::ObjectNotFound(_)) => {}
307 Err(err) => return Err(err),
308 }
309 }
310
311 #[cfg(test)]
312 crate::hot_path_test_metrics::record_loose_path_open_for_active_scope();
313 self.loose.read_info(oid)
314 }
315
316 pub(crate) fn read_unless_larger(&self, oid: &ObjectId, limit: u64) -> Result<LocalSizedRead> {
326 let probe = || {
327 match pack::locate_packed_whole_object(&self.objects_dir, oid, false)? {
328 pack::PackedEntry::Whole(whole) => {
329 return Ok(LocalSizedRead::Packed(Some(whole.size)))
330 }
331 pack::PackedEntry::NotWhole => return Ok(LocalSizedRead::Packed(None)),
332 pack::PackedEntry::Absent => {}
333 }
334 Ok(match self.loose.read_unless_larger(oid, limit)? {
335 Some(read) => LocalSizedRead::Loose(read),
336 None => LocalSizedRead::Missing,
337 })
338 };
339 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
340 probe()
341 } else {
342 PackStore::with_context(self.packs.pack_store().clone(), probe)
343 }
344 }
345
346 fn read_with_pack_retry(&self, oid: &ObjectId) -> Result<Option<Object>> {
347 match self.try_read_object(oid) {
348 Ok(Some(obj)) => Ok(Some(obj)),
349 Ok(None) => {
350 if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
351 self.try_read_object(oid)
352 } else {
353 Ok(None)
354 }
355 }
356 Err(err) => Err(err),
357 }
358 }
359
360 fn read_with_pack_retry_into(
361 &self,
362 oid: &ObjectId,
363 populate_delta_lru: bool,
364 buf: &mut Vec<u8>,
365 ) -> Result<Option<ObjectKind>> {
366 match self.try_read_object_into(oid, populate_delta_lru, buf) {
367 Ok(Some(kind)) => Ok(Some(kind)),
368 Ok(None) => {
369 if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
370 self.try_read_object_into(oid, populate_delta_lru, buf)
371 } else {
372 Ok(None)
373 }
374 }
375 Err(err) => Err(err),
376 }
377 }
378
379 fn read_info_with_pack_retry(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
380 match self.try_read_info(oid) {
381 Ok(Some(info)) => Ok(Some(info)),
382 Ok(None) => {
383 if pack::reprepare_pack_directory_on_miss(&self.objects_dir)? {
384 self.try_read_info(oid)
385 } else {
386 Ok(None)
387 }
388 }
389 Err(err) => Err(err),
390 }
391 }
392
393 fn contains_in_packs(&self, oid: &ObjectId) -> Result<bool> {
394 let indexes = pack::read_local_pack_indexes_cached(&self.objects_dir)?;
395 if indexes.is_empty() && !pack_directory_has_index_files(&self.objects_dir) {
396 if self.midx_tip_listing_for_exists && self.use_midx {
397 match midx_oid_listed_in_tip(&self.objects_dir, oid) {
398 Ok(Some(true)) => return Ok(true),
399 Ok(Some(false)) | Ok(None) => {}
400 Err(_) => return Ok(false),
401 }
402 }
403 return Ok(false);
404 }
405 let filter = super::PackFilter {
406 include_promisor: true,
407 include_cruft: true,
408 };
409 if self.packs.contains_filtered(oid, filter)? {
410 return Ok(true);
411 }
412 if pack::reprepare_pack_directory_on_miss(&self.objects_dir)?
413 && self.packs.contains_filtered(oid, filter)?
414 {
415 return Ok(true);
416 }
417 if self.midx_tip_listing_for_exists && self.use_midx {
418 match midx_oid_listed_in_tip(&self.objects_dir, oid) {
419 Ok(Some(true)) => return Ok(true),
420 Ok(Some(false)) | Ok(None) => {}
421 Err(_) => return Ok(false),
422 }
423 }
424 Ok(false)
425 }
426}
427
428impl FilesSource {
429 pub(crate) fn read_into_with_delta_policy(
432 &self,
433 oid: &ObjectId,
434 populate_delta_lru: bool,
435 buf: &mut Vec<u8>,
436 ) -> Result<Option<ObjectKind>> {
437 let mut read = || self.read_with_pack_retry_into(oid, populate_delta_lru, buf);
438 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
439 read()
440 } else {
441 PackStore::with_context(self.packs.pack_store().clone(), read)
442 }
443 }
444
445 pub(crate) fn read_packed_commit_header_block_into(
447 &self,
448 oid: &ObjectId,
449 buf: &mut Vec<u8>,
450 ) -> Result<bool> {
451 let mut read = || pack::read_packed_commit_header_block_into(&self.objects_dir, oid, buf);
452 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
453 read()
454 } else {
455 PackStore::with_context(self.packs.pack_store().clone(), read)
456 }
457 }
458}
459
460impl ObjectStore for FilesSource {
461 fn hash_algo(&self) -> HashAlgo {
462 ObjectStore::hash_algo(&self.loose)
463 }
464
465 fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
466 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
467 return self.read_with_pack_retry(oid);
468 }
469 PackStore::with_context(self.packs.pack_store().clone(), || {
470 self.read_with_pack_retry(oid)
471 })
472 }
473
474 fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
475 self.read_into_with_delta_policy(oid, false, buf)
476 }
477
478 fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
479 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
480 return self.read_info_with_pack_retry(oid);
481 }
482 PackStore::with_context(self.packs.pack_store().clone(), || {
483 self.read_info_with_pack_retry(oid)
484 })
485 }
486
487 fn contains(&self, oid: &ObjectId) -> Result<bool> {
488 if empty_tree_oid_matches(self.hash_algo(), oid) {
489 return Ok(true);
490 }
491 if self.loose.object_path(oid).is_file() {
492 return Ok(true);
493 }
494 let check = || self.contains_in_packs(oid);
495 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
496 check()
497 } else {
498 PackStore::with_context(self.packs.pack_store().clone(), check)
499 }
500 }
501
502 fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
503 let filter = super::PackFilter {
507 include_promisor: true,
508 include_cruft: true,
509 };
510 let probe = || {
511 oids.iter()
512 .map(|oid| {
513 if self.packs.contains_filtered(oid, filter).unwrap_or(false) {
514 return Ok(true);
515 }
516 self.contains(oid)
517 })
518 .collect()
519 };
520 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
521 probe()
522 } else {
523 PackStore::with_context(self.packs.pack_store().clone(), probe)
524 }
525 }
526
527 fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
528 if empty_tree_oid_matches(self.hash_algo(), oid) {
529 return Ok(Some(ObjectStream {
530 kind: ObjectKind::Tree,
531 size: 0,
532 reader: Box::new(std::io::Cursor::new(Vec::<u8>::new())),
533 }));
534 }
535 PackStore::with_context(self.packs.pack_store().clone(), || {
536 if self.use_midx {
537 match self.midx.open_stream(oid) {
538 Ok(Some(stream)) => return Ok(Some(stream)),
539 Ok(None) => {}
540 Err(Error::Midx(_)) => {}
541 Err(err) if Self::midx_read_should_fallback_to_packs(&err) => {}
542 Err(err) => return Err(err),
543 }
544 }
545 match self.packs.open_stream(oid) {
546 Ok(Some(stream)) => return Ok(Some(stream)),
547 Ok(None) | Err(Error::ObjectNotFound(_)) => {}
548 Err(err) => return Err(err),
549 }
550 self.loose.open_stream(oid)
551 })
552 }
553
554 fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
555 let mut seen = HashSet::new();
556 PackStore::with_context(self.packs.pack_store().clone(), || {
557 let mut layer = |store: &dyn ObjectStore| -> Result<bool> {
558 super::for_each_propagate_break(store, &mut |oid| {
559 if !seen.insert(*oid) {
560 return ControlFlow::Continue(());
561 }
562 f(oid)
563 })
564 };
565 if self.use_midx && layer(&self.midx)? {
566 return Ok(());
567 }
568 if layer(&self.packs)? {
569 return Ok(());
570 }
571 let _ = layer(&self.loose)?;
572 Ok(())
573 })
574 }
575
576 fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
577 ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
578 let mut seen: HashSet<ObjectId> = out.iter().copied().collect();
579 if PackStore::read_context_matches_objects_dir(&self.objects_dir) {
580 files_lookup_prefix_layers(self, prefix, limit, out, &mut seen)
581 } else {
582 PackStore::with_context(self.packs.pack_store().clone(), || {
583 files_lookup_prefix_layers(self, prefix, limit, out, &mut seen)
584 })
585 }
586 }
587
588 fn refresh(&self) -> Result<bool> {
589 PackStore::with_context(self.packs.pack_store().clone(), || {
590 let mut changed = self.packs.refresh()?;
591 if self.midx.refresh()? {
592 changed = true;
593 }
594 Ok(changed)
595 })
596 }
597}
598
599impl WritableObjectStore for FilesSource {
600 fn write(&self, kind: ObjectKind, data: &[u8], options: WriteOptions) -> Result<ObjectId> {
601 let oid = hash::hash_object(self.hash_algo(), kind, data)?;
602 if let Some(existing) = self.try_finish_duplicate_write(&oid, options)? {
603 return Ok(existing);
604 }
605 self.loose.write(kind, data, options)
606 }
607
608 fn freshen(&self, oid: &ObjectId) -> Result<bool> {
609 if self.loose.freshen(oid)? {
610 return Ok(true);
611 }
612 PackStore::with_context(self.packs.pack_store().clone(), || {
613 self.packs.freshen_pack_containing(oid)
614 })
615 }
616}
617
618fn pack_directory_has_index_files(objects_dir: &Path) -> bool {
619 let pack_dir = objects_dir.join("pack");
620 let Ok(entries) = std::fs::read_dir(pack_dir) else {
621 return false;
622 };
623 entries
624 .filter_map(|e| e.ok())
625 .any(|ent| ent.path().extension().is_some_and(|ext| ext == "idx"))
626}
627
628fn files_lookup_prefix_layers(
629 src: &FilesSource,
630 prefix: &str,
631 limit: usize,
632 out: &mut Vec<ObjectId>,
633 seen: &mut HashSet<ObjectId>,
634) -> Result<()> {
635 if src.use_midx {
636 merge_prefix_matches(&src.midx, prefix, limit, out, seen)?;
637 if limit != 0 && out.len() >= limit {
638 return Ok(());
639 }
640 }
641 merge_prefix_matches(&src.packs, prefix, limit, out, seen)?;
642 if limit != 0 && out.len() >= limit {
643 return Ok(());
644 }
645 merge_prefix_matches(&src.loose, prefix, limit, out, seen)
646}
647
648fn merge_prefix_matches(
649 store: &dyn ObjectStore,
650 prefix: &str,
651 limit: usize,
652 out: &mut Vec<ObjectId>,
653 seen: &mut HashSet<ObjectId>,
654) -> Result<()> {
655 if limit != 0 && out.len() >= limit {
656 return Ok(());
657 }
658 if limit == 0 {
659 let mut scratch = Vec::new();
660 store.lookup_prefix(prefix, 0, &mut scratch)?;
661 for oid in scratch {
662 if seen.insert(oid) {
663 out.push(oid);
664 }
665 }
666 return Ok(());
667 }
668
669 let goal = limit;
670 let mut fetch = goal.saturating_sub(out.len());
671 loop {
672 let mut scratch = Vec::new();
673 store.lookup_prefix(prefix, fetch, &mut scratch)?;
674 let scratch_len = scratch.len();
675 let before = out.len();
676 for oid in scratch {
677 if seen.insert(oid) {
678 out.push(oid);
679 if out.len() >= goal {
680 return Ok(());
681 }
682 }
683 }
684 if out.len() >= goal {
685 return Ok(());
686 }
687 if scratch_len < fetch {
688 return Ok(());
689 }
690 let need = goal.saturating_sub(out.len());
691 if need == 0 {
692 return Ok(());
693 }
694 fetch = if before == out.len() {
695 fetch.saturating_add(need)
696 } else {
697 scratch_len.saturating_add(need)
698 };
699 }
700}