Skip to content

Commit e83601b

Browse files
committed
os: use WIN32_FIND_DATA.Reserved0 to identify symlinks
os.Stat implementation uses instructions described at https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/ to distinguish symlinks. In particular, it calls GetFileAttributesEx or FindFirstFile and checks either WIN32_FILE_ATTRIBUTE_DATA.dwFileAttributes or WIN32_FIND_DATA.dwFileAttributes to see if FILE_ATTRIBUTES_REPARSE_POINT flag is set. And that seems to worked fine so far. But now we discovered that OneDrive root folder is determined as directory: c:\>dir C:\Users\Alex | grep OneDrive 30/11/2017 07:25 PM <DIR> OneDrive c:\> while Go identified it as symlink. But we did not follow Microsoft's advice to the letter - we never checked WIN32_FIND_DATA.Reserved0. And adding that extra check makes Go treat OneDrive as symlink. So use FindFirstFile and WIN32_FIND_DATA.Reserved0 to determine symlinks. Fixes #22579 Change-Id: I0cb88929eb8b47b1d24efaf1907ad5a0e20de83f Reviewed-on: https://go-review.googlesource.com/86556 Reviewed-by: Brad Fitzpatrick <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent d7eb490 commit e83601b

File tree

4 files changed

+250
-135
lines changed

4 files changed

+250
-135
lines changed

Diff for: src/os/dir_windows.go

+4-13
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,10 @@ func (file *File) readdir(n int) (fi []FileInfo, err error) {
4646
if name == "." || name == ".." { // Useless names
4747
continue
4848
}
49-
f := &fileStat{
50-
name: name,
51-
sys: syscall.Win32FileAttributeData{
52-
FileAttributes: d.FileAttributes,
53-
CreationTime: d.CreationTime,
54-
LastAccessTime: d.LastAccessTime,
55-
LastWriteTime: d.LastWriteTime,
56-
FileSizeHigh: d.FileSizeHigh,
57-
FileSizeLow: d.FileSizeLow,
58-
},
59-
path: file.dirinfo.path,
60-
appendNameToPath: true,
61-
}
49+
f := newFileStatFromWin32finddata(d)
50+
f.name = name
51+
f.path = file.dirinfo.path
52+
f.appendNameToPath = true
6253
n--
6354
fi = append(fi, f)
6455
}

Diff for: src/os/os_windows_test.go

+86
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"internal/poll"
1010
"internal/syscall/windows"
11+
"internal/syscall/windows/registry"
1112
"internal/testenv"
1213
"io"
1314
"io/ioutil"
@@ -893,3 +894,88 @@ func main() {
893894
}
894895
}
895896
}
897+
898+
func testIsDir(t *testing.T, path string, fi os.FileInfo) {
899+
t.Helper()
900+
if !fi.IsDir() {
901+
t.Errorf("%q should be a directory", path)
902+
}
903+
if fi.Mode()&os.ModeSymlink != 0 {
904+
t.Errorf("%q should not be a symlink", path)
905+
}
906+
}
907+
908+
func findOneDriveDir() (string, error) {
909+
// as per https://stackoverflow.com/questions/42519624/how-to-determine-location-of-onedrive-on-windows-7-and-8-in-c
910+
const onedrivekey = `SOFTWARE\Microsoft\OneDrive`
911+
k, err := registry.OpenKey(registry.CURRENT_USER, onedrivekey, registry.READ)
912+
if err != nil {
913+
return "", fmt.Errorf("OpenKey(%q) failed: %v", onedrivekey, err)
914+
}
915+
defer k.Close()
916+
917+
path, _, err := k.GetStringValue("UserFolder")
918+
if err != nil {
919+
return "", fmt.Errorf("reading UserFolder failed: %v", err)
920+
}
921+
return path, nil
922+
}
923+
924+
// TestOneDrive verifies that OneDrive folder is a directory and not a symlink.
925+
func TestOneDrive(t *testing.T) {
926+
dir, err := findOneDriveDir()
927+
if err != nil {
928+
t.Skipf("Skipping, because we did not find OneDrive directory: %v", err)
929+
}
930+
931+
// test os.Stat
932+
fi, err := os.Stat(dir)
933+
if err != nil {
934+
t.Fatal(err)
935+
}
936+
testIsDir(t, dir, fi)
937+
938+
// test os.Lstat
939+
fi, err = os.Lstat(dir)
940+
if err != nil {
941+
t.Fatal(err)
942+
}
943+
testIsDir(t, dir, fi)
944+
945+
// test os.File.Stat
946+
f, err := os.Open(dir)
947+
if err != nil {
948+
t.Fatal(err)
949+
}
950+
defer f.Close()
951+
952+
fi, err = f.Stat()
953+
if err != nil {
954+
t.Fatal(err)
955+
}
956+
testIsDir(t, dir, fi)
957+
958+
// test os.FileInfo returned by os.Readdir
959+
parent, err := os.Open(filepath.Dir(dir))
960+
if err != nil {
961+
t.Fatal(err)
962+
}
963+
defer parent.Close()
964+
965+
fis, err := parent.Readdir(-1)
966+
if err != nil {
967+
t.Fatal(err)
968+
}
969+
fi = nil
970+
base := filepath.Base(dir)
971+
for _, fi2 := range fis {
972+
if fi2.Name() == base {
973+
fi = fi2
974+
break
975+
}
976+
}
977+
if fi == nil {
978+
t.Errorf("failed to find %q in its parent", dir)
979+
}
980+
testIsDir(t, dir, fi)
981+
}

