os: add Root.RemoveAll

For #67002

Change-Id: If59dab4fd934a115d8ff383826525330de750b54
Reviewed-on: https://go-review.googlesource.com/c/go/+/661595
Reviewed-by: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
This commit is contained in:
Damien Neil 2025-03-28 16:40:34 -07:00 committed by Gopher Robot
parent 26e05b95c2
commit 8960970009
9 changed files with 151 additions and 5 deletions

View File

@ -4,5 +4,6 @@ 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) RemoveAll(string) error #67002
pkg os, method (*Root) Rename(string, string) error #67002
pkg os, method (*Root) Symlink(string, string) error #67002

View File

@ -6,5 +6,6 @@ The [os.Root] type supports the following additional methods:
* [os.Root.Lchown]
* [os.Root.Link]
* [os.Root.Readlink]
* [os.Root.RemoveAll]
* [os.Root.Rename]
* [os.Root.Symlink]

View File

@ -44,7 +44,7 @@ func removeAll(path string) error {
}
defer parent.Close()
if err := removeAllFrom(parent, base); err != nil {
if err := removeAllFrom(sysfdType(parent.Fd()), base); err != nil {
if pathErr, ok := err.(*PathError); ok {
pathErr.Path = parentDir + string(PathSeparator) + pathErr.Path
err = pathErr
@ -54,9 +54,7 @@ func removeAll(path string) error {
return nil
}
func removeAllFrom(parent *File, base string) error {
parentFd := sysfdType(parent.Fd())
func removeAllFrom(parentFd sysfdType, base string) error {
// Simple case: if Unlink (aka remove) works, we're done.
err := removefileat(parentFd, base)
if err == nil || IsNotExist(err) {
@ -109,7 +107,7 @@ func removeAllFrom(parent *File, base string) error {
respSize = len(names)
for _, name := range names {
err := removeAllFrom(file, name)
err := removeAllFrom(sysfdType(file.Fd()), name)
if err != nil {
if pathErr, ok := err.(*PathError); ok {
pathErr.Path = base + string(PathSeparator) + pathErr.Path

View File

@ -177,6 +177,12 @@ func (r *Root) Remove(name string) error {
return rootRemove(r, name)
}
// RemoveAll removes the named file or directory and any children that it contains.
// See [RemoveAll] for more details.
func (r *Root) RemoveAll(name string) error {
return rootRemoveAll(r, name)
}
// Stat returns a [FileInfo] describing the named file in the root.
// See [Stat] for more details.
func (r *Root) Stat(name string) (FileInfo, error) {

View File

@ -9,6 +9,7 @@ package os
import (
"errors"
"sync/atomic"
"syscall"
"time"
)
@ -156,6 +157,25 @@ func rootRemove(r *Root, name string) error {
return nil
}
func rootRemoveAll(r *Root, name string) error {
if endsWithDot(name) {
// Consistency with os.RemoveAll: Return EINVAL when trying to remove .
return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL}
}
if err := checkPathEscapesLstat(r, name); err != nil {
if err == syscall.ENOTDIR {
// Some intermediate path component is not a directory.
// RemoveAll treats this as success (since the target doesn't exist).
return nil
}
return &PathError{Op: "RemoveAll", Path: name, Err: err}
}
if err := RemoveAll(joinPath(r.root.name, name)); err != nil {
return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)}
}
return nil
}
func rootReadlink(r *Root, name string) (string, error) {
if err := checkPathEscapesLstat(r, name); err != nil {
return "", &PathError{Op: "readlinkat", Path: name, Err: err}

View File

@ -138,6 +138,28 @@ func rootRemove(r *Root, name string) error {
return nil
}
func rootRemoveAll(r *Root, name string) error {
// Consistency with os.RemoveAll: Strip trailing /s from the name,
// so RemoveAll("not_a_directory/") succeeds.
for len(name) > 0 && IsPathSeparator(name[len(name)-1]) {
name = name[:len(name)-1]
}
if endsWithDot(name) {
// Consistency with os.RemoveAll: Return EINVAL when trying to remove .
return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL}
}
_, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
return struct{}{}, removeAllFrom(parent, name)
})
if IsNotExist(err) {
return nil
}
if err != nil {
return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)}
}
return err
}
func rootRename(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) {

View File

@ -630,6 +630,46 @@ func TestRootRemoveDirectory(t *testing.T) {
}
}
func TestRootRemoveAll(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
wantError := test.wantError
if test.ltarget != "" {
// Remove doesn't follow symlinks in the final path component,
// so it will successfully remove ltarget.
wantError = false
target = filepath.Join(root.Name(), test.ltarget)
} else if target != "" {
if err := os.Mkdir(target, 0o777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(target, "file"), nil, 0o666); err != nil {
t.Fatal(err)
}
}
targetExists := true
if _, err := root.Lstat(test.open); errors.Is(err, os.ErrNotExist) {
// If the target doesn't exist, RemoveAll succeeds rather
// than returning ErrNotExist.
targetExists = false
wantError = false
}
err := root.RemoveAll(test.open)
if errEndsTest(t, err, wantError, "root.RemoveAll(%q)", test.open) {
return
}
if !targetExists {
return
}
_, err = os.Lstat(target)
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
}
})
}
}
func TestRootOpenFileAsRoot(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "target")
@ -958,6 +998,9 @@ type rootConsistencyTest struct {
// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
// function return different errors for this test.
detailedErrorMismatch func(t *testing.T) bool
// check is called before the test starts, and may t.Skip if necessary.
check func(t *testing.T)
}
var rootConsistencyTestCases = []rootConsistencyTest{{
@ -1115,6 +1158,16 @@ var rootConsistencyTestCases = []rootConsistencyTest{{
// and os.Open returns "The file cannot be accessed by the system.".
return runtime.GOOS == "windows"
},
check: func(t *testing.T) {
if runtime.GOOS == "windows" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") {
// Root.RemoveAll notices that a/ is not a directory,
// and returns success.
// os.RemoveAll tries to open a/ and fails because
// it is not a regular file.
// The inconsistency here isn't worth fixing, so just skip this test.
t.Skip("known inconsistency on windows")
}
},
}, {
name: "question mark",
open: "?",
@ -1156,6 +1209,10 @@ func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path stri
}
t.Run(test.name, func(t *testing.T) {
if test.check != nil {
test.check(t)
}
dir1 := makefs(t, test.fs)
dir2 := makefs(t, test.fs)
if test.fsFunc != nil {
@ -1321,6 +1378,23 @@ func TestRootConsistencyRemove(t *testing.T) {
}
}
func TestRootConsistencyRemoveAll(t *testing.T) {
for _, test := range rootConsistencyTestCases {
if test.open == "." || test.open == "./" {
continue // can't remove the root itself
}
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
var err error
if r == nil {
err = os.RemoveAll(path)
} else {
err = r.RemoveAll(path)
}
return "", err
})
}
}
func TestRootConsistencyStat(t *testing.T) {
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
@ -1759,3 +1833,21 @@ func TestOpenInRoot(t *testing.T) {
}
}
}
func TestRootRemoveDot(t *testing.T) {
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
if err := root.Remove("."); err == nil {
t.Errorf(`root.Remove(".") = %v, want error`, err)
}
if err := root.RemoveAll("."); err == nil {
t.Errorf(`root.RemoveAll(".") = %v, want error`, err)
}
if _, err := os.Stat(dir); err != nil {
t.Error(`root.Remove(All)?(".") removed the root`)
}
}

View File

@ -14,6 +14,9 @@ import (
"time"
)
// sysfdType is the native type of a file handle
// (int on Unix, syscall.Handle on Windows),
// permitting helper functions to be written portably.
type sysfdType = int
// openRootNolog is OpenRoot.

View File

@ -77,6 +77,9 @@ func rootCleanPath(s string, prefix, suffix []string) (string, error) {
return s, nil
}
// sysfdType is the native type of a file handle
// (int on Unix, syscall.Handle on Windows),
// permitting helper functions to be written portably.
type sysfdType = syscall.Handle
// openRootNolog is OpenRoot.