Skip to main content

ic_host_fs/durable/tree/
mod.rs

1//! Synchronize and checksum a caller-held directory before caller-owned publication.
2//!
3//! Extracted from IC Backup's ordered descriptor sync visitor. Product records,
4//! expected-checksum admission, publication policy and recovery remain caller-owned.
5
6use crate::read::tree::{
7    TreeVisitor,
8    checksum::{ChecksumVisitor, TreeChecksumError},
9    walk_tree,
10};
11use ic_host_artifacts::artifact::Sha256Digest;
12use rustix::{fd::OwnedFd, fs::FileType};
13use std::{
14    fs::File,
15    io::{self, Seek, SeekFrom},
16    os::fd::AsFd,
17    path::Path,
18};
19
20/// Ordered pre-sync observations; an error stops before the selected sync.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum TreeSyncStep {
23    /// A regular file is about to be synchronized, then read for its digest.
24    RegularFile,
25    /// All observed children of a nested directory were processed before its sync.
26    NestedDirectory,
27    /// All observed children of the held root were processed before its sync.
28    RootDirectory,
29}
30
31/// Sync each regular file before hashing it, then sync directories in post-order.
32///
33/// Uses the no-follow/nonblocking descriptor walker and exact path/NUL/lowercase-
34/// digest/newline composition. The root descriptor selects an inode; `display_root`
35/// is diagnostic only. File streams use the explicit per-file byte allowance.
36/// Callers own total bytes, entries, depth, time, memory and stable namespace/content
37/// custody. Sequential sync/read observations do not freeze bytes or path identities.
38///
39/// The hook runs before each sync and can refuse it. This operation neither creates,
40/// renames nor removes entries, publishes a directory, syncs its parent link, compares
41/// an expected digest nor decides recovery. Errors may follow earlier completed syncs.
42///
43/// # Errors
44/// Rejects unsafe/non-UTF-8 children, bounded-stream failures, hook and filesystem
45/// errors. Native I/O identity is retained in `TreeChecksumError::Io`.
46pub fn sync_checksum_directory_at(
47    directory: &File,
48    display_root: &Path,
49    max_file_bytes: u64,
50    mut before_sync: impl FnMut(TreeSyncStep, &Path) -> io::Result<()>,
51) -> Result<Sha256Digest, TreeChecksumError> {
52    let directory_fd = directory.as_fd().try_clone_to_owned()?;
53    let mut visitor = SyncVisitor {
54        checksum: ChecksumVisitor::new(max_file_bytes),
55        before_sync: &mut before_sync,
56    };
57    walk_tree(&directory_fd, Path::new(""), display_root, &mut visitor)?;
58    visitor.checksum.finish()
59}
60
61struct SyncVisitor<'a, F> {
62    checksum: ChecksumVisitor,
63    before_sync: &'a mut F,
64}
65
66impl<F> TreeVisitor for SyncVisitor<'_, F>
67where
68    F: FnMut(TreeSyncStep, &Path) -> io::Result<()>,
69{
70    type Error = TreeChecksumError;
71
72    fn file(&mut self, fd: &OwnedFd, relative: &Path, display: &Path) -> Result<(), Self::Error> {
73        let mut file = File::from(fd.try_clone()?);
74        (self.before_sync)(TreeSyncStep::RegularFile, display)?;
75        file.sync_all()?;
76        file.seek(SeekFrom::Start(0))?;
77        self.checksum.record_file(file, relative)
78    }
79
80    fn enter_directory(&mut self, _relative: &Path, _display: &Path) -> Result<(), Self::Error> {
81        Ok(())
82    }
83
84    fn leave_directory(
85        &mut self,
86        fd: &OwnedFd,
87        relative: &Path,
88        display: &Path,
89    ) -> Result<(), Self::Error> {
90        let step = if relative.as_os_str().is_empty() {
91            TreeSyncStep::RootDirectory
92        } else {
93            TreeSyncStep::NestedDirectory
94        };
95        (self.before_sync)(step, display)?;
96        rustix::fs::fsync(fd).map_err(io::Error::from)?;
97        Ok(())
98    }
99
100    fn unsupported(&self, display: &Path, kind: FileType) -> Self::Error {
101        self.checksum.unsupported(display, kind)
102    }
103
104    fn non_utf8(&self, display: &Path) -> Self::Error {
105        self.checksum.non_utf8(display)
106    }
107}
108
109#[cfg(test)]
110mod tests;