blob: 33a2f3374331b8ea253a9c808099088bf5cde66a [file]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package os_test
import (
"bytes"
"errors"
"flag"
"fmt"
"internal/testenv"
"io"
"io/fs"
"iter"
"net"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
"time"
)
// testMaybeRooted calls f in two subtests,
// one with a Root and one with a nil r.
func testMaybeRooted(t *testing.T, f func(t *testing.T, r *os.Root)) {
t.Run("NoRoot", func(t *testing.T) {
t.Chdir(t.TempDir())
f(t, nil)
})
t.Run("InRoot", func(t *testing.T) {
t.Chdir(t.TempDir())
r, err := os.OpenRoot(".")
if err != nil {
t.Fatal(err)
}
defer r.Close()
f(t, r)
})
}
// makefs creates a test filesystem layout and returns the path to its root.
//
// Each entry in the slice is a file, directory, or symbolic link to create:
//
// - "d/": directory d
// - "f": file f with contents f
// - "a => b": symlink a with target b
//
// The directory containing the filesystem is always named ROOT.
// $ABS is replaced with the absolute path of the directory containing the filesystem.
//
// Parent directories are automatically created as needed.
//
// makefs calls t.Skip if the layout contains features not supported by the current GOOS.
func makefs(t *testing.T, fs []string) string {
root := filepath.Join(t.TempDir(), "ROOT")
if err := os.Mkdir(root, 0o777); err != nil {
t.Fatal(err)
}
for _, ent := range fs {
ent = strings.ReplaceAll(ent, "$ABS", root)
base, link, isLink := strings.Cut(ent, " => ")
if isLink {
if runtime.GOOS == "wasip1" && path.IsAbs(link) {
t.Skip("absolute link targets not supported on " + runtime.GOOS)
}
if runtime.GOOS == "plan9" {
t.Skip("symlinks not supported on " + runtime.GOOS)
}
ent = base
}
if err := os.MkdirAll(path.Join(root, path.Dir(base)), 0o777); err != nil {
t.Fatal(err)
}
if isLink {
if err := os.Symlink(link, path.Join(root, base)); err != nil {
t.Fatal(err)
}
} else if strings.HasSuffix(ent, "/") {
if err := os.MkdirAll(path.Join(root, ent), 0o777); err != nil {
t.Fatal(err)
}
} else {
if err := os.WriteFile(path.Join(root, ent), []byte(ent), 0o666); err != nil {
t.Fatal(err)
}
}
}
return root
}
// hasLink reports whether the test filesystem layout fs
// contains at least a single symlink.
func hasLink(fs []string) bool {
for _, ent := range fs {
isLink := strings.Contains(ent, " => ")
if isLink {
return true
}
}
return false
}
// A rootTest is a test case for os.Root.
type rootTest struct {
name string
// fs is the test filesystem layout. See makefs above.
fs []string
// open is the filename to access in the test.
open string
// target is the filename that we expect to be accessed, after resolving all symlinks.
// For test cases where the operation fails due to an escaping path such as ../ROOT/x,
// the target is the filename that should not have been opened.
target string
// ltarget is the filename that we expect to accessed, after resolving all symlinks
// except the last one. This is the file we expect to be removed by Remove or statted
// by Lstat.
//
// If the last path component in open is not a symlink, ltarget should be "".
ltarget string
// wantError is true if accessing the file should fail.
wantError bool
// alwaysFails is true if the open operation is expected to fail
// even when using non-openat operations.
//
// This lets us check that tests that are expected to fail because (for example)
// a path escapes the directory root will succeed when the escaping checks are not
// performed.
alwaysFails bool
}
// run sets up the test filesystem layout, os.OpenDirs the root, and calls f.
func (test *rootTest) run(t *testing.T, f func(t *testing.T, target string, d *os.Root)) {
t.Run(test.name, func(t *testing.T) {
if hasLink(test.fs) {
testenv.MustHaveSymlink(t)
}
root := makefs(t, test.fs)
d, err := os.OpenRoot(root)
if err != nil {
t.Fatal(err)
}
defer d.Close()
// The target is a file that will be accessed,
// or a file that should not be accessed
// (because doing so escapes the root).
target := test.target
if test.target != "" {
target = filepath.Join(root, test.target)
}
f(t, target, d)
})
}
// errEndsTest checks the error result of a test,
// verifying that it succeeded or failed as expected.
//
// It returns true if the test is done due to encountering an expected error.
// false if the test should continue.
func errEndsTest(t *testing.T, err error, wantError bool, format string, args ...any) bool {
t.Helper()
if wantError {
if err == nil {
op := fmt.Sprintf(format, args...)
t.Fatalf("%v = nil; want error", op)
}
return true
} else {
if err != nil {
op := fmt.Sprintf(format, args...)
t.Fatalf("%v = %v; want success", op, err)
}
return false
}
}
var rootTestCases = []rootTest{{
name: "plain path",
fs: []string{},
open: "target",
target: "target",
}, {
name: "path in directory",
fs: []string{
"a/b/c/",
},
open: "a/b/c/target",
target: "a/b/c/target",
}, {
name: "symlink",
fs: []string{
"link => target",
},
open: "link",
target: "target",
ltarget: "link",
}, {
name: "symlink dotdot slash",
fs: []string{
"link => ../",
},
open: "link",
ltarget: "link",
wantError: true,
}, {
name: "symlink ending in slash",
fs: []string{
"dir/",
"link => dir/",
},
open: "link/target",
target: "dir/target",
}, {
name: "slash after symlink to file",
fs: []string{
"link => ../ROOT/target",
},
open: "link/",
target: "target",
wantError: true,
}, {
name: "slash after symlink to dir",
fs: []string{
"link => ../ROOT/target",
"target/",
},
open: "link/",
wantError: true,
}, {
name: "symlink dotdot dotdot slash",
fs: []string{
"dir/link => ../../",
},
open: "dir/link",
ltarget: "dir/link",
wantError: true,
}, {
name: "symlink chain",
fs: []string{
"link => a/b/c/target",
"a/b => e",
"a/e => ../f",
"f => g/h/i",
"g/h/i => ..",
"g/c/",
},
open: "link",
target: "g/c/target",
ltarget: "link",
}, {
name: "path with dot",
fs: []string{
"a/b/",
},
open: "./a/./b/./target",
target: "a/b/target",
}, {
name: "path with dotdot",
fs: []string{
"a/b/",
},
open: "a/../a/b/../../a/b/../b/target",
target: "a/b/target",
}, {
name: "path with dotdot slash",
fs: []string{},
open: "../",
wantError: true,
}, {
name: "path with dotdot dotdot slash",
fs: []string{
"a/",
},
open: "a/../../",
wantError: true,
}, {
name: "dotdot no symlink",
fs: []string{
"a/",
},
open: "a/../target",
target: "target",
}, {
name: "dotdot after symlink",
fs: []string{
"a => b/c",
"b/c/",
},
open: "a/../target",
target: func() string {
if runtime.GOOS == "windows" {
// On Windows, the path is cleaned before symlink resolution.
return "target"
}
return "b/target"
}(),
}, {
name: "dotdot before symlink",
fs: []string{
"a => b/c",
"b/c/",
},
open: "b/../a/target",
target: "b/c/target",
}, {
name: "symlink ends in dot",
fs: []string{
"a => b/.",
"b/",
},
open: "a/target",
target: "b/target",
}, {
name: "directory does not exist",
fs: []string{},
open: "a/file",
wantError: true,
alwaysFails: true,
}, {
name: "empty path",
fs: []string{},
open: "",
wantError: true,
alwaysFails: true,
}, {
name: "symlink cycle",
fs: []string{
"a => a",
},
open: "a",
ltarget: "a",
wantError: true,
alwaysFails: true,
}, {
name: "path escapes",
fs: []string{},
open: "../ROOT/target",
target: "target",
wantError: true,
}, {
name: "long path escapes",
fs: []string{
"a/",
},
open: "a/../../ROOT/target",
target: "target",
wantError: true,
}, {
name: "absolute symlink",
fs: []string{
"link => $ABS/target",
},
open: "link",
ltarget: "link",
target: "target",
wantError: true,
}, {
name: "relative symlink",
fs: []string{
"link => ../ROOT/target",
},
open: "link",
target: "target",
ltarget: "link",
wantError: true,
}, {
name: "symlink chain escapes",
fs: []string{
"link => a/b/c/target",
"a/b => e",
"a/e => ../../ROOT",
"c/",
},
open: "link",
target: "c/target",
ltarget: "link",
wantError: true,
}}
func TestRootOpen_File(t *testing.T) {
want := []byte("target")
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if target != "" {
if err := os.WriteFile(target, want, 0o666); err != nil {
t.Fatal(err)
}
}
f, err := root.Open(test.open)
if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
return
}
defer f.Close()
got, err := io.ReadAll(f)
if err != nil || !bytes.Equal(got, want) {
t.Errorf(`Dir.Open(%q): read content %q, %v; want %q`, test.open, string(got), err, string(want))
}
})
}
}
func TestRootOpen_Directory(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if target != "" {
if err := os.Mkdir(target, 0o777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(target+"/found", nil, 0o666); err != nil {
t.Fatal(err)
}
}
f, err := root.Open(test.open)
if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
return
}
defer f.Close()
got, err := f.Readdirnames(-1)
if err != nil {
t.Errorf(`Dir.Open(%q).Readdirnames: %v`, test.open, err)
}
if want := []string{"found"}; !slices.Equal(got, want) {
t.Errorf(`Dir.Open(%q).Readdirnames: %q, want %q`, test.open, got, want)
}
})
}
}
func TestRootCreate(t *testing.T) {
want := []byte("target")
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
f, err := root.Create(test.open)
if errEndsTest(t, err, test.wantError, "root.Create(%q)", test.open) {
return
}
if _, err := f.Write(want); err != nil {
t.Fatal(err)
}
f.Close()
got, err := os.ReadFile(target)
if err != nil {
t.Fatalf(`reading file created with root.Create(%q): %v`, test.open, err)
}
if !bytes.Equal(got, want) {
t.Fatalf(`reading file created with root.Create(%q): got %q; want %q`, test.open, got, want)
}
})
}
}
func TestRootChmod(t *testing.T) {
if runtime.GOOS == "wasip1" {
t.Skip("Chmod not supported on " + runtime.GOOS)
}
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if target != "" {
// Create a file with no read/write permissions,
// to ensure we can use Chmod on an inaccessible file.
if err := os.WriteFile(target, nil, 0o000); err != nil {
t.Fatal(err)
}
}
if runtime.GOOS == "windows" {
// On Windows, Chmod("symlink") affects the link, not its target.
// See issue 71492.
fi, err := root.Lstat(test.open)
if err == nil && !fi.Mode().IsRegular() {
t.Skip("https://go.dev/issue/71492")
}
}
want := os.FileMode(0o666)
err := root.Chmod(test.open, want)
if errEndsTest(t, err, test.wantError, "root.Chmod(%q)", test.open) {
return
}
st, err := os.Stat(target)
if err != nil {
t.Fatalf("os.Stat(%q) = %v", target, err)
}
if got := st.Mode(); got != want {
t.Errorf("after root.Chmod(%q, %v): file mode = %v, want %v", test.open, want, got, want)
}
})
}
}
func TestRootChtimes(t *testing.T) {
// Don't check atimes if the fs is mounted noatime,
// or on Plan 9 which does not permit changing atimes to arbitrary values.
checkAtimes := !hasNoatime() && runtime.GOOS != "plan9"
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if target != "" {
if err := os.WriteFile(target, nil, 0o666); err != nil {
t.Fatal(err)
}
}
for _, times := range []struct {
atime, mtime time.Time
}{{
atime: time.Now().Add(-1 * time.Minute),
mtime: time.Now().Add(-1 * time.Minute),
}, {
atime: time.Now().Add(1 * time.Minute),
mtime: time.Now().Add(1 * time.Minute),
}, {
atime: time.Time{},
mtime: time.Now(),
}, {
atime: time.Now(),
mtime: time.Time{},
}} {
switch runtime.GOOS {
case "js", "plan9":
times.atime = times.atime.Truncate(1 * time.Second)
times.mtime = times.mtime.Truncate(1 * time.Second)
case "illumos":
times.atime = times.atime.Truncate(1 * time.Microsecond)
times.mtime = times.mtime.Truncate(1 * time.Microsecond)
}
err := root.Chtimes(test.open, times.atime, times.mtime)
if errEndsTest(t, err, test.wantError, "root.Chtimes(%q)", test.open) {
return
}
st, err := os.Stat(target)
if err != nil {
t.Fatalf("os.Stat(%q) = %v", target, err)
}
if got := st.ModTime(); !times.mtime.IsZero() && !got.Equal(times.mtime) {
t.Errorf("after root.Chtimes(%q, %v, %v): got mtime=%v, want %v", test.open, times.atime, times.mtime, got, times.mtime)
}
if checkAtimes {
if got := os.Atime(st); !times.atime.IsZero() && !got.Equal(times.atime) {
t.Errorf("after root.Chtimes(%q, %v, %v): got atime=%v, want %v", test.open, times.atime, times.mtime, got, times.atime)
}
}
}
})
}
}
func TestRootMkdir(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 != "" {
// This case is trying to mkdir("some symlink"),
// which is an error (but not an escape).
wantError = true
}
err := root.Mkdir(test.open, 0o777)
if errEndsTest(t, err, wantError, "root.Create(%q)", test.open) {
return
}
fi, err := os.Lstat(target)
if err != nil {
t.Fatalf(`stat file created with Root.Mkdir(%q): %v`, test.open, err)
}
if !fi.IsDir() {
t.Fatalf(`stat file created with Root.Mkdir(%q): not a directory`, test.open)
}
if mode := fi.Mode(); mode&0o777 == 0 {
// Issue #73559: We're not going to worry about the exact
// mode bits (which will have been modified by umask),
// but there should be mode bits.
t.Fatalf(`stat file created with Root.Mkdir(%q): mode=%v, want non-zero`, test.open, mode)
}
})
}
}
func TestRootMkdirAll(t *testing.T) {
for _, test := range rootTestCases {
if test.name == "directory does not exist" {
// Test expects error, mkdirall creates the missing directory.
// TestRootMultiMkdirAll covers this case better anyway, just skip.
continue
}
test.run(t, func(t *testing.T, target string, root *os.Root) {
wantError := test.wantError
if test.ltarget != "" {
// This case is trying to mkdir("some symlink"),
// which is an error (but not an escape).
wantError = true
}
err := root.MkdirAll(test.open, 0o777)
if errEndsTest(t, err, wantError, "root.MkdirAll(%q)", test.open) {
return
}
fi, err := os.Lstat(target)
if err != nil {
t.Fatalf(`stat file created with Root.MkdirAll(%q): %v`, test.open, err)
}
if !fi.IsDir() {
t.Fatalf(`stat file created with Root.MkdirAll(%q): not a directory`, test.open)
}
if mode := fi.Mode(); mode&0o777 == 0 {
// Issue #73559: We're not going to worry about the exact
// mode bits (which will have been modified by umask),
// but there should be mode bits.
t.Fatalf(`stat file created with Root.MkdirAll(%q): mode=%v, want non-zero`, test.open, mode)
}
})
}
}
func TestRootOpenRoot(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if target != "" {
if err := os.Mkdir(target, 0o777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(target+"/f", nil, 0o666); err != nil {
t.Fatal(err)
}
}
rr, err := root.OpenRoot(test.open)
if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) {
return
}
defer rr.Close()
f, err := rr.Open("f")
if err != nil {
t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err)
}
f.Close()
})
}
}
func TestRootRemoveFile(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.WriteFile(target, nil, 0o666); err != nil {
t.Fatal(err)
}
}
err := root.Remove(test.open)
if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
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 TestRootRemoveDirectory(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)
}
}
err := root.Remove(test.open)
if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
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 TestRootRemoveAll(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if strings.HasSuffix(test.open, "/") {
// The test is removing a file with a trailing /.
// RemoveAll ignores trailing /s
// If the file is a symlink, it will remove the symlink.
fullname := filepath.Join(root.Name(), test.open)
if st, err := os.Lstat(fullname); err == nil && st.Mode().Type() == fs.ModeSymlink {
test.ltarget = test.open
}
}
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")
if err := os.WriteFile(target, nil, 0o666); err != nil {
t.Fatal(err)
}
r, err := os.OpenRoot(target)
if err == nil {
r.Close()
t.Fatal("os.OpenRoot(file) succeeded; want failure")
}
r, err = os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer r.Close()
rr, err := r.OpenRoot("target")
if err == nil {
rr.Close()
t.Fatal("Root.OpenRoot(file) succeeded; want failure")
}
}
func TestRootStat(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
const content = "content"
if target != "" {
if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
t.Fatal(err)
}
}
fi, err := root.Stat(test.open)
if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) {
return
}
if got, want := fi.Name(), filepath.Base(test.open); got != want {
t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
}
if got, want := fi.Size(), int64(len(content)); got != want {
t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
}
})
}
}
func TestRootLstat(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
const content = "content"
wantError := test.wantError
if test.ltarget != "" {
// Lstat will stat the final link, rather than following it.
wantError = false
} else if target != "" {
if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
t.Fatal(err)
}
}
fi, err := root.Lstat(test.open)
if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) {
return
}
if got, want := fi.Name(), filepath.Base(test.open); got != want {
t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
}
if test.ltarget == "" {
if got := fi.Mode(); got&os.ModeSymlink != 0 {
t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got)
}
if got, want := fi.Size(), int64(len(content)); got != want {
t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
}
} else {
if got := fi.Mode(); got&os.ModeSymlink == 0 {
t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got)
}
}
})
}
}
func TestRootReadlink(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
const content = "content"
wantError := test.wantError
if test.ltarget != "" {
// Readlink will read the final link, rather than following it.
wantError = false
} else {
// Readlink fails on non-link targets.
wantError = true
}
got, err := root.Readlink(test.open)
if errEndsTest(t, err, wantError, "root.Readlink(%q)", test.open) {
return
}
want, err := os.Readlink(filepath.Join(root.Name(), test.ltarget))
if err != nil {
t.Fatalf("os.Readlink(%q) = %v, want success", test.ltarget, err)
}
if got != want {
t.Errorf("root.Readlink(%q) = %q, want %q", test.open, got, want)
}
})
}
}
// 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) {
if target != "" {
if err := os.WriteFile(target, want, 0o666); err != nil {
t.Fatal(err)
}
}
wantError := test.wantError
var linkTarget string
if test.ltarget != "" {
// Rename will rename the link, not the file linked to.
wantError = false
var err error
linkTarget, err = root.Readlink(test.ltarget)
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
}
// Windows allows creating a hard link to a file symlink,
// but not to a directory symlink.
//
// This uses os.Stat to check the link target, because this
// is easier than figuring out whether the link itself is a
// directory link. The link was created with os.Symlink,
// which creates directory links when the target is a directory,
// so this is good enough for a test.
if !rename && runtime.GOOS == "windows" {
st, err := os.Stat(filepath.Join(root.Name(), test.ltarget))
if err == nil && st.IsDir() {
wantError = true
}
}
}
const dstPath = "destination"
// Plan 9 doesn't allow cross-directory renames.
if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
wantError = true
}
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 != "" {
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", dstFullPath, got, err, linkTarget)
}
} else {
got, err := os.ReadFile(dstFullPath)
if err != nil || !bytes.Equal(got, 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)
}
}
})
}
}
// 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) {
const srcPath = "source"
if err := os.WriteFile(filepath.Join(root.Name(), srcPath), want, 0o666); err != nil {
t.Fatal(err)
}
if runtime.GOOS == "windows" && strings.HasSuffix(test.open, "/") {
// Windows will ignore trailing slashes in the rename/link target.
p := strings.TrimSuffix(test.open, "/")
st, err := root.Lstat(p)
if err == nil && st.Mode().Type() == fs.ModeSymlink {
test.ltarget = p
}
}
target = test.target
wantError := test.wantError
if test.ltarget != "" {
// Rename will overwrite the final link rather than follow it.
target = test.ltarget
wantError = false
}
// Plan 9 doesn't allow cross-directory renames.
if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
wantError = true
}
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))
}
})
}
}
func TestRootSymlink(t *testing.T) {
testenv.MustHaveSymlink(t)
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
wantError := test.wantError
if test.ltarget != "" {
// We can't create a symlink over an existing symlink.
wantError = true
}
const wantTarget = "linktarget"
err := root.Symlink(wantTarget, test.open)
if errEndsTest(t, err, wantError, "root.Symlink(%q)", test.open) {
return
}
got, err := os.Readlink(target)
if err != nil || got != wantTarget {
t.Fatalf("ReadLink(%q) = %q, %v; want %q, nil", target, got, err, wantTarget)
}
})
}
}
// A rootConsistencyTest is a test case comparing os.Root behavior with
// the corresponding non-Root function.
//
// These tests verify that, for example, Root.Open("file/./") and os.Open("file/./")
// have the same result, although the specific result may vary by platform.
type rootConsistencyTest struct {
name string
// fs is the test filesystem layout. See makefs above.
// fsFunc is called to modify the test filesystem, or replace it.
fs []string
fsFunc func(t *testing.T, dir string) string
// open is the filename to access in the test.
open string
// 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{{
name: "file",
fs: []string{
"target",
},
open: "target",
}, {
name: "dir slash dot",
fs: []string{
"target/file",
},
open: "target/.",
}, {
name: "dot",
fs: []string{
"file",
},
open: ".",
}, {
name: "file slash dot",
fs: []string{
"target",
},
open: "target/.",
detailedErrorMismatch: func(t *testing.T) bool {
// FreeBSD returns EPERM in the non-Root case.
return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
},
}, {
name: "dir slash",
fs: []string{
"target/file",
},
open: "target/",
}, {
name: "dot slash",
fs: []string{
"file",
},
open: "./",
}, {
name: "file slash",
fs: []string{
"target",
},
open: "target/",
detailedErrorMismatch: func(t *testing.T) bool {
// os.Create returns ENOTDIR or EISDIR depending on the platform.
return runtime.GOOS == "js"
},
}, {
name: "file in path",
fs: []string{
"file",
},
open: "file/target",
}, {
name: "directory in path missing",
open: "dir/target",
}, {
name: "target does not exist",
open: "target",
}, {
name: "symlink slash",
fs: []string{
"target/file",
"link => target",
},
open: "link/",
check: func(t *testing.T) {
if runtime.GOOS == "linux" && strings.HasPrefix(t.Name(), "TestRootConsistencyRename/") {
// Linux does not resolve "symlink" in rename("symlink/", "target").
t.Skip("known inconsistency on linux")
}
if strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") {
// Root.RemoveAll and os.RemoveAll are not always consistent here.
t.Skip("known inconsistency in RemoveAll")
}
},
}, {
name: "symlink slash dot",
fs: []string{
"target/file",
"link => target",
},
open: "link/.",
}, {
name: "unresolved symlink",
fs: []string{
"link => target",
},
open: "link",
}, {
name: "resolved symlink",
fs: []string{
"link => target",
"target",
},
open: "link",
}, {
name: "dotdot in path after symlink",
fs: []string{
"a => b/c",
"b/c/",
"b/target",
},
open: "a/../target",
}, {
name: "symlink to dir ends in slash",
fs: []string{
"dir/",
"link => dir/",
},
open: "link",
}, {
name: "symlink to file ends in slash",
fs: []string{
"file",
"link => file/",
},
open: "link",
}, {
name: "long file name",
open: strings.Repeat("a", 500),
}, {
name: "unreadable directory",
fs: []string{
"dir/target",
},
fsFunc: func(t *testing.T, dir string) string {
os.Chmod(filepath.Join(dir, "dir"), 0)
t.Cleanup(func() {
os.Chmod(filepath.Join(dir, "dir"), 0o700)
})
return dir
},
open: "dir/target",
}, {
name: "unix domain socket target",
fsFunc: func(t *testing.T, dir string) string {
return tempDirWithUnixSocket(t, "a")
},
open: "a",
}, {
name: "unix domain socket in path",
fsFunc: func(t *testing.T, dir string) string {
return tempDirWithUnixSocket(t, "a")
},
open: "a/b",
detailedErrorMismatch: func(t *testing.T) bool {
// On Windows, os.Root.Open returns "The directory name is invalid."
// and os.Open returns "The file cannot be accessed by the system.".
return runtime.GOOS == "windows"
},
check: func(t *testing.T) {
if strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") {
switch runtime.GOOS {
case "windows":
// 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")
case "js":
// GOOS=js behavior varies with what the underlying OS is.
t.Skip("known inconsistency with GOOS=js")
}
}
},
}, {
name: "question mark",
open: "?",
}, {
name: "nul byte",
open: "\x00",
}}
func tempDirWithUnixSocket(t *testing.T, name string) string {
dir := t.TempDir()
addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name))
if err != nil {
t.Skipf("net.ResolveUnixAddr: %v", err)
}
conn, err := net.ListenUnix("unix", addr)
if err != nil {
t.Skipf("net.ListenUnix: %v", err)
}
t.Cleanup(func() {
conn.Close()
})
return dir
}
func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) {
if runtime.GOOS == "wasip1" {
// On wasip, non-Root functions clean paths before opening them,
// resulting in inconsistent behavior.
// https://go.dev/issue/69509
t.Skip("#69509: inconsistent results on wasip1")
}
t.Run(test.name, func(t *testing.T) {
if test.check != nil {
test.check(t)
}
if hasLink(test.fs) {
testenv.MustHaveSymlink(t)
}
dir1 := makefs(t, test.fs)
dir2 := makefs(t, test.fs)
if test.fsFunc != nil {
dir1 = test.fsFunc(t, dir1)
dir2 = test.fsFunc(t, dir2)
}
r, err := os.OpenRoot(dir1)
if err != nil {
t.Fatal(err)
}
defer r.Close()
res1, err1 := f(t, test.open, r)
res2, err2 := f(t, dir2+"/"+test.open, nil)
if res1 != res2 || ((err1 == nil) != (err2 == nil)) {
t.Errorf("with root: res=%v", res1)
t.Errorf(" err=%v", err1)
t.Errorf("without root: res=%v", res2)
t.Errorf(" err=%v", err2)
t.Errorf("want consistent results, got mismatch")
}
if err1 != nil || err2 != nil {
underlyingError := func(how string, err error) error {
switch e := err1.(type) {
case *os.PathError:
return e.Err
case *os.LinkError:
return e.Err
default:
t.Fatalf("%v, expected PathError or LinkError; got: %v", how, err)
}
return nil
}
e1 := underlyingError("with root", err1)
e2 := underlyingError("without root", err1)
detailedErrorMismatch := false
if f := test.detailedErrorMismatch; f != nil {
detailedErrorMismatch = f(t)
}
if runtime.GOOS == "plan9" {
// Plan9 syscall errors aren't comparable.
detailedErrorMismatch = true
}
if !detailedErrorMismatch && e1 != e2 {
t.Errorf("with root: err=%v", e1)
t.Errorf("without root: err=%v", e2)
t.Errorf("want consistent results, got mismatch")
}
}
})
}
func TestRootConsistencyOpen(t *testing.T) {
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
var f *os.File
var err error
if r == nil {
f, err = os.Open(path)
} else {
f, err = r.Open(path)
}
if err != nil {
return "", err
}
defer f.Close()
fi, err := f.Stat()
if err == nil && !fi.IsDir() {
b, err := io.ReadAll(f)
return string(b), err
} else {
names, err := f.Readdirnames(-1)
slices.Sort(names)
return fmt.Sprintf("%q", names), err
}
})
}
}
func TestRootConsistencyCreate(t *testing.T) {
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
var f *os.File
var err error
if r == nil {
f, err = os.Create(path)
} else {
f, err = r.Create(path)
}
if err == nil {
f.Write([]byte("file contents"))
f.Close()
}
return "", err
})
}
}
func TestRootConsistencyChmod(t *testing.T) {
if runtime.GOOS == "wasip1" {
t.Skip("Chmod not supported on " + runtime.GOOS)
}
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
chmod := os.Chmod
lstat := os.Lstat
if r != nil {
chmod = r.Chmod
lstat = r.Lstat
}
var m1, m2 os.FileMode
if err := chmod(path, 0o555); err != nil {
return "chmod 0o555", err
}
fi, err := lstat(path)
if err == nil {
m1 = fi.Mode()
}
if err = chmod(path, 0o777); err != nil {
return "chmod 0o777", err
}
fi, err = lstat(path)
if err == nil {
m2 = fi.Mode()
}
return fmt.Sprintf("%v %v", m1, m2), err
})
}
}
func TestRootConsistencyMkdir(t *testing.T) {
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
var err error
if r == nil {
err = os.Mkdir(path, 0o777)
} else {
err = r.Mkdir(path, 0o777)
}
return "", err
})
}
}
func TestRootConsistencyMkdirAll(t *testing.T) {
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
var err error
if r == nil {
err = os.MkdirAll(path, 0o777)
} else {
err = r.MkdirAll(path, 0o777)
}
return "", err
})
}
}
func TestRootConsistencyRemove(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.Remove(path)
} else {
err = r.Remove(path)
}
return "", err
})
}
}
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) {
var fi os.FileInfo
var err error
if r == nil {
fi, err = os.Stat(path)
} else {
fi, err = r.Stat(path)
}
if err != nil {
return "", err
}
return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
})
}
}
func TestRootConsistencyLstat(t *testing.T) {
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
var fi os.FileInfo
var err error
if r == nil {
fi, err = os.Lstat(path)
} else {
fi, err = r.Lstat(path)
}
if err != nil {
return "", err
}
return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
})
}
}
func TestRootConsistencyReadlink(t *testing.T) {
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
if r == nil {
return os.Readlink(path)
} else {
return r.Readlink(path)
}
})
}
}
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")
}
// Run this test in two directions:
// Renaming the test path to a known-good path (from),
// and renaming a known-good path to the test path (to).
for _, name := range []string{"from", "to"} {
t.Run(name, func(t *testing.T) {
for _, test := range rootConsistencyTestCases {
if runtime.GOOS == "windows" {
// On Windows, Rename("/path/to/.", x) succeeds,
// because Windows cleans the path to just "/path/to".
// Root.Rename(".", x) fails as expected.
// Don't run this consistency test on Windows.
if test.open == "." || test.open == "./" {
continue
}
}
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
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 {
lstat = r.Lstat
}
otherPath := "other"
if r == nil {
otherPath = filepath.Join(t.TempDir(), otherPath)
}
var srcPath, dstPath string
if name == "from" {
srcPath = path
dstPath = otherPath
} else {
srcPath = otherPath
dstPath = path
}
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)
if err != nil {
t.Errorf("stat(%q) after successful copy: %v", dstPath, err)
return "stat error", err
}
return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
})
}
})
}
}
func TestRootConsistencySymlink(t *testing.T) {
testenv.MustHaveSymlink(t)
for _, test := range rootConsistencyTestCases {
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
const target = "linktarget"
var err error
var got string
if r == nil {
err = os.Symlink(target, path)
got, _ = os.Readlink(target)
} else {
err = r.Symlink(target, path)
got, _ = r.Readlink(target)
}
return got, err
})
}
}
func TestRootRenameAfterOpen(t *testing.T) {
switch runtime.GOOS {
case "windows":
t.Skip("renaming open files not supported on " + runtime.GOOS)
case "js", "plan9":
t.Skip("openat not supported on " + runtime.GOOS)
case "wasip1":
if os.Getenv("GOWASIRUNTIME") == "wazero" {
t.Skip("wazero does not track renamed directories")
}
}
dir := t.TempDir()
// Create directory "a" and open it.
if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil {
t.Fatal(err)
}
dirf, err := os.OpenRoot(filepath.Join(dir, "a"))
if err != nil {
t.Fatal(err)
}
defer dirf.Close()
// Rename "a" => "b", and create "b/f".
if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil {
t.Fatal(err)
}
// Open "f", and confirm that we see it.
f, err := dirf.OpenFile("f", os.O_RDONLY, 0)
if err != nil {
t.Fatalf("reading file after renaming parent: %v", err)
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
t.Fatal(err)
}
if got, want := string(b), "hello"; got != want {
t.Fatalf("file contents: %q, want %q", got, want)
}
// f.Name reflects the original path we opened the directory under (".../a"), not "b".
if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want {
t.Errorf("f.Name() = %q, want %q", got, want)
}
}
func TestRootNonPermissionMode(t *testing.T) {
r, err := os.OpenRoot(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer r.Close()
if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil {
t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error")
}
if err := r.Mkdir("file", 0o1777); err == nil {
t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error")
}
}
func TestRootUseAfterClose(t *testing.T) {
r, err := os.OpenRoot(t.TempDir())
if err != nil {
t.Fatal(err)
}
r.Close()
for _, test := range []struct {
name string
f func(r *os.Root, filename string) error
}{{
name: "Open",
f: func(r *os.Root, filename string) error {
_, err := r.Open(filename)
return err
},
}, {
name: "Create",
f: func(r *os.Root, filename string) error {
_, err := r.Create(filename)
return err
},
}, {
name: "OpenFile",
f: func(r *os.Root, filename string) error {
_, err := r.OpenFile(filename, os.O_RDWR, 0o666)
return err
},
}, {
name: "OpenRoot",
f: func(r *os.Root, filename string) error {
_, err := r.OpenRoot(filename)
return err
},
}, {
name: "Mkdir",
f: func(r *os.Root, filename string) error {
return r.Mkdir(filename, 0o777)
},
}} {
err := test.f(r, "target")
pe, ok := err.(*os.PathError)
if !ok || pe.Path != "target" || pe.Err != os.ErrClosed {
t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err)
}
}
}
func TestRootConcurrentClose(t *testing.T) {
r, err := os.OpenRoot(t.TempDir())
if err != nil {
t.Fatal(err)
}
ch := make(chan error, 1)
go func() {
defer close(ch)
first := true
for {
f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666)
if err != nil {
ch <- err
return
}
if first {
ch <- nil
first = false
}
f.Close()
if runtime.GOARCH == "wasm" {
// TODO(go.dev/issue/71134) can lead to goroutine starvation.
runtime.Gosched()
}
}
}()
if err := <-ch; err != nil {
t.Errorf("OpenFile: %v, want success", err)
}
r.Close()
if err := <-ch; !errors.Is(err, os.ErrClosed) {
t.Errorf("OpenFile: %v, want ErrClosed", err)
}
}
// TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse.
//
// We create a deeply nested directory:
//
// base/a/a/a/a/ [...] /a
//
// And a path that descends into the tree, then returns to the top using ..:
//
// base/a/a/a/a/ [...] /a/../../../ [..] /../a/f
//
// While opening this file, we rename base/a/a to base/b.
// A naive lookup operation will resolve the path to base/f.
func TestRootRaceRenameDir(t *testing.T) {
dir := t.TempDir()
r, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer r.Close()
const depth = 4
os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777)
path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f"
os.WriteFile(dir+"/f", []byte("secret"), 0o666)
os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666)
// Compute how long it takes to open the path in the common case.
const tries = 10
var total time.Duration
for range tries {
start := time.Now()
f, err := r.Open(path)
if err != nil {
t.Fatal(err)
}
b, err := io.ReadAll(f)
if err != nil {
t.Fatal(err)
}
if string(b) != "public" {
t.Fatalf("read %q, want %q", b, "public")
}
f.Close()
total += time.Since(start)
}
avg := total / tries
// We're trying to exploit a race, so try this a number of times.
for range 100 {
// Start a goroutine to open the file.
gotc := make(chan []byte)
go func() {
f, err := r.Open(path)
if err != nil {
gotc <- nil
}
defer f.Close()
b, _ := io.ReadAll(f)
gotc <- b
}()
// Wait for the open operation to partially complete,
// and then rename a directory near the root.
time.Sleep(avg / 4)
if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil {
// Windows and Plan9 won't let us rename a directory if we have
// an open handle for it, so an error here is expected.
switch runtime.GOOS {
case "windows", "plan9":
default:
t.Fatal(err)
}
}
got := <-gotc
os.Rename(dir+"/b", dir+"/base/a")
if len(got) > 0 && string(got) != "public" {
t.Errorf("read file: %q; want error or 'public'", got)
}
}
}
func TestRootSymlinkToRoot(t *testing.T) {
testenv.MustHaveSymlink(t)
dir := makefs(t, []string{
"d/d => ..",
})
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
if err := root.Mkdir("d/d/new", 0777); err != nil {
t.Fatal(err)
}
f, err := root.Open("d/d")
if err != nil {
t.Fatal(err)
}
defer f.Close()
names, err := f.Readdirnames(-1)
if err != nil {
t.Fatal(err)
}
slices.Sort(names)
if got, want := names, []string{"d", "new"}; !slices.Equal(got, want) {
t.Errorf("root contains: %q, want %q", got, want)
}
}
func TestOpenInRoot(t *testing.T) {
testenv.MustHaveSymlink(t)
dir := makefs(t, []string{
"file",
"link => ../ROOT/file",
})
f, err := os.OpenInRoot(dir, "file")
if err != nil {
t.Fatalf("OpenInRoot(`file`) = %v, want success", err)
}
f.Close()
for _, name := range []string{
"link",
"../ROOT/file",
dir + "/file",
} {
f, err := os.OpenInRoot(dir, name)
if err == nil {
f.Close()
t.Fatalf("OpenInRoot(%q) = nil, want error", name)
}
}
}
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`)
}
}
func TestRootWriteReadFile(t *testing.T) {
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
name := "filename"
want := []byte("file contents")
if err := root.WriteFile(name, want, 0o666); err != nil {
t.Fatalf("root.WriteFile(%q, %q, 0o666) = %v; want nil", name, want, err)
}
got, err := root.ReadFile(name)
if err != nil {
t.Fatalf("root.ReadFile(%q) = %q, %v; want %q, nil", name, got, err, want)
}
}
func TestRootName(t *testing.T) {
dir := t.TempDir()
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
if got, want := root.Name(), dir; got != want {
t.Errorf("root.Name() = %q, want %q", got, want)
}
f, err := root.Create("file")
if err != nil {
t.Fatal(err)
}
defer f.Close()
if got, want := f.Name(), filepath.Join(dir, "file"); got != want {
t.Errorf(`root.Create("file").Name() = %q, want %q`, got, want)
}
if err := root.Mkdir("dir", 0o777); err != nil {
t.Fatal(err)
}
subroot, err := root.OpenRoot("dir")
if err != nil {
t.Fatal(err)
}
defer subroot.Close()
if got, want := subroot.Name(), filepath.Join(dir, "dir"); got != want {
t.Errorf(`root.OpenRoot("dir").Name() = %q, want %q`, got, want)
}
}
// TestRootNoLstat verifies that we do not use lstat (possibly escaping the root)
// when reading directories in a Root.
func TestRootNoLstat(t *testing.T) {
if runtime.GOARCH == "wasm" {
t.Skip("wasm lacks fstatat")
}
dir := makefs(t, []string{
"subdir/",
})
const size = 42
contents := strings.Repeat("x", size)
if err := os.WriteFile(dir+"/subdir/file", []byte(contents), 0666); err != nil {
t.Fatal(err)
}
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
test := func(name string, fn func(t *testing.T, f *os.File)) {
t.Run(name, func(t *testing.T) {
os.SetStatHook(t, func(f *os.File, name string) (os.FileInfo, error) {
if f == nil {
t.Errorf("unexpected Lstat(%q)", name)
}
return nil, nil
})
f, err := root.Open("subdir")
if err != nil {
t.Fatal(err)
}
defer f.Close()
fn(t, f)
})
}
checkFileInfo := func(t *testing.T, fi fs.FileInfo) {
t.Helper()
if got, want := fi.Name(), "file"; got != want {
t.Errorf("FileInfo.Name() = %q, want %q", got, want)
}
if got, want := fi.Size(), int64(size); got != want {
t.Errorf("FileInfo.Size() = %v, want %v", got, want)
}
}
checkDirEntry := func(t *testing.T, d fs.DirEntry) {
t.Helper()
if got, want := d.Name(), "file"; got != want {
t.Errorf("DirEntry.Name() = %q, want %q", got, want)
}
if got, want := d.IsDir(), false; got != want {
t.Errorf("DirEntry.IsDir() = %v, want %v", got, want)
}
fi, err := d.Info()
if err != nil {
t.Fatalf("DirEntry.Info() = _, %v", err)
}
checkFileInfo(t, fi)
}
test("Stat", func(t *testing.T, subdir *os.File) {
fi, err := subdir.Stat()
if err != nil {
t.Fatal(err)
}
if !fi.IsDir() {
t.Fatalf(`Open("subdir").Stat().IsDir() = false, want true`)
}
})
// File.ReadDir, returning []DirEntry
test("ReadDirEntry", func(t *testing.T, subdir *os.File) {
dirents, err := subdir.ReadDir(-1)
if err != nil {
t.Fatal(err)
}
if len(dirents) != 1 {
t.Fatalf(`Open("subdir").ReadDir(-1) = {%v}, want {file}`, dirents)
}
checkDirEntry(t, dirents[0])
})
// File.Readdir, returning []FileInfo
test("ReadFileInfo", func(t *testing.T, subdir *os.File) {
fileinfos, err := subdir.Readdir(-1)
if err != nil {
t.Fatal(err)
}
if len(fileinfos) != 1 {
t.Fatalf(`Open("subdir").Readdir(-1) = {%v}, want {file}`, fileinfos)
}
checkFileInfo(t, fileinfos[0])
})
// File.Readdirnames, returning []string
test("Readdirnames", func(t *testing.T, subdir *os.File) {
names, err := subdir.Readdirnames(-1)
if err != nil {
t.Fatal(err)
}
if got, want := names, []string{"file"}; !slices.Equal(got, want) {
t.Fatalf(`Open("subdir").Readdirnames(-1) = %q, want %q`, got, want)
}
})
}
// A rootMultiTest is state for testing an os.Root operation in one configuration among many.
// Each execution of a rootMultiTest varies in several ways:
//
// - With or without an *os.Root, to check consistency between root/non-root operations.
// - With a target that may be a file, directory, symlink, or entirely absent.
// - With various paths referencing the target: "target", "DIR/../target", etc.
// - When the target is a symlink, with various link target paths.
//
// For example, a single test execution might be:
// In an *os.Root, copy "source" to "DIR/../target".
// "source" is a file, and "target" is a symlink to "../ROOT/s_target". "s_target" is a directory.
// (In this case, we expect the test to fail due to the path escape in the symlink.)
type rootMultiTest struct {
// dir is the directory containing the test.
// dir will always contain a directory named "ROOT"
// and a subdir named "ROOT/DIR".
dir string
// root is the *Root for the test. May be nil.
root *os.Root
// source and target are files acted on by the test.
// target is always set; source is only set for tests which request two files.
source testFileDesc
target testFileDesc
// sourcePath and targetPath are the paths which should be used to acceess
// the source/target.
sourcePath string
targetPath string
sourceInfo os.FileInfo
targetInfo os.FileInfo
// op is the operation being performed, used for reporting errors.
op string
}
var testVerbose = flag.Bool("verbose", false, "verbose")
// A rootMultiTest function may return this error to disable
// the check that in-root and out-of-root functions have the same outcome.
var errSkipRootConsistencyCheck = errors.New("skip root consistency check")
// runRootMultiTest runs f in a variety of configurations.
// See above.
func runRootMultiTest(t *testing.T, f func(*testing.T, *rootMultiTest) (string, error)) {
for target := range allTestFileDescs() {
t.Run(target.String(), func(t *testing.T) {
var source testFileDesc // unused
runRootMultiTestDescs(t, source, target, f)
})
}
}
// runRootMultiTest2 runs f in a variety of configurations,
// with both source and target files.
// See above.
func runRootMultiTest2(t *testing.T, f func(*testing.T, *rootMultiTest) (string, error)) {
// A "simple" desc is one which contains only direct references.
// When not running the comprehensive (but slow) set of test variations,
// we only test variations where at least one of source and target is simple.
isSimple := func(desc testFileDesc) bool {
if desc.ref.template != "BASE" {
return false
}
if desc.kind == testFileSymlink && desc.target.ref.template != "BASE" {
return false
}
return true
}
for source := range allTestFileDescs() {
for target := range allTestFileDescs() {
if !*rootComprehensive && !isSimple(source) && !isSimple(target) {
continue
}
name := fmt.Sprintf("%s_to_%s", source, target)
t.Run(name, func(t *testing.T) {
runRootMultiTestDescs(t, source, target, f)
})
}
}
}
// setOp sets the operation performed by the test (logged in errors).
//
// This currently assumes the operation will be a method of os.Root and a function in os
// (e.g., root.Open/os.Open).
func (test *rootMultiTest) setOp(format string, a ...any) {
if test.root != nil {
test.op = "root."
} else {
test.op = "os."
}
test.op += fmt.Sprintf(format, a...)
}
var errAny = errors.New("any error")
func (test *rootMultiTest) errorf(t *testing.T, format string, args ...any) {
t.Errorf("%v:", test.op)
t.Fatalf(" "+format, args...)
}
// wantError tests whether got matches want.
// If want is errAny, got may be any non-nil error.
func (test *rootMultiTest) wantError(t *testing.T, got, want error) {
t.Helper()
if errors.Is(got, want) || (got != nil && want == errAny) {
return
}
t.Fatalf("%v:\ngot error: %v\nwant error: %v", test.op, got, want)
}
func runRootMultiTestDescs(t *testing.T, source, target testFileDesc, f func(*testing.T, *rootMultiTest) (string, error)) {
rootTest := newRootTest(t, source, target, true)
osTest := newRootTest(t, source, target, false)
initialContent := dirTreeContents(t, rootTest.dir)
t.Cleanup(func() {
if t.Failed() {
t.Log("Initial directory contents:")
for _, line := range initialContent {
t.Logf(" %v", line)
}
}
})
rootResult, rootErr := f(t, rootTest)
if runtime.GOOS == "darwin" {
// Darwin appears to have a kernel bug which causes restrictions on paths
// with a trailing / to not be applied during uncached path lookups.
// These restrictions are applied during cached lookups, so the results
// of operating on /-suffixed paths are inconsistent.
//
// An example of this Darwin behavior (as of 25.4.0) is:
// $ mkdir -p test/dir
// $ echo hello > test/file
// $ ln -s dir/../file test/link
// $ cat test/link/
// hello
// $ cat test/link/
// cat: test/link/: Not a directory
//
// Since Darwin isn't consistent with itself, we can't verify that we're
// consistent with it.
if rootTest.source.anySlashSuffix() || rootTest.target.anySlashSuffix() {
return
}
}
if runtime.GOOS == "wasip1" || runtime.GOOS == "js" {
// WASI runtimes don't have any consistent behavior for handling paths with
// a trailing /, so skip consistency tests for these paths.
if rootTest.source.anySlashSuffix() || rootTest.target.anySlashSuffix() {
return
}
}
osResult, osErr := f(t, osTest)
t.Cleanup(func() {
if t.Failed() || !*testVerbose {
return
}
rootContent := dirTreeContents(t, rootTest.dir)
osContent := dirTreeContents(t, osTest.dir)
t.Log("Initial directory contents:")
for _, line := range initialContent {
t.Logf(" %v", line)
}
t.Logf("%v:", rootTest.op)
t.Logf(" result: %v", rootResult)
t.Logf(" error: %v", rootErr)
for _, line := range rootContent {
t.Logf(" %v", line)
}
t.Logf("%v:", osTest.op)
t.Logf(" result: %v", osResult)
t.Logf(" error: %v", osErr)
for _, line := range osContent {
t.Logf(" %v", line)
}
})
if errors.Is(rootErr, os.ErrPathEscapes) {
// os.Root forbids this operation (and is therefore not consistent with
// the non-root version).
return
}
if rootErr == errSkipRootConsistencyCheck || osErr == errSkipRootConsistencyCheck {
return
}
// Consistency check: Performing the same operation in and out of a root
// should produce the same results.
if rootResult != osResult {
t.Errorf("inconsistent results in/out of root")
t.Errorf("%v:", rootTest.op)
t.Errorf(" result: %v", rootResult)
t.Errorf("%v:", osTest.op)
t.Errorf(" result: %v", osResult)
}
if (rootErr == nil) != (osErr == nil) {
t.Errorf("inconsistent errors in/out of root")
t.Errorf("%v:", rootTest.op)
t.Errorf(" error: %v", rootErr)
t.Errorf("%v:", osTest.op)
t.Errorf(" error: %v", osErr)
}
// Filesystem consistency check: Same files in the same places.
rootContent := dirTreeContents(t, rootTest.dir)
osContent := dirTreeContents(t, osTest.dir)
if !slices.Equal(rootContent, osContent) {
t.Errorf("inconsistent filesystem after running in/out of root")
t.Errorf("%v:", rootTest.op)
for _, line := range rootContent {
t.Errorf(" %v", line)
}
t.Errorf("%v:", osTest.op)
for _, line := range osContent {
t.Errorf(" %v", line)
}
}
}
func newRootTest(t *testing.T, source, target testFileDesc, inRoot bool) *rootMultiTest {
dir := makefs(t, []string{
"DIR/",
})
var root *os.Root
if inRoot {
var err error
root, err = os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
root.Close()
})
}
test := &rootMultiTest{
dir: dir,
root: root,
source: source,
target: target,
}
createFile := func(name string, desc testFileDesc) (path string, fi os.FileInfo) {
if desc.kind == testFileUnused {
return "", nil
}
fi = desc.create(t, dir, name, name)
path = desc.ref.path(dir, name)
if !inRoot && !filepath.IsAbs(path) {
path = dir + "/" + path
}
return path, fi
}
test.sourcePath, test.sourceInfo = createFile("source", source)
test.targetPath, test.targetInfo = createFile("target", target)
return test
}
// testFileKind is a kind of file.
type testFileKind int
const (
testFileUnused = testFileKind(iota)
testFileAbsent // file does not exist
testFileFile // regular file
testFileDir // directory
testFileSymlink // symlink
testFileMax
// testFileError represents a path which fails during resolution,
// such as "a/b" where "a" does not exist.
testFileError
)
func (kind testFileKind) String() string {
switch kind {
case testFileUnused:
return "unused"
case testFileAbsent:
return "absent"
case testFileFile:
return "file"
case testFileDir:
return "dir"
case testFileSymlink:
return "symlink"
case testFileError:
return "error"
default:
return fmt.Sprintf("testFileKind(%d)", kind)
}
}
// testFileRef is a kind of reference to a file.
//
// Many path names can refer to the same file: f, ./f, /abs/path/to/f, somedir/../f, etc.
// A testFileRef describes some form of reference.
type testFileRef struct {
// name is the name of the reference (not the file name).
// These are a bit cryptic to keep test names short:
// s (/ slash), p (.. parent), b (base), d (directory), r (root)
name string
// template is a template path.
//
// templates assume that the file is contained in a directory named "ROOT",
// and that "ROOT/DIR" exists and is a directory.
//
// The string BASE in the template may be replaced with the file's basename.
//
// Absolute path templates start with /ROOT.
template string
// escapes indicates whether the path escapes the current directory.
escapes bool
}
var testFileRefs = []testFileRef{
{escapes: false, name: "b", template: "BASE"},
{escapes: false, name: "bs", template: "BASE/"},
{escapes: false, name: "dpb", template: "DIR/../BASE"},
{escapes: false, name: "dpbs", template: "DIR/../BASE/"},
{escapes: true, name: "prb", template: "../ROOT/BASE"},
{escapes: true, name: "prbs", template: "../ROOT/BASE/"},
{escapes: true, name: "srb", template: "/ROOT/BASE"},
{escapes: true, name: "srbs", template: "/ROOT/BASE/"},
}
// testFileLimitedRefs is a smaller set of references which do not exercise path escapes
// (see allTestFileDescs).
var testFileLimitedRefs = testFileRefs[0:2]
// path creates a path using the template.
//
// dir is the absolute path to the root directory (which must be named "ROOT").
// base is the name of the target file within the root directory.
func (ref testFileRef) path(dir, base string) string {
p := ref.template
p = strings.ReplaceAll(p, "BASE", base)
if trim, ok := strings.CutPrefix(p, "/ROOT"); ok {
p = dir + trim
}
return p
}
// hasSlashSuffix reports whether the file reference ends in a /.
func (ref testFileRef) hasSlashSuffix() bool {
return strings.HasSuffix(ref.template, "/")
}
// testFileDesc is a description of a type of file, combining the kind and reference type.
//
// Some sample testFileDescs:
// - "name", a plain file.
// - "DIR/../name", a directory
// - "name/", where name is a symlink to "DIR/../target/", where target is a plain file.
type testFileDesc struct {
kind testFileKind
ref testFileRef
target *testFileDesc // symlink target, nil when kind is not testFileSymlink
}
var rootComprehensive = flag.Bool("root_comprehensive", false,
"run many more os.Root test variations (slow, uncertain value)")
// allTestFileDescs returns an iterator over all the testFileDescs we use in tests.
func allTestFileDescs() iter.Seq[testFileDesc] {
// A testFileDesc contains a reference type ("name", "d/../name", "../r/name", etc.) and
// a file kind (file, directory, symlink, etc.).
//
// When the kind is symlink, the desc contains a reference type and file kind for
// the link target as well. We only exercise one level of symlink (although we
// could do more), so this means a testFileDesc effectively contains four axes of
// variation: ref, kind, symlink ref, symlink kind.
//
// For example:
//
// - "name" is a file
// - "d/../name" is a directory
// - "name" is a symlink to "name2" which is a file
// - "d/../name" is a symlink to "d/../name2" which is a directory
// - etc.
//
// It is feasible to test every possible variation of these four axes,
// but this is quite a few tests and gets quite slow. So by default we exclude
// some variations. We test:
//
// - every reference to every kind, except symlink
// - direct and direct/ references to a symlink to every reference to a file
// - a direct reference to a symlink to a direct reference to every kind (except file)
//
// The full set of variations may be enabled with the -comprehensive_root_tests flag.
return func(yield func(testFileDesc) bool) {
// Every type of reference to every type of file, except symlink.
for _, ref := range testFileRefs {
for kind := range testFileMax {
if kind == testFileUnused || kind == testFileSymlink {
continue
}
desc := testFileDesc{
kind: kind,
ref: ref,
}
if !yield(desc) {
return
}
}
}
// Unless we're being comprehensive, only direct references to symlinks.
refs := testFileRefs
if !*rootComprehensive {
refs = testFileLimitedRefs
}
for _, ref := range refs {
for linkKind := range testFileMax {
if linkKind == testFileUnused || linkKind == testFileSymlink {
continue
}
linkRefs := testFileRefs
if !*rootComprehensive && linkKind != testFileFile && linkKind != testFileDir {
linkRefs = testFileLimitedRefs
}
for _, linkRef := range linkRefs {
desc := testFileDesc{
kind: testFileSymlink,
ref: ref,
target: &testFileDesc{
kind: linkKind,
ref: linkRef,
},
}
if !yield(desc) {
return
}
}
}
}
}
}
// String returns the target name.
//
// These are somewhat cryptic to keep test names short.
// For example, "bsSdpbD" is:
//
// bs - "BASE/"
// S - symlink
// dpb - "DIR/../BASE"
// D - directory
//
// So, open "file1/", where file1 is a symlink to "DIR/../file2", where file2 is a directory.
func (desc testFileDesc) String() string {
s := desc.ref.name + strings.ToUpper(desc.kind.String()[:1])
if desc.kind == testFileSymlink {
s += desc.target.String()
}
return s
}
// escapes reports whether accessing this file escapes the root,
// either because the file name escapes or because some element of a symlink chain escapes.
func (desc testFileDesc) escapes() bool {
if desc.ref.escapes {
return true
}
if desc.kind == testFileSymlink {
return desc.target.escapes()
}
return false
}
func (desc testFileDesc) lescapes() bool {
if desc.ref.escapes {
return true
}
if runtime.GOOS == "windows" {
// On POSIX filesystems, a trailing slash at the end of a path causes
// symlinks in the last path component to be resolved.
// On Windows, a trailing slash does not cause symlink resolution.
return false
}
if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink {
return desc.target.escapes()
}
return false
}
// finalKind reports the kind of the file after following all symlinks.
func (desc testFileDesc) finalKind() testFileKind {
if desc.kind == testFileSymlink {
return desc.target.finalKind()
}
return desc.kind
}
func (desc testFileDesc) lfinalKind() testFileKind {
switch runtime.GOOS {
case "windows":
if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink && desc.target.kind != testFileDir {
return testFileError
}
default:
if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink {
return desc.target.finalKind()
}
}
return desc.kind
}
func (desc testFileDesc) isError() bool {
if runtime.GOOS == "js" {
return false
}
var isError func(desc testFileDesc, hasSuffix bool) bool
isError = func(desc testFileDesc, hasSuffix bool) bool {
if desc.ref.escapes {
return false
}
if desc.ref.hasSlashSuffix() {
hasSuffix = true
}
switch desc.kind {
case testFileDir:
return false
case testFileSymlink:
if runtime.GOOS == "windows" && hasSuffix && desc.target.kind != testFileDir {
return true
}
return isError(*desc.target, hasSuffix)
default:
return hasSuffix
}
}
return isError(desc, false)
}
func (desc testFileDesc) isSymlinkToDir() bool {
if desc.kind != testFileSymlink {
return false
}
if desc.ref.escapes {
return false
}
if desc.finalKind() == testFileDir {
return true
}
return false
}
// anySlashSuffix reports whether any of the names in the file
// (either the initial name, or a symlink target)
// include a trailing /.
func (desc testFileDesc) anySlashSuffix() bool {
name := desc.ref.template
if len(name) > 0 && os.IsPathSeparator(name[len(name)-1]) {
return true
}
if desc.kind == testFileSymlink {
return desc.target.anySlashSuffix()
}
return false
}
// anySlashSuffix reports whether the name of the file includes a trailing /.
func (desc testFileDesc) slashSuffix() bool {
name := desc.ref.template
if len(name) > 0 && os.IsPathSeparator(name[len(name)-1]) {
return true
}
return false
}
// create creates the file(s) for this descriptor.
//
// dir is the test root directory.
// base is the base name of the file we will open within the root.
// (If there are symlinks, base is the start of the symlink chain.)
//
// Tests may create, delete, or move files, which makes it useful to have a way to identify
// and track the files that existed at the start of the test. The token parameter identifies
// which file we're creating. When symlinks are involved, the token is used in creating the
// final, non-symlink file.
func (desc testFileDesc) create(t *testing.T, dir, base, token string) (fi os.FileInfo) {
path := filepath.Join(dir, base)
switch desc.kind {
case testFileAbsent:
// File does not exist.
case testFileFile:
// Regular file. We use the token as the file contents.
if err := os.WriteFile(path, []byte(token), 0o666); err != nil {
t.Fatal(err)
}
case testFileDir:
// Directory. We create a subdir within the directory named "c_"+token.
// (The "c_" prefix is to distinguish this subdir from any files that may
// have the same name as the token.)
if err := os.Mkdir(path, 0o777); err != nil {
t.Fatal(err)
}
case testFileSymlink:
// Symlink. We create a symlink target named "s_"+base.
if runtime.GOOS == "plan9" {
t.Skip("symlinks not supported on " + runtime.GOOS)
}
linktarget := desc.target.ref.path(dir, "s_"+base)
if runtime.GOOS == "wasip1" && filepath.IsAbs(linktarget) {
t.Skip("absolute link targets not supported on " + runtime.GOOS)
}
fi = desc.target.create(t, dir, "s_"+base, token)
if err := os.Symlink(linktarget, path); err != nil {
t.Fatal(err)
}
default:
t.Fatalf("can't create file of kind: %v", desc.kind)
}
if desc.kind == testFileFile || desc.kind == testFileDir {
var err error
fi, err = os.Lstat(path)
if err != nil {
t.Fatal(err)
}
}
return fi
}
// testRootDescribeFile returns a string identifying a file.
//
// It returns "" if f is nil.
// It returns "source" or "target" if f is the source or target file in the test.
// Otherwise, it returns "unknown file".
func (test *rootMultiTest) describeFile(t *testing.T, f *os.File) string {
if f == nil {
return ""
}
fi, err := f.Stat()
if err != nil {
t.Fatal(err)
}
switch {
case os.SameFile(fi, test.sourceInfo):
return "source"
case os.SameFile(fi, test.targetInfo):
return "target"
default:
return "unknown file"
}
}
// dirTreeContents returns a description of the contents of directory.
// For example:
//
// drwxrwxrwx dir/
// -rw-rw-rw- dir/file "file contents"
// Lrw-rw-rw- symlink => dir/file
func dirTreeContents(t *testing.T, dir string) (contents []string) {
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatal(err)
}
defer root.Close()
fs.WalkDir(root.FS(), ".", func(path string, d fs.DirEntry, err error) error {
if path == "." {
return nil
}
info, err := d.Info()
if err != nil {
t.Fatal(err)
}
ent := info.Mode().String() + " " + path
switch d.Type() {
case fs.ModeDir:
ent += "/"
case fs.ModeSymlink:
target, err := root.Readlink(path)
if err != nil {
t.Fatal(err)
}
if filepath.IsAbs(target) {
relPath, err := filepath.Rel(dir, target)
if err == nil && filepath.IsLocal(relPath) {
target = "/.../" + relPath
}
}
ent += " => " + target
default:
f, err := root.Open(path)
if err != nil {
ent += " (unreadable)"
} else {
content, err := io.ReadAll(f)
if err != nil {
t.Fatal(err)
}
ent += fmt.Sprintf(" %q", content)
}
}
contents = append(contents, ent)
return nil
})
return contents
}
// TestRootMultiOpen tests os.Root.Open.
//
// This also serves as a prototypical example of using rootMultiTest
// (see also the doc comment on rootMultiTest above).
func TestRootMultiOpen(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
// This function will be run many times, with different inputs:
// - in and out of a Root
// - opening a file, directory, symlink, or nothing at all
// - opening various names: target, DIR/../target, /abs/path/to/target, etc.
//
// The test function should perform the requested operation
// (for example: open "target" in a Root),
// verify that the result is consistent with expectations,
// and then return a description of the result.
//
// The returned description is used to validate consistent behavior
// between operations in and out of a Root.
var open = os.Open
if test.root != nil {
open = test.root.Open
}
test.setOp("Open(%q)", test.targetPath) // test's operation, for errors
f, gotErr := open(test.targetPath)
if gotErr == nil {
defer f.Close()
}
// testRootDescribeFile returns a string identifying a file.
//
// This is always "source" or "target" for the source/target files in a test,
// or "" if f is nil.
// (Note that most tests use only a target file, no source.)
got := test.describeFile(t, f)
switch {
case test.root != nil && test.target.escapes():
// The operation escapes the root.
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.finalKind() == testFileAbsent:
// The file does not exist ("absent").
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
// The file name or a symlink target contain a trailing slash.
// Trailing slashes are handled differently on different platforms,
// so we won't try to assert an outcome when they are present.
// runRootMultiTest will verify that root.Open and os.Open
// produce consistent results.
default:
// We should have successfully opened the file.
test.wantError(t, gotErr, nil)
if want := "target"; got != want {
t.Fatalf("opened file %q, want %q", got, want)
}
}
// Return the name of the file opened (possibly "" for nothing) and the error.
// runRootMultiTest will compare the results for in-a-root and out-of-a-root
// to validate that they are the same.
return got, gotErr
})
}
func TestRootMultiChmod(t *testing.T) {
if runtime.GOOS == "wasip1" {
t.Skip("Chmod not supported on " + runtime.GOOS)
}
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var (
chmod = os.Chmod
stat = os.Stat
lstat = os.Lstat
)
if test.root != nil {
chmod = test.root.Chmod
stat = test.root.Stat
lstat = test.root.Lstat
}
// Using the wrong mode here can cause problems during test cleanup,
// if we leave a temp dir with a mode that prevents listing or removing
// its contents.
//
// read+execute permissions let us list directory contents,
// and we restore writability before deleting the temp dir.
wantMode := os.FileMode(0o500) // readable, executable
if runtime.GOOS == "windows" {
// On Windows, the only modes we support are the default (777/rwx)
// or read-only (444/r-x). Making a directory read-only doesn't prevent
// listing its contents, so we can use 444 here.
wantMode = 0o444 // readable
}
t.Cleanup(func() {
chmod(test.targetPath, 0o700)
})
test.setOp("Chmod(%q, %o)", test.targetPath, wantMode)
gotErr := chmod(test.targetPath, wantMode)
escapes := test.target.escapes()
targetKind := test.target.finalKind()
if runtime.GOOS == "windows" {
// On Windows, Chmod("symlink") affects the link, not its target.
// See issue #71492.
stat = lstat
escapes = test.target.ref.escapes
targetKind = test.target.kind
}
var gotMode fs.FileMode
switch {
case test.root != nil && escapes:
test.wantError(t, gotErr, os.ErrPathEscapes)
case targetKind == testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
// Don't expect anything, just be consistent with the OS.
default:
test.wantError(t, gotErr, nil)
fi, err := stat(test.targetPath)
if err != nil {
t.Fatalf("could not stat target: %v", err)
}
if runtime.GOOS == "windows" && !fi.Mode().IsRegular() {
// See issue #71492.
break
}
gotMode = fi.Mode() & fs.ModePerm
if gotMode != wantMode {
t.Fatalf("file %q:\ngot mode: %v\nwant mode: %v", test.targetPath, gotMode, wantMode)
}
}
if runtime.GOOS == "windows" && test.root == nil && gotErr != nil {
// On Windows, os.Chmod calls GetFileAttributes on the target.
// This seems to fail in a number of situations where the os.Root
// chmod path works. For now, just skip the consistency check
// when os.Chmod fails.
return "", errSkipRootConsistencyCheck
}
return gotMode.String(), gotErr
})
}
func TestRootMultiCreate(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var create = os.Create
if test.root != nil {
create = test.root.Create
}
test.setOp("Create(%q)", test.targetPath) // test's operation, for errors
f, gotErr := create(test.targetPath)
if gotErr == nil {
defer f.Close()
}
switch {
case test.target.isError():
test.wantError(t, gotErr, errAny)
case runtime.GOOS == "windows" && test.target.isSymlinkToDir():
// The error here is because the link is a Windows directory link,
// not because the link target is a directory.
test.wantError(t, gotErr, errAny)
case test.root != nil && test.target.escapes():
// The operation escapes the root.
test.wantError(t, gotErr, os.ErrPathEscapes)
default:
}
return "", gotErr
})
}
func TestRootMultiLink(t *testing.T) {
if runtime.GOOS == "wasip1" {
switch os.Getenv("GOWASIRUNTIME") {
case "", "wasmtime":
// This test fails when run with wasmtime, because os.RemoveAll fails
// to remove the test tempdir.
t.Skip("test seems to tickle a wasmtime bug")
}
}
testenv.MustHaveLink(t)
runRootMultiTest2(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var (
rename = os.Link
)
if test.root != nil {
rename = test.root.Link
}
test.setOp("Link(%q, %q)", test.sourcePath, test.targetPath)
gotErr := rename(test.sourcePath, test.targetPath)
switch {
case test.root != nil && test.source.lescapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.source.lfinalKind() == testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.source.kind == testFileSymlink:
// os.Link(old, new) may or may not deference old when it is a symlink.
// POSIX says that link(2) should deference the source, but implementations
// are inconsistent.
return "", errSkipRootConsistencyCheck
case test.source.slashSuffix() && test.source.lfinalKind() != testFileDir:
test.wantError(t, gotErr, errAny)
}
return "", gotErr
})
}
func TestRootMultiLstat(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var (
lstat = os.Lstat
)
if test.root != nil {
lstat = test.root.Lstat
}
test.setOp("Lstat(%q)", test.targetPath)
gotStat, gotErr := lstat(test.targetPath)
result := ""
if gotStat != nil {
result = gotStat.Mode().String()
}
escapes := test.target.lescapes()
finalKind := test.target.lfinalKind()
if runtime.GOOS == "windows" && test.target.ref.hasSlashSuffix() {
// When the target of lstat has a trailing slash,
// Windows follows it.
escapes = test.target.escapes()
finalKind = test.target.finalKind()
}
switch {
case test.root != nil && escapes:
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.kind == testFileAbsent:
// Target does not exist.
test.wantError(t, gotErr, errAny)
case finalKind == testFileSymlink:
test.wantError(t, gotErr, nil)
if got, want := gotStat.Mode().Type(), fs.ModeSymlink; got != want {
test.errorf(t, "got mode %v, want %v", got, want)
}
case gotErr != nil:
default:
if !os.SameFile(gotStat, test.targetInfo) {
test.errorf(t, "stat result is not for target file; want it to be")
}
}
return result, gotErr
})
}
func TestRootMultiMkdir(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var (
mkdir = os.Mkdir
stat = os.Stat
)
if test.root != nil {
mkdir = test.root.Mkdir
stat = test.root.Stat
}
test.setOp("Mkdir(%q, 0o777)", test.targetPath)
gotErr := mkdir(test.targetPath, 0o777)
switch {
case test.root != nil && test.target.ref.escapes:
// "mkdir ../target", or equivalent escaping path.
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.slashSuffix() && test.target.kind == testFileSymlink:
// "mkdir symlink/", inconsistent behavior across platforms
// as to whether this follows the symlink or not.
//
// If the symlink escapes, this needs to be some kind of error though.
if test.root != nil && test.target.escapes() {
test.wantError(t, gotErr, errAny)
}
if runtime.GOOS == "openbsd" {
// Known inconsistency: OpenBSD doesn't resolve the final
// symlink when creating a directory.
return "", errSkipRootConsistencyCheck
}
case test.target.kind != testFileAbsent:
// "mkdir target", where target exists.
test.wantError(t, gotErr, errAny)
default:
test.wantError(t, gotErr, nil)
fi, err := stat(test.targetPath)
if err != nil {
t.Fatalf("could not stat target: %v", err)
}
if !fi.IsDir() {
t.Fatalf("%q: not a directory, expected it to be", test.targetPath)
}
}
return "", gotErr
})
}
func TestRootMultiMkdirAllShallow(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
return testRootMultiMkdirAll(t, test, test.targetPath)
})
}
func TestRootMultiMkdirAllDeep(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
targetPath := test.targetPath
if len(targetPath) > 0 && os.IsPathSeparator(targetPath[len(targetPath)-1]) {
targetPath += "a/b/"
} else {
targetPath += "/a/b"
}
return testRootMultiMkdirAll(t, test, targetPath)
})
}
func testRootMultiMkdirAll(t *testing.T, test *rootMultiTest, targetPath string) (string, error) {
var mkdirAll = os.MkdirAll
if test.root != nil {
mkdirAll = test.root.MkdirAll
}
test.setOp("MkdirAll(%q, 0o777)", targetPath)
gotErr := mkdirAll(targetPath, 0o777)
switch {
case test.root != nil && test.target.lescapes():
// "mkdir ../target", or equivalent escaping path.
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.root != nil && test.target.escapes():
// "mkdir ../target", or equivalent escaping path.
test.wantError(t, gotErr, errAny)
return "", errSkipRootConsistencyCheck
case test.root != nil && test.target.kind == testFileSymlink && test.target.target.kind == testFileAbsent && targetPath != test.targetPath:
// A minor inconsistency between Root.MkdirAll and os.MkdirAll:
// When an intermediate component of the tree being constructed is a
// dangling symlink, Root.MkdirAll will follow the symlink and create
// its target directory, while os.MkdirAll will fail with an error.
return "", errSkipRootConsistencyCheck
default:
}
return "", gotErr
}
func TestRootMultiRename(t *testing.T) {
if runtime.GOOS == "wasip1" {
switch os.Getenv("GOWASIRUNTIME") {
case "", "wasmtime":
// This test fails when run with wasmtime, because os.RemoveAll fails
// to remove the test tempdir.
t.Skip("test seems to tickle a wasmtime bug")
}
}
runRootMultiTest2(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var (
rename = os.Rename
)
if test.root != nil {
rename = test.root.Rename
}
// TODO: target directory (if any) should be empty
test.setOp("Rename(%q, %q)", test.sourcePath, test.targetPath)
gotErr := rename(test.sourcePath, test.targetPath)
if runtime.GOOS == "windows" &&
(test.source.finalKind() != test.target.finalKind() || test.source.kind == testFileSymlink || test.target.kind == testFileSymlink) {
// os.Rename on Windows is implemented using MoveFileEx,
// while Root.Rename is implemented using NtSetInformationFileEx
// with an explicit request for POSIX semantics.
//
// This means the two do not behave the same when renaming
// a file onto a directory or vice-versa.
//
// We should make this consistent, but for now just skip
// the consistency checks in this case.
return "", errSkipRootConsistencyCheck
}
switch {
case test.root != nil && test.source.lescapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.source.lfinalKind() == testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.source.slashSuffix() && test.source.lfinalKind() != testFileDir && runtime.GOOS != "js":
test.wantError(t, gotErr, errAny)
case test.root != nil && test.target.lescapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case runtime.GOOS == "plan9":
// Plan9 rename behaves differently.
// Just rely on consistency checks.
case test.target.lfinalKind() == testFileDir:
// POSIX rename() will replace an empty target directory,
// but os.Rename will not.
test.wantError(t, gotErr, errAny)
case test.source.lfinalKind() == testFileDir && test.target.lfinalKind() != testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.source.anySlashSuffix() || test.target.anySlashSuffix():
if runtime.GOOS == "openbsd" {
// Known inconsistency: OpenBSD doesn't resolve the final
// symlink when creating a directory.
return "", errSkipRootConsistencyCheck
}
default:
test.wantError(t, gotErr, nil)
// TODO: check that the file is in its new location
}
if runtime.GOOS == "linux" && (test.source.slashSuffix() || test.target.slashSuffix()) {
return "", errSkipRootConsistencyCheck
}
return "", gotErr
})
}
func TestRootMultiReadFile(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var readFile = os.ReadFile
if test.root != nil {
readFile = test.root.ReadFile
}
test.setOp("ReadFile(%q)", test.targetPath)
data, gotErr := readFile(test.targetPath)
var got string
if gotErr == nil {
got = string(data)
}
switch {
case test.root != nil && test.target.escapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.finalKind() == testFileAbsent:
test.wantError(t, gotErr, errAny)
case runtime.GOOS == "plan9":
// Plan9 lets you read from directories.
// Just rely on consistency checks.
case test.target.finalKind() == testFileDir:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
// Trailing slashes are handled differently on different platforms,
// so we won't try to assert an outcome when they are present.
// runRootMultiTest will verify that root.ReadFile and os.ReadFile
// produce consistent results.
default:
test.wantError(t, gotErr, nil)
if want := "target"; got != want {
t.Fatalf("read file content %q, want %q", got, want)
}
}
return got, gotErr
})
}
func TestRootMultiStat(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var stat = os.Stat
if test.root != nil {
stat = test.root.Stat
}
test.setOp("Stat(%q)", test.targetPath)
gotStat, gotErr := stat(test.targetPath)
switch {
case test.target.isError():
test.wantError(t, gotErr, errAny)
case test.root != nil && test.target.escapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.finalKind() == testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
default:
test.wantError(t, gotErr, nil)
if !os.SameFile(gotStat, test.targetInfo) {
test.errorf(t, "stat result is not for target file; want it to be")
}
}
return "", gotErr
})
}
func TestRootMultiRemove(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var remove = os.Remove
if test.root != nil {
remove = test.root.Remove
}
test.setOp("Remove(%q)", test.targetPath)
gotErr := remove(test.targetPath)
switch {
case test.root != nil && test.target.lescapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.kind == testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
if runtime.GOOS == "linux" {
// Linux treats rmdir("symlink/") as an error when
// "symlink" is a symlink to a directory.
// Root.Remove prefers the POSIX interpretation
// of resolving the symlink.
return "", errSkipRootConsistencyCheck
}
default:
test.wantError(t, gotErr, nil)
}
return "", gotErr
})
}
func TestRootMultiRemoveAll(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var removeAll = os.RemoveAll
if test.root != nil {
removeAll = test.root.RemoveAll
}
test.setOp("RemoveAll(%q)", test.targetPath)
gotErr := removeAll(test.targetPath)
switch {
case test.root != nil && test.target.ref.escapes:
// This is only checking target.ref.escapes,
// not target.lescapes(), because RemoveAll strips
// terminal slashes.
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.anySlashSuffix():
// We are inconsistent on some platforms on whether
// RemoveAll("symlink/") removes the link or the link target.
// Something worth addressing, but for now skip the check.
return "", errSkipRootConsistencyCheck
default:
test.wantError(t, gotErr, nil)
}
return "", gotErr
})
}
func TestRootMultiChtimes(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var chtimes = os.Chtimes
if test.root != nil {
chtimes = test.root.Chtimes
}
now := time.Now()
test.setOp("Chtimes(%q, %v, %v)", test.targetPath, now, now)
gotErr := chtimes(test.targetPath, now, now)
switch {
case test.target.isError():
test.wantError(t, gotErr, errAny)
case test.root != nil && test.target.escapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.finalKind() == testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
default:
test.wantError(t, gotErr, nil)
}
return "", gotErr
})
}
func TestRootMultiReadlink(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var readlink = os.Readlink
if test.root != nil {
readlink = test.root.Readlink
}
test.setOp("Readlink(%q)", test.targetPath)
got, gotErr := readlink(test.targetPath)
if suffix, ok := strings.CutPrefix(got, test.dir); ok {
// Replace absolute path prefix with /.../
got = "/..." + suffix
}
switch {
case test.root != nil && test.target.lescapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.kind != testFileSymlink:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
default:
test.wantError(t, gotErr, nil)
}
return got, gotErr
})
}
func TestRootMultiWriteFile(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var writeFile = os.WriteFile
if test.root != nil {
writeFile = test.root.WriteFile
}
test.setOp("WriteFile(%q, ...)", test.targetPath)
gotErr := writeFile(test.targetPath, []byte("data"), 0o666)
switch {
case test.target.isError():
test.wantError(t, gotErr, errAny)
case runtime.GOOS == "windows" && test.target.isSymlinkToDir():
test.wantError(t, gotErr, errAny)
case test.root != nil && test.target.escapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.finalKind() == testFileDir:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
default:
test.wantError(t, gotErr, nil)
}
return "", gotErr
})
}
func TestRootMultiOpenFile(t *testing.T) {
runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) {
var openFile = os.OpenFile
if test.root != nil {
openFile = test.root.OpenFile
}
test.setOp("OpenFile(%q, O_RDONLY, 0)", test.targetPath)
f, gotErr := openFile(test.targetPath, os.O_RDONLY, 0)
if gotErr == nil {
defer f.Close()
}
got := test.describeFile(t, f)
switch {
case test.root != nil && test.target.escapes():
test.wantError(t, gotErr, os.ErrPathEscapes)
case test.target.finalKind() == testFileAbsent:
test.wantError(t, gotErr, errAny)
case test.target.anySlashSuffix():
default:
test.wantError(t, gotErr, nil)
if want := "target"; got != want {
t.Fatalf("opened file %q, want %q", got, want)
}
}
return got, gotErr
})
}