github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/sysfs/file.go (about) 1 package sysfs 2 3 import ( 4 "io" 5 "io/fs" 6 "os" 7 "time" 8 9 experimentalsys "github.com/bananabytelabs/wazero/experimental/sys" 10 "github.com/bananabytelabs/wazero/internal/fsapi" 11 "github.com/bananabytelabs/wazero/sys" 12 ) 13 14 func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) { 15 // Return constant stat, which has fake times, but keep the underlying 16 // file mode. Fake times are needed to pass wasi-testsuite. 17 // https://github.com/WebAssembly/wasi-testsuite/blob/af57727/tests/rust/src/bin/fd_filestat_get.rs#L1-L19 18 var mode fs.FileMode 19 if st, err := f.Stat(); err != nil { 20 return nil, err 21 } else { 22 mode = st.Mode() 23 } 24 var flag experimentalsys.Oflag 25 if stdin { 26 flag = experimentalsys.O_RDONLY 27 } else { 28 flag = experimentalsys.O_WRONLY 29 } 30 var file fsapi.File 31 if of, ok := f.(*os.File); ok { 32 // This is ok because functions that need path aren't used by stdioFile 33 file = newOsFile("", flag, 0, of) 34 } else { 35 file = &fsFile{file: f} 36 } 37 return &stdioFile{File: file, st: sys.Stat_t{Mode: mode, Nlink: 1}}, nil 38 } 39 40 func OpenFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (*os.File, experimentalsys.Errno) { 41 if flag&experimentalsys.O_DIRECTORY != 0 && flag&(experimentalsys.O_WRONLY|experimentalsys.O_RDWR) != 0 { 42 return nil, experimentalsys.EISDIR // invalid to open a directory writeable 43 } 44 return openFile(path, flag, perm) 45 } 46 47 func OpenOSFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) { 48 f, errno := OpenFile(path, flag, perm) 49 if errno != 0 { 50 return nil, errno 51 } 52 return newOsFile(path, flag, perm, f), 0 53 } 54 55 func OpenFSFile(fs fs.FS, path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) { 56 if flag&experimentalsys.O_DIRECTORY != 0 && flag&(experimentalsys.O_WRONLY|experimentalsys.O_RDWR) != 0 { 57 return nil, experimentalsys.EISDIR // invalid to open a directory writeable 58 } 59 f, err := fs.Open(path) 60 if errno := experimentalsys.UnwrapOSError(err); errno != 0 { 61 return nil, errno 62 } 63 // Don't return an os.File because the path is not absolute. osFile needs 64 // the path to be real and certain FS.File impls are subrooted. 65 return &fsFile{fs: fs, name: path, file: f}, 0 66 } 67 68 type stdioFile struct { 69 fsapi.File 70 st sys.Stat_t 71 } 72 73 // SetAppend implements File.SetAppend 74 func (f *stdioFile) SetAppend(bool) experimentalsys.Errno { 75 // Ignore for stdio. 76 return 0 77 } 78 79 // IsAppend implements File.SetAppend 80 func (f *stdioFile) IsAppend() bool { 81 return true 82 } 83 84 // Stat implements File.Stat 85 func (f *stdioFile) Stat() (sys.Stat_t, experimentalsys.Errno) { 86 return f.st, 0 87 } 88 89 // Close implements File.Close 90 func (f *stdioFile) Close() experimentalsys.Errno { 91 return 0 92 } 93 94 // fsFile is used for wrapped fs.File, like os.Stdin or any fs.File 95 // implementation. Notably, this does not have access to the full file path. 96 // so certain operations can't be supported, such as inode lookups on Windows. 97 type fsFile struct { 98 experimentalsys.UnimplementedFile 99 100 // fs is the file-system that opened the file, or nil when wrapped for 101 // pre-opens like stdio. 102 fs fs.FS 103 104 // name is what was used in fs for Open, so it may not be the actual path. 105 name string 106 107 // file is always set, possibly an os.File like os.Stdin. 108 file fs.File 109 110 // reopenDir is true if reopen should be called before Readdir. This flag 111 // is deferred until Readdir to prevent redundant rewinds. This could 112 // happen if Seek(0) was called twice, or if in Windows, Seek(0) was called 113 // before Readdir. 114 reopenDir bool 115 116 // closed is true when closed was called. This ensures proper sys.EBADF 117 closed bool 118 119 // cachedStat includes fields that won't change while a file is open. 120 cachedSt *cachedStat 121 } 122 123 type cachedStat struct { 124 // dev is the same as sys.Stat_t Dev. 125 dev uint64 126 127 // dev is the same as sys.Stat_t Ino. 128 ino sys.Inode 129 130 // isDir is sys.Stat_t Mode masked with fs.ModeDir 131 isDir bool 132 } 133 134 // cachedStat returns the cacheable parts of sys.Stat_t or an error if they 135 // couldn't be retrieved. 136 func (f *fsFile) cachedStat() (dev uint64, ino sys.Inode, isDir bool, errno experimentalsys.Errno) { 137 if f.cachedSt == nil { 138 if _, errno = f.Stat(); errno != 0 { 139 return 140 } 141 } 142 return f.cachedSt.dev, f.cachedSt.ino, f.cachedSt.isDir, 0 143 } 144 145 // Dev implements the same method as documented on sys.File 146 func (f *fsFile) Dev() (uint64, experimentalsys.Errno) { 147 dev, _, _, errno := f.cachedStat() 148 return dev, errno 149 } 150 151 // Ino implements the same method as documented on sys.File 152 func (f *fsFile) Ino() (sys.Inode, experimentalsys.Errno) { 153 _, ino, _, errno := f.cachedStat() 154 return ino, errno 155 } 156 157 // IsDir implements the same method as documented on sys.File 158 func (f *fsFile) IsDir() (bool, experimentalsys.Errno) { 159 _, _, isDir, errno := f.cachedStat() 160 return isDir, errno 161 } 162 163 // IsAppend implements the same method as documented on sys.File 164 func (f *fsFile) IsAppend() bool { 165 return false 166 } 167 168 // SetAppend implements the same method as documented on sys.File 169 func (f *fsFile) SetAppend(bool) (errno experimentalsys.Errno) { 170 return fileError(f, f.closed, experimentalsys.ENOSYS) 171 } 172 173 // Stat implements the same method as documented on sys.File 174 func (f *fsFile) Stat() (sys.Stat_t, experimentalsys.Errno) { 175 if f.closed { 176 return sys.Stat_t{}, experimentalsys.EBADF 177 } 178 179 st, errno := statFile(f.file) 180 switch errno { 181 case 0: 182 f.cachedSt = &cachedStat{dev: st.Dev, ino: st.Ino, isDir: st.Mode&fs.ModeDir == fs.ModeDir} 183 case experimentalsys.EIO: 184 errno = experimentalsys.EBADF 185 } 186 return st, errno 187 } 188 189 // Read implements the same method as documented on sys.File 190 func (f *fsFile) Read(buf []byte) (n int, errno experimentalsys.Errno) { 191 if n, errno = read(f.file, buf); errno != 0 { 192 // Defer validation overhead until we've already had an error. 193 errno = fileError(f, f.closed, errno) 194 } 195 return 196 } 197 198 // Pread implements the same method as documented on sys.File 199 func (f *fsFile) Pread(buf []byte, off int64) (n int, errno experimentalsys.Errno) { 200 if ra, ok := f.file.(io.ReaderAt); ok { 201 if n, errno = pread(ra, buf, off); errno != 0 { 202 // Defer validation overhead until we've already had an error. 203 errno = fileError(f, f.closed, errno) 204 } 205 return 206 } 207 208 // See /RATIONALE.md "fd_pread: io.Seeker fallback when io.ReaderAt is not supported" 209 if rs, ok := f.file.(io.ReadSeeker); ok { 210 // Determine the current position in the file, as we need to revert it. 211 currentOffset, err := rs.Seek(0, io.SeekCurrent) 212 if err != nil { 213 return 0, fileError(f, f.closed, experimentalsys.UnwrapOSError(err)) 214 } 215 216 // Put the read position back when complete. 217 defer func() { _, _ = rs.Seek(currentOffset, io.SeekStart) }() 218 219 // If the current offset isn't in sync with this reader, move it. 220 if off != currentOffset { 221 if _, err = rs.Seek(off, io.SeekStart); err != nil { 222 return 0, fileError(f, f.closed, experimentalsys.UnwrapOSError(err)) 223 } 224 } 225 226 n, err = rs.Read(buf) 227 if errno = experimentalsys.UnwrapOSError(err); errno != 0 { 228 // Defer validation overhead until we've already had an error. 229 errno = fileError(f, f.closed, errno) 230 } 231 } else { 232 errno = experimentalsys.ENOSYS // unsupported 233 } 234 return 235 } 236 237 // Seek implements the same method as documented on sys.File 238 func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno experimentalsys.Errno) { 239 // If this is a directory, and we're attempting to seek to position zero, 240 // we have to re-open the file to ensure the directory state is reset. 241 var isDir bool 242 if offset == 0 && whence == io.SeekStart { 243 if isDir, errno = f.IsDir(); errno == 0 && isDir { 244 f.reopenDir = true 245 return 246 } 247 } 248 249 if s, ok := f.file.(io.Seeker); ok { 250 if newOffset, errno = seek(s, offset, whence); errno != 0 { 251 // Defer validation overhead until we've already had an error. 252 errno = fileError(f, f.closed, errno) 253 } 254 } else { 255 errno = experimentalsys.ENOSYS // unsupported 256 } 257 return 258 } 259 260 // Readdir implements the same method as documented on sys.File 261 // 262 // Notably, this uses readdirFile or fs.ReadDirFile if available. This does not 263 // return inodes on windows. 264 func (f *fsFile) Readdir(n int) (dirents []experimentalsys.Dirent, errno experimentalsys.Errno) { 265 // Windows lets you Readdir after close, FS.File also may not implement 266 // close in a meaningful way. read our closed field to return consistent 267 // results. 268 if f.closed { 269 errno = experimentalsys.EBADF 270 return 271 } 272 273 if f.reopenDir { // re-open the directory if needed. 274 f.reopenDir = false 275 if errno = adjustReaddirErr(f, f.closed, f.reopen()); errno != 0 { 276 return 277 } 278 } 279 280 if of, ok := f.file.(readdirFile); ok { 281 // We can't use f.name here because it is the path up to the sys.FS, 282 // not necessarily the real path. For this reason, Windows may not be 283 // able to populate inodes. However, Darwin and Linux will. 284 if dirents, errno = readdir(of, "", n); errno != 0 { 285 errno = adjustReaddirErr(f, f.closed, errno) 286 } 287 return 288 } 289 290 // Try with FS.ReadDirFile which is available on api.FS implementations 291 // like embed:FS. 292 if rdf, ok := f.file.(fs.ReadDirFile); ok { 293 entries, e := rdf.ReadDir(n) 294 if errno = adjustReaddirErr(f, f.closed, e); errno != 0 { 295 return 296 } 297 dirents = make([]experimentalsys.Dirent, 0, len(entries)) 298 for _, e := range entries { 299 // By default, we don't attempt to read inode data 300 dirents = append(dirents, experimentalsys.Dirent{Name: e.Name(), Type: e.Type()}) 301 } 302 } else { 303 errno = experimentalsys.EBADF // not a directory 304 } 305 return 306 } 307 308 // Write implements the same method as documented on sys.File. 309 func (f *fsFile) Write(buf []byte) (n int, errno experimentalsys.Errno) { 310 if w, ok := f.file.(io.Writer); ok { 311 if n, errno = write(w, buf); errno != 0 { 312 // Defer validation overhead until we've already had an error. 313 errno = fileError(f, f.closed, errno) 314 } 315 } else { 316 errno = experimentalsys.ENOSYS // unsupported 317 } 318 return 319 } 320 321 // Pwrite implements the same method as documented on sys.File. 322 func (f *fsFile) Pwrite(buf []byte, off int64) (n int, errno experimentalsys.Errno) { 323 if wa, ok := f.file.(io.WriterAt); ok { 324 if n, errno = pwrite(wa, buf, off); errno != 0 { 325 // Defer validation overhead until we've already had an error. 326 errno = fileError(f, f.closed, errno) 327 } 328 } else { 329 errno = experimentalsys.ENOSYS // unsupported 330 } 331 return 332 } 333 334 // Close implements the same method as documented on sys.File. 335 func (f *fsFile) Close() experimentalsys.Errno { 336 if f.closed { 337 return 0 338 } 339 f.closed = true 340 return f.close() 341 } 342 343 func (f *fsFile) close() experimentalsys.Errno { 344 return experimentalsys.UnwrapOSError(f.file.Close()) 345 } 346 347 // IsNonblock implements the same method as documented on fsapi.File 348 func (f *fsFile) IsNonblock() bool { 349 return false 350 } 351 352 // SetNonblock implements the same method as documented on fsapi.File 353 func (f *fsFile) SetNonblock(bool) experimentalsys.Errno { 354 return experimentalsys.ENOSYS 355 } 356 357 // Poll implements the same method as documented on fsapi.File 358 func (f *fsFile) Poll(fsapi.Pflag, int32) (ready bool, errno experimentalsys.Errno) { 359 return false, experimentalsys.ENOSYS 360 } 361 362 // dirError is used for commands that work against a directory, but not a file. 363 func dirError(f experimentalsys.File, isClosed bool, errno experimentalsys.Errno) experimentalsys.Errno { 364 if vErrno := validate(f, isClosed, false, true); vErrno != 0 { 365 return vErrno 366 } 367 return errno 368 } 369 370 // fileError is used for commands that work against a file, but not a directory. 371 func fileError(f experimentalsys.File, isClosed bool, errno experimentalsys.Errno) experimentalsys.Errno { 372 if vErrno := validate(f, isClosed, true, false); vErrno != 0 { 373 return vErrno 374 } 375 return errno 376 } 377 378 // validate is used to making syscalls which will fail. 379 func validate(f experimentalsys.File, isClosed, wantFile, wantDir bool) experimentalsys.Errno { 380 if isClosed { 381 return experimentalsys.EBADF 382 } 383 384 isDir, errno := f.IsDir() 385 if errno != 0 { 386 return errno 387 } 388 389 if wantFile && isDir { 390 return experimentalsys.EISDIR 391 } else if wantDir && !isDir { 392 return experimentalsys.ENOTDIR 393 } 394 return 0 395 } 396 397 func read(r io.Reader, buf []byte) (n int, errno experimentalsys.Errno) { 398 if len(buf) == 0 { 399 return 0, 0 // less overhead on zero-length reads. 400 } 401 402 n, err := r.Read(buf) 403 return n, experimentalsys.UnwrapOSError(err) 404 } 405 406 func pread(ra io.ReaderAt, buf []byte, off int64) (n int, errno experimentalsys.Errno) { 407 if len(buf) == 0 { 408 return 0, 0 // less overhead on zero-length reads. 409 } 410 411 n, err := ra.ReadAt(buf, off) 412 return n, experimentalsys.UnwrapOSError(err) 413 } 414 415 func seek(s io.Seeker, offset int64, whence int) (int64, experimentalsys.Errno) { 416 if uint(whence) > io.SeekEnd { 417 return 0, experimentalsys.EINVAL // negative or exceeds the largest valid whence 418 } 419 420 newOffset, err := s.Seek(offset, whence) 421 return newOffset, experimentalsys.UnwrapOSError(err) 422 } 423 424 // reopenFile allows re-opening a file for reasons such as applying flags or 425 // directory iteration. 426 type reopenFile func() experimentalsys.Errno 427 428 // compile-time check to ensure fsFile.reopen implements reopenFile. 429 var _ reopenFile = (*fsFile)(nil).reopen 430 431 // reopen implements the same method as documented on reopenFile. 432 func (f *fsFile) reopen() experimentalsys.Errno { 433 _ = f.close() 434 var err error 435 f.file, err = f.fs.Open(f.name) 436 return experimentalsys.UnwrapOSError(err) 437 } 438 439 // readdirFile allows masking the `Readdir` function on os.File. 440 type readdirFile interface { 441 Readdir(n int) ([]fs.FileInfo, error) 442 } 443 444 // readdir uses readdirFile.Readdir, special casing windows when path !="". 445 func readdir(f readdirFile, path string, n int) (dirents []experimentalsys.Dirent, errno experimentalsys.Errno) { 446 fis, e := f.Readdir(n) 447 if errno = experimentalsys.UnwrapOSError(e); errno != 0 { 448 return 449 } 450 451 dirents = make([]experimentalsys.Dirent, 0, len(fis)) 452 453 // linux/darwin won't have to fan out to lstat, but windows will. 454 var ino sys.Inode 455 for fi := range fis { 456 t := fis[fi] 457 // inoFromFileInfo is more efficient than sys.NewStat_t, as it gets the 458 // inode without allocating an instance and filling other fields. 459 if ino, errno = inoFromFileInfo(path, t); errno != 0 { 460 return 461 } 462 dirents = append(dirents, experimentalsys.Dirent{Name: t.Name(), Ino: ino, Type: t.Mode().Type()}) 463 } 464 return 465 } 466 467 func write(w io.Writer, buf []byte) (n int, errno experimentalsys.Errno) { 468 if len(buf) == 0 { 469 return 0, 0 // less overhead on zero-length writes. 470 } 471 472 n, err := w.Write(buf) 473 return n, experimentalsys.UnwrapOSError(err) 474 } 475 476 func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno experimentalsys.Errno) { 477 if len(buf) == 0 { 478 return 0, 0 // less overhead on zero-length writes. 479 } 480 481 n, err := w.WriteAt(buf, off) 482 return n, experimentalsys.UnwrapOSError(err) 483 } 484 485 func chtimes(path string, atim, mtim int64) (errno experimentalsys.Errno) { //nolint:unused 486 // When both inputs are omitted, there is nothing to change. 487 if atim == experimentalsys.UTIME_OMIT && mtim == experimentalsys.UTIME_OMIT { 488 return 489 } 490 491 // UTIME_OMIT is expensive until progress is made in Go, as it requires a 492 // stat to read-back the value to re-apply. 493 // - https://github.com/golang/go/issues/32558. 494 // - https://go-review.googlesource.com/c/go/+/219638 (unmerged) 495 var st sys.Stat_t 496 if atim == experimentalsys.UTIME_OMIT || mtim == experimentalsys.UTIME_OMIT { 497 if st, errno = stat(path); errno != 0 { 498 return 499 } 500 } 501 502 var atime, mtime time.Time 503 if atim == experimentalsys.UTIME_OMIT { 504 atime = epochNanosToTime(st.Atim) 505 mtime = epochNanosToTime(mtim) 506 } else if mtim == experimentalsys.UTIME_OMIT { 507 atime = epochNanosToTime(atim) 508 mtime = epochNanosToTime(st.Mtim) 509 } else { 510 atime = epochNanosToTime(atim) 511 mtime = epochNanosToTime(mtim) 512 } 513 return experimentalsys.UnwrapOSError(os.Chtimes(path, atime, mtime)) 514 } 515 516 func epochNanosToTime(epochNanos int64) time.Time { //nolint:unused 517 seconds := epochNanos / 1e9 518 nanos := epochNanos % 1e9 519 return time.Unix(seconds, nanos) 520 }