From 53003621720ff39c4745a76f76847123c27b01ea Mon Sep 17 00:00:00 2001 From: Alex Brainman Date: Thu, 27 Apr 2017 17:43:33 +1000 Subject: [PATCH] os: reimplement windows os.Stat Currently windows Stat uses combination of Lstat and Readlink to walk symlinks until it reaches file or directory. Windows Readlink is implemented via Windows DeviceIoControl(FSCTL_GET_REPARSE_POINT, ...) call, but that call does not work on network shares or inside of Docker container (see issues #18555 ad #19922 for details). But Raymond Chen suggests different approach: https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ - he suggests to use Windows I/O manager to dereferences the symbolic link. This appears to work for all normal symlinks, but also for network shares and inside of Docker container. This CL implements described procedure. I also had to adjust TestStatSymlinkLoop, because the test is expecting Stat to return syscall.ELOOP for symlink with a loop. But new Stat returns Windows error of ERROR_CANT_RESOLVE_FILENAME = 1921 instead. I could map ERROR_CANT_RESOLVE_FILENAME into syscall.ELOOP, but I suspect the former is broader than later. And ERROR_CANT_RESOLVE_FILENAME message text of "The name of the file cannot be resolved by the system." sounds fine to me. Fixes #10935 Fixes #18555 Fixes #19922 Change-Id: I979636064cdbdb9c7c840cf8ae73fe2c24499879 Reviewed-on: https://go-review.googlesource.com/41834 Reviewed-by: Harshavardhana Reviewed-by: Ian Lance Taylor --- src/os/os_windows_test.go | 4 +- src/os/stat_windows.go | 111 ++++++++++++++++++++++++++------------ 2 files changed, 78 insertions(+), 37 deletions(-) diff --git a/src/os/os_windows_test.go b/src/os/os_windows_test.go index fcf6fa6d1b..04c4a4af33 100644 --- a/src/os/os_windows_test.go +++ b/src/os/os_windows_test.go @@ -667,8 +667,8 @@ func TestStatSymlinkLoop(t *testing.T) { defer os.Remove("x") _, err = os.Stat("x") - if perr, ok := err.(*os.PathError); !ok || perr.Err != syscall.ELOOP { - t.Errorf("expected *PathError with ELOOP, got %T: %v\n", err, err) + if _, ok := err.(*os.PathError); !ok { + t.Errorf("expected *PathError, got %T: %v\n", err, err) } } diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go index 9b10f8b5cb..a3304de4f1 100644 --- a/src/os/stat_windows.go +++ b/src/os/stat_windows.go @@ -61,32 +61,85 @@ func (file *File) Stat() (FileInfo, error) { // Stat returns a FileInfo structure describing the named file. // If there is an error, it will be of type *PathError. func Stat(name string) (FileInfo, error) { - var fi FileInfo - var err error - link := name - for i := 0; i < 255; i++ { - fi, err = Lstat(link) - if err != nil { - return nil, err - } - if fi.Mode()&ModeSymlink == 0 { - fi.(*fileStat).name = basename(name) - return fi, nil + if len(name) == 0 { + return nil, &PathError{"Stat", name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)} + } + if name == DevNull { + return &devNullStat, nil + } + namep, err := syscall.UTF16PtrFromString(fixLongPath(name)) + if err != nil { + return nil, &PathError{"Stat", name, err} + } + + // Use Windows I/O manager to dereferences the symbolic link, as per + // https://blogs.msdn.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 { + if err == windows.ERROR_SHARING_VIOLATION { + // try FindFirstFile now that CreateFile failed + return statWithFindFirstFile(name, namep) } - newlink, err := Readlink(link) + return nil, &PathError{"CreateFile", name, err} + } + defer syscall.CloseHandle(h) + + var d syscall.ByHandleFileInformation + err = syscall.GetFileInformationByHandle(h, &d) + if err != nil { + return nil, &PathError{"GetFileInformationByHandle", name, err} + } + return &fileStat{ + name: basename(name), + sys: syscall.Win32FileAttributeData{ + 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, + // 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. + // Also do not set fileStat.filetype, because it is only used for + // console and stdin/stdout. But you cannot call os.Stat for these. + }, nil +} + +// statWithFindFirstFile is used by Stat to handle special case of stating +// c:\pagefile.sys. We might discovered other files need similar treatment. +func statWithFindFirstFile(name string, namep *uint16) (FileInfo, error) { + var fd syscall.Win32finddata + h, err := syscall.FindFirstFile(namep, &fd) + if err != nil { + return nil, &PathError{"FindFirstFile", name, err} + } + syscall.FindClose(h) + + fullpath := name + if !isAbs(fullpath) { + fullpath, err = syscall.FullPath(fullpath) if err != nil { - return nil, err - } - switch { - case isAbs(newlink): - link = newlink - case len(newlink) > 0 && IsPathSeparator(newlink[0]): - link = volumeName(link) + newlink - default: - link = dirname(link) + `\` + newlink + return nil, &PathError{"FullPath", name, err} } } - return nil, &PathError{"Stat", name, syscall.ELOOP} + return &fileStat{ + name: basename(name), + path: fullpath, + sys: syscall.Win32FileAttributeData{ + FileAttributes: fd.FileAttributes, + CreationTime: fd.CreationTime, + LastAccessTime: fd.LastAccessTime, + LastWriteTime: fd.LastWriteTime, + FileSizeHigh: fd.FileSizeHigh, + FileSizeLow: fd.FileSizeLow, + }, + }, nil } // Lstat returns the FileInfo structure describing the named file. @@ -111,19 +164,7 @@ func Lstat(name string) (FileInfo, error) { return nil, &PathError{"GetFileAttributesEx", name, e} } // try FindFirstFile now that GetFileAttributesEx failed - var fd syscall.Win32finddata - h, e2 := syscall.FindFirstFile(namep, &fd) - if e2 != nil { - return nil, &PathError{"FindFirstFile", name, e} - } - syscall.FindClose(h) - - fs.sys.FileAttributes = fd.FileAttributes - fs.sys.CreationTime = fd.CreationTime - fs.sys.LastAccessTime = fd.LastAccessTime - fs.sys.LastWriteTime = fd.LastWriteTime - fs.sys.FileSizeHigh = fd.FileSizeHigh - fs.sys.FileSizeLow = fd.FileSizeLow + return statWithFindFirstFile(name, namep) } fs.path = name if !isAbs(fs.path) { -- 2.50.0