xianyun_executor/
tempfile.rs1use std::fs::{File, OpenOptions};
6use std::io::{Seek, SeekFrom, Write};
7use std::path::{Path, PathBuf};
8
9pub struct Tempfile {
11 f: File,
12 path: PathBuf,
13}
14
15impl Tempfile {
16 pub fn new(dir: &str, pattern: &str) -> Result<Self, std::io::Error> {
21 let base = if dir.is_empty() {
22 std::env::temp_dir()
23 } else {
24 PathBuf::from(dir)
25 };
26
27 let (prefix, suffix) = match pattern.find('*') {
28 Some(idx) => (&pattern[..idx], &pattern[idx + 1..]),
29 None => (pattern, ""),
30 };
31
32 for _ in 0..10_000 {
33 let random = format!("{:x}", rand_suffix());
34 let candidate = base.join(format!("{prefix}{random}{suffix}"));
35
36 match OpenOptions::new()
37 .read(true)
38 .write(true)
39 .create_new(true)
40 .open(&candidate)
41 {
42 Ok(f) => {
43 return Ok(Self { f, path: candidate });
44 }
45 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
46 Err(e) => return Err(e),
47 }
48 }
49
50 Err(std::io::Error::other(
51 "creating tempfile: exhausted retries",
52 ))
53 }
54
55 pub fn name(&self) -> &Path {
57 &self.path
58 }
59
60 pub fn overwrite(&mut self, content: &[u8]) -> Result<(), std::io::Error> {
62 self.f.set_len(0)?;
63 self.f.seek(SeekFrom::Start(0))?;
64 self.f.write_all(content)?;
65 self.f.sync_all()?;
66 Ok(())
67 }
68}
69
70fn rand_suffix() -> u64 {
71 use std::time::{SystemTime, UNIX_EPOCH};
72 let nanos = SystemTime::now()
73 .duration_since(UNIX_EPOCH)
74 .unwrap_or_default()
75 .as_nanos();
76 let pid = u64::from(std::process::id());
77 (nanos as u64) ^ pid.wrapping_mul(0x9E37_79B9_7F4A_7C15)
78}
79
80impl Drop for Tempfile {
81 fn drop(&mut self) {
86 let _ = std::fs::remove_file(&self.path);
90 }
91}
92
93pub fn ensure_temp_dir_exists() -> Result<PathBuf, std::io::Error> {
95 let temp_dir = std::env::temp_dir();
96 ensure_dir_exists(&temp_dir, |p| {
97 std::fs::create_dir(p).or_else(|e| {
98 if e.kind() == std::io::ErrorKind::AlreadyExists {
99 Ok(())
100 } else {
101 Err(e)
102 }
103 })
104 })?;
105 Ok(temp_dir)
106}
107
108pub fn ensure_dir_exists(
112 dir: &Path,
113 mut mkdir: impl FnMut(&Path) -> Result<(), std::io::Error>,
114) -> Result<(), std::io::Error> {
115 let mut path = PathBuf::new();
116
117 for component in dir.components() {
118 match component {
119 std::path::Component::RootDir => {
120 path.push(std::path::MAIN_SEPARATOR_STR);
121 continue;
122 }
123 std::path::Component::Normal(part) => path.push(part),
124 _ => continue,
125 }
126
127 match mkdir(&path) {
129 Ok(()) => {}
130 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
131 Err(e) => {
132 return Err(std::io::Error::other(format!(
133 "creating {}: {e}",
134 path.display()
135 )));
136 }
137 }
138 }
139
140 Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
150 fn new_creates_and_deletes_on_drop() {
151 let mut f = Tempfile::new("", "caproni-test-*").unwrap();
152
153 assert!(f.name().to_string_lossy().contains("caproni-test-"));
154
155 f.overwrite(b"woof").unwrap();
156
157 let b = std::fs::read(f.name()).unwrap();
158 assert_eq!(b, b"woof");
159
160 let path = f.name().to_path_buf();
161 drop(f);
162 assert!(!path.exists());
163 }
164
165 struct HostFs {
169 dirs: std::collections::HashSet<PathBuf>,
170 }
171
172 impl HostFs {
173 fn mkdir(&mut self, path: &Path) -> Result<(), std::io::Error> {
174 if self.dirs.contains(path) {
175 return Err(std::io::Error::new(
176 std::io::ErrorKind::AlreadyExists,
177 "exists",
178 ));
179 }
180
181 let parent = path.parent().unwrap_or(Path::new("/"));
182 if !self.dirs.contains(parent) {
183 return Err(std::io::Error::new(
184 std::io::ErrorKind::NotFound,
185 "parent missing",
186 ));
187 }
188
189 self.dirs.insert(path.to_path_buf());
190
191 Ok(())
192 }
193 }
194
195 #[test]
197 fn ensure_dir_exists_when_stat_is_answered_by_the_pod() {
198 let tmp_dir = tempfile::tempdir().unwrap();
199 let temp_dir_path = tmp_dir.path().to_path_buf();
200
201 assert!(
203 temp_dir_path.exists(),
204 "stat must succeed, which is what makes MkdirAll return early in Go"
205 );
206
207 let mut host = HostFs {
209 dirs: std::collections::HashSet::from([PathBuf::from("/")]),
210 };
211
212 let err = host.mkdir(&temp_dir_path.join("caproni-noop-1"));
213 assert!(
214 err.is_err(),
215 "a temp dir cannot be created below a missing parent"
216 );
217
218 ensure_dir_exists(&temp_dir_path, |p| host.mkdir(p)).unwrap();
219 assert!(
220 host.dirs.contains(&temp_dir_path),
221 "TMPDIR must exist on the host, not just in the pod"
222 );
223
224 host.mkdir(&temp_dir_path.join("caproni-noop-2")).unwrap();
225
226 ensure_dir_exists(&temp_dir_path, |p| host.mkdir(p)).unwrap();
228 }
229}