aboutsummaryrefslogtreecommitdiff
path: root/src/testing
diff options
context:
space:
mode:
authorRoxy Light <roxy@zombiezen.com>2024-07-16 10:21:30 -0700
committerCherry Mui <cherryyz@google.com>2025-02-03 08:38:43 -0800
commitf7b8dd9033663944e3b563afaeb55dace4c060fc (patch)
treeecb239b026d072c4a8e8cb4a6307fe4079074bda /src/testing
parent9896da303a74c7af02f711fbb49ac08e4ef3590b (diff)
downloadgo-f7b8dd9033663944e3b563afaeb55dace4c060fc.tar.xz
io/fs: add ReadLinkFS interface
Added implementations for *io/fs.subFS, os.DirFS, and testing/fstest.MapFS. Amended testing/fstest.TestFS to check behavior. Addressed TODOs in archive/tar and os.CopyFS around symbolic links. I am deliberately not changing archive/zip in this CL, since it currently does not resolve symlinks as part of its filesystem implementation. I am unsure of the compatibility restrictions on doing so, so figured it would be better to address independently. testing/fstest.MapFS now includes resolution of symlinks, with MapFile.Data storing the symlink data. The behavior change there seemed less intrusive, especially given its intended usage in tests, and it is especially helpful in testing the io/fs function implementations. Fixes #49580 Change-Id: I58ec6915e8cc97341cdbfd9c24c67d1b60139447 Reviewed-on: https://go-review.googlesource.com/c/go/+/385534 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Daniel Martí <mvdan@mvdan.cc> Reviewed-by: Bryan Mills <bcmills@google.com> Reviewed-by: Cherry Mui <cherryyz@google.com> Reviewed-by: Quim Muntal <quimmuntal@gmail.com> Reviewed-by: Funda Secgin <fundasecgin33@gmail.com>
Diffstat (limited to 'src/testing')
-rw-r--r--src/testing/fstest/mapfs.go115
-rw-r--r--src/testing/fstest/mapfs_test.go66
-rw-r--r--src/testing/fstest/testfs.go29
-rw-r--r--src/testing/fstest/testfs_test.go3
4 files changed, 202 insertions, 11 deletions
diff --git a/src/testing/fstest/mapfs.go b/src/testing/fstest/mapfs.go
index 5e3720b0ed..5ce03985e1 100644
--- a/src/testing/fstest/mapfs.go
+++ b/src/testing/fstest/mapfs.go
@@ -15,7 +15,7 @@ import (
// A MapFS is a simple in-memory file system for use in tests,
// represented as a map from path names (arguments to Open)
-// to information about the files or directories they represent.
+// to information about the files, directories, or symbolic links they represent.
//
// The map need not include parent directories for files contained
// in the map; those will be synthesized if needed.
@@ -34,21 +34,27 @@ type MapFS map[string]*MapFile
// A MapFile describes a single file in a [MapFS].
type MapFile struct {
- Data []byte // file content
+ Data []byte // file content or symlink destination
Mode fs.FileMode // fs.FileInfo.Mode
ModTime time.Time // fs.FileInfo.ModTime
Sys any // fs.FileInfo.Sys
}
var _ fs.FS = MapFS(nil)
+var _ fs.ReadLinkFS = MapFS(nil)
var _ fs.File = (*openMapFile)(nil)
-// Open opens the named file.
+// Open opens the named file after following any symbolic links.
func (fsys MapFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
- file := fsys[name]
+ realName, ok := fsys.resolveSymlinks(name)
+ if !ok {
+ return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
+ }
+
+ file := fsys[realName]
if file != nil && file.Mode&fs.ModeDir == 0 {
// Ordinary file
return &openMapFile{name, mapFileInfo{path.Base(name), file}, 0}, nil
@@ -59,10 +65,8 @@ func (fsys MapFS) Open(name string) (fs.File, error) {
// But file can also be non-nil, in case the user wants to set metadata for the directory explicitly.
// Either way, we need to construct the list of children of this directory.
var list []mapFileInfo
- var elem string
var need = make(map[string]bool)
- if name == "." {
- elem = "."
+ if realName == "." {
for fname, f := range fsys {
i := strings.Index(fname, "/")
if i < 0 {
@@ -74,8 +78,7 @@ func (fsys MapFS) Open(name string) (fs.File, error) {
}
}
} else {
- elem = name[strings.LastIndex(name, "/")+1:]
- prefix := name + "/"
+ prefix := realName + "/"
for fname, f := range fsys {
if strings.HasPrefix(fname, prefix) {
felem := fname[len(prefix):]
@@ -107,9 +110,103 @@ func (fsys MapFS) Open(name string) (fs.File, error) {
if file == nil {
file = &MapFile{Mode: fs.ModeDir | 0555}
}
+ var elem string
+ if name == "." {
+ elem = "."
+ } else {
+ elem = name[strings.LastIndex(name, "/")+1:]
+ }
return &mapDir{name, mapFileInfo{elem, file}, list, 0}, nil
}
+func (fsys MapFS) resolveSymlinks(name string) (_ string, ok bool) {
+ // Fast path: if a symlink is in the map, resolve it.
+ if file := fsys[name]; file != nil && file.Mode.Type() == fs.ModeSymlink {
+ target := string(file.Data)
+ if path.IsAbs(target) {
+ return "", false
+ }
+ return fsys.resolveSymlinks(path.Join(path.Dir(name), target))
+ }
+
+ // Check if each parent directory (starting at root) is a symlink.
+ for i := 0; i < len(name); {
+ j := strings.Index(name[i:], "/")
+ var dir string
+ if j < 0 {
+ dir = name
+ i = len(name)
+ } else {
+ dir = name[:i+j]
+ i += j
+ }
+ if file := fsys[dir]; file != nil && file.Mode.Type() == fs.ModeSymlink {
+ target := string(file.Data)
+ if path.IsAbs(target) {
+ return "", false
+ }
+ return fsys.resolveSymlinks(path.Join(path.Dir(dir), target) + name[i:])
+ }
+ i += len("/")
+ }
+ return name, fs.ValidPath(name)
+}
+
+// ReadLink returns the destination of the named symbolic link.
+func (fsys MapFS) ReadLink(name string) (string, error) {
+ info, err := fsys.lstat(name)
+ if err != nil {
+ return "", &fs.PathError{Op: "readlink", Path: name, Err: err}
+ }
+ if info.f.Mode.Type() != fs.ModeSymlink {
+ return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrInvalid}
+ }
+ return string(info.f.Data), nil
+}
+
+// Lstat returns a FileInfo describing the named file.
+// If the file is a symbolic link, the returned FileInfo describes the symbolic link.
+// Lstat makes no attempt to follow the link.
+func (fsys MapFS) Lstat(name string) (fs.FileInfo, error) {
+ info, err := fsys.lstat(name)
+ if err != nil {
+ return nil, &fs.PathError{Op: "lstat", Path: name, Err: err}
+ }
+ return info, nil
+}
+
+func (fsys MapFS) lstat(name string) (*mapFileInfo, error) {
+ if !fs.ValidPath(name) {
+ return nil, fs.ErrNotExist
+ }
+ realDir, ok := fsys.resolveSymlinks(path.Dir(name))
+ if !ok {
+ return nil, fs.ErrNotExist
+ }
+ elem := path.Base(name)
+ realName := path.Join(realDir, elem)
+
+ file := fsys[realName]
+ if file != nil {
+ return &mapFileInfo{elem, file}, nil
+ }
+
+ if realName == "." {
+ return &mapFileInfo{elem, &MapFile{Mode: fs.ModeDir | 0555}}, nil
+ }
+ // Maybe a directory.
+ prefix := realName + "/"
+ for fname := range fsys {
+ if strings.HasPrefix(fname, prefix) {
+ return &mapFileInfo{elem, &MapFile{Mode: fs.ModeDir | 0555}}, nil
+ }
+ }
+ // If the directory name is not in the map,
+ // and there are no children of the name in the map,
+ // then the directory is treated as not existing.
+ return nil, fs.ErrNotExist
+}
+
// fsOnly is a wrapper that hides all but the fs.FS methods,
// to avoid an infinite recursion when implementing special
// methods in terms of helpers that would use them.
diff --git a/src/testing/fstest/mapfs_test.go b/src/testing/fstest/mapfs_test.go
index 6381a2e56c..e7ff4180ec 100644
--- a/src/testing/fstest/mapfs_test.go
+++ b/src/testing/fstest/mapfs_test.go
@@ -57,3 +57,69 @@ func TestMapFSFileInfoName(t *testing.T) {
t.Errorf("MapFS FileInfo.Name want:\n%s\ngot:\n%s\n", want, got)
}
}
+
+func TestMapFSSymlink(t *testing.T) {
+ const fileContent = "If a program is too slow, it must have a loop.\n"
+ m := MapFS{
+ "fortune/k/ken.txt": {Data: []byte(fileContent)},
+ "dirlink": {Data: []byte("fortune/k"), Mode: fs.ModeSymlink},
+ "linklink": {Data: []byte("dirlink"), Mode: fs.ModeSymlink},
+ "ken.txt": {Data: []byte("dirlink/ken.txt"), Mode: fs.ModeSymlink},
+ }
+ if err := TestFS(m, "fortune/k/ken.txt", "dirlink", "ken.txt", "linklink"); err != nil {
+ t.Error(err)
+ }
+
+ gotData, err := fs.ReadFile(m, "ken.txt")
+ if string(gotData) != fileContent || err != nil {
+ t.Errorf("fs.ReadFile(m, \"ken.txt\") = %q, %v; want %q, <nil>", gotData, err, fileContent)
+ }
+ gotLink, err := fs.ReadLink(m, "dirlink")
+ if want := "fortune/k"; gotLink != want || err != nil {
+ t.Errorf("fs.ReadLink(m, \"dirlink\") = %q, %v; want %q, <nil>", gotLink, err, fileContent)
+ }
+ gotInfo, err := fs.Lstat(m, "dirlink")
+ if err != nil {
+ t.Errorf("fs.Lstat(m, \"dirlink\") = _, %v; want _, <nil>", err)
+ } else {
+ if got, want := gotInfo.Name(), "dirlink"; got != want {
+ t.Errorf("fs.Lstat(m, \"dirlink\").Name() = %q; want %q", got, want)
+ }
+ if got, want := gotInfo.Mode(), fs.ModeSymlink; got != want {
+ t.Errorf("fs.Lstat(m, \"dirlink\").Mode() = %v; want %v", got, want)
+ }
+ }
+ gotInfo, err = fs.Stat(m, "dirlink")
+ if err != nil {
+ t.Errorf("fs.Stat(m, \"dirlink\") = _, %v; want _, <nil>", err)
+ } else {
+ if got, want := gotInfo.Name(), "dirlink"; got != want {
+ t.Errorf("fs.Stat(m, \"dirlink\").Name() = %q; want %q", got, want)
+ }
+ if got, want := gotInfo.Mode(), fs.ModeDir|0555; got != want {
+ t.Errorf("fs.Stat(m, \"dirlink\").Mode() = %v; want %v", got, want)
+ }
+ }
+ gotInfo, err = fs.Lstat(m, "linklink")
+ if err != nil {
+ t.Errorf("fs.Lstat(m, \"linklink\") = _, %v; want _, <nil>", err)
+ } else {
+ if got, want := gotInfo.Name(), "linklink"; got != want {
+ t.Errorf("fs.Lstat(m, \"linklink\").Name() = %q; want %q", got, want)
+ }
+ if got, want := gotInfo.Mode(), fs.ModeSymlink; got != want {
+ t.Errorf("fs.Lstat(m, \"linklink\").Mode() = %v; want %v", got, want)
+ }
+ }
+ gotInfo, err = fs.Stat(m, "linklink")
+ if err != nil {
+ t.Errorf("fs.Stat(m, \"linklink\") = _, %v; want _, <nil>", err)
+ } else {
+ if got, want := gotInfo.Name(), "linklink"; got != want {
+ t.Errorf("fs.Stat(m, \"linklink\").Name() = %q; want %q", got, want)
+ }
+ if got, want := gotInfo.Mode(), fs.ModeDir|0555; got != want {
+ t.Errorf("fs.Stat(m, \"linklink\").Mode() = %v; want %v", got, want)
+ }
+ }
+}
diff --git a/src/testing/fstest/testfs.go b/src/testing/fstest/testfs.go
index affdfa6429..1fb84b8928 100644
--- a/src/testing/fstest/testfs.go
+++ b/src/testing/fstest/testfs.go
@@ -20,6 +20,9 @@ import (
// TestFS tests a file system implementation.
// It walks the entire tree of files in fsys,
// opening and checking that each file behaves correctly.
+// Symbolic links are not followed,
+// but their Lstat values are checked
+// if the file system implements [fs.ReadLinkFS].
// 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 contain at least the listed files; it can also contain others.
@@ -156,9 +159,14 @@ func (t *fsTester) checkDir(dir string) {
path := prefix + name
t.checkStat(path, info)
t.checkOpen(path)
- if info.IsDir() {
+ switch info.Type() {
+ case fs.ModeDir:
t.checkDir(path)
- } else {
+ case fs.ModeSymlink:
+ // No further processing.
+ // Avoid following symlinks to avoid potentially unbounded recursion.
+ t.files = append(t.files, path)
+ default:
t.checkFile(path)
}
}
@@ -440,6 +448,23 @@ func (t *fsTester) checkStat(path string, entry fs.DirEntry) {
t.errorf("%s: fsys.Stat(...) = %s\n\twant %s", path, finfo2, finfo)
}
}
+
+ if fsys, ok := t.fsys.(fs.ReadLinkFS); ok {
+ info2, err := fsys.Lstat(path)
+ if err != nil {
+ t.errorf("%s: fsys.Lstat: %v", path, err)
+ return
+ }
+ fientry2 := formatInfoEntry(info2)
+ if fentry != fientry2 {
+ t.errorf("%s: mismatch:\n\tentry = %s\n\tfsys.Lstat(...) = %s", path, fentry, fientry2)
+ }
+ feinfo := formatInfo(einfo)
+ finfo2 := formatInfo(info2)
+ if feinfo != finfo2 {
+ t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfsys.Lstat(...) = %s\n", path, feinfo, finfo2)
+ }
+ }
}
// checkDirList checks that two directory lists contain the same files and file info.
diff --git a/src/testing/fstest/testfs_test.go b/src/testing/fstest/testfs_test.go
index 2ef1053a01..d6d6d89b89 100644
--- a/src/testing/fstest/testfs_test.go
+++ b/src/testing/fstest/testfs_test.go
@@ -28,6 +28,9 @@ func TestSymlink(t *testing.T) {
if err := os.Symlink(filepath.Join(tmp, "hello"), filepath.Join(tmp, "hello.link")); err != nil {
t.Fatal(err)
}
+ if err := os.Symlink("hello", filepath.Join(tmp, "hello_rel.link")); err != nil {
+ t.Fatal(err)
+ }
if err := TestFS(tmpfs, "hello", "hello.link"); err != nil {
t.Fatal(err)