open_file_windows.go

  1package sysfs
  2
  3import (
  4	"io/fs"
  5	"os"
  6	"strings"
  7	"syscall"
  8	"unsafe"
  9
 10	"github.com/tetratelabs/wazero/experimental/sys"
 11)
 12
 13func openFile(path string, oflag sys.Oflag, perm fs.FileMode) (*os.File, sys.Errno) {
 14	isDir := oflag&sys.O_DIRECTORY > 0
 15	flag := toOsOpenFlag(oflag)
 16
 17	// TODO: document why we are opening twice
 18	fd, err := open(path, flag|syscall.O_CLOEXEC, uint32(perm))
 19	if err == nil {
 20		return os.NewFile(uintptr(fd), path), 0
 21	}
 22
 23	// TODO: Set FILE_SHARE_DELETE for directory as well.
 24	f, err := os.OpenFile(path, flag, perm)
 25	errno := sys.UnwrapOSError(err)
 26	if errno == 0 {
 27		return f, 0
 28	}
 29
 30	switch errno {
 31	case sys.EINVAL:
 32		// WASI expects ENOTDIR for a file path with a trailing slash.
 33		if strings.HasSuffix(path, "/") {
 34			errno = sys.ENOTDIR
 35		}
 36	// To match expectations of WASI, e.g. TinyGo TestStatBadDir, return
 37	// ENOENT, not ENOTDIR.
 38	case sys.ENOTDIR:
 39		errno = sys.ENOENT
 40	case sys.ENOENT:
 41		if isSymlink(path) {
 42			// Either symlink or hard link not found. We change the returned
 43			// errno depending on if it is symlink or not to have consistent
 44			// behavior across OSes.
 45			if isDir {
 46				// Dangling symlink dir must raise ENOTDIR.
 47				errno = sys.ENOTDIR
 48			} else {
 49				errno = sys.ELOOP
 50			}
 51		}
 52	}
 53	return f, errno
 54}
 55
 56const supportedSyscallOflag = sys.O_NONBLOCK
 57
 58// Map to synthetic values here https://github.com/golang/go/blob/go1.20/src/syscall/types_windows.go#L34-L48
 59func withSyscallOflag(oflag sys.Oflag, flag int) int {
 60	// O_DIRECTORY not defined in windows
 61	// O_DSYNC not defined in windows
 62	// O_NOFOLLOW not defined in windows
 63	if oflag&sys.O_NONBLOCK != 0 {
 64		flag |= syscall.O_NONBLOCK
 65	}
 66	// O_RSYNC not defined in windows
 67	return flag
 68}
 69
 70func isSymlink(path string) bool {
 71	if st, e := os.Lstat(path); e == nil && st.Mode()&os.ModeSymlink != 0 {
 72		return true
 73	}
 74	return false
 75}
 76
 77// # Differences from syscall.Open
 78//
 79// This code is based on syscall.Open from the below link with some differences
 80// https://github.com/golang/go/blame/go1.20/src/syscall/syscall_windows.go#L308-L379
 81//
 82//   - syscall.O_CREAT doesn't imply syscall.GENERIC_WRITE as that breaks
 83//     flag expectations in wasi.
 84//   - add support for setting FILE_SHARE_DELETE.
 85func open(path string, mode int, perm uint32) (fd syscall.Handle, err error) {
 86	if len(path) == 0 {
 87		return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
 88	}
 89	pathp, err := syscall.UTF16PtrFromString(path)
 90	if err != nil {
 91		return syscall.InvalidHandle, err
 92	}
 93	var access uint32
 94	switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
 95	case syscall.O_RDONLY:
 96		access = syscall.GENERIC_READ
 97	case syscall.O_WRONLY:
 98		access = syscall.GENERIC_WRITE
 99	case syscall.O_RDWR:
100		access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
101	}
102	if mode&syscall.O_APPEND != 0 {
103		access &^= syscall.GENERIC_WRITE
104		access |= syscall.FILE_APPEND_DATA
105	}
106	sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE)
107	var sa *syscall.SecurityAttributes
108	if mode&syscall.O_CLOEXEC == 0 {
109		var _sa syscall.SecurityAttributes
110		_sa.Length = uint32(unsafe.Sizeof(sa))
111		_sa.InheritHandle = 1
112		sa = &_sa
113	}
114	var createmode uint32
115	switch {
116	case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
117		createmode = syscall.CREATE_NEW
118	case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC):
119		createmode = syscall.CREATE_ALWAYS
120	case mode&syscall.O_CREAT == syscall.O_CREAT:
121		createmode = syscall.OPEN_ALWAYS
122	case mode&syscall.O_TRUNC == syscall.O_TRUNC:
123		createmode = syscall.TRUNCATE_EXISTING
124	default:
125		createmode = syscall.OPEN_EXISTING
126	}
127	var attrs uint32 = syscall.FILE_ATTRIBUTE_NORMAL
128	if perm&syscall.S_IWRITE == 0 {
129		attrs = syscall.FILE_ATTRIBUTE_READONLY
130		if createmode == syscall.CREATE_ALWAYS {
131			// We have been asked to create a read-only file.
132			// If the file already exists, the semantics of
133			// the Unix open system call is to preserve the
134			// existing permissions. If we pass CREATE_ALWAYS
135			// and FILE_ATTRIBUTE_READONLY to CreateFile,
136			// and the file already exists, CreateFile will
137			// change the file permissions.
138			// Avoid that to preserve the Unix semantics.
139			h, e := syscall.CreateFile(pathp, access, sharemode, sa, syscall.TRUNCATE_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0)
140			switch e {
141			case syscall.ERROR_FILE_NOT_FOUND, syscall.ERROR_PATH_NOT_FOUND:
142				// File does not exist. These are the same
143				// errors as Errno.Is checks for ErrNotExist.
144				// Carry on to create the file.
145			default:
146				// Success or some different error.
147				return h, e
148			}
149		}
150	}
151
152	// This shouldn't be included before 1.20 to have consistent behavior.
153	// https://github.com/golang/go/commit/0f0aa5d8a6a0253627d58b3aa083b24a1091933f
154	if createmode == syscall.OPEN_EXISTING && access == syscall.GENERIC_READ {
155		// Necessary for opening directory handles.
156		attrs |= syscall.FILE_FLAG_BACKUP_SEMANTICS
157	}
158
159	h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
160	return h, e
161}