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> { 20 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 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. 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 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 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 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 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 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 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