Skip to content

Commit 14bb35b

Browse files
authored
Merge pull request #195 from puerco/clean-paths
Use path.Clean instead of filepath.Clean in iofs.Open
2 parents 6d0bae5 + 0d56dc0 commit 14bb35b

2 files changed

Lines changed: 57 additions & 9 deletions

File tree

helper/iofs/iofs.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package iofs
55
import (
66
"io"
77
"io/fs"
8-
"path/filepath"
8+
"strings"
99

1010
billyfs "github.com/go-git/go-billy/v6"
1111
"github.com/go-git/go-billy/v6/helper/polyfill"
@@ -30,9 +30,16 @@ var (
3030

3131
// TODO: implement fs.GlobFS, which will be a fair bit more code.
3232

33+
// validPath checks that name is a valid io/fs path. In addition to the
34+
// standard fs.ValidPath checks, it rejects backslashes which on Windows
35+
// would be interpreted as path separators by the underlying billy filesystem.
36+
func validPath(name string) bool {
37+
return fs.ValidPath(name) && !strings.Contains(name, `\`)
38+
}
39+
3340
// Open opens the named file on the underlying FS, implementing fs.FS (returning a file or error).
3441
func (a *adapterFs) Open(name string) (fs.File, error) {
35-
if name[0] == '/' || name != filepath.Clean(name) {
42+
if !validPath(name) {
3643
// fstest.TestFS explicitly checks that these should return error.
3744
// MemFS performs the clean internally, so we need to block that here for testing purposes.
3845
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
@@ -54,16 +61,25 @@ func (a *adapterFs) Open(name string) (fs.File, error) {
5461

5562
// ReadDir reads the named directory, implementing fs.ReadDirFS (returning a listing or error).
5663
func (a *adapterFs) ReadDir(name string) ([]fs.DirEntry, error) {
64+
if !validPath(name) {
65+
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
66+
}
5767
return a.fs.ReadDir(name)
5868
}
5969

6070
// Stat returns information on the named file, implementing fs.StatFS (returning FileInfo or error).
6171
func (a *adapterFs) Stat(name string) (fs.FileInfo, error) {
72+
if !validPath(name) {
73+
return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
74+
}
6275
return a.fs.Stat(name)
6376
}
6477

6578
// ReadFile reads the named file and returns its contents, implementing fs.ReadFileFS (returning contents or error).
6679
func (a *adapterFs) ReadFile(name string) ([]byte, error) {
80+
if !validPath(name) {
81+
return nil, &fs.PathError{Op: "readfile", Path: name, Err: fs.ErrInvalid}
82+
}
6783
stat, err := a.fs.Stat(name)
6884
if err != nil {
6985
return nil, err

helper/iofs/iofs_test.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,58 @@ func TestWithFSTest(t *testing.T) {
2525
iofs := New(memfs)
2626

2727
files := map[string]string{
28-
"foo.txt": "hello, world",
29-
"bar.txt": "goodbye, world",
30-
filepath.Join("dir", "baz.txt"): "こんにちわ, world",
28+
"foo.txt": "hello, world",
29+
"bar.txt": "goodbye, world",
30+
"dir/baz.txt": "こんにちわ, world",
3131
}
3232
createdFiles := make([]string, 0, len(files))
3333
for filename, contents := range files {
3434
makeFile(t, memfs, filename, contents)
3535
createdFiles = append(createdFiles, filename)
3636
}
3737

38-
if runtime.GOOS == "windows" {
39-
t.Skip("fstest.TestFS is not yet windows path aware")
40-
}
41-
4238
err := fstest.TestFS(iofs, createdFiles...)
4339
if err != nil {
4440
checkFsTestError(t, err, files)
4541
}
4642
}
4743

44+
// TestOpenForwardSlashPath verifies that Open works with forward-slash paths
45+
// on all platforms. This is a regression test ensuring filepath.Clean is not
46+
// used when cleaning paths in Open() (it should use path.Clean) so valid
47+
// fs.FS paths like "dir/file.txt" are not rejected on Windows.
48+
func TestOpenForwardSlashPath(t *testing.T) {
49+
t.Parallel()
50+
mem := memfs.New()
51+
adapter := New(mem)
52+
53+
makeFile(t, mem, "dir/subdir/file.txt", "content")
54+
55+
tests := []struct {
56+
name string
57+
path string
58+
wantErr bool
59+
}{
60+
{"simple-nested", "dir/subdir/file.txt", false},
61+
{"directory", "dir/subdir", false},
62+
{"dot-path", ".", false},
63+
{"absolute-path-rejected", "/dir/subdir/file.txt", true},
64+
}
65+
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
t.Parallel()
69+
f, err := adapter.Open(tt.path)
70+
if tt.wantErr {
71+
require.Error(t, err)
72+
return
73+
}
74+
require.NoError(t, err)
75+
require.NoError(t, f.Close())
76+
})
77+
}
78+
}
79+
4880
func TestDeletes(t *testing.T) {
4981
t.Parallel()
5082
memfs := memfs.New()

0 commit comments

Comments
 (0)