pkg os, method (*Root) Chown(string, int, int) error #67002
pkg os, method (*Root) Chtimes(string, time.Time, time.Time) error #67002
pkg os, method (*Root) Lchown(string, int, int) error #67002
+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
* [os.Root.Chown]
* [os.Root.Chtimes]
* [os.Root.Lchown]
+ * [os.Root.Link]
* [os.Root.Readlink]
* [os.Root.Rename]
TEXT ·libc_fchmodat_trampoline(SB),NOSPLIT,$0-0; JMP libc_fchmodat(SB)
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)
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)
}
return nil
}
+
+func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) error {
+ oldp, err := syscall.BytePtrFromString(oldpath)
+ if err != nil {
+ return err
+ }
+ newp, err := syscall.BytePtrFromString(newpath)
+ if err != nil {
+ return err
+ }
+ _, _, errno := syscall.Syscall6(linkatTrap,
+ uintptr(olddirfd),
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)),
+ uintptr(flag),
+ 0)
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
//go:cgo_import_dynamic libc_fchmodat fchmodat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_fchownat fchownat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_fstatat fstatat "libc.a/shr_64.o"
+//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_unlinkat unlinkat "libc.a/shr_64.o"
}
return nil
}
+
+func libc_linkat_trampoline()
+
+//go:cgo_import_dynamic libc_linkat linkat "/usr/lib/libSystem.B.dylib"
+
+func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) 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_linkat_trampoline),
+ uintptr(olddirfd),
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)),
+ uintptr(flag),
+ 0)
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
//go:linkname procFchmodat libc_fchmodat
//go:linkname procFchownat libc_fchownat
//go:linkname procRenameat libc_renameat
+//go:linkname procLinkat libc_linkat
var (
procFstatat,
procMkdirat,
procFchmodat,
procFchownat,
- procRenameat uintptr
+ procRenameat,
+ procLinkat uintptr
)
func Unlinkat(dirfd int, path string, flags int) error {
}
return nil
}
+
+func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) 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(&procLinkat)), 5,
+ uintptr(olddirfd),
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)),
+ uintptr(flag),
+ 0)
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
}
return nil
}
+
+func libc_linkat_trampoline()
+
+//go:cgo_import_dynamic libc_linkat linkat "libc.so"
+
+func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) 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_linkat_trampoline),
+ uintptr(olddirfd),
+ uintptr(unsafe.Pointer(oldp)),
+ uintptr(newdirfd),
+ uintptr(unsafe.Pointer(newp)),
+ uintptr(flag),
+ 0)
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
//go:cgo_import_dynamic libc_fchmodat fchmodat "libc.so"
//go:cgo_import_dynamic libc_fchownat fchownat "libc.so"
//go:cgo_import_dynamic libc_fstatat fstatat "libc.so"
+//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_unlinkat unlinkat "libc.so"
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
+ linkatTrap uintptr = syscall.SYS_LINKAT
AT_EACCESS = 0x4
AT_FDCWD = 0xfffafdcd
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
+ linkatTrap uintptr = syscall.SYS_LINKAT
)
mkdiratTrap uintptr = syscall.SYS_MKDIRAT
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
+ linkatTrap uintptr = syscall.SYS_LINKAT
)
const (
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
+ linkatTrap uintptr = syscall.SYS_LINKAT
)
const (
))
}
+//go:wasmimport wasi_snapshot_preview1 path_link
+//go:noescape
+func path_link(oldFd int32, oldFlags uint32, 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 == "" {
+ return syscall.EINVAL
+ }
+ return errnoErr(path_link(
+ int32(olddirfd),
+ 0,
+ unsafe.StringData(oldpath),
+ size(len(oldpath)),
+ int32(newdirfd),
+ unsafe.StringData(newpath),
+ size(len(newpath)),
+ ))
+}
+
//go:wasmimport wasi_snapshot_preview1 path_create_directory
//go:noescape
func path_create_directory(fd int32, path *byte, pathLen size) syscall.Errno
}
return err
}
+
+func Linkat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
+ objAttrs := &OBJECT_ATTRIBUTES{}
+ if err := objAttrs.init(olddirfd, oldpath); err != nil {
+ return err
+ }
+ var h syscall.Handle
+ err := NtOpenFile(
+ &h,
+ SYNCHRONIZE|FILE_WRITE_ATTRIBUTES,
+ objAttrs,
+ &IO_STATUS_BLOCK{},
+ FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
+ FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT,
+ )
+ if err != nil {
+ return ntCreateFileError(err, 0)
+ }
+ defer syscall.CloseHandle(h)
+
+ linkInfo := FILE_LINK_INFORMATION{
+ RootDirectory: newdirfd,
+ }
+ p16, err := syscall.UTF16FromString(newpath)
+ if err != nil {
+ return err
+ }
+ if len(p16) > len(linkInfo.FileName) {
+ return syscall.EINVAL
+ }
+ copy(linkInfo.FileName[:], p16)
+ linkInfo.FileNameLength = uint32((len(p16) - 1) * 2)
+
+ const (
+ FileLinkInformation = 11
+ )
+ err = NtSetInformationFile(
+ h,
+ &IO_STATUS_BLOCK{},
+ uintptr(unsafe.Pointer(&linkInfo)),
+ uint32(unsafe.Sizeof(FILE_LINK_INFORMATION{})),
+ FileLinkInformation,
+ )
+ if st, ok := err.(NTStatus); ok {
+ return st.Errno()
+ }
+ return err
+}
FileNameLength uint32
FileName [syscall.MAX_PATH]uint16
}
+
+// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_link_information
+type FILE_LINK_INFORMATION struct {
+ ReplaceIfExists bool
+ RootDirectory syscall.Handle
+ FileNameLength uint32
+ FileName [syscall.MAX_PATH]uint16
+}
}
func TestHardLink(t *testing.T) {
+ testMaybeRooted(t, testHardLink)
+}
+func testHardLink(t *testing.T, root *Root) {
testenv.MustHaveLink(t)
- t.Chdir(t.TempDir())
+
+ var (
+ create = Create
+ link = Link
+ stat = Stat
+ op = "link"
+ )
+ if root != nil {
+ create = root.Create
+ link = root.Link
+ stat = root.Stat
+ op = "linkat"
+ }
from, to := "hardlinktestfrom", "hardlinktestto"
- file, err := Create(to)
+ file, err := create(to)
if err != nil {
t.Fatalf("open %q failed: %v", to, err)
}
if err = file.Close(); err != nil {
t.Errorf("close %q failed: %v", to, err)
}
- err = Link(to, from)
+ err = link(to, from)
if err != nil {
t.Fatalf("link %q, %q failed: %v", to, from, err)
}
none := "hardlinktestnone"
- err = Link(none, none)
+ err = link(none, none)
// Check the returned error is well-formed.
if lerr, ok := err.(*LinkError); !ok || lerr.Error() == "" {
t.Errorf("link %q, %q failed to return a valid error", none, none)
}
- tostat, err := Stat(to)
+ tostat, err := stat(to)
if err != nil {
t.Fatalf("stat %q failed: %v", to, err)
}
- fromstat, err := Stat(from)
+ fromstat, err := stat(from)
if err != nil {
t.Fatalf("stat %q failed: %v", from, err)
}
t.Errorf("link %q, %q did not create hard link", to, from)
}
// We should not be able to perform the same Link() a second time
- err = Link(to, from)
+ err = link(to, from)
switch err := err.(type) {
case *LinkError:
- if err.Op != "link" {
- t.Errorf("Link(%q, %q) err.Op = %q; want %q", to, from, err.Op, "link")
+ if err.Op != op {
+ t.Errorf("Link(%q, %q) err.Op = %q; want %q", to, from, err.Op, op)
}
if err.Old != to {
t.Errorf("Link(%q, %q) err.Old = %q; want %q", to, from, err.Old, to)
return rootRename(r, oldname, newname)
}
+// Link creates newname as a hard link to the oldname file.
+// Both paths are relative to the root.
+// See [Link] for more details.
+//
+// If oldname is a symbolic link, Link creates new link to oldname and not its target.
+// This behavior may differ from that of [Link] on some platforms.
+//
+// When GOOS=js, Link returns an error if oldname is a symbolic link.
+func (r *Root) Link(oldname, newname string) error {
+ return rootLink(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 rootLink(r *Root, oldname, newname string) error {
+ if err := checkPathEscapesLstat(r, oldname); err != nil {
+ return &PathError{Op: "linkat", Path: oldname, Err: err}
+ }
+ fullOldName := joinPath(r.root.name, oldname)
+ if fs, err := Lstat(fullOldName); err == nil && fs.Mode()&ModeSymlink != 0 {
+ return &PathError{Op: "linkat", Path: oldname, Err: errors.New("cannot create a hard link to a symlink")}
+ }
+ if err := checkPathEscapesLstat(r, newname); err != nil {
+ return &PathError{Op: "linkat", Path: newname, Err: err}
+ }
+ err := Link(fullOldName, joinPath(r.root.name, newname))
+ if err != nil {
+ return &LinkError{"linkat", oldname, newname, underlyingError(err)}
+ }
+ return nil
+}
return err
}
+func rootLink(r *Root, oldname, newname string) error {
+ _, err := doInRoot(r, oldname, func(oldparent sysfdType, oldname string) (struct{}, error) {
+ _, err := doInRoot(r, newname, func(newparent sysfdType, newname string) (struct{}, error) {
+ return struct{}{}, linkat(oldparent, oldname, newparent, newname)
+ })
+ return struct{}{}, err
+ })
+ if err != nil {
+ return &LinkError{"linkat", oldname, newname, err}
+ }
+ return err
+}
+
// doInRoot performs an operation on a path in a Root.
//
// It opens the directory containing the final element of the path,
"bytes"
"errors"
"fmt"
+ "internal/testenv"
"io"
"io/fs"
"net"
// TestRootRenameFrom tests renaming the test case target to a known-good path.
func TestRootRenameFrom(t *testing.T) {
+ testRootMoveFrom(t, true)
+}
+
+// TestRootRenameFrom tests linking the test case target to a known-good path.
+func TestRootLinkFrom(t *testing.T) {
+ testenv.MustHaveLink(t)
+ testRootMoveFrom(t, false)
+}
+
+func testRootMoveFrom(t *testing.T, rename bool) {
want := []byte("target")
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if err != nil {
t.Fatalf("root.Readlink(%q) = %v, want success", test.ltarget, err)
}
+
+ // When GOOS=js, creating a hard link to a symlink fails.
+ if !rename && runtime.GOOS == "js" {
+ wantError = true
+ }
}
const dstPath = "destination"
wantError = true
}
- err := root.Rename(test.open, dstPath)
- if errEndsTest(t, err, wantError, "root.Rename(%q, %q)", test.open, dstPath) {
+ var op string
+ var err error
+ if rename {
+ op = "Rename"
+ err = root.Rename(test.open, dstPath)
+ } else {
+ op = "Link"
+ err = root.Link(test.open, dstPath)
+ }
+ if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, test.open, dstPath) {
return
}
+ origPath := target
if test.ltarget != "" {
- got, err := os.Readlink(filepath.Join(root.Name(), dstPath))
+ origPath = filepath.Join(root.Name(), test.ltarget)
+ }
+ _, err = os.Lstat(origPath)
+ if rename {
+ if !errors.Is(err, os.ErrNotExist) {
+ t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", origPath, err)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("after linking file, error accessing original: %v", err)
+ }
+ }
+
+ dstFullPath := filepath.Join(root.Name(), dstPath)
+ if test.ltarget != "" {
+ got, err := os.Readlink(dstFullPath)
if err != nil || got != linkTarget {
- t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstPath, got, err, linkTarget)
+ t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstFullPath, got, err, linkTarget)
}
} else {
- got, err := os.ReadFile(filepath.Join(root.Name(), dstPath))
+ got, err := os.ReadFile(dstFullPath)
if err != nil || !bytes.Equal(got, want) {
- t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstPath, string(got), err, string(want))
+ t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstFullPath, string(got), err, string(want))
}
+ st, err := os.Lstat(dstFullPath)
+ if err != nil || st.Mode()&fs.ModeSymlink != 0 {
+ t.Errorf(`os.Lstat(%q) = %v, %v; want non-symlink`, dstFullPath, st.Mode(), err)
+ }
+
}
})
}
// TestRootRenameTo tests renaming a known-good path to the test case target.
func TestRootRenameTo(t *testing.T) {
+ testRootMoveTo(t, true)
+}
+
+// TestRootLinkTo tests renaming a known-good path to the test case target.
+func TestRootLinkTo(t *testing.T) {
+ testenv.MustHaveLink(t)
+ testRootMoveTo(t, true)
+}
+
+func testRootMoveTo(t *testing.T, rename bool) {
want := []byte("target")
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
wantError = true
}
- err := root.Rename(srcPath, test.open)
- if errEndsTest(t, err, wantError, "root.Rename(%q, %q)", srcPath, test.open) {
+ var err error
+ var op string
+ if rename {
+ op = "Rename"
+ err = root.Rename(srcPath, test.open)
+ } else {
+ op = "Link"
+ err = root.Link(srcPath, test.open)
+ }
+ if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, srcPath, test.open) {
return
}
+ _, err = os.Lstat(filepath.Join(root.Name(), srcPath))
+ if rename {
+ if !errors.Is(err, os.ErrNotExist) {
+ t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", srcPath, err)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("after linking file, error accessing original: %v", err)
+ }
+ }
+
got, err := os.ReadFile(filepath.Join(root.Name(), target))
if err != nil || !bytes.Equal(got, want) {
t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, target, string(got), err, string(want))
}
func TestRootConsistencyRename(t *testing.T) {
+ testRootConsistencyMove(t, true)
+}
+
+func TestRootConsistencyLink(t *testing.T) {
+ testenv.MustHaveLink(t)
+ testRootConsistencyMove(t, false)
+}
+
+func testRootConsistencyMove(t *testing.T, rename bool) {
if runtime.GOOS == "plan9" {
// This test depends on moving files between directories.
t.Skip("Plan 9 does not support cross-directory renames")
}
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
- rename := os.Rename
+ var move func(oldname, newname string) error
+ switch {
+ case rename && r == nil:
+ move = os.Rename
+ case rename && r != nil:
+ move = r.Rename
+ case !rename && r == nil:
+ move = os.Link
+ case !rename && r != nil:
+ move = r.Link
+ }
lstat := os.Lstat
if r != nil {
- rename = r.Rename
lstat = r.Lstat
}
dstPath = path
}
- if err := rename(srcPath, dstPath); err != nil {
+ if !rename {
+ // When the source is a symlink, Root.Link creates
+ // a hard link to the symlink.
+ // os.Link does whatever the link syscall does,
+ // which varies between operating systems and
+ // their versions.
+ // Skip running the consistency test when
+ // the source is a symlink.
+ fi, err := lstat(srcPath)
+ if err == nil && fi.Mode()&os.ModeSymlink != 0 {
+ return "", nil
+ }
+ }
+
+ if err := move(srcPath, dstPath); err != nil {
return "", err
}
fi, err := lstat(dstPath)
return unix.Renameat(oldfd, oldname, newfd, newname)
}
+func linkat(oldfd int, oldname string, newfd int, newname string) error {
+ return unix.Linkat(oldfd, oldname, newfd, newname, 0)
+}
+
// checkSymlink resolves the symlink name in parent,
// and returns errSymlink with the link contents.
//
return windows.Renameat(oldfd, oldname, newfd, newname)
}
+func linkat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newname string) error {
+ return windows.Linkat(oldfd, oldname, newfd, newname)
+}
+
func readlinkat(dirfd syscall.Handle, name string) (string, error) {
fd, err := openat(dirfd, name, windows.O_OPEN_REPARSE, 0)
if err != nil {