Skip to main content

ic_host_fs/scratch/
mod.rs

1//! Private retained and scoped invocation files without durable publication.
2
3#[cfg(unix)]
4mod temporary;
5#[cfg(unix)]
6pub use temporary::TemporaryFile;
7
8#[cfg(unix)]
9use std::{
10    fs::{self, File, OpenOptions},
11    io::Write as _,
12    os::unix::fs::OpenOptionsExt as _,
13    sync::atomic::{AtomicU64, Ordering},
14};
15use std::{
16    io,
17    path::{Path, PathBuf},
18};
19
20#[cfg(all(test, unix))]
21mod tests;
22
23#[cfg(unix)]
24static SEQUENCE: AtomicU64 = AtomicU64::new(0);
25
26/// Private scratch creation failed, retaining the original IO and cleanup causes.
27#[derive(Debug, thiserror::Error)]
28pub enum PrivateBytesError {
29    /// Validation, parent resolution or opening failed before creating a file.
30    #[error("private scratch creation failed")]
31    BeforeCreation(#[source] io::Error),
32    /// A newly created file could not be written completely.
33    #[error("private scratch write{} failed", if .cleanup.is_some() { " and cleanup" } else { "" })]
34    Write {
35        /// Absolute candidate path for caller-owned recovery if cleanup failed.
36        path: PathBuf,
37        /// Original write failure, which is also this error's source.
38        source: io::Error,
39        /// Failed removal after closing the file; `None` means removal succeeded.
40        cleanup: Option<io::Error>,
41    },
42}
43
44/// Create a complete, closed private invocation file under an existing parent.
45///
46/// The caller selects the bytes, their budget, filename prefix and nonzero maximum
47/// creation attempts. The prefix is one normal filename component without NUL,
48/// CR or LF. Parent aliases are resolved before allocation, making the result
49/// absolute. Each attempt uses create-new and Unix mode 0600 subject to umask;
50/// occupied files, symlinks and directories are preserved and consume one attempt.
51/// Only an occupied, uncreated candidate is retried. Writes are never retried by
52/// allocating another file. The file is closed before returning or removing a
53/// failed partial write; both write and cleanup failures remain inspectable.
54///
55/// This performs no disk synchronization. Successful files have no automatic
56/// cleanup: callers own their namespace, retention and remote-effect recovery.
57/// They must exclude concurrent replacement of the parent and candidate names;
58/// canonicalizing a parent is not hostile-directory confinement. Platforms
59/// without the Unix privacy contract fail before creation.
60///
61/// # Errors
62/// Returns [`PrivateBytesError::BeforeCreation`] for invalid requests, unsupported
63/// platforms, parent/open failures and exhausted collisions. A partial write
64/// returns [`PrivateBytesError::Write`], including any failed removal.
65pub fn create_private_bytes(
66    parent: &Path,
67    prefix: &str,
68    bytes: &[u8],
69    attempts: usize,
70) -> Result<PathBuf, PrivateBytesError> {
71    #[cfg(unix)]
72    {
73        create_with_sequence(parent, prefix, bytes, attempts, || {
74            SEQUENCE.fetch_add(1, Ordering::Relaxed)
75        })
76    }
77    #[cfg(not(unix))]
78    {
79        let _ = (parent, prefix, bytes, attempts);
80        Err(PrivateBytesError::BeforeCreation(io::Error::new(
81            io::ErrorKind::Unsupported,
82            "private scratch files require Unix permissions",
83        )))
84    }
85}
86
87#[cfg(unix)]
88fn create_with_sequence(
89    parent: &Path,
90    prefix: &str,
91    bytes: &[u8],
92    attempts: usize,
93    mut sequence: impl FnMut() -> u64,
94) -> Result<PathBuf, PrivateBytesError> {
95    if attempts == 0 {
96        return Err(PrivateBytesError::BeforeCreation(io::Error::new(
97            io::ErrorKind::InvalidInput,
98            "invalid private scratch request",
99        )));
100    }
101    validate_prefix(prefix).map_err(PrivateBytesError::BeforeCreation)?;
102    let parent = parent
103        .canonicalize()
104        .map_err(PrivateBytesError::BeforeCreation)?;
105    for _ in 0..attempts {
106        let path = parent.join(format!("{prefix}-{}-{}", std::process::id(), sequence()));
107        match OpenOptions::new()
108            .write(true)
109            .create_new(true)
110            .mode(0o600)
111            .open(&path)
112        {
113            Ok(file) => {
114                return finish_candidate(
115                    file,
116                    path,
117                    |file| file.write_all(bytes),
118                    |path| fs::remove_file(path),
119                );
120            }
121            Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
122            Err(source) => return Err(PrivateBytesError::BeforeCreation(source)),
123        }
124    }
125    Err(PrivateBytesError::BeforeCreation(io::Error::new(
126        io::ErrorKind::AlreadyExists,
127        "private scratch creation attempts exhausted",
128    )))
129}
130
131#[cfg(unix)]
132fn validate_prefix(prefix: &str) -> io::Result<()> {
133    use std::path::Component;
134    let mut components = Path::new(prefix).components();
135    if prefix.contains(['\0', '\r', '\n'])
136        || !matches!(
137            (components.next(), components.next()),
138            (Some(Component::Normal(name)), None) if name == std::ffi::OsStr::new(prefix)
139        )
140    {
141        return Err(io::Error::new(
142            io::ErrorKind::InvalidInput,
143            "invalid private scratch request",
144        ));
145    }
146    Ok(())
147}
148
149#[cfg(unix)]
150fn finish_candidate(
151    mut file: File,
152    path: PathBuf,
153    write: impl FnOnce(&mut File) -> io::Result<()>,
154    remove: impl FnOnce(&Path) -> io::Result<()>,
155) -> Result<PathBuf, PrivateBytesError> {
156    let result = write(&mut file);
157    drop(file);
158    match result {
159        Ok(()) => Ok(path),
160        Err(source) => {
161            let cleanup = remove(&path).err();
162            Err(PrivateBytesError::Write {
163                path,
164                source,
165                cleanup,
166            })
167        }
168    }
169}