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