pkg os, func OpenRoot(string) (*Root, error) #67002
pkg os, method (*Root) Close() error #67002
pkg os, method (*Root) Create(string) (*File, error) #67002
+pkg os, method (*Root) Lstat(string) (fs.FileInfo, error) #67002
pkg os, method (*Root) Mkdir(string, fs.FileMode) error #67002
pkg os, method (*Root) Name() string #67002
pkg os, method (*Root) Open(string) (*File, error) #67002
pkg os, method (*Root) OpenFile(string, int, fs.FileMode) (*File, error) #67002
pkg os, method (*Root) OpenRoot(string) (*Root, error) #67002
pkg os, method (*Root) Remove(string) error #67002
+pkg os, method (*Root) Stat(string) (fs.FileInfo, error) #67002
pkg os, type Root struct #67002
"unsafe"
)
+// The values of these constants are not part of the WASI API.
const (
// UTIME_OMIT is the sentinel value to indicate that a time value should not
// be changed. It is useful for example to indicate for example with UtimesNano
// Its value must match syscall/fs_wasip1.go
UTIME_OMIT = -0x2
- AT_REMOVEDIR = 0x200
+ AT_REMOVEDIR = 0x200
+ AT_SYMLINK_NOFOLLOW = 0x100
)
func Unlinkat(dirfd int, path string, flags int) error {
return syscall.Openat(dirfd, path, flags, perm)
}
+func Fstatat(dirfd int, path string, stat *syscall.Stat_t, flags int) error {
+ var filestatFlags uint32
+ if flags&AT_SYMLINK_NOFOLLOW == 0 {
+ filestatFlags |= syscall.LOOKUP_SYMLINK_FOLLOW
+ }
+ return errnoErr(path_filestat_get(
+ int32(dirfd),
+ uint32(filestatFlags),
+ unsafe.StringData(path),
+ size(len(path)),
+ unsafe.Pointer(stat),
+ ))
+}
+
+//go:wasmimport wasi_snapshot_preview1 path_filestat_get
+//go:noescape
+func path_filestat_get(fd int32, flags uint32, path *byte, pathLen size, buf unsafe.Pointer) syscall.Errno
+
func Readlinkat(dirfd int, path string, buf []byte) (int, error) {
var nwritten size
errno := path_readlink(
const (
O_DIRECTORY = 0x100000 // target must be a directory
O_NOFOLLOW_ANY = 0x20000000 // disallow symlinks anywhere in the path
+ O_OPEN_REPARSE = 0x40000000 // FILE_OPEN_REPARSE_POINT, used by Lstat
)
func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall.Handle, e1 error) {
case syscall.O_RDWR:
access = FILE_GENERIC_READ | FILE_GENERIC_WRITE
options |= FILE_NON_DIRECTORY_FILE
+ default:
+ // Stat opens files without requesting read or write permissions,
+ // but we still need to request SYNCHRONIZE.
+ access = SYNCHRONIZE
}
if flag&syscall.O_CREAT != 0 {
access |= FILE_GENERIC_WRITE
return syscall.InvalidHandle, err
}
+ if flag&O_OPEN_REPARSE != 0 {
+ options |= FILE_OPEN_REPARSE_POINT
+ }
+
// We don't use FILE_OVERWRITE/FILE_OVERWRITE_IF, because when opening
// a file with FILE_ATTRIBUTE_READONLY these will replace an existing
// file with a new, read-only one.
ERROR_LOCK_FAILED syscall.Errno = 167
ERROR_NO_TOKEN syscall.Errno = 1008
ERROR_NO_UNICODE_TRANSLATION syscall.Errno = 1113
+ ERROR_CANT_ACCESS_FILE syscall.Errno = 1920
)
const (
return rootRemove(r, name)
}
+// Stat returns a [FileInfo] describing the named file in the root.
+// See [Stat] for more details.
+func (r *Root) Stat(name string) (FileInfo, error) {
+ r.logStat(name)
+ return rootStat(r, name, false)
+}
+
+// Lstat returns a [FileInfo] describing the named file in the root.
+// If the file is a symbolic link, the returned FileInfo
+// describes the symbolic link.
+// See [Lstat] for more details.
+func (r *Root) Lstat(name string) (FileInfo, error) {
+ r.logStat(name)
+ return rootStat(r, name, true)
+}
+
func (r *Root) logOpen(name string) {
if log := testlog.Logger(); log != nil {
// This won't be right if r's name has changed since it was opened,
}
}
+func (r *Root) logStat(name string) {
+ if log := testlog.Logger(); log != nil {
+ // This won't be right if r's name has changed since it was opened,
+ // but it's the best we can do.
+ log.Stat(joinPath(r.Name(), name))
+ }
+}
+
// splitPathInRoot splits a path into components
// and joins it with the given prefix and suffix.
//
return f, nil
}
+func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
+ var fi FileInfo
+ var err error
+ if lstat {
+ err = checkPathEscapesLstat(r, name)
+ if err == nil {
+ fi, err = Lstat(joinPath(r.root.name, name))
+ }
+ } else {
+ err = checkPathEscapes(r, name)
+ if err == nil {
+ fi, err = Stat(joinPath(r.root.name, name))
+ }
+ }
+ if err != nil {
+ return nil, &PathError{Op: "statat", Path: name, Err: underlyingError(err)}
+ }
+ return fi, nil
+}
+
func rootMkdir(r *Root, name string, perm FileMode) error {
if err := checkPathEscapes(r, name); err != nil {
return &PathError{Op: "mkdirat", Path: name, Err: err}
}
}
+func TestRootStat(t *testing.T) {
+ for _, test := range rootTestCases {
+ test.run(t, func(t *testing.T, target string, root *os.Root) {
+ const content = "content"
+ if target != "" {
+ if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ fi, err := root.Stat(test.open)
+ if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) {
+ return
+ }
+ if got, want := fi.Name(), filepath.Base(test.open); got != want {
+ t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
+ }
+ if got, want := fi.Size(), int64(len(content)); got != want {
+ t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
+ }
+ })
+ }
+}
+
+func TestRootLstat(t *testing.T) {
+ for _, test := range rootTestCases {
+ test.run(t, func(t *testing.T, target string, root *os.Root) {
+ const content = "content"
+ wantError := test.wantError
+ if test.ltarget != "" {
+ // Lstat will stat the final link, rather than following it.
+ wantError = false
+ } else if target != "" {
+ if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ fi, err := root.Lstat(test.open)
+ if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) {
+ return
+ }
+ if got, want := fi.Name(), filepath.Base(test.open); got != want {
+ t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
+ }
+ if test.ltarget == "" {
+ if got := fi.Mode(); got&os.ModeSymlink != 0 {
+ t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got)
+ }
+ if got, want := fi.Size(), int64(len(content)); got != want {
+ t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
+ }
+ } else {
+ if got := fi.Mode(); got&os.ModeSymlink == 0 {
+ t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got)
+ }
+ }
+ })
+ }
+}
+
// A rootConsistencyTest is a test case comparing os.Root behavior with
// the corresponding non-Root function.
//
"link => target",
},
open: "link/",
+}, {
+ name: "symlink slash dot",
+ fs: []string{
+ "target/file",
+ "link => target",
+ },
+ open: "link/.",
+}, {
+ name: "file symlink slash",
+ fs: []string{
+ "target",
+ "link => target",
+ },
+ open: "link/",
+ detailedErrorMismatch: func(t *testing.T) bool {
+ // os.Create returns ENOTDIR or EISDIR depending on the platform.
+ return runtime.GOOS == "js"
+ },
}, {
name: "unresolved symlink",
fs: []string{
}
}
+func TestRootConsistencyStat(t *testing.T) {
+ for _, test := range rootConsistencyTestCases {
+ test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
+ var fi os.FileInfo
+ var err error
+ if r == nil {
+ fi, err = os.Stat(path)
+ } else {
+ fi, err = r.Stat(path)
+ }
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
+ })
+ }
+}
+
+func TestRootConsistencyLstat(t *testing.T) {
+ for _, test := range rootConsistencyTestCases {
+ test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
+ var fi os.FileInfo
+ var err error
+ if r == nil {
+ fi, err = os.Lstat(path)
+ } else {
+ fi, err = r.Lstat(path)
+ }
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
+ })
+ }
+}
+
func TestRootRenameAfterOpen(t *testing.T) {
switch runtime.GOOS {
case "windows":
return fd, err
}
+func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
+ fi, err := doInRoot(r, name, func(parent sysfdType, n string) (FileInfo, error) {
+ var fs fileStat
+ if err := unix.Fstatat(parent, n, &fs.sys, unix.AT_SYMLINK_NOFOLLOW); err != nil {
+ return nil, err
+ }
+ fillFileStatFromSys(&fs, name)
+ if !lstat && fs.Mode()&ModeSymlink != 0 {
+ return nil, checkSymlink(parent, n, syscall.ELOOP)
+ }
+ return &fs, nil
+ })
+ if err != nil {
+ return nil, &PathError{Op: "statat", Path: name, Err: err}
+ }
+ return fi, nil
+}
+
func mkdirat(fd int, name string, perm FileMode) error {
return ignoringEINTR(func() error {
return unix.Mkdirat(fd, name, syscallMode(perm))
return h, err
}
+func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
+ if len(name) > 0 && IsPathSeparator(name[len(name)-1]) {
+ // When a filename ends with a path separator,
+ // Lstat behaves like Stat.
+ //
+ // This behavior is not based on a principled decision here,
+ // merely the empirical evidence that Lstat behaves this way.
+ lstat = false
+ }
+ fi, err := doInRoot(r, name, func(parent syscall.Handle, n string) (FileInfo, error) {
+ fd, err := openat(parent, n, windows.O_OPEN_REPARSE, 0)
+ if err != nil {
+ return nil, err
+ }
+ defer syscall.CloseHandle(fd)
+ fi, err := statHandle(name, fd)
+ if err != nil {
+ return nil, err
+ }
+ if !lstat && fi.(*fileStat).isReparseTagNameSurrogate() {
+ link, err := readReparseLinkHandle(fd)
+ if err != nil {
+ return nil, err
+ }
+ return nil, errSymlink(link)
+ }
+ return fi, nil
+ })
+ if err != nil {
+ return nil, &PathError{Op: "statat", Path: name, Err: err}
+ }
+ return fi, nil
+}
+
func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
return windows.Mkdirat(dirfd, name, syscallMode(perm))
}
case syscall.FILETYPE_SYMBOLIC_LINK:
fs.mode |= ModeSymlink
}
+
+ // WASI does not support unix-like permissions, but Go programs are likely
+ // to expect the permission bits to not be zero so we set defaults to help
+ // avoid breaking applications that are migrating to WASM.
+ if fs.sys.Filetype == syscall.FILETYPE_DIRECTORY {
+ fs.mode |= 0700
+ } else {
+ fs.mode |= 0600
+ }
}
// For testing.
O_CLOEXEC = 0x80000
o_DIRECTORY = 0x100000 // used by internal/syscall/windows
o_NOFOLLOW_ANY = 0x20000000 // used by internal/syscall/windows
+ o_OPEN_REPARSE = 0x40000000 // used by internal/syscall/windows
)
const (