Diff for: src/os/stat_windows.go

+25-113
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
package os
66

77
import (
8-
"internal/syscall/windows"
98
"syscall"
10-
"unsafe"
119
)
1210

1311
// Stat returns the FileInfo structure describing file.
@@ -34,26 +32,12 @@ func (file *File) Stat() (FileInfo, error) {
3432
return &fileStat{name: basename(file.name), filetype: ft}, nil
3533
}
3634

37-
var d syscall.ByHandleFileInformation
38-
err = file.pfd.GetFileInformationByHandle(&d)
35+
fs, err := newFileStatFromGetFileInformationByHandle(file.name, file.pfd.Sysfd)
3936
if err != nil {
40-
return nil, &PathError{"GetFileInformationByHandle", file.name, err}
41-
}
42-
return &fileStat{
43-
name: basename(file.name),
44-
sys: syscall.Win32FileAttributeData{
45-
FileAttributes: d.FileAttributes,
46-
CreationTime: d.CreationTime,
47-
LastAccessTime: d.LastAccessTime,
48-
LastWriteTime: d.LastWriteTime,
49-
FileSizeHigh: d.FileSizeHigh,
50-
FileSizeLow: d.FileSizeLow,
51-
},
52-
filetype: ft,
53-
vol: d.VolumeSerialNumber,
54-
idxhi: d.FileIndexHigh,
55-
idxlo: d.FileIndexLow,
56-
}, nil
37+
return nil, err
38+
}
39+
fs.filetype = ft
40+
return fs, err
5741
}
5842

