Skip to content

Commit

Permalink
os: use WIN32_FIND_DATA.Reserved0 to identify symlinks
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
alexbrainman committed Mar 7, 2018
1 parent d7eb490 commit e83601b
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 135 deletions.
17 changes: 4 additions & 13 deletions src/os/dir_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,10 @@ func (file *File) readdir(n int) (fi []FileInfo, err error) {
if name == "." || name == ".." { // Useless names
continue
}
f := &fileStat{
name: name,
sys: syscall.Win32FileAttributeData{
FileAttributes: d.FileAttributes,
CreationTime: d.CreationTime,
LastAccessTime: d.LastAccessTime,
LastWriteTime: d.LastWriteTime,
FileSizeHigh: d.FileSizeHigh,
FileSizeLow: d.FileSizeLow,
},
path: file.dirinfo.path,
appendNameToPath: true,
}
f := newFileStatFromWin32finddata(d)
f.name = name
f.path = file.dirinfo.path
f.appendNameToPath = true
n--
fi = append(fi, f)
}
Expand Down
86 changes: 86 additions & 0 deletions src/os/os_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"internal/poll"
"internal/syscall/windows"
"internal/syscall/windows/registry"
"internal/testenv"
"io"
"io/ioutil"
Expand Down Expand Up @@ -893,3 +894,88 @@ func main() {
}
}
}

func testIsDir(t *testing.T, path string, fi os.FileInfo) {
t.Helper()
if !fi.IsDir() {
t.Errorf("%q should be a directory", path)
}
if fi.Mode()&os.ModeSymlink != 0 {
t.Errorf("%q should not be a symlink", path)
}
}

func findOneDriveDir() (string, error) {
// as per https://stackoverflow.com/questions/42519624/how-to-determine-location-of-onedrive-on-windows-7-and-8-in-c
const onedrivekey = `SOFTWARE\Microsoft\OneDrive`
k, err := registry.OpenKey(registry.CURRENT_USER, onedrivekey, registry.READ)
if err != nil {
return "", fmt.Errorf("OpenKey(%q) failed: %v", onedrivekey, err)
}
defer k.Close()

path, _, err := k.GetStringValue("UserFolder")
if err != nil {
return "", fmt.Errorf("reading UserFolder failed: %v", err)
}
return path, nil
}

// TestOneDrive verifies that OneDrive folder is a directory and not a symlink.
func TestOneDrive(t *testing.T) {
dir, err := findOneDriveDir()
if err != nil {
t.Skipf("Skipping, because we did not find OneDrive directory: %v", err)
}

// test os.Stat
fi, err := os.Stat(dir)
if err != nil {
t.Fatal(err)
}
testIsDir(t, dir, fi)

// test os.Lstat
fi, err = os.Lstat(dir)
if err != nil {
t.Fatal(err)
}
testIsDir(t, dir, fi)

// test os.File.Stat
f, err := os.Open(dir)
if err != nil {
t.Fatal(err)
}
defer f.Close()

fi, err = f.Stat()
if err != nil {
t.Fatal(err)
}
testIsDir(t, dir, fi)

// test os.FileInfo returned by os.Readdir
parent, err := os.Open(filepath.Dir(dir))
if err != nil {
t.Fatal(err)
}
defer parent.Close()

fis, err := parent.Readdir(-1)
if err != nil {
t.Fatal(err)
}
fi = nil
base := filepath.Base(dir)
for _, fi2 := range fis {
if fi2.Name() == base {
fi = fi2
break
}
}
if fi == nil {
t.Errorf("failed to find %q in its parent", dir)
}
testIsDir(t, dir, fi)
}
138 changes: 25 additions & 113 deletions src/os/stat_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
package os

import (
"internal/syscall/windows"
"syscall"
"unsafe"
)

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

var d syscall.ByHandleFileInformation
err = file.pfd.GetFileInformationByHandle(&d)
fs, err := newFileStatFromGetFileInformationByHandle(file.name, file.pfd.Sysfd)
if err != nil {
return nil, &PathError{"GetFileInformationByHandle", file.name, err}
}
return &fileStat{
name: basename(file.name),
sys: syscall.Win32FileAttributeData{
FileAttributes: d.FileAttributes,
CreationTime: d.CreationTime,
LastAccessTime: d.LastAccessTime,
LastWriteTime: d.LastWriteTime,
FileSizeHigh: d.FileSizeHigh,
FileSizeLow: d.FileSizeLow,
},
filetype: ft,
vol: d.VolumeSerialNumber,
idxhi: d.FileIndexHigh,
idxlo: d.FileIndexLow,
}, nil
return nil, err
}
fs.filetype = ft
return fs, err
}

// statNolog implements Stat for Windows.
Expand All @@ -68,91 +52,27 @@ func statNolog(name string) (FileInfo, error) {
if err != nil {
return nil, &PathError{"Stat", name, err}
}
// Apparently (see https://golang.org/issues/19922#issuecomment-300031421)
// GetFileAttributesEx is fastest approach to get file info.
// It does not work for symlinks. But symlinks are rare,
// so try GetFileAttributesEx first.
var fs fileStat
err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fs.sys)))
if err == nil && fs.sys.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 {
fs.path = name
if !isAbs(fs.path) {
fs.path, err = syscall.FullPath(fs.path)
if err != nil {
return nil, &PathError{"FullPath", name, err}
}
fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep)
if err != nil {
return nil, err
}
if !fs.isSymlink() {
err = fs.updatePathAndName(name)
if err != nil {
return nil, err
}
fs.name = basename(name)
return &fs, nil
return fs, nil
}
// Use Windows I/O manager to dereference 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 statting
// c:\pagefile.sys. We might discover that 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, &PathError{"FullPath", name, err}
}
}
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
return newFileStatFromGetFileInformationByHandle(name, h)
}

// lstatNolog implements Lstat for Windows.
Expand All @@ -163,25 +83,17 @@ func lstatNolog(name string) (FileInfo, error) {
if name == DevNull {
return &devNullStat, nil
}
fs := &fileStat{name: basename(name)}
namep, e := syscall.UTF16PtrFromString(fixLongPath(name))
if e != nil {
return nil, &PathError{"Lstat", name, e}
namep, err := syscall.UTF16PtrFromString(fixLongPath(name))
if err != nil {
return nil, &PathError{"Lstat", name, err}
}
fs, err := newFileStatFromGetFileAttributesExOrFindFirstFile(name, namep)
if err != nil {
return nil, err
}
e = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fs.sys)))
if e != nil {
if e != windows.ERROR_SHARING_VIOLATION {
return nil, &PathError{"GetFileAttributesEx", name, e}
}
// try FindFirstFile now that GetFileAttributesEx failed
return statWithFindFirstFile(name, namep)
}
fs.path = name
if !isAbs(fs.path) {
fs.path, e = syscall.FullPath(fs.path)
if e != nil {
return nil, &PathError{"FullPath", name, e}
}
err = fs.updatePathAndName(name)
if err != nil {
return nil, err
}
return fs, nil
}
Loading

0 comments on commit e83601b

Please sign in to comment.