temp_file/lib.rs
1//! temp-file
2//! =========
3//! [](https://crates.io/crates/temp-file)
4//! [](https://gitlab.com/leonhard-llc/ops/-/raw/main/temp-file/LICENSE)
5//! [](https://github.com/rust-secure-code/safety-dance/)
6//! [](https://gitlab.com/leonhard-llc/ops/-/pipelines)
7//!
8//! Provides a `TempFile` struct.
9//!
10//! # Features
11//! - Makes a file in a system temporary directory
12//! - Deletes the file on drop
13//! - Optional name prefix, name suffix, contents, and directory.
14//! - Depends only on `std`
15//! - `forbid(unsafe_code)`
16//! - 100% test coverage
17//!
18//! # Limitations
19//! - Not security-hardened. See
20//! [Secure Programming for Linux and Unix HOWTO - 7.10. Avoid Race Conditions](https://tldp.org/HOWTO/Secure-Programs-HOWTO/avoid-race.html)
21//! and [`mkstemp`](https://linux.die.net/man/3/mkstemp).
22//! - Not tested on [Windows](https://gitlab.com/leonhard-llc/ops/-/issues/6).
23//!
24//! # Alternatives
25//! - [`tempfile`](https://crates.io/crates/tempfile)
26//! - Popular and mature
27//! - Supports some security-sensitive use cases
28//! - Contains `unsafe`, dependencies full of `unsafe`
29//! - Heavy dependencies (libc, winapi, rand, etc.)
30//! - [`test-temp-file`](https://crates.io/crates/test-temp-file)
31//! - Depends on crates which contain `unsafe`
32//! - Incomplete documentation
33//! - [`temp_file_name`](https://crates.io/crates/temp_file_name)
34//! - Does not delete file
35//! - Usage is not straightforward. Missing example.
36//! - [`mktemp`](https://crates.io/crates/mktemp)
37//! - Sets file mode 0600 on unix
38//! - Contains `unsafe`
39//! - No readme or online docs
40//!
41//! # Related Crates
42//! - [`temp-dir`](https://crates.io/crates/temp-dir)
43//!
44//! # Example
45//! ```rust
46//! let t = temp_file::with_contents(b"abc");
47//! // Prints "/tmp/1a9b0".
48//! println!("{:?}", t.path());
49//! assert_eq!(
50//! "abc",
51//! std::fs::read_to_string(t.path()).unwrap(),
52//! );
53//! // Prints "/tmp/1a9b1".
54//! println!("{:?}", temp_file::empty().path());
55//! ```
56//!
57//! # Cargo Geiger Safety Report
58//! # Changelog
59//! - v0.1.9 - `AsRef<Path>`
60//! - v0.1.8 - Work when the file already exists.
61//! - v0.1.7 - Add `in_dir`, `with_suffix`, and `TempFileBuilder`.
62//! - v0.1.6
63//! - Return `std::io::Error` instead of `String`.
64//! - Add
65//! [`cleanup`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.cleanup).
66//! - v0.1.5 - Increase test coverage
67//! - v0.1.4 - Add
68//! [`leak`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.leak)
69//! and
70//! [`panic_on_cleanup_error`](https://docs.rs/temp-file/latest/temp_file/struct.TempFile.html#method.panic_on_cleanup_error).
71//! - v0.1.3 - Update docs
72//! - v0.1.2 - Update example
73//! - v0.1.1 - Minor code cleanup, update docs
74//! - v0.1.0 - Initial version
75#![forbid(unsafe_code)]
76// TODO(mleonhard) Implement features requested of `tempfile` crate:
77// https://github.com/Stebalien/tempfile/issues
78use core::sync::atomic::{AtomicU32, Ordering};
79use std::io::ErrorKind;
80use std::path::{Path, PathBuf};
81use std::sync::atomic::AtomicBool;
82
83#[doc(hidden)]
84pub static INTERNAL_COUNTER: AtomicU32 = AtomicU32::new(0);
85#[doc(hidden)]
86pub static INTERNAL_RETRY: AtomicBool = AtomicBool::new(true);
87
88pub struct TempFileBuilder {
89 dir_path: Option<PathBuf>,
90 prefix: Option<String>,
91 suffix: Option<String>,
92}
93impl TempFileBuilder {
94 #[allow(clippy::new_without_default)]
95 #[must_use]
96 pub fn new() -> Self {
97 Self {
98 dir_path: None,
99 prefix: None,
100 suffix: None,
101 }
102 }
103
104 #[must_use]
105 pub fn in_dir(mut self, p: impl AsRef<Path>) -> Self {
106 self.dir_path = Some(p.as_ref().to_path_buf());
107 self
108 }
109
110 #[must_use]
111 pub fn prefix(mut self, s: impl AsRef<str>) -> Self {
112 self.prefix = Some(s.as_ref().to_string());
113 self
114 }
115
116 #[must_use]
117 pub fn suffix(mut self, s: impl AsRef<str>) -> Self {
118 self.suffix = Some(s.as_ref().to_string());
119 self
120 }
121
122 /// Creates the temp file.
123 ///
124 /// # Errors
125 /// Returns `Err` when it fails to create the file.
126 pub fn build(self) -> Result<TempFile, std::io::Error> {
127 TempFile::internal_new(
128 self.dir_path.as_deref(),
129 self.prefix.as_ref().map(AsRef::as_ref),
130 self.suffix.as_ref().map(AsRef::as_ref),
131 )
132 }
133}
134
135/// The path of an existing writable file in a system temporary directory.
136///
137/// Deletes the file on drop. Ignores errors deleting the file.
138///
139/// # Example
140/// ```rust
141/// use temp_file::TempFile;
142/// let t = TempFile::new()
143/// .unwrap()
144/// .with_contents(b"abc")
145/// .unwrap();
146/// // Prints "/tmp/1a9b0".
147/// println!("{:?}", t.path());
148/// assert_eq!(
149/// "abc",
150/// std::fs::read_to_string(t.path()).unwrap(),
151/// );
152/// // Prints "/tmp/1a9b1".
153/// println!("{:?}", TempFile::new().unwrap().path());
154/// ```
155#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)]
156pub struct TempFile {
157 path_buf: PathBuf,
158 delete_on_drop: bool,
159 panic_on_delete_err: bool,
160}
161impl TempFile {
162 fn internal_new(
163 dir: Option<&Path>,
164 prefix: Option<&str>,
165 suffix: Option<&str>,
166 ) -> Result<Self, std::io::Error> {
167 let dir = dir.map_or_else(std::env::temp_dir, Path::to_path_buf);
168 loop {
169 let filename = format!(
170 "{}{:x}{:x}{}",
171 prefix.unwrap_or(""),
172 std::process::id(),
173 INTERNAL_COUNTER.fetch_add(1, Ordering::AcqRel),
174 suffix.unwrap_or(""),
175 );
176 let file_path = dir.join(filename);
177 let mut open_opts = std::fs::OpenOptions::new();
178 open_opts.create_new(true);
179 open_opts.write(true);
180 match open_opts.open(&file_path) {
181 Err(e)
182 if e.kind() == ErrorKind::AlreadyExists
183 && INTERNAL_RETRY.load(Ordering::Acquire) => {}
184 Err(e) => {
185 return Err(std::io::Error::new(
186 e.kind(),
187 format!("error creating file {file_path:?}: {e}"),
188 ))
189 }
190 Ok(_) => {
191 return Ok(Self {
192 path_buf: file_path,
193 delete_on_drop: true,
194 panic_on_delete_err: false,
195 })
196 }
197 }
198 }
199 }
200
201 fn remove_file(path: &Path) -> Result<(), std::io::Error> {
202 match std::fs::remove_file(path) {
203 Ok(()) => Ok(()),
204 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
205 Err(e) => Err(std::io::Error::new(
206 e.kind(),
207 format!("error removing file {path:?}: {e}"),
208 )),
209 }
210 }
211
212 /// Create a new empty file in a system temporary directory.
213 ///
214 /// Drop the returned struct to delete the file.
215 ///
216 /// # Errors
217 /// Returns `Err` when it fails to create the file.
218 ///
219 /// # Example
220 /// ```rust
221 /// // Prints "/tmp/1a9b0".
222 /// println!("{:?}", temp_file::TempFile::new().unwrap().path());
223 /// ```
224 pub fn new() -> Result<Self, std::io::Error> {
225 Self::internal_new(None, None, None)
226 }
227
228 /// Create a new empty file in `dir`.
229 ///
230 /// Drop the returned struct to delete the file.
231 ///
232 /// # Errors
233 /// Returns `Err` when it fails to create the file.
234 ///
235 /// # Example
236 /// ```
237 /// // Prints "/tmp/temp_uploads/1a9b0".
238 /// let dir = std::env::temp_dir().join("temp_uploads");
239 /// # std::fs::create_dir(&dir).unwrap();
240 /// println!("{:?}", temp_file::TempFile::in_dir(&dir).unwrap().path());
241 /// # std::fs::remove_dir(&dir).unwrap();
242 /// ```
243 pub fn in_dir(dir: impl AsRef<Path>) -> Result<Self, std::io::Error> {
244 Self::internal_new(Some(dir.as_ref()), None, None)
245 }
246
247 /// Create a new empty file in a system temporary directory.
248 /// Use `prefix` as the first part of the file's name.
249 ///
250 /// Drop the returned struct to delete the file.
251 ///
252 /// # Errors
253 /// Returns `Err` when it fails to create the file.
254 ///
255 /// # Example
256 /// ```rust
257 /// // Prints "/tmp/ok1a9b0".
258 /// println!("{:?}", temp_file::TempFile::with_prefix("ok").unwrap().path());
259 /// ```
260 pub fn with_prefix(prefix: impl AsRef<str>) -> Result<Self, std::io::Error> {
261 Self::internal_new(None, Some(prefix.as_ref()), None)
262 }
263
264 /// Create a new empty file in a system temporary directory.
265 /// Use `suffix` as the last part of the file's name.
266 ///
267 /// You can use this to give the filename a particular extension.
268 ///
269 /// Drop the returned struct to delete the file.
270 ///
271 /// # Errors
272 /// Returns `Err` when it fails to create the file.
273 ///
274 /// # Example
275 /// ```rust
276 /// // Prints "/tmp/1a9b0.txt".
277 /// println!("{:?}", temp_file::TempFile::with_suffix(".txt").unwrap().path());
278 /// ```
279 pub fn with_suffix(suffix: impl AsRef<str>) -> Result<Self, std::io::Error> {
280 Self::internal_new(None, None, Some(suffix.as_ref()))
281 }
282
283 /// Write `contents` to the file.
284 ///
285 /// # Errors
286 /// Returns `Err` when it fails to write all of `contents` to the file.
287 #[allow(clippy::missing_panics_doc)]
288 pub fn with_contents(self, contents: &[u8]) -> Result<Self, std::io::Error> {
289 let path = self.path_buf.as_path();
290 std::fs::write(path, contents).map_err(|e| {
291 std::io::Error::new(e.kind(), format!("error writing file {path:?}: {e}"))
292 })?;
293 Ok(self)
294 }
295
296 /// Remove the file now. Do nothing later on drop.
297 ///
298 /// # Errors
299 /// Returns an error if the file exists and we fail to remove it.
300 #[allow(clippy::missing_panics_doc)]
301 pub fn cleanup(mut self) -> Result<(), std::io::Error> {
302 let result = Self::remove_file(&self.path_buf);
303 if result.is_ok() {
304 self.delete_on_drop = false;
305 }
306 result
307 }
308
309 /// Make the struct panic on drop if it hits an error while
310 /// removing the file.
311 #[must_use]
312 pub fn panic_on_cleanup_error(mut self) -> Self {
313 self.panic_on_delete_err = true;
314 self
315 }
316
317 /// Do not delete the file.
318 ///
319 /// This is useful when debugging a test.
320 pub fn leak(mut self) {
321 self.delete_on_drop = false;
322 }
323
324 /// The path to the file.
325 #[must_use]
326 #[allow(clippy::missing_panics_doc)]
327 pub fn path(&self) -> &Path {
328 self.path_buf.as_path()
329 }
330}
331impl Drop for TempFile {
332 fn drop(&mut self) {
333 if self.delete_on_drop {
334 let result = Self::remove_file(self.path_buf.as_path());
335 if self.panic_on_delete_err {
336 if let Err(e) = result {
337 panic!("{}", e);
338 }
339 }
340 }
341 }
342}
343impl AsRef<Path> for TempFile {
344 fn as_ref(&self) -> &Path {
345 self.path()
346 }
347}
348
349/// Create a new empty file in a system temporary directory.
350///
351/// # Panics
352/// Panics if it cannot create the file.
353#[must_use]
354pub fn empty() -> TempFile {
355 TempFile::new().unwrap()
356}
357
358/// Create a new file in a system temporary directory
359/// and writes `contents` to it.
360///
361/// # Panics
362/// Panics if it fails to create the file or fails to write all of `contents`.
363#[must_use]
364pub fn with_contents(contents: &[u8]) -> TempFile {
365 TempFile::new().unwrap().with_contents(contents).unwrap()
366}