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