]> Cypherpunks repositories - gostls13.git/commitdiff
os: reimplement windows os.Stat
authorAlex Brainman <alex.brainman@gmail.com>
Thu, 27 Apr 2017 07:43:33 +0000 (17:43 +1000)
committerAlex Brainman <alex.brainman@gmail.com>
Sun, 7 May 2017 01:26:02 +0000 (01:26 +0000)
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 <hrshvardhana@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
src/os/os_windows_test.go
src/os/stat_windows.go

index fcf6fa6d1bc7527ec7d75a647eeb9047114e8611..04c4a4af3347ca7ee2694b6921db34ddc814640b 100644 (file)
@@ -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)
        }
 }
 
index 9b10f8b5cb4e3e74ee129554cfd8b1bad64584ac..a3304de4f1fb7bfa4b54ee7907f8ad3a2f2efd91 100644 (file)
@@ -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) {