Skip to content

Commit f7b8dd9

Browse files
zombiezencherrymui
authored andcommitted
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 <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Daniel Martí <[email protected]> Reviewed-by: Bryan Mills <[email protected]> Reviewed-by: Cherry Mui <[email protected]> Reviewed-by: Quim Muntal <[email protected]> Reviewed-by: Funda Secgin <[email protected]>
1 parent 9896da3 commit f7b8dd9

File tree

19 files changed

+688
-67
lines changed

19 files changed

+688
-67
lines changed

api/next/49580.txt

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pkg io/fs, func Lstat(FS, string) (FileInfo, error) #49580
2+
pkg io/fs, func ReadLink(FS, string) (string, error) #49580
3+
pkg io/fs, type ReadLinkFS interface { Lstat, Open, ReadLink } #49580
4+
pkg io/fs, type ReadLinkFS interface, Lstat(string) (FileInfo, error) #49580
5+
pkg io/fs, type ReadLinkFS interface, Open(string) (File, error) #49580
6+
pkg io/fs, type ReadLinkFS interface, ReadLink(string) (string, error) #49580
7+
pkg testing/fstest, method (MapFS) Lstat(string) (fs.FileInfo, error) #49580
8+
pkg testing/fstest, method (MapFS) ReadLink(string) (string, error) #49580
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The [*Writer.AddFS] implementation now supports symbolic links
2+
for filesystems that implement [io/fs.ReadLinkFS].
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A new [ReadLinkFS] interface provides the ability to read symbolic links in a filesystem.
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The filesystem returned by [DirFS] implements the new [io/fs.ReadLinkFS] interface.
2+
[CopyFS] supports symlinks when copying filesystems that implement [io/fs.ReadLinkFS].
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[MapFS] implements the new [io/fs.ReadLinkFS] interface.
2+
[TestFS] will verify the functionality of the [io/fs.ReadLinkFS] interface if implemented.
3+
[TestFS] will no longer follow symlinks to avoid unbounded recursion.

