osfile.go

  1package sysfs
  2
  3import (
  4	"io"
  5	"io/fs"
  6	"os"
  7
  8	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
  9	"github.com/tetratelabs/wazero/internal/fsapi"
 10	"github.com/tetratelabs/wazero/sys"
 11)
 12
 13func newOsFile(path string, flag experimentalsys.Oflag, perm fs.FileMode, f *os.File) fsapi.File {
 14	// On POSIX, if a file is removed from or added to the directory after the
 15	// most recent call to opendir() or rewinddir(), whether a subsequent call
 16	// to readdir() returns an entry for that file is unspecified.
 17	//
 18	// And Windows cannot read files added to a directory after it was opened.
 19	// This was noticed in #1087 in zig tests.
 20	//
 21	// So there is no guarantee that files added after opendir() will be visible
 22	// in readdir(). Since we want those files to be visible, we need to
 23	// reopendir() to get the new state of the directory before readdir().
 24	return &osFile{path: path, flag: flag, perm: perm, reopenDir: true, file: f, fd: f.Fd()}
 25}
 26
 27// osFile is a file opened with this package, and uses os.File or syscalls to
 28// implement api.File.
 29type osFile struct {
 30	path string
 31	flag experimentalsys.Oflag
 32	perm fs.FileMode
 33	file *os.File
 34	fd   uintptr
 35
 36	// reopenDir is true if reopen should be called before Readdir. This flag
 37	// is deferred until Readdir to prevent redundant rewinds. This could
 38	// happen if Seek(0) was called twice, or if in Windows, Seek(0) was called
 39	// before Readdir.
 40	reopenDir bool
 41
 42	// closed is true when closed was called. This ensures proper sys.EBADF
 43	closed bool
 44
 45	// cachedStat includes fields that won't change while a file is open.
 46	cachedSt *cachedStat
 47}
 48
 49// cachedStat returns the cacheable parts of sys.Stat_t or an error if they
 50// couldn't be retrieved.
 51func (f *osFile) cachedStat() (dev uint64, ino sys.Inode, isDir bool, errno experimentalsys.Errno) {
 52	if f.cachedSt == nil {
 53		if _, errno = f.Stat(); errno != 0 {
 54			return
 55		}
 56	}
 57	return f.cachedSt.dev, f.cachedSt.ino, f.cachedSt.isDir, 0
 58}
 59
 60// Dev implements the same method as documented on sys.File
 61func (f *osFile) Dev() (uint64, experimentalsys.Errno) {
 62	dev, _, _, errno := f.cachedStat()
 63	return dev, errno
 64}
 65
 66// Ino implements the same method as documented on sys.File
 67func (f *osFile) Ino() (sys.Inode, experimentalsys.Errno) {
 68	_, ino, _, errno := f.cachedStat()
 69	return ino, errno
 70}
 71
 72// IsDir implements the same method as documented on sys.File
 73func (f *osFile) IsDir() (bool, experimentalsys.Errno) {
 74	_, _, isDir, errno := f.cachedStat()
 75	return isDir, errno
 76}
 77
 78// IsAppend implements File.IsAppend
 79func (f *osFile) IsAppend() bool {
 80	return f.flag&experimentalsys.O_APPEND == experimentalsys.O_APPEND
 81}
 82
 83// SetAppend implements the same method as documented on sys.File
 84func (f *osFile) SetAppend(enable bool) (errno experimentalsys.Errno) {
 85	if enable {
 86		f.flag |= experimentalsys.O_APPEND
 87	} else {
 88		f.flag &= ^experimentalsys.O_APPEND
 89	}
 90
 91	// appendMode cannot be changed later, so we have to re-open the file
 92	// https://github.com/golang/go/blob/go1.23/src/os/file_unix.go#L60
 93	return fileError(f, f.closed, f.reopen())
 94}
 95
 96func (f *osFile) reopen() (errno experimentalsys.Errno) {
 97	var (
 98		isDir  bool
 99		offset int64
100		err    error
101	)
102
103	isDir, errno = f.IsDir()
104	if errno != 0 {
105		return errno
106	}
107
108	if !isDir {
109		offset, err = f.file.Seek(0, io.SeekCurrent)
110		if err != nil {
111			return experimentalsys.UnwrapOSError(err)
112		}
113	}
114
115	// Clear any create or trunc flag, as we are re-opening, not re-creating.
116	flag := f.flag &^ (experimentalsys.O_CREAT | experimentalsys.O_TRUNC)
117	file, errno := OpenFile(f.path, flag, f.perm)
118	if errno != 0 {
119		return errno
120	}
121	errno = f.checkSameFile(file)
122	if errno != 0 {
123		return errno
124	}
125
126	if !isDir {
127		_, err = file.Seek(offset, io.SeekStart)
128		if err != nil {
129			_ = file.Close()
130			return experimentalsys.UnwrapOSError(err)
131		}
132	}
133
134	// Only update f on success.
135	_ = f.file.Close()
136	f.file = file
137	f.fd = file.Fd()
138	return 0
139}
140
141func (f *osFile) checkSameFile(osf *os.File) experimentalsys.Errno {
142	fi1, err := f.file.Stat()
143	if err != nil {
144		return experimentalsys.UnwrapOSError(err)
145	}
146	fi2, err := osf.Stat()
147	if err != nil {
148		return experimentalsys.UnwrapOSError(err)
149	}
150	if os.SameFile(fi1, fi2) {
151		return 0
152	}
153	return experimentalsys.ENOENT
154}
155
156// IsNonblock implements the same method as documented on fsapi.File
157func (f *osFile) IsNonblock() bool {
158	return isNonblock(f)
159}
160
161// SetNonblock implements the same method as documented on fsapi.File
162func (f *osFile) SetNonblock(enable bool) (errno experimentalsys.Errno) {
163	if enable {
164		f.flag |= experimentalsys.O_NONBLOCK
165	} else {
166		f.flag &= ^experimentalsys.O_NONBLOCK
167	}
168	if errno = setNonblock(f.fd, enable); errno != 0 {
169		return fileError(f, f.closed, errno)
170	}
171	return 0
172}
173
174// Stat implements the same method as documented on sys.File
175func (f *osFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
176	if f.closed {
177		return sys.Stat_t{}, experimentalsys.EBADF
178	}
179
180	st, errno := statFile(f.file)
181	switch errno {
182	case 0:
183		f.cachedSt = &cachedStat{dev: st.Dev, ino: st.Ino, isDir: st.Mode&fs.ModeDir == fs.ModeDir}
184	case experimentalsys.EIO:
185		errno = experimentalsys.EBADF
186	}
187	return st, errno
188}
189
190// Read implements the same method as documented on sys.File
191func (f *osFile) Read(buf []byte) (n int, errno experimentalsys.Errno) {
192	if len(buf) == 0 {
193		return 0, 0 // Short-circuit 0-len reads.
194	}
195	if nonBlockingFileReadSupported && f.IsNonblock() {
196		n, errno = readFd(f.fd, buf)
197	} else {
198		n, errno = read(f.file, buf)
199	}
200	if errno != 0 {
201		// Defer validation overhead until we've already had an error.
202		errno = fileError(f, f.closed, errno)
203	}
204	return
205}
206
207// Pread implements the same method as documented on sys.File
208func (f *osFile) Pread(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
209	if n, errno = pread(f.file, buf, off); errno != 0 {
210		// Defer validation overhead until we've already had an error.
211		errno = fileError(f, f.closed, errno)
212	}
213	return
214}
215
216// Seek implements the same method as documented on sys.File
217func (f *osFile) Seek(offset int64, whence int) (newOffset int64, errno experimentalsys.Errno) {
218	if newOffset, errno = seek(f.file, offset, whence); errno != 0 {
219		// Defer validation overhead until we've already had an error.
220		errno = fileError(f, f.closed, errno)
221
222		// If the error was trying to rewind a directory, re-open it. Notably,
223		// seeking to zero on a directory doesn't work on Windows with Go 1.19.
224		if errno == experimentalsys.EISDIR && offset == 0 && whence == io.SeekStart {
225			errno = 0
226			f.reopenDir = true
227		}
228	}
229	return
230}
231
232// Poll implements the same method as documented on fsapi.File
233func (f *osFile) Poll(flag fsapi.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
234	return poll(f.fd, flag, timeoutMillis)
235}
236
237// Readdir implements File.Readdir. Notably, this uses "Readdir", not
238// "ReadDir", from os.File.
239func (f *osFile) Readdir(n int) (dirents []experimentalsys.Dirent, errno experimentalsys.Errno) {
240	if f.reopenDir { // re-open the directory if needed.
241		f.reopenDir = false
242		if errno = adjustReaddirErr(f, f.closed, f.reopen()); errno != 0 {
243			return
244		}
245	}
246
247	if dirents, errno = readdir(f.file, f.path, n); errno != 0 {
248		errno = adjustReaddirErr(f, f.closed, errno)
249	}
250	return
251}
252
253// Write implements the same method as documented on sys.File
254func (f *osFile) Write(buf []byte) (n int, errno experimentalsys.Errno) {
255	if len(buf) == 0 {
256		return 0, 0 // Short-circuit 0-len writes.
257	}
258	if nonBlockingFileWriteSupported && f.IsNonblock() {
259		n, errno = writeFd(f.fd, buf)
260	} else if n, errno = write(f.file, buf); errno != 0 {
261		// Defer validation overhead until we've already had an error.
262		errno = fileError(f, f.closed, errno)
263	}
264	return
265}
266
267// Pwrite implements the same method as documented on sys.File
268func (f *osFile) Pwrite(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
269	if n, errno = pwrite(f.file, buf, off); errno != 0 {
270		// Defer validation overhead until we've already had an error.
271		errno = fileError(f, f.closed, errno)
272	}
273	return
274}
275
276// Truncate implements the same method as documented on sys.File
277func (f *osFile) Truncate(size int64) (errno experimentalsys.Errno) {
278	if size < 0 {
279		return experimentalsys.EINVAL
280	}
281	if errno = experimentalsys.UnwrapOSError(f.file.Truncate(size)); errno != 0 {
282		// Defer validation overhead until we've already had an error.
283		errno = fileError(f, f.closed, errno)
284	}
285	return
286}
287
288// Sync implements the same method as documented on sys.File
289func (f *osFile) Sync() experimentalsys.Errno {
290	return fsync(f.file)
291}
292
293// Datasync implements the same method as documented on sys.File
294func (f *osFile) Datasync() experimentalsys.Errno {
295	return datasync(f.file)
296}
297
298// Utimens implements the same method as documented on sys.File
299func (f *osFile) Utimens(atim, mtim int64) experimentalsys.Errno {
300	if f.closed {
301		return experimentalsys.EBADF
302	}
303
304	err := futimens(f.fd, atim, mtim)
305	return experimentalsys.UnwrapOSError(err)
306}
307
308// Close implements the same method as documented on sys.File
309func (f *osFile) Close() experimentalsys.Errno {
310	if f.closed {
311		return 0
312	}
313	f.closed = true
314	return f.close()
315}
316
317func (f *osFile) close() experimentalsys.Errno {
318	return experimentalsys.UnwrapOSError(f.file.Close())
319}