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}