| // 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" | |
| "fmt" | |
| "internal/testenv" | |
| "io" | |
| "io/fs" | |
| "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 := path.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: "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{}, | |
| 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 !wantError { | |
| fi, err := os.Lstat(filepath.Join(root.Name(), test.open)) | |
| if err == nil && fi.Mode().Type() == fs.ModeSymlink { | |
| // This case is trying to mkdir("some symlink"), | |
| // which is an error. | |
| 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 { | |
| test.run(t, func(t *testing.T, target string, root *os.Root) { | |
| wantError := test.wantError | |
| if !wantError { | |
| fi, err := os.Lstat(filepath.Join(root.Name(), test.open)) | |
| if err == nil && fi.Mode().Type() == fs.ModeSymlink { | |
| // This case is trying to mkdir("some symlink"), | |
| // which is an error. | |
| wantError = true | |
| } | |
| } | |
| err := root.Mkdir(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) { | |
| 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) | |
| } | |
| 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/", | |
| }, { | |
| name: "symlink slash dot", | |
| fs: []string{ | |
| "target/file", | |
| "link => target", | |
| }, | |
| open: "link/.", | |
| }, { | |
| name: "file symlink slash", | |
| fs: []string{ | |
| "target", | |
| "link => target", | |
| }, | |
| open: "link/", | |
| detailedErrorMismatch: func(t *testing.T) bool { | |
| // os.Create returns ENOTDIR or EISDIR depending on the platform. | |
| return runtime.GOOS == "js" | |
| }, | |
| }, { | |
| 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 runtime.GOOS == "windows" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") { | |
| // Root.RemoveAll notices that a/ is not a directory, | |
| // and returns success. | |
| // os.RemoveAll tries to open a/ and fails because | |
| // it is not a regular file. | |
| // The inconsistency here isn't worth fixing, so just skip this test. | |
| t.Skip("known inconsistency on windows") | |
| } | |
| }, | |
| }, { | |
| name: "question mark", | |
| open: "?", | |
| }, { | |
| 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) | |
| } | |
| }) | |
| } |