pkg os, method (*Root) Link(string, string) error #67002
pkg os, method (*Root) Readlink(string) (string, error) #67002
pkg os, method (*Root) Rename(string, string) error #67002
+pkg os, method (*Root) Symlink(string, string) error #67002
* [os.Root.Link]
* [os.Root.Readlink]
* [os.Root.Rename]
+ * [os.Root.Symlink]
TEXT ·libc_fchownat_trampoline(SB),NOSPLIT,$0-0; JMP libc_fchownat(SB)
TEXT ·libc_renameat_trampoline(SB),NOSPLIT,$0-0; JMP libc_renameat(SB)
TEXT ·libc_linkat_trampoline(SB),NOSPLIT,$0-0; JMP libc_linkat(SB)
+TEXT ·libc_symlinkat_trampoline(SB),NOSPLIT,$0-0; JMP libc_symlinkat(SB)
JMP libc_renameat(SB)
TEXT ·libc_linkat_trampoline(SB),NOSPLIT,$0-0
JMP libc_linkat(SB)
+TEXT ·libc_symlinkat_trampoline(SB),NOSPLIT,$0-0
+ JMP libc_symlinkat(SB)
}
return nil
}
+
+func Symlinkat(oldpath string, newdirfd int, newpath string) error {
+ oldp, err := syscall.BytePtrFromString(oldpath)
+ if err != nil {
+ return err
+ }
+ newp, err := syscall.BytePtrFromString(newpath)
+ if err != nil {
+ return err
+ }
+ _, _, errno := syscall.Syscall(symlinkatTrap,
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)))
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
//go:cgo_import_dynamic libc_linkat linkat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_openat openat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_renameat renameat "libc.a/shr_64.o"
+//go:cgo_import_dynamic libc_symlinkat symlinkat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_unlinkat unlinkat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_readlinkat readlinkat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_mkdirat mkdirat "libc.a/shr_64.o"
}
return nil
}
+
+func libc_symlinkat_trampoline()
+
+//go:cgo_import_dynamic libc_symlinkat symlinkat "/usr/lib/libSystem.B.dylib"
+
+func Symlinkat(oldpath string, newdirfd int, newpath string) error {
+ oldp, err := syscall.BytePtrFromString(oldpath)
+ if err != nil {
+ return err
+ }
+ newp, err := syscall.BytePtrFromString(newpath)
+ if err != nil {
+ return err
+ }
+ _, _, errno := syscall_syscall6(abi.FuncPCABI0(libc_symlinkat_trampoline),
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)),
+ 0,
+ 0,
+ 0)
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
//go:linkname procFchownat libc_fchownat
//go:linkname procRenameat libc_renameat
//go:linkname procLinkat libc_linkat
+//go:linkname procSymlinkat libc_symlinkat
var (
procFstatat,
procFchmodat,
procFchownat,
procRenameat,
- procLinkat uintptr
+ procLinkat,
+ procSymlinkat uintptr
)
func Unlinkat(dirfd int, path string, flags int) error {
}
return nil
}
+
+func Symlinkat(oldpath string, newdirfd int, newpath string) error {
+ oldp, err := syscall.BytePtrFromString(oldpath)
+ if err != nil {
+ return err
+ }
+ newp, err := syscall.BytePtrFromString(newpath)
+ if err != nil {
+ return err
+ }
+ _, _, errno := syscall6(uintptr(unsafe.Pointer(&procSymlinkat)), 3,
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)),
+ 0, 0, 0)
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
}
return nil
}
+
+func libc_symlinkat_trampoline()
+
+//go:cgo_import_dynamic libc_symlinkat symlinkat "libc.so"
+
+func Symlinkat(oldpath string, newdirfd int, newpath string) error {
+ oldp, err := syscall.BytePtrFromString(oldpath)
+ if err != nil {
+ return err
+ }
+ newp, err := syscall.BytePtrFromString(newpath)
+ if err != nil {
+ return err
+ }
+ _, _, errno := syscall_syscall6(abi.FuncPCABI0(libc_symlinkat_trampoline),
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)),
+ 0,
+ 0,
+ 0)
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
//go:cgo_import_dynamic libc_linkat linkat "libc.so"
//go:cgo_import_dynamic libc_openat openat "libc.so"
//go:cgo_import_dynamic libc_renameat renameat "libc.so"
+//go:cgo_import_dynamic libc_symlinkat symlinkat "libc.so"
//go:cgo_import_dynamic libc_unlinkat unlinkat "libc.so"
//go:cgo_import_dynamic libc_readlinkat readlinkat "libc.so"
//go:cgo_import_dynamic libc_mkdirat mkdirat "libc.so"
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
linkatTrap uintptr = syscall.SYS_LINKAT
+ symlinkatTrap uintptr = syscall.SYS_SYMLINKAT
AT_EACCESS = 0x4
AT_FDCWD = 0xfffafdcd
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
linkatTrap uintptr = syscall.SYS_LINKAT
+ symlinkatTrap uintptr = syscall.SYS_SYMLINKAT
)
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
linkatTrap uintptr = syscall.SYS_LINKAT
+ symlinkatTrap uintptr = syscall.SYS_SYMLINKAT
)
const (
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
linkatTrap uintptr = syscall.SYS_LINKAT
+ symlinkatTrap uintptr = syscall.SYS_SYMLINKAT
)
const (
))
}
+//go:wasmimport wasi_snapshot_preview1 path_create_directory
+//go:noescape
+func path_create_directory(fd int32, path *byte, pathLen size) syscall.Errno
+
func Fchmodat(dirfd int, path string, mode uint32, flags int) error {
// WASI preview 1 doesn't support changing file modes.
return syscall.ENOSYS
return syscall.ENOSYS
}
-//go:wasmimport wasi_snapshot_preview1 path_rename
-//go:noescape
-func path_rename(oldFd int32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno
-
func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error {
if oldpath == "" || newpath == "" {
return syscall.EINVAL
))
}
-//go:wasmimport wasi_snapshot_preview1 path_link
+//go:wasmimport wasi_snapshot_preview1 path_rename
//go:noescape
-func path_link(oldFd int32, oldFlags uint32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno
+func path_rename(oldFd int32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno
func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) error {
if oldpath == "" || newpath == "" {
))
}
-//go:wasmimport wasi_snapshot_preview1 path_create_directory
+//go:wasmimport wasi_snapshot_preview1 path_link
//go:noescape
-func path_create_directory(fd int32, path *byte, pathLen size) syscall.Errno
+func path_link(oldFd int32, oldFlags uint32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno
+
+func Symlinkat(oldpath string, newdirfd int, newpath string) error {
+ if oldpath == "" || newpath == "" {
+ return syscall.EINVAL
+ }
+ return errnoErr(path_symlink(
+ unsafe.StringData(oldpath),
+ size(len(oldpath)),
+ int32(newdirfd),
+ unsafe.StringData(newpath),
+ size(len(newpath)),
+ ))
+}
+
+//go:wasmimport wasi_snapshot_preview1 path_symlink
+//go:noescape
+func path_symlink(oldPath *byte, oldPathLen size, fd int32, newPath *byte, newPathLen size) syscall.Errno
func errnoErr(errno syscall.Errno) error {
if errno == 0 {
package windows
import (
+ "runtime"
+ "structs"
"syscall"
"unsafe"
)
}
return err
}
+
+// SymlinkatFlags configure Symlinkat.
+//
+// Symbolic links have two properties: They may be directory or file links,
+// and they may be absolute or relative.
+//
+// The Windows API defines flags describing these properties
+// (SYMBOLIC_LINK_FLAG_DIRECTORY and SYMLINK_FLAG_RELATIVE),
+// but the flags are passed to different system calls and
+// do not have distinct values, so we define our own enumeration
+// that permits expressing both.
+type SymlinkatFlags uint
+
+const (
+ SYMLINKAT_DIRECTORY = SymlinkatFlags(1 << iota)
+ SYMLINKAT_RELATIVE
+)
+
+func Symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error {
+ // Temporarily acquire symlink-creating privileges if possible.
+ // This is the behavior of CreateSymbolicLinkW.
+ //
+ // (When passed the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag,
+ // CreateSymbolicLinkW ignores errors in acquiring privileges, as we do here.)
+ return withPrivilege("SeCreateSymbolicLinkPrivilege", func() error {
+ return symlinkat(oldname, newdirfd, newname, flags)
+ })
+}
+
+func symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error {
+ oldnameu16, err := syscall.UTF16FromString(oldname)
+ if err != nil {
+ return err
+ }
+ oldnameu16 = oldnameu16[:len(oldnameu16)-1] // trim off terminal NUL
+
+ var options uint32
+ if flags&SYMLINKAT_DIRECTORY != 0 {
+ options |= FILE_DIRECTORY_FILE
+ } else {
+ options |= FILE_NON_DIRECTORY_FILE
+ }
+
+ objAttrs := &OBJECT_ATTRIBUTES{}
+ if err := objAttrs.init(newdirfd, newname); err != nil {
+ return err
+ }
+ var h syscall.Handle
+ err = NtCreateFile(
+ &h,
+ SYNCHRONIZE|FILE_WRITE_ATTRIBUTES|DELETE,
+ objAttrs,
+ &IO_STATUS_BLOCK{},
+ nil,
+ syscall.FILE_ATTRIBUTE_NORMAL,
+ 0,
+ FILE_CREATE,
+ FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|options,
+ 0,
+ 0,
+ )
+ if err != nil {
+ return ntCreateFileError(err, 0)
+ }
+ defer syscall.CloseHandle(h)
+
+ // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_reparse_data_buffer
+ type reparseDataBufferT struct {
+ _ structs.HostLayout
+
+ ReparseTag uint32
+ ReparseDataLength uint16
+ Reserved uint16
+
+ SubstituteNameOffset uint16
+ SubstituteNameLength uint16
+ PrintNameOffset uint16
+ PrintNameLength uint16
+ Flags uint32
+ }
+
+ const (
+ headerSize = uint16(unsafe.Offsetof(reparseDataBufferT{}.SubstituteNameOffset))
+ bufferSize = uint16(unsafe.Sizeof(reparseDataBufferT{}))
+ )
+
+ // Data buffer containing a SymbolicLinkReparseBuffer followed by the link target.
+ rdbbuf := make([]byte, bufferSize+uint16(2*len(oldnameu16)))
+
+ rdb := (*reparseDataBufferT)(unsafe.Pointer(&rdbbuf[0]))
+ rdb.ReparseTag = syscall.IO_REPARSE_TAG_SYMLINK
+ rdb.ReparseDataLength = uint16(len(rdbbuf)) - uint16(headerSize)
+ rdb.SubstituteNameOffset = 0
+ rdb.SubstituteNameLength = uint16(2 * len(oldnameu16))
+ rdb.PrintNameOffset = 0
+ rdb.PrintNameLength = rdb.SubstituteNameLength
+ if flags&SYMLINKAT_RELATIVE != 0 {
+ rdb.Flags = SYMLINK_FLAG_RELATIVE
+ }
+
+ namebuf := rdbbuf[bufferSize:]
+ copy(namebuf, unsafe.String((*byte)(unsafe.Pointer(&oldnameu16[0])), 2*len(oldnameu16)))
+
+ err = syscall.DeviceIoControl(
+ h,
+ FSCTL_SET_REPARSE_POINT,
+ &rdbbuf[0],
+ uint32(len(rdbbuf)),
+ nil,
+ 0,
+ nil,
+ nil)
+ if err != nil {
+ // Creating the symlink has failed, so try to remove the file.
+ const FileDispositionInformation = 13
+ NtSetInformationFile(
+ h,
+ &IO_STATUS_BLOCK{},
+ uintptr(unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
+ DeleteFile: true,
+ })),
+ uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
+ FileDispositionInformation,
+ )
+ return err
+ }
+
+ return nil
+}
+
+// withPrivilege temporariliy acquires the named privilege and runs f.
+// If the privilege cannot be acquired it runs f anyway,
+// which should fail with an appropriate error.
+func withPrivilege(privilege string, f func() error) error {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+
+ err := ImpersonateSelf(SecurityImpersonation)
+ if err != nil {
+ return f()
+ }
+ defer RevertToSelf()
+
+ curThread, err := GetCurrentThread()
+ if err != nil {
+ return f()
+ }
+ var token syscall.Token
+ err = OpenThreadToken(curThread, syscall.TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES, false, &token)
+ if err != nil {
+ return f()
+ }
+ defer syscall.CloseHandle(syscall.Handle(token))
+
+ privStr, err := syscall.UTF16PtrFromString(privilege)
+ if err != nil {
+ return f()
+ }
+ var tokenPriv TOKEN_PRIVILEGES
+ err = LookupPrivilegeValue(nil, privStr, &tokenPriv.Privileges[0].Luid)
+ if err != nil {
+ return f()
+ }
+
+ tokenPriv.PrivilegeCount = 1
+ tokenPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
+ err = AdjustTokenPrivileges(token, false, &tokenPriv, 0, nil, nil)
+ if err != nil {
+ return f()
+ }
+
+ return f()
+}
}
func TestSymlink(t *testing.T) {
+ testMaybeRooted(t, testSymlink)
+}
+func testSymlink(t *testing.T, root *Root) {
testenv.MustHaveSymlink(t)
- t.Chdir(t.TempDir())
+
+ var (
+ create = Create
+ open = Open
+ symlink = Symlink
+ stat = Stat
+ lstat = Lstat
+ readlink = Readlink
+ )
+ if root != nil {
+ create = root.Create
+ open = root.Open
+ symlink = root.Symlink
+ stat = root.Stat
+ lstat = root.Lstat
+ readlink = root.Readlink
+ }
from, to := "symlinktestfrom", "symlinktestto"
- file, err := Create(to)
+ file, err := create(to)
if err != nil {
t.Fatalf("Create(%q) failed: %v", to, err)
}
if err = file.Close(); err != nil {
t.Errorf("Close(%q) failed: %v", to, err)
}
- err = Symlink(to, from)
+ err = symlink(to, from)
if err != nil {
t.Fatalf("Symlink(%q, %q) failed: %v", to, from, err)
}
- tostat, err := Lstat(to)
+ tostat, err := lstat(to)
if err != nil {
t.Fatalf("Lstat(%q) failed: %v", to, err)
}
if tostat.Mode()&ModeSymlink != 0 {
t.Fatalf("Lstat(%q).Mode()&ModeSymlink = %v, want 0", to, tostat.Mode()&ModeSymlink)
}
- fromstat, err := Stat(from)
+ fromstat, err := stat(from)
if err != nil {
t.Fatalf("Stat(%q) failed: %v", from, err)
}
if !SameFile(tostat, fromstat) {
t.Errorf("Symlink(%q, %q) did not create symlink", to, from)
}
- fromstat, err = Lstat(from)
+ fromstat, err = lstat(from)
if err != nil {
t.Fatalf("Lstat(%q) failed: %v", from, err)
}
if fromstat.Mode()&ModeSymlink == 0 {
t.Fatalf("Lstat(%q).Mode()&ModeSymlink = 0, want %v", from, ModeSymlink)
}
- fromstat, err = Stat(from)
+ fromstat, err = stat(from)
if err != nil {
t.Fatalf("Stat(%q) failed: %v", from, err)
}
if fromstat.Mode()&ModeSymlink != 0 {
t.Fatalf("Stat(%q).Mode()&ModeSymlink = %v, want 0", from, fromstat.Mode()&ModeSymlink)
}
- s, err := Readlink(from)
+ s, err := readlink(from)
if err != nil {
t.Fatalf("Readlink(%q) failed: %v", from, err)
}
if s != to {
t.Fatalf("Readlink(%q) = %q, want %q", from, s, to)
}
- file, err = Open(from)
+ file, err = open(from)
if err != nil {
t.Fatalf("Open(%q) failed: %v", from, err)
}
return rootLink(r, oldname, newname)
}
+// Symlink creates newname as a symbolic link to oldname.
+// See [Symlink] for more details.
+//
+// Symlink does not validate oldname,
+// which may reference a location outside the root.
+//
+// On Windows, a directory link is created if oldname references
+// a directory within the root. Otherwise a file link is created.
+func (r *Root) Symlink(oldname, newname string) error {
+ return rootSymlink(r, oldname, newname)
+}
+
func (r *Root) logOpen(name string) {
if log := testlog.Logger(); log != nil {
// This won't be right if r's name has changed since it was opened,
}
return nil
}
+
+func rootSymlink(r *Root, oldname, newname string) error {
+ if err := checkPathEscapesLstat(r, newname); err != nil {
+ return &PathError{Op: "symlinkat", Path: newname, Err: err}
+ }
+ err := Symlink(oldname, joinPath(r.root.name, newname))
+ if err != nil {
+ return &LinkError{"symlinkat", oldname, newname, underlyingError(err)}
+ }
+ return nil
+}
}
}
+func TestRootSymlink(t *testing.T) {
+ testenv.MustHaveSymlink(t)
+ for _, test := range rootTestCases {
+ test.run(t, func(t *testing.T, target string, root *os.Root) {
+ wantError := test.wantError
+ if test.ltarget != "" {
+ // We can't create a symlink over an existing symlink.
+ wantError = true
+ }
+
+ const wantTarget = "linktarget"
+ err := root.Symlink(wantTarget, test.open)
+ if errEndsTest(t, err, wantError, "root.Symlink(%q)", test.open) {
+ return
+ }
+ got, err := os.Readlink(target)
+ if err != nil || got != wantTarget {
+ t.Fatalf("ReadLink(%q) = %q, %v; want %q, nil", target, got, err, wantTarget)
+ }
+ })
+ }
+}
+
// A rootConsistencyTest is a test case comparing os.Root behavior with
// the corresponding non-Root function.
//
}
}
+func TestRootConsistencySymlink(t *testing.T) {
+ testenv.MustHaveSymlink(t)
+ for _, test := range rootConsistencyTestCases {
+ test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
+ const target = "linktarget"
+ var err error
+ var got string
+ if r == nil {
+ err = os.Symlink(target, path)
+ got, _ = os.Readlink(target)
+ } else {
+ err = r.Symlink(target, path)
+ got, _ = r.Readlink(target)
+ }
+ return got, err
+ })
+ }
+}
+
func TestRootRenameAfterOpen(t *testing.T) {
switch runtime.GOOS {
case "windows":
return fi, nil
}
+func rootSymlink(r *Root, oldname, newname string) error {
+ _, err := doInRoot(r, newname, func(parent sysfdType, name string) (struct{}, error) {
+ return struct{}{}, symlinkat(oldname, parent, name)
+ })
+ if err != nil {
+ return &LinkError{"symlinkat", oldname, newname, err}
+ }
+ return nil
+}
+
// On systems which use fchmodat, fchownat, etc., we have a race condition:
// When "name" is a symlink, Root.Chmod("name") should act on the target of that link.
// However, fchmodat doesn't allow us to chmod a file only if it is not a symlink;
return unix.Linkat(oldfd, oldname, newfd, newname, 0)
}
+func symlinkat(oldname string, newfd int, newname string) error {
+ return unix.Symlinkat(oldname, newfd, newname)
+}
+
// checkSymlink resolves the symlink name in parent,
// and returns errSymlink with the link contents.
//
return fi, nil
}
+func rootSymlink(r *Root, oldname, newname string) error {
+ if oldname == "" {
+ return syscall.EINVAL
+ }
+
+ // CreateSymbolicLinkW converts volume-relative paths into absolute ones.
+ // Do the same.
+ if filepathlite.VolumeNameLen(oldname) > 0 && !filepathlite.IsAbs(oldname) {
+ p, err := syscall.FullPath(oldname)
+ if err == nil {
+ oldname = p
+ }
+ }
+
+ // If oldname can be resolved to a directory in the root, create a directory link.
+ // Otherwise, create a file link.
+ var flags windows.SymlinkatFlags
+ if filepathlite.VolumeNameLen(oldname) == 0 && !IsPathSeparator(oldname[0]) {
+ // oldname is a path relative to the directory containing newname.
+ // Prepend newname's directory to it to make a path relative to the root.
+ // For example, if oldname=old and newname=a\new, destPath=a\old.
+ destPath := oldname
+ if dir := dirname(newname); dir != "." {
+ destPath = dir + `\` + oldname
+ }
+ fi, err := r.Stat(destPath)
+ if err == nil && fi.IsDir() {
+ flags |= windows.SYMLINKAT_DIRECTORY
+ }
+ }
+
+ // Empirically, CreateSymbolicLinkW appears to set the relative flag iff
+ // the target does not contain a volume name.
+ if filepathlite.VolumeNameLen(oldname) == 0 {
+ flags |= windows.SYMLINKAT_RELATIVE
+ }
+
+ _, err := doInRoot(r, newname, func(parent sysfdType, name string) (struct{}, error) {
+ return struct{}{}, windows.Symlinkat(oldname, parent, name, flags)
+ })
+ if err != nil {
+ return &LinkError{"symlinkat", oldname, newname, err}
+ }
+ return nil
+}
+
func chmodat(parent syscall.Handle, name string, mode FileMode) error {
// Currently, on Windows os.Chmod("symlink") will act on "symlink",
// not on any file it points to.
import (
"errors"
+ "fmt"
+ "internal/syscall/windows"
"os"
"path/filepath"
+ "syscall"
"testing"
+ "unsafe"
)
// Verify that Root.Open rejects Windows reserved names.
t.Fatalf("os.Stat(file) after deletion: %v, want ErrNotFound", err)
}
}
+
+// TestRootSymlinkRelativity tests that symlinks created using Root.Symlink have the
+// same SYMLINK_FLAG_RELATIVE value as ones creates using os.Symlink.
+func TestRootSymlinkRelativity(t *testing.T) {
+ dir := t.TempDir()
+ root, err := os.OpenRoot(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer root.Close()
+
+ for i, test := range []struct {
+ name string
+ target string
+ }{{
+ name: "relative",
+ target: `foo`,
+ }, {
+ name: "absolute",
+ target: `C:\foo`,
+ }, {
+ name: "current working directory-relative",
+ target: `C:foo`,
+ }, {
+ name: "root-relative",
+ target: `\foo`,
+ }, {
+ name: "question prefix",
+ target: `\\?\foo`,
+ }, {
+ name: "relative with dot dot",
+ target: `a\..\b`, // could be cleaned (but isn't)
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ name := fmt.Sprintf("symlink_%v", i)
+ if err := os.Symlink(test.target, filepath.Join(dir, name)); err != nil {
+ t.Fatal(err)
+ }
+ if err := root.Symlink(test.target, name+"_at"); err != nil {
+ t.Fatal(err)
+ }
+
+ osRDB, err := readSymlinkReparseData(filepath.Join(dir, name))
+ if err != nil {
+ t.Fatal(err)
+ }
+ rootRDB, err := readSymlinkReparseData(filepath.Join(dir, name+"_at"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if osRDB.Flags != rootRDB.Flags {
+ t.Errorf("symlink target %q: Symlink flags = %x, Root.Symlink flags = %x", test.target, osRDB.Flags, rootRDB.Flags)
+ }
+
+ // Compare the link target.
+ // os.Symlink converts current working directory-relative links
+ // such as c:foo into absolute links.
+ osTarget, err := os.Readlink(filepath.Join(dir, name))
+ if err != nil {
+ t.Fatal(err)
+ }
+ rootTarget, err := os.Readlink(filepath.Join(dir, name+"_at"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if osTarget != rootTarget {
+ t.Errorf("symlink created with target %q: Symlink target = %q, Root.Symlink target = %q", test.target, osTarget, rootTarget)
+ }
+ })
+ }
+}
+
+func readSymlinkReparseData(name string) (*windows.SymbolicLinkReparseBuffer, error) {
+ nameu16, err := syscall.UTF16FromString(name)
+ if err != nil {
+ return nil, err
+ }
+ h, err := syscall.CreateFile(&nameu16[0], syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING,
+ syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
+ if err != nil {
+ return nil, err
+ }
+ defer syscall.CloseHandle(h)
+
+ var rdbbuf [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte
+ 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 nil, err
+ }
+
+ rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
+ if rdb.ReparseTag != syscall.IO_REPARSE_TAG_SYMLINK {
+ return nil, fmt.Errorf("%q: not a symlink", name)
+ }
+
+ bufoff := unsafe.Offsetof(rdb.DUMMYUNIONNAME)
+ symlinkBuf := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdbbuf[bufoff]))
+
+ return symlinkBuf, nil
+}
+
+// TestRootSymlinkToDirectory tests that Root.Symlink creates directory links
+// when the target is a directory contained within the root.
+func TestRootSymlinkToDirectory(t *testing.T) {
+ dir := t.TempDir()
+ root, err := os.OpenRoot(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer root.Close()
+
+ if err := os.Mkdir(filepath.Join(dir, "dir"), 0777); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
+ t.Fatal(err)
+ }
+
+ dir2 := t.TempDir()
+
+ for i, test := range []struct {
+ name string
+ target string
+ wantDir bool
+ }{{
+ name: "directory outside root",
+ target: dir2,
+ wantDir: false,
+ }, {
+ name: "directory inside root",
+ target: "dir",
+ wantDir: true,
+ }, {
+ name: "file inside root",
+ target: "file",
+ wantDir: false,
+ }, {
+ name: "nonexistent inside root",
+ target: "nonexistent",
+ wantDir: false,
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ name := fmt.Sprintf("symlink_%v", i)
+ if err := root.Symlink(test.target, name); err != nil {
+ t.Fatal(err)
+ }
+
+ // Lstat strips the directory mode bit from reparse points,
+ // so we need to use GetFileInformationByHandle directly to
+ // determine if this is a directory link.
+ nameu16, err := syscall.UTF16PtrFromString(filepath.Join(dir, name))
+ if err != nil {
+ t.Fatal(err)
+ }
+ h, err := syscall.CreateFile(nameu16, 0, 0, nil, syscall.OPEN_EXISTING,
+ syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer syscall.CloseHandle(h)
+ var fi syscall.ByHandleFileInformation
+ if err := syscall.GetFileInformationByHandle(h, &fi); err != nil {
+ t.Fatal(err)
+ }
+ gotDir := fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0
+
+ if got, want := gotDir, test.wantDir; got != want {
+ t.Errorf("link target %q: isDir = %v, want %v", test.target, got, want)
+ }
+ })
+ }
+}