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