outdir_tempdir/
lib.rs

1//! # OUTDIR-TEMPDIR
2//! A crate for cargo-test to create temporary directories.  
3//! The temporary directories are always created in the `OUT_DIR`.
4//!
5//! # Usage
6//! Add dependency to your `Cargo.toml`.
7//! ```toml
8//! [dev-dependencies]
9//! outdir-tempdir = "0.2"
10//! ```
11//!
12//! # Examples
13//! Create a temporary directory with automatic removal.
14//! ```no_run
15//! # use crate::*;
16//! #[test]
17//! fn test_something() {
18//!     // Create a randomly named temporary directory
19//!     // and automatically remove it upon dropping
20//!     let dir = TempDir::new().autorm();
21//!
22//!     // Get temporary directory
23//!     // (/path/to/crate/target/(debug|release)/build/outdir-tempdir-<random>/out/test-<random>)
24//!     let tempdir = dir.path();
25//!
26//!     // Test your code using `tempdir`
27//!     // ...
28//!
29//!     // Remove the temporary directory when the `dir` variable is dropped
30//! }
31//! ```
32//!
33//! Create a temporary directory without automatic removal.
34//! ```no_run
35//! # use crate::*;
36//! #[test]
37//! fn test_something() {
38//!     // Create a randomly named temporary directory
39//!     let dir = TempDir::new();
40//!
41//!     // Get temporary directory
42//!     // (/path/to/crate/target/(debug|release)/build/outdir-tempdir-<random>/out/test-<random>)
43//!     let tempdir = dir.path();
44//!
45//!     // Test your code using `tempdir`
46//!     // ...
47//!
48//!     // The temporary directory will not be deleted even when the `dir` variable is dropped
49//! }
50//! ```
51//!
52//! Create a temporary directory using the specified path.
53//! ```no_run
54//! # use crate::*;
55//! #[test]
56//! fn test_something() {
57//!     // Create a temporary directory with a specified path 'foo/bar/baz'
58//!     // and automatically remove it upon dropping
59//!     let dir = TempDir::with_path("foo/bar/baz").autorm();
60//!
61//!     // Get temporary directory
62//!     // (/path/to/crate/target/(debug|release)/build/outdir-tempdir-<random>/out/foo/bar/baz)
63//!     let tempdir = dir.path();
64//!
65//!     // Test your code using `tempdir`
66//!     // ...
67//!
68//!     // Remove the temporary directory when the `dir` variable is dropped
69//! }
70//! ```
71mod error;
72pub use crate::error::{Error, Result};
73use std::fs;
74use std::path::{Component, Path, PathBuf};
75use uuid::Uuid;
76
77/// Provides a function to creating a temporary directory that will be automatically removed upon being dropped.
78pub struct TempDir {
79    root: PathBuf,
80    target: PathBuf,
81    full: PathBuf,
82    autorm: bool,
83}
84
85impl TempDir {
86    /// Create a randomly named temporary directory.
87    ///
88    /// # Panics
89    ///
90    /// This function panics if the temporary directory cannot be created.  
91    /// (because testing cannot proceed)
92    pub fn new() -> Self {
93        TempDir::with_path(format!("test-{}", Uuid::new_v4()))
94    }
95
96    /// Create a temporary directory with a specified path.
97    ///
98    /// # Panics
99    ///
100    /// This function triggers a panic under the following conditions.  
101    /// (because testing cannot proceed)
102    ///
103    /// * Attempting to access the parent directory (which may result in escaping from `OUT_DIR`).
104    /// * Attempting to access the root directory (for the same reason).
105    /// * Specifying the current directory (which may lead to the deletion of `OUT_DIR`).
106    /// * Failing to create the temporary directory.
107    pub fn with_path<P: AsRef<Path>>(path: P) -> Self {
108        Self::with_path_safe(path).unwrap()
109    }
110
111    /// Create a temporary directory with a specified path.
112    ///
113    /// # Errors
114    ///
115    /// Attempting to access the parent directory will result in a `ParentDirContains` error, as it could lead to escaping from `OUT_DIR`.
116    /// Similarly, attempting to access the root directory will result in a `RootDirContains` error for the same reason.
117    /// If the current directory is specified, there is a potential risk of deleting `OUT_DIR`, resulting in an `InvalidPath` error.
118    /// If the temporary directory cannot be created, it will lead to an `Io` error.
119    pub fn with_path_safe<P: AsRef<Path>>(path: P) -> Result<Self> {
120        let path = path.as_ref();
121        let target = cleansing_path(path)?;
122
123        let target_root = target_root().ok_or(Error::OutDirNotFound)?;
124        let target_full_path = target_root.join(&target);
125
126        if target_root == target_full_path {
127            return Err(Error::InvalidPath(path.to_path_buf()));
128        }
129
130        fs::create_dir_all(target_full_path.as_path())?;
131
132        Ok(Self {
133            root: target_root,
134            target,
135            full: target_full_path,
136            autorm: false,
137        })
138    }
139
140    /// Enable automatically removal.
141    pub fn autorm(mut self) -> Self {
142        self.autorm = true;
143        self
144    }
145
146    /// Get path to the temporary directory.
147    pub fn path(&self) -> &Path {
148        self.full.as_path()
149    }
150}
151
152impl Drop for TempDir {
153    /// Remove the temporary directory if autorm is true.
154    fn drop(&mut self) {
155        if self.autorm {
156            if let Some(topdir) = self.target.iter().next() {
157                let rmdir = self.root.join(topdir);
158                fs::remove_dir_all(rmdir).unwrap();
159            }
160        }
161    }
162}
163
164impl Default for TempDir {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170/// Get `OUT_DIR` as temporary directory root.
171fn target_root() -> Option<PathBuf> {
172    Some(PathBuf::from(std::env!("OUT_DIR")))
173}
174
175/// Clean up the specified path.
176///
177/// # Errors
178///
179/// Attempting to access the parent directory will result in a `ParentDirContains` error, as it could lead to escaping from `OUT_DIR`.
180/// Similarly, attempting to access the root directory will result in a `RootDirContains` error for the same reason.
181fn cleansing_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
182    let path = path.as_ref();
183    let mut ret = PathBuf::new();
184    for item in path.components() {
185        match item {
186            Component::Normal(x) => ret.push(x),
187            Component::CurDir => (), // ignore
188            Component::ParentDir => return Err(Error::ParentDirContains(path.to_path_buf())),
189            Component::Prefix(_) | Component::RootDir => {
190                return Err(Error::RootDirContains(path.to_path_buf()))
191            }
192        }
193    }
194
195    Ok(ret)
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use std::path::MAIN_SEPARATOR;
202
203    #[test]
204    fn test_cleansing_path() {
205        let sep = MAIN_SEPARATOR;
206
207        // Normal check
208        let expected = PathBuf::from(format!("foo{sep}bar{sep}baz"));
209        let actual = cleansing_path("foo/bar/baz").unwrap();
210        assert_eq!(actual, expected);
211
212        #[cfg(target_os = "windows")]
213        {
214            let expected = PathBuf::from(format!("foo{sep}bar{sep}baz"));
215            let actual = cleansing_path("foo\\bar\\baz").unwrap();
216            assert_eq!(actual, expected);
217        }
218
219        // Current directory check
220        let expected = PathBuf::from(format!("tmp{sep}path"));
221        let actual = cleansing_path("./tmp/path").unwrap();
222        assert_eq!(actual, expected);
223
224        #[cfg(target_os = "windows")]
225        {
226            let expected = PathBuf::from(format!("tmp{sep}path"));
227            let actual = cleansing_path(".\\tmp\\path").unwrap();
228            assert_eq!(actual, expected);
229        }
230
231        // Root check
232        let name = "/tmp/path";
233        match cleansing_path(name) {
234            Err(Error::RootDirContains(s)) => assert_eq!(s, PathBuf::from(name)),
235            _ => panic!(),
236        }
237
238        #[cfg(target_os = "windows")]
239        {
240            let name = "C:\\tmp\\path";
241            match cleansing_path(name) {
242                Err(Error::RootDirContains(s)) => assert_eq!(s, PathBuf::from(name)),
243                _ => panic!(),
244            }
245        }
246
247        // Parent directory check
248        let name = "../tmp/path";
249        match cleansing_path(name) {
250            Err(Error::ParentDirContains(s)) => assert_eq!(s, PathBuf::from(name)),
251            _ => panic!(),
252        }
253
254        #[cfg(target_os = "windows")]
255        {
256            let name = "..\\tmp\\path";
257            match cleansing_path(name) {
258                Err(Error::ParentDirContains(s)) => assert_eq!(s, PathBuf::from(name)),
259                _ => panic!(),
260            }
261        }
262    }
263
264    #[test]
265    fn test_dir() {
266        // no auto remove dir
267        let mut rmdir = {
268            let temp = TempDir::with_path("foo/bar/baz");
269            assert!(temp.path().try_exists().unwrap());
270            assert!(temp.path().is_dir());
271            temp.path().to_path_buf()
272        };
273        assert!(rmdir.try_exists().unwrap());
274        assert!(rmdir.is_dir());
275        rmdir.pop();
276        rmdir.pop();
277        fs::remove_dir_all(&rmdir).unwrap();
278        assert!(!rmdir.try_exists().unwrap());
279
280        // auto remove dir
281        let rmdir = {
282            let temp = TempDir::with_path("foo/bar/baz").autorm();
283            assert!(temp.path().try_exists().unwrap());
284            assert!(temp.path().is_dir());
285            temp.path().to_path_buf()
286        };
287        assert!(!rmdir.try_exists().unwrap());
288    }
289}