// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package os_test

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"slices"
	"strings"
	"testing"
	"time"
)

// testMaybeRooted calls f in two subtests,
// one with a Root and one with a nil r.
func testMaybeRooted(t *testing.T, f func(t *testing.T, r *os.Root)) {
	t.Run("NoRoot", func(t *testing.T) {
		t.Chdir(t.TempDir())
		f(t, nil)
	})
	t.Run("InRoot", func(t *testing.T) {
		t.Chdir(t.TempDir())
		r, err := os.OpenRoot(".")
		if err != nil {
			t.Fatal(err)
		}
		defer r.Close()
		f(t, r)
	})
}

// makefs creates a test filesystem layout and returns the path to its root.
//
// Each entry in the slice is a file, directory, or symbolic link to create:
//
//   - "d/": directory d
//   - "f": file f with contents f
//   - "a => b": symlink a with target b
//
// The directory containing the filesystem is always named ROOT.
// $ABS is replaced with the absolute path of the directory containing the filesystem.
//
// Parent directories are automatically created as needed.
//
// makefs calls t.Skip if the layout contains features not supported by the current GOOS.
func makefs(t *testing.T, fs []string) string {
	root := path.Join(t.TempDir(), "ROOT")
	if err := os.Mkdir(root, 0o777); err != nil {
		t.Fatal(err)
	}
	for _, ent := range fs {
		ent = strings.ReplaceAll(ent, "$ABS", root)
		base, link, isLink := strings.Cut(ent, " => ")
		if isLink {
			if runtime.GOOS == "wasip1" && path.IsAbs(link) {
				t.Skip("absolute link targets not supported on " + runtime.GOOS)
			}
			if runtime.GOOS == "plan9" {
				t.Skip("symlinks not supported on " + runtime.GOOS)
			}
			ent = base
		}
		if err := os.MkdirAll(path.Join(root, path.Dir(base)), 0o777); err != nil {
			t.Fatal(err)
		}
		if isLink {
			if err := os.Symlink(link, path.Join(root, base)); err != nil {
				t.Fatal(err)
			}
		} else if strings.HasSuffix(ent, "/") {
			if err := os.MkdirAll(path.Join(root, ent), 0o777); err != nil {
				t.Fatal(err)
			}
		} else {
			if err := os.WriteFile(path.Join(root, ent), []byte(ent), 0o666); err != nil {
				t.Fatal(err)
			}
		}
	}
	return root
}

// 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) {
		root := makefs(t, test.fs)
		d, err := os.OpenRoot(root)
		if err != nil {
			t.Fatal(err)
		}
		defer d.Close()
		// The target is a file that will be accessed,
		// or a file that should not be accessed
		// (because doing so escapes the root).
		target := test.target
		if test.target != "" {
			target = filepath.Join(root, test.target)
		}
		f(t, target, d)
	})
}

// errEndsTest checks the error result of a test,
// verifying that it succeeded or failed as expected.
//
// It returns true if the test is done due to encountering an expected error.
// false if the test should continue.
func errEndsTest(t *testing.T, err error, wantError bool, format string, args ...any) bool {
	t.Helper()
	if wantError {
		if err == nil {
			op := fmt.Sprintf(format, args...)
			t.Fatalf("%v = nil; want error", op)
		}
		return true
	} else {
		if err != nil {
			op := fmt.Sprintf(format, args...)
			t.Fatalf("%v = %v; want success", op, err)
		}
		return false
	}
}

