diff options
| author | Alex Brainman <alex.brainman@gmail.com> | 2018-10-20 16:30:57 +1100 |
|---|---|---|
| committer | Alex Brainman <alex.brainman@gmail.com> | 2018-11-02 07:30:03 +0000 |
| commit | f10815898c0732e2e6cdb697d6f95f33f8650b4e (patch) | |
| tree | cd6140743719a8c6c9066c6866ec70c46fc4f342 /src/os | |
| parent | d154ef60a0c88be98c70bbe1c5735fb7b1f45250 (diff) | |
| download | go-f10815898c0732e2e6cdb697d6f95f33f8650b4e.tar.xz | |
os: use CreateFile for Stat of symlinks
Stat uses Windows FindFirstFile + CreateFile to gather symlink
information - FindFirstFile determines if file is a symlink,
and then CreateFile follows symlink to capture target details.
Lstat only uses FindFirstFile.
This CL replaces current approach with just a call to CreateFile.
Lstat uses FILE_FLAG_OPEN_REPARSE_POINT flag, that instructs
CreateFile not to follow symlink. Other than that both Stat and
Lstat look the same now. New code is simpler.
CreateFile + GetFileInformationByHandle (unlike FindFirstFile)
does not report reparse tag of a file. I tried to ignore reparse
tag altogether. And it works for symlinks and mount points.
Unfortunately (see https://github.com/moby/moby/issues/37026),
files on deduped disk volumes are reported with
FILE_ATTRIBUTE_REPARSE_POINT attribute set and reparse tag set
to IO_REPARSE_TAG_DEDUP. So, if we ignore reparse tag, Lstat
interprets deduped volume files as symlinks. That is incorrect.
So I had to add GetFileInformationByHandleEx call to gather
reparse tag after calling CreateFile and GetFileInformationByHandle.
Fixes #27225
Fixes #27515
Change-Id: If60233bcf18836c147597cc17450d82f3f88c623
Reviewed-on: https://go-review.googlesource.com/c/143578
Run-TryBot: Alex Brainman <alex.brainman@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Kirill Kolyshkin <kolyshkin@gmail.com>
Diffstat (limited to 'src/os')
| -rw-r--r-- | src/os/stat_test.go | 10 | ||||
| -rw-r--r-- | src/os/stat_windows.go | 85 | ||||
| -rw-r--r-- | src/os/types_windows.go | 69 |
3 files changed, 65 insertions, 99 deletions
diff --git a/src/os/stat_test.go b/src/os/stat_test.go index da20a4fdbf..60f3b4c587 100644 --- a/src/os/stat_test.go +++ b/src/os/stat_test.go @@ -250,10 +250,6 @@ func TestFileAndSymlinkStats(t *testing.T) { // see issue 27225 for details func TestSymlinkWithTrailingSlash(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skipping on windows; issue 27225") - } - testenv.MustHaveSymlink(t) tmpdir, err := ioutil.TempDir("", "TestSymlinkWithTrailingSlash") @@ -274,7 +270,11 @@ func TestSymlinkWithTrailingSlash(t *testing.T) { } dirlinkWithSlash := dirlink + string(os.PathSeparator) - testDirStats(t, dirlinkWithSlash) + if runtime.GOOS == "windows" { + testSymlinkStats(t, dirlinkWithSlash, true) + } else { + testDirStats(t, dirlinkWithSlash) + } fi1, err := os.Stat(dir) if err != nil { diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go index 19cc0cf6b7..f4700f5818 100644 --- a/src/os/stat_windows.go +++ b/src/os/stat_windows.go @@ -5,7 +5,9 @@ package os import ( + "internal/syscall/windows" "syscall" + "unsafe" ) // isNulName returns true if name is NUL file name. @@ -58,33 +60,59 @@ func (file *File) Stat() (FileInfo, error) { return fs, err } -// statNolog implements Stat for Windows. -func statNolog(name string) (FileInfo, error) { +// 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{"Stat", name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)} + return nil, &PathError{funcname, name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)} } if isNulName(name) { return &devNullStat, nil } namep, err := syscall.UTF16PtrFromString(fixLongPath(name)) if err != nil { - return nil, &PathError{"Stat", name, err} + return nil, &PathError{funcname, name, err} } - fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep) - if err != nil { - return nil, 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{ + path: name, + FileAttributes: fa.FileAttributes, + CreationTime: fa.CreationTime, + LastAccessTime: fa.LastAccessTime, + LastWriteTime: fa.LastWriteTime, + FileSizeHigh: fa.FileSizeHigh, + FileSizeLow: fa.FileSizeLow, + } + // Gather full path to be used by os.SameFile later. + if !isAbs(fs.path) { + fs.path, err = syscall.FullPath(fs.path) + if err != nil { + return nil, &PathError{"FullPath", name, err} + } + } + fs.name = basename(name) + return fs, nil } - if !fs.isSymlink() { - err = fs.updatePathAndName(name) + // 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, err + return nil, &PathError{"FindFirstFile", name, err} } - return fs, nil + syscall.FindClose(sh) + return newFileStatFromWin32finddata(&fd), nil } - // Use Windows I/O manager to dereference the symbolic link, as per - // https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ + + // Finally use CreateFile. h, err := syscall.CreateFile(namep, 0, 0, nil, - syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0) + syscall.OPEN_EXISTING, createFileAttrs, 0) if err != nil { return nil, &PathError{"CreateFile", name, err} } @@ -93,25 +121,16 @@ func statNolog(name string) (FileInfo, error) { 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) { - if len(name) == 0 { - return nil, &PathError{"Lstat", name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)} - } - if isNulName(name) { - return &devNullStat, nil - } - namep, err := syscall.UTF16PtrFromString(fixLongPath(name)) - if err != nil { - return nil, &PathError{"Lstat", name, err} - } - fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep) - if err != nil { - return nil, err - } - err = fs.updatePathAndName(name) - if err != nil { - return nil, err - } - return fs, nil + 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_windows.go b/src/os/types_windows.go index 7ebeec50ef..8636dc7f05 100644 --- a/src/os/types_windows.go +++ b/src/os/types_windows.go @@ -47,6 +47,13 @@ func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (f if err != nil { return nil, &PathError{"GetFileInformationByHandle", path, 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 { + return nil, &PathError{"GetFileInformationByHandleEx", path, err} + } + return &fileStat{ name: basename(path), FileAttributes: d.FileAttributes, @@ -58,6 +65,7 @@ func newFileStatFromGetFileInformationByHandle(path string, h syscall.Handle) (f 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. @@ -78,67 +86,6 @@ func newFileStatFromWin32finddata(d *syscall.Win32finddata) *fileStat { } } -// newFileStatFromGetFileAttributesExOrFindFirstFile calls GetFileAttributesEx -// and FindFirstFile to gather all required information about the provided file path pathp. -func newFileStatFromGetFileAttributesExOrFindFirstFile(path string, pathp *uint16) (*fileStat, error) { - // As suggested by Microsoft, use GetFileAttributes() to acquire the file information, - // and if it's a reparse point use FindFirstFile() to get the tag: - // https://msdn.microsoft.com/en-us/library/windows/desktop/aa363940(v=vs.85).aspx - // Notice that always calling FindFirstFile can create performance problems - // (https://golang.org/issues/19922#issuecomment-300031421) - var fa syscall.Win32FileAttributeData - err := syscall.GetFileAttributesEx(pathp, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) - if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { - // Not a symlink. - return &fileStat{ - FileAttributes: fa.FileAttributes, - CreationTime: fa.CreationTime, - LastAccessTime: fa.LastAccessTime, - LastWriteTime: fa.LastWriteTime, - FileSizeHigh: fa.FileSizeHigh, - FileSizeLow: fa.FileSizeLow, - }, nil - } - // GetFileAttributesEx returns ERROR_INVALID_NAME if called - // for invalid file name like "*.txt". Do not attempt to call - // FindFirstFile with "*.txt", because FindFirstFile will - // succeed. So just return ERROR_INVALID_NAME instead. - // see https://golang.org/issue/24999 for details. - if errno, _ := err.(syscall.Errno); errno == windows.ERROR_INVALID_NAME { - return nil, &PathError{"GetFileAttributesEx", path, err} - } - // We might have symlink here. But some directories also have - // FileAttributes FILE_ATTRIBUTE_REPARSE_POINT bit set. - // For example, OneDrive directory is like that - // (see golang.org/issue/22579 for details). - // So use FindFirstFile instead to distinguish directories like - // OneDrive from real symlinks (see instructions described at - // https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ - // and in particular bits about using both FileAttributes and - // Reserved0 fields). - var fd syscall.Win32finddata - sh, err := syscall.FindFirstFile(pathp, &fd) - if err != nil { - return nil, &PathError{"FindFirstFile", path, err} - } - syscall.FindClose(sh) - - return newFileStatFromWin32finddata(&fd), nil -} - -func (fs *fileStat) updatePathAndName(name string) error { - fs.path = name - if !isAbs(fs.path) { - var err error - fs.path, err = syscall.FullPath(fs.path) - if err != nil { - return &PathError{"FullPath", name, err} - } - } - fs.name = basename(name) - return nil -} - func (fs *fileStat) isSymlink() bool { // Use instructions described at // https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ |
