Skip to main content

xianyun_executor/
tempfile.rs

1//! Temporary files that delete themselves on close.
2//!
3//! Ported from `internal/tempfile/tempfile.go`. Not yet implemented.
4
5use std::fs::{File, OpenOptions};
6use std::io::{Seek, SeekFrom, Write};
7use std::path::{Path, PathBuf};
8
9/// Ported from `Tempfile`.
10pub struct Tempfile {
11    f: File,
12    path: PathBuf,
13}
14
15impl Tempfile {
16    /// Ported from `New`. Replicates Go's `os.CreateTemp` behaviour: `dir`
17    /// empty means the system temp dir, and a single `*` in `pattern` is
18    /// replaced by a random suffix (with the file created exclusively,
19    /// retrying on collision).
20    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    /// Ported from `Tempfile.Name`.
56    pub fn name(&self) -> &Path {
57        &self.path
58    }
59
60    /// Ported from `Tempfile.Overwrite`.
61    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    /// Ported from `Tempfile.Close`. Go exposes this as an explicit
82    /// `io.Closer` the caller must call; Rust's `Drop` makes cleanup
83    /// automatic on scope exit, which is the idiomatic replacement (no
84    /// caller can "forget" to close it, unlike Go).
85    fn drop(&mut self) {
86        // Intentionally not propagating errors: panicking in `Drop` during
87        // unwinding aborts the process, and Go's Close() error is
88        // typically ignored by callers anyway (defer f.Close()).
89        let _ = std::fs::remove_file(&self.path);
90    }
91}
92
93/// Ported from `EnsureTempDirExists`.
94pub 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
108/// Ported from `ensureDirExists`. `mkdir` is a parameter so tests can
109/// model the host filesystem, same as the Go version's injected `mkdir
110/// func(string, os.FileMode) error`.
111pub 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        // Never stat: mirrord answers it from the pod.
128        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    // --- Ported from tempfile_test.go ---
148
149    #[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    // --- Ported from ensure_dir_test.go ---
166
167    /// Models the host filesystem, the only place mirrord's writes land.
168    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    /// Reproduces the caproni run panic under mirrord 3.253.1.
196    #[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        // A real directory stands in for the pod's TMPDIR, so stat succeeds.
202        assert!(
203            temp_dir_path.exists(),
204            "stat must succeed, which is what makes MkdirAll return early in Go"
205        );
206
207        // The host has the root only, like /tmp without /tmp/gitlab.
208        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        // Running twice must not fail on the components it already created.
227        ensure_dir_exists(&temp_dir_path, |p| host.mkdir(p)).unwrap();
228    }
229}