From 2291cae2af659876e93a3e1f95c708abb1475d02 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Fri, 3 Jul 2020 12:25:49 -0400 Subject: os: use keyed literals for PathError Necessary to move PathError to io/fs. For #41190. Change-Id: I05e87675f38a22f0570d4366b751b6169f7a1b13 Reviewed-on: https://go-review.googlesource.com/c/go/+/243900 Trust: Russ Cox Run-TryBot: Russ Cox Reviewed-by: Rob Pike Reviewed-by: Ian Lance Taylor --- src/os/file.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/os/file.go') diff --git a/src/os/file.go b/src/os/file.go index 05d2f83283..5f16fc28ee 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -127,7 +127,7 @@ func (f *File) ReadAt(b []byte, off int64) (n int, err error) { } if off < 0 { - return 0, &PathError{"readat", f.name, errors.New("negative offset")} + return 0, &PathError{Op: "readat", Path: f.name, Err: errors.New("negative offset")} } for len(b) > 0 { @@ -203,7 +203,7 @@ func (f *File) WriteAt(b []byte, off int64) (n int, err error) { } if off < 0 { - return 0, &PathError{"writeat", f.name, errors.New("negative offset")} + return 0, &PathError{Op: "writeat", Path: f.name, Err: errors.New("negative offset")} } for len(b) > 0 { @@ -253,7 +253,7 @@ func (f *File) WriteString(s string) (n int, err error) { // If there is an error, it will be of type *PathError. func Mkdir(name string, perm FileMode) error { if runtime.GOOS == "windows" && isWindowsNulName(name) { - return &PathError{"mkdir", name, syscall.ENOTDIR} + return &PathError{Op: "mkdir", Path: name, Err: syscall.ENOTDIR} } longName := fixLongPath(name) e := ignoringEINTR(func() error { @@ -261,7 +261,7 @@ func Mkdir(name string, perm FileMode) error { }) if e != nil { - return &PathError{"mkdir", name, e} + return &PathError{Op: "mkdir", Path: name, Err: e} } // mkdir(2) itself won't handle the sticky bit on *BSD and Solaris @@ -291,7 +291,7 @@ func setStickyBit(name string) error { func Chdir(dir string) error { if e := syscall.Chdir(dir); e != nil { testlog.Open(dir) // observe likely non-existent directory - return &PathError{"chdir", dir, e} + return &PathError{Op: "chdir", Path: dir, Err: e} } if log := testlog.Logger(); log != nil { wd, err := Getwd() @@ -366,7 +366,7 @@ func (f *File) wrapErr(op string, err error) error { if err == poll.ErrFileClosing { err = ErrClosed } - return &PathError{op, f.name, err} + return &PathError{Op: op, Path: f.name, Err: err} } // TempDir returns the default directory to use for temporary files. -- cgit v1.3 From b1f76f7a220a806d74bf55da374ea89467753e1f Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Mon, 6 Jul 2020 11:27:38 -0400 Subject: os: add DirFS It will inevitably be important to be able to pass an operating system directory to code written to expect an fs.FS. os.DirFS provides the conversion. For #41190. Change-Id: Id1a8fcbe4c7a30de2c47dea0504e9481a88b1b39 Reviewed-on: https://go-review.googlesource.com/c/go/+/243911 Trust: Russ Cox Reviewed-by: Rob Pike --- src/os/file.go | 19 +++++++++++++++++++ src/os/os_test.go | 7 +++++++ 2 files changed, 26 insertions(+) (limited to 'src/os/file.go') diff --git a/src/os/file.go b/src/os/file.go index 5f16fc28ee..835d44ab8c 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -45,6 +45,7 @@ import ( "internal/poll" "internal/testlog" "io" + "io/fs" "runtime" "syscall" "time" @@ -608,3 +609,21 @@ func isWindowsNulName(name string) bool { } return true } + +// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir. +func DirFS(dir string) fs.FS { + return dirFS(dir) +} + +type dirFS string + +func (dir dirFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid} + } + f, err := Open(string(dir) + "/" + name) + if err != nil { + return nil, err // nil fs.File + } + return f, nil +} diff --git a/src/os/os_test.go b/src/os/os_test.go index 8f14263401..378ddf58dd 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -23,6 +23,7 @@ import ( "sync" "syscall" "testing" + "testing/fstest" "time" ) @@ -2671,3 +2672,9 @@ func TestOpenFileKeepsPermissions(t *testing.T) { t.Errorf("Stat after OpenFile is %v, should be writable", fi.Mode()) } } + +func TestDirFS(t *testing.T) { + if err := fstest.TestFS(DirFS("./signal"), "signal.go", "internal/pty/pty.go"); err != nil { + t.Fatal(err) + } +} -- cgit v1.3 From c193279e2c9e62e8ddc0893484251b4411461d62 Mon Sep 17 00:00:00 2001 From: Chris Waldon Date: Sun, 29 Nov 2020 23:58:29 +0000 Subject: os: return proper user directories on iOS Separating iOS into its own runtime constant broke the logic here to derive the correct home, cache, and config directories on iOS devices. Fixes #42878 Change-Id: Ie4ff57895fcc34b0a9af45554ea3a346447d2e7a GitHub-Last-Rev: 5e74e64917fa46e9c6e0d963cab5194ab89e2f64 GitHub-Pull-Request: golang/go#42879 Reviewed-on: https://go-review.googlesource.com/c/go/+/273947 Reviewed-by: Cherry Zhang Trust: Emmanuel Odeke --- src/os/file.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src/os/file.go') diff --git a/src/os/file.go b/src/os/file.go index 835d44ab8c..420e62ef2c 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -406,7 +406,7 @@ func UserCacheDir() (string, error) { return "", errors.New("%LocalAppData% is not defined") } - case "darwin": + case "darwin", "ios": dir = Getenv("HOME") if dir == "" { return "", errors.New("$HOME is not defined") @@ -457,7 +457,7 @@ func UserConfigDir() (string, error) { return "", errors.New("%AppData% is not defined") } - case "darwin": + case "darwin", "ios": dir = Getenv("HOME") if dir == "" { return "", errors.New("$HOME is not defined") @@ -505,10 +505,8 @@ func UserHomeDir() (string, error) { switch runtime.GOOS { case "android": return "/sdcard", nil - case "darwin": - if runtime.GOARCH == "arm64" { - return "/", nil - } + case "ios": + return "/", nil } return "", errors.New(enverr + " is not defined") } -- cgit v1.3 From 3d913a926675d8d6fcdc3cfaefd3136dfeba06e1 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Thu, 29 Oct 2020 13:51:20 -0400 Subject: os: add ReadFile, WriteFile, CreateTemp (was TempFile), MkdirTemp (was TempDir) from io/ioutil io/ioutil was a poorly defined collection of helpers. Proposal #40025 moved out the generic I/O helpers to io. This CL for proposal #42026 moves the OS-specific helpers to os, making the entire io/ioutil package deprecated. For #42026. Change-Id: I018bcb2115ef2ff1bc7ca36a9247eda429af21ad Reviewed-on: https://go-review.googlesource.com/c/go/+/266364 Trust: Russ Cox Trust: Emmanuel Odeke Run-TryBot: Russ Cox TryBot-Result: Go Bot Reviewed-by: Emmanuel Odeke --- src/io/ioutil/ioutil.go | 70 +++++------------ src/os/dir.go | 22 +++++- src/os/example_test.go | 96 +++++++++++++++++++++++ src/os/export_test.go | 1 + src/os/file.go | 60 +++++++++++++++ src/os/os_test.go | 26 ++++++- src/os/read_test.go | 127 +++++++++++++++++++++++++++++++ src/os/removeall_test.go | 7 +- src/os/tempfile.go | 118 +++++++++++++++++++++++++++++ src/os/tempfile_test.go | 193 +++++++++++++++++++++++++++++++++++++++++++++++ src/os/testdata/hello | 1 + src/runtime/stubs.go | 3 + 12 files changed, 666 insertions(+), 58 deletions(-) create mode 100644 src/os/read_test.go create mode 100644 src/os/tempfile.go create mode 100644 src/os/tempfile_test.go create mode 100644 src/os/testdata/hello (limited to 'src/os/file.go') diff --git a/src/io/ioutil/ioutil.go b/src/io/ioutil/ioutil.go index a001c86b2f..45682b89c9 100644 --- a/src/io/ioutil/ioutil.go +++ b/src/io/ioutil/ioutil.go @@ -3,6 +3,11 @@ // license that can be found in the LICENSE file. // Package ioutil implements some I/O utility functions. +// +// As of Go 1.16, the same functionality is now provided +// by package io or package os, and those implementations +// should be preferred in new code. +// See the specific function documentation for details. package ioutil import ( @@ -26,67 +31,30 @@ func ReadAll(r io.Reader) ([]byte, error) { // A successful call returns err == nil, not err == EOF. Because ReadFile // reads the whole file, it does not treat an EOF from Read as an error // to be reported. +// +// As of Go 1.16, this function simply calls os.ReadFile. func ReadFile(filename string) ([]byte, error) { - f, err := os.Open(filename) - if err != nil { - return nil, err - } - defer f.Close() - // It's a good but not certain bet that FileInfo will tell us exactly how much to - // read, so let's try it but be prepared for the answer to be wrong. - const minRead = 512 - var n int64 = minRead - - if fi, err := f.Stat(); err == nil { - // As initial capacity for readAll, use Size + a little extra in case Size - // is zero, and to avoid another allocation after Read has filled the - // buffer. The readAll call will read into its allocated internal buffer - // cheaply. If the size was wrong, we'll either waste some space off the end - // or reallocate as needed, but in the overwhelmingly common case we'll get - // it just right. - if size := fi.Size() + minRead; size > n { - n = size - } - } - - if int64(int(n)) != n { - n = minRead - } - - b := make([]byte, 0, n) - for { - if len(b) == cap(b) { - // Add more capacity (let append pick how much). - b = append(b, 0)[:len(b)] - } - n, err := f.Read(b[len(b):cap(b)]) - b = b[:len(b)+n] - if err != nil { - if err == io.EOF { - err = nil - } - return b, err - } - } + return os.ReadFile(filename) } // WriteFile writes data to a file named by filename. // If the file does not exist, WriteFile creates it with permissions perm // (before umask); otherwise WriteFile truncates it before writing, without changing permissions. +// +// As of Go 1.16, this function simply calls os.WriteFile. func WriteFile(filename string, data []byte, perm fs.FileMode) error { - f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) - if err != nil { - return err - } - _, err = f.Write(data) - if err1 := f.Close(); err == nil { - err = err1 - } - return err + return os.WriteFile(filename, data, perm) } // ReadDir reads the directory named by dirname and returns -// a list of directory entries sorted by filename. +// a list of fs.FileInfo for the directory's contents, +// sorted by filename. If an error occurs reading the directory, +// ReadDir returns no directory entries along with the error. +// +// As of Go 1.16, os.ReadDir is a more efficient and correct choice: +// it returns a list of fs.DirEntry instead of fs.FileInfo, +// and it returns partial results in the case of an error +// midway through reading a directory. func ReadDir(dirname string) ([]fs.FileInfo, error) { f, err := os.Open(dirname) if err != nil { diff --git a/src/os/dir.go b/src/os/dir.go index 1d90b970e7..5306bcb3ba 100644 --- a/src/os/dir.go +++ b/src/os/dir.go @@ -4,7 +4,10 @@ package os -import "io/fs" +import ( + "io/fs" + "sort" +) type readdirMode int @@ -103,3 +106,20 @@ func (f *File) ReadDir(n int) ([]DirEntry, error) { // testingForceReadDirLstat forces ReadDir to call Lstat, for testing that code path. // This can be difficult to provoke on some Unix systems otherwise. var testingForceReadDirLstat bool + +// ReadDir reads the named directory, +// returning all its directory entries sorted by filename. +// If an error occurs reading the directory, +// ReadDir returns the entries it was able to read before the error, +// along with the error. +func ReadDir(name string) ([]DirEntry, error) { + f, err := Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + dirs, err := f.ReadDir(-1) + sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) + return dirs, err +} diff --git a/src/os/example_test.go b/src/os/example_test.go index fbb277b6f1..3adce51784 100644 --- a/src/os/example_test.go +++ b/src/os/example_test.go @@ -9,6 +9,7 @@ import ( "io/fs" "log" "os" + "path/filepath" "time" ) @@ -144,3 +145,98 @@ func ExampleUnsetenv() { os.Setenv("TMPDIR", "/my/tmp") defer os.Unsetenv("TMPDIR") } + +func ExampleReadDir() { + files, err := os.ReadDir(".") + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + fmt.Println(file.Name()) + } +} + +func ExampleMkdirTemp() { + dir, err := os.MkdirTemp("", "example") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) // clean up + + file := filepath.Join(dir, "tmpfile") + if err := os.WriteFile(file, []byte("content"), 0666); err != nil { + log.Fatal(err) + } +} + +func ExampleMkdirTemp_suffix() { + logsDir, err := os.MkdirTemp("", "*-logs") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(logsDir) // clean up + + // Logs can be cleaned out earlier if needed by searching + // for all directories whose suffix ends in *-logs. + globPattern := filepath.Join(os.TempDir(), "*-logs") + matches, err := filepath.Glob(globPattern) + if err != nil { + log.Fatalf("Failed to match %q: %v", globPattern, err) + } + + for _, match := range matches { + if err := os.RemoveAll(match); err != nil { + log.Printf("Failed to remove %q: %v", match, err) + } + } +} + +func ExampleCreateTemp() { + f, err := os.CreateTemp("", "example") + if err != nil { + log.Fatal(err) + } + defer os.Remove(f.Name()) // clean up + + if _, err := f.Write([]byte("content")); err != nil { + log.Fatal(err) + } + if err := f.Close(); err != nil { + log.Fatal(err) + } +} + +func ExampleCreateTemp_suffix() { + f, err := os.CreateTemp("", "example.*.txt") + if err != nil { + log.Fatal(err) + } + defer os.Remove(f.Name()) // clean up + + if _, err := f.Write([]byte("content")); err != nil { + f.Close() + log.Fatal(err) + } + if err := f.Close(); err != nil { + log.Fatal(err) + } +} + +func ExampleReadFile() { + data, err := os.ReadFile("testdata/hello") + if err != nil { + log.Fatal(err) + } + os.Stdout.Write(data) + + // Output: + // Hello, Gophers! +} + +func ExampleWriteFile() { + err := os.WriteFile("testdata/hello", []byte("Hello, Gophers!"), 0666) + if err != nil { + log.Fatal(err) + } +} diff --git a/src/os/export_test.go b/src/os/export_test.go index d66264a68f..f3cb1a2bef 100644 --- a/src/os/export_test.go +++ b/src/os/export_test.go @@ -10,3 +10,4 @@ var Atime = atime var LstatP = &lstat var ErrWriteAtInAppendMode = errWriteAtInAppendMode var TestingForceReadDirLstat = &testingForceReadDirLstat +var ErrPatternHasSeparator = errPatternHasSeparator diff --git a/src/os/file.go b/src/os/file.go index 420e62ef2c..304b055dbe 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -625,3 +625,63 @@ func (dir dirFS) Open(name string) (fs.File, error) { } return f, nil } + +// ReadFile reads the named file and returns the contents. +// A successful call returns err == nil, not err == EOF. +// Because ReadFile reads the whole file, it does not treat an EOF from Read +// as an error to be reported. +func ReadFile(name string) ([]byte, error) { + f, err := Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + var size int + if info, err := f.Stat(); err == nil { + size64 := info.Size() + if int64(int(size64)) == size64 { + size = int(size64) + } + } + size++ // one byte for final read at EOF + + // If a file claims a small size, read at least 512 bytes. + // In particular, files in Linux's /proc claim size 0 but + // then do not work right if read in small pieces, + // so an initial read of 1 byte would not work correctly. + if size < 512 { + size = 512 + } + + data := make([]byte, 0, size) + for { + if len(data) >= cap(data) { + d := append(data[:cap(data)], 0) + data = d[:len(data)] + } + n, err := f.Read(data[len(data):cap(data)]) + data = data[:len(data)+n] + if err != nil { + if err == io.EOF { + err = nil + } + return data, err + } + } +} + +// WriteFile writes data to the named file, creating it if necessary. +// If the file does not exist, WriteFile creates it with permissions perm (before umask); +// otherwise WriteFile truncates it before writing, without changing permissions. +func WriteFile(name string, data []byte, perm FileMode) error { + f, err := OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm) + if err != nil { + return err + } + _, err = f.Write(data) + if err1 := f.Close(); err1 != nil && err == nil { + err = err1 + } + return err +} diff --git a/src/os/os_test.go b/src/os/os_test.go index a1c0578887..c5e5cbbb1b 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -419,19 +419,19 @@ func testReadDir(dir string, contents []string, t *testing.T) { } } -func TestReaddirnames(t *testing.T) { +func TestFileReaddirnames(t *testing.T) { testReaddirnames(".", dot, t) testReaddirnames(sysdir.name, sysdir.files, t) testReaddirnames(t.TempDir(), nil, t) } -func TestReaddir(t *testing.T) { +func TestFileReaddir(t *testing.T) { testReaddir(".", dot, t) testReaddir(sysdir.name, sysdir.files, t) testReaddir(t.TempDir(), nil, t) } -func TestReadDir(t *testing.T) { +func TestFileReadDir(t *testing.T) { testReadDir(".", dot, t) testReadDir(sysdir.name, sysdir.files, t) testReadDir(t.TempDir(), nil, t) @@ -1235,6 +1235,7 @@ func TestChmod(t *testing.T) { } func checkSize(t *testing.T, f *File, size int64) { + t.Helper() dir, err := f.Stat() if err != nil { t.Fatalf("Stat %q (looking for size %d): %s", f.Name(), size, err) @@ -2690,3 +2691,22 @@ func TestDirFS(t *testing.T) { t.Fatal(err) } } + +func TestReadFileProc(t *testing.T) { + // Linux files in /proc report 0 size, + // but then if ReadFile reads just a single byte at offset 0, + // the read at offset 1 returns EOF instead of more data. + // ReadFile has a minimum read size of 512 to work around this, + // but test explicitly that it's working. + name := "/proc/sys/fs/pipe-max-size" + if _, err := Stat(name); err != nil { + t.Skip(err) + } + data, err := ReadFile(name) + if err != nil { + t.Fatal(err) + } + if len(data) == 0 || data[len(data)-1] != '\n' { + t.Fatalf("read %s: not newline-terminated: %q", name, data) + } +} diff --git a/src/os/read_test.go b/src/os/read_test.go new file mode 100644 index 0000000000..5c58d7d7df --- /dev/null +++ b/src/os/read_test.go @@ -0,0 +1,127 @@ +// Copyright 2009 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" + . "os" + "path/filepath" + "testing" +) + +func checkNamedSize(t *testing.T, path string, size int64) { + dir, err := Stat(path) + if err != nil { + t.Fatalf("Stat %q (looking for size %d): %s", path, size, err) + } + if dir.Size() != size { + t.Errorf("Stat %q: size %d want %d", path, dir.Size(), size) + } +} + +func TestReadFile(t *testing.T) { + filename := "rumpelstilzchen" + contents, err := ReadFile(filename) + if err == nil { + t.Fatalf("ReadFile %s: error expected, none found", filename) + } + + filename = "read_test.go" + contents, err = ReadFile(filename) + if err != nil { + t.Fatalf("ReadFile %s: %v", filename, err) + } + + checkNamedSize(t, filename, int64(len(contents))) +} + +func TestWriteFile(t *testing.T) { + f, err := CreateTemp("", "ioutil-test") + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer Remove(f.Name()) + + msg := "Programming today is a race between software engineers striving to " + + "build bigger and better idiot-proof programs, and the Universe trying " + + "to produce bigger and better idiots. So far, the Universe is winning." + + if err := WriteFile(f.Name(), []byte(msg), 0644); err != nil { + t.Fatalf("WriteFile %s: %v", f.Name(), err) + } + + data, err := ReadFile(f.Name()) + if err != nil { + t.Fatalf("ReadFile %s: %v", f.Name(), err) + } + + if string(data) != msg { + t.Fatalf("ReadFile: wrong data:\nhave %q\nwant %q", string(data), msg) + } +} + +func TestReadOnlyWriteFile(t *testing.T) { + if Getuid() == 0 { + t.Skipf("Root can write to read-only files anyway, so skip the read-only test.") + } + + // We don't want to use CreateTemp directly, since that opens a file for us as 0600. + tempDir, err := MkdirTemp("", t.Name()) + if err != nil { + t.Fatal(err) + } + defer RemoveAll(tempDir) + filename := filepath.Join(tempDir, "blurp.txt") + + shmorp := []byte("shmorp") + florp := []byte("florp") + err = WriteFile(filename, shmorp, 0444) + if err != nil { + t.Fatalf("WriteFile %s: %v", filename, err) + } + err = WriteFile(filename, florp, 0444) + if err == nil { + t.Fatalf("Expected an error when writing to read-only file %s", filename) + } + got, err := ReadFile(filename) + if err != nil { + t.Fatalf("ReadFile %s: %v", filename, err) + } + if !bytes.Equal(got, shmorp) { + t.Fatalf("want %s, got %s", shmorp, got) + } +} + +func TestReadDir(t *testing.T) { + dirname := "rumpelstilzchen" + _, err := ReadDir(dirname) + if err == nil { + t.Fatalf("ReadDir %s: error expected, none found", dirname) + } + + dirname = "." + list, err := ReadDir(dirname) + if err != nil { + t.Fatalf("ReadDir %s: %v", dirname, err) + } + + foundFile := false + foundSubDir := false + for _, dir := range list { + switch { + case !dir.IsDir() && dir.Name() == "read_test.go": + foundFile = true + case dir.IsDir() && dir.Name() == "exec": + foundSubDir = true + } + } + if !foundFile { + t.Fatalf("ReadDir %s: read_test.go file not found", dirname) + } + if !foundSubDir { + t.Fatalf("ReadDir %s: exec directory not found", dirname) + } +} diff --git a/src/os/removeall_test.go b/src/os/removeall_test.go index bc9c468ce3..90efa313ea 100644 --- a/src/os/removeall_test.go +++ b/src/os/removeall_test.go @@ -355,11 +355,12 @@ func TestRemoveAllButReadOnlyAndPathError(t *testing.T) { // The error should be of type *PathError. // see issue 30491 for details. if pathErr, ok := err.(*PathError); ok { - if g, w := pathErr.Path, filepath.Join(tempDir, "b", "y"); g != w { - t.Errorf("got %q, expected pathErr.path %q", g, w) + want := filepath.Join(tempDir, "b", "y") + if pathErr.Path != want { + t.Errorf("RemoveAll(%q): err.Path=%q, want %q", tempDir, pathErr.Path, want) } } else { - t.Errorf("got %T, expected *fs.PathError", err) + t.Errorf("RemoveAll(%q): error has type %T, want *fs.PathError", tempDir, err) } for _, dir := range dirs { diff --git a/src/os/tempfile.go b/src/os/tempfile.go new file mode 100644 index 0000000000..2728485c32 --- /dev/null +++ b/src/os/tempfile.go @@ -0,0 +1,118 @@ +// Copyright 2010 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 + +import ( + "errors" + "strings" +) + +// fastrand provided by runtime. +// We generate random temporary file names so that there's a good +// chance the file doesn't exist yet - keeps the number of tries in +// TempFile to a minimum. +func fastrand() uint32 + +func nextRandom() string { + return uitoa(uint(fastrand())) +} + +// CreateTemp creates a new temporary file in the directory dir, +// opens the file for reading and writing, and returns the resulting file. +// The filename is generated by taking pattern and adding a random string to the end. +// If pattern includes a "*", the random string replaces the last "*". +// If dir is the empty string, TempFile uses the default directory for temporary files, as returned by TempDir. +// Multiple programs or goroutines calling CreateTemp simultaneously will not choose the same file. +// The caller can use the file's Name method to find the pathname of the file. +// It is the caller's responsibility to remove the file when it is no longer needed. +func CreateTemp(dir, pattern string) (*File, error) { + if dir == "" { + dir = TempDir() + } + + prefix, suffix, err := prefixAndSuffix(pattern) + if err != nil { + return nil, &PathError{Op: "createtemp", Path: pattern, Err: err} + } + prefix = joinPath(dir, prefix) + + try := 0 + for { + name := prefix + nextRandom() + suffix + f, err := OpenFile(name, O_RDWR|O_CREATE|O_EXCL, 0600) + if IsExist(err) { + if try++; try < 10000 { + continue + } + return nil, &PathError{Op: "createtemp", Path: dir + string(PathSeparator) + prefix + "*" + suffix, Err: ErrExist} + } + return f, err + } +} + +var errPatternHasSeparator = errors.New("pattern contains path separator") + +// prefixAndSuffix splits pattern by the last wildcard "*", if applicable, +// returning prefix as the part before "*" and suffix as the part after "*". +func prefixAndSuffix(pattern string) (prefix, suffix string, err error) { + for i := 0; i < len(pattern); i++ { + if IsPathSeparator(pattern[i]) { + return "", "", errPatternHasSeparator + } + } + if pos := strings.LastIndex(pattern, "*"); pos != -1 { + prefix, suffix = pattern[:pos], pattern[pos+1:] + } else { + prefix = pattern + } + return prefix, suffix, nil +} + +// MkdirTemp creates a new temporary directory in the directory dir +// and returns the pathname of the new directory. +// The new directory's name is generated by adding a random string to the end of pattern. +// If pattern includes a "*", the random string replaces the last "*" instead. +// If dir is the empty string, TempFile uses the default directory for temporary files, as returned by TempDir. +// Multiple programs or goroutines calling MkdirTemp simultaneously will not choose the same directory. +// It is the caller's responsibility to remove the directory when it is no longer needed. +func MkdirTemp(dir, pattern string) (string, error) { + if dir == "" { + dir = TempDir() + } + + prefix, suffix, err := prefixAndSuffix(pattern) + if err != nil { + return "", &PathError{Op: "mkdirtemp", Path: pattern, Err: err} + } + prefix = joinPath(dir, prefix) + + try := 0 + for { + name := prefix + nextRandom() + suffix + err := Mkdir(name, 0700) + if err == nil { + return name, nil + } + if IsExist(err) { + if try++; try < 10000 { + continue + } + return "", &PathError{Op: "mkdirtemp", Path: dir + string(PathSeparator) + prefix + "*" + suffix, Err: ErrExist} + } + if IsNotExist(err) { + if _, err := Stat(dir); IsNotExist(err) { + return "", err + } + } + return "", err + } +} + +func joinPath(dir, name string) string { + if len(dir) > 0 && IsPathSeparator(dir[len(dir)-1]) { + return dir + name + } + return dir + string(PathSeparator) + name +} diff --git a/src/os/tempfile_test.go b/src/os/tempfile_test.go new file mode 100644 index 0000000000..e71a2444c9 --- /dev/null +++ b/src/os/tempfile_test.go @@ -0,0 +1,193 @@ +// Copyright 2010 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 ( + "errors" + "io/fs" + . "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +func TestCreateTemp(t *testing.T) { + dir, err := MkdirTemp("", "TestCreateTempBadDir") + if err != nil { + t.Fatal(err) + } + defer RemoveAll(dir) + + nonexistentDir := filepath.Join(dir, "_not_exists_") + f, err := CreateTemp(nonexistentDir, "foo") + if f != nil || err == nil { + t.Errorf("CreateTemp(%q, `foo`) = %v, %v", nonexistentDir, f, err) + } +} + +func TestCreateTempPattern(t *testing.T) { + tests := []struct{ pattern, prefix, suffix string }{ + {"tempfile_test", "tempfile_test", ""}, + {"tempfile_test*", "tempfile_test", ""}, + {"tempfile_test*xyz", "tempfile_test", "xyz"}, + } + for _, test := range tests { + f, err := CreateTemp("", test.pattern) + if err != nil { + t.Errorf("CreateTemp(..., %q) error: %v", test.pattern, err) + continue + } + defer Remove(f.Name()) + base := filepath.Base(f.Name()) + f.Close() + if !(strings.HasPrefix(base, test.prefix) && strings.HasSuffix(base, test.suffix)) { + t.Errorf("CreateTemp pattern %q created bad name %q; want prefix %q & suffix %q", + test.pattern, base, test.prefix, test.suffix) + } + } +} + +func TestCreateTempBadPattern(t *testing.T) { + tmpDir, err := MkdirTemp("", t.Name()) + if err != nil { + t.Fatal(err) + } + defer RemoveAll(tmpDir) + + const sep = string(PathSeparator) + tests := []struct { + pattern string + wantErr bool + }{ + {"ioutil*test", false}, + {"tempfile_test*foo", false}, + {"tempfile_test" + sep + "foo", true}, + {"tempfile_test*" + sep + "foo", true}, + {"tempfile_test" + sep + "*foo", true}, + {sep + "tempfile_test" + sep + "*foo", true}, + {"tempfile_test*foo" + sep, true}, + } + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + tmpfile, err := CreateTemp(tmpDir, tt.pattern) + if tmpfile != nil { + defer tmpfile.Close() + } + if tt.wantErr { + if err == nil { + t.Errorf("CreateTemp(..., %#q) succeeded, expected error", tt.pattern) + } + if !errors.Is(err, ErrPatternHasSeparator) { + t.Errorf("CreateTemp(..., %#q): %v, expected ErrPatternHasSeparator", tt.pattern, err) + } + } else if err != nil { + t.Errorf("CreateTemp(..., %#q): %v", tt.pattern, err) + } + }) + } +} + +func TestMkdirTemp(t *testing.T) { + name, err := MkdirTemp("/_not_exists_", "foo") + if name != "" || err == nil { + t.Errorf("MkdirTemp(`/_not_exists_`, `foo`) = %v, %v", name, err) + } + + tests := []struct { + pattern string + wantPrefix, wantSuffix string + }{ + {"tempfile_test", "tempfile_test", ""}, + {"tempfile_test*", "tempfile_test", ""}, + {"tempfile_test*xyz", "tempfile_test", "xyz"}, + } + + dir := filepath.Clean(TempDir()) + + runTestMkdirTemp := func(t *testing.T, pattern, wantRePat string) { + name, err := MkdirTemp(dir, pattern) + if name == "" || err != nil { + t.Fatalf("MkdirTemp(dir, `tempfile_test`) = %v, %v", name, err) + } + defer Remove(name) + + re := regexp.MustCompile(wantRePat) + if !re.MatchString(name) { + t.Errorf("MkdirTemp(%q, %q) created bad name\n\t%q\ndid not match pattern\n\t%q", dir, pattern, name, wantRePat) + } + } + + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + wantRePat := "^" + regexp.QuoteMeta(filepath.Join(dir, tt.wantPrefix)) + "[0-9]+" + regexp.QuoteMeta(tt.wantSuffix) + "$" + runTestMkdirTemp(t, tt.pattern, wantRePat) + }) + } + + // Separately testing "*xyz" (which has no prefix). That is when constructing the + // pattern to assert on, as in the previous loop, using filepath.Join for an empty + // prefix filepath.Join(dir, ""), produces the pattern: + // ^[0-9]+xyz$ + // yet we just want to match + // "^/[0-9]+xyz" + t.Run("*xyz", func(t *testing.T) { + wantRePat := "^" + regexp.QuoteMeta(filepath.Join(dir)) + regexp.QuoteMeta(string(filepath.Separator)) + "[0-9]+xyz$" + runTestMkdirTemp(t, "*xyz", wantRePat) + }) +} + +// test that we return a nice error message if the dir argument to TempDir doesn't +// exist (or that it's empty and TempDir doesn't exist) +func TestMkdirTempBadDir(t *testing.T) { + dir, err := MkdirTemp("", "MkdirTempBadDir") + if err != nil { + t.Fatal(err) + } + defer RemoveAll(dir) + + badDir := filepath.Join(dir, "not-exist") + _, err = MkdirTemp(badDir, "foo") + if pe, ok := err.(*fs.PathError); !ok || !IsNotExist(err) || pe.Path != badDir { + t.Errorf("TempDir error = %#v; want PathError for path %q satisifying IsNotExist", err, badDir) + } +} + +func TestMkdirTempBadPattern(t *testing.T) { + tmpDir, err := MkdirTemp("", t.Name()) + if err != nil { + t.Fatal(err) + } + defer RemoveAll(tmpDir) + + const sep = string(PathSeparator) + tests := []struct { + pattern string + wantErr bool + }{ + {"ioutil*test", false}, + {"tempfile_test*foo", false}, + {"tempfile_test" + sep + "foo", true}, + {"tempfile_test*" + sep + "foo", true}, + {"tempfile_test" + sep + "*foo", true}, + {sep + "tempfile_test" + sep + "*foo", true}, + {"tempfile_test*foo" + sep, true}, + } + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + _, err := MkdirTemp(tmpDir, tt.pattern) + if tt.wantErr { + if err == nil { + t.Errorf("MkdirTemp(..., %#q) succeeded, expected error", tt.pattern) + } + if !errors.Is(err, ErrPatternHasSeparator) { + t.Errorf("MkdirTemp(..., %#q): %v, expected ErrPatternHasSeparator", tt.pattern, err) + } + } else if err != nil { + t.Errorf("MkdirTemp(..., %#q): %v", tt.pattern, err) + } + }) + } +} diff --git a/src/os/testdata/hello b/src/os/testdata/hello new file mode 100644 index 0000000000..e47c092a51 --- /dev/null +++ b/src/os/testdata/hello @@ -0,0 +1 @@ +Hello, Gophers! diff --git a/src/runtime/stubs.go b/src/runtime/stubs.go index d77cb4d460..b55c3c0590 100644 --- a/src/runtime/stubs.go +++ b/src/runtime/stubs.go @@ -133,6 +133,9 @@ func sync_fastrand() uint32 { return fastrand() } //go:linkname net_fastrand net.fastrand func net_fastrand() uint32 { return fastrand() } +//go:linkname os_fastrand os.fastrand +func os_fastrand() uint32 { return fastrand() } + // in internal/bytealg/equal_*.s //go:noescape func memequal(a, b unsafe.Pointer, size uintptr) bool -- cgit v1.3 From 478bde3a4388997924a02ee9296864866d8ba3ba Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Wed, 2 Dec 2020 12:49:20 -0500 Subject: io/fs: add Sub Sub provides a convenient way to refer to a subdirectory automatically in future operations, like Unix's chdir(2). The CL also includes updates to fstest to check Sub implementations. As part of updating fstest, I changed the meaning of TestFS's expected list to introduce a special case: if you list no expected files, that means the FS must be empty. In general it's OK not to list all the expected files, but if you list none, that's almost certainly a mistake - if your FS were broken and empty, you wouldn't find out. Making no expected files mean "must be empty" makes the mistake less likely - if your file system ever worked, then your test will keep it working. That change found a testing bug: embedtest was making exactly that mistake. Fixes #42322. Change-Id: I63fd4aa866b30061a0e51ca9a1927e576d6ec41e Reviewed-on: https://go-review.googlesource.com/c/go/+/274856 Trust: Russ Cox Run-TryBot: Russ Cox TryBot-Result: Go Bot Reviewed-by: Ian Lance Taylor --- doc/go1.16.html | 2 +- src/embed/internal/embedtest/embed_test.go | 2 +- src/io/fs/readdir_test.go | 12 ++- src/io/fs/readfile_test.go | 16 ++++ src/io/fs/sub.go | 127 +++++++++++++++++++++++++++++ src/io/fs/sub_test.go | 57 +++++++++++++ src/os/file.go | 7 ++ src/testing/fstest/mapfs.go | 10 +++ src/testing/fstest/testfs.go | 42 ++++++++++ 9 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 src/io/fs/sub.go create mode 100644 src/io/fs/sub_test.go (limited to 'src/os/file.go') diff --git a/doc/go1.16.html b/doc/go1.16.html index 4d4b459009..62d9b97db8 100644 --- a/doc/go1.16.html +++ b/doc/go1.16.html @@ -384,7 +384,7 @@ Do not send CLs removing the interior tags from such phrases. the new embed.FS type implements fs.FS, as does zip.Reader. - The new os.Dir function + The new os.DirFS function provides an implementation of fs.FS backed by a tree of operating system files.

