Skip to main content

ic_host_fs/durable/stream/
mod.rs

1//! Typed streamed publication through the existing durable engine.
2
3use super::NamedWriteError;
4use std::{fs, path::Path};
5
6#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
7use super::supported::WriterCustody;
8
9/// How complete staging becomes the selected directory entry.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum PublicationMode {
12    /// Atomically replace the selected entry without following its final symlink.
13    Replace,
14    /// Publish only if the selected entry is absent, including at the final race.
15    CreateNew,
16}
17
18/// Caller-selected publication and staging permissions; no policy defaults.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct WriteOptions {
21    /// Whether an existing destination can be replaced.
22    pub mode: PublicationMode,
23    /// Unix permission bits for the created file, reduced by the process umask.
24    ///
25    /// Only `0o000..=0o777` is accepted. Use `0o600` for owner-only staging.
26    /// Existing target permissions are not copied; the producer may further
27    /// restrict the held file's permissions before successful completion.
28    pub permissions: u32,
29}
30
31/// Stream a typed producer into durable publication at a caller-selected path.
32///
33/// Owns parent creation, exclusive staging, file synchronization,
34/// publication and parent synchronization for pathname writes. The
35/// callback may serialize through a bounded writer and retain its own error
36/// type, without buffering a complete document or converting that error to I/O.
37/// Missing parents are created with the existing ordinary directory semantics;
38/// owner-only file permissions do not establish private parent custody.
39///
40/// Callers own input admission, output limits, target policy and parent hierarchy
41/// control. Exclude concurrent namespace writers, keep the stage a regular file
42/// with one link and finish all writable handles before returning success.
43/// Panics/interruption may retain staging; publication is not a multi-file
44/// transaction, sandbox or retry mechanism.
45///
46/// # Errors
47/// Retains the original producer error and separate cleanup failures in the
48/// shared [`NamedWriteError`]. Before/after-publication filesystem failures stay
49/// distinguishable. Invalid permissions and unsupported hosts fail before the
50/// producer is called. Paths containing NUL and directory-required targets ending
51/// in `/` or `/.` are rejected before parent creation or staging. Reconcile
52/// after-publication failures before retrying.
53pub fn write_with<T, E>(
54    path: &Path,
55    options: WriteOptions,
56    produce: impl FnOnce(&mut fs::File) -> Result<T, E>,
57) -> Result<T, NamedWriteError<E>> {
58    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
59    {
60        super::supported::commit_path_with_options(
61            path,
62            options,
63            |_, file| produce(file),
64            (WriterCustody::Writable, |_, _| Ok(())),
65            |_, _| Ok(()),
66        )
67    }
68    #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
69    {
70        let _ = (path, options, produce);
71        Err(NamedWriteError::before(std::io::Error::new(
72            std::io::ErrorKind::Unsupported,
73            "durable atomic file publication is unsupported",
74        )))
75    }
76}
77
78/// Stream through an existing parent, admitting the result after file sync.
79///
80/// Missing parents are never created. Opens the selected parent once as a
81/// directory, following parent aliases; all staging/publication then uses its held
82/// descriptor, even after a rename. This is pathname convenience, not root
83/// confinement or private-parent admission. The final entry uses the selected
84/// publication mode and is never followed.
85///
86/// Shares [`write_at_validated_with`]'s producer, permissions, custody, admission,
87/// cleanup and before/after-publication contracts. Admission runs after file sync
88/// and before publication. Use `|_| Ok(())` when no additional admission is needed.
89/// Callers retain concurrency, cancellation, validation and retry policy.
90///
91/// # Errors
92/// Missing/special-file parents and invalid paths/permissions refuse before
93/// production. Retains typed producer/admission and independent cleanup failures;
94/// filesystem syscall latency remains unbounded. Reconcile published failures.
95pub fn write_existing_parent_validated_with<T, E>(
96    path: &Path,
97    options: WriteOptions,
98    produce: impl FnOnce(&mut fs::File) -> Result<T, E>,
99    admit: impl FnOnce(&T) -> Result<(), E>,
100) -> Result<T, NamedWriteError<E>> {
101    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
102    {
103        use rustix::fs::{Mode, OFlags, open};
104        use std::os::fd::AsFd as _;
105        // Preserve permission refusal before attempting to open the parent.
106        super::supported::native_permissions(options.permissions)
107            .map_err(NamedWriteError::before)?;
108        let (parent, name) =
109            super::supported::split_target(path).map_err(NamedWriteError::before)?;
110        let parent = open(
111            parent,
112            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NONBLOCK | OFlags::CLOEXEC,
113            Mode::empty(),
114        )
115        .map_err(|source| NamedWriteError::before(source.into()))?;
116        write_at_validated_with(parent.as_fd(), name, options, produce, admit)
117    }
118    #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
119    {
120        let _ = (path, options, produce, admit);
121        Err(NamedWriteError::before(std::io::Error::new(
122            std::io::ErrorKind::Unsupported,
123            "durable atomic file publication is unsupported",
124        )))
125    }
126}
127
128/// Stream beneath an admitted directory, then admit the result before publication.
129///
130/// `name` is one nonempty filename component excluding dot, dot-dot, slash and
131/// NUL. The held parent descriptor must name a directory open for synchronization.
132/// Staging, identity checks, cleanup, publication and parent sync remain relative
133/// to it across namespace renames. No parent is created or pathname reopened.
134///
135/// Caller-selected options control replace/create-only publication and permissions.
136/// Replace affects the final entry itself, including symlinks; create-only refuses
137/// an occupied name. This is neither root confinement nor a compare-and-swap.
138/// After production and file sync, `admit` receives the result and may reject it
139/// while the old destination remains untouched. Use `|_| Ok(())` for no additional
140/// admission. It must not mutate staging or issue publication effects.
141///
142/// Callers retain directory/namespace custody, target policy, output limits,
143/// cancellation and retry policy. Finish all writable handles/external producers
144/// before returning. The callback/interruption obligations of [`write_with`] apply.
145///
146/// # Errors
147/// Invalid names/permissions or non-directory descriptors refuse before production.
148/// Filesystem-specific name encoding may still refuse at publication. Retains
149/// typed producer/admission, cleanup and before/after-publication failures;
150/// reconcile already-published results before retrying.
151#[cfg(unix)]
152pub fn write_at_validated_with<T, E>(
153    parent: std::os::fd::BorrowedFd<'_>,
154    name: &std::ffi::OsStr,
155    options: WriteOptions,
156    produce: impl FnOnce(&mut fs::File) -> Result<T, E>,
157    admit: impl FnOnce(&T) -> Result<(), E>,
158) -> Result<T, NamedWriteError<E>> {
159    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
160    {
161        let permissions = super::supported::native_permissions(options.permissions)
162            .map_err(NamedWriteError::before)?;
163        super::supported::commit_at_with_hook(
164            parent,
165            None,
166            name,
167            (options.mode, permissions),
168            |_, file| produce(file),
169            (WriterCustody::Writable, |_, value| admit(value)),
170            |_, _| Ok(()),
171        )
172    }
173    #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
174    {
175        let _ = (parent, name, options, produce, admit);
176        Err(NamedWriteError::before(std::io::Error::new(
177            std::io::ErrorKind::Unsupported,
178            "durable atomic file publication is unsupported",
179        )))
180    }
181}
182
183/// Stream, synchronize and close a writer before admitting its staged pathname.
184///
185/// Shares [`write_with`]'s engine, permissions and publication modes. After
186/// successful production and file sync, a verified read-only descriptor retains
187/// inode custody while the library's writable descriptor is closed. `admit` then
188/// receives the absolute staged path and the producer's value before publication.
189/// This allows executable admission on hosts that reject executing a writable inode.
190///
191/// The producer must close all writable clones and finish external writers before
192/// returning. Admission must not modify the file and must finish its child processes
193/// before returning. The library does not choose executable versions, arguments,
194/// environment, byte limits or retries. Read permission is required when reopening
195/// the stage; execute permission is caller-selected. All parent-custody and
196/// interruption obligations of [`write_with`] apply. A read-only descriptor
197/// is not a sandbox or a guarantee against concurrent content modification.
198///
199/// # Errors
200/// Producer/admission failures retain `E` in [`NamedWriteError::Producer`].
201/// Reopening, identity, sync and publication failures retain their filesystem
202/// phase. Admission rejection preserves the prior destination and attempts
203/// owned-entry cleanup. Reconcile after-publication failures before retrying.
204pub fn write_validated_with<T, E>(
205    path: &Path,
206    options: WriteOptions,
207    produce: impl FnOnce(&mut fs::File) -> Result<T, E>,
208    admit: impl FnOnce(&Path, &T) -> Result<(), E>,
209) -> Result<T, NamedWriteError<E>> {
210    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
211    {
212        super::supported::validate_target_path(path).map_err(NamedWriteError::before)?;
213        let absolute = std::path::absolute(path).map_err(NamedWriteError::before)?;
214        super::supported::commit_path_with_options(
215            &absolute,
216            options,
217            |_, file| produce(file),
218            (WriterCustody::ReadOnly, admit),
219            |_, _| Ok(()),
220        )
221    }
222    #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
223    {
224        let _ = (path, options, produce, admit);
225        Err(NamedWriteError::before(std::io::Error::new(
226            std::io::ErrorKind::Unsupported,
227            "durable atomic file publication is unsupported",
228        )))
229    }
230}
231
232#[cfg(all(
233    test,
234    any(target_os = "linux", target_os = "android", target_vendor = "apple")
235))]
236mod tests;