camino_tempfile_ext/fixture/
tools.rs

1// Copyright (c) The camino-tempfile Contributors
2// Adapted from assert_fs: Copyright (c) The assert_fs Contributors
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6use super::{ChildPath, FixtureError, FixtureKind, ResultChainExt};
7use camino::Utf8Path;
8use camino_tempfile::{NamedUtf8TempFile, Utf8TempDir};
9use globwalk::GlobWalkerBuilder;
10use std::{fs, io::Write, path::Path};
11
12/// Create empty directories at [`ChildPath`].
13pub trait PathCreateDir {
14    /// Create an empty directory at [`ChildPath`].
15    ///
16    /// # Examples
17    ///
18    /// ```rust
19    /// use camino_tempfile_ext::prelude::*;
20    ///
21    /// let temp = Utf8TempDir::new().unwrap();
22    /// temp.child("subdir").create_dir_all().unwrap();
23    /// temp.close().unwrap();
24    /// ```
25    ///
26    fn create_dir_all(&self) -> Result<(), FixtureError>;
27}
28
29impl PathCreateDir for ChildPath {
30    fn create_dir_all(&self) -> Result<(), FixtureError> {
31        create_dir_all(self.as_path())
32    }
33}
34
35/// Create empty files at [`ChildPath`].
36///
37pub trait FileTouch {
38    /// Create an empty file at [`ChildPath`].
39    ///
40    /// # Examples
41    ///
42    /// ```rust
43    /// use camino_tempfile_ext::prelude::*;
44    ///
45    /// let temp = Utf8TempDir::new().unwrap();
46    /// temp.child("foo.txt").touch().unwrap();
47    /// temp.close().unwrap();
48    /// ```
49    ///
50    fn touch(&self) -> Result<(), FixtureError>;
51}
52
53impl FileTouch for ChildPath {
54    fn touch(&self) -> Result<(), FixtureError> {
55        touch(self.as_path())
56    }
57}
58
59impl FileTouch for NamedUtf8TempFile {
60    fn touch(&self) -> Result<(), FixtureError> {
61        touch(self.path())
62    }
63}
64
65/// Write a binary file at [`ChildPath`].
66///
67pub trait FileWriteBin {
68    /// Write a binary file at [`ChildPath`].
69    ///
70    /// # Examples
71    ///
72    /// ```rust
73    /// use camino_tempfile_ext::prelude::*;
74    ///
75    /// let temp = Utf8TempDir::new().unwrap();
76    /// temp
77    ///     .child("foo.txt")
78    ///     .write_binary(b"To be or not to be...")
79    ///     .unwrap();
80    /// temp.close().unwrap();
81    /// ```
82    ///
83    fn write_binary(&self, data: &[u8]) -> Result<(), FixtureError>;
84}
85
86impl FileWriteBin for ChildPath {
87    fn write_binary(&self, data: &[u8]) -> Result<(), FixtureError> {
88        write_binary(self.as_path(), data)
89    }
90}
91
92impl FileWriteBin for NamedUtf8TempFile {
93    fn write_binary(&self, data: &[u8]) -> Result<(), FixtureError> {
94        write_binary(self.path(), data)
95    }
96}
97
98/// Write a text file at a [`ChildPath`] or [`NamedUtf8TempFile`].
99pub trait FileWriteStr {
100    /// Write a text file at [`ChildPath`] or [`NamedUtf8TempFile`].
101    ///
102    /// # Examples
103    ///
104    /// ```rust
105    /// use camino_tempfile_ext::prelude::*;
106    ///
107    /// let temp = Utf8TempDir::new().unwrap();
108    /// temp
109    ///    .child("foo.txt")
110    ///    .write_str("To be or not to be...")
111    ///    .unwrap();
112    /// temp.close().unwrap();
113    /// ```
114    fn write_str(&self, data: &str) -> Result<(), FixtureError>;
115}
116
117impl FileWriteStr for ChildPath {
118    fn write_str(&self, data: &str) -> Result<(), FixtureError> {
119        write_str(self.as_path(), data)
120    }
121}
122
123impl FileWriteStr for NamedUtf8TempFile {
124    fn write_str(&self, data: &str) -> Result<(), FixtureError> {
125        write_str(self.path(), data)
126    }
127}
128
129/// Write (copy) a file to [`ChildPath`].
130pub trait FileWriteFile {
131    /// Write (copy) a file to [`ChildPath`].
132    ///
133    /// # Examples
134    ///
135    /// ```rust
136    /// use camino::Utf8Path;
137    /// use camino_tempfile_ext::prelude::*;
138    ///
139    /// let temp = Utf8TempDir::new().unwrap();
140    /// temp
141    ///    .child("foo.txt")
142    ///    .write_file(Utf8Path::new("Cargo.toml"))
143    ///    .unwrap();
144    /// temp.close().unwrap();
145    /// ```
146    ///
147    fn write_file(&self, data: &Utf8Path) -> Result<(), FixtureError>;
148}
149
150impl FileWriteFile for ChildPath {
151    fn write_file(&self, data: &Utf8Path) -> Result<(), FixtureError> {
152        write_file(self.as_path(), data)
153    }
154}
155
156impl FileWriteFile for NamedUtf8TempFile {
157    fn write_file(&self, data: &Utf8Path) -> Result<(), FixtureError> {
158        write_file(self.path(), data)
159    }
160}
161
162/// Copy files into [`Utf8TempDir`].
163pub trait PathCopy {
164    /// Copy files and directories into the current path from the `source` according to the glob
165    /// `patterns`.
166    ///
167    /// # Examples
168    ///
169    /// ```rust
170    /// use camino_tempfile_ext::prelude::*;
171    ///
172    /// let temp = Utf8TempDir::new().unwrap();
173    /// temp.copy_from(".", &["*.rs"]).unwrap();
174    /// temp.close().unwrap();
175    /// ```
176    fn copy_from<P: AsRef<Utf8Path>, S: AsRef<str>>(
177        &self,
178        source: P,
179        patterns: &[S],
180    ) -> Result<(), FixtureError>;
181}
182
183impl PathCopy for Utf8TempDir {
184    fn copy_from<P: AsRef<Utf8Path>, S: AsRef<str>>(
185        &self,
186        source: P,
187        patterns: &[S],
188    ) -> Result<(), FixtureError> {
189        copy_files(self.path(), source.as_ref(), patterns)
190    }
191}
192
193impl PathCopy for ChildPath {
194    fn copy_from<P: AsRef<Utf8Path>, S: AsRef<str>>(
195        &self,
196        source: P,
197        patterns: &[S],
198    ) -> Result<(), FixtureError> {
199        copy_files(self.as_path(), source.as_ref(), patterns)
200    }
201}
202
203/// Create a symlink to a target file.
204///
205pub trait SymlinkToFile {
206    /// Create a symlink to the provided target file.
207    ///
208    /// # Examples
209    ///
210    /// ```rust
211    /// use camino_tempfile_ext::prelude::*;
212    ///
213    /// let temp = Utf8TempDir::new().unwrap();
214    /// let real_file = temp.child("real_file");
215    /// real_file.touch().unwrap();
216    ///
217    /// temp.child("link_file").symlink_to_file(real_file.as_path()).unwrap();
218    ///
219    /// temp.close().unwrap();
220    /// ```
221    fn symlink_to_file<P: AsRef<Path>>(&self, target: P) -> Result<(), FixtureError>;
222}
223
224impl SymlinkToFile for ChildPath {
225    fn symlink_to_file<P: AsRef<Path>>(&self, target: P) -> Result<(), FixtureError> {
226        symlink_to_file(self.as_path(), target.as_ref())
227    }
228}
229
230impl SymlinkToFile for NamedUtf8TempFile {
231    fn symlink_to_file<P: AsRef<Path>>(&self, target: P) -> Result<(), FixtureError> {
232        symlink_to_file(self.path(), target.as_ref())
233    }
234}
235
236/// Create a symlink to a target directory.
237pub trait SymlinkToDir {
238    /// Create a symlink to the provided target directory.
239    ///
240    /// # Examples
241    ///
242    /// ```rust
243    /// use camino_tempfile_ext::prelude::*;
244    ///
245    /// let temp = Utf8TempDir::new().unwrap();
246    /// let real_dir = temp.child("real_dir");
247    /// real_dir.create_dir_all().unwrap();
248    ///
249    /// temp.child("link_dir").symlink_to_dir(real_dir.as_path()).unwrap();
250    ///
251    /// temp.close().unwrap();
252    /// ```
253    fn symlink_to_dir<P: AsRef<Path>>(&self, target: P) -> Result<(), FixtureError>;
254}
255
256impl SymlinkToDir for ChildPath {
257    fn symlink_to_dir<P: AsRef<Path>>(&self, target: P) -> Result<(), FixtureError> {
258        symlink_to_dir(self.as_path(), target.as_ref())
259    }
260}
261
262impl SymlinkToDir for Utf8TempDir {
263    fn symlink_to_dir<P: AsRef<Path>>(&self, target: P) -> Result<(), FixtureError> {
264        symlink_to_dir(self.path(), target.as_ref())
265    }
266}
267
268fn ensure_parent_dir(path: &Utf8Path) -> Result<(), FixtureError> {
269    if let Some(parent) = path.parent() {
270        fs::create_dir_all(parent).chain(FixtureError::new(FixtureKind::CreateDir))?;
271    }
272    Ok(())
273}
274
275fn create_dir_all(path: &Utf8Path) -> Result<(), FixtureError> {
276    fs::create_dir_all(path).chain(FixtureError::new(FixtureKind::CreateDir))?;
277    Ok(())
278}
279
280fn touch(path: &Utf8Path) -> Result<(), FixtureError> {
281    ensure_parent_dir(path)?;
282    fs::File::create(path).chain(FixtureError::new(FixtureKind::WriteFile))?;
283    Ok(())
284}
285
286fn write_binary(path: &Utf8Path, data: &[u8]) -> Result<(), FixtureError> {
287    ensure_parent_dir(path)?;
288    let mut file = fs::File::create(path).chain(FixtureError::new(FixtureKind::WriteFile))?;
289    file.write_all(data)
290        .chain(FixtureError::new(FixtureKind::WriteFile))?;
291    Ok(())
292}
293
294fn write_str(path: &Utf8Path, data: &str) -> Result<(), FixtureError> {
295    ensure_parent_dir(path)?;
296    write_binary(path, data.as_bytes()).chain(FixtureError::new(FixtureKind::WriteFile))
297}
298
299fn write_file(path: &Utf8Path, data: &Utf8Path) -> Result<(), FixtureError> {
300    ensure_parent_dir(path)?;
301    fs::copy(data, path).chain(FixtureError::new(FixtureKind::CopyFile))?;
302    Ok(())
303}
304
305fn copy_files<S>(target: &Utf8Path, source: &Utf8Path, patterns: &[S]) -> Result<(), FixtureError>
306where
307    S: AsRef<str>,
308{
309    // `walkdir`, on Windows, seems to convert "." into "" which then fails.
310    let source = source
311        .canonicalize()
312        .chain(FixtureError::new(FixtureKind::Walk))?;
313
314    // Use a regular `Path` rather than `Utf8Path` for this -- no particular
315    // reason to restrict to UTF-8 paths within subdirectories like this.
316    let target = target.as_std_path();
317
318    for entry in GlobWalkerBuilder::from_patterns(&source, patterns)
319        .follow_links(true)
320        .build()
321        .chain(FixtureError::new(FixtureKind::Walk))?
322    {
323        let entry = entry.chain(FixtureError::new(FixtureKind::Walk))?;
324        let rel = entry
325            .path()
326            .strip_prefix(&source)
327            .expect("entries to be under `source`");
328        let target_path = target.join(rel);
329        if entry.file_type().is_dir() {
330            fs::create_dir_all(target_path).chain(FixtureError::new(FixtureKind::CreateDir))?;
331        } else if entry.file_type().is_file() {
332            fs::create_dir_all(target_path.parent().expect("at least `target` exists"))
333                .chain(FixtureError::new(FixtureKind::CreateDir))?;
334            fs::copy(entry.path(), target_path).chain(FixtureError::new(FixtureKind::CopyFile))?;
335        }
336    }
337    Ok(())
338}
339
340#[cfg(windows)]
341fn symlink_to_file(link: &Utf8Path, target: &Path) -> Result<(), FixtureError> {
342    std::os::windows::fs::symlink_file(target, link)
343        .chain(FixtureError::new(FixtureKind::Symlink))?;
344    Ok(())
345}
346
347#[cfg(windows)]
348fn symlink_to_dir(link: &Utf8Path, target: &Path) -> Result<(), FixtureError> {
349    std::os::windows::fs::symlink_dir(target, link)
350        .chain(FixtureError::new(FixtureKind::Symlink))?;
351    Ok(())
352}
353
354#[cfg(not(windows))]
355fn symlink_to_file(link: &Utf8Path, target: &Path) -> Result<(), FixtureError> {
356    std::os::unix::fs::symlink(target, link).chain(FixtureError::new(FixtureKind::Symlink))?;
357    Ok(())
358}
359
360#[cfg(not(windows))]
361fn symlink_to_dir(link: &Utf8Path, target: &Path) -> Result<(), FixtureError> {
362    std::os::unix::fs::symlink(target, link).chain(FixtureError::new(FixtureKind::Symlink))?;
363    Ok(())
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use crate::fixture::PathChild;
370
371    #[test]
372    fn test_symlink_to_file() {
373        let temp_dir = Utf8TempDir::new().unwrap();
374        let file = temp_dir.child("file");
375        file.touch().unwrap();
376        let link = temp_dir.child("link");
377        link.symlink_to_file(&file).unwrap();
378
379        assert!(link.exists());
380        assert!(link.is_symlink());
381        assert_eq!(link.read_link_utf8().unwrap(), file);
382    }
383}