1
0
mirror of https://github.com/golang/go synced 2024-11-18 12:04:57 -07:00

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 <hrshvardhana@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
Alex Brainman 2017-04-27 17:43:33 +10:00
parent e94b9d4143
commit 5300362172
2 changed files with 79 additions and 38 deletions

View 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)
}
}

View 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 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)
}
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
}
if fi.Mode()&ModeSymlink == 0 {
fi.(*fileStat).name = basename(name)
return fi, nil
}
newlink, err := Readlink(link)
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) {