From ec9fd3fb38c5aa97345cae517d6a15204581b851 Mon Sep 17 00:00:00 2001 From: Dan Kegel Date: Sun, 28 Nov 2021 08:30:55 -0800 Subject: [PATCH] os, syscall: implement Stat and Lstat File.Stat is left as a stub for now. Tests are a bit stubbed down because os.ReadDir, os.Symlink, and t.TempDir are not yet (fully) implemented. TODO: reimport tests from upstream as those materialize. --- src/os/export_windows_test.go | 12 ++ src/os/file.go | 25 ---- src/os/file_unix.go | 16 ++ src/os/file_windows.go | 18 +++ src/os/path_unix.go | 35 +++++ src/os/path_windows.go | 216 +++++++++++++++++++++++++++ src/os/stat.go | 21 +++ src/os/stat_darwin.go | 51 +++++++ src/os/stat_linux.go | 53 +++++++ src/os/stat_test.go | 96 ++++++++++++ src/os/stat_unix.go | 42 ++++++ src/os/stat_windows.go | 89 +++++++++++ src/os/types.go | 25 ++++ src/os/types_unix.go | 30 ++++ src/os/types_windows.go | 231 +++++++++++++++++++++++++++++ src/syscall/syscall_libc_darwin.go | 94 ++++++++++++ src/syscall/syscall_libc_wasi.go | 89 +++++++++++ 17 files changed, 1118 insertions(+), 25 deletions(-) create mode 100644 src/os/export_windows_test.go create mode 100644 src/os/path_unix.go create mode 100644 src/os/path_windows.go create mode 100644 src/os/stat.go create mode 100644 src/os/stat_darwin.go create mode 100644 src/os/stat_linux.go create mode 100644 src/os/stat_test.go create mode 100644 src/os/stat_unix.go create mode 100644 src/os/stat_windows.go create mode 100644 src/os/types.go create mode 100644 src/os/types_unix.go create mode 100644 src/os/types_windows.go diff --git a/src/os/export_windows_test.go b/src/os/export_windows_test.go new file mode 100644 index 00000000..96837950 --- /dev/null +++ b/src/os/export_windows_test.go @@ -0,0 +1,12 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +// Export for testing. + +var ( + FixLongPath = fixLongPath + CanUseLongPaths = canUseLongPaths +) diff --git a/src/os/file.go b/src/os/file.go index 45c14be6..eb6e00c8 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -171,11 +171,6 @@ func (f *File) Stat() (FileInfo, error) { return nil, &PathError{"stat", f.name, ErrNotImplemented} } -// Sync is a stub, not yet implemented -func (f *File) Sync() error { - return ErrNotImplemented -} - func (f *File) SyscallConn() (syscall.RawConn, error) { return nil, ErrNotImplemented } @@ -190,16 +185,6 @@ func (f *File) Truncate(size int64) error { return &PathError{"truncate", f.name, ErrNotImplemented} } -const ( - PathSeparator = '/' // OS-specific path separator - PathListSeparator = ':' // OS-specific path list separator -) - -// IsPathSeparator reports whether c is a directory separator character. -func IsPathSeparator(c uint8) bool { - return PathSeparator == c -} - // PathError records an error and the operation and file path that caused it. // TODO: PathError moved to io/fs in go 1.16 and left an alias in os/errors.go. // Do the same once we drop support for go 1.15. @@ -245,16 +230,6 @@ const ( O_TRUNC int = syscall.O_TRUNC ) -// Stat is a stub, not yet implemented -func Stat(name string) (FileInfo, error) { - return nil, &PathError{"stat", name, ErrNotImplemented} -} - -// Lstat is a stub, not yet implemented -func Lstat(name string) (FileInfo, error) { - return nil, &PathError{"lstat", name, ErrNotImplemented} -} - func Getwd() (string, error) { return syscall.Getwd() } diff --git a/src/os/file_unix.go b/src/os/file_unix.go index d52df132..d797893c 100644 --- a/src/os/file_unix.go +++ b/src/os/file_unix.go @@ -50,3 +50,19 @@ func (f unixFileHandle) ReadAt(b []byte, offset int64) (n int, err error) { } return } + +// ignoringEINTR makes a function call and repeats it if it returns an +// EINTR error. This appears to be required even though we install all +// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. +// Also #20400 and #36644 are issues in which a signal handler is +// installed without setting SA_RESTART. None of these are the common case, +// but there are enough of them that it seems that we can't avoid +// an EINTR loop. +func ignoringEINTR(fn func() error) error { + for { + err := fn() + if err != syscall.EINTR { + return err + } + } +} diff --git a/src/os/file_windows.go b/src/os/file_windows.go index 85d6fe9a..6d992187 100644 --- a/src/os/file_windows.go +++ b/src/os/file_windows.go @@ -56,3 +56,21 @@ func tempDir() string { func (f unixFileHandle) ReadAt(b []byte, offset int64) (n int, err error) { return -1, ErrNotImplemented } + +// isWindowsNulName reports whether name is os.DevNull ('NUL') on Windows. +// True is returned if name is 'NUL' whatever the case. +func isWindowsNulName(name string) bool { + if len(name) != 3 { + return false + } + if name[0] != 'n' && name[0] != 'N' { + return false + } + if name[1] != 'u' && name[1] != 'U' { + return false + } + if name[2] != 'l' && name[2] != 'L' { + return false + } + return true +} diff --git a/src/os/path_unix.go b/src/os/path_unix.go new file mode 100644 index 00000000..f1bbb49d --- /dev/null +++ b/src/os/path_unix.go @@ -0,0 +1,35 @@ +// +build darwin linux,!baremetal + +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +const ( + PathSeparator = '/' // OS-specific path separator + PathListSeparator = ':' // OS-specific path list separator +) + +// IsPathSeparator reports whether c is a directory separator character. +func IsPathSeparator(c uint8) bool { + return PathSeparator == c +} + +// basename removes trailing slashes and the leading directory name from path name. +func basename(name string) string { + i := len(name) - 1 + // Remove trailing slashes + for ; i > 0 && name[i] == '/'; i-- { + name = name[:i] + } + // Remove leading directory name + for i--; i >= 0; i-- { + if name[i] == '/' { + name = name[i+1:] + break + } + } + + return name +} diff --git a/src/os/path_windows.go b/src/os/path_windows.go new file mode 100644 index 00000000..73ccd389 --- /dev/null +++ b/src/os/path_windows.go @@ -0,0 +1,216 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +const ( + PathSeparator = '\\' // OS-specific path separator + PathListSeparator = ';' // OS-specific path list separator +) + +// IsPathSeparator reports whether c is a directory separator character. +func IsPathSeparator(c uint8) bool { + // NOTE: Windows accept / as path separator. + return c == '\\' || c == '/' +} + +// basename removes trailing slashes and the leading +// directory name and drive letter from path name. +func basename(name string) string { + // Remove drive letter + if len(name) == 2 && name[1] == ':' { + name = "." + } else if len(name) > 2 && name[1] == ':' { + name = name[2:] + } + i := len(name) - 1 + // Remove trailing slashes + for ; i > 0 && (name[i] == '/' || name[i] == '\\'); i-- { + name = name[:i] + } + // Remove leading directory name + for i--; i >= 0; i-- { + if name[i] == '/' || name[i] == '\\' { + name = name[i+1:] + break + } + } + return name +} + +func isAbs(path string) (b bool) { + v := volumeName(path) + if v == "" { + return false + } + path = path[len(v):] + if path == "" { + return false + } + return IsPathSeparator(path[0]) +} + +func volumeName(path string) (v string) { + if len(path) < 2 { + return "" + } + // with drive letter + c := path[0] + if path[1] == ':' && + ('0' <= c && c <= '9' || 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z') { + return path[:2] + } + // is it UNC + if l := len(path); l >= 5 && IsPathSeparator(path[0]) && IsPathSeparator(path[1]) && + !IsPathSeparator(path[2]) && path[2] != '.' { + // first, leading `\\` and next shouldn't be `\`. its server name. + for n := 3; n < l-1; n++ { + // second, next '\' shouldn't be repeated. + if IsPathSeparator(path[n]) { + n++ + // third, following something characters. its share name. + if !IsPathSeparator(path[n]) { + if path[n] == '.' { + break + } + for ; n < l; n++ { + if IsPathSeparator(path[n]) { + break + } + } + return path[:n] + } + break + } + } + } + return "" +} + +func fromSlash(path string) string { + // Replace each '/' with '\\' if present + var pathbuf []byte + var lastSlash int + for i, b := range path { + if b == '/' { + if pathbuf == nil { + pathbuf = make([]byte, len(path)) + } + copy(pathbuf[lastSlash:], path[lastSlash:i]) + pathbuf[i] = '\\' + lastSlash = i + 1 + } + } + if pathbuf == nil { + return path + } + + copy(pathbuf[lastSlash:], path[lastSlash:]) + return string(pathbuf) +} + +func dirname(path string) string { + vol := volumeName(path) + i := len(path) - 1 + for i >= len(vol) && !IsPathSeparator(path[i]) { + i-- + } + dir := path[len(vol) : i+1] + last := len(dir) - 1 + if last > 0 && IsPathSeparator(dir[last]) { + dir = dir[:last] + } + if dir == "" { + dir = "." + } + return vol + dir +} + +// This is set via go:linkname on runtime.canUseLongPaths, and is true when the OS +// supports opting into proper long path handling without the need for fixups. +var canUseLongPaths bool + +// fixLongPath returns the extended-length (\\?\-prefixed) form of +// path when needed, in order to avoid the default 260 character file +// path limit imposed by Windows. If path is not easily converted to +// the extended-length form (for example, if path is a relative path +// or contains .. elements), or is short enough, fixLongPath returns +// path unmodified. +// +// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath +func fixLongPath(path string) string { + if canUseLongPaths { + return path + } + // Do nothing (and don't allocate) if the path is "short". + // Empirically (at least on the Windows Server 2013 builder), + // the kernel is arbitrarily okay with < 248 bytes. That + // matches what the docs above say: + // "When using an API to create a directory, the specified + // path cannot be so long that you cannot append an 8.3 file + // name (that is, the directory name cannot exceed MAX_PATH + // minus 12)." Since MAX_PATH is 260, 260 - 12 = 248. + // + // The MSDN docs appear to say that a normal path that is 248 bytes long + // will work; empirically the path must be less then 248 bytes long. + if len(path) < 248 { + // Don't fix. (This is how Go 1.7 and earlier worked, + // not automatically generating the \\?\ form) + return path + } + + // The extended form begins with \\?\, as in + // \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt. + // The extended form disables evaluation of . and .. path + // elements and disables the interpretation of / as equivalent + // to \. The conversion here rewrites / to \ and elides + // . elements as well as trailing or duplicate separators. For + // simplicity it avoids the conversion entirely for relative + // paths or paths containing .. elements. For now, + // \\server\share paths are not converted to + // \\?\UNC\server\share paths because the rules for doing so + // are less well-specified. + if len(path) >= 2 && path[:2] == `\\` { + // Don't canonicalize UNC paths. + return path + } + if !isAbs(path) { + // Relative path + return path + } + + const prefix = `\\?` + + pathbuf := make([]byte, len(prefix)+len(path)+len(`\`)) + copy(pathbuf, prefix) + n := len(path) + r, w := 0, len(prefix) + for r < n { + switch { + case IsPathSeparator(path[r]): + // empty block + r++ + case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])): + // /./ + r++ + case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])): + // /../ is currently unhandled + return path + default: + pathbuf[w] = '\\' + w++ + for ; r < n && !IsPathSeparator(path[r]); r++ { + pathbuf[w] = path[r] + w++ + } + } + } + // A drive's root directory needs a trailing \ + if w == len(`\\?\c:`) { + pathbuf[w] = '\\' + w++ + } + return string(pathbuf[:w]) +} diff --git a/src/os/stat.go b/src/os/stat.go new file mode 100644 index 00000000..8e192556 --- /dev/null +++ b/src/os/stat.go @@ -0,0 +1,21 @@ +// +build !baremetal,!js + +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +// Stat returns a FileInfo describing the named file. +// If there is an error, it will be of type *PathError. +func Stat(name string) (FileInfo, error) { + return statNolog(name) +} + +// Lstat returns a FileInfo describing the named file. +// If the file is a symbolic link, the returned FileInfo +// describes the symbolic link. Lstat makes no attempt to follow the link. +// If there is an error, it will be of type *PathError. +func Lstat(name string) (FileInfo, error) { + return lstatNolog(name) +} diff --git a/src/os/stat_darwin.go b/src/os/stat_darwin.go new file mode 100644 index 00000000..a27a3b66 --- /dev/null +++ b/src/os/stat_darwin.go @@ -0,0 +1,51 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = timespecToTime(fs.sys.Mtim) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK, syscall.S_IFWHT: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +func timespecToTime(ts syscall.Timespec) time.Time { + return time.Unix(int64(ts.Sec), int64(ts.Nsec)) +} + +// For testing. +func atime(fi FileInfo) time.Time { + return timespecToTime(fi.Sys().(*syscall.Stat_t).Atim) +} diff --git a/src/os/stat_linux.go b/src/os/stat_linux.go new file mode 100644 index 00000000..c4164f70 --- /dev/null +++ b/src/os/stat_linux.go @@ -0,0 +1,53 @@ +// +build linux,!baremetal + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "syscall" + "time" +) + +func fillFileStatFromSys(fs *fileStat, name string) { + fs.name = basename(name) + fs.size = fs.sys.Size + fs.modTime = timespecToTime(fs.sys.Mtim) + fs.mode = FileMode(fs.sys.Mode & 0777) + switch fs.sys.Mode & syscall.S_IFMT { + case syscall.S_IFBLK: + fs.mode |= ModeDevice + case syscall.S_IFCHR: + fs.mode |= ModeDevice | ModeCharDevice + case syscall.S_IFDIR: + fs.mode |= ModeDir + case syscall.S_IFIFO: + fs.mode |= ModeNamedPipe + case syscall.S_IFLNK: + fs.mode |= ModeSymlink + case syscall.S_IFREG: + // nothing to do + case syscall.S_IFSOCK: + fs.mode |= ModeSocket + } + if fs.sys.Mode&syscall.S_ISGID != 0 { + fs.mode |= ModeSetgid + } + if fs.sys.Mode&syscall.S_ISUID != 0 { + fs.mode |= ModeSetuid + } + if fs.sys.Mode&syscall.S_ISVTX != 0 { + fs.mode |= ModeSticky + } +} + +func timespecToTime(ts syscall.Timespec) time.Time { + return time.Unix(int64(ts.Sec), int64(ts.Nsec)) +} + +// For testing. +func atime(fi FileInfo) time.Time { + return timespecToTime(fi.Sys().(*syscall.Stat_t).Atim) +} diff --git a/src/os/stat_test.go b/src/os/stat_test.go new file mode 100644 index 00000000..797562ad --- /dev/null +++ b/src/os/stat_test.go @@ -0,0 +1,96 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os_test + +import ( + "io/fs" + "os" + "path/filepath" + "testing" +) + +// testStatAndLstat verifies that all os.Stat, os.Lstat os.File.Stat and os.Readdir work. +func testStatAndLstat(t *testing.T, path string, isLink bool, statCheck, lstatCheck func(*testing.T, string, fs.FileInfo)) { + // TODO: revert to upstream test once fstat and readdir are implemented + // test os.Stat + sfi, err := os.Stat(path) + if err != nil { + t.Error(err) + return + } + statCheck(t, path, sfi) + + // test os.Lstat + lsfi, err := os.Lstat(path) + if err != nil { + t.Error(err) + return + } + lstatCheck(t, path, lsfi) + + if isLink { + if os.SameFile(sfi, lsfi) { + t.Errorf("stat and lstat of %q should not be the same", path) + } + } else { + if !os.SameFile(sfi, lsfi) { + t.Errorf("stat and lstat of %q should be the same", path) + } + } +} + +// testIsDir verifies that fi refers to directory. +func testIsDir(t *testing.T, path string, fi fs.FileInfo) { + t.Helper() + if !fi.IsDir() { + t.Errorf("%q should be a directory", path) + } + if fi.Mode()&fs.ModeSymlink != 0 { + t.Errorf("%q should not be a symlink", path) + } +} + +// testIsFile verifies that fi refers to file. +func testIsFile(t *testing.T, path string, fi fs.FileInfo) { + t.Helper() + if fi.IsDir() { + t.Errorf("%q should not be a directory", path) + } + if fi.Mode()&fs.ModeSymlink != 0 { + t.Errorf("%q should not be a symlink", path) + } +} + +func testDirStats(t *testing.T, path string) { + testStatAndLstat(t, path, false, testIsDir, testIsDir) +} + +func testFileStats(t *testing.T, path string) { + testStatAndLstat(t, path, false, testIsFile, testIsFile) +} + +func TestDirAndSymlinkStats(t *testing.T) { + // TODO: revert to upstream test once symlinks and t.TempDir are implemented + tmpdir := os.TempDir() + dir := filepath.Join(tmpdir, "dir") + os.Remove(dir) + if err := os.Mkdir(dir, 0777); err != nil { + t.Fatal(err) + return + } + testDirStats(t, dir) + +} + +func TestFileAndSymlinkStats(t *testing.T) { + // TODO: revert to upstream test once symlinks and t.TempDir are implemented + tmpdir := os.TempDir() + file := filepath.Join(tmpdir, "file") + if err := os.WriteFile(file, []byte("abcdefg"), 0644); err != nil { + t.Fatal(err) + return + } + testFileStats(t, file) +} diff --git a/src/os/stat_unix.go b/src/os/stat_unix.go new file mode 100644 index 00000000..0f310648 --- /dev/null +++ b/src/os/stat_unix.go @@ -0,0 +1,42 @@ +// +build darwin linux,!baremetal + +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "syscall" +) + +// Sync is a stub, not yet implemented +func (f *File) Sync() error { + return ErrNotImplemented +} + +// statNolog stats a file with no test logging. +func statNolog(name string) (FileInfo, error) { + var fs fileStat + err := ignoringEINTR(func() error { + return syscall.Stat(name, &fs.sys) + }) + if err != nil { + return nil, &PathError{Op: "stat", Path: name, Err: err} + } + fillFileStatFromSys(&fs, name) + return &fs, nil +} + +// lstatNolog lstats a file with no test logging. +func lstatNolog(name string) (FileInfo, error) { + var fs fileStat + err := ignoringEINTR(func() error { + return syscall.Lstat(name, &fs.sys) + }) + if err != nil { + return nil, &PathError{Op: "lstat", Path: name, Err: err} + } + fillFileStatFromSys(&fs, name) + return &fs, nil +} diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go new file mode 100644 index 00000000..f32ffc3a --- /dev/null +++ b/src/os/stat_windows.go @@ -0,0 +1,89 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "internal/syscall/windows" + "syscall" + "unsafe" +) + +// Sync is a stub, not yet implemented +func (f *File) Sync() error { + return ErrNotImplemented +} + +// stat implements both Stat and Lstat of a file. +func stat(funcname, name string, createFileAttrs uint32) (FileInfo, error) { + if len(name) == 0 { + return nil, &PathError{Op: funcname, Path: name, Err: syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)} + } + if isWindowsNulName(name) { + return &devNullStat, nil + } + namep, err := syscall.UTF16PtrFromString(fixLongPath(name)) + if err != nil { + return nil, &PathError{Op: funcname, Path: name, Err: err} + } + + // Try GetFileAttributesEx first, because it is faster than CreateFile. + // See https://golang.org/issues/19922#issuecomment-300031421 for details. + var fa syscall.Win32FileAttributeData + err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) + if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { + // Not a symlink. + fs := &fileStat{ + FileAttributes: fa.FileAttributes, + CreationTime: fa.CreationTime, + LastAccessTime: fa.LastAccessTime, + LastWriteTime: fa.LastWriteTime, + FileSizeHigh: fa.FileSizeHigh, + FileSizeLow: fa.FileSizeLow, + } + if err := fs.saveInfoFromPath(name); err != nil { + return nil, err + } + return fs, nil + } + // GetFileAttributesEx fails with ERROR_SHARING_VIOLATION error for + // files, like c:\pagefile.sys. Use FindFirstFile for such files. + if err == windows.ERROR_SHARING_VIOLATION { + var fd syscall.Win32finddata + sh, err := syscall.FindFirstFile(namep, &fd) + if err != nil { + return nil, &PathError{Op: "FindFirstFile", Path: name, Err: err} + } + syscall.FindClose(sh) + fs := newFileStatFromWin32finddata(&fd) + if err := fs.saveInfoFromPath(name); err != nil { + return nil, err + } + return fs, nil + } + + // Finally use CreateFile. + h, err := syscall.CreateFile(namep, 0, 0, nil, + syscall.OPEN_EXISTING, createFileAttrs, 0) + if err != nil { + return nil, &PathError{Op: "CreateFile", Path: name, Err: err} + } + defer syscall.CloseHandle(h) + + return newFileStatFromGetFileInformationByHandle(name, h) +} + +// statNolog implements Stat for Windows. +func statNolog(name string) (FileInfo, error) { + return stat("Stat", name, syscall.FILE_FLAG_BACKUP_SEMANTICS) +} + +// lstatNolog implements Lstat for Windows. +func lstatNolog(name string) (FileInfo, error) { + attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS) + // Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink. + // See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted + attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT + return stat("Lstat", name, attrs) +} diff --git a/src/os/types.go b/src/os/types.go new file mode 100644 index 00000000..4cf02ff3 --- /dev/null +++ b/src/os/types.go @@ -0,0 +1,25 @@ +// +build !baremetal,!js + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +func (fs *fileStat) Name() string { return fs.name } +func (fs *fileStat) IsDir() bool { return fs.Mode().IsDir() } + +// SameFile reports whether fi1 and fi2 describe the same file. +// For example, on Unix this means that the device and inode fields +// of the two underlying structures are identical; on other systems +// the decision may be based on the path names. +// SameFile only applies to results returned by this package's Stat. +// It returns false in other cases. +func SameFile(fi1, fi2 FileInfo) bool { + fs1, ok1 := fi1.(*fileStat) + fs2, ok2 := fi2.(*fileStat) + if !ok1 || !ok2 { + return false + } + return sameFile(fs1, fs2) +} diff --git a/src/os/types_unix.go b/src/os/types_unix.go new file mode 100644 index 00000000..5e345b55 --- /dev/null +++ b/src/os/types_unix.go @@ -0,0 +1,30 @@ +// +build darwin linux,!baremetal + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "syscall" + "time" +) + +// A fileStat is the implementation of FileInfo returned by Stat and Lstat. +type fileStat struct { + name string + size int64 + mode FileMode + modTime time.Time + sys syscall.Stat_t +} + +func (fs *fileStat) Size() int64 { return fs.size } +func (fs *fileStat) Mode() FileMode { return fs.mode } +func (fs *fileStat) ModTime() time.Time { return fs.modTime } +func (fs *fileStat) Sys() interface{} { return &fs.sys } + +func sameFile(fs1, fs2 *fileStat) bool { + return fs1.sys.Dev == fs2.sys.Dev && fs1.sys.Ino == fs2.sys.Ino +} diff --git a/src/os/types_windows.go b/src/os/types_windows.go new file mode 100644 index 00000000..59bf5ca3 --- /dev/null +++ b/src/os/types_windows.go @@ -0,0 +1,231 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package os + +import ( + "internal/syscall/windows" + "sync" + "syscall" + "time" + "unsafe" +) + +// A fileStat is the implementation of FileInfo returned by Stat and Lstat. +type fileStat struct { + name string + + // from ByHandleFileInformation, Win32FileAttributeData and Win32finddata + FileAttributes uint32 + CreationTime syscall.Filetime + LastAccessTime syscall.Filetime + LastWriteTime syscall.Filetime + FileSizeHigh uint32 + FileSizeLow uint32 + + // from Win32finddata + Reserved0 uint32 + + // what syscall.GetFileType returns + filetype uint32 + + // used to implement SameFile + sync.Mutex + path string + vol uint32 + idxhi uint32 + idxlo uint32 + appendNameToPath bool +} + +// newFileStatFromGetFileInformationByHandle calls GetFileInformationByHandle +// to gather all required information about the file handle h. +func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (fs *fileStat, err error) { + var d syscall.ByHandleFileInformation + err = syscall.GetFileInformationByHandle(h, &d) + if err != nil { + return nil, &PathError{Op: "GetFileInformationByHandle", Path: path, Err: err} + } + + var ti windows.FILE_ATTRIBUTE_TAG_INFO + err = windows.GetFileInformationByHandleEx(h, windows.FileAttributeTagInfo, (*byte)(unsafe.Pointer(&ti)), uint32(unsafe.Sizeof(ti))) + if err != nil { + if errno, ok := err.(syscall.Errno); ok && errno == windows.ERROR_INVALID_PARAMETER { + // It appears calling GetFileInformationByHandleEx with + // FILE_ATTRIBUTE_TAG_INFO fails on FAT file system with + // ERROR_INVALID_PARAMETER. Clear ti.ReparseTag in that + // instance to indicate no symlinks are possible. + ti.ReparseTag = 0 + } else { + return nil, &PathError{Op: "GetFileInformationByHandleEx", Path: path, Err: err} + } + } + + return &fileStat{ + name: basename(path), + FileAttributes: d.FileAttributes, + CreationTime: d.CreationTime, + LastAccessTime: d.LastAccessTime, + LastWriteTime: d.LastWriteTime, + FileSizeHigh: d.FileSizeHigh, + FileSizeLow: d.FileSizeLow, + vol: d.VolumeSerialNumber, + idxhi: d.FileIndexHigh, + idxlo: d.FileIndexLow, + Reserved0: ti.ReparseTag, + // fileStat.path is used by os.SameFile to decide if it needs + // to fetch vol, idxhi and idxlo. But these are already set, + // so set fileStat.path to "" to prevent os.SameFile doing it again. + }, nil +} + +// newFileStatFromWin32finddata copies all required information +// from syscall.Win32finddata d into the newly created fileStat. +func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat { + return &fileStat{ + FileAttributes: d.FileAttributes, + CreationTime: d.CreationTime, + LastAccessTime: d.LastAccessTime, + LastWriteTime: d.LastWriteTime, + FileSizeHigh: d.FileSizeHigh, + FileSizeLow: d.FileSizeLow, + Reserved0: d.Reserved0, + } +} + +func (fs *fileStat) isSymlink() bool { + // Use instructions described at + // https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ + // to recognize whether it's a symlink. + if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { + return false + } + return fs.Reserved0 == syscall.IO_REPARSE_TAG_SYMLINK || + fs.Reserved0 == windows.IO_REPARSE_TAG_MOUNT_POINT +} + +func (fs *fileStat) Size() int64 { + return int64(fs.FileSizeHigh)<<32 + int64(fs.FileSizeLow) +} + +func (fs *fileStat) Mode() (m FileMode) { + if fs == &devNullStat { + return ModeDevice | ModeCharDevice | 0666 + } + if fs.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 { + m |= 0444 + } else { + m |= 0666 + } + if fs.isSymlink() { + return m | ModeSymlink + } + if fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + m |= ModeDir | 0111 + } + switch fs.filetype { + case syscall.FILE_TYPE_PIPE: + m |= ModeNamedPipe + case syscall.FILE_TYPE_CHAR: + m |= ModeDevice | ModeCharDevice + } + return m +} + +func (fs *fileStat) ModTime() time.Time { + return time.Unix(0, fs.LastWriteTime.Nanoseconds()) +} + +// Sys returns syscall.Win32FileAttributeData for file fs. +func (fs *fileStat) Sys() interface{} { + return &syscall.Win32FileAttributeData{ + FileAttributes: fs.FileAttributes, + CreationTime: fs.CreationTime, + LastAccessTime: fs.LastAccessTime, + LastWriteTime: fs.LastWriteTime, + FileSizeHigh: fs.FileSizeHigh, + FileSizeLow: fs.FileSizeLow, + } +} + +func (fs *fileStat) loadFileId() error { + fs.Lock() + defer fs.Unlock() + if fs.path == "" { + // already done + return nil + } + var path string + if fs.appendNameToPath { + path = fs.path + `\` + fs.name + } else { + path = fs.path + } + pathp, err := syscall.UTF16PtrFromString(path) + if err != nil { + return err + } + attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS) + if fs.isSymlink() { + // Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink. + // See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted + attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT + } + h, err := syscall.CreateFile(pathp, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0) + if err != nil { + return err + } + defer syscall.CloseHandle(h) + var i syscall.ByHandleFileInformation + err = syscall.GetFileInformationByHandle(h, &i) + if err != nil { + return err + } + fs.path = "" + fs.vol = i.VolumeSerialNumber + fs.idxhi = i.FileIndexHigh + fs.idxlo = i.FileIndexLow + return nil +} + +// saveInfoFromPath saves full path of the file to be used by os.SameFile later, +// and set name from path. +func (fs *fileStat) saveInfoFromPath(path string) error { + fs.path = path + if !isAbs(fs.path) { + var err error + fs.path, err = syscall.FullPath(fs.path) + if err != nil { + return &PathError{Op: "FullPath", Path: path, Err: err} + } + } + fs.name = basename(path) + return nil +} + +// devNullStat is fileStat structure describing DevNull file ("NUL"). +var devNullStat = fileStat{ + name: DevNull, + // hopefully this will work for SameFile + vol: 0, + idxhi: 0, + idxlo: 0, +} + +func sameFile(fs1, fs2 *fileStat) bool { + e := fs1.loadFileId() + if e != nil { + return false + } + e = fs2.loadFileId() + if e != nil { + return false + } + return fs1.vol == fs2.vol && fs1.idxhi == fs2.idxhi && fs1.idxlo == fs2.idxlo +} + +// For testing. +func atime(fi FileInfo) time.Time { + return time.Unix(0, fi.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) +} diff --git a/src/syscall/syscall_libc_darwin.go b/src/syscall/syscall_libc_darwin.go index 481ba383..9bc2c581 100644 --- a/src/syscall/syscall_libc_darwin.go +++ b/src/syscall/syscall_libc_darwin.go @@ -2,6 +2,10 @@ package syscall +import ( + "unsafe" +) + // This file defines errno and constants to match the darwin libsystem ABI. // Values have been copied from src/syscall/zerrors_darwin_amd64.go. @@ -97,3 +101,93 @@ const ( MAP_ANON = 0x1000 // allocated from memory, swap space MAP_ANONYMOUS = MAP_ANON ) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +// Go chose Linux's field names for Stat_t, see https://github.com/golang/go/issues/31735 +type Stat_t struct { + Dev int32 + Mode uint16 + Nlink uint16 + Ino uint64 + Uid uint32 + Gid uint32 + Rdev int32 + Pad_cgo_0 [4]byte + Atim Timespec + Mtim Timespec + Ctim Timespec + Btim Timespec + Size int64 + Blocks int64 + Blksize int32 + Flags uint32 + Gen uint32 + Lspare int32 + Qspare [2]int64 +} + +// Source: https://github.com/apple/darwin-xnu/blob/main/bsd/sys/_types/_s_ifmt.h +const ( + S_IEXEC = 0x40 + S_IFBLK = 0x6000 + S_IFCHR = 0x2000 + S_IFDIR = 0x4000 + S_IFIFO = 0x1000 + S_IFLNK = 0xa000 + S_IFMT = 0xf000 + S_IFREG = 0x8000 + S_IFSOCK = 0xc000 + S_IFWHT = 0xe000 + S_IREAD = 0x100 + S_IRGRP = 0x20 + S_IROTH = 0x4 + S_IRUSR = 0x100 + S_IRWXG = 0x38 + S_IRWXO = 0x7 + S_IRWXU = 0x1c0 + S_ISGID = 0x400 + S_ISTXT = 0x200 + S_ISUID = 0x800 + S_ISVTX = 0x200 + S_IWGRP = 0x10 + S_IWOTH = 0x2 + S_IWRITE = 0x80 + S_IWUSR = 0x80 + S_IXGRP = 0x8 + S_IXOTH = 0x1 + S_IXUSR = 0x40 +) + +func Stat(path string, p *Stat_t) (err error) { + data := cstring(path) + n := libc_stat(&data[0], unsafe.Pointer(p)) + + if n < 0 { + err = getErrno() + } + return +} + +func Lstat(path string, p *Stat_t) (err error) { + data := cstring(path) + n := libc_lstat(&data[0], unsafe.Pointer(p)) + if n < 0 { + err = getErrno() + } + return +} + +// The odd $INODE64 suffix is an Apple compatibility feature, see https://assert.cc/posts/darwin_use_64_bit_inode_vs_ctypes/ +// Without it, you get the old, smaller struct stat from mac os 10.2 or so. + +// int stat(const char *path, struct stat * buf); +//export stat$INODE64 +func libc_stat(pathname *byte, ptr unsafe.Pointer) int32 + +// int lstat(const char *path, struct stat * buf); +//export lstat$INODE64 +func libc_lstat(pathname *byte, ptr unsafe.Pointer) int32 diff --git a/src/syscall/syscall_libc_wasi.go b/src/syscall/syscall_libc_wasi.go index 7a71c35f..9686a998 100644 --- a/src/syscall/syscall_libc_wasi.go +++ b/src/syscall/syscall_libc_wasi.go @@ -2,6 +2,10 @@ package syscall +import ( + "unsafe" +) + // https://github.com/WebAssembly/wasi-libc/blob/main/expected/wasm32-wasi/predefined-macros.txt type Signal int @@ -139,3 +143,88 @@ const ( EXDEV Errno = 75 /* Cross-device link */ ENOTCAPABLE Errno = 76 /* Extension: Capabilities insufficient. */ ) + +// https://github.com/WebAssembly/wasi-libc/blob/main/libc-bottom-half/headers/public/__struct_timespec.h +type Timespec struct { + Sec int32 + Nsec int64 +} + +// https://github.com/WebAssembly/wasi-libc/blob/main/libc-bottom-half/headers/public/__struct_stat.h +// https://github.com/WebAssembly/wasi-libc/blob/main/libc-bottom-half/headers/public/__typedef_ino_t.h +// etc. +// Go chose Linux's field names for Stat_t, see https://github.com/golang/go/issues/31735 +type Stat_t struct { + Dev uint64 + Ino uint64 + Nlink uint64 + Mode uint32 + Uid uint32 + Gid uint32 + Pad_cgo_0 [4]byte + Rdev uint64 + Size int64 + Blksize int32 + Blocks int64 + + Atim Timespec + Mtim Timespec + Ctim Timespec + Qspare [3]int64 +} + +// https://github.com/WebAssembly/wasi-libc/blob/main/libc-top-half/musl/include/sys/stat.h +const ( + S_IFBLK = 0x6000 + S_IFCHR = 0x2000 + S_IFDIR = 0x4000 + S_IFIFO = 0x1000 + S_IFLNK = 0xa000 + S_IFMT = 0xf000 + S_IFREG = 0x8000 + S_IFSOCK = 0xc000 + S_IREAD = 0x100 + S_IRGRP = 0x20 + S_IROTH = 0x4 + S_IRUSR = 0x100 + S_IRWXG = 0x38 + S_IRWXO = 0x7 + S_IRWXU = 0x1c0 + S_ISGID = 0x400 + S_ISUID = 0x800 + S_ISVTX = 0x200 + S_IWGRP = 0x10 + S_IWOTH = 0x2 + S_IWRITE = 0x80 + S_IWUSR = 0x80 + S_IXGRP = 0x8 + S_IXOTH = 0x1 + S_IXUSR = 0x40 +) + +func Stat(path string, p *Stat_t) (err error) { + data := cstring(path) + n := libc_stat(&data[0], unsafe.Pointer(p)) + + if n < 0 { + err = getErrno() + } + return +} + +func Lstat(path string, p *Stat_t) (err error) { + data := cstring(path) + n := libc_lstat(&data[0], unsafe.Pointer(p)) + if n < 0 { + err = getErrno() + } + return +} + +// int stat(const char *path, struct stat * buf); +//export stat +func libc_stat(pathname *byte, ptr unsafe.Pointer) int32 + +// int lstat(const char *path, struct stat * buf); +//export lstat +func libc_lstat(pathname *byte, ptr unsafe.Pointer) int32