diff --git a/src/embed/internal/embedtest/embed_test.go b/src/embed/internal/embedtest/embed_test.go index b1707a4c04..c6a7bea7a3 100644 --- a/src/embed/internal/embedtest/embed_test.go +++ b/src/embed/internal/embedtest/embed_test.go @@ -65,7 +65,7 @@ func TestGlobal(t *testing.T) { testFiles(t, global, "testdata/hello.txt", "hello, world\n") testFiles(t, global, "testdata/glass.txt", "I can eat glass and it doesn't hurt me.\n") - if err := fstest.TestFS(global); err != nil { + if err := fstest.TestFS(global, "concurrency.txt", "testdata/hello.txt"); err != nil { t.Fatal(err) } diff --git a/src/io/fs/readdir_test.go b/src/io/fs/readdir_test.go index 46a4bc2788..405bfa67ca 100644 --- a/src/io/fs/readdir_test.go +++ b/src/io/fs/readdir_test.go @@ -16,12 +16,12 @@ func (readDirOnly) Open(name string) (File, error) { return nil, ErrNotExist } func TestReadDir(t *testing.T) { check := func(desc string, dirs []DirEntry, err error) { t.Helper() - if err != nil || len(dirs) != 1 || dirs[0].Name() != "hello.txt" { + if err != nil || len(dirs) != 2 || dirs[0].Name() != "hello.txt" || dirs[1].Name() != "sub" { var names []string for _, d := range dirs { names = append(names, d.Name()) } - t.Errorf("ReadDir(%s) = %v, %v, want %v, nil", desc, names, err, []string{"hello.txt"}) + t.Errorf("ReadDir(%s) = %v, %v, want %v, nil", desc, names, err, []string{"hello.txt", "sub"}) } } @@ -32,4 +32,12 @@ func TestReadDir(t *testing.T) { // Test that ReadDir uses Open when the method is not present. dirs, err = ReadDir(openOnly{testFsys}, ".") check("openOnly", dirs, err) + + // Test that ReadDir on Sub of . works (sub_test checks non-trivial subs). + sub, err := Sub(testFsys, ".") + if err != nil { + t.Fatal(err) + } + dirs, err = ReadDir(sub, ".") + check("sub(.)", dirs, err) } diff --git a/src/io/fs/readfile_test.go b/src/io/fs/readfile_test.go index 0afa334ace..07219c1445 100644 --- a/src/io/fs/readfile_test.go +++ b/src/io/fs/readfile_test.go @@ -18,6 +18,12 @@ var testFsys = fstest.MapFS{ ModTime: time.Now(), Sys: &sysValue, }, + "sub/goodbye.txt": { + Data: []byte("goodbye, world"), + Mode: 0456, + ModTime: time.Now(), + Sys: &sysValue, + }, } var sysValue int @@ -40,4 +46,14 @@ func TestReadFile(t *testing.T) { if string(data) != "hello, world" || err != nil { t.Fatalf(`ReadFile(openOnly, "hello.txt") = %q, %v, want %q, nil`, data, err, "hello, world") } + + // Test that ReadFile on Sub of . works (sub_test checks non-trivial subs). + sub, err := Sub(testFsys, ".") + if err != nil { + t.Fatal(err) + } + data, err = ReadFile(sub, "hello.txt") + if string(data) != "hello, world" || err != nil { + t.Fatalf(`ReadFile(sub(.), "hello.txt") = %q, %v, want %q, nil`, data, err, "hello, world") + } } diff --git a/src/io/fs/sub.go b/src/io/fs/sub.go new file mode 100644 index 0000000000..381f409504 --- /dev/null +++ b/src/io/fs/sub.go @@ -0,0 +1,127 @@ +// Copyright 2020 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 fs + +import ( + "errors" + "path" +) + +// A SubFS is a file system with a Sub method. +type SubFS interface { + FS + + // Sub returns an FS corresponding to the subtree rooted at dir. + Sub(dir string) (FS, error) +} + +// Sub returns an FS corresponding to the subtree rooted at fsys's dir. +// +// If fs implements SubFS, Sub calls returns fsys.Sub(dir). +// Otherwise, if dir is ".", Sub returns fsys unchanged. +// Otherwise, Sub returns a new FS implementation sub that, +// in effect, implements sub.Open(dir) as fsys.Open(path.Join(dir, name)). +// The implementation also translates calls to ReadDir, ReadFile, and Glob appropriately. +// +// Note that Sub(os.DirFS("/"), "prefix") is equivalent to os.DirFS("/prefix") +// and that neither of them guarantees to avoid operating system +// accesses outside "/prefix", because the implementation of os.DirFS +// does not check for symbolic links inside "/prefix" that point to +// other directories. That is, os.DirFS is not a general substitute for a +// chroot-style security mechanism, and Sub does not change that fact. +func Sub(fsys FS, dir string) (FS, error) { + if !ValidPath(dir) { + return nil, &PathError{Op: "sub", Path: dir, Err: errors.New("invalid name")} + } + if dir == "." { + return fsys, nil + } + if fsys, ok := fsys.(SubFS); ok { + return fsys.Sub(dir) + } + return &subFS{fsys, dir}, nil +} + +type subFS struct { + fsys FS + dir string +} + +// fullName maps name to the fully-qualified name dir/name. +func (f *subFS) fullName(op string, name string) (string, error) { + if !ValidPath(name) { + return "", &PathError{Op: op, Path: name, Err: errors.New("invalid name")} + } + return path.Join(f.dir, name), nil +} + +// shorten maps name, which should start with f.dir, back to the suffix after f.dir. +func (f *subFS) shorten(name string) (rel string, ok bool) { + if name == f.dir { + return ".", true + } + if len(name) >= len(f.dir)+2 && name[len(f.dir)] == '/' && name[:len(f.dir)] == f.dir { + return name[len(f.dir)+1:], true + } + return "", false +} + +// fixErr shortens any reported names in PathErrors by stripping dir. +func (f *subFS) fixErr(err error) error { + if e, ok := err.(*PathError); ok { + if short, ok := f.shorten(e.Path); ok { + e.Path = short + } + } + return err +} + +func (f *subFS) Open(name string) (File, error) { + full, err := f.fullName("open", name) + if err != nil { + return nil, err + } + file, err := f.fsys.Open(full) + return file, f.fixErr(err) +} + +func (f *subFS) ReadDir(name string) ([]DirEntry, error) { + full, err := f.fullName("open", name) + if err != nil { + return nil, err + } + dir, err := ReadDir(f.fsys, full) + return dir, f.fixErr(err) +} + +func (f *subFS) ReadFile(name string) ([]byte, error) { + full, err := f.fullName("open", name) + if err != nil { + return nil, err + } + data, err := ReadFile(f.fsys, full) + return data, f.fixErr(err) +} + +func (f *subFS) Glob(pattern string) ([]string, error) { + // Check pattern is well-formed. + if _, err := path.Match(pattern, ""); err != nil { + return nil, err + } + if pattern == "." { + return []string{"."}, nil + } + + full := f.dir + "/" + pattern + list, err := Glob(f.fsys, full) + for i, name := range list { + name, ok := f.shorten(name) + if !ok { + return nil, errors.New("invalid result from inner fsys Glob: " + name + " not in " + f.dir) // can't use fmt in this package + } + list[i] = name + } + return list, f.fixErr(err) +} diff --git a/src/io/fs/sub_test.go b/src/io/fs/sub_test.go new file mode 100644 index 0000000000..451b0efb02 --- /dev/null +++ b/src/io/fs/sub_test.go @@ -0,0 +1,57 @@ +// Copyright 2020 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 fs_test + +import ( + . "io/fs" + "testing" +) + +type subOnly struct{ SubFS } + +func (subOnly) Open(name string) (File, error) { return nil, ErrNotExist } + +func TestSub(t *testing.T) { + check := func(desc string, sub FS, err error) { + t.Helper() + if err != nil { + t.Errorf("Sub(sub): %v", err) + return + } + data, err := ReadFile(sub, "goodbye.txt") + if string(data) != "goodbye, world" || err != nil { + t.Errorf(`ReadFile(%s, "goodbye.txt" = %q, %v, want %q, nil`, desc, string(data), err, "goodbye, world") + } + + dirs, err := ReadDir(sub, ".") + if err != nil || len(dirs) != 1 || dirs[0].Name() != "goodbye.txt" { + var names []string + for _, d := range dirs { + names = append(names, d.Name()) + } + t.Errorf(`ReadDir(%s, ".") = %v, %v, want %v, nil`, desc, names, err, []string{"goodbye.txt"}) + } + } + + // Test that Sub uses the method when present. + sub, err := Sub(subOnly{testFsys}, "sub") + check("subOnly", sub, err) + + // Test that Sub uses Open when the method is not present. + sub, err = Sub(openOnly{testFsys}, "sub") + check("openOnly", sub, err) + + _, err = sub.Open("nonexist") + if err == nil { + t.Fatal("Open(nonexist): succeeded") + } + pe, ok := err.(*PathError) + if !ok { + t.Fatalf("Open(nonexist): error is %T, want *PathError", err) + } + if pe.Path != "nonexist" { + t.Fatalf("Open(nonexist): err.Path = %q, want %q", pe.Path, "nonexist") + } +} diff --git a/src/os/file.go b/src/os/file.go index 304b055dbe..416bc0efa6 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -609,6 +609,13 @@ func isWindowsNulName(name string) bool { } // DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir. +// +// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the +// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the +// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside +// the /prefix tree, then using DirFS does not stop the access any more than using +// os.Open does. DirFS is therefore not a general substitute for a chroot-style security +// mechanism when the directory tree contains arbitrary content. func DirFS(dir string) fs.FS { return dirFS(dir) } diff --git a/src/testing/fstest/mapfs.go b/src/testing/fstest/mapfs.go index 10e56f5b3c..a5d4a23fac 100644 --- a/src/testing/fstest/mapfs.go +++ b/src/testing/fstest/mapfs.go @@ -132,6 +132,16 @@ func (fsys MapFS) Glob(pattern string) ([]string, error) { return fs.Glob(fsOnly{fsys}, pattern) } +type noSub struct { + MapFS +} + +func (noSub) Sub() {} // not the fs.SubFS signature + +func (fsys MapFS) Sub(dir string) (fs.FS, error) { + return fs.Sub(noSub{fsys}, dir) +} + // A mapFileInfo implements fs.FileInfo and fs.DirEntry for a given map file. type mapFileInfo struct { name string diff --git a/src/testing/fstest/testfs.go b/src/testing/fstest/testfs.go index 4912a271b2..2602bdf0cc 100644 --- a/src/testing/fstest/testfs.go +++ b/src/testing/fstest/testfs.go @@ -22,6 +22,8 @@ import ( // It walks the entire tree of files in fsys, // opening and checking that each file behaves correctly. // It also checks that the file system contains at least the expected files. +// As a special case, if no expected files are listed, fsys must be empty. +// Otherwise, fsys must only contain at least the listed files: it can also contain others. // // If TestFS finds any misbehaviors, it returns an error reporting all of them. // The error text spans multiple lines, one per detected misbehavior. @@ -33,6 +35,32 @@ import ( // } // func TestFS(fsys fs.FS, expected ...string) error { + if err := testFS(fsys, expected...); err != nil { + return err + } + for _, name := range expected { + if i := strings.Index(name, "/"); i >= 0 { + dir, dirSlash := name[:i], name[:i+1] + var subExpected []string + for _, name := range expected { + if strings.HasPrefix(name, dirSlash) { + subExpected = append(subExpected, name[len(dirSlash):]) + } + } + sub, err := fs.Sub(fsys, dir) + if err != nil { + return err + } + if err := testFS(sub, subExpected...); err != nil { + return fmt.Errorf("testing fs.Sub(fsys, %s): %v", dir, err) + } + break // one sub-test is enough + } + } + return nil +} + +func testFS(fsys fs.FS, expected ...string) error { t := fsTester{fsys: fsys} t.checkDir(".") t.checkOpen(".") @@ -43,6 +71,20 @@ func TestFS(fsys fs.FS, expected ...string) error { for _, file := range t.files { found[file] = true } + delete(found, ".") + if len(expected) == 0 && len(found) > 0 { + var list []string + for k := range found { + if k != "." { + list = append(list, k) + } + } + sort.Strings(list) + if len(list) > 15 { + list = append(list[:10], "...") + } + t.errorf("expected empty file system but found files:\n%s", strings.Join(list, "\n")) + } for _, name := range expected { if !found[name] { t.errorf("expected but not found: %s", name) -- cgit v1.3