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