mirror of
https://github.com/golang/go.git
synced 2025-05-05 23:53:05 +00:00
os: add Root.Stat and Root.Lstat
For #67002 Change-Id: I0903f45dbb4c44ea0280c340c96c5f3c3c0781be Reviewed-on: https://go-review.googlesource.com/c/go/+/627475 Reviewed-by: Ian Lance Taylor <iant@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
This commit is contained in:
parent
49d24d469e
commit
3d56891969
@ -1,10 +1,12 @@
|
|||||||
pkg os, func OpenRoot(string) (*Root, error) #67002
|
pkg os, func OpenRoot(string) (*Root, error) #67002
|
||||||
pkg os, method (*Root) Close() error #67002
|
pkg os, method (*Root) Close() error #67002
|
||||||
pkg os, method (*Root) Create(string) (*File, 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) Mkdir(string, fs.FileMode) error #67002
|
||||||
pkg os, method (*Root) Name() string #67002
|
pkg os, method (*Root) Name() string #67002
|
||||||
pkg os, method (*Root) Open(string) (*File, error) #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) OpenFile(string, int, fs.FileMode) (*File, error) #67002
|
||||||
pkg os, method (*Root) OpenRoot(string) (*Root, error) #67002
|
pkg os, method (*Root) OpenRoot(string) (*Root, error) #67002
|
||||||
pkg os, method (*Root) Remove(string) 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
|
pkg os, type Root struct #67002
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The values of these constants are not part of the WASI API.
|
||||||
const (
|
const (
|
||||||
// UTIME_OMIT is the sentinel value to indicate that a time value should not
|
// 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
|
// be changed. It is useful for example to indicate for example with UtimesNano
|
||||||
@ -18,7 +19,8 @@ const (
|
|||||||
// Its value must match syscall/fs_wasip1.go
|
// Its value must match syscall/fs_wasip1.go
|
||||||
UTIME_OMIT = -0x2
|
UTIME_OMIT = -0x2
|
||||||
|
|
||||||
AT_REMOVEDIR = 0x200
|
AT_REMOVEDIR = 0x200
|
||||||
|
AT_SYMLINK_NOFOLLOW = 0x100
|
||||||
)
|
)
|
||||||
|
|
||||||
func Unlinkat(dirfd int, path string, flags int) error {
|
func Unlinkat(dirfd int, path string, flags int) error {
|
||||||
@ -49,6 +51,24 @@ func Openat(dirfd int, path string, flags int, perm uint32) (int, error) {
|
|||||||
return syscall.Openat(dirfd, path, flags, perm)
|
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) {
|
func Readlinkat(dirfd int, path string, buf []byte) (int, error) {
|
||||||
var nwritten size
|
var nwritten size
|
||||||
errno := path_readlink(
|
errno := path_readlink(
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
O_DIRECTORY = 0x100000 // target must be a directory
|
O_DIRECTORY = 0x100000 // target must be a directory
|
||||||
O_NOFOLLOW_ANY = 0x20000000 // disallow symlinks anywhere in the path
|
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) {
|
func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall.Handle, e1 error) {
|
||||||
@ -37,6 +38,10 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
|
|||||||
case syscall.O_RDWR:
|
case syscall.O_RDWR:
|
||||||
access = FILE_GENERIC_READ | FILE_GENERIC_WRITE
|
access = FILE_GENERIC_READ | FILE_GENERIC_WRITE
|
||||||
options |= FILE_NON_DIRECTORY_FILE
|
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 {
|
if flag&syscall.O_CREAT != 0 {
|
||||||
access |= FILE_GENERIC_WRITE
|
access |= FILE_GENERIC_WRITE
|
||||||
@ -70,6 +75,10 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
|
|||||||
return syscall.InvalidHandle, err
|
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
|
// We don't use FILE_OVERWRITE/FILE_OVERWRITE_IF, because when opening
|
||||||
// a file with FILE_ATTRIBUTE_READONLY these will replace an existing
|
// a file with FILE_ATTRIBUTE_READONLY these will replace an existing
|
||||||
// file with a new, read-only one.
|
// file with a new, read-only one.
|
||||||
|
@ -41,6 +41,7 @@ const (
|
|||||||
ERROR_LOCK_FAILED syscall.Errno = 167
|
ERROR_LOCK_FAILED syscall.Errno = 167
|
||||||
ERROR_NO_TOKEN syscall.Errno = 1008
|
ERROR_NO_TOKEN syscall.Errno = 1008
|
||||||
ERROR_NO_UNICODE_TRANSLATION syscall.Errno = 1113
|
ERROR_NO_UNICODE_TRANSLATION syscall.Errno = 1113
|
||||||
|
ERROR_CANT_ACCESS_FILE syscall.Errno = 1920
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -126,6 +126,22 @@ func (r *Root) Remove(name string) error {
|
|||||||
return rootRemove(r, name)
|
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) {
|
func (r *Root) logOpen(name string) {
|
||||||
if log := testlog.Logger(); log != nil {
|
if log := testlog.Logger(); log != nil {
|
||||||
// This won't be right if r's name has changed since it was opened,
|
// This won't be right if r's name has changed since it was opened,
|
||||||
@ -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
|
// splitPathInRoot splits a path into components
|
||||||
// and joins it with the given prefix and suffix.
|
// and joins it with the given prefix and suffix.
|
||||||
//
|
//
|
||||||
|
@ -75,6 +75,26 @@ func rootOpenFileNolog(r *Root, name string, flag int, perm FileMode) (*File, er
|
|||||||
return f, nil
|
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 {
|
func rootMkdir(r *Root, name string, perm FileMode) error {
|
||||||
if err := checkPathEscapes(r, name); err != nil {
|
if err := checkPathEscapes(r, name); err != nil {
|
||||||
return &PathError{Op: "mkdirat", Path: name, Err: err}
|
return &PathError{Op: "mkdirat", Path: name, Err: err}
|
||||||
|
@ -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
|
// A rootConsistencyTest is a test case comparing os.Root behavior with
|
||||||
// the corresponding non-Root function.
|
// the corresponding non-Root function.
|
||||||
//
|
//
|
||||||
@ -599,6 +660,24 @@ var rootConsistencyTestCases = []rootConsistencyTest{{
|
|||||||
"link => target",
|
"link => target",
|
||||||
},
|
},
|
||||||
open: "link/",
|
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",
|
name: "unresolved symlink",
|
||||||
fs: []string{
|
fs: []string{
|
||||||
@ -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) {
|
func TestRootRenameAfterOpen(t *testing.T) {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
case "windows":
|
||||||
|
@ -113,6 +113,24 @@ func rootOpenDir(parent int, name string) (int, error) {
|
|||||||
return fd, err
|
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 {
|
func mkdirat(fd int, name string, perm FileMode) error {
|
||||||
return ignoringEINTR(func() error {
|
return ignoringEINTR(func() error {
|
||||||
return unix.Mkdirat(fd, name, syscallMode(perm))
|
return unix.Mkdirat(fd, name, syscallMode(perm))
|
||||||
|
@ -198,6 +198,40 @@ func rootOpenDir(parent syscall.Handle, name string) (syscall.Handle, error) {
|
|||||||
return h, err
|
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 {
|
func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
|
||||||
return windows.Mkdirat(dirfd, name, syscallMode(perm))
|
return windows.Mkdirat(dirfd, name, syscallMode(perm))
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,15 @@ func fillFileStatFromSys(fs *fileStat, name string) {
|
|||||||
case syscall.FILETYPE_SYMBOLIC_LINK:
|
case syscall.FILETYPE_SYMBOLIC_LINK:
|
||||||
fs.mode |= ModeSymlink
|
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.
|
// For testing.
|
||||||
|
@ -48,6 +48,7 @@ const (
|
|||||||
O_CLOEXEC = 0x80000
|
O_CLOEXEC = 0x80000
|
||||||
o_DIRECTORY = 0x100000 // used by internal/syscall/windows
|
o_DIRECTORY = 0x100000 // used by internal/syscall/windows
|
||||||
o_NOFOLLOW_ANY = 0x20000000 // used by internal/syscall/windows
|
o_NOFOLLOW_ANY = 0x20000000 // used by internal/syscall/windows
|
||||||
|
o_OPEN_REPARSE = 0x40000000 // used by internal/syscall/windows
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user