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;