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