blob: aecd89669b482c450de6bcb438059e29f27a5694 [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package poll
import (
"sync/atomic"
"syscall"
"unsafe"
)
type SysFile struct {
// RefCountPtr is a pointer to the reference count of Sysfd.
//
// WASI preview 1 lacks a dup(2) system call. When the os and net packages
// need to share a file/socket, instead of duplicating the underlying file
// descriptor, we instead provide a way to copy FD instances and manage the
// underlying file descriptor with reference counting.
RefCountPtr *int32
// RefCount is the reference count of Sysfd. When a copy of an FD is made,
// it points to the reference count of the original FD instance.
RefCount int32
// Cache for the file type, lazily initialized when Seek is called.
Filetype uint32
// If the file represents a directory, this field contains the current
// readdir position. It is reset to zero if the program calls Seek(0, 0).
Dircookie uint64
// Absolute path of the file, as returned by syscall.PathOpen;
// this is used by Fchdir to emulate setting the current directory
// to an open file descriptor.
Path string
// TODO(achille): it could be meaningful to move isFile from FD to a method
// on this struct type, and expose it as `IsFile() bool` which derives the
// result from the Filetype field. We would need to ensure that Filetype is
// always set instead of being lazily initialized.
}
func (s *SysFile) init() {
if s.RefCountPtr == nil {
s.RefCount = 1
s.RefCountPtr = &s.RefCount
}
}
func (s *SysFile) ref() SysFile {
atomic.AddInt32(s.RefCountPtr, +1)
return SysFile{RefCountPtr: s.RefCountPtr}
}
func (s *SysFile) destroy(fd int) error {
if s.RefCountPtr != nil && atomic.AddInt32(s.RefCountPtr, -1) > 0 {
return nil
}
// We don't use ignoringEINTR here because POSIX does not define
// whether the descriptor is closed if close returns EINTR.
// If the descriptor is indeed closed, using a loop would race
// with some other goroutine opening a new descriptor.
// (The Linux kernel guarantees that it is closed on an EINTR error.)
return CloseFunc(fd)
}
// Copy creates a copy of the FD.
//
// The FD instance points to the same underlying file descriptor. The file
// descriptor isn't closed until all FD instances that refer to it have been
// closed/destroyed.
func (fd *FD) Copy() FD {
return FD{
Sysfd: fd.Sysfd,
SysFile: fd.SysFile.ref(),
IsStream: fd.IsStream,
ZeroReadIsEOF: fd.ZeroReadIsEOF,
isBlocking: fd.isBlocking,
isFile: fd.isFile,
}
}
// dupCloseOnExecOld always errors on wasip1 because there is no mechanism to
// duplicate file descriptors.
func dupCloseOnExecOld(fd int) (int, string, error) {
return -1, "dup", syscall.ENOSYS
}
// Fchdir wraps syscall.Fchdir.
func (fd *FD) Fchdir() error {
if err := fd.incref(); err != nil {
return err
}
defer fd.decref()
return syscall.Chdir(fd.Path)
}
// ReadDir wraps syscall.ReadDir.
// We treat this like an ordinary system call rather than a call
// that tries to fill the buffer.
func (fd *FD) ReadDir(buf []byte, cookie syscall.Dircookie) (int, error) {
if err := fd.incref(); err != nil {
return 0, err
}
defer fd.decref()
for {
n, err := syscall.ReadDir(fd.Sysfd, buf, cookie)
if err != nil {
n = 0
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
// Do not call eofError; caller does not expect to see io.EOF.
return n, err
}
}
func (fd *FD) ReadDirent(buf []byte) (int, error) {
n, err := fd.ReadDir(buf, fd.Dircookie)
if err != nil {
return 0, err
}
if n <= 0 {
return n, nil // EOF
}
// We assume that the caller of ReadDirent will consume the entire buffer
// up to the last full entry, so we scan through the buffer looking for the
// value of the last next cookie.
b := buf[:n]
for len(b) > 0 {
next, ok := direntNext(b)
if !ok {
break
}
size, ok := direntReclen(b)
if !ok {
break
}
if size > uint64(len(b)) {
break
}
fd.Dircookie = syscall.Dircookie(next)
b = b[size:]
}
// Trim a potentially incomplete trailing entry; this is necessary because
// the code in src/os/dir_unix.go does not deal well with partial values in
// calls to direntReclen, etc... and ends up causing an early EOF before all
// directory entries were consumed. ReadDirent is called with a large enough
// buffer (8 KiB) that at least one entry should always fit, tho this seems
// a bit brittle but cannot be addressed without a large change of the
// algorithm in the os.(*File).readdir method.
return n - len(b), nil
}
// Seek wraps syscall.Seek.
func (fd *FD) Seek(offset int64, whence int) (int64, error) {
if err := fd.incref(); err != nil {
return 0, err
}
defer fd.decref()
// syscall.Filetype is a uint8 but we store it as a uint32 in SysFile in
// order to use atomic load/store on the field, which is why we have to
// perform this type conversion.
fileType := syscall.Filetype(atomic.LoadUint32(&fd.Filetype))
if fileType == syscall.FILETYPE_UNKNOWN {
var stat syscall.Stat_t
if err := fd.Fstat(&stat); err != nil {
return 0, err
}
fileType = stat.Filetype
atomic.StoreUint32(&fd.Filetype, uint32(fileType))
}
if fileType == syscall.FILETYPE_DIRECTORY {
// If the file descriptor is opened on a directory, we reset the readdir
// cookie when seeking back to the beginning to allow reusing the file
// descriptor to scan the directory again.
if offset == 0 && whence == 0 {
fd.Dircookie = 0
return 0, nil
} else {
return 0, syscall.EINVAL
}
}
return syscall.Seek(fd.Sysfd, offset, whence)
}
// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-dirent-record
const sizeOfDirent = 24
func direntReclen(buf []byte) (uint64, bool) {
namelen, ok := direntNamlen(buf)
return sizeOfDirent + namelen, ok
}
func direntNamlen(buf []byte) (uint64, bool) {
return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen))
}
func direntNext(buf []byte) (uint64, bool) {
return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Next), unsafe.Sizeof(syscall.Dirent{}.Next))
}
// readInt returns the size-bytes unsigned integer in native byte order at offset off.
func readInt(b []byte, off, size uintptr) (u uint64, ok bool) {
if len(b) < int(off+size) {
return 0, false
}
return readIntLE(b[off:], size), true
}
func readIntLE(b []byte, size uintptr) uint64 {
switch size {
case 1:
return uint64(b[0])
case 2:
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8
case 4:
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24
case 8:
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
default:
panic("internal/poll: readInt with unsupported size")
}
}