Skip to content

Commit 5300362

Browse files
committed
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 <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent e94b9d4 commit 5300362

File tree

2 files changed

+78
-37
lines changed

2 files changed

+78
-37
lines changed

src/os/os_windows_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,8 +667,8 @@ func TestStatSymlinkLoop(t *testing.T) {
667667
defer os.Remove("x")
668668

669669
_, err = os.Stat("x")
670-
if perr, ok := err.(*os.PathError); !ok || perr.Err != syscall.ELOOP {
671-
t.Errorf("expected *PathError with ELOOP, got %T: %v\n", err, err)
670+
if _, ok := err.(*os.PathError); !ok {
671+
t.Errorf("expected *PathError, got %T: %v\n", err, err)
672672
}
673673
}
674674

src/os/stat_windows.go

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,85 @@ func (file *File) Stat() (FileInfo, error) {
6161
// Stat returns a FileInfo structure describing the named file.
6262
// If there is an error, it will be of type *PathError.
6363
func Stat(name string) (FileInfo, error) {
64-
var fi FileInfo
65-
var err error
66-
link := name
67-
for i := 0; i < 255; i++ {
68-
fi, err = Lstat(link)
69-
if err != nil {
70-
return nil, err
71-
}
72-
if fi.Mode()&ModeSymlink == 0 {
73-
fi.(*fileStat).name = basename(name)
74-
return fi, nil
64+
if len(name) == 0 {
65+
return nil, &PathError{"Stat", name, syscall.Errno(syscall.ERROR_PATH_NOT_FOUND)}
66+
}
67+
if name == DevNull {
68+
return &devNullStat, nil
69+
}
70+
namep, err := syscall.UTF16PtrFromString(fixLongPath(name))
71+
if err != nil {
72+
return nil, &PathError{"Stat", name, err}
73+
}
74+
75+
// Use Windows I/O manager to dereferences the symbolic link, as per
76+
// https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/
77+
h, err := syscall.CreateFile(namep, 0, 0, nil,
78+
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
79+
if err != nil {
80+
if err == windows.ERROR_SHARING_VIOLATION {
81+
// try FindFirstFile now that CreateFile failed
82+
return statWithFindFirstFile(name, namep)
7583
}
76-
newlink, err := Readlink(link)
84+
return nil, &PathError{"CreateFile", name, err}
85+
}
86+
defer syscall.CloseHandle(h)
87+
88+
var d syscall.ByHandleFileInformation
89+
err = syscall.GetFileInformationByHandle(h, &d)
90+
if err != nil {
91+
return nil, &PathError{"GetFileInformationByHandle", name, err}
92+
}
93+
return &fileStat{
94+
name: basename(name),
95+
sys: syscall.Win32FileAttributeData{
96+
FileAttributes: d.FileAttributes,
97+
CreationTime: d.CreationTime,
98+
LastAccessTime: d.LastAccessTime,
99+
LastWriteTime: d.LastWriteTime,
100+
FileSizeHigh: d.FileSizeHigh,
101+
FileSizeLow: d.FileSizeLow,
102+
},
103+
vol: d.VolumeSerialNumber,
104+
idxhi: d.FileIndexHigh,
105+
idxlo: d.FileIndexLow,
106+
// fileStat.path is used by os.SameFile to decide, if it needs
107+
// to fetch vol, idxhi and idxlo. But these are already set,
108+
// so set fileStat.path to "" to prevent os.SameFile doing it again.
109+
// Also do not set fileStat.filetype, because it is only used for
110+
// console and stdin/stdout. But you cannot call os.Stat for these.
111+
}, nil
112+
}
113+
114+
// statWithFindFirstFile is used by Stat to handle special case of stating
115+
// c:\pagefile.sys. We might discovered other files need similar treatment.
116+
func statWithFindFirstFile(name string, namep *uint16) (FileInfo, error) {
117+
var fd syscall.Win32finddata
118+
h, err := syscall.FindFirstFile(namep, &fd)
119+
if err != nil {
120+
return nil, &PathError{"FindFirstFile", name, err}
121+
}
122+
syscall.FindClose(h)
123+
124+
fullpath := name
125+
if !isAbs(fullpath) {
126+
fullpath, err = syscall.FullPath(fullpath)
77127
if err != nil {
78-
return nil, err
79-
}
80-
switch {
81-
case isAbs(newlink):
82-
link = newlink
83-
case len(newlink) > 0 && IsPathSeparator(newlink[0]):
84-
link = volumeName(link) + newlink
85-
default:
86-
link = dirname(link) + `\` + newlink
128+
return nil, &PathError{"FullPath", name, err}
87129
}
88130
}
89-
return nil, &PathError{"Stat", name, syscall.ELOOP}
131+
return &fileStat{
132+
name: basename(name),
133+
path: fullpath,
134+
sys: syscall.Win32FileAttributeData{
135+
FileAttributes: fd.FileAttributes,
136+
CreationTime: fd.CreationTime,
137+
LastAccessTime: fd.LastAccessTime,
138+
LastWriteTime: fd.LastWriteTime,
139+
FileSizeHigh: fd.FileSizeHigh,
140+
FileSizeLow: fd.FileSizeLow,
141+
},
142+
}, nil
90143
}
91144

92145
// Lstat returns the FileInfo structure describing the named file.
@@ -111,19 +164,7 @@ func Lstat(name string) (FileInfo, error) {
111164
return nil, &PathError{"GetFileAttributesEx", name, e}
112165
}
113166
// try FindFirstFile now that GetFileAttributesEx failed
114-
var fd syscall.Win32finddata
115-
h, e2 := syscall.FindFirstFile(namep, &fd)
116-
if e2 != nil {
117-
return nil, &PathError{"FindFirstFile", name, e}
118-
}
119-
syscall.FindClose(h)
120-
121-
fs.sys.FileAttributes = fd.FileAttributes
122-
fs.sys.CreationTime = fd.CreationTime
123-
fs.sys.LastAccessTime = fd.LastAccessTime
124-
fs.sys.LastWriteTime = fd.LastWriteTime
125-
fs.sys.FileSizeHigh = fd.FileSizeHigh
126-
fs.sys.FileSizeLow = fd.FileSizeLow
167+
return statWithFindFirstFile(name, namep)
127168
}
128169
fs.path = name
129170
if !isAbs(fs.path) {

0 commit comments

Comments
 (0)