"io"
"io/fs"
"os"
+ "os/exec"
"path/filepath"
"reflect"
"runtime"
func TestStatPagefile(t *testing.T) {
t.Parallel()
- fi, err := os.Stat(`c:\pagefile.sys`)
+ const path = `c:\pagefile.sys`
+ fi, err := os.Stat(path)
if err == nil {
if fi.Name() == "" {
- t.Fatal(`FileInfo of c:\pagefile.sys has empty name`)
+ t.Fatalf("Stat(%q).Name() is empty", path)
}
+ t.Logf("Stat(%q).Size() = %v", path, fi.Size())
return
}
if os.IsNotExist(err) {
t.Error(err)
}
}
+
+func TestAppExecLinkStat(t *testing.T) {
+ // We expect executables installed to %LOCALAPPDATA%\Microsoft\WindowsApps to
+ // be reparse points with tag IO_REPARSE_TAG_APPEXECLINK. Here we check that
+ // such reparse points are treated as irregular (but executable) files, not
+ // broken symlinks.
+ appdata := os.Getenv("LOCALAPPDATA")
+ if appdata == "" {
+ t.Skipf("skipping: LOCALAPPDATA not set")
+ }
+
+ pythonExeName := "python3.exe"
+ pythonPath := filepath.Join(appdata, `Microsoft\WindowsApps`, pythonExeName)
+
+ lfi, err := os.Lstat(pythonPath)
+ if err != nil {
+ t.Skip("skipping test, because Python 3 is not installed via the Windows App Store on this system; see https://golang.org/issue/42919")
+ }
+
+ // An APPEXECLINK reparse point is not a symlink, so os.Readlink should return
+ // a non-nil error for it, and Stat should return results identical to Lstat.
+ linkName, err := os.Readlink(pythonPath)
+ if err == nil {
+ t.Errorf("os.Readlink(%q) = %q, but expected an error\n(should be an APPEXECLINK reparse point, not a symlink)", pythonPath, linkName)
+ }
+
+ sfi, err := os.Stat(pythonPath)
+ if err != nil {
+ t.Fatalf("Stat %s: %v", pythonPath, err)
+ }
+
+ if lfi.Name() != sfi.Name() {
+ t.Logf("os.Lstat(%q) = %+v", pythonPath, lfi)
+ t.Logf("os.Stat(%q) = %+v", pythonPath, sfi)
+ t.Errorf("files should be same")
+ }
+
+ if lfi.Name() != pythonExeName {
+ t.Errorf("Stat %s: got %q, but wanted %q", pythonPath, lfi.Name(), pythonExeName)
+ }
+ if m := lfi.Mode(); m&fs.ModeSymlink != 0 {
+ t.Errorf("%q should be a file, not a link (mode=0x%x)", pythonPath, uint32(m))
+ }
+ if m := lfi.Mode(); m&fs.ModeDir != 0 {
+ t.Errorf("%q should be a file, not a directory (mode=0x%x)", pythonPath, uint32(m))
+ }
+ if m := lfi.Mode(); m&fs.ModeIrregular == 0 {
+ // A reparse point is not a regular file, but we don't have a more appropriate
+ // ModeType bit for it, so it should be marked as irregular.
+ t.Errorf("%q should not be a regular file (mode=0x%x)", pythonPath, uint32(m))
+ }
+
+ if sfi.Name() != pythonExeName {
+ t.Errorf("Stat %s: got %q, but wanted %q", pythonPath, sfi.Name(), pythonExeName)
+ }
+ if m := sfi.Mode(); m&fs.ModeSymlink != 0 {
+ t.Errorf("%q should be a file, not a link (mode=0x%x)", pythonPath, uint32(m))
+ }
+ if m := sfi.Mode(); m&fs.ModeDir != 0 {
+ t.Errorf("%q should be a file, not a directory (mode=0x%x)", pythonPath, uint32(m))
+ }
+ if m := sfi.Mode(); m&fs.ModeIrregular == 0 {
+ // A reparse point is not a regular file, but we don't have a more appropriate
+ // ModeType bit for it, so it should be marked as irregular.
+ t.Errorf("%q should not be a regular file (mode=0x%x)", pythonPath, uint32(m))
+ }
+
+ p, err := exec.LookPath(pythonPath)
+ if err != nil {
+ t.Errorf("exec.LookPath(%q): %v", pythonPath, err)
+ }
+ if p != pythonPath {
+ t.Errorf("exec.LookPath(%q) = %q; want %q", pythonPath, p, pythonPath)
+ }
+}
}
}
+func testSymlinkSameFileOpen(t *testing.T, link string) {
+ f, err := os.Open(link)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer f.Close()
+
+ fi, err := f.Stat()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ fi2, err := os.Stat(link)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ if !os.SameFile(fi, fi2) {
+ t.Errorf("os.Open(%q).Stat() and os.Stat(%q) are not the same file", link, link)
+ }
+}
+
func TestDirAndSymlinkStats(t *testing.T) {
testenv.MustHaveSymlink(t)
t.Parallel()
}
testSymlinkStats(t, dirlink, true)
testSymlinkSameFile(t, dir, dirlink)
+ testSymlinkSameFileOpen(t, dirlink)
linklink := filepath.Join(tmpdir, "linklink")
if err := os.Symlink(dirlink, linklink); err != nil {
}
testSymlinkStats(t, linklink, true)
testSymlinkSameFile(t, dir, linklink)
+ testSymlinkSameFileOpen(t, linklink)
}
func TestFileAndSymlinkStats(t *testing.T) {
}
testSymlinkStats(t, filelink, false)
testSymlinkSameFile(t, file, filelink)
+ testSymlinkSameFileOpen(t, filelink)
linklink := filepath.Join(tmpdir, "linklink")
if err := os.Symlink(filelink, linklink); err != nil {
}
testSymlinkStats(t, linklink, false)
testSymlinkSameFile(t, file, linklink)
+ testSymlinkSameFileOpen(t, linklink)
}
// see issue 27225 for details
}
// stat implements both Stat and Lstat of a file.
-func stat(funcname, name string, createFileAttrs uint32) (FileInfo, error) {
+func stat(funcname, name string, followSymlinks bool) (FileInfo, error) {
if len(name) == 0 {
return nil, &PathError{Op: funcname, Path: name, Err: syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)}
}
// See https://golang.org/issues/19922#issuecomment-300031421 for details.
var fa syscall.Win32FileAttributeData
err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa)))
+
+ // 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)
+ if fd.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
+ // Not a symlink or mount point. FindFirstFile is good enough.
+ fs := newFileStatFromWin32finddata(&fd)
+ if err := fs.saveInfoFromPath(name); err != nil {
+ return nil, err
+ }
+ return fs, nil
+ }
+ }
+
if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
- // Not a symlink.
+ // The file is definitely not a symlink, because it isn't any kind of reparse point.
+ // The information we got from GetFileAttributesEx is good enough for now.
fs := &fileStat{
FileAttributes: fa.FileAttributes,
CreationTime: fa.CreationTime,
}
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)
+ // Use CreateFile to determine whether the file is a symlink and, if so,
+ // save information about the link target.
+ // Set FILE_FLAG_BACKUP_SEMANTICS so that CreateFile will create the handle
+ // even if name refers to a directory.
+ h, err := syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil {
+ // Since CreateFile failed, we can't determine whether name refers to a
+ // symlink, or some other kind of reparse point. Since we can't return a
+ // FileInfo with a known-accurate Mode, we must return an error.
return nil, &PathError{Op: "CreateFile", Path: name, Err: err}
}
- defer syscall.CloseHandle(h)
- return statHandle(name, h)
+
+ fi, err := statHandle(name, h)
+ syscall.CloseHandle(h)
+ if err == nil && followSymlinks && fi.(*fileStat).isSymlink() {
+ // To obtain information about the link target, we reopen the file without
+ // FILE_FLAG_OPEN_REPARSE_POINT and examine the resulting handle.
+ // (See https://devblogs.microsoft.com/oldnewthing/20100212-00/?p=14963.)
+ h, err = syscall.CreateFile(namep, 0, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
+ if err != nil {
+ // name refers to a symlink, but we couldn't resolve the symlink target.
+ return nil, &PathError{Op: "CreateFile", Path: name, Err: err}
+ }
+ defer syscall.CloseHandle(h)
+ return statHandle(name, h)
+ }
+ return fi, err
}
func statHandle(name string, h syscall.Handle) (FileInfo, error) {
// statNolog implements Stat for Windows.
func statNolog(name string) (FileInfo, error) {
- return stat("Stat", name, syscall.FILE_FLAG_BACKUP_SEMANTICS)
+ return stat("Stat", name, true)
}
// 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)
+ return stat("Lstat", name, false)
}
FileSizeLow uint32
// from Win32finddata
- Reserved0 uint32
+ ReparseTag uint32
// what syscall.GetFileType returns
filetype uint32
vol: d.VolumeSerialNumber,
idxhi: d.FileIndexHigh,
idxlo: d.FileIndexLow,
- Reserved0: ti.ReparseTag,
+ 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.
// newFileStatFromWin32finddata copies all required information
// from syscall.Win32finddata d into the newly created fileStat.
func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat {
- return &fileStat{
+ fs := &fileStat{
FileAttributes: d.FileAttributes,
CreationTime: d.CreationTime,
LastAccessTime: d.LastAccessTime,
LastWriteTime: d.LastWriteTime,
FileSizeHigh: d.FileSizeHigh,
FileSizeLow: d.FileSizeLow,
- Reserved0: d.Reserved0,
}
+ 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 {
- // 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
+ // 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 {
case syscall.FILE_TYPE_CHAR:
m |= ModeDevice | ModeCharDevice
}
+ if fs.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && m&ModeType == 0 {
+ m |= ModeIrregular
+ }
return m
}
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
- }
+
+ // 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