package windows
+import (
+ "syscall"
+ "unsafe"
+)
+
const (
FSCTL_SET_REPARSE_POINT = 0x000900A4
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
// 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
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,
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])
+}
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())
}
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
+ }
+ }
+}
package os
import (
+ "errors"
"internal/poll"
"internal/syscall/windows"
"runtime"
}
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
+}
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")
+}