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, thiserror::Error)]
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
67/// Produce and validate at an owned absolute sibling path, then durably publish it.
68///
69/// Uses the same creation, synchronization and publication engine as [`super::write_with`].
70/// `options` selects replace/create-only publication and staging permissions;
71/// permissions are reduced by the process umask. Missing parents use ordinary
72/// directory creation, without establishing private parent custody.
73/// The callback receives an exclusively created, empty regular file. An external
74/// tool must write/truncate that inode, not unlink it or replace it by rename.
75/// The callback owns executable admission, arguments, environment, output byte
76/// limits and content validation. A precreated empty file is not proof of output:
77/// validate it before returning `Ok`, even when the process exits successfully.
78/// Its value is returned only after complete durable publication.
79///
80/// The caller must control the parent hierarchy and exclude concurrent namespace
81/// mutation throughout the call. Identity checks reject observed staging/parent
82/// replacement, symlinks, special files and additional hard links; they cannot
83/// make pathname comparison and rename atomic against a hostile directory writer.
84/// All producers and writable handles must finish before `Ok`. This is neither a
85/// child sandbox nor a process-tree supervisor. No retries occur.
86///
87/// Ordinary failures attempt to unlink only the still-owned staging entry and
88/// retain any cleanup error. Foreign replacements and files moved elsewhere are
89/// never removed. Panics, interruption and uncooperative surviving children can
90/// leave staging or other effects; caller-owned reconciliation remains necessary.
91///
92/// # Errors
93/// Returns the original producer error or a filesystem failure with explicit
94/// before/after-publication state. After-publication failures must be reconciled
95/// before retrying. Invalid permissions refuse before staging or production;
96/// create-only mode atomically refuses an existing final entry. Unsupported
97/// hosts return a before-publication error.
98pub fn write_named_with<T, E>(
99    path: &Path,
100    options: WriteOptions,
101    produce: impl FnOnce(&Path) -> Result<T, E>,
102) -> Result<T, NamedWriteError<E>> {
103    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
104    {
105        super::supported::validate_target_path(path).map_err(NamedWriteError::before)?;
106        let absolute = std::path::absolute(path).map_err(NamedWriteError::before)?;
107        super::supported::commit_path_with_options(
108            &absolute,
109            options,
110            |stage, _| produce(stage),
111            (super::supported::WriterCustody::Writable, |_, _| Ok(())),
112            |_, _| Ok(()),
113        )
114    }
115    #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
116    {
117        let _ = (path, options, produce);
118        Err(NamedWriteError::before(io::Error::new(
119            io::ErrorKind::Unsupported,
120            "durable atomic file publication is unsupported",
121        )))
122    }
123}
124
125#[cfg(all(test, unix))]
126mod tests;