composefs/
repository.rs

1//! Content-addressable repository for composefs objects.
2//!
3//! This module provides a repository abstraction for storing and retrieving
4//! content-addressed objects, splitstreams, and images with fs-verity
5//! verification and garbage collection support.
6
7use 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};
16
17use anyhow::{bail, ensure, Context, Result};
18use once_cell::sync::OnceCell;
19use rustix::{
20    fs::{
21        fdatasync, flock, linkat, mkdirat, open, openat, readlinkat, AtFlags, Dir, FileType,
22        FlockOperation, Mode, OFlags, CWD,
23    },
24    io::{Errno, Result as ErrnoResult},
25};
26use sha2::{Digest, Sha256};
27
28use crate::{
29    fsverity::{
30        compute_verity, enable_verity_maybe_copy, ensure_verity_equal, measure_verity,
31        CompareVerityError, EnableVerityError, FsVerityHashValue, MeasureVerityError,
32    },
33    mount::{composefs_fsmount, mount_at},
34    splitstream::{DigestMap, SplitStreamReader, SplitStreamWriter},
35    util::{proc_self_fd, replace_symlinkat, ErrnoFilter, Sha256Digest},
36};
37
38/// Call openat() on the named subdirectory of "dirfd", possibly creating it first.
39///
40/// We assume that the directory will probably exist (ie: we try the open first), and on ENOENT, we
41/// mkdirat() and retry.
42fn ensure_dir_and_openat(dirfd: impl AsFd, filename: &str, flags: OFlags) -> ErrnoResult<OwnedFd> {
43    match openat(
44        &dirfd,
45        filename,
46        flags | OFlags::CLOEXEC | OFlags::DIRECTORY,
47        0o666.into(),
48    ) {
49        Ok(file) => Ok(file),
50        Err(Errno::NOENT) => match mkdirat(&dirfd, filename, 0o777.into()) {
51            Ok(()) | Err(Errno::EXIST) => openat(
52                dirfd,
53                filename,
54                flags | OFlags::CLOEXEC | OFlags::DIRECTORY,
55                0o666.into(),
56            ),
57            Err(other) => Err(other),
58        },
59        Err(other) => Err(other),
60    }
61}
62
63/// A content-addressable repository for composefs objects.
64///
65/// Stores content-addressed objects, splitstreams, and images with fsverity
66/// verification. Objects are stored by their fsverity digest, streams by SHA256
67/// content hash, and both support named references for persistence across
68/// garbage collection.
69#[derive(Debug)]
70pub struct Repository<ObjectID: FsVerityHashValue> {
71    repository: OwnedFd,
72    objects: OnceCell<OwnedFd>,
73    insecure: bool,
74    _data: std::marker::PhantomData<ObjectID>,
75}
76
77impl<ObjectID: FsVerityHashValue> Drop for Repository<ObjectID> {
78    fn drop(&mut self) {
79        flock(&self.repository, FlockOperation::Unlock).expect("repository unlock failed");
80    }
81}
82
83impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
84    /// Return the objects directory.
85    pub fn objects_dir(&self) -> ErrnoResult<&OwnedFd> {
86        self.objects
87            .get_or_try_init(|| ensure_dir_and_openat(&self.repository, "objects", OFlags::PATH))
88    }
89
90    /// Open a repository at the target directory and path.
91    pub fn open_path(dirfd: impl AsFd, path: impl AsRef<Path>) -> Result<Self> {
92        let path = path.as_ref();
93
94        // O_PATH isn't enough because flock()
95        let repository = openat(dirfd, path, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())
96            .with_context(|| format!("Cannot open composefs repository at {}", path.display()))?;
97
98        flock(&repository, FlockOperation::LockShared)
99            .context("Cannot lock composefs repository")?;
100
101        Ok(Self {
102            repository,
103            objects: OnceCell::new(),
104            insecure: false,
105            _data: std::marker::PhantomData,
106        })
107    }
108
109    /// Open the default user-owned composefs repository.
110    pub fn open_user() -> Result<Self> {
111        let home = std::env::var("HOME").with_context(|| "$HOME must be set when in user mode")?;
112
113        Self::open_path(CWD, PathBuf::from(home).join(".var/lib/composefs"))
114    }
115
116    /// Open the default system-global composefs repository.
117    pub fn open_system() -> Result<Self> {
118        Self::open_path(CWD, PathBuf::from("/sysroot/composefs".to_string()))
119    }
120
121    fn ensure_dir(&self, dir: impl AsRef<Path>) -> ErrnoResult<()> {
122        mkdirat(&self.repository, dir.as_ref(), 0o755.into()).or_else(|e| match e {
123            Errno::EXIST => Ok(()),
124            _ => Err(e),
125        })
126    }
127
128    /// Asynchronously ensures an object exists in the repository.
129    ///
130    /// Same as `ensure_object` but runs the operation on a blocking thread pool
131    /// to avoid blocking async tasks. Returns the fsverity digest of the object.
132    pub async fn ensure_object_async(self: &Arc<Self>, data: Vec<u8>) -> Result<ObjectID> {
133        let self_ = Arc::clone(self);
134        tokio::task::spawn_blocking(move || self_.ensure_object(&data)).await?
135    }
136
137    /// Given a blob of data, store it in the repository.
138    pub fn ensure_object(&self, data: &[u8]) -> Result<ObjectID> {
139        let dirfd = self.objects_dir()?;
140        let id: ObjectID = compute_verity(data);
141
142        let path = id.to_object_pathname();
143
144        // the usual case is that the file will already exist
145        match openat(
146            dirfd,
147            &path,
148            OFlags::RDONLY | OFlags::CLOEXEC,
149            Mode::empty(),
150        ) {
151            Ok(fd) => {
152                // measure the existing file to ensure that it's correct
153                // TODO: try to replace file if it's broken?
154                match ensure_verity_equal(&fd, &id) {
155                    Ok(()) => {}
156                    Err(CompareVerityError::Measure(MeasureVerityError::VerityMissing))
157                        if self.insecure =>
158                    {
159                        match enable_verity_maybe_copy::<ObjectID>(dirfd, fd.as_fd()) {
160                            Ok(Some(fd)) => ensure_verity_equal(&fd, &id)?,
161                            Ok(None) => ensure_verity_equal(&fd, &id)?,
162                            Err(other) => Err(other)?,
163                        }
164                    }
165                    Err(CompareVerityError::Measure(
166                        MeasureVerityError::FilesystemNotSupported,
167                    )) if self.insecure => {}
168                    Err(other) => Err(other)?,
169                }
170                return Ok(id);
171            }
172            Err(Errno::NOENT) => {
173                // in this case we'll create the file
174            }
175            Err(other) => {
176                return Err(other).context("Checking for existing object in repository")?;
177            }
178        }
179
180        let fd = ensure_dir_and_openat(dirfd, &id.to_object_dir(), OFlags::RDWR | OFlags::TMPFILE)?;
181        let mut file = File::from(fd);
182        file.write_all(data)?;
183        fdatasync(&file)?;
184
185        // We can't enable verity with an open writable fd, so re-open and close the old one.
186        let ro_fd = open(
187            proc_self_fd(&file),
188            OFlags::RDONLY | OFlags::CLOEXEC,
189            Mode::empty(),
190        )?;
191        drop(file);
192
193        let ro_fd = match enable_verity_maybe_copy::<ObjectID>(dirfd, ro_fd.as_fd()) {
194            Ok(maybe_fd) => {
195                let ro_fd = maybe_fd.unwrap_or(ro_fd);
196                match ensure_verity_equal(&ro_fd, &id) {
197                    Ok(()) => ro_fd,
198                    Err(CompareVerityError::Measure(
199                        MeasureVerityError::VerityMissing
200                        | MeasureVerityError::FilesystemNotSupported,
201                    )) if self.insecure => ro_fd,
202                    Err(other) => Err(other).context("Double-checking verity digest")?,
203                }
204            }
205            Err(EnableVerityError::FilesystemNotSupported) if self.insecure => ro_fd,
206            Err(other) => Err(other).context("Enabling verity digest")?,
207        };
208
209        match linkat(
210            CWD,
211            proc_self_fd(&ro_fd),
212            dirfd,
213            path,
214            AtFlags::SYMLINK_FOLLOW,
215        ) {
216            Ok(()) => {}
217            Err(Errno::EXIST) => {
218                // TODO: strictly, we should measure the newly-appeared file
219            }
220            Err(other) => {
221                return Err(other).context("Linking created object file");
222            }
223        }
224
225        Ok(id)
226    }
227
228    fn open_with_verity(&self, filename: &str, expected_verity: &ObjectID) -> Result<OwnedFd> {
229        let fd = self.openat(filename, OFlags::RDONLY)?;
230        match ensure_verity_equal(&fd, expected_verity) {
231            Ok(()) => {}
232            Err(CompareVerityError::Measure(
233                MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported,
234            )) if self.insecure => {}
235            Err(other) => Err(other)?,
236        }
237        Ok(fd)
238    }
239
240    /// By default fsverity is required to be enabled on the target
241    /// filesystem. Setting this disables verification of digests
242    /// and an instance of [`Self`] can be used on a filesystem
243    /// without fsverity support.
244    pub fn set_insecure(&mut self, insecure: bool) -> &mut Self {
245        self.insecure = insecure;
246        self
247    }
248
249    /// Creates a SplitStreamWriter for writing a split stream.
250    /// You should write the data to the returned object and then pass it to .store_stream() to
251    /// store the result.
252    pub fn create_stream(
253        self: &Arc<Self>,
254        sha256: Option<Sha256Digest>,
255        maps: Option<DigestMap<ObjectID>>,
256    ) -> SplitStreamWriter<ObjectID> {
257        SplitStreamWriter::new(self, maps, sha256)
258    }
259
260    fn format_object_path(id: &ObjectID) -> String {
261        format!("objects/{}", id.to_object_pathname())
262    }
263
264    /// Check if the provided splitstream is present in the repository;
265    /// if so, return its fsverity digest.
266    pub fn has_stream(&self, sha256: &Sha256Digest) -> Result<Option<ObjectID>> {
267        let stream_path = format!("streams/{}", hex::encode(sha256));
268
269        match readlinkat(&self.repository, &stream_path, []) {
270            Ok(target) => {
271                // NB: This is kinda unsafe: we depend that the symlink didn't get corrupted
272                // we could also measure the verity of the destination object, but it doesn't
273                // improve anything, since we don't know if it was the original one.
274                //
275                // One thing we *could* do here is to iterate the entire file and verify the sha256
276                // content hash.  That would allow us to reestablish a solid link between
277                // content-sha256 and verity digest.
278                let bytes = target.as_bytes();
279                ensure!(
280                    bytes.starts_with(b"../"),
281                    "stream symlink has incorrect prefix"
282                );
283                Ok(Some(ObjectID::from_object_pathname(bytes)?))
284            }
285            Err(Errno::NOENT) => Ok(None),
286            Err(err) => Err(err)?,
287        }
288    }
289
290    /// Similar to [`Self::has_stream`] but performs more expensive verification.
291    pub fn check_stream(&self, sha256: &Sha256Digest) -> Result<Option<ObjectID>> {
292        let stream_path = format!("streams/{}", hex::encode(sha256));
293        match self.openat(&stream_path, OFlags::RDONLY) {
294            Ok(stream) => {
295                let path = readlinkat(&self.repository, stream_path, [])?;
296                let measured_verity = match measure_verity(&stream) {
297                    Ok(found) => found,
298                    Err(
299                        MeasureVerityError::VerityMissing
300                        | MeasureVerityError::FilesystemNotSupported,
301                    ) if self.insecure => FsVerityHashValue::from_object_pathname(path.to_bytes())?,
302                    Err(other) => Err(other)?,
303                };
304                let mut context = Sha256::new();
305                let mut split_stream = SplitStreamReader::new(File::from(stream))?;
306
307                // check the verity of all linked streams
308                for entry in &split_stream.refs.map {
309                    if self.check_stream(&entry.body)?.as_ref() != Some(&entry.verity) {
310                        bail!("reference mismatch");
311                    }
312                }
313
314                // check this stream
315                split_stream.cat(&mut context, |id| -> Result<Vec<u8>> {
316                    let mut data = vec![];
317                    File::from(self.open_object(id)?).read_to_end(&mut data)?;
318                    Ok(data)
319                })?;
320                if *sha256 != Into::<[u8; 32]>::into(context.finalize()) {
321                    bail!("Content didn't match!");
322                }
323
324                Ok(Some(measured_verity))
325            }
326            Err(Errno::NOENT) => Ok(None),
327            Err(err) => Err(err)?,
328        }
329    }
330
331    /// Write the given splitstream to the repository with the
332    /// provided name.
333    pub fn write_stream(
334        &self,
335        writer: SplitStreamWriter<ObjectID>,
336        reference: Option<&str>,
337    ) -> Result<ObjectID> {
338        let Some((.., ref sha256)) = writer.sha256 else {
339            bail!("Writer doesn't have sha256 enabled");
340        };
341        let stream_path = format!("streams/{}", hex::encode(sha256));
342        let object_id = writer.done()?;
343        let object_path = Self::format_object_path(&object_id);
344        self.symlink(&stream_path, &object_path)?;
345
346        if let Some(name) = reference {
347            let reference_path = format!("streams/refs/{name}");
348            self.symlink(&reference_path, &stream_path)?;
349        }
350
351        Ok(object_id)
352    }
353
354    /// Assign the given name to a stream.  The stream must already exist.  After this operation it
355    /// will be possible to refer to the stream by its new name 'refs/{name}'.
356    pub fn name_stream(&self, sha256: Sha256Digest, name: &str) -> Result<()> {
357        let stream_path = format!("streams/{}", hex::encode(sha256));
358        let reference_path = format!("streams/refs/{name}");
359        self.symlink(&reference_path, &stream_path)?;
360        Ok(())
361    }
362
363    /// Ensures that the stream with a given SHA256 digest exists in the repository.
364    ///
365    /// This tries to find the stream by the `sha256` digest of its contents.  If the stream is
366    /// already in the repository, the object ID (fs-verity digest) is read from the symlink.  If
367    /// the stream is not already in the repository, a `SplitStreamWriter` is created and passed to
368    /// `callback`.  On return, the object ID of the stream will be calculated and it will be
369    /// written to disk (if it wasn't already created by someone else in the meantime).
370    ///
371    /// In both cases, if `reference` is provided, it is used to provide a fixed name for the
372    /// object.  Any object that doesn't have a fixed reference to it is subject to garbage
373    /// collection.  It is an error if this reference already exists.
374    ///
375    /// On success, the object ID of the new object is returned.  It is expected that this object
376    /// ID will be used when referring to the stream from other linked streams.
377    pub fn ensure_stream(
378        self: &Arc<Self>,
379        sha256: &Sha256Digest,
380        callback: impl FnOnce(&mut SplitStreamWriter<ObjectID>) -> Result<()>,
381        reference: Option<&str>,
382    ) -> Result<ObjectID> {
383        let stream_path = format!("streams/{}", hex::encode(sha256));
384
385        let object_id = match self.has_stream(sha256)? {
386            Some(id) => id,
387            None => {
388                let mut writer = self.create_stream(Some(*sha256), None);
389                callback(&mut writer)?;
390                let object_id = writer.done()?;
391
392                let object_path = Self::format_object_path(&object_id);
393                self.symlink(&stream_path, &object_path)?;
394                object_id
395            }
396        };
397
398        if let Some(name) = reference {
399            let reference_path = format!("streams/refs/{name}");
400            self.symlink(&reference_path, &stream_path)?;
401        }
402
403        Ok(object_id)
404    }
405
406    /// Open a splitstream with the given name.
407    pub fn open_stream(
408        &self,
409        name: &str,
410        verity: Option<&ObjectID>,
411    ) -> Result<SplitStreamReader<File, ObjectID>> {
412        let filename = format!("streams/{name}");
413
414        let file = File::from(if let Some(verity_hash) = verity {
415            self.open_with_verity(&filename, verity_hash)
416                .with_context(|| format!("Opening ref 'streams/{name}'"))?
417        } else {
418            self.openat(&filename, OFlags::RDONLY)
419                .with_context(|| format!("Opening ref 'streams/{name}'"))?
420        });
421
422        SplitStreamReader::new(file)
423    }
424
425    /// Given an object identifier (a digest), return a read-only file descriptor
426    /// for its contents. The fsverity digest is verified (if the repository is not in `insecure` mode).
427    pub fn open_object(&self, id: &ObjectID) -> Result<OwnedFd> {
428        self.open_with_verity(&Self::format_object_path(id), id)
429    }
430
431    /// Merges a splitstream into a single continuous stream.
432    ///
433    /// Opens the named splitstream, resolves all object references, and writes
434    /// the complete merged content to the provided writer. Optionally verifies
435    /// the splitstream's fsverity digest matches the expected value.
436    pub fn merge_splitstream(
437        &self,
438        name: &str,
439        verity: Option<&ObjectID>,
440        stream: &mut impl Write,
441    ) -> Result<()> {
442        let mut split_stream = self.open_stream(name, verity)?;
443        split_stream.cat(stream, |id| -> Result<Vec<u8>> {
444            let mut data = vec![];
445            File::from(self.open_object(id)?).read_to_end(&mut data)?;
446            Ok(data)
447        })?;
448
449        Ok(())
450    }
451
452    /// Write `data into the repository as an image with the given `name`.
453    ///
454    /// The fsverity digest is returned.
455    ///
456    /// # Integrity
457    ///
458    /// This function is not safe for untrusted users.
459    pub fn write_image(&self, name: Option<&str>, data: &[u8]) -> Result<ObjectID> {
460        let object_id = self.ensure_object(data)?;
461
462        let object_path = Self::format_object_path(&object_id);
463        let image_path = format!("images/{}", object_id.to_hex());
464
465        self.symlink(&image_path, &object_path)?;
466
467        if let Some(reference) = name {
468            let ref_path = format!("images/refs/{reference}");
469            self.symlink(&ref_path, &image_path)?;
470        }
471
472        Ok(object_id)
473    }
474
475    /// Import the data from the provided read into the repository as an image.
476    ///
477    /// The fsverity digest is returned.
478    ///
479    /// # Integrity
480    ///
481    /// This function is not safe for untrusted users.
482    pub fn import_image<R: Read>(&self, name: &str, image: &mut R) -> Result<ObjectID> {
483        let mut data = vec![];
484        image.read_to_end(&mut data)?;
485        self.write_image(Some(name), &data)
486    }
487
488    /// Returns the fd of the image and whether or not verity should be
489    /// enabled when mounting it.
490    fn open_image(&self, name: &str) -> Result<(OwnedFd, bool)> {
491        let image = self
492            .openat(&format!("images/{name}"), OFlags::RDONLY)
493            .with_context(|| format!("Opening ref 'images/{name}'"))?;
494
495        if name.contains("/") {
496            return Ok((image, true));
497        }
498
499        // A name with no slashes in it is taken to be a sha256 fs-verity digest
500        match measure_verity::<ObjectID>(&image) {
501            Ok(found) if found == FsVerityHashValue::from_hex(name)? => Ok((image, true)),
502            Ok(_) => bail!("fs-verity content mismatch"),
503            Err(MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported)
504                if self.insecure =>
505            {
506                Ok((image, false))
507            }
508            Err(other) => Err(other)?,
509        }
510    }
511
512    /// Create a detached mount of an image. This file descriptor can then
513    /// be attached via e.g. `move_mount`.
514    pub fn mount(&self, name: &str) -> Result<OwnedFd> {
515        let (image, enable_verity) = self.open_image(name)?;
516        Ok(composefs_fsmount(
517            image,
518            name,
519            self.objects_dir()?,
520            enable_verity,
521        )?)
522    }
523
524    /// Mount the image with the provided digest at the target path.
525    pub fn mount_at(&self, name: &str, mountpoint: impl AsRef<Path>) -> Result<()> {
526        Ok(mount_at(
527            self.mount(name)?,
528            CWD,
529            &canonicalize(mountpoint)?,
530        )?)
531    }
532
533    /// Creates a relative symlink within the repository.
534    ///
535    /// Computes the correct relative path from the symlink location to the target,
536    /// creating any necessary intermediate directories. Atomically replaces any
537    /// existing symlink at the specified name.
538    pub fn symlink(&self, name: impl AsRef<Path>, target: impl AsRef<Path>) -> ErrnoResult<()> {
539        let name = name.as_ref();
540
541        let mut symlink_components = name.parent().unwrap().components().peekable();
542        let mut target_components = target.as_ref().components().peekable();
543
544        let mut symlink_ancestor = PathBuf::new();
545
546        // remove common leading components
547        while symlink_components.peek() == target_components.peek() {
548            symlink_ancestor.push(symlink_components.next().unwrap());
549            target_components.next().unwrap();
550        }
551
552        let mut relative = PathBuf::new();
553        // prepend a "../" for each ancestor of the symlink
554        // and create those ancestors as we do so
555        for symlink_component in symlink_components {
556            symlink_ancestor.push(symlink_component);
557            self.ensure_dir(&symlink_ancestor)?;
558            relative.push("..");
559        }
560
561        // now build the relative path from the remaining components of the target
562        for target_component in target_components {
563            relative.push(target_component);
564        }
565
566        // Atomically replace existing symlink
567        replace_symlinkat(&relative, &self.repository, name)
568    }
569
570    fn read_symlink_hashvalue(dirfd: &OwnedFd, name: &CStr) -> Result<ObjectID> {
571        let link_content = readlinkat(dirfd, name, [])?;
572        Ok(ObjectID::from_object_pathname(link_content.to_bytes())?)
573    }
574
575    fn walk_symlinkdir(fd: OwnedFd, objects: &mut HashSet<ObjectID>) -> Result<()> {
576        for item in Dir::read_from(&fd)? {
577            let entry = item?;
578            // NB: the underlying filesystem must support returning filetype via direntry
579            // that's a reasonable assumption, since it must also support fsverity...
580            match entry.file_type() {
581                FileType::Directory => {
582                    let filename = entry.file_name();
583                    if filename != c"." && filename != c".." {
584                        let dirfd = openat(&fd, filename, OFlags::RDONLY, Mode::empty())?;
585                        Self::walk_symlinkdir(dirfd, objects)?;
586                    }
587                }
588                FileType::Symlink => {
589                    objects.insert(Self::read_symlink_hashvalue(&fd, entry.file_name())?);
590                }
591                _ => {
592                    bail!("Unexpected file type encountered");
593                }
594            }
595        }
596
597        Ok(())
598    }
599
600    /// Open the provided path in the repository.
601    fn openat(&self, name: &str, flags: OFlags) -> ErrnoResult<OwnedFd> {
602        // Unconditionally add CLOEXEC as we always want it.
603        openat(
604            &self.repository,
605            name,
606            flags | OFlags::CLOEXEC,
607            Mode::empty(),
608        )
609    }
610
611    fn gc_category(&self, category: &str) -> Result<HashSet<ObjectID>> {
612        let mut objects = HashSet::new();
613
614        let Some(category_fd) = self
615            .openat(category, OFlags::RDONLY | OFlags::DIRECTORY)
616            .filter_errno(Errno::NOENT)
617            .context("Opening {category} dir in repository")?
618        else {
619            return Ok(objects);
620        };
621
622        if let Some(refs) = openat(
623            &category_fd,
624            "refs",
625            OFlags::RDONLY | OFlags::DIRECTORY,
626            Mode::empty(),
627        )
628        .filter_errno(Errno::NOENT)
629        .context("Opening {category}/refs dir in repository")?
630        {
631            Self::walk_symlinkdir(refs, &mut objects)?;
632        }
633
634        for item in Dir::read_from(&category_fd)? {
635            let entry = item?;
636            let filename = entry.file_name();
637            if filename != c"refs" && filename != c"." && filename != c".." {
638                if entry.file_type() != FileType::Symlink {
639                    bail!("category directory contains non-symlink");
640                }
641
642                // TODO: we need to sort this out.  the symlink itself might be a sha256 content ID
643                // (as for splitstreams), not an object/ to be preserved.
644                continue;
645
646                /*
647                let mut value = Sha256HashValue::EMPTY;
648                hex::decode_to_slice(filename.to_bytes(), &mut value)?;
649
650                if !objects.contains(&value) {
651                    println!("rm {}/{:?}", category, filename);
652                }
653                */
654            }
655        }
656
657        Ok(objects)
658    }
659
660    /// Given an image, return the set of all objects referenced by it.
661    pub fn objects_for_image(&self, name: &str) -> Result<HashSet<ObjectID>> {
662        let (image, _) = self.open_image(name)?;
663        let mut data = vec![];
664        std::fs::File::from(image).read_to_end(&mut data)?;
665        Ok(crate::erofs::reader::collect_objects(&data)?)
666    }
667
668    /// Perform a garbage collection operation.
669    ///
670    /// # Locking
671    ///
672    /// An exclusive lock is held for the duration of this operation.
673    pub fn gc(&self) -> Result<()> {
674        flock(&self.repository, FlockOperation::LockExclusive)?;
675
676        let mut objects = HashSet::new();
677
678        for ref object in self.gc_category("images")? {
679            println!("{object:?} lives as an image");
680            objects.insert(object.clone());
681            objects.extend(self.objects_for_image(&object.to_hex())?);
682        }
683
684        for object in self.gc_category("streams")? {
685            println!("{object:?} lives as a stream");
686            objects.insert(object.clone());
687
688            let mut split_stream = self.open_stream(&object.to_hex(), None)?;
689            split_stream.get_object_refs(|id| {
690                println!("   with {id:?}");
691                objects.insert(id.clone());
692            })?;
693        }
694
695        for first_byte in 0x0..=0xff {
696            let dirfd = match self.openat(
697                &format!("objects/{first_byte:02x}"),
698                OFlags::RDONLY | OFlags::DIRECTORY,
699            ) {
700                Ok(fd) => fd,
701                Err(Errno::NOENT) => continue,
702                Err(e) => Err(e)?,
703            };
704            for item in Dir::new(dirfd)? {
705                let entry = item?;
706                let filename = entry.file_name();
707                if filename != c"." && filename != c".." {
708                    let id =
709                        ObjectID::from_object_dir_and_basename(first_byte, filename.to_bytes())?;
710                    if !objects.contains(&id) {
711                        println!("rm objects/{first_byte:02x}/{filename:?}");
712                    } else {
713                        println!("# objects/{first_byte:02x}/{filename:?} lives");
714                    }
715                }
716            }
717        }
718
719        Ok(flock(&self.repository, FlockOperation::LockShared)?) // XXX: finally { } ?
720    }
721
722    // fn fsck(&self) -> Result<()> {
723    //     unimplemented!()
724    // }
725}