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
20pub struct DirBuilder {
22 printable_names: bool,
23 file_types: Vec<FileType>,
24}
25
26impl DirBuilder {
27 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 pub fn printable_names(mut self, value: bool) -> Self {
53 self.printable_names = value;
54 self
55 }
56
57 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 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 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 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 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
235pub struct Dir {
239 dir: TempDir,
240}
241
242impl Dir {
243 pub fn path(&self) -> &Path {
245 self.dir.path()
246 }
247
248 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#[derive(Arbitrary, Debug, PartialEq, Eq, Clone, Copy)]
262pub enum FileType {
263 Regular,
265 Directory,
267 Fifo,
269 Socket,
271 BlockDevice,
273 CharDevice,
275 Symlink,
277 HardLink,
279}
280
281pub 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
296pub 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 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#[derive(PartialEq, Eq, Debug, Clone)]
351pub struct FileInfo {
352 pub path: PathBuf,
354 pub metadata: Metadata,
356 pub contents: Vec<u8>,
358}
359
360#[derive(PartialEq, Eq, Clone, Debug)]
362pub struct Metadata {
363 pub dev: u64,
365 pub ino: u64,
367 pub mode: u32,
369 pub uid: u32,
371 pub gid: u32,
373 pub nlink: u32,
375 pub rdev: u64,
377 pub mtime: u64,
379 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 libc::makedev(1, 3)
426}
427
428#[cfg(target_os = "macos")]
429fn arbitrary_char_dev() -> libc::dev_t {
430 libc::makedev(3, 2)
432}