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 =
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 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 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 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
230pub struct Dir {
234 dir: TempDir,
235}
236
237impl Dir {
238 pub fn path(&self) -> &Path {
240 self.dir.path()
241 }
242
243 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#[derive(Arbitrary, Debug, PartialEq, Eq, Clone, Copy)]
257pub enum FileType {
258 Regular,
260 Directory,
262 Fifo,
264 Socket,
266 BlockDevice,
268 CharDevice,
270 Symlink,
272 HardLink,
274}
275
276pub 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
291pub 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 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#[derive(PartialEq, Eq, Debug, Clone)]
346pub struct FileInfo {
347 pub path: PathBuf,
349 pub metadata: Metadata,
351 pub contents: Vec<u8>,
353}
354
355#[derive(PartialEq, Eq, Clone, Debug)]
357pub struct Metadata {
358 pub dev: u64,
360 pub ino: u64,
362 pub mode: u32,
364 pub uid: u32,
366 pub gid: u32,
368 pub nlink: u32,
370 pub rdev: u64,
372 pub mtime: u64,
374 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 libc::makedev(1, 3)
421}
422
423#[cfg(target_os = "macos")]
424fn arbitrary_char_dev() -> libc::dev_t {
425 libc::makedev(3, 2)
427}