github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/sysfs/adapter_test.go (about) 1 package sysfs 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/fs" 8 "os" 9 "path/filepath" 10 "runtime" 11 "syscall" 12 "testing" 13 14 experimentalsys "github.com/wasilibs/wazerox/experimental/sys" 15 "github.com/wasilibs/wazerox/internal/fstest" 16 "github.com/wasilibs/wazerox/internal/testing/require" 17 "github.com/wasilibs/wazerox/sys" 18 ) 19 20 func TestAdaptFS_MkDir(t *testing.T) { 21 testFS := &AdaptFS{FS: os.DirFS(t.TempDir())} 22 23 err := testFS.Mkdir("mkdir", fs.ModeDir) 24 require.EqualErrno(t, experimentalsys.ENOSYS, err) 25 } 26 27 func TestAdaptFS_Chmod(t *testing.T) { 28 testFS := &AdaptFS{FS: os.DirFS(t.TempDir())} 29 30 err := testFS.Chmod("chmod", fs.ModeDir) 31 require.EqualErrno(t, experimentalsys.ENOSYS, err) 32 } 33 34 func TestAdaptFS_Rename(t *testing.T) { 35 tmpDir := t.TempDir() 36 testFS := &AdaptFS{FS: os.DirFS(tmpDir)} 37 38 file1 := "file1" 39 file1Path := joinPath(tmpDir, file1) 40 file1Contents := []byte{1} 41 err := os.WriteFile(file1Path, file1Contents, 0o600) 42 require.NoError(t, err) 43 44 file2 := "file2" 45 file2Path := joinPath(tmpDir, file2) 46 file2Contents := []byte{2} 47 err = os.WriteFile(file2Path, file2Contents, 0o600) 48 require.NoError(t, err) 49 50 err = testFS.Rename(file1, file2) 51 require.EqualErrno(t, experimentalsys.ENOSYS, err) 52 } 53 54 func TestAdaptFS_Rmdir(t *testing.T) { 55 tmpDir := t.TempDir() 56 testFS := &AdaptFS{FS: os.DirFS(tmpDir)} 57 58 path := "rmdir" 59 realPath := joinPath(tmpDir, path) 60 require.NoError(t, os.Mkdir(realPath, 0o700)) 61 62 err := testFS.Rmdir(path) 63 require.EqualErrno(t, experimentalsys.ENOSYS, err) 64 } 65 66 func TestAdaptFS_Unlink(t *testing.T) { 67 tmpDir := t.TempDir() 68 testFS := &AdaptFS{FS: os.DirFS(tmpDir)} 69 70 path := "unlink" 71 realPath := joinPath(tmpDir, path) 72 require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) 73 74 err := testFS.Unlink(path) 75 require.EqualErrno(t, experimentalsys.ENOSYS, err) 76 } 77 78 func TestAdaptFS_UtimesNano(t *testing.T) { 79 tmpDir := t.TempDir() 80 testFS := &AdaptFS{FS: os.DirFS(tmpDir)} 81 82 path := "utimes" 83 realPath := joinPath(tmpDir, path) 84 require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600)) 85 86 err := testFS.Utimens(path, experimentalsys.UTIME_OMIT, experimentalsys.UTIME_OMIT) 87 require.EqualErrno(t, experimentalsys.ENOSYS, err) 88 } 89 90 func TestAdaptFS_Open_Read(t *testing.T) { 91 tmpDir := t.TempDir() 92 // Create a subdirectory, so we can test reads outside the sys.FS root. 93 tmpDir = joinPath(tmpDir, t.Name()) 94 require.NoError(t, os.Mkdir(tmpDir, 0o700)) 95 require.NoError(t, fstest.WriteTestFiles(tmpDir)) 96 testFS := &AdaptFS{FS: os.DirFS(tmpDir)} 97 98 // We can't correct operating system portability issues with os.DirFS on 99 // windows. Use syscall.DirFS instead! 100 testOpen_Read(t, testFS, statSetsIno(), runtime.GOOS != "windows") 101 102 t.Run("path outside root invalid", func(t *testing.T) { 103 _, err := testFS.OpenFile("../foo", experimentalsys.O_RDONLY, 0) 104 105 // sys.FS doesn't allow relative path lookups 106 require.EqualErrno(t, experimentalsys.EINVAL, err) 107 }) 108 } 109 110 func TestAdaptFS_Lstat(t *testing.T) { 111 tmpDir := t.TempDir() 112 testFS := &AdaptFS{FS: os.DirFS(tmpDir)} 113 require.NoError(t, fstest.WriteTestFiles(tmpDir)) 114 115 for _, path := range []string{"animals.txt", "sub", "sub-link"} { 116 fullPath := joinPath(tmpDir, path) 117 linkPath := joinPath(tmpDir, path+"-link") 118 require.NoError(t, os.Symlink(fullPath, linkPath)) 119 120 _, errno := testFS.Lstat(filepath.Base(linkPath)) 121 require.EqualErrno(t, 0, errno) 122 } 123 } 124 125 func TestAdaptFS_Stat(t *testing.T) { 126 tmpDir := t.TempDir() 127 testFS := &AdaptFS{FS: os.DirFS(tmpDir)} 128 require.NoError(t, fstest.WriteTestFiles(tmpDir)) 129 130 testStat(t, testFS) 131 } 132 133 // hackFS cheats the fs.FS contract by opening for write (sys.O_RDWR). 134 // 135 // Until we have an alternate public interface for filesystems, some users will 136 // rely on this. Via testing, we ensure we don't accidentally break them. 137 type hackFS string 138 139 func (dir hackFS) Open(name string) (fs.File, error) { 140 path := ensureTrailingPathSeparator(string(dir)) + name 141 142 if f, err := os.OpenFile(path, os.O_RDWR, 0); err == nil { 143 return f, nil 144 } else if errors.Is(err, syscall.EISDIR) { 145 return os.OpenFile(path, os.O_RDONLY, 0) 146 } else if errors.Is(err, syscall.ENOENT) { 147 return os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0o444) 148 } else { 149 return nil, err 150 } 151 } 152 153 // TestAdaptFS_HackedWrites ensures we allow writes even if they violate the 154 // api.FS contract. 155 func TestAdaptFS_HackedWrites(t *testing.T) { 156 tmpDir := t.TempDir() 157 testFS := &AdaptFS{FS: hackFS(tmpDir)} 158 159 testOpen_O_RDWR(t, tmpDir, testFS) 160 } 161 162 // MaskOsFS helps prove that the fsFile implementation behaves the same way 163 // when a fs.FS returns an os.File or methods we use from it. 164 type MaskOsFS struct { 165 Fs fs.FS 166 167 // ZeroIno helps us test stat with sys.Stat_t work. 168 ZeroIno bool 169 } 170 171 // Open implements the same method as documented on fs.FS 172 func (fs *MaskOsFS) Open(name string) (fs.File, error) { 173 if f, err := fs.Fs.Open(name); err != nil { 174 return nil, err 175 } else if osF, ok := f.(*os.File); !ok { 176 return nil, fmt.Errorf("input not an os.File %v", osF) 177 } else if fs.ZeroIno { 178 return &zeroInoOsFile{osF}, nil 179 } else { 180 return struct{ methodsUsedByFsAdapter }{osF}, nil 181 } 182 } 183 184 // zeroInoOsFile wraps an os.File to override functions that can return 185 // fs.FileInfo. 186 type zeroInoOsFile struct{ *os.File } 187 188 // Readdir implements the same method as documented on os.File 189 func (f *zeroInoOsFile) Readdir(n int) ([]fs.FileInfo, error) { 190 infos, err := f.File.Readdir(n) 191 if err != nil { 192 return nil, err 193 } 194 for i := range infos { 195 infos[i] = withZeroIno(infos[i]) 196 } 197 return infos, nil 198 } 199 200 // Stat implements the same method as documented on fs.File 201 func (f *zeroInoOsFile) Stat() (fs.FileInfo, error) { 202 info, err := f.File.Stat() 203 if err != nil { 204 return nil, err 205 } 206 return withZeroIno(info), nil 207 } 208 209 // withZeroIno clears the sys.Inode which is non-zero on most operating 210 // systems. We test for zero ensure stat logic always checks for sys.Stat_t 211 // first. If that failed, at least one OS would return a non-zero value. 212 func withZeroIno(info fs.FileInfo) fs.FileInfo { 213 st := sys.NewStat_t(info) 214 st.Ino = 0 // clear 215 return &sysFileInfo{info, &st} 216 } 217 218 // sysFileInfo wraps a fs.FileInfo to return *sys.Stat_t from Sys. 219 type sysFileInfo struct { 220 fs.FileInfo 221 sys *sys.Stat_t 222 } 223 224 // Sys implements the same method as documented on fs.FileInfo 225 func (i *sysFileInfo) Sys() any { 226 return i.sys 227 } 228 229 // methodsUsedByFsAdapter includes all functions Adapt supports. This includes 230 // the ability to write files and seek files or directories (directories only 231 // to zero). 232 // 233 // A fs.File implementing this should be functionally equivalent to an os.File, 234 // even if both are less ideal than using DirFS directly, especially on 235 // Windows. 236 // 237 // For example, on Windows, we cannot reliably read the inode for a 238 // sys.Dirent with any of these functions. 239 type methodsUsedByFsAdapter interface { 240 // fs.File is used to implement `stat`, `read` and `close`. 241 fs.File 242 243 // Fd is only used on windows, to back-fill the inode on `stat`. 244 // When implemented, this should dispatch to the same function on os.File. 245 Fd() uintptr 246 247 // io.ReaderAt is used to implement `pread`. 248 io.ReaderAt 249 250 // io.Seeker is used to implement `seek` on a file or directory. It is also 251 // used to implement `pread` when io.ReaderAt isn't implemented. 252 io.Seeker 253 // ^-- TODO: we can also use this to backfill support for `pwrite` 254 255 // Readdir is used to implement `readdir`, and attempts to retrieve inodes. 256 // When implemented, this should dispatch to the same function on os.File. 257 Readdir(n int) ([]fs.FileInfo, error) 258 259 // Readdir is used to implement `readdir` when Readdir is not available. 260 fs.ReadDirFile 261 262 // io.Writer is used to implement `write`. 263 io.Writer 264 265 // io.WriterAt is used to implement `pwrite`. 266 io.WriterAt 267 }