]> Cypherpunks repositories - gostls13.git/commitdiff
os: add Root.Link
authorDamien Neil <dneil@google.com>
Thu, 20 Mar 2025 19:41:21 +0000 (12:41 -0700)
committerGopher Robot <gobot@golang.org>
Mon, 24 Mar 2025 14:53:38 +0000 (07:53 -0700)
For #67002

Change-Id: I223f3f2dbc8b02726f4ce5a017c628c4a20f109a
Reviewed-on: https://go-review.googlesource.com/c/go/+/659757
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
24 files changed:
api/next/67002.txt
doc/next/6-stdlib/99-minor/os/67002.md
src/internal/syscall/unix/asm_darwin.s
src/internal/syscall/unix/asm_openbsd.s
src/internal/syscall/unix/at.go
src/internal/syscall/unix/at_aix.go
src/internal/syscall/unix/at_darwin.go
src/internal/syscall/unix/at_libc.go
src/internal/syscall/unix/at_openbsd.go
src/internal/syscall/unix/at_solaris.go
src/internal/syscall/unix/at_sysnum_dragonfly.go
src/internal/syscall/unix/at_sysnum_freebsd.go
src/internal/syscall/unix/at_sysnum_linux.go
src/internal/syscall/unix/at_sysnum_netbsd.go
src/internal/syscall/unix/at_wasip1.go
src/internal/syscall/windows/at_windows.go
src/internal/syscall/windows/types_windows.go
src/os/os_test.go
src/os/root.go
src/os/root_noopenat.go
src/os/root_openat.go
src/os/root_test.go
src/os/root_unix.go
src/os/root_windows.go

index 0ac3a9e7bf1bcecb0dcc11a1d01ee4968beb79c1..98c532f1d3afec75dadbf01706a7d407027e4e46 100644 (file)
@@ -2,5 +2,6 @@ pkg os, method (*Root) Chmod(string, fs.FileMode) error #67002
 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
index b87ab6f2b7e57b3d7c2d1544756f1d2328591b9f..ff087748c55295b68a14a57aadb3f48cc32739c6 100644 (file)
@@ -4,5 +4,6 @@ The [os.Root] type supports the following additional methods:
   * [os.Root.Chown]
   * [os.Root.Chtimes]
   * [os.Root.Lchown]
+  * [os.Root.Link]
   * [os.Root.Readlink]
   * [os.Root.Rename]
index a72240f512ed3cda958e03101fdabbb6da4dffad..79d384c941c1f6906faadca3fabccd3c2b804b46 100644 (file)
@@ -28,3 +28,4 @@ TEXT ·libc_mkdirat_trampoline(SB),NOSPLIT,$0-0; JMP libc_mkdirat(SB)
 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)
index 2b88b6988cdcaf0d1791f1072261a5d6d0a84a9a..481dd7d700cfd03099df11a5996b0ec17dd81106 100644 (file)
@@ -20,3 +20,5 @@ 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)
index be7920c115864eed771ef7eb49b23242675b689e..4549a07f8cdd57c2b09d6e4c35e5e9e50076f59a 100644 (file)
@@ -136,3 +136,25 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) 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 := 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
+}
index d277cd332fbef478070c01690683972d4c9e71ea..573554927e4e5305f192fed95237ce1d4621c384 100644 (file)
@@ -7,6 +7,7 @@ package unix
 //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"
index 4f39b76ad176b1388e6592ddb7c4e8804f5886f0..61437672eecf9db299d5e9cba495b100bf139eb6 100644 (file)
@@ -128,3 +128,29 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
        }
        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
+}
index 36a9f22b2afc6bbb4c53de38cd0ea0747fbe36a5..b32d3bba39121f1a07470e254f6aa6463d14606a 100644 (file)
@@ -19,6 +19,7 @@ import (
 //go:linkname procFchmodat libc_fchmodat
 //go:linkname procFchownat libc_fchownat
 //go:linkname procRenameat libc_renameat
+//go:linkname procLinkat libc_linkat
 
 var (
        procFstatat,
@@ -28,7 +29,8 @@ var (
        procMkdirat,
        procFchmodat,
        procFchownat,
-       procRenameat uintptr
+       procRenameat,
+       procLinkat uintptr
 )
 
 func Unlinkat(dirfd int, path string, flags int) error {
@@ -184,3 +186,25 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) 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
+}
index bd56aac70d183cb56424e6e609dcf39bb6b406c5..2a433930f3351d3009557d6bed9fa48995cdc9fd 100644 (file)
@@ -119,3 +119,29 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
        }
        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
+}
index 1241827cffe826ab50e21469be1c4a9999110a25..abfda15688fac0b7a0325b1579951cbaf7b035f3 100644 (file)
@@ -16,6 +16,7 @@ func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, e
 //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"