var rootTestCases = []rootTest{{
	name:   "plain path",
	fs:     []string{},
	open:   "target",
	target: "target",
}, {
	name: "path in directory",
	fs: []string{
		"a/b/c/",
	},
	open:   "a/b/c/target",
	target: "a/b/c/target",
}, {
	name: "symlink",
	fs: []string{
		"link => target",
	},
	open:    "link",
	target:  "target",
	ltarget: "link",
}, {
	name: "symlink dotdot slash",
	fs: []string{
		"link => ../",
	},
	open:      "link",
	ltarget:   "link",
	wantError: true,
}, {
	name: "symlink ending in slash",
	fs: []string{
		"dir/",
		"link => dir/",
	},
	open:   "link/target",
	target: "dir/target",
}, {
	name: "symlink dotdot dotdot slash",
	fs: []string{
		"dir/link => ../../",
	},
	open:      "dir/link",
	ltarget:   "dir/link",
	wantError: true,
}, {
	name: "symlink chain",
	fs: []string{
		"link => a/b/c/target",
		"a/b => e",
		"a/e => ../f",
		"f => g/h/i",
		"g/h/i => ..",
		"g/c/",
	},
	open:    "link",
	target:  "g/c/target",
	ltarget: "link",
}, {
	name: "path with dot",
	fs: []string{
		"a/b/",
	},
	open:   "./a/./b/./target",
	target: "a/b/target",
}, {
	name: "path with dotdot",
	fs: []string{
		"a/b/",
	},
	open:   "a/../a/b/../../a/b/../b/target",
	target: "a/b/target",
}, {
	name:      "path with dotdot slash",
	fs:        []string{},
	open:      "../",
	wantError: true,
}, {
	name:      "path with dotdot dotdot slash",
	fs:        []string{},
	open:      "a/../../",
	wantError: true,
}, {
	name: "dotdot no symlink",
	fs: []string{
		"a/",
	},
	open:   "a/../target",
	target: "target",
}, {
	name: "dotdot after symlink",
	fs: []string{
		"a => b/c",
		"b/c/",
	},
	open: "a/../target",
	target: func() string {
		if runtime.GOOS == "windows" {
			// On Windows, the path is cleaned before symlink resolution.
			return "target"
		}
		return "b/target"
	}(),
}, {
	name: "dotdot before symlink",
	fs: []string{
		"a => b/c",
		"b/c/",
	},
	open:   "b/../a/target",
	target: "b/c/target",
}, {
	name: "symlink ends in dot",
	fs: []string{
		"a => b/.",
		"b/",
	},
	open:   "a/target",
	target: "b/target",
}, {
	name:        "directory does not exist",
	fs:          []string{},
	open:        "a/file",
	wantError:   true,
	alwaysFails: true,
}, {
	name:        "empty path",
	fs:          []string{},
	open:        "",
	wantError:   true,
	alwaysFails: true,
}, {
	name: "symlink cycle",
	fs: []string{
		"a => a",
	},
	open:        "a",
	ltarget:     "a",
	wantError:   true,
	alwaysFails: true,
}, {
	name:      "path escapes",
	fs:        []string{},
	open:      "../ROOT/target",
	target:    "target",
	wantError: true,
}, {
	name: "long path escapes",
	fs: []string{
		"a/",
	},
	open:      "a/../../ROOT/target",
	target:    "target",
	wantError: true,
}, {
	name: "absolute symlink",
	fs: []string{
		"link => $ABS/target",
	},
	open:      "link",
	ltarget:   "link",
	target:    "target",
	wantError: true,
}, {
	name: "relative symlink",
	fs: []string{
		"link => ../ROOT/target",
	},
	open:      "link",
	target:    "target",
	ltarget:   "link",
	wantError: true,
}, {
	name: "symlink chain escapes",
	fs: []string{
		"link => a/b/c/target",
		"a/b => e",
		"a/e => ../../ROOT",
		"c/",
	},
	open:      "link",
	target:    "c/target",
	ltarget:   "link",
	wantError: true,
}}

func TestRootOpen_File(t *testing.T) {
	want := []byte("target")
	for _, test := range rootTestCases {
		test.run(t, func(t *testing.T, target string, root *os.Root) {
			if target != "" {
				if err := os.WriteFile(target, want, 0o666); err != nil {
					t.Fatal(err)
				}
			}
			f, err := root.Open(test.open)
			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
				return
			}
			defer f.Close()
			got, err := io.ReadAll(f)
			if err != nil || !bytes.Equal(got, want) {
				t.Errorf(`Dir.Open(%q): read content %q, %v; want %q`, test.open, string(got), err, string(want))
			}
		})
	}
}

func TestRootOpen_Directory(t *testing.T) {
	for _, test := range rootTestCases {
		test.run(t, func(t *testing.T, target string, root *os.Root) {
			if target != "" {
				if err := os.Mkdir(target, 0o777); err != nil {
					t.Fatal(err)
				}
				if err := os.WriteFile(target+"/found", nil, 0o666); err != nil {
					t.Fatal(err)
				}
			}
			f, err := root.Open(test.open)
			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
				return
			}
			defer f.Close()
			got, err := f.Readdirnames(-1)
			if err != nil {
				t.Errorf(`Dir.Open(%q).Readdirnames: %v`, test.open, err)
			}
			if want := []string{"found"}; !slices.Equal(got, want) {
				t.Errorf(`Dir.Open(%q).Readdirnames: %q, want %q`, test.open, got, want)
			}
		})
	}
}

func TestRootCreate(t *testing.T) {
	want := []byte("target")
	for _, test := range rootTestCases {
		test.run(t, func(t *testing.T, target string, root *os.Root) {
			f, err := root.Create(test.open)
			if errEndsTest(t, err, test.wantError, "root.Create(%q)", test.open) {
				return
			}
			if _, err := f.Write(want); err != nil {
				t.Fatal(err)
			}
			f.Close()
			got, err := os.ReadFile(target)
			if err != nil {
				t.Fatalf(`reading file created with root.Create(%q): %v`, test.open, err)
			}
			if !bytes.Equal(got, want) {
				t.Fatalf(`reading file created with root.Create(%q): got %q; want %q`, test.open, got, want)
			}
		})
	}
}

