1// Copyright 2023 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package poll
6
7import (
8	"internal/byteorder"
9	"sync/atomic"
10	"syscall"
11	"unsafe"
12)
13
14type SysFile struct {
15	// RefCountPtr is a pointer to the reference count of Sysfd.
16	//
17	// WASI preview 1 lacks a dup(2) system call. When the os and net packages
18	// need to share a file/socket, instead of duplicating the underlying file
19	// descriptor, we instead provide a way to copy FD instances and manage the
20	// underlying file descriptor with reference counting.
21	RefCountPtr *int32
22
23	// RefCount is the reference count of Sysfd. When a copy of an FD is made,
24	// it points to the reference count of the original FD instance.
25	RefCount int32
26
27	// Cache for the file type, lazily initialized when Seek is called.
28	Filetype uint32
29
30	// If the file represents a directory, this field contains the current
31	// readdir position. It is reset to zero if the program calls Seek(0, 0).
32	Dircookie uint64
33
34	// Absolute path of the file, as returned by syscall.PathOpen;
35	// this is used by Fchdir to emulate setting the current directory
36	// to an open file descriptor.
37	Path string
38
39	// TODO(achille): it could be meaningful to move isFile from FD to a method
40	// on this struct type, and expose it as `IsFile() bool` which derives the
41	// result from the Filetype field. We would need to ensure that Filetype is
42	// always set instead of being lazily initialized.
43}
44
45func (s *SysFile) init() {
46	if s.RefCountPtr == nil {
47		s.RefCount = 1
48		s.RefCountPtr = &s.RefCount
49	}
50}
51
52func (s *SysFile) ref() SysFile {
53	atomic.AddInt32(s.RefCountPtr, +1)
54	return SysFile{RefCountPtr: s.RefCountPtr}
55}
56
57func (s *SysFile) destroy(fd int) error {
58	if s.RefCountPtr != nil && atomic.AddInt32(s.RefCountPtr, -1) > 0 {
59		return nil
60	}
61
62	// We don't use ignoringEINTR here because POSIX does not define
63	// whether the descriptor is closed if close returns EINTR.
64	// If the descriptor is indeed closed, using a loop would race
65	// with some other goroutine opening a new descriptor.
66	// (The Linux kernel guarantees that it is closed on an EINTR error.)
67	return CloseFunc(fd)
68}
69
70// Copy creates a copy of the FD.
71//
72// The FD instance points to the same underlying file descriptor. The file
73// descriptor isn't closed until all FD instances that refer to it have been
74// closed/destroyed.
75func (fd *FD) Copy() FD {
76	return FD{
77		Sysfd:         fd.Sysfd,
78		SysFile:       fd.SysFile.ref(),
79		IsStream:      fd.IsStream,
80		ZeroReadIsEOF: fd.ZeroReadIsEOF,
81		isBlocking:    fd.isBlocking,
82		isFile:        fd.isFile,
83	}
84}
85
86// dupCloseOnExecOld always errors on wasip1 because there is no mechanism to
87// duplicate file descriptors.
88func dupCloseOnExecOld(fd int) (int, string, error) {
89	return -1, "dup", syscall.ENOSYS
90}
91
92// Fchdir wraps syscall.Fchdir.
93func (fd *FD) Fchdir() error {
94	if err := fd.incref(); err != nil {
95		return err
96	}
97	defer fd.decref()
98	return syscall.Chdir(fd.Path)
99}
100
101// ReadDir wraps syscall.ReadDir.
102// We treat this like an ordinary system call rather than a call
103// that tries to fill the buffer.
104func (fd *FD) ReadDir(buf []byte, cookie syscall.Dircookie) (int, error) {
105	if err := fd.incref(); err != nil {
106		return 0, err
107	}
108	defer fd.decref()
109	for {
110		n, err := syscall.ReadDir(fd.Sysfd, buf, cookie)
111		if err != nil {
112			n = 0
113			if err == syscall.EAGAIN && fd.pd.pollable() {
114				if err = fd.pd.waitRead(fd.isFile); err == nil {
115					continue
116				}
117			}
118		}
119		// Do not call eofError; caller does not expect to see io.EOF.
120		return n, err
121	}
122}
123
124func (fd *FD) ReadDirent(buf []byte) (int, error) {
125	n, err := fd.ReadDir(buf, fd.Dircookie)
126	if err != nil {
127		return 0, err
128	}
129	if n <= 0 {
130		return n, nil // EOF
131	}
132
133	// We assume that the caller of ReadDirent will consume the entire buffer
134	// up to the last full entry, so we scan through the buffer looking for the
135	// value of the last next cookie.
136	b := buf[:n]
137
138	for len(b) > 0 {
139		next, ok := direntNext(b)
140		if !ok {
141			break
142		}
143		size, ok := direntReclen(b)
144		if !ok {
145			break
146		}
147		if size > uint64(len(b)) {
148			break
149		}
150		fd.Dircookie = syscall.Dircookie(next)
151		b = b[size:]
152	}
153
154	// Trim a potentially incomplete trailing entry; this is necessary because
155	// the code in src/os/dir_unix.go does not deal well with partial values in
156	// calls to direntReclen, etc... and ends up causing an early EOF before all
157	// directory entries were consumed. ReadDirent is called with a large enough
158	// buffer (8 KiB) that at least one entry should always fit, tho this seems
159	// a bit brittle but cannot be addressed without a large change of the
160	// algorithm in the os.(*File).readdir method.
161	return n - len(b), nil
162}
163
164// Seek wraps syscall.Seek.
165func (fd *FD) Seek(offset int64, whence int) (int64, error) {
166	if err := fd.incref(); err != nil {
167		return 0, err
168	}
169	defer fd.decref()
170	// syscall.Filetype is a uint8 but we store it as a uint32 in SysFile in
171	// order to use atomic load/store on the field, which is why we have to
172	// perform this type conversion.
173	fileType := syscall.Filetype(atomic.LoadUint32(&fd.Filetype))
174
175	if fileType == syscall.FILETYPE_UNKNOWN {
176		var stat syscall.Stat_t
177		if err := fd.Fstat(&stat); err != nil {
178			return 0, err
179		}
180		fileType = stat.Filetype
181		atomic.StoreUint32(&fd.Filetype, uint32(fileType))
182	}
183
184	if fileType == syscall.FILETYPE_DIRECTORY {
185		// If the file descriptor is opened on a directory, we reset the readdir
186		// cookie when seeking back to the beginning to allow reusing the file
187		// descriptor to scan the directory again.
188		if offset == 0 && whence == 0 {
189			fd.Dircookie = 0
190			return 0, nil
191		} else {
192			return 0, syscall.EINVAL
193		}
194	}
195
196	return syscall.Seek(fd.Sysfd, offset, whence)
197}
198
199// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-dirent-record
200const sizeOfDirent = 24
201
202func direntReclen(buf []byte) (uint64, bool) {
203	namelen, ok := direntNamlen(buf)
204	return sizeOfDirent + namelen, ok
205}
206
207func direntNamlen(buf []byte) (uint64, bool) {
208	return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen))
209}
210
211func direntNext(buf []byte) (uint64, bool) {
212	return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Next), unsafe.Sizeof(syscall.Dirent{}.Next))
213}
214
215// readInt returns the size-bytes unsigned integer in native byte order at offset off.
216func readInt(b []byte, off, size uintptr) (u uint64, ok bool) {
217	if len(b) < int(off+size) {
218		return 0, false
219	}
220	return readIntLE(b[off:], size), true
221}
222
223func readIntLE(b []byte, size uintptr) uint64 {
224	switch size {
225	case 1:
226		return uint64(b[0])
227	case 2:
228		return uint64(byteorder.LeUint16(b))
229	case 4:
230		return uint64(byteorder.LeUint32(b))
231	case 8:
232		return uint64(byteorder.LeUint64(b))
233	default:
234		panic("internal/poll: readInt with unsupported size")
235	}
236}
237