Skip to main content

ic_host_fs/durable/directory/
mod.rs

1//! Retained private directory creation and pathname admission.
2use std::{fs::File, io, path::Path};
3
4mod publish;
5
6pub use publish::{
7    DirectoryCommitError, DirectoryCommitOutcome, DirectoryCommitStep,
8    commit_existing_directory_with,
9};
10
11/// Creation failure distinguishes a refused claim from a possibly visible directory.
12#[derive(Debug, thiserror::Error)]
13pub enum DirectoryCreationError {
14    /// No directory was created by this call (including an occupied name).
15    #[error("private directory creation refused")]
16    BeforeCreation(#[source] io::Error),
17    /// The directory was created; admission or durability could not be confirmed.
18    /// Retain and reconcile the original name rather than automatically retrying.
19    #[error("created private directory admission failed")]
20    AfterCreation(#[source] io::Error),
21}
22
23/// Create one owner-only directory, synchronize its parent link and retain its descriptor.
24///
25/// Uses `0700` reduced by umask; never creates parents, follows a final symlink,
26/// replaces an occupied entry or removes evidence on error/Drop. Callers own
27/// trusted ancestors, root confinement and exclusion of concurrent namespace writers.
28/// The descriptor remains the original directory after a rename; use
29/// [`admit_directory`] before effects that require the original pathname claim.
30///
31/// # Errors
32/// Returns creation phase and native I/O cause, including unsupported hosts.
33pub fn create_private_directory(path: &Path) -> Result<File, DirectoryCreationError> {
34    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
35    {
36        use std::os::unix::fs::DirBuilderExt as _;
37        let parent = path
38            .parent()
39            .filter(|p| !p.as_os_str().is_empty())
40            .unwrap_or_else(|| Path::new("."));
41        // Admission precedes creation so an invalid namespace leaves no new claim.
42        for directory in [
43            path.to_path_buf(),
44            parent
45                .canonicalize()
46                .map_err(DirectoryCreationError::BeforeCreation)?,
47        ] {
48            if directory
49                .as_os_str()
50                .as_encoded_bytes()
51                .iter()
52                .any(|b| matches!(b, b'\r' | b'\n'))
53            {
54                return Err(DirectoryCreationError::BeforeCreation(
55                    io::ErrorKind::InvalidInput.into(),
56                ));
57            }
58        }
59        std::fs::DirBuilder::new()
60            .mode(0o700)
61            .create(path)
62            .map_err(DirectoryCreationError::BeforeCreation)?;
63        let finish = || -> io::Result<File> {
64            use rustix::fs::{Mode, OFlags, open};
65            File::open(parent)?.sync_all()?;
66            let file = File::from(open(
67                path,
68                OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
69                Mode::empty(),
70            )?);
71            admit_directory(path, &file)?;
72            Ok(file)
73        };
74        finish().map_err(DirectoryCreationError::AfterCreation)
75    }
76    #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
77    {
78        let _ = path;
79        Err(DirectoryCreationError::BeforeCreation(
80            io::ErrorKind::Unsupported.into(),
81        ))
82    }
83}
84
85/// Require a no-follow directory pathname to still identify the held directory.
86///
87/// This is an observation, not protection against a subsequent concurrent rename.
88/// Callers must retain trusted ancestors and exclusive namespace custody.
89/// # Errors
90/// Missing, symlink, non-directory, replaced, non-directory descriptor and unsupported hosts refuse.
91pub fn admit_directory(path: &Path, directory: &File) -> io::Result<()> {
92    #[cfg(unix)]
93    {
94        use crate::identity::FileIdentity;
95        let current = std::fs::symlink_metadata(path)?;
96        let held = directory.metadata()?;
97        if !current.is_dir()
98            || !held.is_dir()
99            || FileIdentity::from_metadata(&current) != FileIdentity::from_metadata(&held)
100        {
101            return Err(io::ErrorKind::InvalidInput.into());
102        }
103        Ok(())
104    }
105    #[cfg(not(unix))]
106    {
107        let _ = (path, directory);
108        Err(io::ErrorKind::Unsupported.into())
109    }
110}
111
112#[cfg(test)]
113mod tests;