ic_host_fs/scratch/
mod.rs1#[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#[derive(Debug, thiserror::Error)]
28pub enum PrivateBytesError {
29 #[error("private scratch creation failed")]
31 BeforeCreation(#[source] io::Error),
32 #[error("private scratch write{} failed", if .cleanup.is_some() { " and cleanup" } else { "" })]
34 Write {
35 path: PathBuf,
37 source: io::Error,
39 cleanup: Option<io::Error>,
41 },
42}
43
44pub 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}