github.com/tetratelabs/wazero@v1.2.1/internal/sysfs/file.go (about)

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