From d2d1fd68b6299d4645298e6d70fe8e8cfd98001a Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 20 Mar 2025 12:41:21 -0700 Subject: [PATCH] os: add Root.Link For #67002 Change-Id: I223f3f2dbc8b02726f4ce5a017c628c4a20f109a Reviewed-on: https://go-review.googlesource.com/c/go/+/659757 Reviewed-by: Quim Muntal LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil Reviewed-by: Ian Lance Taylor --- api/next/67002.txt | 1 + doc/next/6-stdlib/99-minor/os/67002.md | 1 + src/internal/syscall/unix/asm_darwin.s | 1 + src/internal/syscall/unix/asm_openbsd.s | 2 + src/internal/syscall/unix/at.go | 22 +++ src/internal/syscall/unix/at_aix.go | 1 + src/internal/syscall/unix/at_darwin.go | 26 ++++ src/internal/syscall/unix/at_libc.go | 26 +++- src/internal/syscall/unix/at_openbsd.go | 26 ++++ src/internal/syscall/unix/at_solaris.go | 1 + .../syscall/unix/at_sysnum_dragonfly.go | 1 + .../syscall/unix/at_sysnum_freebsd.go | 1 + src/internal/syscall/unix/at_sysnum_linux.go | 1 + src/internal/syscall/unix/at_sysnum_netbsd.go | 1 + src/internal/syscall/unix/at_wasip1.go | 19 +++ src/internal/syscall/windows/at_windows.go | 48 +++++++ src/internal/syscall/windows/types_windows.go | 8 ++ src/os/os_test.go | 33 +++-- src/os/root.go | 12 ++ src/os/root_noopenat.go | 18 +++ src/os/root_openat.go | 13 ++ src/os/root_test.go | 128 ++++++++++++++++-- src/os/root_unix.go | 4 + src/os/root_windows.go | 4 + 24 files changed, 377 insertions(+), 21 deletions(-) diff --git a/api/next/67002.txt b/api/next/67002.txt index 0ac3a9e7bf..98c532f1d3 100644 --- a/api/next/67002.txt +++ b/api/next/67002.txt @@ -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 diff --git a/doc/next/6-stdlib/99-minor/os/67002.md b/doc/next/6-stdlib/99-minor/os/67002.md index b87ab6f2b7..ff087748c5 100644 --- a/doc/next/6-stdlib/99-minor/os/67002.md +++ b/doc/next/6-stdlib/99-minor/os/67002.md @@ -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] diff --git a/src/internal/syscall/unix/asm_darwin.s b/src/internal/syscall/unix/asm_darwin.s index a72240f512..79d384c941 100644 --- a/src/internal/syscall/unix/asm_darwin.s +++ b/src/internal/syscall/unix/asm_darwin.s @@ -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) diff --git a/src/internal/syscall/unix/asm_openbsd.s b/src/internal/syscall/unix/asm_openbsd.s index 2b88b6988c..481dd7d700 100644 --- a/src/internal/syscall/unix/asm_openbsd.s +++ b/src/internal/syscall/unix/asm_openbsd.s @@ -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) diff --git a/src/internal/syscall/unix/at.go b/src/internal/syscall/unix/at.go index be7920c115..4549a07f8c 100644 --- a/src/internal/syscall/unix/at.go +++ b/src/internal/syscall/unix/at.go @@ -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 +} diff --git a/src/internal/syscall/unix/at_aix.go b/src/internal/syscall/unix/at_aix.go index d277cd332f..573554927e 100644 --- a/src/internal/syscall/unix/at_aix.go +++ b/src/internal/syscall/unix/at_aix.go @@ -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" diff --git a/src/internal/syscall/unix/at_darwin.go b/src/internal/syscall/unix/at_darwin.go index 4f39b76ad1..61437672ee 100644 --- a/src/internal/syscall/unix/at_darwin.go +++ b/src/internal/syscall/unix/at_darwin.go @@ -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 +} diff --git a/src/internal/syscall/unix/at_libc.go b/src/internal/syscall/unix/at_libc.go index 36a9f22b2a..b32d3bba39 100644 --- a/src/internal/syscall/unix/at_libc.go +++ b/src/internal/syscall/unix/at_libc.go @@ -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 +} diff --git a/src/internal/syscall/unix/at_openbsd.go b/src/internal/syscall/unix/at_openbsd.go index bd56aac70d..2a433930f3 100644 --- a/src/internal/syscall/unix/at_openbsd.go +++ b/src/internal/syscall/unix/at_openbsd.go @@ -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 +} diff --git a/src/internal/syscall/unix/at_solaris.go b/src/internal/syscall/unix/at_solaris.go index 1241827cff..abfda15688 100644 --- a/src/internal/syscall/unix/at_solaris.go +++ b/src/internal/syscall/unix/at_solaris.go @@ -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" diff --git a/src/internal/syscall/unix/at_sysnum_dragonfly.go b/src/internal/syscall/unix/at_sysnum_dragonfly.go index f4418776cd..3ba2c54152 100644 --- a/src/internal/syscall/unix/at_sysnum_dragonfly.go +++ b/src/internal/syscall/unix/at_sysnum_dragonfly.go @@ -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 diff --git a/src/internal/syscall/unix/at_sysnum_freebsd.go b/src/internal/syscall/unix/at_sysnum_freebsd.go index d1bec15343..032b8b5276 100644 --- a/src/internal/syscall/unix/at_sysnum_freebsd.go +++ b/src/internal/syscall/unix/at_sysnum_freebsd.go @@ -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 ) diff --git a/src/internal/syscall/unix/at_sysnum_linux.go b/src/internal/syscall/unix/at_sysnum_linux.go index 35cc4307e9..6b8bebff2a 100644 --- a/src/internal/syscall/unix/at_sysnum_linux.go +++ b/src/internal/syscall/unix/at_sysnum_linux.go @@ -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 ( diff --git a/src/internal/syscall/unix/at_sysnum_netbsd.go b/src/internal/syscall/unix/at_sysnum_netbsd.go index db42be58b7..01e10ddd59 100644 --- a/src/internal/syscall/unix/at_sysnum_netbsd.go +++ b/src/internal/syscall/unix/at_sysnum_netbsd.go @@ -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 ( diff --git a/src/internal/syscall/unix/at_wasip1.go b/src/internal/syscall/unix/at_wasip1.go index 2bd55ca0e7..72537caf1e 100644 --- a/src/internal/syscall/unix/at_wasip1.go +++ b/src/internal/syscall/unix/at_wasip1.go @@ -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 diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go index 311e143b9b..4b939d46ab 100644 --- a/src/internal/syscall/windows/at_windows.go +++ b/src/internal/syscall/windows/at_windows.go @@ -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 +} diff --git a/src/internal/syscall/windows/types_windows.go b/src/internal/syscall/windows/types_windows.go index 718a4b863a..adc8b00bd8 100644 --- a/src/internal/syscall/windows/types_windows.go +++ b/src/internal/syscall/windows/types_windows.go @@ -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 +} diff --git a/src/os/os_test.go b/src/os/os_test.go index cca1b58fe7..3ab8226e44 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -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) diff --git a/src/os/root.go b/src/os/root.go index 55ccd20478..8c82f94866 100644 --- a/src/os/root.go +++ b/src/os/root.go @@ -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, diff --git a/src/os/root_noopenat.go b/src/os/root_noopenat.go index 4a4aa684af..d53d02394d 100644 --- a/src/os/root_noopenat.go +++ b/src/os/root_noopenat.go @@ -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 +} diff --git a/src/os/root_openat.go b/src/os/root_openat.go index 2cb867459b..6591825648 100644 --- a/src/os/root_openat.go +++ b/src/os/root_openat.go @@ -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, diff --git a/src/os/root_test.go b/src/os/root_test.go index 5ed8fe0146..7db8ce0e58 100644 --- a/src/os/root_test.go +++ b/src/os/root_test.go @@ -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) diff --git a/src/os/root_unix.go b/src/os/root_unix.go index dc22651423..f2a88f546a 100644 --- a/src/os/root_unix.go +++ b/src/os/root_unix.go @@ -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. // diff --git a/src/os/root_windows.go b/src/os/root_windows.go index f4d2f4152b..0c37acb089 100644 --- a/src/os/root_windows.go +++ b/src/os/root_windows.go @@ -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 { -- 2.51.0