1#![doc = include_str!("../README.md")]
2
3use std::{
4 env,
5 fs::File,
6 io::Write,
7 path::{Path, PathBuf},
8};
9
10use path_clean::PathClean;
11use rand::{distributions::Alphanumeric, thread_rng, Rng};
12
13#[derive(Debug)]
16pub struct TempDirectory {
17 path: PathBuf,
18 delete_on_drop: bool,
19}
20
21impl TempDirectory {
22 #[must_use]
23 #[allow(clippy::missing_const_for_fn)]
24 pub fn path(&self) -> &Path {
25 &self.path
26 }
27}
28
29#[derive(Debug, thiserror::Error)]
31pub enum BuildError {
32 #[error("Failed to create the root directory '{0}': {1}")]
33 FailedToCreateRootDirectory(PathBuf, std::io::Error),
34 #[error("Failed to create directory '{0}': {1}")]
35 FailedToCreateDirectory(PathBuf, std::io::Error),
36 #[error("Failed to delete directory '{0}': {1}")]
37 FailedToDeleteDirectory(PathBuf, std::io::Error),
38 #[error("Failed to create file '{0}': {1}")]
39 FailedToCreateFile(PathBuf, std::io::Error),
40 #[error("Failed to read source file '{0}': {1}")]
41 FailedToCopyFile(PathBuf, std::io::Error),
42 #[error("Failed to write file '{0}': {1}")]
43 FailedToWriteFile(PathBuf, std::io::Error),
44 #[error("The entry '{0}' is outside the temporary directory")]
45 EntryOutsideDirectory(PathBuf),
46 #[error("The entry {0} has an empty name")]
47 EmptyEntryName(usize),
48 #[error("The entry '{0}' is already existing")]
49 DuplicateEntry(PathBuf),
50}
51
52#[derive(Debug)]
71pub struct TempDirectoryBuilder {
72 root: PathBuf,
74 entries: Vec<Entry>,
76 delete_on_drop: bool,
78}
79
80impl Default for TempDirectoryBuilder {
81 fn default() -> Self {
83 Self {
84 entries: vec![],
85 root: random_temp_directory(),
86 delete_on_drop: true,
87 }
88 }
89}
90
91impl Drop for TempDirectory {
92 fn drop(&mut self) {
93 if self.delete_on_drop {
94 let _ = std::fs::remove_dir_all(&self.path);
95 }
96 }
97}
98
99impl TempDirectoryBuilder {
100 #[must_use]
103 pub fn root_folder(mut self, dir: impl AsRef<Path>) -> Self {
104 self.root = dir.as_ref().to_path_buf();
105 self
106 }
107
108 #[must_use]
111 pub const fn delete_on_drop(mut self, delete_on_drop: bool) -> Self {
112 self.delete_on_drop = delete_on_drop;
113 self
114 }
115
116 #[must_use]
117 fn add(mut self, path: impl AsRef<Path>, kind: Kind) -> Self {
118 self.entries.push(Entry {
119 path: path.as_ref().to_path_buf(),
120 kind,
121 });
122 self
123 }
124
125 #[must_use]
129 pub fn add_empty_file<P: AsRef<Path>>(self, path: P) -> Self {
130 self.add(path, Kind::EmptyFile)
131 }
132
133 #[must_use]
137 pub fn add_directory(self, path: impl AsRef<Path>) -> Self {
138 self.add(path, Kind::Directory)
139 }
140
141 #[must_use]
146 #[allow(clippy::needless_pass_by_value)]
147 pub fn add_text_file(self, path: impl AsRef<Path>, text: impl ToString) -> Self {
148 self.add(path, Kind::TextFile(text.to_string()))
149 }
150
151 #[must_use]
156 pub fn add_binary_file(self, path: impl AsRef<Path>, content: &[u8]) -> Self {
157 self.add(path, Kind::BinaryFile(content.to_vec()))
158 }
159
160 #[must_use]
165 pub fn add_file(self, path: impl AsRef<Path>, file: impl AsRef<Path>) -> Self {
166 self.add(path, Kind::FileToCopy(file.as_ref().to_path_buf()))
167 }
168
169 pub fn build(&self) -> Result<TempDirectory, BuildError> {
175 if !self.root.exists() {
176 std::fs::create_dir_all(&self.root)
177 .map_err(|err| BuildError::FailedToCreateRootDirectory(self.root.clone(), err))?;
178 }
179
180 for (entry_index, entry) in self.entries.iter().enumerate() {
181 if entry.path.as_os_str().is_empty() {
182 return Err(BuildError::EmptyEntryName(entry_index));
183 }
184
185 let entry_path = self.root.join(&entry.path).clean();
186
187 if !entry_path.starts_with(&self.root) {
188 return Err(BuildError::EntryOutsideDirectory(entry.path.clone()));
189 }
190
191 if entry_path.exists() {
192 return Err(BuildError::DuplicateEntry(entry_path));
193 }
194
195 if let Some(parent_dir) = Path::new(&entry_path).parent() {
196 std::fs::create_dir_all(parent_dir).map_err(|err| {
197 BuildError::FailedToCreateDirectory(parent_dir.to_path_buf(), err)
198 })?;
199 }
200
201 match &entry.kind {
202 Kind::Directory => {
203 std::fs::create_dir(&entry_path)
204 .map_err(|err| BuildError::FailedToCreateDirectory(entry_path, err))?;
205 }
206 Kind::EmptyFile => {
207 File::create(&entry_path)
208 .map_err(|err| BuildError::FailedToCreateFile(entry_path, err))?;
209 }
210 Kind::TextFile(text) => {
211 let mut new_file = File::create(&entry_path)
212 .map_err(|err| BuildError::FailedToCreateFile(entry_path.clone(), err))?;
213
214 new_file
215 .write_all(text.as_bytes())
216 .map_err(|err| BuildError::FailedToWriteFile(entry_path, err))?;
217 }
218 Kind::BinaryFile(bytes) => {
219 let mut new_file = File::create(&entry_path)
220 .map_err(|err| BuildError::FailedToCreateFile(entry_path.clone(), err))?;
221
222 new_file
223 .write_all(bytes)
224 .map_err(|err| BuildError::FailedToWriteFile(entry_path, err))?;
225 }
226 Kind::FileToCopy(source_path) => {
227 std::fs::copy(source_path, &entry_path)
228 .map_err(|err| BuildError::FailedToCopyFile(source_path.clone(), err))?;
229 }
230 }
231 }
232
233 Ok(TempDirectory {
234 path: self.root.clone(),
235 delete_on_drop: self.delete_on_drop,
236 })
237 }
238}
239
240fn random_temp_directory() -> PathBuf {
241 loop {
242 let random_string: String = thread_rng()
243 .sample_iter(&Alphanumeric)
244 .take(5)
245 .map(char::from)
246 .collect();
247
248 let path = env::temp_dir().join(random_string);
249
250 if !path.exists() {
251 return path;
252 }
253 }
254}
255
256#[derive(Debug)]
257enum Kind {
258 Directory,
259 EmptyFile,
260 TextFile(String),
261 BinaryFile(Vec<u8>),
262 FileToCopy(PathBuf),
263}
264
265#[derive(Debug)]
267struct Entry {
268 path: PathBuf,
270 kind: Kind,
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_temp_dir() {
280 let temp_dir = TempDirectoryBuilder::default().build().unwrap();
281
282 assert!(temp_dir.path().exists());
283 assert!(temp_dir.path().is_dir());
284 }
285
286 #[test]
287 fn test_add_text_file() {
288 let expected_content = "bar";
289 let entry_name = "foo.txt";
290 let temp_dir = TempDirectoryBuilder::default()
291 .add_text_file(entry_name, expected_content)
292 .build()
293 .unwrap();
294 let entry_path = temp_dir.path().join(entry_name);
295
296 assert!(entry_path.exists());
297
298 let content = std::fs::read_to_string(entry_path).expect("read text in foo.txt");
299
300 assert_eq!(content, expected_content);
301 }
302
303 #[test]
304 fn test_add_binary_file() {
305 let expected_content = [98u8, 97u8, 114u8];
306 let entry_name = "foo.txt";
307 let temp_dir = TempDirectoryBuilder::default()
308 .add_binary_file(entry_name, &expected_content)
309 .build()
310 .unwrap();
311 let entry_path = temp_dir.path().join(entry_name);
312
313 assert!(entry_path.exists());
314
315 let content = std::fs::read(entry_path).expect("read foo.txt");
316
317 assert_eq!(content, expected_content);
318 }
319
320 #[test]
321 fn test_add_empty_file() {
322 let entry_name = "empty_file.txt";
323 let temp_dir = TempDirectoryBuilder::default()
324 .add_empty_file(entry_name)
325 .build()
326 .unwrap();
327 let entry_path = temp_dir.path().join(entry_name);
328
329 assert!(entry_path.exists());
330
331 let created_entry_metadata = std::fs::metadata(entry_path).expect("get entry metadata");
332
333 assert_eq!(created_entry_metadata.len(), 0);
334 }
335
336 #[test]
337 fn test_add_directory() {
338 let entry_name = "empty_directory";
339 let temp_dir = TempDirectoryBuilder::default()
340 .add_directory(entry_name)
341 .build()
342 .unwrap();
343 let entry_path = temp_dir.path().join(entry_name);
344
345 assert!(entry_path.exists());
346 assert!(entry_path.is_dir());
347 }
348
349 #[test]
350 fn test_add_file() {
351 let entry_name = "test.rs";
352 let source_file_path = file!();
353 let temp_dir = TempDirectoryBuilder::default()
354 .add_file(entry_name, source_file_path)
355 .build()
356 .unwrap();
357 let entry_path = temp_dir.path().join(entry_name);
358
359 assert!(entry_path.exists());
360 assert!(entry_path.is_file());
361
362 let entry_content = std::fs::read_to_string(entry_path).unwrap();
363 let source_content = std::fs::read_to_string(source_file_path).unwrap();
364
365 assert_eq!(entry_content, source_content);
366 }
367
368 #[test]
369 fn test_temp_dir_is_dropped() {
370 let temp_dir = TempDirectoryBuilder::default().build().unwrap();
371
372 let temp_dir_path = temp_dir.path().to_path_buf();
373
374 assert!(temp_dir_path.exists());
375 assert!(temp_dir_path.is_dir());
376
377 drop(temp_dir);
378
379 assert!(!temp_dir_path.exists())
380 }
381
382 #[test]
383 fn test_entry_outside_temp_dir() {
384 let path_outside_temp_dir = std::env::temp_dir().join("outside");
385 let builder = TempDirectoryBuilder::default().add_empty_file(path_outside_temp_dir);
386 let error = builder.build().unwrap_err();
387
388 assert!(matches!(error, BuildError::EntryOutsideDirectory(_)));
389 }
390
391 #[test]
392 fn test_source_file_does_not_exists() {
393 let source_file_path = std::env::temp_dir().join("not existing file");
394 let builder = TempDirectoryBuilder::default().add_file("foo", source_file_path);
395 let error = builder.build().unwrap_err();
396
397 assert!(matches!(error, BuildError::FailedToCopyFile(..)));
398 }
399
400 #[test]
401 fn test_duplicated_entries() {
402 let builder = TempDirectoryBuilder::default()
403 .add_empty_file("foo")
404 .add_empty_file("foo");
405 let error = builder.build().unwrap_err();
406
407 assert!(matches!(error, BuildError::DuplicateEntry(..)));
408 }
409
410 #[test]
411 fn test_entry_outside_directory() {
412 let builder = TempDirectoryBuilder::default().add_empty_file("../foo");
413 let error = builder.build().unwrap_err();
414
415 assert!(matches!(error, BuildError::EntryOutsideDirectory(..)));
416 }
417
418 #[test]
419 fn test_empty_entry_name() {
420 let builder = TempDirectoryBuilder::default().add_empty_file("");
421 let error = builder.build().unwrap_err();
422
423 assert!(matches!(error, BuildError::EmptyEntryName(0)));
424 }
425}