func TestRootMkdir(t *testing.T) {
	for _, test := range rootTestCases {
		test.run(t, func(t *testing.T, target string, root *os.Root) {
			wantError := test.wantError
			if !wantError {
				fi, err := os.Lstat(filepath.Join(root.Name(), test.open))
				if err == nil && fi.Mode().Type() == fs.ModeSymlink {
					// This case is trying to mkdir("some symlink"),
					// which is an error.
					wantError = true
				}
			}

			err := root.Mkdir(test.open, 0o777)
			if errEndsTest(t, err, wantError, "root.Create(%q)", test.open) {
				return
			}
			fi, err := os.Lstat(target)
			if err != nil {
				t.Fatalf(`stat file created with Root.Mkdir(%q): %v`, test.open, err)
			}
			if !fi.IsDir() {
				t.Fatalf(`stat file created with Root.Mkdir(%q): not a directory`, test.open)
			}
			if mode := fi.Mode(); mode&0o777 == 0 {
				// Issue #73559: We're not going to worry about the exact
				// mode bits (which will have been modified by umask),
				// but there should be mode bits.
				t.Fatalf(`stat file created with Root.Mkdir(%q): mode=%v, want non-zero`, test.open, mode)
			}
		})
	}
}

func 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 TestRootOpenFileAsRoot(t *testing.T) {
	dir := t.TempDir()
	target := filepath.Join(dir, "target")
	if err := os.WriteFile(target, nil, 0o666); err != nil {
		t.Fatal(err)
	}
	_, err := os.OpenRoot(target)
	if err == nil {
		t.Fatal("os.OpenRoot(file) succeeded; want failure")
	}
	r, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer r.Close()
	_, err = r.OpenRoot("target")
	if err == nil {
		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)
				}
			}
		})
	}
}

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

var rootConsistencyTestCases = []rootConsistencyTest{{
	name: "file",
	fs: []string{
		"target",
	},
	open: "target",
}, {
	name: "dir slash dot",
	fs: []string{
		"target/file",
	},
	open: "target/.",
}, {
	name: "dot",
	fs: []string{
		"file",
	},
	open: ".",
}, {
	name: "file slash dot",
	fs: []string{
		"target",
	},
	open: "target/.",
	detailedErrorMismatch: func(t *testing.T) bool {
		// FreeBSD returns EPERM in the non-Root case.
		return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
	},
}, {
	name: "dir slash",
	fs: []string{
		"target/file",
	},
	open: "target/",
}, {
	name: "dot slash",
	fs: []string{
		"file",
	},
	open: "./",
}, {
	name: "file slash",
	fs: []string{
		"target",
	},
	open: "target/",
	detailedErrorMismatch: func(t *testing.T) bool {
		// os.Create returns ENOTDIR or EISDIR depending on the platform.
		return runtime.GOOS == "js"
	},
}, {
	name: "file in path",
	fs: []string{
		"file",
	},
	open: "file/target",
}, {
	name: "directory in path missing",
	open: "dir/target",
}, {
	name: "target does not exist",
	open: "target",
}, {
	name: "symlink slash",
	fs: []string{
		"target/file",
		"link => target",
	},
	open: "link/",
}, {
	name: "symlink slash dot",
	fs: []string{
		"target/file",
		"link => target",
	},
	open: "link/.",
}, {
	name: "file symlink slash",
	fs: []string{
		"target",
		"link => target",
	},
	open: "link/",
	detailedErrorMismatch: func(t *testing.T) bool {
		// os.Create returns ENOTDIR or EISDIR depending on the platform.
		return runtime.GOOS == "js"
	},
}, {
	name: "unresolved symlink",
	fs: []string{
		"link => target",
	},
	open: "link",
}, {
	name: "resolved symlink",
	fs: []string{
		"link => target",
		"target",
	},
	open: "link",
}, {
	name: "dotdot in path after symlink",
	fs: []string{
		"a => b/c",
		"b/c/",
		"b/target",
	},
	open: "a/../target",
}, {
	name: "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"
	},
}, {
	name: "question mark",
	open: "?",
}, {
	name: "nul byte",
	open: "\x00",
}}

func tempDirWithUnixSocket(t *testing.T, name string) string {
	dir, err := os.MkdirTemp("", "")
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() {
		if err := os.RemoveAll(dir); err != nil {
			t.Error(err)
		}
	})
	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) {
		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 {
			e1, ok := err1.(*os.PathError)
			if !ok {
				t.Fatalf("with root, expected PathError; got: %v", err1)
			}
			e2, ok := err2.(*os.PathError)
			if !ok {
				t.Fatalf("without root, expected PathError; got: %v", 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.Err != e2.Err {
				t.Errorf("with root:    err=%v", e1.Err)
				t.Errorf("without root: err=%v", e2.Err)
				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 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 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 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 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) {
	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) {
	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 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)
	}
}
