Skip to main content

ic_host_fs/durable/named/
mod.rs

1//! Named producer access to the existing durable publication engine.
2
3use super::WriteOptions;
4use std::{fmt, io, path::Path};
5
6/// Failure from a named or streamed producer and durable publication, retaining cleanup evidence.
7#[derive(Debug)]
8pub enum NamedWriteError<E> {
9    /// The producer or its output validation failed; no publication was attempted.
10    Producer {
11        /// Original consumer error, including any bounded process evidence.
12        source: E,
13        /// Failure to remove the owned staging entry; replacements are left untouched.
14        cleanup_error: Option<io::Error>,
15    },
16    /// Filesystem admission, synchronization or rename failed before publication.
17    BeforePublication {
18        /// Original filesystem error or an invalid-data identity rejection.
19        source: io::Error,
20        /// Failure to remove the owned staging entry; replacements are left untouched.
21        cleanup_error: Option<io::Error>,
22    },
23    /// Publication succeeded, but staging cleanup or final directory sync failed.
24    AfterPublication {
25        /// Original cleanup or synchronization error. The new output is visible;
26        /// final directory durability is not established. Reconcile before retrying.
27        source: io::Error,
28    },
29}
30
31impl<E> NamedWriteError<E> {
32    pub(super) const fn before(source: io::Error) -> Self {
33        Self::BeforePublication {
34            source,
35            cleanup_error: None,
36        }
37    }
38}
39
40impl<E: fmt::Display> fmt::Display for NamedWriteError<E> {
41    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::Producer { source, .. } => write!(formatter, "staged producer failed: {source}"),
44            Self::BeforePublication { source, .. } => {
45                write!(formatter, "publication failed before rename: {source}")
46            }
47            Self::AfterPublication { source } => write!(
48                formatter,
49                "output published but completion failed: {source}"
50            ),
51        }?;
52        if let Self::Producer {
53            cleanup_error: Some(cleanup),
54            ..
55        }
56        | Self::BeforePublication {
57            cleanup_error: Some(cleanup),
58            ..
59        } = self
60        {
61            write!(formatter, "; staging cleanup failed: {cleanup}")?;
62        }
63        Ok(())
64    }
65}
66
67impl<E: std::error::Error + 'static> std::error::Error for NamedWriteError<E> {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        match self {
70            Self::Producer { source, .. } => Some(source),
71            Self::BeforePublication { source, .. } | Self::AfterPublication { source } => {
72                Some(source)
73            }
74        }
75    }
76}
77
78/// Produce and validate at an owned absolute sibling path, then durably publish it.
79///
80/// Uses the same creation, synchronization and publication engine as [`super::write_with`].
81/// `options` selects replace/create-only publication and staging permissions;
82/// permissions are reduced by the process umask. Missing parents use ordinary
83/// directory creation, without establishing private parent custody.
84/// The callback receives an exclusively created, empty regular file. An external
85/// tool must write/truncate that inode, not unlink it or replace it by rename.
86/// The callback owns executable admission, arguments, environment, output byte
87/// limits and content validation. A precreated empty file is not proof of output:
88/// validate it before returning `Ok`, even when the process exits successfully.
89/// Its value is returned only after complete durable publication.
90///
91/// The caller must control the parent hierarchy and exclude concurrent namespace
92/// mutation throughout the call. Identity checks reject observed staging/parent
93/// replacement, symlinks, special files and additional hard links; they cannot
94/// make pathname comparison and rename atomic against a hostile directory writer.
95/// All producers and writable handles must finish before `Ok`. This is neither a
96/// child sandbox nor a process-tree supervisor. No retries occur.
97///
98/// Ordinary failures attempt to unlink only the still-owned staging entry and
99/// retain any cleanup error. Foreign replacements and files moved elsewhere are
100/// never removed. Panics, interruption and uncooperative surviving children can
101/// leave staging or other effects; caller-owned reconciliation remains necessary.
102///
103/// # Errors
104/// Returns the original producer error or a filesystem failure with explicit
105/// before/after-publication state. After-publication failures must be reconciled
106/// before retrying. Invalid permissions refuse before staging or production;
107/// create-only mode atomically refuses an existing final entry. Unsupported
108/// hosts return a before-publication error.
109pub fn write_named_with<T, E>(
110    path: &Path,
111    options: WriteOptions,
112    produce: impl FnOnce(&Path) -> Result<T, E>,
113) -> Result<T, NamedWriteError<E>> {
114    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
115    {
116        super::supported::validate_target_path(path).map_err(NamedWriteError::before)?;
117        let absolute = std::path::absolute(path).map_err(NamedWriteError::before)?;
118        super::supported::commit_path_with_options(
119            &absolute,
120            options,
121            |stage, _| produce(stage),
122            (super::supported::WriterCustody::Writable, |_, _| Ok(())),
123            |_, _| Ok(()),
124        )
125    }
126    #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
127    {
128        let _ = (path, options, produce);
129        Err(NamedWriteError::before(io::Error::new(
130            io::ErrorKind::Unsupported,
131            "durable atomic file publication is unsupported",
132        )))
133    }
134}
135
136#[cfg(all(test, unix))]
137mod tests;