mirror of
https://github.com/golang/go
synced 2024-11-17 00:14:50 -07:00
os: make Readlink work with symlinks with target like \??\Volume{ABCD}\
windows-arm TMP directory live inside such link (see https://github.com/golang/go/issues/29746#issuecomment-456526811 for details), so symlinks like that will be common at least on windows-arm. This CL builds on current syscall.Readlink implementation. Main difference between the two is how new code handles symlink targets, like \??\Volume{ABCD}\. New implementation uses Windows CreateFile API with FILE_FLAG_OPEN_REPARSE_POINT flag to get \??\Volume{ABCD}\ file handle. And then it uses Windows GetFinalPathNameByHandle with VOLUME_NAME_DOS flag to convert that handle into standard Windows path. FILE_FLAG_OPEN_REPARSE_POINT flag ensures that symlink is not followed when CreateFile opens the file. Fixes #30463 Change-Id: I33b18227ce36144caed694169ef2e429fd995fb4 Reviewed-on: https://go-review.googlesource.com/c/164201 Run-TryBot: Alex Brainman <alex.brainman@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
parent
e27402aee0
commit
2edd559223
@ -4,6 +4,11 @@
|
||||
|
||||
package windows
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
FSCTL_SET_REPARSE_POINT = 0x000900A4
|
||||
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
|
||||
@ -15,6 +20,13 @@ const (
|
||||
// in https://msdn.microsoft.com/en-us/library/cc232007.aspx
|
||||
// and https://msdn.microsoft.com/en-us/library/cc232006.aspx.
|
||||
|
||||
type REPARSE_DATA_BUFFER struct {
|
||||
ReparseTag uint32
|
||||
ReparseDataLength uint16
|
||||
Reserved uint16
|
||||
DUMMYUNIONNAME byte
|
||||
}
|
||||
|
||||
// REPARSE_DATA_BUFFER_HEADER is a common part of REPARSE_DATA_BUFFER structure.
|
||||
type REPARSE_DATA_BUFFER_HEADER struct {
|
||||
ReparseTag uint32
|
||||
@ -46,6 +58,12 @@ type SymbolicLinkReparseBuffer struct {
|
||||
PathBuffer [1]uint16
|
||||
}
|
||||
|
||||
// Path returns path stored in rb.
|
||||
func (rb *SymbolicLinkReparseBuffer) Path() string {
|
||||
p := (*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))
|
||||
return syscall.UTF16ToString(p[rb.SubstituteNameOffset/2 : (rb.SubstituteNameOffset+rb.SubstituteNameLength)/2])
|
||||
}
|
||||
|
||||
type MountPointReparseBuffer struct {
|
||||
// The integer that contains the offset, in bytes,
|
||||
// of the substitute name string in the PathBuffer array,
|
||||
@ -62,3 +80,9 @@ type MountPointReparseBuffer struct {
|
||||
PrintNameLength uint16
|
||||
PathBuffer [1]uint16
|
||||
}
|
||||
|
||||
// Path returns path stored in rb.
|
||||
func (rb *MountPointReparseBuffer) Path() string {
|
||||
p := (*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))
|
||||
return syscall.UTF16ToString(p[rb.SubstituteNameOffset/2 : (rb.SubstituteNameOffset+rb.SubstituteNameLength)/2])
|
||||
}
|
||||
|
@ -7,32 +7,12 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sigpipe() // implemented in package runtime
|
||||
|
||||
// Readlink returns the destination of the named symbolic link.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func Readlink(name string) (string, error) {
|
||||
for len := 128; ; len *= 2 {
|
||||
b := make([]byte, len)
|
||||
n, e := fixCount(syscall.Readlink(fixLongPath(name), b))
|
||||
// buffer too small
|
||||
if runtime.GOOS == "aix" && e == syscall.ERANGE {
|
||||
continue
|
||||
}
|
||||
if e != nil {
|
||||
return "", &PathError{"readlink", name, e}
|
||||
}
|
||||
if n < len {
|
||||
return string(b[0:n]), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syscallMode returns the syscall-specific mode bits from Go's portable mode bits.
|
||||
func syscallMode(i FileMode) (o uint32) {
|
||||
o |= uint32(i.Perm())
|
||||
|
@ -399,3 +399,22 @@ func (f *File) readdir(n int) (fi []FileInfo, err error) {
|
||||
}
|
||||
return fi, err
|
||||
}
|
||||
|
||||
// Readlink returns the destination of the named symbolic link.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func Readlink(name string) (string, error) {
|
||||
for len := 128; ; len *= 2 {
|
||||
b := make([]byte, len)
|
||||
n, e := fixCount(syscall.Readlink(name, b))
|
||||
// buffer too small
|
||||
if runtime.GOOS == "aix" && e == syscall.ERANGE {
|
||||
continue
|
||||
}
|
||||
if e != nil {
|
||||
return "", &PathError{"readlink", name, e}
|
||||
}
|
||||
if n < len {
|
||||
return string(b[0:n]), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"internal/poll"
|
||||
"internal/syscall/windows"
|
||||
"runtime"
|
||||
@ -396,3 +397,122 @@ func Symlink(oldname, newname string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openSymlink calls CreateFile Windows API with FILE_FLAG_OPEN_REPARSE_POINT
|
||||
// parameter, so that Windows does not follow symlink, if path is a symlink.
|
||||
// openSymlink returns opened file handle.
|
||||
func openSymlink(path string) (syscall.Handle, error) {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
attrs := uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS)
|
||||
// Use FILE_FLAG_OPEN_REPARSE_POINT, otherwise CreateFile will follow symlink.
|
||||
// See https://docs.microsoft.com/en-us/windows/desktop/FileIO/symbolic-link-effects-on-file-systems-functions#createfile-and-createfiletransacted
|
||||
attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT
|
||||
h, err := syscall.CreateFile(p, 0, 0, nil, syscall.OPEN_EXISTING, attrs, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// normaliseLinkPath converts absolute paths returned by
|
||||
// DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, ...)
|
||||
// into paths acceptable by all Windows APIs.
|
||||
// For example, it coverts
|
||||
// \??\C:\foo\bar into C:\foo\bar
|
||||
// \??\UNC\foo\bar into \\foo\bar
|
||||
// \??\Volume{abc}\ into C:\
|
||||
func normaliseLinkPath(path string) (string, error) {
|
||||
if len(path) < 4 || path[:4] != `\??\` {
|
||||
// unexpected path, return it as is
|
||||
return path, nil
|
||||
}
|
||||
// we have path that start with \??\
|
||||
s := path[4:]
|
||||
switch {
|
||||
case len(s) >= 2 && s[1] == ':': // \??\C:\foo\bar
|
||||
return s, nil
|
||||
case len(s) >= 4 && s[:4] == `UNC\`: // \??\UNC\foo\bar
|
||||
return `\\` + s[4:], nil
|
||||
}
|
||||
|
||||
// handle paths, like \??\Volume{abc}\...
|
||||
|
||||
err := windows.LoadGetFinalPathNameByHandle()
|
||||
if err != nil {
|
||||
// we must be using old version of Windows
|
||||
return "", err
|
||||
}
|
||||
|
||||
h, err := openSymlink(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
|
||||
buf := make([]uint16, 100)
|
||||
for {
|
||||
n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), windows.VOLUME_NAME_DOS)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n < uint32(len(buf)) {
|
||||
break
|
||||
}
|
||||
buf = make([]uint16, n)
|
||||
}
|
||||
s = syscall.UTF16ToString(buf)
|
||||
if len(s) > 4 && s[:4] == `\\?\` {
|
||||
s = s[4:]
|
||||
if len(s) > 3 && s[:3] == `UNC` {
|
||||
// return path like \\server\share\...
|
||||
return `\` + s[3:], nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s)
|
||||
}
|
||||
|
||||
func readlink(path string) (string, error) {
|
||||
h, err := openSymlink(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
|
||||
rdbbuf := make([]byte, syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
|
||||
var bytesReturned uint32
|
||||
err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
|
||||
switch rdb.ReparseTag {
|
||||
case syscall.IO_REPARSE_TAG_SYMLINK:
|
||||
rb := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME))
|
||||
s := rb.Path()
|
||||
if rb.Flags&windows.SYMLINK_FLAG_RELATIVE != 0 {
|
||||
return s, nil
|
||||
}
|
||||
return normaliseLinkPath(s)
|
||||
case windows.IO_REPARSE_TAG_MOUNT_POINT:
|
||||
return normaliseLinkPath((*windows.MountPointReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)).Path())
|
||||
default:
|
||||
// the path is not a symlink or junction but another type of reparse
|
||||
// point
|
||||
return "", syscall.ENOENT
|
||||
}
|
||||
}
|
||||
|
||||
// Readlink returns the destination of the named symbolic link.
|
||||
// If there is an error, it will be of type *PathError.
|
||||
func Readlink(name string) (string, error) {
|
||||
s, err := readlink(fixLongPath(name))
|
||||
if err != nil {
|
||||
return "", &PathError{"readlink", name, err}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
@ -1050,3 +1050,118 @@ func TestRootDirAsTemp(t *testing.T) {
|
||||
t.Fatalf("unexpected child process output %q, want %q", have, want)
|
||||
}
|
||||
}
|
||||
|
||||
func testReadlink(t *testing.T, path, want string) {
|
||||
got, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf(`Readlink(%q): got %q, want %q`, path, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func mklink(t *testing.T, link, target string) {
|
||||
output, err := osexec.Command("cmd", "/c", "mklink", link, target).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func mklinkj(t *testing.T, link, target string) {
|
||||
output, err := osexec.Command("cmd", "/c", "mklink", "/J", link, target).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func mklinkd(t *testing.T, link, target string) {
|
||||
output, err := osexec.Command("cmd", "/c", "mklink", "/D", link, target).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run mklink %v %v: %v %q", link, target, err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsReadlink(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "TestWindowsReadlink")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
// Make sure tmpdir is not a symlink, otherwise tests will fail.
|
||||
tmpdir, err = filepath.EvalSymlinks(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.Chdir(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chdir(wd)
|
||||
|
||||
vol := filepath.VolumeName(tmpdir)
|
||||
output, err := osexec.Command("cmd", "/c", "mountvol", vol, "/L").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output)
|
||||
}
|
||||
ntvol := strings.Trim(string(output), " \n\r")
|
||||
|
||||
dir := filepath.Join(tmpdir, "dir")
|
||||
err = os.MkdirAll(dir, 0777)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
absdirjlink := filepath.Join(tmpdir, "absdirjlink")
|
||||
mklinkj(t, absdirjlink, dir)
|
||||
testReadlink(t, absdirjlink, dir)
|
||||
|
||||
ntdirjlink := filepath.Join(tmpdir, "ntdirjlink")
|
||||
mklinkj(t, ntdirjlink, ntvol+absdirjlink[len(filepath.VolumeName(absdirjlink)):])
|
||||
testReadlink(t, ntdirjlink, absdirjlink)
|
||||
|
||||
ntdirjlinktolink := filepath.Join(tmpdir, "ntdirjlinktolink")
|
||||
mklinkj(t, ntdirjlinktolink, ntvol+absdirjlink[len(filepath.VolumeName(absdirjlink)):])
|
||||
testReadlink(t, ntdirjlinktolink, absdirjlink)
|
||||
|
||||
mklinkj(t, "reldirjlink", "dir")
|
||||
testReadlink(t, "reldirjlink", dir) // relative directory junction resolves to absolute path
|
||||
|
||||
// Make sure we have sufficient privilege to run mklink command.
|
||||
testenv.MustHaveSymlink(t)
|
||||
|
||||
absdirlink := filepath.Join(tmpdir, "absdirlink")
|
||||
mklinkd(t, absdirlink, dir)
|
||||
testReadlink(t, absdirlink, dir)
|
||||
|
||||
ntdirlink := filepath.Join(tmpdir, "ntdirlink")
|
||||
mklinkd(t, ntdirlink, ntvol+absdirlink[len(filepath.VolumeName(absdirlink)):])
|
||||
testReadlink(t, ntdirlink, absdirlink)
|
||||
|
||||
mklinkd(t, "reldirlink", "dir")
|
||||
testReadlink(t, "reldirlink", "dir")
|
||||
|
||||
file := filepath.Join(tmpdir, "file")
|
||||
err = ioutil.WriteFile(file, []byte(""), 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
filelink := filepath.Join(tmpdir, "filelink")
|
||||
mklink(t, filelink, file)
|
||||
testReadlink(t, filelink, file)
|
||||
|
||||
linktofilelink := filepath.Join(tmpdir, "linktofilelink")
|
||||
mklink(t, linktofilelink, ntvol+filelink[len(filepath.VolumeName(filelink)):])
|
||||
testReadlink(t, linktofilelink, filelink)
|
||||
|
||||
mklink(t, "relfilelink", "file")
|
||||
testReadlink(t, "relfilelink", "file")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user