random_dir/
dir.rs

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