index f4418776cdc4b6476e4d2159d52cb403f864b1ef..3ba2c54152a4be14c3470060aa3283e30eb0d2d9 100644 (file)
@@ -15,6 +15,7 @@ const (
        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
index d1bec1534352c72c222480bc200f660e698dc63a..032b8b5276251f1f6d5fe5269c03224eb0ad44f8 100644 (file)
@@ -22,4 +22,5 @@ const (
        fchmodatTrap       uintptr = syscall.SYS_FCHMODAT
        fchownatTrap       uintptr = syscall.SYS_FCHOWNAT
        renameatTrap       uintptr = syscall.SYS_RENAMEAT
+       linkatTrap         uintptr = syscall.SYS_LINKAT
 )
index 35cc4307e92f3dde94c678d05a211a9671054149..6b8bebff2ab955050df90e7effe2e4fcd21a93a1 100644 (file)
@@ -13,6 +13,7 @@ const (
        mkdiratTrap    uintptr = syscall.SYS_MKDIRAT
        fchmodatTrap   uintptr = syscall.SYS_FCHMODAT
        fchownatTrap   uintptr = syscall.SYS_FCHOWNAT
+       linkatTrap     uintptr = syscall.SYS_LINKAT
 )
 
 const (
index db42be58b72c21563e1cfefde06fe544a72253d1..01e10ddd59b9934dd05946857a6fa927fbb7db2d 100644 (file)
@@ -15,6 +15,7 @@ const (
        fchmodatTrap   uintptr = syscall.SYS_FCHMODAT
        fchownatTrap   uintptr = syscall.SYS_FCHOWNAT
        renameatTrap   uintptr = syscall.SYS_RENAMEAT
+       linkatTrap     uintptr = syscall.SYS_LINKAT
 )
 
 const (
index 2bd55ca0e71c1244f3736721dfa7d0c8a588cabd..72537caf1ea0797cd717edafe6401a859da23d81 100644 (file)
@@ -129,6 +129,25 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
        ))
 }
 
+//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
index 311e143b9b72981bdf1d4244243a51954f9efcb2..4b939d46ab4e0300671882619afb8500d0a4c8cf 100644 (file)
@@ -328,3 +328,51 @@ func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle,
        }
        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
+}
index 718a4b863a4d57fbdf992574e2437b9550054a7f..adc8b00bd8993532ba3480d595254c2c4fe92696 100644 (file)
@@ -238,3 +238,11 @@ type FILE_RENAME_INFORMATION_EX struct {
        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
+}
index cca1b58fe7d78e1fbf4e4a1aa660f985f4fa354c..3ab8226e4480c680ca719608bb8e51a86110941a 100644 (file)
@@ -850,34 +850,49 @@ func TestReaddirOfFile(t *testing.T) {
 }
 
 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)
        }
@@ -885,11 +900,11 @@ func TestHardLink(t *testing.T) {
                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)
index 55ccd2047892fc16051f0d367655224d77a7a33e..8c82f9486650f60ab3477bfb3a367b880f67e0cc 100644 (file)
@@ -206,6 +206,18 @@ func (r *Root) Rename(oldname, newname string) error {
        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,
index 4a4aa684afa045ea105a6f96fc172eb374cc5e32..d53d02394db214f46becfdaadb157eda942f694c 100644 (file)
@@ -180,3 +180,21 @@ func rootRename(r *Root, oldname, newname string) error {
        }
        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
+}
index 2cb867459b33f716bd9730b6915008a059018b74..65918256486c2a9d2227a6c272232eb5481eee8e 100644 (file)
@@ -151,6 +151,19 @@ func rootRename(r *Root, oldname, newname string) error {
        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,
index 5ed8fe0146a7557c395cd1b623dff4b731a84081..7db8ce0e5845eff436dc0e46161fb1994821320e 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "errors"
        "fmt"
+       "internal/testenv"
        "io"
        "io/fs"
        "net"
@@ -701,6 +702,16 @@ func TestRootReadlink(t *testing.T) {
 
 // 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) {
@@ -719,6 +730,11 @@ func TestRootRenameFrom(t *testing.T) {
                                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"
@@ -728,21 +744,50 @@ func TestRootRenameFrom(t *testing.T) {
                                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)
+                               }
+
                        }
                })
        }
@@ -750,6 +795,16 @@ func TestRootRenameFrom(t *testing.T) {
 
 // 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) {
@@ -771,11 +826,30 @@ func TestRootRenameTo(t *testing.T) {
                                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))
@@ -1201,6 +1275,15 @@ func TestRootConsistencyReadlink(t *testing.T) {
 }
 
 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")
@@ -1222,10 +1305,19 @@ func TestRootConsistencyRename(t *testing.T) {
                                }
 
                                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
                                        }
 
@@ -1243,7 +1335,21 @@ func TestRootConsistencyRename(t *testing.T) {
                                                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)
index dc22651423863d7d98608aa6d2184b7f1598fc20..f2a88f546aade17f3374db974fa40f81a9b2bff5 100644 (file)
@@ -213,6 +213,10 @@ func renameat(oldfd int, oldname string, newfd int, newname string) error {
        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.
 //
index f4d2f4152be14820586fc82a2714fd70165ea789..0c37acb0895a2e8cac15c374188ae1969c635076 100644 (file)
@@ -319,6 +319,10 @@ func renameat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newnam
        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 {