github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/sysfs/sysfs_test.go (about) 1 package sysfs 2 3 import ( 4 _ "embed" 5 "io" 6 "io/fs" 7 "os" 8 "path" 9 "runtime" 10 "sort" 11 "testing" 12 13 experimentalsys "github.com/bananabytelabs/wazero/experimental/sys" 14 "github.com/bananabytelabs/wazero/internal/platform" 15 "github.com/bananabytelabs/wazero/internal/testing/require" 16 "github.com/bananabytelabs/wazero/sys" 17 ) 18 19 func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS experimentalsys.FS) { 20 file := "file" 21 realPath := path.Join(tmpDir, file) 22 err := os.WriteFile(realPath, []byte{}, 0o600) 23 require.NoError(t, err) 24 25 f, errno := testFS.OpenFile(file, experimentalsys.O_RDWR, 0) 26 require.EqualErrno(t, 0, errno) 27 defer f.Close() 28 29 // If the write flag was honored, we should be able to write! 30 fileContents := []byte{1, 2, 3, 4} 31 n, errno := f.Write(fileContents) 32 require.EqualErrno(t, 0, errno) 33 require.Equal(t, len(fileContents), n) 34 35 // Verify the contents actually wrote. 36 b, err := os.ReadFile(realPath) 37 require.NoError(t, err) 38 require.Equal(t, fileContents, b) 39 40 require.EqualErrno(t, 0, f.Close()) 41 42 // re-create as read-only, using 0444 to allow read-back on windows. 43 require.NoError(t, os.Remove(realPath)) 44 f, errno = testFS.OpenFile(file, experimentalsys.O_RDONLY|experimentalsys.O_CREAT, 0o444) 45 require.EqualErrno(t, 0, errno) 46 defer f.Close() 47 48 if runtime.GOOS != "windows" { 49 // If the read-only flag was honored, we should not be able to write! 50 _, err = f.Write(fileContents) 51 require.EqualErrno(t, experimentalsys.EBADF, experimentalsys.UnwrapOSError(err)) 52 } 53 54 // Verify stat on the file 55 stat, errno := f.Stat() 56 require.EqualErrno(t, 0, errno) 57 require.Equal(t, fs.FileMode(0o444), stat.Mode.Perm()) 58 59 // from os.TestDirFSPathsValid 60 if runtime.GOOS != "windows" { 61 t.Run("strange name", func(t *testing.T) { 62 f, errno = testFS.OpenFile(`e:xperi\ment.txt`, experimentalsys.O_WRONLY|experimentalsys.O_CREAT|experimentalsys.O_TRUNC, 0o600) 63 require.EqualErrno(t, 0, errno) 64 defer f.Close() 65 66 _, errno = f.Stat() 67 require.EqualErrno(t, 0, errno) 68 }) 69 } 70 71 t.Run("O_TRUNC", func(t *testing.T) { 72 tmpDir := t.TempDir() 73 testFS := DirFS(tmpDir) 74 75 name := "truncate" 76 realPath := path.Join(tmpDir, name) 77 require.NoError(t, os.WriteFile(realPath, []byte("123456"), 0o0666)) 78 79 f, errno = testFS.OpenFile(name, experimentalsys.O_RDWR|experimentalsys.O_TRUNC, 0o444) 80 require.EqualErrno(t, 0, errno) 81 require.EqualErrno(t, 0, f.Close()) 82 83 actual, err := os.ReadFile(realPath) 84 require.NoError(t, err) 85 require.Equal(t, 0, len(actual)) 86 }) 87 } 88 89 func testOpen_Read(t *testing.T, testFS experimentalsys.FS, requireFileIno, expectDirIno bool) { 90 t.Helper() 91 92 t.Run("doesn't exist", func(t *testing.T) { 93 _, errno := testFS.OpenFile("nope", experimentalsys.O_RDONLY, 0) 94 95 // We currently follow os.Open not syscall.Open, so the error is wrapped. 96 require.EqualErrno(t, experimentalsys.ENOENT, errno) 97 }) 98 99 t.Run("readdir . opens root", func(t *testing.T) { 100 f, errno := testFS.OpenFile(".", experimentalsys.O_RDONLY, 0) 101 require.EqualErrno(t, 0, errno) 102 defer f.Close() 103 104 dirents := requireReaddir(t, f, -1, expectDirIno) 105 106 // Scrub inodes so we can compare expectations without them. 107 for i := range dirents { 108 dirents[i].Ino = 0 109 } 110 111 require.Equal(t, []experimentalsys.Dirent{ 112 {Name: "animals.txt", Type: 0}, 113 {Name: "dir", Type: fs.ModeDir}, 114 {Name: "empty.txt", Type: 0}, 115 {Name: "emptydir", Type: fs.ModeDir}, 116 {Name: "sub", Type: fs.ModeDir}, 117 }, dirents) 118 }) 119 120 t.Run("readdir empty", func(t *testing.T) { 121 f, errno := testFS.OpenFile("emptydir", experimentalsys.O_RDONLY, 0) 122 require.EqualErrno(t, 0, errno) 123 defer f.Close() 124 125 entries := requireReaddir(t, f, -1, expectDirIno) 126 require.Zero(t, len(entries)) 127 }) 128 129 t.Run("readdir partial", func(t *testing.T) { 130 dirF, errno := testFS.OpenFile("dir", experimentalsys.O_RDONLY, 0) 131 require.EqualErrno(t, 0, errno) 132 defer dirF.Close() 133 134 dirents1, errno := dirF.Readdir(1) 135 require.EqualErrno(t, 0, errno) 136 require.Equal(t, 1, len(dirents1)) 137 138 dirents2, errno := dirF.Readdir(1) 139 require.EqualErrno(t, 0, errno) 140 require.Equal(t, 1, len(dirents2)) 141 142 // read exactly the last entry 143 dirents3, errno := dirF.Readdir(1) 144 require.EqualErrno(t, 0, errno) 145 require.Equal(t, 1, len(dirents3)) 146 147 dirents := []experimentalsys.Dirent{dirents1[0], dirents2[0], dirents3[0]} 148 sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name }) 149 150 requireIno(t, dirents, expectDirIno) 151 152 // Scrub inodes so we can compare expectations without them. 153 for i := range dirents { 154 dirents[i].Ino = 0 155 } 156 157 require.Equal(t, []experimentalsys.Dirent{ 158 {Name: "-", Type: 0}, 159 {Name: "a-", Type: fs.ModeDir}, 160 {Name: "ab-", Type: 0}, 161 }, dirents) 162 163 // no error reading an exhausted directory 164 _, errno = dirF.Readdir(1) 165 require.EqualErrno(t, 0, errno) 166 }) 167 168 t.Run("file exists", func(t *testing.T) { 169 f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0) 170 require.EqualErrno(t, 0, errno) 171 defer f.Close() 172 173 fileContents := []byte(`bear 174 cat 175 shark 176 dinosaur 177 human 178 `) 179 // Ensure it implements Pread 180 lenToRead := len(fileContents) - 1 181 buf := make([]byte, lenToRead) 182 n, errno := f.Pread(buf, 1) 183 require.EqualErrno(t, 0, errno) 184 require.Equal(t, lenToRead, n) 185 require.Equal(t, fileContents[1:], buf) 186 187 // Ensure it implements Seek 188 offset, errno := f.Seek(1, io.SeekStart) 189 require.EqualErrno(t, 0, errno) 190 require.Equal(t, int64(1), offset) 191 192 // Read should pick up from position 1 193 n, errno = f.Read(buf) 194 require.EqualErrno(t, 0, errno) 195 require.Equal(t, lenToRead, n) 196 require.Equal(t, fileContents[1:], buf) 197 }) 198 199 t.Run("file stat includes inode", func(t *testing.T) { 200 f, errno := testFS.OpenFile("empty.txt", experimentalsys.O_RDONLY, 0) 201 require.EqualErrno(t, 0, errno) 202 defer f.Close() 203 204 st, errno := f.Stat() 205 require.EqualErrno(t, 0, errno) 206 207 // Results are inconsistent, so don't validate the opposite. 208 if requireFileIno { 209 require.NotEqual(t, uint64(0), st.Ino, "%+v", st) 210 } 211 }) 212 213 // Make sure O_RDONLY isn't treated bitwise as it is usually zero. 214 t.Run("or'd flag", func(t *testing.T) { 215 // Example of a flag that can be or'd into O_RDONLY even if not 216 // currently supported in WASI or GOOS=js 217 const O_NOATIME = experimentalsys.Oflag(0x40000) 218 219 f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY|O_NOATIME, 0) 220 require.EqualErrno(t, 0, errno) 221 defer f.Close() 222 }) 223 224 t.Run("writing to a read-only file is EBADF", func(t *testing.T) { 225 f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0) 226 require.EqualErrno(t, 0, errno) 227 defer f.Close() 228 229 _, errno = f.Write([]byte{1, 2, 3, 4}) 230 require.EqualErrno(t, experimentalsys.EBADF, errno) 231 }) 232 233 t.Run("opening a directory with O_RDWR is EISDIR", func(t *testing.T) { 234 _, errno := testFS.OpenFile("sub", experimentalsys.O_DIRECTORY|experimentalsys.O_RDWR, 0) 235 require.EqualErrno(t, experimentalsys.EISDIR, errno) 236 }) 237 } 238 239 func testLstat(t *testing.T, testFS experimentalsys.FS) { 240 _, errno := testFS.Lstat("cat") 241 require.EqualErrno(t, experimentalsys.ENOENT, errno) 242 _, errno = testFS.Lstat("sub/cat") 243 require.EqualErrno(t, experimentalsys.ENOENT, errno) 244 245 var st sys.Stat_t 246 247 t.Run("dir", func(t *testing.T) { 248 st, errno = testFS.Lstat(".") 249 require.EqualErrno(t, 0, errno) 250 require.True(t, st.Mode.IsDir()) 251 require.NotEqual(t, uint64(0), st.Ino) 252 }) 253 254 var stFile sys.Stat_t 255 256 t.Run("file", func(t *testing.T) { 257 stFile, errno = testFS.Lstat("animals.txt") 258 require.EqualErrno(t, 0, errno) 259 260 require.Zero(t, stFile.Mode.Type()) 261 require.Equal(t, int64(30), stFile.Size) 262 require.NotEqual(t, uint64(0), st.Ino) 263 }) 264 265 t.Run("link to file", func(t *testing.T) { 266 requireLinkStat(t, testFS, "animals.txt", stFile) 267 }) 268 269 var stSubdir sys.Stat_t 270 t.Run("subdir", func(t *testing.T) { 271 stSubdir, errno = testFS.Lstat("sub") 272 require.EqualErrno(t, 0, errno) 273 274 require.True(t, stSubdir.Mode.IsDir()) 275 require.NotEqual(t, uint64(0), st.Ino) 276 }) 277 278 t.Run("link to dir", func(t *testing.T) { 279 requireLinkStat(t, testFS, "sub", stSubdir) 280 }) 281 282 t.Run("link to dir link", func(t *testing.T) { 283 pathLink := "sub-link" 284 stLink, errno := testFS.Lstat(pathLink) 285 require.EqualErrno(t, 0, errno) 286 287 requireLinkStat(t, testFS, pathLink, stLink) 288 }) 289 } 290 291 func requireLinkStat(t *testing.T, testFS experimentalsys.FS, path string, stat sys.Stat_t) { 292 link := path + "-link" 293 stLink, errno := testFS.Lstat(link) 294 require.EqualErrno(t, 0, errno) 295 296 require.NotEqual(t, stat.Ino, stLink.Ino) // inodes are not equal 297 require.Equal(t, fs.ModeSymlink, stLink.Mode.Type()) 298 // From https://linux.die.net/man/2/lstat: 299 // The size of a symbolic link is the length of the pathname it 300 // contains, without a terminating null byte. 301 if runtime.GOOS == "windows" { // size is zero, not the path length 302 require.Zero(t, stLink.Size) 303 } else { 304 require.Equal(t, int64(len(path)), stLink.Size) 305 } 306 } 307 308 func testStat(t *testing.T, testFS experimentalsys.FS) { 309 _, errno := testFS.Stat("cat") 310 require.EqualErrno(t, experimentalsys.ENOENT, errno) 311 _, errno = testFS.Stat("sub/cat") 312 require.EqualErrno(t, experimentalsys.ENOENT, errno) 313 314 st, errno := testFS.Stat("sub/test.txt") 315 require.EqualErrno(t, 0, errno) 316 317 require.False(t, st.Mode.IsDir()) 318 require.NotEqual(t, uint64(0), st.Dev) 319 require.NotEqual(t, uint64(0), st.Ino) 320 321 st, errno = testFS.Stat("sub") 322 require.EqualErrno(t, 0, errno) 323 324 require.True(t, st.Mode.IsDir()) 325 // windows before go 1.20 has trouble reading the inode information on directories. 326 if runtime.GOOS != "windows" || platform.IsAtLeastGo120 { 327 require.NotEqual(t, uint64(0), st.Dev) 328 require.NotEqual(t, uint64(0), st.Ino) 329 } 330 } 331 332 // requireReaddir ensures the input file is a directory, and returns its 333 // entries. 334 func requireReaddir(t *testing.T, f experimentalsys.File, n int, expectDirIno bool) []experimentalsys.Dirent { 335 entries, errno := f.Readdir(n) 336 require.EqualErrno(t, 0, errno) 337 338 sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) 339 requireIno(t, entries, expectDirIno) 340 return entries 341 } 342 343 func testReadlink(t *testing.T, readFS, writeFS experimentalsys.FS) { 344 testLinks := []struct { 345 old, dst string 346 }{ 347 // Same dir. 348 {old: "animals.txt", dst: "symlinked-animals.txt"}, 349 {old: "sub/test.txt", dst: "sub/symlinked-test.txt"}, 350 // Parent to sub. 351 {old: "animals.txt", dst: "sub/symlinked-animals.txt"}, 352 // Sub to parent. 353 {old: "sub/test.txt", dst: "symlinked-zoo.txt"}, 354 } 355 356 for _, tl := range testLinks { 357 errno := writeFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat 358 require.Zero(t, errno, "%v", tl) 359 360 dst, errno := readFS.Readlink(tl.dst) 361 require.EqualErrno(t, 0, errno) 362 require.Equal(t, tl.old, dst) 363 } 364 365 t.Run("errors", func(t *testing.T) { 366 _, err := readFS.Readlink("sub/test.txt") 367 require.Error(t, err) 368 _, err = readFS.Readlink("") 369 require.Error(t, err) 370 _, err = readFS.Readlink("animals.txt") 371 require.Error(t, err) 372 }) 373 } 374 375 func requireIno(t *testing.T, dirents []experimentalsys.Dirent, expectDirIno bool) { 376 for i := range dirents { 377 d := dirents[i] 378 if expectDirIno { 379 require.NotEqual(t, uint64(0), d.Ino, "%+v", d) 380 d.Ino = 0 381 } else { 382 require.Zero(t, d.Ino, "%+v", d) 383 } 384 } 385 } 386 387 // joinPath avoids us having to rename fields just to avoid conflict with the 388 // path package. 389 func joinPath(dirName, baseName string) string { 390 return path.Join(dirName, baseName) 391 }