From dd47971877352d5825d877901313657237c1755b Mon Sep 17 00:00:00 2001 From: xushiwei Date: Thu, 11 Jul 2024 14:47:03 +0800 Subject: [PATCH] patch os: File --- internal/lib/os/file.go | 3 +- internal/lib/os/file_unix.go | 199 +++++++++++++++++++++++++ internal/lib/os/os.go | 1 - internal/lib/os/path_plan9.go | 19 +++ internal/lib/os/path_unix.go | 75 ++++++++++ internal/lib/os/path_windows.go | 227 +++++++++++++++++++++++++++++ internal/lib/os/stat.go | 19 +++ internal/lib/os/stat_darwin.go | 42 ++++++ internal/lib/os/stat_linux.go | 42 ++++++ internal/lib/os/stat_unix.go | 57 ++++++++ internal/lib/os/types.go | 31 +--- internal/lib/os/types_plan9.go | 30 ++++ internal/lib/os/types_unix.go | 30 ++++ internal/lib/os/types_windows.go | 241 +++++++++++++++++++++++++++++++ 14 files changed, 990 insertions(+), 26 deletions(-) create mode 100644 internal/lib/os/file_unix.go create mode 100644 internal/lib/os/path_plan9.go create mode 100644 internal/lib/os/path_unix.go create mode 100644 internal/lib/os/path_windows.go create mode 100644 internal/lib/os/stat.go create mode 100644 internal/lib/os/stat_darwin.go create mode 100644 internal/lib/os/stat_linux.go create mode 100644 internal/lib/os/stat_unix.go create mode 100644 internal/lib/os/types_plan9.go create mode 100644 internal/lib/os/types_unix.go create mode 100644 internal/lib/os/types_windows.go diff --git a/internal/lib/os/file.go b/internal/lib/os/file.go index 5bb2ec29..a522f7f3 100644 --- a/internal/lib/os/file.go +++ b/internal/lib/os/file.go @@ -217,6 +217,7 @@ func setStickyBit(name string) error { } return Chmod(name, fi.Mode()|ModeSticky) } +*/ // Open opens the named file for reading. If successful, methods on // the returned file can be used for reading; the associated file @@ -242,7 +243,6 @@ func Create(name string) (*File, error) { // methods on the returned File can be used for I/O. // If there is an error, it will be of type *PathError. func OpenFile(name string, flag int, perm FileMode) (*File, error) { - testlog.Open(name) f, err := openFileNolog(name, flag, perm) if err != nil { return nil, err @@ -252,6 +252,7 @@ func OpenFile(name string, flag int, perm FileMode) (*File, error) { return f, nil } +/* // lstat is overridden in tests. var lstat = Lstat diff --git a/internal/lib/os/file_unix.go b/internal/lib/os/file_unix.go new file mode 100644 index 00000000..1450ff18 --- /dev/null +++ b/internal/lib/os/file_unix.go @@ -0,0 +1,199 @@ +// 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. + +//go:build unix || (js && wasm) || wasip1 + +package os + +import ( + "io/fs" + "runtime" +) + +// Fd returns the integer Unix file descriptor referencing the open file. +// If f is closed, the file descriptor becomes invalid. +// If f is garbage collected, a finalizer may close the file descriptor, +// making it invalid; see runtime.SetFinalizer for more information on when +// a finalizer might be run. On Unix systems this will cause the SetDeadline +// methods to stop working. +// Because file descriptors can be reused, the returned file descriptor may +// only be closed through the Close method of f, or by its finalizer during +// garbage collection. Otherwise, during garbage collection the finalizer +// may close an unrelated file descriptor with the same (reused) number. +// +// As an alternative, see the f.SyscallConn method. +func (f *File) Fd() uintptr { + if f == nil { + return ^(uintptr(0)) + } + + return f.fd +} + +// NewFile returns a new File with the given file descriptor and +// name. The returned value will be nil if fd is not a valid file +// descriptor. On Unix systems, if the file descriptor is in +// non-blocking mode, NewFile will attempt to return a pollable File +// (one for which the SetDeadline methods work). +// +// After passing it to NewFile, fd may become invalid under the same +// conditions described in the comments of the Fd method, and the same +// constraints apply. +func NewFile(fd uintptr, name string) *File { + return &File{fd: fd, name: name} +} + +/* TODO(xsw): +// NewFile returns a new File with the given file descriptor and +// name. The returned value will be nil if fd is not a valid file +// descriptor. On Unix systems, if the file descriptor is in +// non-blocking mode, NewFile will attempt to return a pollable File +// (one for which the SetDeadline methods work). +// +// After passing it to NewFile, fd may become invalid under the same +// conditions described in the comments of the Fd method, and the same +// constraints apply. +func NewFile(fd uintptr, name string) *File { + fdi := int(fd) + if fdi < 0 { + return nil + } + + kind := kindNewFile + appendMode := false + if flags, err := unix.Fcntl(fdi, syscall.F_GETFL, 0); err == nil { + if unix.HasNonblockFlag(flags) { + kind = kindNonBlock + } + appendMode = flags&syscall.O_APPEND != 0 + } + f := newFile(fdi, name, kind) + f.appendMode = appendMode + return f +} + +// net_newUnixFile is a hidden entry point called by net.conn.File. +// This is used so that a nonblocking network connection will become +// blocking if code calls the Fd method. We don't want that for direct +// calls to NewFile: passing a nonblocking descriptor to NewFile should +// remain nonblocking if you get it back using Fd. But for net.conn.File +// the call to NewFile is hidden from the user. Historically in that case +// the Fd method has returned a blocking descriptor, and we want to +// retain that behavior because existing code expects it and depends on it. +// +//-go:linkname net_newUnixFile net.newUnixFile +func net_newUnixFile(fd int, name string) *File { + if fd < 0 { + panic("invalid FD") + } + + f := newFile(fd, name, kindNonBlock) + f.nonblock = true // tell Fd to return blocking descriptor + return f +} +*/ + +// newFileKind describes the kind of file to newFile. +type newFileKind int + +const ( + // kindNewFile means that the descriptor was passed to us via NewFile. + kindNewFile newFileKind = iota + // kindOpenFile means that the descriptor was opened using + // Open, Create, or OpenFile (without O_NONBLOCK). + kindOpenFile + // kindPipe means that the descriptor was opened using Pipe. + kindPipe + // kindNonBlock means that the descriptor is already in + // non-blocking mode. + kindNonBlock + // kindNoPoll means that we should not put the descriptor into + // non-blocking mode, because we know it is not a pipe or FIFO. + // Used by openFdAt for directories. + kindNoPoll +) + +// TODO(xsw): +// func sigpipe() // implemented in package runtime + +// epipecheck raises SIGPIPE if we get an EPIPE error on standard +// output or standard error. See the SIGPIPE docs in os/signal, and +// issue 11845. +func epipecheck(file *File, e error) { + /* TODO(xsw): + if e == syscall.EPIPE && file.stdoutOrErr { + sigpipe() + } + */ + panic("todo: os.epipecheck") +} + +// DevNull is the name of the operating system's “null device.” +// On Unix-like systems, it is "/dev/null"; on Windows, "NUL". +const DevNull = "/dev/null" + +// openFileNolog is the Unix implementation of OpenFile. +// Changes here should be reflected in openFdAt, if relevant. +func openFileNolog(name string, flag int, perm FileMode) (*File, error) { + panic("todo: os.openFileNolog") +} + +func tempDir() string { + dir := Getenv("TMPDIR") + if dir == "" { + if runtime.GOOS == "android" { + dir = "/data/local/tmp" + } else { + dir = "/tmp" + } + } + return dir +} + +type unixDirent struct { + parent string + name string + typ FileMode + info FileInfo +} + +func (d *unixDirent) Name() string { return d.name } +func (d *unixDirent) IsDir() bool { return d.typ.IsDir() } +func (d *unixDirent) Type() FileMode { return d.typ } + +func (d *unixDirent) Info() (FileInfo, error) { + /* TODO(xsw): + if d.info != nil { + return d.info, nil + } + return lstat(d.parent + "/" + d.name) + */ + panic("todo: os.unixDirent.Info") +} + +func (d *unixDirent) String() string { + return fs.FormatDirEntry(d) +} + +/* TODO(xsw): +func newUnixDirent(parent, name string, typ FileMode) (DirEntry, error) { + ude := &unixDirent{ + parent: parent, + name: name, + typ: typ, + } + if typ != ^FileMode(0) && !testingForceReadDirLstat { + return ude, nil + } + + info, err := lstat(parent + "/" + name) + if err != nil { + return nil, err + } + + ude.typ = info.Mode().Type() + ude.info = info + return ude, nil +} +*/ diff --git a/internal/lib/os/os.go b/internal/lib/os/os.go index 5bb4c9c0..d701c0f1 100644 --- a/internal/lib/os/os.go +++ b/internal/lib/os/os.go @@ -202,7 +202,6 @@ func Getwd() (dir string, err error) { // func Hostname() (name string, err error) // func IsExist(err error) bool // func IsNotExist(err error) bool -// func IsPathSeparator(c uint8) bool // func IsPermission(err error) bool // func IsTimeout(err error) bool diff --git a/internal/lib/os/path_plan9.go b/internal/lib/os/path_plan9.go new file mode 100644 index 00000000..a54b4b98 --- /dev/null +++ b/internal/lib/os/path_plan9.go @@ -0,0 +1,19 @@ +// 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 = '\000' // OS-specific path list separator +) + +// IsPathSeparator reports whether c is a directory separator character. +func IsPathSeparator(c uint8) bool { + return PathSeparator == c +} + +func fixRootDirectory(p string) string { + return p +} diff --git a/internal/lib/os/path_unix.go b/internal/lib/os/path_unix.go new file mode 100644 index 00000000..c975cdb1 --- /dev/null +++ b/internal/lib/os/path_unix.go @@ -0,0 +1,75 @@ +// 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. + +//go:build unix || (js && wasm) || wasip1 + +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 +} + +// splitPath returns the base name and parent directory. +func splitPath(path string) (string, string) { + // if no better parent is found, the path is relative from "here" + dirname := "." + + // Remove all but one leading slash. + for len(path) > 1 && path[0] == '/' && path[1] == '/' { + path = path[1:] + } + + i := len(path) - 1 + + // Remove trailing slashes. + for ; i > 0 && path[i] == '/'; i-- { + path = path[:i] + } + + // if no slashes in path, base is path + basename := path + + // Remove leading directory path + for i--; i >= 0; i-- { + if path[i] == '/' { + if i == 0 { + dirname = path[:1] + } else { + dirname = path[:i] + } + basename = path[i+1:] + break + } + } + + return dirname, basename +} + +func fixRootDirectory(p string) string { + return p +} diff --git a/internal/lib/os/path_windows.go b/internal/lib/os/path_windows.go new file mode 100644 index 00000000..3356908a --- /dev/null +++ b/internal/lib/os/path_windows.go @@ -0,0 +1,227 @@ +// 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 accepts / 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]) +} + +// fixRootDirectory fixes a reference to a drive's root directory to +// have the required trailing slash. +func fixRootDirectory(p string) string { + if len(p) == len(`\\?\c:`) { + if IsPathSeparator(p[0]) && IsPathSeparator(p[1]) && p[2] == '?' && IsPathSeparator(p[3]) && p[5] == ':' { + return p + `\` + } + } + return p +} diff --git a/internal/lib/os/stat.go b/internal/lib/os/stat.go new file mode 100644 index 00000000..1d2def35 --- /dev/null +++ b/internal/lib/os/stat.go @@ -0,0 +1,19 @@ +// 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/internal/lib/os/stat_darwin.go b/internal/lib/os/stat_darwin.go new file mode 100644 index 00000000..0725f6bb --- /dev/null +++ b/internal/lib/os/stat_darwin.go @@ -0,0 +1,42 @@ +// 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 = time.Unix(fs.sys.Mtimespec.Unix()) + 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 + } +} diff --git a/internal/lib/os/stat_linux.go b/internal/lib/os/stat_linux.go new file mode 100644 index 00000000..0b3e4538 --- /dev/null +++ b/internal/lib/os/stat_linux.go @@ -0,0 +1,42 @@ +// 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 = time.Unix(fs.sys.Mtim.Unix()) + 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 + } +} diff --git a/internal/lib/os/stat_unix.go b/internal/lib/os/stat_unix.go new file mode 100644 index 00000000..1c826576 --- /dev/null +++ b/internal/lib/os/stat_unix.go @@ -0,0 +1,57 @@ +// 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. + +//go:build unix || (js && wasm) || wasip1 + +package os + +// Stat returns the FileInfo structure describing file. +// If there is an error, it will be of type *PathError. +func (f *File) Stat() (FileInfo, error) { + /* TODO(xsw): + if f == nil { + return nil, ErrInvalid + } + var fs fileStat + err := os.Fstat(c.Int(f.fd), &fs.sys) + if err != 0 { + return nil, &PathError{Op: "stat", Path: f.name, Err: syscall.Errno(err)} + } + fillFileStatFromSys(&fs, f.name) + return &fs, nil + */ + panic("todo: os.File.Stat") +} + +// statNolog stats a file with no test logging. +func statNolog(name string) (FileInfo, error) { + /* TODO(xsw): + 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 + */ + panic("todo: os.statNolog") +} + +// lstatNolog lstats a file with no test logging. +func lstatNolog(name string) (FileInfo, error) { + /* TODO(xsw): + 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 + */ + panic("todo: os.lstatNolog") +} diff --git a/internal/lib/os/types.go b/internal/lib/os/types.go index 5c311568..40a66ae4 100644 --- a/internal/lib/os/types.go +++ b/internal/lib/os/types.go @@ -22,19 +22,7 @@ type File struct { fd uintptr name string appendMode bool -} - -// NewFile returns a new File with the given file descriptor and -// name. The returned value will be nil if fd is not a valid file -// descriptor. On Unix systems, if the file descriptor is in -// non-blocking mode, NewFile will attempt to return a pollable File -// (one for which the SetDeadline methods work). -// -// After passing it to NewFile, fd may become invalid under the same -// conditions described in the comments of the Fd method, and the same -// constraints apply. -func NewFile(fd uintptr, name string) *File { - return &File{fd: fd, name: name} + nonblock bool } // write writes len(b) bytes to the File. @@ -118,10 +106,8 @@ const ( ModePerm = fs.ModePerm // Unix permission bits, 0o777 ) -/* TODO(xsw): 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 @@ -130,13 +116,10 @@ func (fs *fileStat) IsDir() bool { return fs.Mode().IsDir() } // 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) - */ - panic("todo") + fs1, ok1 := fi1.(*fileStat) + fs2, ok2 := fi2.(*fileStat) + if !ok1 || !ok2 { + return false + } + return sameFile(fs1, fs2) } diff --git a/internal/lib/os/types_plan9.go b/internal/lib/os/types_plan9.go new file mode 100644 index 00000000..adb40130 --- /dev/null +++ b/internal/lib/os/types_plan9.go @@ -0,0 +1,30 @@ +// 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 any +} + +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() any { return fs.sys } + +func sameFile(fs1, fs2 *fileStat) bool { + a := fs1.sys.(*syscall.Dir) + b := fs2.sys.(*syscall.Dir) + return a.Qid.Path == b.Qid.Path && a.Type == b.Type && a.Dev == b.Dev +} diff --git a/internal/lib/os/types_unix.go b/internal/lib/os/types_unix.go new file mode 100644 index 00000000..1b90a5a1 --- /dev/null +++ b/internal/lib/os/types_unix.go @@ -0,0 +1,30 @@ +// 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. + +//go:build !windows && !plan9 + +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() any { 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/internal/lib/os/types_windows.go b/internal/lib/os/types_windows.go new file mode 100644 index 00000000..f2ec29ec --- /dev/null +++ b/internal/lib/os/types_windows.go @@ -0,0 +1,241 @@ +// 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 + ReparseTag 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, + ReparseTag: 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 { + fs := &fileStat{ + FileAttributes: d.FileAttributes, + CreationTime: d.CreationTime, + LastAccessTime: d.LastAccessTime, + LastWriteTime: d.LastWriteTime, + FileSizeHigh: d.FileSizeHigh, + FileSizeLow: d.FileSizeLow, + } + if d.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 { + // Per https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataw: + // “If the dwFileAttributes member includes the FILE_ATTRIBUTE_REPARSE_POINT + // attribute, this member specifies the reparse point tag. Otherwise, this + // value is undefined and should not be used.” + fs.ReparseTag = d.Reserved0 + } + return fs +} + +func (fs *fileStat) isSymlink() bool { + // As of https://go.dev/cl/86556, we treat MOUNT_POINT reparse points as + // symlinks because otherwise certain directory junction tests in the + // path/filepath package would fail. + // + // However, + // https://learn.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions + // seems to suggest that directory junctions should be treated like hard + // links, not symlinks. + // + // TODO(bcmills): Get more input from Microsoft on what the behavior ought to + // be for MOUNT_POINT reparse points. + + return fs.ReparseTag == syscall.IO_REPARSE_TAG_SYMLINK || + fs.ReparseTag == 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.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 + } + if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && m&ModeType == 0 { + m |= ModeIrregular + } + 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() any { + 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 + } + + // Per https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points-and-file-operations, + // “Applications that use the CreateFile function should specify the + // FILE_FLAG_OPEN_REPARSE_POINT flag when opening the file if it is a reparse + // point.” + // + // And per https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew, + // “If the file is not a reparse point, then this flag is ignored.” + // + // So we set FILE_FLAG_OPEN_REPARSE_POINT unconditionally, since we want + // information about the reparse point itself. + // + // If the file is a symlink, the symlink target should have already been + // resolved when the fileStat was created, so we don't need to worry about + // resolving symlink reparse points again here. + attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS | 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 +} + +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 +}