1use crate::random_id::RandomId;
4use std::{
5 env,
6 fs::{self, Metadata},
7 io::{self, ErrorKind},
8 os::unix::fs::MetadataExt,
9 path::{Path, PathBuf},
10};
11use tracing::debug;
12
13pub(crate) fn remove_file(path: &Path) -> io::Result<()> {
15 match fs::remove_file(path) {
16 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
17 result => result,
18 }
19}
20
21#[derive(Debug)]
23pub(crate) struct TempFile(PathBuf);
24
25impl TempFile {
26 pub(crate) fn new(directory: Option<&Path>, prefix: &str, suffix: &str) -> Self {
31 let dir = directory.map_or_else(env::temp_dir, Path::to_path_buf);
32 let unique = RandomId::new();
33 Self(dir.join(format!("{prefix}{unique}{suffix}")))
34 }
35
36 pub(crate) fn path(&self) -> &Path {
38 &self.0
39 }
40}
41
42impl Drop for TempFile {
43 fn drop(&mut self) {
44 if let Err(e) = remove_file(&self.0) {
49 debug!("Unable to remove temp file {}: {}", self.0.display(), e);
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub(crate) struct FileId {
59 dev: u64,
60 ino: u64,
61}
62
63impl FileId {
64 pub(crate) fn new(metadata: &Metadata) -> Self {
66 Self {
67 dev: metadata.dev(),
68 ino: metadata.ino(),
69 }
70 }
71
72 pub(crate) fn is_at(self, path: &Path) -> bool {
75 fs::symlink_metadata(path).is_ok_and(|m| Self::new(&m) == self)
76 }
77
78 pub(crate) fn remove_at(self, path: &Path) -> Result<(), RemoveError> {
81 match fs::symlink_metadata(path) {
82 Ok(m) if Self::new(&m) == self => fs::remove_file(path).map_err(RemoveError::Remove),
83 Ok(_) => Ok(()),
84 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
85 Err(e) => Err(RemoveError::Stat(e)),
86 }
87 }
88}
89
90#[derive(Debug)]
92pub(crate) enum RemoveError {
93 Stat(io::Error),
95
96 Remove(io::Error),
98}
99
100pub(crate) fn check_absolute(what: &str, path: &Path) -> anyhow::Result<()> {
103 if !path.is_absolute() {
104 anyhow::bail!("{what} {:?} is not absolute", path.display())
105 }
106 Ok(())
107}
108
109pub(crate) fn check_file_path(what: &str, path: &Path) -> anyhow::Result<()> {
116 use std::os::unix::ffi::OsStrExt as _;
117
118 check_absolute(what, path)?;
119 let bytes = path.as_os_str().as_bytes();
120 if path.file_name().is_none() || bytes.ends_with(b"/") || bytes.ends_with(b"/.") {
121 anyhow::bail!("{what} {:?} does not name a file", path.display())
122 }
123 Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn missing_files_count_as_removed() -> io::Result<()> {
132 let dir = tempfile::tempdir()?;
133 let path = dir.path().join("file");
134 remove_file(&path)?;
135 fs::write(&path, "")?;
136 remove_file(&path)?;
137 assert!(!path.exists());
138 assert!(remove_file(dir.path()).is_err());
139 Ok(())
140 }
141
142 #[test]
143 fn temp_files_get_removed_on_drop() -> io::Result<()> {
144 let dir = tempfile::tempdir()?;
145 let file = TempFile::new(Some(dir.path()), "prefix-", ".suffix");
146 let path = file.path().to_path_buf();
147 let name = path.file_name().and_then(|name| name.to_str());
148 assert!(name.is_some_and(|n| n.starts_with("prefix-") && n.ends_with(".suffix")));
149 assert_eq!(path.parent(), Some(dir.path()));
150 assert_ne!(
151 TempFile::new(Some(dir.path()), "prefix-", ".suffix").path(),
152 path
153 );
154
155 assert!(!path.exists());
157 fs::write(&path, "")?;
158 drop(file);
159 assert!(!path.exists());
160
161 let file = TempFile::new(None, "prefix-", ".suffix");
163 assert_eq!(file.path().parent(), Some(env::temp_dir().as_path()));
164 drop(file);
165 Ok(())
166 }
167
168 #[test]
169 fn only_the_same_file_gets_removed() -> io::Result<()> {
170 let dir = tempfile::tempdir()?;
171 let path = dir.path().join("file");
172 fs::write(&path, "")?;
173 let id = FileId::new(&fs::metadata(&path)?);
174 assert!(id.is_at(&path));
175 let link = dir.path().join("link");
177 std::os::unix::fs::symlink(&path, &link)?;
178 assert!(!id.is_at(&link));
179
180 let other = dir.path().join("other");
182 fs::write(&other, "")?;
183 let _held = fs::File::open(&path)?;
184 fs::rename(&other, &path)?;
185 assert!(!id.is_at(&path));
186 id.remove_at(&path).expect("another file stays");
187 assert!(path.exists());
188
189 let id = FileId::new(&fs::metadata(&path)?);
190 id.remove_at(&path).expect("remove the file");
191 assert!(!path.exists());
192 id.remove_at(&path)
193 .expect("a missing file counts as removed");
194
195 let under_file = path.join("file");
197 fs::write(&path, "")?;
198 let id = FileId::new(&fs::metadata(&path)?);
199 assert!(matches!(
200 id.remove_at(&under_file),
201 Err(RemoveError::Stat(_))
202 ));
203 Ok(())
204 }
205
206 #[test]
207 fn paths_have_to_be_absolute() {
208 assert!(check_absolute("bundle path", Path::new("/bundle")).is_ok());
209 for path in ["", "bundle", "./bundle"] {
210 let err = check_absolute("bundle path", Path::new(path)).unwrap_err();
211 assert!(err.to_string().contains("is not absolute"), "{err}");
212 }
213 }
214
215 #[test]
216 fn file_paths_have_to_name_a_file() {
217 for path in [
218 "/exit",
219 "/dir/exit",
220 "/dir/./exit",
221 "/dir/.exit",
222 "/dir/exit.",
223 ] {
224 assert!(check_file_path("exit path", Path::new(path)).is_ok());
225 }
226 for path in ["/", "/dir/..", "/..", "/dir/exit/", "/dir/exit/."] {
228 let err = check_file_path("exit path", Path::new(path)).unwrap_err();
229 assert_eq!(
230 err.to_string(),
231 format!("exit path {path:?} does not name a file")
232 );
233 }
234 let err = check_file_path("exit path", Path::new("exit")).unwrap_err();
235 assert!(err.to_string().contains("is not absolute"), "{err}");
236 }
237}