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