Skip to main content

random_dir/
dir.rs

1use std::collections::HashMap;
2use std::fs::create_dir_all;
3use std::fs::hard_link;
4use std::fs::read_link;
5use std::fs::File;
6use std::io::Error;
7use std::io::Write;
8use std::path::Path;
9use std::path::PathBuf;
10use std::path::MAIN_SEPARATOR_STR;
11use std::time::Duration;
12use std::time::SystemTime;
13
14use arbitrary::Arbitrary;
15use arbitrary::Unstructured;
16use normalize_path::NormalizePath;
17use tempfile::TempDir;
18use walkdir::WalkDir;
19
20/// [`Dir`] configuration.
21pub struct DirBuilder {
22    printable_names: bool,
23    file_types: Vec<FileType>,
24}
25
26impl DirBuilder {
27    /// Create new directory builder with default parameters.
28    pub fn new() -> Self {
29        Self {
30            #[cfg(not(any(target_os = "macos", windows)))]
31            printable_names: false,
32            #[cfg(any(target_os = "macos", windows))]
33            printable_names: true,
34            #[cfg(not(any(target_os = "macos", windows)))]
35            file_types: ALL_FILE_TYPES.into(),
36            #[cfg(target_os = "macos")]
37            file_types: {
38                use FileType::*;
39                [Regular, Directory, Fifo, Socket, Symlink, HardLink].into()
40            },
41            #[cfg(target_os = "windows")]
42            file_types: {
43                use FileType::*;
44                [Regular, Directory, Symlink, HardLink].into()
45            },
46        }
47    }
48
49    /// Generate files with printable names, i.e. names consisting only from printable characters.
50    ///
51    /// Useful to test CLI applications.
52    pub fn printable_names(mut self, value: bool) -> Self {
53        self.printable_names = value;
54        self
55    }
56
57    /// Which file types to generate?
58    ///
59    /// By default any Unix file type can be generated.
60    pub fn file_types<I>(mut self, file_types: I) -> Self
61    where
62        I: IntoIterator<Item = FileType>,
63    {
64        self.file_types = file_types.into_iter().collect();
65        self
66    }
67
68    /// Create a temprary directory with random contents.
69    pub fn create(self, u: &mut Unstructured<'_>) -> arbitrary::Result<Dir> {
70        use FileType::*;
71        #[cfg(unix)]
72        let random_path = |u: &mut Unstructured<'_>| -> arbitrary::Result<PathBuf> {
73            let path = if self.printable_names {
74                let len: usize = u.int_in_range(1..=10)?;
75                let mut string = String::with_capacity(len);
76                for _ in 0..len {
77                    string.push(u.int_in_range(b'a'..=b'z')? as char);
78                }
79                std::ffi::CString::new(string).unwrap()
80            } else {
81                u.arbitrary()?
82            };
83            use std::os::unix::ffi::OsStringExt;
84            let path = std::ffi::OsString::from_vec(path.into_bytes());
85            let path: PathBuf = path.into();
86            Ok(path)
87        };
88        #[cfg(not(unix))]
89        let random_path = |u: &mut Unstructured<'_>| -> arbitrary::Result<PathBuf> {
90            let len: usize = u.int_in_range(1..=10)?;
91            let mut string = String::with_capacity(len);
92            for _ in 0..len {
93                string.push(u.int_in_range(b'a'..=b'z')? as char);
94            }
95            Ok(string.into())
96        };
97        let dir = TempDir::new().unwrap();
98        let mut files = Vec::new();
99        let num_files: usize = u.int_in_range(0..=10)?;
100        for _ in 0..num_files {
101            let path = random_path(u)?;
102            if path.as_os_str().is_empty() {
103                // do not allow empty paths
104                continue;
105            }
106            let path = match path.strip_prefix(MAIN_SEPARATOR_STR) {
107                Ok(path) => path,
108                Err(_) => path.as_path(),
109            };
110            let path = dir.path().join(path).normalize();
111            if path.is_dir() || files.contains(&path) {
112                // the path aliased some existing directory
113                continue;
114            }
115            create_dir_all(path.parent().unwrap()).unwrap();
116            let mut kind: FileType = *u.choose(&self.file_types[..])?;
117            if matches!(kind, FileType::HardLink | FileType::Symlink) && files.is_empty() {
118                kind = Regular;
119            }
120            let t = {
121                let t = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
122                let dt = t.duration_since(SystemTime::UNIX_EPOCH).unwrap();
123                SystemTime::UNIX_EPOCH
124                    + Duration::new(
125                        u.int_in_range(0..=dt.as_secs())?,
126                        u.int_in_range(0..=999_999_999)?,
127                    )
128            };
129            match kind {
130                Regular => {
131                    let contents: Vec<u8> = u.arbitrary()?;
132                    let mut file = File::create(&path).unwrap();
133                    file.write_all(&contents).unwrap();
134                    #[cfg(unix)]
135                    {
136                        use std::fs::Permissions;
137                        use std::os::unix::fs::PermissionsExt;
138                        let mode = u.int_in_range(0..=0o777)? | 0o400;
139                        file.set_permissions(Permissions::from_mode(mode)).unwrap();
140                    }
141                    file.set_modified(t).unwrap();
142                }
143                #[cfg(unix)]
144                Directory => {
145                    use std::os::unix::fs::DirBuilderExt;
146                    let mode = u.int_in_range(0..=0o777)? | 0o500;
147                    std::fs::DirBuilder::new()
148                        .mode(mode)
149                        .recursive(true)
150                        .create(&path)
151                        .unwrap();
152                    let path = crate::path_to_c_string(path.clone()).unwrap();
153                    crate::set_file_modified_time(&path, t).unwrap();
154                }
155                #[cfg(not(unix))]
156                Directory => {
157                    std::fs::DirBuilder::new()
158                        .recursive(true)
159                        .create(&path)
160                        .unwrap();
161                }
162                #[cfg(unix)]
163                Fifo => {
164                    let mode = u.int_in_range(0..=0o777)? | 0o400;
165                    let path = crate::path_to_c_string(path.clone()).unwrap();
166                    crate::mkfifo(&path, mode).unwrap();
167                    crate::set_file_modified_time(&path, t).unwrap();
168                }
169                #[cfg(unix)]
170                Socket => {
171                    use std::os::unix::net::UnixDatagram;
172                    UnixDatagram::bind(&path).unwrap();
173                    let path = crate::path_to_c_string(path.clone()).unwrap();
174                    crate::set_file_modified_time(&path, t).unwrap();
175                }
176                #[cfg(unix)]
177                BlockDevice => {
178                    // dev loop
179                    let dev = libc::makedev(7, 0);
180                    let mode = u.int_in_range(0o400..=0o777)?;
181                    let path = crate::path_to_c_string(path.clone()).unwrap();
182                    crate::mknod(&path, mode, dev).unwrap();
183                    crate::set_file_modified_time(&path, t).unwrap();
184                }
185                #[cfg(unix)]
186                CharDevice => {
187                    let dev = arbitrary_char_dev();
188                    let mode = u.int_in_range(0o400..=0o777)?;
189                    let path = crate::path_to_c_string(path.clone()).unwrap();
190                    crate::mknod(&path, mode, dev).unwrap();
191                    crate::set_file_modified_time(&path, t).unwrap();
192                }
193                #[cfg(unix)]
194                Symlink => {
195                    use std::os::unix::fs::symlink;
196                    let original = u.choose(&files[..]).unwrap();
197                    symlink(original, &path).unwrap();
198                }
199                #[cfg(windows)]
200                Symlink => {
201                    use std::os::windows::fs::symlink_file;
202                    let original = u.choose(&files[..]).unwrap();
203                    symlink_file(original, &path).unwrap();
204                }
205                #[cfg(all(not(unix), not(windows)))]
206                Symlink => panic!("Unsupported file type: {kind:?}"),
207                HardLink => {
208                    let original = u.choose(&files[..]).unwrap();
209                    assert!(
210                        hard_link(original, &path).is_ok(),
211                        "original = `{}`, path = `{}`",
212                        original.display(),
213                        path.display()
214                    );
215                }
216                #[cfg(not(unix))]
217                Socket | Fifo | CharDevice | BlockDevice => {
218                    panic!("Unsupported file type: {kind:?}")
219                }
220            }
221            if kind != FileType::Directory {
222                files.push(path.clone());
223            }
224        }
225        Ok(Dir { dir })
226    }
227}
228
229impl Default for DirBuilder {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235/// Directory with randomly generated contents.
236///
237/// Automatically Deleted on drop.
238pub struct Dir {
239    dir: TempDir,
240}
241
242impl Dir {
243    /// Get directory path.
244    pub fn path(&self) -> &Path {
245        self.dir.path()
246    }
247
248    /// Transform into inner representation.
249    pub fn into_inner(self) -> TempDir {
250        self.dir
251    }
252}
253
254impl<'a> Arbitrary<'a> for Dir {
255    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
256        DirBuilder::new().create(u)
257    }
258}
259
260/// File type.
261#[derive(Arbitrary, Debug, PartialEq, Eq, Clone, Copy)]
262pub enum FileType {
263    /// Regular file.
264    Regular,
265    /// A directory.
266    Directory,
267    /// Named pipe.
268    Fifo,
269    /// UNIX socket.
270    Socket,
271    /// Block device.
272    BlockDevice,
273    /// Character device.
274    CharDevice,
275    /// Symbolic link.
276    Symlink,
277    /// Hard link.
278    HardLink,
279}
280
281/// All file types supported by the platform.
282pub const ALL_FILE_TYPES: [FileType; 8] = {
283    use FileType::*;
284    [
285        Regular,
286        Directory,
287        Fifo,
288        Socket,
289        BlockDevice,
290        CharDevice,
291        Symlink,
292        HardLink,
293    ]
294};
295
296/// Recursively list specified directory.
297///
298/// This function always returns the same entries in the same order for the same directory.
299/// It also remaps inodes to make listings of the two directories conataining the same files
300/// consistent.
301///
302/// The intended usage is to compare the contents (files and metadata) of the two directories.
303pub fn list_dir_all<P: AsRef<Path>>(dir: P) -> Result<Vec<FileInfo>, Error> {
304    let dir = dir.as_ref();
305    let mut files = Vec::new();
306    for entry in WalkDir::new(dir).into_iter() {
307        let entry = entry?;
308        if entry.path() == dir {
309            continue;
310        }
311        let metadata = entry.path().symlink_metadata()?;
312        let contents = if metadata.is_file() {
313            std::fs::read(entry.path()).unwrap()
314        } else if metadata.is_symlink() {
315            let target = read_link(entry.path()).unwrap();
316            target.as_os_str().as_encoded_bytes().to_vec()
317        } else {
318            Vec::new()
319        };
320        let path = entry.path().strip_prefix(dir).map_err(Error::other)?;
321        let metadata: Metadata = (&metadata).try_into()?;
322        files.push(FileInfo {
323            path: path.to_path_buf(),
324            metadata,
325            contents,
326        });
327    }
328    files.sort_by(|a, b| a.path.cmp(&b.path));
329    // remap inodes
330    use std::collections::hash_map::Entry::*;
331    let mut inodes = HashMap::new();
332    let mut next_inode = 0;
333    for file in files.iter_mut() {
334        let old = file.metadata.ino;
335        let inode = match inodes.entry(old) {
336            Vacant(v) => {
337                let inode = next_inode;
338                v.insert(next_inode);
339                next_inode += 1;
340                inode
341            }
342            Occupied(o) => *o.get(),
343        };
344        file.metadata.ino = inode;
345    }
346    Ok(files)
347}
348
349/// File's path, metadata and contents.
350#[derive(PartialEq, Eq, Debug, Clone)]
351pub struct FileInfo {
352    /// Path.
353    pub path: PathBuf,
354    /// Metadata.
355    pub metadata: Metadata,
356    /// File contents.
357    pub contents: Vec<u8>,
358}
359
360/// File's metadata.
361#[derive(PartialEq, Eq, Clone, Debug)]
362pub struct Metadata {
363    /// Containing device number.
364    pub dev: u64,
365    /// Inode.
366    pub ino: u64,
367    /// File mode.
368    pub mode: u32,
369    /// Owner's user id.
370    pub uid: u32,
371    /// Owner's group id.
372    pub gid: u32,
373    /// No. of hard links.
374    pub nlink: u32,
375    /// Device number of the file itself.
376    pub rdev: u64,
377    /// Last modification time.
378    pub mtime: u64,
379    /// File size in bytes.
380    pub file_size: u64,
381}
382
383impl TryFrom<&std::fs::Metadata> for Metadata {
384    type Error = Error;
385
386    #[cfg(unix)]
387    fn try_from(other: &std::fs::Metadata) -> Result<Self, Error> {
388        use std::os::unix::fs::MetadataExt;
389        Ok(Self {
390            dev: other.dev(),
391            ino: other.ino(),
392            mode: other.mode(),
393            uid: other.uid(),
394            gid: other.gid(),
395            nlink: other.nlink() as u32,
396            rdev: other.rdev(),
397            mtime: other.mtime() as u64,
398            file_size: other.size(),
399        })
400    }
401
402    #[cfg(not(unix))]
403    fn try_from(other: &std::fs::Metadata) -> Result<Self, Error> {
404        Ok(Self {
405            dev: 0,
406            ino: 0,
407            mode: 0,
408            uid: 0,
409            gid: 0,
410            nlink: 1,
411            rdev: 0,
412            mtime: other
413                .modified()?
414                .duration_since(SystemTime::UNIX_EPOCH)
415                .unwrap_or(Duration::ZERO)
416                .as_secs(),
417            file_size: other.len(),
418        })
419    }
420}
421
422#[cfg(target_os = "linux")]
423fn arbitrary_char_dev() -> libc::dev_t {
424    // /dev/null
425    libc::makedev(1, 3)
426}
427
428#[cfg(target_os = "macos")]
429fn arbitrary_char_dev() -> libc::dev_t {
430    // /dev/null
431    libc::makedev(3, 2)
432}