src/archive/tar/writer.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -415,11 +415,17 @@ func (tw *Writer) AddFS(fsys fs.FS) error {
415415
if err != nil {
416416
return err
417417
}
418-
// TODO(#49580): Handle symlinks when fs.ReadLinkFS is available.
419-
if !d.IsDir() && !info.Mode().IsRegular() {
418+
linkTarget := ""
419+
if typ := d.Type(); typ == fs.ModeSymlink {
420+
var err error
421+
linkTarget, err = fs.ReadLink(fsys, name)
422+
if err != nil {
423+
return err
424+
}
425+
} else if !typ.IsRegular() && typ != fs.ModeDir {
420426
return errors.New("tar: cannot add non-regular file")
421427
}
422-
h, err := FileInfoHeader(info, "")
428+
h, err := FileInfoHeader(info, linkTarget)
423429
if err != nil {
424430
return err
425431
}
@@ -430,7 +436,7 @@ func (tw *Writer) AddFS(fsys fs.FS) error {
430436
if err := tw.WriteHeader(h); err != nil {
431437
return err
432438
}
433-
if d.IsDir() {
439+
if !d.Type().IsRegular() {
434440
return nil
435441
}
436442
f, err := fsys.Open(name)

src/archive/tar/writer_test.go

+19-13
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,7 @@ func TestWriterAddFS(t *testing.T) {
13421342
"emptyfolder": {Mode: 0o755 | os.ModeDir},
13431343
"file.go": {Data: []byte("hello")},
13441344
"subfolder/another.go": {Data: []byte("world")},
1345+
"symlink.go": {Mode: 0o777 | os.ModeSymlink, Data: []byte("file.go")},
13451346
// Notably missing here is the "subfolder" directory. This makes sure even
13461347
// if we don't have a subfolder directory listed.
13471348
}
@@ -1370,7 +1371,7 @@ func TestWriterAddFS(t *testing.T) {
13701371
for _, name := range names {
13711372
entriesLeft--
13721373

1373-
entryInfo, err := fsys.Stat(name)
1374+
entryInfo, err := fsys.Lstat(name)
13741375
if err != nil {
13751376
t.Fatalf("getting entry info error: %v", err)
13761377
}
@@ -1396,18 +1397,23 @@ func TestWriterAddFS(t *testing.T) {
13961397
name, entryInfo.Mode(), hdr.FileInfo().Mode())
13971398
}
13981399

1399-
if entryInfo.IsDir() {
1400-
continue
1401-
}
1402-
1403-
data, err := io.ReadAll(tr)
1404-
if err != nil {
1405-
t.Fatal(err)
1406-
}
1407-
origdata := fsys[name].Data
1408-
if string(data) != string(origdata) {
1409-
t.Fatalf("test fs has file content %v; archive header has %v",
1410-
data, origdata)
1400+
switch entryInfo.Mode().Type() {
1401+
case fs.ModeDir:
1402+
// No additional checks necessary.
1403+
case fs.ModeSymlink:
1404+
origtarget := string(fsys[name].Data)
1405+
if hdr.Linkname != origtarget {
1406+
t.Fatalf("test fs has link content %s; archive header %v", origtarget, hdr.Linkname)
1407+
}
1408+
default:
1409+
data, err := io.ReadAll(tr)
1410+
if err != nil {
1411+
t.Fatal(err)
1412+
}
1413+
origdata := fsys[name].Data
1414+
if string(data) != string(origdata) {
1415+
t.Fatalf("test fs has file content %v; archive header has %v", origdata, data)
1416+
}
14111417
}
14121418
}
14131419
if entriesLeft > 0 {

src/io/fs/readlink.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package fs
6+
7+
// ReadLinkFS is the interface implemented by a file system
8+
// that supports reading symbolic links.
9+
type ReadLinkFS interface {
10+
FS
11+
12+
// ReadLink returns the destination of the named symbolic link.
13+
// If there is an error, it should be of type [*PathError].
14+
ReadLink(name string) (string, error)
15+
16+
// Lstat returns a [FileInfo] describing the named file.
17+
// If the file is a symbolic link, the returned [FileInfo] describes the symbolic link.
18+
// Lstat makes no attempt to follow the link.
19+
// If there is an error, it should be of type [*PathError].
20+
Lstat(name string) (FileInfo, error)
21+
}
22+
23+
// ReadLink returns the destination of the named symbolic link.
24+
//
25+
// If fsys does not implement [ReadLinkFS], then ReadLink returns an error.
26+
func ReadLink(fsys FS, name string) (string, error) {
27+
sym, ok := fsys.(ReadLinkFS)
28+
if !ok {
29+
return "", &PathError{Op: "readlink", Path: name, Err: ErrInvalid}
30+
}
31+
return sym.ReadLink(name)
32+
}
33+
34+
// Lstat returns a [FileInfo] describing the named file.
35+
// If the file is a symbolic link, the returned [FileInfo] describes the symbolic link.
36+
// Lstat makes no attempt to follow the link.
37+
//
38+
// If fsys does not implement [ReadLinkFS], then Lstat is identical to [Stat].
39+
func Lstat(fsys FS, name string) (FileInfo, error) {
40+
sym, ok := fsys.(ReadLinkFS)
41+
if !ok {
42+
return Stat(fsys, name)
43+
}
44+
return sym.Lstat(name)
45+
}

src/io/fs/readlink_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package fs_test
6+
7+
import (
8+
. "io/fs"
9+
"testing"
10+
"testing/fstest"
11+
)
12+
13+
func TestReadLink(t *testing.T) {
14+
testFS := fstest.MapFS{
15+
"foo": {
16+
Data: []byte("bar"),
17+
Mode: ModeSymlink | 0o777,
18+
},
19+
"bar": {
20+
Data: []byte("Hello, World!\n"),
21+
Mode: 0o644,
22+
},
23+
24+
"dir/parentlink": {
25+
Data: []byte("../bar"),
26+
Mode: ModeSymlink | 0o777,
27+
},
28+
"dir/link": {
29+
Data: []byte("file"),
30+
Mode: ModeSymlink | 0o777,
31+
},
32+
"dir/file": {
33+
Data: []byte("Hello, World!\n"),
34+
Mode: 0o644,
35+
},
36+
}
37+
38+
check := func(fsys FS, name string, want string) {
39+
t.Helper()
40+
got, err := ReadLink(fsys, name)
41+
if got != want || err != nil {
42+
t.Errorf("ReadLink(%q) = %q, %v; want %q, <nil>", name, got, err, want)
43+
}
44+
}
45+
46+
check(testFS, "foo", "bar")
47+
check(testFS, "dir/parentlink", "../bar")
48+
check(testFS, "dir/link", "file")
49+
50+
// Test that ReadLink on Sub works.
51+
sub, err := Sub(testFS, "dir")
52+
if err != nil {
53+
t.Fatal(err)
54+
}
55+
56+
check(sub, "link", "file")
57+
check(sub, "parentlink", "../bar")
58+
}
59+
60+
func TestLstat(t *testing.T) {
61+
testFS := fstest.MapFS{
62+
"foo": {
63+
Data: []byte("bar"),
64+
Mode: ModeSymlink | 0o777,
65+
},
66+
"bar": {
67+
Data: []byte("Hello, World!\n"),
68+
Mode: 0o644,
69+
},
70+
71+
"dir/parentlink": {
72+
Data: []byte("../bar"),
73+
Mode: ModeSymlink | 0o777,
74+
},
75+
"dir/link": {
76+
Data: []byte("file"),
77+
Mode: ModeSymlink | 0o777,
78+
},
79+
"dir/file": {
80+
Data: []byte("Hello, World!\n"),
81+
Mode: 0o644,
82+
},
83+
}
84+
85+
check := func(fsys FS, name string, want FileMode) {
86+
t.Helper()
87+
info, err := Lstat(fsys, name)
88+
var got FileMode
89+
if err == nil {
90+
got = info.Mode()
91+
}
92+
if got != want || err != nil {
93+
t.Errorf("Lstat(%q) = %v, %v; want %v, <nil>", name, got, err, want)
94+
}
95+
}
96+
97+
check(testFS, "foo", ModeSymlink|0o777)
98+
check(testFS, "bar", 0o644)
99+
100+
// Test that Lstat on Sub works.
101+
sub, err := Sub(testFS, "dir")
102+
if err != nil {
103+
t.Fatal(err)
104+
}
105+
check(sub, "link", ModeSymlink|0o777)
106+
}

src/io/fs/sub.go

+32-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ type SubFS interface {
2323
// Otherwise, if fs implements [SubFS], Sub returns fsys.Sub(dir).
2424
// Otherwise, Sub returns a new [FS] implementation sub that,
2525
// in effect, implements sub.Open(name) as fsys.Open(path.Join(dir, name)).
26-
// The implementation also translates calls to ReadDir, ReadFile, and Glob appropriately.
26+
// The implementation also translates calls to ReadDir, ReadFile,
27+
// ReadLink, Lstat, and Glob appropriately.
2728
//
2829
// Note that Sub(os.DirFS("/"), "prefix") is equivalent to os.DirFS("/prefix")
2930
// and that neither of them guarantees to avoid operating system
@@ -44,6 +45,12 @@ func Sub(fsys FS, dir string) (FS, error) {
4445
return &subFS{fsys, dir}, nil
4546
}
4647

48+
var _ FS = (*subFS)(nil)
49+
var _ ReadDirFS = (*subFS)(nil)
50+
var _ ReadFileFS = (*subFS)(nil)
51+
var _ ReadLinkFS = (*subFS)(nil)
52+
var _ GlobFS = (*subFS)(nil)
53+
4754
type subFS struct {
4855
fsys FS
4956
dir string
@@ -105,6 +112,30 @@ func (f *subFS) ReadFile(name string) ([]byte, error) {
105112
return data, f.fixErr(err)
106113
}
107114

115+
func (f *subFS) ReadLink(name string) (string, error) {
116+
full, err := f.fullName("readlink", name)
117+
if err != nil {
118+
return "", err
119+
}
120+
target, err := ReadLink(f.fsys, full)
121+
if err != nil {
122+
return "", f.fixErr(err)
123+
}
124+
return target, nil
125+
}
126+
127+
func (f *subFS) Lstat(name string) (FileInfo, error) {
128+
full, err := f.fullName("lstat", name)
129+
if err != nil {
130+
return nil, err
131+
}
132+
info, err := Lstat(f.fsys, full)
133+
if err != nil {
134+
return nil, f.fixErr(err)
135+
}
136+
return info, nil
137+
}
138+
108139
func (f *subFS) Glob(pattern string) ([]string, error) {
109140
// Check pattern is well-formed.
110141
if _, err := path.Match(pattern, ""); err != nil {

src/io/fs/walk_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,47 @@ func TestWalkDir(t *testing.T) {
110110
})
111111
}
112112

113+
func TestWalkDirSymlink(t *testing.T) {
114+
fsys := fstest.MapFS{
115+
"link": {Data: []byte("dir"), Mode: ModeSymlink},
116+
"dir/a": {},
117+
"dir/b/c": {},
118+
"dir/d": {Data: []byte("b"), Mode: ModeSymlink},
119+
}
120+
121+
wantTypes := map[string]FileMode{
122+
"link": ModeDir,
123+
"link/a": 0,
124+
"link/b": ModeDir,
125+
"link/b/c": 0,
126+
"link/d": ModeSymlink,
127+
}
128+
marks := make(map[string]int)
129+
walkFn := func(path string, entry DirEntry, err error) error {
130+
marks[path]++
131+
if want, ok := wantTypes[path]; !ok {
132+
t.Errorf("Unexpected path %q in walk", path)
133+
} else if got := entry.Type(); got != want {
134+
t.Errorf("%s entry type = %v; want %v", path, got, want)
135+
}
136+
if err != nil {
137+
t.Errorf("Walking %s: %v", path, err)
138+
}
139+
return nil
140+
}
141+
142+
// Expect no errors.
143+
err := WalkDir(fsys, "link", walkFn)
144+
if err != nil {
145+
t.Fatalf("no error expected, found: %s", err)
146+
}
147+
for path := range wantTypes {
148+
if got := marks[path]; got != 1 {
149+
t.Errorf("%s visited %d times; expected 1", path, got)
150+
}
151+
}
152+
}
153+
113154
func TestIssue51617(t *testing.T) {
114155
dir := t.TempDir()
115156
for _, sub := range []string{"a", filepath.Join("a", "bad"), filepath.Join("a", "next")} {

0 commit comments

Comments
 (0)