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}