1 #![expect(unsafe_op_in_unsafe_fn, reason = "old code, not worth updating yet")]
2 
3 use std::{env, mem, process, slice, str};
4 use test_programs::preview1::open_scratch_directory;
5 
6 const BUF_LEN: usize = 256;
7 
8 struct DirEntry {
9     dirent: wasip1::Dirent,
10     name: String,
11 }
12 
13 // Manually reading the output from fd_readdir is tedious and repetitive,
14 // so encapsulate it into an iterator
15 struct ReadDir<'a> {
16     buf: &'a [u8],
17 }
18 
19 impl<'a> ReadDir<'a> {
from_slice(buf: &'a [u8]) -> Self20     fn from_slice(buf: &'a [u8]) -> Self {
21         Self { buf }
22     }
23 }
24 
25 impl<'a> Iterator for ReadDir<'a> {
26     type Item = DirEntry;
27 
next(&mut self) -> Option<DirEntry>28     fn next(&mut self) -> Option<DirEntry> {
29         unsafe {
30             if self.buf.len() < mem::size_of::<wasip1::Dirent>() {
31                 return None;
32             }
33 
34             // Read the data
35             let dirent_ptr = self.buf.as_ptr() as *const wasip1::Dirent;
36             let dirent = dirent_ptr.read_unaligned();
37 
38             if self.buf.len() < mem::size_of::<wasip1::Dirent>() + dirent.d_namlen as usize {
39                 return None;
40             }
41 
42             let name_ptr = dirent_ptr.offset(1) as *const u8;
43             // NOTE Linux syscall returns a NUL-terminated name, but WASI doesn't
44             let namelen = dirent.d_namlen as usize;
45             let slice = slice::from_raw_parts(name_ptr, namelen);
46             let name = str::from_utf8(slice).expect("invalid utf8").to_owned();
47 
48             // Update the internal state
49             let delta = mem::size_of_val(&dirent) + namelen;
50             self.buf = &self.buf[delta..];
51 
52             DirEntry { dirent, name }.into()
53         }
54     }
55 }
56 
57 /// Return the entries plus a bool indicating EOF.
exec_fd_readdir(fd: wasip1::Fd, cookie: wasip1::Dircookie) -> (Vec<DirEntry>, bool)58 unsafe fn exec_fd_readdir(fd: wasip1::Fd, cookie: wasip1::Dircookie) -> (Vec<DirEntry>, bool) {
59     let mut buf: [u8; BUF_LEN] = [0; BUF_LEN];
60     let bufused =
61         wasip1::fd_readdir(fd, buf.as_mut_ptr(), BUF_LEN, cookie).expect("failed fd_readdir");
62     assert!(bufused <= BUF_LEN);
63 
64     let sl = slice::from_raw_parts(buf.as_ptr(), bufused);
65     let dirs: Vec<_> = ReadDir::from_slice(sl).collect();
66     let eof = bufused < BUF_LEN;
67     (dirs, eof)
68 }
69 
assert_empty_dir(dir_fd: wasip1::Fd)70 unsafe fn assert_empty_dir(dir_fd: wasip1::Fd) {
71     let stat = wasip1::fd_filestat_get(dir_fd).expect("failed filestat");
72 
73     let (mut dirs, eof) = exec_fd_readdir(dir_fd, 0);
74     assert!(eof, "expected to read the entire directory");
75     dirs.sort_by_key(|d| d.name.clone());
76     assert_eq!(dirs.len(), 2, "expected two entries in an empty directory");
77     let mut dirs = dirs.into_iter();
78 
79     // the first entry should be `.`
80     let dir = dirs.next().expect("first entry is None");
81     assert_eq!(dir.name, ".", "first name");
82     assert_eq!(dir.dirent.d_type, wasip1::FILETYPE_DIRECTORY, "first type");
83     assert_eq!(dir.dirent.d_ino, stat.ino);
84     assert_eq!(dir.dirent.d_namlen, 1);
85 
86     // the second entry should be `..`
87     let dir = dirs.next().expect("second entry is None");
88     assert_eq!(dir.name, "..", "second name");
89     assert_eq!(dir.dirent.d_type, wasip1::FILETYPE_DIRECTORY, "second type");
90 
91     assert!(
92         dirs.next().is_none(),
93         "the directory should be seen as empty"
94     );
95 }
96 
test_fd_readdir(dir_fd: wasip1::Fd)97 unsafe fn test_fd_readdir(dir_fd: wasip1::Fd) {
98     // Check the behavior in an empty directory
99     assert_empty_dir(dir_fd);
100 
101     // Add a file and check the behavior
102     let file_fd = wasip1::path_open(
103         dir_fd,
104         0,
105         "file",
106         wasip1::OFLAGS_CREAT,
107         wasip1::RIGHTS_FD_READ | wasip1::RIGHTS_FD_WRITE,
108         0,
109         0,
110     )
111     .expect("failed to create file");
112     assert!(
113         file_fd > libc::STDERR_FILENO as wasip1::Fd,
114         "file descriptor range check",
115     );
116 
117     let file_stat = wasip1::fd_filestat_get(file_fd).expect("failed filestat");
118     wasip1::fd_close(file_fd).expect("closing a file");
119 
120     wasip1::path_create_directory(dir_fd, "nested").expect("create a directory");
121     let nested_fd = wasip1::path_open(dir_fd, 0, "nested", 0, 0, 0, 0)
122         .expect("failed to open nested directory");
123     let nested_stat = wasip1::fd_filestat_get(nested_fd).expect("failed filestat");
124 
125     // Execute another readdir
126     let (mut dirs, eof) = exec_fd_readdir(dir_fd, 0);
127     assert!(eof, "expected to read the entire directory");
128     assert_eq!(dirs.len(), 4, "expected four entries");
129     // Save the data about the last entry. We need to do it before sorting.
130     let lastfile_cookie = dirs[2].dirent.d_next;
131     let lastfile_name = dirs[3].name.clone();
132     dirs.sort_by_key(|d| d.name.clone());
133     let mut dirs = dirs.into_iter();
134 
135     let dir = dirs.next().expect("first entry is None");
136     assert_eq!(dir.name, ".", "first name");
137     let dir = dirs.next().expect("second entry is None");
138     assert_eq!(dir.name, "..", "second name");
139     let dir = dirs.next().expect("third entry is None");
140     // check the file info
141     assert_eq!(dir.name, "file", "file name doesn't match");
142     assert_eq!(
143         dir.dirent.d_type,
144         wasip1::FILETYPE_REGULAR_FILE,
145         "type for the real file"
146     );
147     assert_eq!(dir.dirent.d_ino, file_stat.ino);
148     let dir = dirs.next().expect("fourth entry is None");
149     // check the directory info
150     assert_eq!(dir.name, "nested", "nested directory name doesn't match");
151     assert_eq!(
152         dir.dirent.d_type,
153         wasip1::FILETYPE_DIRECTORY,
154         "type for the nested directory"
155     );
156     assert_eq!(dir.dirent.d_ino, nested_stat.ino);
157 
158     // check if cookie works as expected
159     let (dirs, eof) = exec_fd_readdir(dir_fd, lastfile_cookie);
160     assert!(eof, "expected to read the entire directory");
161     assert_eq!(dirs.len(), 1, "expected one entry");
162     assert_eq!(dirs[0].name, lastfile_name, "name of the only entry");
163 
164     // check if nested directory shows up as empty
165     assert_empty_dir(nested_fd);
166     wasip1::fd_close(nested_fd).expect("closing a nested directory");
167 
168     wasip1::path_unlink_file(dir_fd, "file").expect("removing a file");
169     wasip1::path_remove_directory(dir_fd, "nested").expect("removing a nested directory");
170 }
171 
test_fd_readdir_lots(dir_fd: wasip1::Fd)172 unsafe fn test_fd_readdir_lots(dir_fd: wasip1::Fd) {
173     // Add a file and check the behavior
174     for count in 0..1000 {
175         let file_fd = wasip1::path_open(
176             dir_fd,
177             0,
178             &format!("file.{count}"),
179             wasip1::OFLAGS_CREAT,
180             wasip1::RIGHTS_FD_READ | wasip1::RIGHTS_FD_WRITE,
181             0,
182             0,
183         )
184         .expect("failed to create file");
185         assert!(
186             file_fd > libc::STDERR_FILENO as wasip1::Fd,
187             "file descriptor range check",
188         );
189         wasip1::fd_close(file_fd).expect("closing a file");
190     }
191 
192     // Count the entries to ensure that we see the correct number.
193     let mut total = 0;
194     let mut cookie = 0;
195     loop {
196         let (dirs, eof) = exec_fd_readdir(dir_fd, cookie);
197         total += dirs.len();
198         if eof {
199             break;
200         }
201         cookie = dirs[dirs.len() - 1].dirent.d_next;
202     }
203     assert_eq!(total, 1002, "expected 1000 entries plus . and ..");
204 
205     for count in 0..1000 {
206         wasip1::path_unlink_file(dir_fd, &format!("file.{count}")).expect("removing a file");
207     }
208 }
209 
test_fd_readdir_unicode_boundary(dir_fd: wasip1::Fd)210 unsafe fn test_fd_readdir_unicode_boundary(dir_fd: wasip1::Fd) {
211     let filename = "Действие";
212     let file_fd = wasip1::path_open(
213         dir_fd,
214         0,
215         filename,
216         wasip1::OFLAGS_CREAT,
217         wasip1::RIGHTS_FD_READ | wasip1::RIGHTS_FD_WRITE,
218         0,
219         0,
220     )
221     .expect("failed to create file");
222     assert!(
223         file_fd > libc::STDERR_FILENO as wasip1::Fd,
224         "file descriptor range check",
225     );
226     wasip1::fd_close(file_fd).expect("closing a file");
227 
228     let mut buf = Vec::new();
229     'outer: loop {
230         let len = wasip1::fd_readdir(dir_fd, buf.as_mut_ptr(), buf.capacity(), 0).unwrap();
231         buf.set_len(len);
232 
233         for entry in ReadDir::from_slice(&buf) {
234             if entry.name == filename {
235                 break 'outer;
236             }
237         }
238         buf = Vec::with_capacity(buf.capacity() + 1);
239     }
240 
241     wasip1::path_unlink_file(dir_fd, filename).expect("removing a file");
242 }
243 
test_fd_readdir_past_end(dir_fd: wasip1::Fd)244 unsafe fn test_fd_readdir_past_end(dir_fd: wasip1::Fd) {
245     let file_fd = wasip1::path_open(
246         dir_fd,
247         0,
248         "a",
249         wasip1::OFLAGS_CREAT,
250         wasip1::RIGHTS_FD_READ | wasip1::RIGHTS_FD_WRITE,
251         0,
252         0,
253     )
254     .expect("failed to create file");
255     wasip1::fd_close(file_fd).expect("closing a file");
256 
257     let mut buf = vec![0; 128];
258     let len = wasip1::fd_readdir(dir_fd, buf.as_mut_ptr(), buf.capacity(), 0).unwrap();
259 
260     let next = ReadDir::from_slice(&buf[..len])
261         .last()
262         .unwrap()
263         .dirent
264         .d_next;
265 
266     let len = wasip1::fd_readdir(dir_fd, buf.as_mut_ptr(), buf.capacity(), next + 1).unwrap();
267     assert_eq!(len, 0);
268 
269     wasip1::path_unlink_file(dir_fd, "a").expect("removing a file");
270 }
271 
main()272 fn main() {
273     let mut args = env::args();
274     let prog = args.next().unwrap();
275     let arg = if let Some(arg) = args.next() {
276         arg
277     } else {
278         eprintln!("usage: {prog} <scratch directory>");
279         process::exit(1);
280     };
281 
282     // Open scratch directory
283     let dir_fd = match open_scratch_directory(&arg) {
284         Ok(dir_fd) => dir_fd,
285         Err(err) => {
286             eprintln!("{err}");
287             process::exit(1)
288         }
289     };
290 
291     // Run the tests.
292     unsafe { test_fd_readdir(dir_fd) }
293     unsafe { test_fd_readdir_lots(dir_fd) }
294     unsafe { test_fd_readdir_unicode_boundary(dir_fd) }
295     unsafe { test_fd_readdir_past_end(dir_fd) }
296 }
297