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
36pub struct DirBuilder {
38 printable_names: bool,
39 file_types: Vec<FileType>,
40}
41
42impl DirBuilder {
43 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 pub fn printable_names(mut self, value: bool) -> Self {
64 self.printable_names = value;
65 self
66 }
67
68 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 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 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 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 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
199pub struct Dir {
203 dir: TempDir,
204}
205
206impl Dir {
207 pub fn path(&self) -> &Path {
209 self.dir.path()
210 }
211
212 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#[derive(Arbitrary, Debug, PartialEq, Eq, Clone, Copy)]
226pub enum FileType {
227 Regular,
229 Directory,
231 Fifo,
233 Socket,
235 BlockDevice,
237 CharDevice,
239 Symlink,
241 HardLink,
243}
244
245pub 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
260pub 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 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#[derive(PartialEq, Eq, Debug, Clone)]
315pub struct FileInfo {
316 pub path: PathBuf,
318 pub metadata: Metadata,
320 pub contents: Vec<u8>,
322}
323
324#[derive(PartialEq, Eq, Clone, Debug)]
326pub struct Metadata {
327 pub dev: u64,
329 pub ino: u64,
331 pub mode: u32,
333 pub uid: u32,
335 pub gid: u32,
337 pub nlink: u32,
339 pub rdev: u64,
341 pub mtime: u64,
343 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 makedev(1, 3)
370}
371
372#[cfg(target_os = "macos")]
373fn arbitrary_char_dev() -> dev_t {
374 unsafe { makedev(3, 2) }
376}