Skip to content

Commit

Permalink
os: add Root.Stat and Root.Lstat
Browse files Browse the repository at this point in the history
For #67002

Change-Id: I0903f45dbb4c44ea0280c340c96c5f3c3c0781be
Reviewed-on: https://go-review.googlesource.com/c/go/+/627475
Reviewed-by: Ian Lance Taylor <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Quim Muntal <[email protected]>
  • Loading branch information
neild committed Nov 20, 2024
1 parent 49d24d4 commit 3d56891
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 1 deletion.
2 changes: 2 additions & 0 deletions api/next/67002.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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
22 changes: 21 additions & 1 deletion src/internal/syscall/unix/at_wasip1.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import (
"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
// to avoid changing AccessTime or ModifiedTime.
// 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 {
Expand Down Expand Up @@ -49,6 +51,24 @@ func Openat(dirfd int, path string, flags int, perm uint32) (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(
Expand Down
9 changes: 9 additions & 0 deletions src/internal/syscall/windows/at_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
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) {
Expand All @@ -37,6 +38,10 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
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
Expand Down Expand Up @@ -70,6 +75,10 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
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.
Expand Down
1 change: 1 addition & 0 deletions src/internal/syscall/windows/syscall_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
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 (
Expand Down
24 changes: 24 additions & 0 deletions src/os/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,22 @@ func (r *Root) Remove(name string) error {
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,
Expand All @@ -134,6 +150,14 @@ func (r *Root) logOpen(name string) {
}
}

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.
//
Expand Down
20 changes: 20 additions & 0 deletions src/os/root_noopenat.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ func rootOpenFileNolog(r *Root, name string, flag int, perm FileMode) (*File, er
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}
Expand Down
115 changes: 115 additions & 0 deletions src/os/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,67 @@ func TestRootOpenFileAsRoot(t *testing.T) {
}
}

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.
//
Expand Down Expand Up @@ -599,6 +660,24 @@ var rootConsistencyTestCases = []rootConsistencyTest{{
"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{
Expand Down Expand Up @@ -817,6 +896,42 @@ func TestRootConsistencyRemove(t *testing.T) {
}
}

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":
Expand Down
18 changes: 18 additions & 0 deletions src/os/root_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,24 @@ func rootOpenDir(parent int, name string) (int, error) {
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))
Expand Down
34 changes: 34 additions & 0 deletions src/os/root_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,40 @@ func rootOpenDir(parent syscall.Handle, name string) (syscall.Handle, error) {
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))
}
Expand Down
9 changes: 9 additions & 0 deletions src/os/stat_wasip1.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ func fillFileStatFromSys(fs *fileStat, name string) {
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.
Expand Down
1 change: 1 addition & 0 deletions src/syscall/types_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const (
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 (
Expand Down

0 comments on commit 3d56891

Please sign in to comment.