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  }