aboutsummaryrefslogtreecommitdiff
path: root/src/os
diff options
context:
space:
mode:
authorDamien Neil <dneil@google.com>2025-03-20 12:41:21 -0700
committerGopher Robot <gobot@golang.org>2025-03-24 07:53:38 -0700
commitd2d1fd68b6299d4645298e6d70fe8e8cfd98001a (patch)
tree8108bd9125f95c6a66e5c2c30db5614a6c939ff8 /src/os
parent4ae6ab2bdfe3ebe8340d0d49fd2bb73f1a3e19ff (diff)
downloadgo-d2d1fd68b6299d4645298e6d70fe8e8cfd98001a.tar.xz
os: add Root.Link
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>
Diffstat (limited to 'src/os')
-rw-r--r--src/os/os_test.go33
-rw-r--r--src/os/root.go12
-rw-r--r--src/os/root_noopenat.go18
-rw-r--r--src/os/root_openat.go13
-rw-r--r--src/os/root_test.go128
-rw-r--r--src/os/root_unix.go4
-rw-r--r--src/os/root_windows.go4
7 files changed, 192 insertions, 20 deletions
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 {