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