| // 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 |
| }) |
| } |