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;