mod_tempdir/lib.rs
1//! # mod-tempdir
2//!
3//! Temporary directory management for Rust. Auto-cleanup on Drop,
4//! collision-resistant naming, cross-platform paths.
5//!
6//! Designed as a `tempfile` replacement at MSRV 1.75. The default
7//! build has zero runtime dependencies outside `std`. An optional
8//! `mod-rand` feature swaps the built-in name mixer for
9//! `mod_rand::tier2::unique_name`, which produces a uniformly
10//! distributed name from a SplitMix + Stafford-finisher pipeline.
11//!
12//! ## Quick example
13//!
14//! ```no_run
15//! use mod_tempdir::TempDir;
16//!
17//! let dir = TempDir::new().unwrap();
18//! // ... use dir.path() to do work ...
19//! // dir is automatically deleted when it goes out of scope
20//! ```
21//!
22//! ## Feature flags
23//!
24//! * `mod-rand` (off by default): use [`mod_rand::tier2::unique_name`][mr-tier2]
25//! for directory naming. The alphabet is Crockford base32 on both
26//! paths, so any caller pattern-matching on the directory basename
27//! keeps working unchanged when the feature is toggled.
28//!
29//! [mr-tier2]: https://docs.rs/mod-rand/latest/mod_rand/tier2/fn.unique_name.html
30//!
31//! To enable in `Cargo.toml`:
32//!
33//! ```toml
34//! mod-tempdir = { version = "0.9", features = ["mod-rand"] }
35//! ```
36//!
37//! ## Cleanup semantics
38//!
39//! `Drop::drop` recursively removes the directory via
40//! [`std::fs::remove_dir_all`]. Failures during cleanup (file in use,
41//! permission denied, network filesystem hiccup) are intentionally
42//! silent: a `Drop` impl must not panic. Use [`TempDir::persist`] to
43//! keep the directory alive past drop if you need to inspect it.
44
45#![cfg_attr(docsrs, feature(doc_cfg))]
46#![warn(missing_docs)]
47#![warn(rust_2018_idioms)]
48
49use std::io;
50use std::path::{Path, PathBuf};
51
52#[cfg(not(feature = "mod-rand"))]
53use std::sync::atomic::{AtomicU64, Ordering};
54#[cfg(not(feature = "mod-rand"))]
55use std::time::{SystemTime, UNIX_EPOCH};
56
57/// A temporary directory that auto-deletes when dropped.
58///
59/// # Example
60///
61/// ```no_run
62/// use mod_tempdir::TempDir;
63///
64/// let dir = TempDir::new().unwrap();
65/// let file_path = dir.path().join("test.txt");
66/// std::fs::write(&file_path, b"hello").unwrap();
67/// // dir and its contents are deleted at end of scope
68/// ```
69#[derive(Debug)]
70pub struct TempDir {
71 path: PathBuf,
72 cleanup_on_drop: bool,
73}
74
75impl TempDir {
76 /// Create a new temporary directory in the system's temp location
77 /// (`/tmp` on Linux/macOS, `%TEMP%` on Windows).
78 ///
79 /// The directory name is a fresh 12-character Crockford base32
80 /// string prefixed with `.tmp-`. With the `mod-rand` feature
81 /// enabled, naming comes from [`mod_rand::tier2::unique_name`][mr-tier2];
82 /// without it, naming comes from an internal process-unique mixer
83 /// (PID + nanosecond clock + atomic counter).
84 ///
85 /// # Errors
86 ///
87 /// Returns the underlying [`io::Error`] from
88 /// [`std::fs::create_dir`] if the directory cannot be created.
89 ///
90 /// [mr-tier2]: https://docs.rs/mod-rand/latest/mod_rand/tier2/fn.unique_name.html
91 pub fn new() -> io::Result<Self> {
92 let name = unique_name(12);
93 let path = std::env::temp_dir().join(format!(".tmp-{name}"));
94 std::fs::create_dir(&path)?;
95 Ok(Self {
96 path,
97 cleanup_on_drop: true,
98 })
99 }
100
101 /// Create a new temporary directory with the given prefix.
102 ///
103 /// The final basename is `{prefix}-{12-char-name}`. The prefix is
104 /// joined verbatim and is the caller's responsibility to sanitize.
105 ///
106 /// # Errors
107 ///
108 /// Returns the underlying [`io::Error`] from
109 /// [`std::fs::create_dir`] if the directory cannot be created.
110 ///
111 /// # Example
112 ///
113 /// ```no_run
114 /// use mod_tempdir::TempDir;
115 ///
116 /// let dir = TempDir::with_prefix("my-app").unwrap();
117 /// assert!(dir
118 /// .path()
119 /// .file_name()
120 /// .unwrap()
121 /// .to_string_lossy()
122 /// .starts_with("my-app-"));
123 /// ```
124 pub fn with_prefix(prefix: &str) -> io::Result<Self> {
125 let name = unique_name(12);
126 let path = std::env::temp_dir().join(format!("{prefix}-{name}"));
127 std::fs::create_dir(&path)?;
128 Ok(Self {
129 path,
130 cleanup_on_drop: true,
131 })
132 }
133
134 /// Return the path of this temporary directory.
135 pub fn path(&self) -> &Path {
136 &self.path
137 }
138
139 /// Consume this `TempDir` and return the path, disabling cleanup
140 /// on drop. The directory and its contents will persist.
141 ///
142 /// Use this when you want to inspect contents after a test fails.
143 pub fn persist(mut self) -> PathBuf {
144 self.cleanup_on_drop = false;
145 self.path.clone()
146 }
147
148 /// Return `true` if the directory will be deleted on drop.
149 pub fn cleanup_on_drop(&self) -> bool {
150 self.cleanup_on_drop
151 }
152}
153
154impl Drop for TempDir {
155 fn drop(&mut self) {
156 if self.cleanup_on_drop {
157 // Cleanup is best-effort and must not panic in Drop. Any
158 // filesystem error (file in use, permission denied) is
159 // intentionally swallowed per REPS section 5.
160 let _ = std::fs::remove_dir_all(&self.path);
161 }
162 }
163}
164
165#[cfg(feature = "mod-rand")]
166#[inline]
167fn unique_name(len: usize) -> String {
168 mod_rand::tier2::unique_name(len)
169}
170
171#[cfg(not(feature = "mod-rand"))]
172fn unique_name(len: usize) -> String {
173 static COUNTER: AtomicU64 = AtomicU64::new(0);
174 const ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
175
176 let pid = std::process::id() as u64;
177 let nanos = SystemTime::now()
178 .duration_since(UNIX_EPOCH)
179 .map(|d| d.as_nanos() as u64)
180 .unwrap_or(0);
181 let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
182
183 // Placeholder mixing. The `mod-rand` feature replaces this entire
184 // function with `mod_rand::tier2::unique_name`.
185 let mut state = pid.wrapping_mul(0x9E3779B97F4A7C15)
186 ^ nanos.wrapping_mul(0xBF58476D1CE4E5B9)
187 ^ counter.wrapping_mul(0x94D049BB133111EB);
188
189 let mut out = String::with_capacity(len);
190 while out.len() < len {
191 out.push(ALPHABET[(state & 31) as usize] as char);
192 state >>= 5;
193 if state == 0 {
194 state = nanos.wrapping_mul(counter.wrapping_add(1));
195 }
196 }
197 out
198}
199
200/// Internal test hook. **Not part of the stable public API.** This
201/// symbol exists only to let this crate's integration tests exercise
202/// the name generator without paying for a filesystem syscall per
203/// sample. External code must not call it; it may be renamed or
204/// removed in any release, including a patch.
205///
206/// Only compiled when the `mod-rand` feature is enabled, since that is
207/// the only test file that needs it.
208#[cfg(feature = "mod-rand")]
209#[doc(hidden)]
210pub fn __unique_name_for_tests(len: usize) -> String {
211 unique_name(len)
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn creates_dir() {
220 let dir = TempDir::new().unwrap();
221 assert!(dir.path().exists());
222 assert!(dir.path().is_dir());
223 }
224
225 #[test]
226 fn auto_cleanup() {
227 let path = {
228 let dir = TempDir::new().unwrap();
229 dir.path().to_path_buf()
230 };
231 // After drop, the directory should no longer exist.
232 assert!(!path.exists());
233 }
234
235 #[test]
236 fn persist_disables_cleanup() {
237 let dir = TempDir::new().unwrap();
238 let path = dir.persist();
239 assert!(path.exists());
240 // Clean up manually since persist was used.
241 std::fs::remove_dir_all(&path).unwrap();
242 }
243
244 #[test]
245 fn with_prefix_works() {
246 let dir = TempDir::with_prefix("test").unwrap();
247 let name = dir.path().file_name().unwrap().to_string_lossy();
248 assert!(name.starts_with("test-"));
249 }
250
251 #[test]
252 fn two_dirs_unique() {
253 let a = TempDir::new().unwrap();
254 let b = TempDir::new().unwrap();
255 assert_ne!(a.path(), b.path());
256 }
257}