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}