1use std::{
8 collections::HashSet,
9 ffi::CStr,
10 fs::{canonicalize, File},
11 io::{Read, Write},
12 os::fd::{AsFd, OwnedFd},
13 path::{Path, PathBuf},
14 sync::Arc,
15 thread::available_parallelism,
16};
17
18use tokio::sync::Semaphore;
19
20use anyhow::{bail, ensure, Context, Result};
21use once_cell::sync::OnceCell;
22use rustix::{
23 fs::{
24 flock, linkat, mkdirat, open, openat, readlinkat, statat, syncfs, AtFlags, Dir, FileType,
25 FlockOperation, Mode, OFlags, CWD,
26 },
27 io::{Errno, Result as ErrnoResult},
28};
29
30use crate::{
31 fsverity::{
32 compute_verity, enable_verity_maybe_copy, ensure_verity_equal, measure_verity,
33 CompareVerityError, EnableVerityError, FsVerityHashValue, FsVerityHasher,
34 MeasureVerityError,
35 },
36 mount::{composefs_fsmount, mount_at},
37 splitstream::{SplitStreamReader, SplitStreamWriter},
38 util::{proc_self_fd, replace_symlinkat, ErrnoFilter},
39};
40
41fn ensure_dir_and_openat(dirfd: impl AsFd, filename: &str, flags: OFlags) -> ErrnoResult<OwnedFd> {
46 match openat(
47 &dirfd,
48 filename,
49 flags | OFlags::CLOEXEC | OFlags::DIRECTORY,
50 0o666.into(),
51 ) {
52 Ok(file) => Ok(file),
53 Err(Errno::NOENT) => match mkdirat(&dirfd, filename, 0o777.into()) {
54 Ok(()) | Err(Errno::EXIST) => openat(
55 dirfd,
56 filename,
57 flags | OFlags::CLOEXEC | OFlags::DIRECTORY,
58 0o666.into(),
59 ),
60 Err(other) => Err(other),
61 },
62 Err(other) => Err(other),
63 }
64}
65
66pub struct Repository<ObjectID: FsVerityHashValue> {
73 repository: OwnedFd,
74 objects: OnceCell<OwnedFd>,
75 write_semaphore: OnceCell<Arc<Semaphore>>,
76 insecure: bool,
77 _data: std::marker::PhantomData<ObjectID>,
78}
79
80impl<ObjectID: FsVerityHashValue> std::fmt::Debug for Repository<ObjectID> {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("Repository")
83 .field("repository", &self.repository)
84 .field("objects", &self.objects)
85 .field("insecure", &self.insecure)
86 .finish_non_exhaustive()
87 }
88}
89
90impl<ObjectID: FsVerityHashValue> Drop for Repository<ObjectID> {
91 fn drop(&mut self) {
92 flock(&self.repository, FlockOperation::Unlock).expect("repository unlock failed");
93 }
94}
95
96impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
97 pub fn objects_dir(&self) -> ErrnoResult<&OwnedFd> {
99 self.objects
100 .get_or_try_init(|| ensure_dir_and_openat(&self.repository, "objects", OFlags::PATH))
101 }
102
103 pub fn write_semaphore(&self) -> Arc<Semaphore> {
109 self.write_semaphore
110 .get_or_init(|| {
111 let max_concurrent = available_parallelism().map(|n| n.get()).unwrap_or(4);
112 Arc::new(Semaphore::new(max_concurrent))
113 })
114 .clone()
115 }
116
117 pub fn open_path(dirfd: impl AsFd, path: impl AsRef<Path>) -> Result<Self> {
119 let path = path.as_ref();
120
121 let repository = openat(dirfd, path, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())
123 .with_context(|| format!("Cannot open composefs repository at {}", path.display()))?;
124
125 flock(&repository, FlockOperation::LockShared)
126 .context("Cannot lock composefs repository")?;
127
128 Ok(Self {
129 repository,
130 objects: OnceCell::new(),
131 write_semaphore: OnceCell::new(),
132 insecure: false,
133 _data: std::marker::PhantomData,
134 })
135 }
136
137 pub fn open_user() -> Result<Self> {
139 let home = std::env::var("HOME").with_context(|| "$HOME must be set when in user mode")?;
140
141 Self::open_path(CWD, PathBuf::from(home).join(".var/lib/composefs"))
142 }
143
144 pub fn open_system() -> Result<Self> {
146 Self::open_path(CWD, PathBuf::from("/sysroot/composefs".to_string()))
147 }
148
149 fn ensure_dir(&self, dir: impl AsRef<Path>) -> ErrnoResult<()> {
150 mkdirat(&self.repository, dir.as_ref(), 0o755.into()).or_else(|e| match e {
151 Errno::EXIST => Ok(()),
152 _ => Err(e),
153 })
154 }
155
156 pub async fn ensure_object_async(self: &Arc<Self>, data: Vec<u8>) -> Result<ObjectID> {
164 let self_ = Arc::clone(self);
165 tokio::task::spawn_blocking(move || self_.ensure_object(&data)).await?
166 }
167
168 pub fn create_object_tmpfile(&self) -> Result<OwnedFd> {
174 let objects_dir = self.objects_dir()?;
175 let fd = openat(
176 objects_dir,
177 ".",
178 OFlags::RDWR | OFlags::TMPFILE | OFlags::CLOEXEC,
179 Mode::from_raw_mode(0o644),
180 )?;
181 Ok(fd)
182 }
183
184 pub fn spawn_finalize_object_tmpfile(
195 self: &Arc<Self>,
196 tmpfile_fd: OwnedFd,
197 size: u64,
198 ) -> tokio::task::JoinHandle<Result<ObjectID>> {
199 let self_ = Arc::clone(self);
200 tokio::task::spawn_blocking(move || self_.finalize_object_tmpfile(tmpfile_fd.into(), size))
201 }
202
203 pub fn finalize_object_tmpfile(&self, file: File, size: u64) -> Result<ObjectID> {
218 let fd_path = proc_self_fd(&file);
220 let ro_fd = open(&*fd_path, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?;
221
222 drop(file);
224
225 let objects_dir = self.objects_dir()?;
227
228 let (ro_fd, verity_enabled) =
232 match enable_verity_maybe_copy::<ObjectID>(objects_dir, ro_fd.as_fd()) {
233 Ok(None) => (ro_fd, true),
234 Ok(Some(new_fd)) => (new_fd, true),
235 Err(EnableVerityError::FilesystemNotSupported) if self.insecure => (ro_fd, false),
236 Err(EnableVerityError::AlreadyEnabled) => (ro_fd, true),
237 Err(other) => return Err(other).context("Enabling verity on tmpfile")?,
238 };
239
240 let id: ObjectID = if verity_enabled {
242 measure_verity(&ro_fd).context("Measuring verity digest")?
243 } else {
244 let mut reader = std::io::BufReader::new(File::from(ro_fd.try_clone()?));
246 Self::compute_verity_digest(&mut reader)?
247 };
248
249 let path = id.to_object_pathname();
251
252 match statat(objects_dir, &path, AtFlags::empty()) {
253 Ok(stat) if stat.st_size as u64 == size => {
254 return Ok(id);
256 }
257 _ => {}
258 }
259
260 let parent_dir = id.to_object_dir();
262 let _ = mkdirat(objects_dir, &parent_dir, Mode::from_raw_mode(0o755));
263
264 match linkat(
266 CWD,
267 proc_self_fd(&ro_fd),
268 objects_dir,
269 &path,
270 AtFlags::SYMLINK_FOLLOW,
271 ) {
272 Ok(()) => Ok(id),
273 Err(Errno::EXIST) => Ok(id), Err(e) => Err(e).context("Linking tmpfile into objects directory")?,
275 }
276 }
277
278 fn compute_verity_digest(reader: &mut impl std::io::BufRead) -> Result<ObjectID> {
281 let mut hasher = FsVerityHasher::<ObjectID>::new();
282
283 loop {
284 let buf = reader.fill_buf()?;
285 if buf.is_empty() {
286 break;
287 }
288 let chunk_size = buf.len().min(FsVerityHasher::<ObjectID>::BLOCK_SIZE);
290 hasher.add_block(&buf[..chunk_size]);
291 reader.consume(chunk_size);
292 }
293
294 Ok(hasher.digest())
295 }
296
297 fn store_object_with_id(&self, data: &[u8], id: &ObjectID) -> Result<()> {
302 let dirfd = self.objects_dir()?;
303 let path = id.to_object_pathname();
304
305 match openat(
307 dirfd,
308 &path,
309 OFlags::RDONLY | OFlags::CLOEXEC,
310 Mode::empty(),
311 ) {
312 Ok(fd) => {
313 match ensure_verity_equal(&fd, id) {
316 Ok(()) => {}
317 Err(CompareVerityError::Measure(MeasureVerityError::VerityMissing))
318 if self.insecure =>
319 {
320 match enable_verity_maybe_copy::<ObjectID>(dirfd, fd.as_fd()) {
321 Ok(Some(fd)) => ensure_verity_equal(&fd, id)?,
322 Ok(None) => ensure_verity_equal(&fd, id)?,
323 Err(other) => Err(other)?,
324 }
325 }
326 Err(CompareVerityError::Measure(
327 MeasureVerityError::FilesystemNotSupported,
328 )) if self.insecure => {}
329 Err(other) => Err(other)?,
330 }
331 return Ok(());
332 }
333 Err(Errno::NOENT) => {
334 }
336 Err(other) => {
337 return Err(other).context("Checking for existing object in repository")?;
338 }
339 }
340
341 let fd = ensure_dir_and_openat(dirfd, &id.to_object_dir(), OFlags::RDWR | OFlags::TMPFILE)?;
342 let mut file = File::from(fd);
343 file.write_all(data)?;
344 let ro_fd = open(
346 proc_self_fd(&file),
347 OFlags::RDONLY | OFlags::CLOEXEC,
348 Mode::empty(),
349 )?;
350 drop(file);
354
355 let ro_fd = match enable_verity_maybe_copy::<ObjectID>(dirfd, ro_fd.as_fd()) {
356 Ok(maybe_fd) => {
357 let ro_fd = maybe_fd.unwrap_or(ro_fd);
358 match ensure_verity_equal(&ro_fd, id) {
359 Ok(()) => ro_fd,
360 Err(CompareVerityError::Measure(
361 MeasureVerityError::VerityMissing
362 | MeasureVerityError::FilesystemNotSupported,
363 )) if self.insecure => ro_fd,
364 Err(other) => Err(other).context("Double-checking verity digest")?,
365 }
366 }
367 Err(EnableVerityError::FilesystemNotSupported) if self.insecure => ro_fd,
368 Err(other) => Err(other).context("Enabling verity digest")?,
369 };
370
371 match linkat(
372 CWD,
373 proc_self_fd(&ro_fd),
374 dirfd,
375 path,
376 AtFlags::SYMLINK_FOLLOW,
377 ) {
378 Ok(()) => {}
379 Err(Errno::EXIST) => {
380 }
382 Err(other) => {
383 return Err(other).context("Linking created object file");
384 }
385 }
386
387 Ok(())
388 }
389
390 pub fn ensure_object(&self, data: &[u8]) -> Result<ObjectID> {
395 let id: ObjectID = compute_verity(data);
396 self.store_object_with_id(data, &id)?;
397 Ok(id)
398 }
399
400 fn open_with_verity(&self, filename: &str, expected_verity: &ObjectID) -> Result<OwnedFd> {
401 let fd = self.openat(filename, OFlags::RDONLY)?;
402 match ensure_verity_equal(&fd, expected_verity) {
403 Ok(()) => {}
404 Err(CompareVerityError::Measure(
405 MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported,
406 )) if self.insecure => {}
407 Err(other) => Err(other)?,
408 }
409 Ok(fd)
410 }
411
412 pub fn set_insecure(&mut self, insecure: bool) -> &mut Self {
417 self.insecure = insecure;
418 self
419 }
420
421 pub fn create_stream(self: &Arc<Self>, content_type: u64) -> SplitStreamWriter<ObjectID> {
425 SplitStreamWriter::new(self, content_type)
426 }
427
428 fn format_object_path(id: &ObjectID) -> String {
429 format!("objects/{}", id.to_object_pathname())
430 }
431
432 fn format_stream_path(content_identifier: &str) -> String {
433 format!("streams/{content_identifier}")
434 }
435
436 pub fn has_stream(&self, content_identifier: &str) -> Result<Option<ObjectID>> {
439 let stream_path = Self::format_stream_path(content_identifier);
440
441 match readlinkat(&self.repository, &stream_path, []) {
442 Ok(target) => {
443 let bytes = target.as_bytes();
444 ensure!(
445 bytes.starts_with(b"../"),
446 "stream symlink has incorrect prefix"
447 );
448 Ok(Some(ObjectID::from_object_pathname(bytes)?))
449 }
450 Err(Errno::NOENT) => Ok(None),
451 Err(err) => Err(err)?,
452 }
453 }
454
455 pub fn write_stream(
465 &self,
466 writer: SplitStreamWriter<ObjectID>,
467 content_identifier: &str,
468 reference: Option<&str>,
469 ) -> Result<ObjectID> {
470 let object_id = writer.done()?;
471
472 self.sync()?;
483
484 let stream_path = Self::format_stream_path(content_identifier);
485 let object_path = Self::format_object_path(&object_id);
486 self.symlink(&stream_path, &object_path)?;
487
488 if let Some(name) = reference {
489 let reference_path = format!("streams/refs/{name}");
490 self.symlink(&reference_path, &stream_path)?;
491 }
492
493 Ok(object_id)
494 }
495
496 pub async fn register_stream(
505 self: &Arc<Self>,
506 object_id: &ObjectID,
507 content_identifier: &str,
508 reference: Option<&str>,
509 ) -> Result<()> {
510 self.sync_async().await?;
511
512 let stream_path = Self::format_stream_path(content_identifier);
513 let object_path = Self::format_object_path(object_id);
514 self.symlink(&stream_path, &object_path)?;
515
516 if let Some(name) = reference {
517 let reference_path = format!("streams/refs/{name}");
518 self.symlink(&reference_path, &stream_path)?;
519 }
520
521 Ok(())
522 }
523
524 pub async fn write_stream_async(
530 self: &Arc<Self>,
531 writer: SplitStreamWriter<ObjectID>,
532 content_identifier: &str,
533 reference: Option<&str>,
534 ) -> Result<ObjectID> {
535 let object_id = writer.done_async().await?;
536
537 self.sync_async().await?;
538
539 let stream_path = Self::format_stream_path(content_identifier);
540 let object_path = Self::format_object_path(&object_id);
541 self.symlink(&stream_path, &object_path)?;
542
543 if let Some(name) = reference {
544 let reference_path = format!("streams/refs/{name}");
545 self.symlink(&reference_path, &stream_path)?;
546 }
547
548 Ok(object_id)
549 }
550
551 pub fn has_named_stream(&self, name: &str) -> Result<bool> {
553 let stream_path = format!("streams/refs/{name}");
554
555 Ok(statat(&self.repository, &stream_path, AtFlags::empty())
556 .filter_errno(Errno::NOENT)
557 .context("Looking for stream {name} in repository")?
558 .map(|s| FileType::from_raw_mode(s.st_mode).is_symlink())
559 .unwrap_or(false))
560 }
561
562 pub fn name_stream(&self, content_identifier: &str, name: &str) -> Result<()> {
565 let stream_path = Self::format_stream_path(content_identifier);
566 let reference_path = format!("streams/refs/{name}");
567 self.symlink(&reference_path, &stream_path)?;
568 Ok(())
569 }
570
571 pub fn ensure_stream(
586 self: &Arc<Self>,
587 content_identifier: &str,
588 content_type: u64,
589 callback: impl FnOnce(&mut SplitStreamWriter<ObjectID>) -> Result<()>,
590 reference: Option<&str>,
591 ) -> Result<ObjectID> {
592 let stream_path = Self::format_stream_path(content_identifier);
593
594 let object_id = match self.has_stream(content_identifier)? {
595 Some(id) => id,
596 None => {
597 let mut writer = self.create_stream(content_type);
598 callback(&mut writer)?;
599 self.write_stream(writer, content_identifier, reference)?
600 }
601 };
602
603 if let Some(name) = reference {
604 let reference_path = format!("streams/refs/{name}");
605 self.symlink(&reference_path, &stream_path)?;
606 }
607
608 Ok(object_id)
609 }
610
611 pub fn open_stream(
613 &self,
614 content_identifier: &str,
615 verity: Option<&ObjectID>,
616 expected_content_type: Option<u64>,
617 ) -> Result<SplitStreamReader<ObjectID>> {
618 let file = File::from(if let Some(verity_hash) = verity {
619 self.open_object(verity_hash)
620 .with_context(|| format!("Opening object '{verity_hash:?}'"))?
621 } else {
622 let filename = Self::format_stream_path(content_identifier);
623 self.openat(&filename, OFlags::RDONLY)
624 .with_context(|| format!("Opening ref '{filename}'"))?
625 });
626
627 SplitStreamReader::new(file, expected_content_type)
628 }
629
630 pub fn open_object(&self, id: &ObjectID) -> Result<OwnedFd> {
633 self.open_with_verity(&Self::format_object_path(id), id)
634 }
635
636 pub fn read_object(&self, id: &ObjectID) -> Result<Vec<u8>> {
638 let mut data = vec![];
639 File::from(self.open_object(id)?).read_to_end(&mut data)?;
640 Ok(data)
641 }
642
643 pub fn merge_splitstream(
649 &self,
650 content_identifier: &str,
651 verity: Option<&ObjectID>,
652 expected_content_type: Option<u64>,
653 output: &mut impl Write,
654 ) -> Result<()> {
655 let mut split_stream =
656 self.open_stream(content_identifier, verity, expected_content_type)?;
657 split_stream.cat(self, output)
658 }
659
660 pub fn write_image(&self, name: Option<&str>, data: &[u8]) -> Result<ObjectID> {
668 let object_id = self.ensure_object(data)?;
669
670 let object_path = Self::format_object_path(&object_id);
671 let image_path = format!("images/{}", object_id.to_hex());
672
673 self.symlink(&image_path, &object_path)?;
674
675 if let Some(reference) = name {
676 let ref_path = format!("images/refs/{reference}");
677 self.symlink(&ref_path, &image_path)?;
678 }
679
680 Ok(object_id)
681 }
682
683 pub fn import_image<R: Read>(&self, name: &str, image: &mut R) -> Result<ObjectID> {
691 let mut data = vec![];
692 image.read_to_end(&mut data)?;
693 self.write_image(Some(name), &data)
694 }
695
696 fn open_image(&self, name: &str) -> Result<(OwnedFd, bool)> {
699 let image = self
700 .openat(&format!("images/{name}"), OFlags::RDONLY)
701 .with_context(|| format!("Opening ref 'images/{name}'"))?;
702
703 if name.contains("/") {
704 return Ok((image, true));
705 }
706
707 match measure_verity::<ObjectID>(&image) {
709 Ok(found) if found == FsVerityHashValue::from_hex(name)? => Ok((image, true)),
710 Ok(_) => bail!("fs-verity content mismatch"),
711 Err(MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported)
712 if self.insecure =>
713 {
714 Ok((image, false))
715 }
716 Err(other) => Err(other)?,
717 }
718 }
719
720 pub fn mount(&self, name: &str) -> Result<OwnedFd> {
723 let (image, enable_verity) = self.open_image(name)?;
724 Ok(composefs_fsmount(
725 image,
726 name,
727 self.objects_dir()?,
728 enable_verity,
729 )?)
730 }
731
732 pub fn mount_at(&self, name: &str, mountpoint: impl AsRef<Path>) -> Result<()> {
734 Ok(mount_at(
735 self.mount(name)?,
736 CWD,
737 &canonicalize(mountpoint)?,
738 )?)
739 }
740
741 pub fn symlink(&self, name: impl AsRef<Path>, target: impl AsRef<Path>) -> ErrnoResult<()> {
747 let name = name.as_ref();
748
749 let mut symlink_components = name.parent().unwrap().components().peekable();
750 let mut target_components = target.as_ref().components().peekable();
751
752 let mut symlink_ancestor = PathBuf::new();
753
754 while symlink_components.peek() == target_components.peek() {
756 symlink_ancestor.push(symlink_components.next().unwrap());
757 target_components.next().unwrap();
758 }
759
760 let mut relative = PathBuf::new();
761 for symlink_component in symlink_components {
764 symlink_ancestor.push(symlink_component);
765 self.ensure_dir(&symlink_ancestor)?;
766 relative.push("..");
767 }
768
769 for target_component in target_components {
771 relative.push(target_component);
772 }
773
774 replace_symlinkat(&relative, &self.repository, name)
776 }
777
778 fn read_symlink_hashvalue(dirfd: &OwnedFd, name: &CStr) -> Result<ObjectID> {
779 let link_content = readlinkat(dirfd, name, [])?;
780 Ok(ObjectID::from_object_pathname(link_content.to_bytes())?)
781 }
782
783 fn walk_symlinkdir(fd: OwnedFd, objects: &mut HashSet<ObjectID>) -> Result<()> {
784 for item in Dir::read_from(&fd)? {
785 let entry = item?;
786 match entry.file_type() {
789 FileType::Directory => {
790 let filename = entry.file_name();
791 if filename != c"." && filename != c".." {
792 let dirfd = openat(&fd, filename, OFlags::RDONLY, Mode::empty())?;
793 Self::walk_symlinkdir(dirfd, objects)?;
794 }
795 }
796 FileType::Symlink => {
797 objects.insert(Self::read_symlink_hashvalue(&fd, entry.file_name())?);
798 }
799 _ => {
800 bail!("Unexpected file type encountered");
801 }
802 }
803 }
804
805 Ok(())
806 }
807
808 fn openat(&self, name: &str, flags: OFlags) -> ErrnoResult<OwnedFd> {
810 openat(
812 &self.repository,
813 name,
814 flags | OFlags::CLOEXEC,
815 Mode::empty(),
816 )
817 }
818
819 fn gc_category(&self, category: &str) -> Result<HashSet<ObjectID>> {
820 let mut objects = HashSet::new();
821
822 let Some(category_fd) = self
823 .openat(category, OFlags::RDONLY | OFlags::DIRECTORY)
824 .filter_errno(Errno::NOENT)
825 .context("Opening {category} dir in repository")?
826 else {
827 return Ok(objects);
828 };
829
830 if let Some(refs) = openat(
831 &category_fd,
832 "refs",
833 OFlags::RDONLY | OFlags::DIRECTORY,
834 Mode::empty(),
835 )
836 .filter_errno(Errno::NOENT)
837 .context("Opening {category}/refs dir in repository")?
838 {
839 Self::walk_symlinkdir(refs, &mut objects)?;
840 }
841
842 for item in Dir::read_from(&category_fd)? {
843 let entry = item?;
844 let filename = entry.file_name();
845 if filename != c"refs" && filename != c"." && filename != c".." {
846 if entry.file_type() != FileType::Symlink {
847 bail!("category directory contains non-symlink");
848 }
849
850 continue;
853
854 }
863 }
864
865 Ok(objects)
866 }
867
868 pub fn objects_for_image(&self, name: &str) -> Result<HashSet<ObjectID>> {
870 let (image, _) = self.open_image(name)?;
871 let mut data = vec![];
872 std::fs::File::from(image).read_to_end(&mut data)?;
873 Ok(crate::erofs::reader::collect_objects(&data)?)
874 }
875
876 pub fn sync(&self) -> Result<()> {
881 syncfs(&self.repository)?;
882 Ok(())
883 }
884
885 pub async fn sync_async(self: &Arc<Self>) -> Result<()> {
890 let self_ = Arc::clone(self);
891 tokio::task::spawn_blocking(move || self_.sync()).await?
892 }
893
894 pub fn gc(&self) -> Result<()> {
900 flock(&self.repository, FlockOperation::LockExclusive)?;
901
902 let mut objects = HashSet::new();
903
904 for ref object in self.gc_category("images")? {
905 println!("{object:?} lives as an image");
906 objects.insert(object.clone());
907 objects.extend(self.objects_for_image(&object.to_hex())?);
908 }
909
910 for first_byte in 0x0..=0xff {
924 let dirfd = match self.openat(
925 &format!("objects/{first_byte:02x}"),
926 OFlags::RDONLY | OFlags::DIRECTORY,
927 ) {
928 Ok(fd) => fd,
929 Err(Errno::NOENT) => continue,
930 Err(e) => Err(e)?,
931 };
932 for item in Dir::new(dirfd)? {
933 let entry = item?;
934 let filename = entry.file_name();
935 if filename != c"." && filename != c".." {
936 let id =
937 ObjectID::from_object_dir_and_basename(first_byte, filename.to_bytes())?;
938 if !objects.contains(&id) {
939 println!("rm objects/{first_byte:02x}/{filename:?}");
940 } else {
941 println!("# objects/{first_byte:02x}/{filename:?} lives");
942 }
943 }
944 }
945 }
946
947 Ok(flock(&self.repository, FlockOperation::LockShared)?) }
949
950 }