5943
// statNolog implements Stat for Windows.
@@ -68,91 +52,27 @@ func statNolog(name string) (FileInfo, error) {
6852
if err != nil {
6953
return nil, &PathError{"Stat", name, err}
7054
}
71-
// Apparently (see https://golang.org/issues/19922#issuecomment-300031421)
72-
// GetFileAttributesEx is fastest approach to get file info.
73-
// It does not work for symlinks. But symlinks are rare,
74-
// so try GetFileAttributesEx first.
75-
var fs fileStat
76-
err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fs.sys)))
77-
if err == nil && fs.sys.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
78-
fs.path = name
79-
if !isAbs(fs.path) {
80-
fs.path, err = syscall.FullPath(fs.path)
81-
if err != nil {
82-
return nil, &PathError{"FullPath", name, err}
83-
}
55+
fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep)
56+
if err != nil {
57+
return nil, err
58+
}
59+
if !fs.isSymlink() {
60+
err = fs.updatePathAndName(name)
61+
if err != nil {
62+
return nil, err
8463
}
85-
fs.name = basename(name)
86-
return &fs, nil
64+
return fs, nil
8765
}
8866
// Use Windows I/O manager to dereference the symbolic link, as per
8967
// https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/
9068
h, err := syscall.CreateFile(namep, 0, 0, nil,
9169
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
9270
if err != nil {
93-
if err == windows.ERROR_SHARING_VIOLATION {
94-
// try FindFirstFile now that CreateFile failed
95-
return statWithFindFirstFile(name, namep)
96-
}
9771
return nil, &PathError{"CreateFile", name, err}
9872
}
9973
defer syscall.CloseHandle(h)
10074

101-
var d syscall.ByHandleFileInformation
102-
err = syscall.GetFileInformationByHandle(h, &d)
103-
if err != nil {
104-
return nil, &PathError{"GetFileInformationByHandle", name, err}
105-
}
106-
return &fileStat{
107-
name: basename(name),
108-
sys: syscall.Win32FileAttributeData{
109-
FileAttributes: d.FileAttributes,
110-
CreationTime: d.CreationTime,
111-
LastAccessTime: d.LastAccessTime,
112-
LastWriteTime: d.LastWriteTime,
113-
FileSizeHigh: d.FileSizeHigh,
114-
FileSizeLow: d.FileSizeLow,
115-
},
116-
vol: d.VolumeSerialNumber,
117-
idxhi: d.FileIndexHigh,
118-
idxlo: d.FileIndexLow,
119-
// fileStat.path is used by os.SameFile to decide if it needs
120-
// to fetch vol, idxhi and idxlo. But these are already set,
121-
// so set fileStat.path to "" to prevent os.SameFile doing it again.
122-
// Also do not set fileStat.filetype, because it is only used for
123-
// console and stdin/stdout. But you cannot call os.Stat for these.
124-
}, nil
125-
}
126-
127-
// statWithFindFirstFile is used by Stat to handle special case of statting
128-
// c:\pagefile.sys. We might discover that other files need similar treatment.
129-
func statWithFindFirstFile(name string, namep *uint16) (FileInfo, error) {
130-
var fd syscall.Win32finddata
131-
h, err := syscall.FindFirstFile(namep, &fd)
132-
if err != nil {
133-
return nil, &PathError{"FindFirstFile", name, err}
134-
}
135-
syscall.FindClose(h)
136-
137-
fullpath := name
138-
if !isAbs(fullpath) {
139-
fullpath, err = syscall.FullPath(fullpath)
140-
if err != nil {
141-
return nil, &PathError{"FullPath", name, err}
142-
}
143-
}
144-
return &fileStat{
145-
name: basename(name),
146-
path: fullpath,
147-
sys: syscall.Win32FileAttributeData{
148-
FileAttributes: fd.FileAttributes,
149-
CreationTime: fd.CreationTime,
150-
LastAccessTime: fd.LastAccessTime,
151-
LastWriteTime: fd.LastWriteTime,
152-
FileSizeHigh: fd.FileSizeHigh,
153-
FileSizeLow: fd.FileSizeLow,
154-
},
155-
}, nil
75+
return newFileStatFromGetFileInformationByHandle(name, h)
15676
}
15777

15878
// lstatNolog implements Lstat for Windows.
@@ -163,25 +83,17 @@ func lstatNolog(name string) (FileInfo, error) {
16383
if name == DevNull {
16484
return &devNullStat, nil
16585
}
166-
fs := &fileStat{name: basename(name)}
167-
namep, e := syscall.UTF16PtrFromString(fixLongPath(name))
168-
if e != nil {
169-
return nil, &PathError{"Lstat", name, e}
86+
namep, err := syscall.UTF16PtrFromString(fixLongPath(name))
87+
if err != nil {
88+
return nil, &PathError{"Lstat", name, err}
89+
}
90+
fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep)
91+
if err != nil {
92+
return nil, err
17093
}
171-
e = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fs.sys)))
172-
if e != nil {
173-
if e != windows.ERROR_SHARING_VIOLATION {
174-
return nil, &PathError{"GetFileAttributesEx", name, e}
175-
}
176-
// try FindFirstFile now that GetFileAttributesEx failed
177-
return statWithFindFirstFile(name, namep)
178-
}
179-
fs.path = name
180-
if !isAbs(fs.path) {
181-
fs.path, e = syscall.FullPath(fs.path)
182-
if e != nil {
183-
return nil, &PathError{"FullPath", name, e}
184-
}
94+
err = fs.updatePathAndName(name)
95+
if err != nil {
96+
return nil, err
18597
}
18698
return fs, nil
18799
}

0 commit comments

Comments
 (0)