Skip to content

Commit 8960970

Browse files
neildgopherbot
authored andcommitted
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>
1 parent 26e05b9 commit 8960970

File tree

9 files changed

+151
-5
lines changed

9 files changed

+151
-5
lines changed

api/next/67002.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ pkg os, method (*Root) Chtimes(string, time.Time, time.Time) error #67002
44
pkg os, method (*Root) Lchown(string, int, int) error #67002
55
pkg os, method (*Root) Link(string, string) error #67002
66
pkg os, method (*Root) Readlink(string) (string, error) #67002
7+
pkg os, method (*Root) RemoveAll(string) error #67002
78
pkg os, method (*Root) Rename(string, string) error #67002
89
pkg os, method (*Root) Symlink(string, string) error #67002

doc/next/6-stdlib/99-minor/os/67002.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ The [os.Root] type supports the following additional methods:
66
* [os.Root.Lchown]
77
* [os.Root.Link]
88
* [os.Root.Readlink]
9+
* [os.Root.RemoveAll]
910
* [os.Root.Rename]
1011
* [os.Root.Symlink]

src/os/removeall_at.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func removeAll(path string) error {
4444
}
4545
defer parent.Close()
4646

47-
if err := removeAllFrom(parent, base); err != nil {
47+
if err := removeAllFrom(sysfdType(parent.Fd()), base); err != nil {
4848
if pathErr, ok := err.(*PathError); ok {
4949
pathErr.Path = parentDir + string(PathSeparator) + pathErr.Path
5050
err = pathErr
@@ -54,9 +54,7 @@ func removeAll(path string) error {
5454
return nil
5555
}
5656

57-
func removeAllFrom(parent *File, base string) error {
58-
parentFd := sysfdType(parent.Fd())
59-
57+
func removeAllFrom(parentFd sysfdType, base string) error {
6058
// Simple case: if Unlink (aka remove) works, we're done.
6159
err := removefileat(parentFd, base)
6260
if err == nil || IsNotExist(err) {
@@ -109,7 +107,7 @@ func removeAllFrom(parent *File, base string) error {
109107

110108
respSize = len(names)
111109
for _, name := range names {
112-
err := removeAllFrom(file, name)
110+
err := removeAllFrom(sysfdType(file.Fd()), name)
113111
if err != nil {
114112
if pathErr, ok := err.(*PathError); ok {
115113
pathErr.Path = base + string(PathSeparator) + pathErr.Path

src/os/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ func (r *Root) Remove(name string) error {
177177
return rootRemove(r, name)
178178
}
179179

180+
// RemoveAll removes the named file or directory and any children that it contains.
181+
// See [RemoveAll] for more details.
182+
func (r *Root) RemoveAll(name string) error {
183+
return rootRemoveAll(r, name)
184+
}
185+
180186
// Stat returns a [FileInfo] describing the named file in the root.
181187
// See [Stat] for more details.
182188
func (r *Root) Stat(name string) (FileInfo, error) {

src/os/root_noopenat.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package os
99
import (
1010
"errors"
1111
"sync/atomic"
12+
"syscall"
1213
"time"
1314
)
1415

@@ -156,6 +157,25 @@ func rootRemove(r *Root, name string) error {
156157
return nil
157158
}
158159

160+
func rootRemoveAll(r *Root, name string) error {
161+
if endsWithDot(name) {
162+
// Consistency with os.RemoveAll: Return EINVAL when trying to remove .
163+
return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL}
164+
}
165+
if err := checkPathEscapesLstat(r, name); err != nil {
166+
if err == syscall.ENOTDIR {
167+
// Some intermediate path component is not a directory.
168+
// RemoveAll treats this as success (since the target doesn't exist).
169+
return nil
170+
}
171+
return &PathError{Op: "RemoveAll", Path: name, Err: err}
172+
}
173+
if err := RemoveAll(joinPath(r.root.name, name)); err != nil {
174+
return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)}
175+
}
176+
return nil
177+
}
178+
159179
func rootReadlink(r *Root, name string) (string, error) {
160180
if err := checkPathEscapesLstat(r, name); err != nil {
161181
return "", &PathError{Op: "readlinkat", Path: name, Err: err}

src/os/root_openat.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,28 @@ func rootRemove(r *Root, name string) error {
138138
return nil
139139
}
140140

141+
func rootRemoveAll(r *Root, name string) error {
142+
// Consistency with os.RemoveAll: Strip trailing /s from the name,
143+
// so RemoveAll("not_a_directory/") succeeds.
144+
for len(name) > 0 && IsPathSeparator(name[len(name)-1]) {
145+
name = name[:len(name)-1]
146+
}
147+
if endsWithDot(name) {
148+
// Consistency with os.RemoveAll: Return EINVAL when trying to remove .
149+
return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL}
150+
}
151+
_, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
152+
return struct{}{}, removeAllFrom(parent, name)
153+
})
154+
if IsNotExist(err) {
155+
return nil
156+
}
157+
if err != nil {
158+
return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)}
159+
}
160+
return err
161+
}
162+
141163
func rootRename(r *Root, oldname, newname string) error {
142164
_, err := doInRoot(r, oldname, func(oldparent sysfdType, oldname string) (struct{}, error) {
143165
_, err := doInRoot(r, newname, func(newparent sysfdType, newname string) (struct{}, error) {

src/os/root_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,46 @@ func TestRootRemoveDirectory(t *testing.T) {
630630
}
631631
}
632632

633+
func TestRootRemoveAll(t *testing.T) {
634+
for _, test := range rootTestCases {
635+
test.run(t, func(t *testing.T, target string, root *os.Root) {
636+
wantError := test.wantError
637+
if test.ltarget != "" {
638+
// Remove doesn't follow symlinks in the final path component,
639+
// so it will successfully remove ltarget.
640+
wantError = false
641+
target = filepath.Join(root.Name(), test.ltarget)
642+
} else if target != "" {
643+
if err := os.Mkdir(target, 0o777); err != nil {
644+
t.Fatal(err)
645+
}
646+
if err := os.WriteFile(filepath.Join(target, "file"), nil, 0o666); err != nil {
647+
t.Fatal(err)
648+
}
649+
}
650+
targetExists := true
651+
if _, err := root.Lstat(test.open); errors.Is(err, os.ErrNotExist) {
652+
// If the target doesn't exist, RemoveAll succeeds rather
653+
// than returning ErrNotExist.
654+
targetExists = false
655+
wantError = false
656+
}
657+
658+
err := root.RemoveAll(test.open)
659+
if errEndsTest(t, err, wantError, "root.RemoveAll(%q)", test.open) {
660+
return
661+
}
662+
if !targetExists {
663+
return
664+
}
665+
_, err = os.Lstat(target)
666+
if !errors.Is(err, os.ErrNotExist) {
667+
t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
668+
}
669+
})
670+
}
671+
}
672+
633673
func TestRootOpenFileAsRoot(t *testing.T) {
634674
dir := t.TempDir()
635675
target := filepath.Join(dir, "target")
@@ -958,6 +998,9 @@ type rootConsistencyTest struct {
958998
// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
959999
// function return different errors for this test.
9601000
detailedErrorMismatch func(t *testing.T) bool
1001+
1002+
// check is called before the test starts, and may t.Skip if necessary.
1003+
check func(t *testing.T)
9611004
}
9621005

9631006
var rootConsistencyTestCases = []rootConsistencyTest{{
@@ -1115,6 +1158,16 @@ var rootConsistencyTestCases = []rootConsistencyTest{{
11151158
// and os.Open returns "The file cannot be accessed by the system.".
11161159
return runtime.GOOS == "windows"
11171160
},
1161+
check: func(t *testing.T) {
1162+
if runtime.GOOS == "windows" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") {
1163+
// Root.RemoveAll notices that a/ is not a directory,
1164+
// and returns success.
1165+
// os.RemoveAll tries to open a/ and fails because
1166+
// it is not a regular file.
1167+
// The inconsistency here isn't worth fixing, so just skip this test.
1168+
t.Skip("known inconsistency on windows")
1169+
}
1170+
},
11181171
}, {
11191172
name: "question mark",
11201173
open: "?",
@@ -1156,6 +1209,10 @@ func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path stri
11561209
}
11571210

11581211
t.Run(test.name, func(t *testing.T) {
1212+
if test.check != nil {
1213+
test.check(t)
1214+
}
1215+
11591216
dir1 := makefs(t, test.fs)
11601217
dir2 := makefs(t, test.fs)
11611218
if test.fsFunc != nil {
@@ -1321,6 +1378,23 @@ func TestRootConsistencyRemove(t *testing.T) {
13211378
}
13221379
}
13231380

1381+
func TestRootConsistencyRemoveAll(t *testing.T) {
1382+
for _, test := range rootConsistencyTestCases {
1383+
if test.open == "." || test.open == "./" {
1384+
continue // can't remove the root itself
1385+
}
1386+
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
1387+
var err error
1388+
if r == nil {
1389+
err = os.RemoveAll(path)
1390+
} else {
1391+
err = r.RemoveAll(path)
1392+
}
1393+
return "", err
1394+
})
1395+
}
1396+
}
1397+
13241398
func TestRootConsistencyStat(t *testing.T) {
13251399
for _, test := range rootConsistencyTestCases {
13261400
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
@@ -1759,3 +1833,21 @@ func TestOpenInRoot(t *testing.T) {
17591833
}
17601834
}
17611835
}
1836+
1837+
func TestRootRemoveDot(t *testing.T) {
1838+
dir := t.TempDir()
1839+
root, err := os.OpenRoot(dir)
1840+
if err != nil {
1841+
t.Fatal(err)
1842+
}
1843+
defer root.Close()
1844+
if err := root.Remove("."); err == nil {
1845+
t.Errorf(`root.Remove(".") = %v, want error`, err)
1846+
}
1847+
if err := root.RemoveAll("."); err == nil {
1848+
t.Errorf(`root.RemoveAll(".") = %v, want error`, err)
1849+
}
1850+
if _, err := os.Stat(dir); err != nil {
1851+
t.Error(`root.Remove(All)?(".") removed the root`)
1852+
}
1853+
}

src/os/root_unix.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import (
1414
"time"
1515
)
1616

17+
// sysfdType is the native type of a file handle
18+
// (int on Unix, syscall.Handle on Windows),
19+
// permitting helper functions to be written portably.
1720
type sysfdType = int
1821

1922
// openRootNolog is OpenRoot.

src/os/root_windows.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ func rootCleanPath(s string, prefix, suffix []string) (string, error) {
7777
return s, nil
7878
}
7979

80+
// sysfdType is the native type of a file handle
81+
// (int on Unix, syscall.Handle on Windows),
82+
// permitting helper functions to be written portably.
8083
type sysfdType = syscall.Handle
8184

8285
// openRootNolog is OpenRoot.

0 commit comments

Comments
 (0)