file.go

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