github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/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/wasilibs/wazerox/experimental/sys"
    10  	"github.com/wasilibs/wazerox/internal/fsapi"
    11  	"github.com/wasilibs/wazerox/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  }