diff options
| author | Damien Neil <dneil@google.com> | 2025-03-20 12:41:21 -0700 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2025-03-24 07:53:38 -0700 |
| commit | d2d1fd68b6299d4645298e6d70fe8e8cfd98001a (patch) | |
| tree | 8108bd9125f95c6a66e5c2c30db5614a6c939ff8 /src/os | |
| parent | 4ae6ab2bdfe3ebe8340d0d49fd2bb73f1a3e19ff (diff) | |
| download | go-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.go | 33 | ||||
| -rw-r--r-- | src/os/root.go | 12 | ||||
| -rw-r--r-- | src/os/root_noopenat.go | 18 | ||||
| -rw-r--r-- | src/os/root_openat.go | 13 | ||||
| -rw-r--r-- | src/os/root_test.go | 128 | ||||
| -rw-r--r-- | src/os/root_unix.go | 4 | ||||
| -rw-r--r-- | src/os/root_windows.go | 4 |
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 { |
