mirror of
https://github.com/golang/go.git
synced 2025-05-05 15:43:04 +00:00
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>
This commit is contained in:
parent
9896da303a
commit
f7b8dd9033
8
api/next/49580.txt
Normal file
8
api/next/49580.txt
Normal file
@ -0,0 +1,8 @@
|
||||
pkg io/fs, func Lstat(FS, string) (FileInfo, error) #49580
|
||||
pkg io/fs, func ReadLink(FS, string) (string, error) #49580
|
||||
pkg io/fs, type ReadLinkFS interface { Lstat, Open, ReadLink } #49580
|
||||
pkg io/fs, type ReadLinkFS interface, Lstat(string) (FileInfo, error) #49580
|
||||
pkg io/fs, type ReadLinkFS interface, Open(string) (File, error) #49580
|
||||
pkg io/fs, type ReadLinkFS interface, ReadLink(string) (string, error) #49580
|
||||
pkg testing/fstest, method (MapFS) Lstat(string) (fs.FileInfo, error) #49580
|
||||
pkg testing/fstest, method (MapFS) ReadLink(string) (string, error) #49580
|
2
doc/next/6-stdlib/99-minor/archive/tar/49580.md
Normal file
2
doc/next/6-stdlib/99-minor/archive/tar/49580.md
Normal file
@ -0,0 +1,2 @@
|
||||
The [*Writer.AddFS] implementation now supports symbolic links
|
||||
for filesystems that implement [io/fs.ReadLinkFS].
|
1
doc/next/6-stdlib/99-minor/io/fs/49580.md
Normal file
1
doc/next/6-stdlib/99-minor/io/fs/49580.md
Normal file
@ -0,0 +1 @@
|
||||
A new [ReadLinkFS] interface provides the ability to read symbolic links in a filesystem.
|
2
doc/next/6-stdlib/99-minor/os/49580.md
Normal file
2
doc/next/6-stdlib/99-minor/os/49580.md
Normal file
@ -0,0 +1,2 @@
|
||||
The filesystem returned by [DirFS] implements the new [io/fs.ReadLinkFS] interface.
|
||||
[CopyFS] supports symlinks when copying filesystems that implement [io/fs.ReadLinkFS].
|
3
doc/next/6-stdlib/99-minor/testing/fstest/49580.md
Normal file
3
doc/next/6-stdlib/99-minor/testing/fstest/49580.md
Normal file
@ -0,0 +1,3 @@
|
||||
[MapFS] implements the new [io/fs.ReadLinkFS] interface.
|
||||
[TestFS] will verify the functionality of the [io/fs.ReadLinkFS] interface if implemented.
|
||||
[TestFS] will no longer follow symlinks to avoid unbounded recursion.
|
@ -415,11 +415,17 @@ func (tw *Writer) AddFS(fsys fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(#49580): Handle symlinks when fs.ReadLinkFS is available.
|
||||
if !d.IsDir() && !info.Mode().IsRegular() {
|
||||
linkTarget := ""
|
||||
if typ := d.Type(); typ == fs.ModeSymlink {
|
||||
var err error
|
||||
linkTarget, err = fs.ReadLink(fsys, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !typ.IsRegular() && typ != fs.ModeDir {
|
||||
return errors.New("tar: cannot add non-regular file")
|
||||
}
|
||||
h, err := FileInfoHeader(info, "")
|
||||
h, err := FileInfoHeader(info, linkTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -430,7 +436,7 @@ func (tw *Writer) AddFS(fsys fs.FS) error {
|
||||
if err := tw.WriteHeader(h); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if !d.Type().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
f, err := fsys.Open(name)
|
||||
|
@ -1342,6 +1342,7 @@ func TestWriterAddFS(t *testing.T) {
|
||||
"emptyfolder": {Mode: 0o755 | os.ModeDir},
|
||||
"file.go": {Data: []byte("hello")},
|
||||
"subfolder/another.go": {Data: []byte("world")},
|
||||
"symlink.go": {Mode: 0o777 | os.ModeSymlink, Data: []byte("file.go")},
|
||||
// Notably missing here is the "subfolder" directory. This makes sure even
|
||||
// if we don't have a subfolder directory listed.
|
||||
}
|
||||
@ -1370,7 +1371,7 @@ func TestWriterAddFS(t *testing.T) {
|
||||
for _, name := range names {
|
||||
entriesLeft--
|
||||
|
||||
entryInfo, err := fsys.Stat(name)
|
||||
entryInfo, err := fsys.Lstat(name)
|
||||
if err != nil {
|
||||
t.Fatalf("getting entry info error: %v", err)
|
||||
}
|
||||
@ -1396,18 +1397,23 @@ func TestWriterAddFS(t *testing.T) {
|
||||
name, entryInfo.Mode(), hdr.FileInfo().Mode())
|
||||
}
|
||||
|
||||
if entryInfo.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origdata := fsys[name].Data
|
||||
if string(data) != string(origdata) {
|
||||
t.Fatalf("test fs has file content %v; archive header has %v",
|
||||
data, origdata)
|
||||
switch entryInfo.Mode().Type() {
|
||||
case fs.ModeDir:
|
||||
// No additional checks necessary.
|
||||
case fs.ModeSymlink:
|
||||
origtarget := string(fsys[name].Data)
|
||||
if hdr.Linkname != origtarget {
|
||||
t.Fatalf("test fs has link content %s; archive header %v", origtarget, hdr.Linkname)
|
||||
}
|
||||
default:
|
||||
data, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
origdata := fsys[name].Data
|
||||
if string(data) != string(origdata) {
|
||||
t.Fatalf("test fs has file content %v; archive header has %v", origdata, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
if entriesLeft > 0 {
|
||||
|
45
src/io/fs/readlink.go
Normal file
45
src/io/fs/readlink.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright 2023 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
|
||||
|
||||
// ReadLinkFS is the interface implemented by a file system
|
||||
// that supports reading symbolic links.
|
||||
type ReadLinkFS interface {
|
||||
FS
|
||||
|
||||
// ReadLink returns the destination of the named symbolic link.
|
||||
// If there is an error, it should be of type [*PathError].
|
||||
ReadLink(name string) (string, error)
|
||||
|
||||
// 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.
|
||||
// If there is an error, it should be of type [*PathError].
|
||||
Lstat(name string) (FileInfo, error)
|
||||
}
|
||||
|
||||
// ReadLink returns the destination of the named symbolic link.
|
||||
//
|
||||
// If fsys does not implement [ReadLinkFS], then ReadLink returns an error.
|
||||
func ReadLink(fsys FS, name string) (string, error) {
|
||||
sym, ok := fsys.(ReadLinkFS)
|
||||
if !ok {
|
||||
return "", &PathError{Op: "readlink", Path: name, Err: ErrInvalid}
|
||||
}
|
||||
return sym.ReadLink(name)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// If fsys does not implement [ReadLinkFS], then Lstat is identical to [Stat].
|
||||
func Lstat(fsys FS, name string) (FileInfo, error) {
|
||||
sym, ok := fsys.(ReadLinkFS)
|
||||
if !ok {
|
||||
return Stat(fsys, name)
|
||||
}
|
||||
return sym.Lstat(name)
|
||||
}
|
106
src/io/fs/readlink_test.go
Normal file
106
src/io/fs/readlink_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2023 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"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestReadLink(t *testing.T) {
|
||||
testFS := fstest.MapFS{
|
||||
"foo": {
|
||||
Data: []byte("bar"),
|
||||
Mode: ModeSymlink | 0o777,
|
||||
},
|
||||
"bar": {
|
||||
Data: []byte("Hello, World!\n"),
|
||||
Mode: 0o644,
|
||||
},
|
||||
|
||||
"dir/parentlink": {
|
||||
Data: []byte("../bar"),
|
||||
Mode: ModeSymlink | 0o777,
|
||||
},
|
||||
"dir/link": {
|
||||
Data: []byte("file"),
|
||||
Mode: ModeSymlink | 0o777,
|
||||
},
|
||||
"dir/file": {
|
||||
Data: []byte("Hello, World!\n"),
|
||||
Mode: 0o644,
|
||||
},
|
||||
}
|
||||
|
||||
check := func(fsys FS, name string, want string) {
|
||||
t.Helper()
|
||||
got, err := ReadLink(fsys, name)
|
||||
if got != want || err != nil {
|
||||
t.Errorf("ReadLink(%q) = %q, %v; want %q, <nil>", name, got, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
check(testFS, "foo", "bar")
|
||||
check(testFS, "dir/parentlink", "../bar")
|
||||
check(testFS, "dir/link", "file")
|
||||
|
||||
// Test that ReadLink on Sub works.
|
||||
sub, err := Sub(testFS, "dir")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
check(sub, "link", "file")
|
||||
check(sub, "parentlink", "../bar")
|
||||
}
|
||||
|
||||
func TestLstat(t *testing.T) {
|
||||
testFS := fstest.MapFS{
|
||||
"foo": {
|
||||
Data: []byte("bar"),
|
||||
Mode: ModeSymlink | 0o777,
|
||||
},
|
||||
"bar": {
|
||||
Data: []byte("Hello, World!\n"),
|
||||
Mode: 0o644,
|
||||
},
|
||||
|
||||
"dir/parentlink": {
|
||||
Data: []byte("../bar"),
|
||||
Mode: ModeSymlink | 0o777,
|
||||
},
|
||||
"dir/link": {
|
||||
Data: []byte("file"),
|
||||
Mode: ModeSymlink | 0o777,
|
||||
},
|
||||
"dir/file": {
|
||||
Data: []byte("Hello, World!\n"),
|
||||
Mode: 0o644,
|
||||
},
|
||||
}
|
||||
|
||||
check := func(fsys FS, name string, want FileMode) {
|
||||
t.Helper()
|
||||
info, err := Lstat(fsys, name)
|
||||
var got FileMode
|
||||
if err == nil {
|
||||
got = info.Mode()
|
||||
}
|
||||
if got != want || err != nil {
|
||||
t.Errorf("Lstat(%q) = %v, %v; want %v, <nil>", name, got, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
check(testFS, "foo", ModeSymlink|0o777)
|
||||
check(testFS, "bar", 0o644)
|
||||
|
||||
// Test that Lstat on Sub works.
|
||||
sub, err := Sub(testFS, "dir")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check(sub, "link", ModeSymlink|0o777)
|
||||
}
|
@ -23,7 +23,8 @@ type SubFS interface {
|
||||
// Otherwise, if fs implements [SubFS], Sub returns fsys.Sub(dir).
|
||||
// Otherwise, Sub returns a new [FS] implementation sub that,
|
||||
// in effect, implements sub.Open(name) as fsys.Open(path.Join(dir, name)).
|
||||
// The implementation also translates calls to ReadDir, ReadFile, and Glob appropriately.
|
||||
// The implementation also translates calls to ReadDir, ReadFile,
|
||||
// ReadLink, Lstat, 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
|
||||
@ -44,6 +45,12 @@ func Sub(fsys FS, dir string) (FS, error) {
|
||||
return &subFS{fsys, dir}, nil
|
||||
}
|
||||
|
||||
var _ FS = (*subFS)(nil)
|
||||
var _ ReadDirFS = (*subFS)(nil)
|
||||
var _ ReadFileFS = (*subFS)(nil)
|
||||
var _ ReadLinkFS = (*subFS)(nil)
|
||||
var _ GlobFS = (*subFS)(nil)
|
||||
|
||||
type subFS struct {
|
||||
fsys FS
|
||||
dir string
|
||||
@ -105,6 +112,30 @@ func (f *subFS) ReadFile(name string) ([]byte, error) {
|
||||
return data, f.fixErr(err)
|
||||
}
|
||||
|
||||
func (f *subFS) ReadLink(name string) (string, error) {
|
||||
full, err := f.fullName("readlink", name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
target, err := ReadLink(f.fsys, full)
|
||||
if err != nil {
|
||||
return "", f.fixErr(err)
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (f *subFS) Lstat(name string) (FileInfo, error) {
|
||||
full, err := f.fullName("lstat", name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := Lstat(f.fsys, full)
|
||||
if err != nil {
|
||||
return nil, f.fixErr(err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (f *subFS) Glob(pattern string) ([]string, error) {
|
||||
// Check pattern is well-formed.
|
||||
if _, err := path.Match(pattern, ""); err != nil {
|
||||
|
@ -110,6 +110,47 @@ func TestWalkDir(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWalkDirSymlink(t *testing.T) {
|
||||
fsys := fstest.MapFS{
|
||||
"link": {Data: []byte("dir"), Mode: ModeSymlink},
|
||||
"dir/a": {},
|
||||
"dir/b/c": {},
|
||||
"dir/d": {Data: []byte("b"), Mode: ModeSymlink},
|
||||
}
|
||||
|
||||
wantTypes := map[string]FileMode{
|
||||
"link": ModeDir,
|
||||
"link/a": 0,
|
||||
"link/b": ModeDir,
|
||||
"link/b/c": 0,
|
||||
"link/d": ModeSymlink,
|
||||
}
|
||||
marks := make(map[string]int)
|
||||
walkFn := func(path string, entry DirEntry, err error) error {
|
||||
marks[path]++
|
||||
if want, ok := wantTypes[path]; !ok {
|
||||
t.Errorf("Unexpected path %q in walk", path)
|
||||
} else if got := entry.Type(); got != want {
|
||||
t.Errorf("%s entry type = %v; want %v", path, got, want)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Walking %s: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expect no errors.
|
||||
err := WalkDir(fsys, "link", walkFn)
|
||||
if err != nil {
|
||||
t.Fatalf("no error expected, found: %s", err)
|
||||
}
|
||||
for path := range wantTypes {
|
||||
if got := marks[path]; got != 1 {
|
||||
t.Errorf("%s visited %d times; expected 1", path, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue51617(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, sub := range []string{"a", filepath.Join("a", "bad"), filepath.Join("a", "next")} {
|
||||
|
@ -140,9 +140,6 @@ func ReadDir(name string) ([]DirEntry, error) {
|
||||
// already exists in the destination, CopyFS will return an error
|
||||
// such that errors.Is(err, fs.ErrExist) will be true.
|
||||
//
|
||||
// Symbolic links in fsys are not supported. A *PathError with Err set
|
||||
// to ErrInvalid is returned when copying from a symbolic link.
|
||||
//
|
||||
// Symbolic links in dir are followed.
|
||||
//
|
||||
// New files added to fsys (including if dir is a subdirectory of fsys)
|
||||
@ -160,35 +157,38 @@ func CopyFS(dir string, fsys fs.FS) error {
|
||||
return err
|
||||
}
|
||||
newPath := joinPath(dir, fpath)
|
||||
if d.IsDir() {
|
||||
return MkdirAll(newPath, 0777)
|
||||
}
|
||||
|
||||
// TODO(panjf2000): handle symlinks with the help of fs.ReadLinkFS
|
||||
// once https://go.dev/issue/49580 is done.
|
||||
// we also need filepathlite.IsLocal from https://go.dev/cl/564295.
|
||||
if !d.Type().IsRegular() {
|
||||
switch d.Type() {
|
||||
case ModeDir:
|
||||
return MkdirAll(newPath, 0777)
|
||||
case ModeSymlink:
|
||||
target, err := fs.ReadLink(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Symlink(target, newPath)
|
||||
case 0:
|
||||
r, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
info, err := r.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := OpenFile(newPath, O_CREATE|O_EXCL|O_WRONLY, 0666|info.Mode()&0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
w.Close()
|
||||
return &PathError{Op: "Copy", Path: newPath, Err: err}
|
||||
}
|
||||
return w.Close()
|
||||
default:
|
||||
return &PathError{Op: "CopyFS", Path: path, Err: ErrInvalid}
|
||||
}
|
||||
|
||||
r, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
info, err := r.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := OpenFile(newPath, O_CREATE|O_EXCL|O_WRONLY, 0666|info.Mode()&0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
w.Close()
|
||||
return &PathError{Op: "Copy", Path: newPath, Err: err}
|
||||
}
|
||||
return w.Close()
|
||||
})
|
||||
}
|
||||
|
@ -700,12 +700,17 @@ func (f *File) SyscallConn() (syscall.RawConn, error) {
|
||||
//
|
||||
// The directory dir must not be "".
|
||||
//
|
||||
// The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
|
||||
// [io/fs.ReadDirFS].
|
||||
// The result implements [io/fs.StatFS], [io/fs.ReadFileFS], [io/fs.ReadDirFS], and
|
||||
// [io/fs.ReadLinkFS].
|
||||
func DirFS(dir string) fs.FS {
|
||||
return dirFS(dir)
|
||||
}
|
||||
|
||||
var _ fs.StatFS = dirFS("")
|
||||
var _ fs.ReadFileFS = dirFS("")
|
||||
var _ fs.ReadDirFS = dirFS("")
|
||||
var _ fs.ReadLinkFS = dirFS("")
|
||||
|
||||
type dirFS string
|
||||
|
||||
func (dir dirFS) Open(name string) (fs.File, error) {
|
||||
@ -777,6 +782,28 @@ func (dir dirFS) Stat(name string) (fs.FileInfo, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (dir dirFS) Lstat(name string) (fs.FileInfo, error) {
|
||||
fullname, err := dir.join(name)
|
||||
if err != nil {
|
||||
return nil, &PathError{Op: "lstat", Path: name, Err: err}
|
||||
}
|
||||
f, err := Lstat(fullname)
|
||||
if err != nil {
|
||||
// See comment in dirFS.Open.
|
||||
err.(*PathError).Path = name
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (dir dirFS) ReadLink(name string) (string, error) {
|
||||
fullname, err := dir.join(name)
|
||||
if err != nil {
|
||||
return "", &PathError{Op: "readlink", Path: name, Err: err}
|
||||
}
|
||||
return Readlink(fullname)
|
||||
}
|
||||
|
||||
// join returns the path for name in dir.
|
||||
func (dir dirFS) join(name string) (string, error) {
|
||||
if dir == "" {
|
||||
|
156
src/os/file_test.go
Normal file
156
src/os/file_test.go
Normal file
@ -0,0 +1,156 @@
|
||||
// Copyright 2023 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 (
|
||||
"internal/testenv"
|
||||
"io/fs"
|
||||
. "os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDirFSReadLink(t *testing.T) {
|
||||
testenv.MustHaveSymlink(t)
|
||||
|
||||
root := t.TempDir()
|
||||
subdir := filepath.Join(root, "dir")
|
||||
if err := Mkdir(subdir, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
links := map[string]string{
|
||||
filepath.Join(root, "parent-link"): filepath.Join("..", "foo"),
|
||||
filepath.Join(root, "sneaky-parent-link"): filepath.Join("dir", "..", "..", "foo"),
|
||||
filepath.Join(root, "abs-link"): filepath.Join(root, "foo"),
|
||||
filepath.Join(root, "rel-link"): "foo",
|
||||
filepath.Join(root, "rel-sub-link"): filepath.Join("dir", "foo"),
|
||||
filepath.Join(subdir, "parent-link"): filepath.Join("..", "foo"),
|
||||
}
|
||||
for newname, oldname := range links {
|
||||
if err := Symlink(oldname, newname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fsys := DirFS(root)
|
||||
want := map[string]string{
|
||||
"rel-link": "foo",
|
||||
"rel-sub-link": filepath.Join("dir", "foo"),
|
||||
"dir/parent-link": filepath.Join("..", "foo"),
|
||||
"parent-link": filepath.Join("..", "foo"),
|
||||
"sneaky-parent-link": filepath.Join("dir", "..", "..", "foo"),
|
||||
"abs-link": filepath.Join(root, "foo"),
|
||||
}
|
||||
for name, want := range want {
|
||||
got, err := fs.ReadLink(fsys, name)
|
||||
if got != want || err != nil {
|
||||
t.Errorf("fs.ReadLink(fsys, %q) = %q, %v; want %q, <nil>", name, got, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirFSLstat(t *testing.T) {
|
||||
testenv.MustHaveSymlink(t)
|
||||
|
||||
root := t.TempDir()
|
||||
subdir := filepath.Join(root, "dir")
|
||||
if err := Mkdir(subdir, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Symlink("dir", filepath.Join(root, "link")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fsys := DirFS(root)
|
||||
want := map[string]fs.FileMode{
|
||||
"link": fs.ModeSymlink,
|
||||
"dir": fs.ModeDir,
|
||||
}
|
||||
for name, want := range want {
|
||||
info, err := fs.Lstat(fsys, name)
|
||||
var got fs.FileMode
|
||||
if info != nil {
|
||||
got = info.Mode().Type()
|
||||
}
|
||||
if got != want || err != nil {
|
||||
t.Errorf("fs.Lstat(fsys, %q).Mode().Type() = %v, %v; want %v, <nil>", name, got, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirFSWalkDir(t *testing.T) {
|
||||
testenv.MustHaveSymlink(t)
|
||||
|
||||
root := t.TempDir()
|
||||
subdir := filepath.Join(root, "dir")
|
||||
if err := Mkdir(subdir, 0o777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Symlink("dir", filepath.Join(root, "link")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteFile(filepath.Join(root, "dir", "a"), nil, 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fsys := DirFS(root)
|
||||
|
||||
t.Run("SymlinkRoot", func(t *testing.T) {
|
||||
wantTypes := map[string]fs.FileMode{
|
||||
"link": fs.ModeDir,
|
||||
"link/a": 0,
|
||||
}
|
||||
marks := make(map[string]int)
|
||||
err := fs.WalkDir(fsys, "link", func(path string, entry fs.DirEntry, err error) error {
|
||||
marks[path]++
|
||||
if want, ok := wantTypes[path]; !ok {
|
||||
t.Errorf("Unexpected path %q in walk", path)
|
||||
} else if got := entry.Type(); got != want {
|
||||
t.Errorf("%s entry type = %v; want %v", path, got, want)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for path := range wantTypes {
|
||||
if got := marks[path]; got != 1 {
|
||||
t.Errorf("%s visited %d times; expected 1", path, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SymlinkPresent", func(t *testing.T) {
|
||||
wantTypes := map[string]fs.FileMode{
|
||||
".": fs.ModeDir,
|
||||
"dir": fs.ModeDir,
|
||||
"dir/a": 0,
|
||||
"link": fs.ModeSymlink,
|
||||
}
|
||||
marks := make(map[string]int)
|
||||
err := fs.WalkDir(fsys, ".", func(path string, entry fs.DirEntry, err error) error {
|
||||
marks[path]++
|
||||
if want, ok := wantTypes[path]; !ok {
|
||||
t.Errorf("Unexpected path %q in walk", path)
|
||||
} else if got := entry.Type(); got != want {
|
||||
t.Errorf("%s entry type = %v; want %v", path, got, want)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", path, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for path := range wantTypes {
|
||||
if got := marks[path]; got != 1 {
|
||||
t.Errorf("%s visited %d times; expected 1", path, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -3725,13 +3725,9 @@ func TestCopyFSWithSymlinks(t *testing.T) {
|
||||
t.Fatalf("Mkdir: %v", err)
|
||||
}
|
||||
|
||||
// TODO(panjf2000): symlinks are currently not supported, and a specific error
|
||||
// will be returned. Verify that error and skip the subsequent test,
|
||||
// revisit this once #49580 is closed.
|
||||
if err := CopyFS(tmpDupDir, fsys); !errors.Is(err, ErrInvalid) {
|
||||
t.Fatalf("got %v, want ErrInvalid", err)
|
||||
if err := CopyFS(tmpDupDir, fsys); err != nil {
|
||||
t.Fatalf("CopyFS: %v", err)
|
||||
}
|
||||
t.Skip("skip the subsequent test and wait for #49580")
|
||||
|
||||
forceMFTUpdateOnWindows(t, tmpDupDir)
|
||||
tmpFsys := DirFS(tmpDupDir)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user