Skip to content

Commit e5b08e6

Browse files
committed
io/fs: allow backslash in ValidPath, reject in os.DirFS.Open
Rejecting backslash introduces problems with presenting underlying OS file systems that contain names with backslash. Rejecting backslash also does not Windows-proof the syntax, because colon can also be a path separator. And we are not going to reject colon from all names. So don't reject backslash either. There is a similar problem on Windows with names containing slashes, but those are more difficult (though not impossible) to create. Also document and enforce that paths must be UTF-8. Fixes #44166. Change-Id: Iac7a9a268025c1fd31010dbaf3f51e1660c7ae2a Reviewed-on: https://go-review.googlesource.com/c/go/+/290709 TryBot-Result: Go Bot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]> Trust: Russ Cox <[email protected]> Run-TryBot: Russ Cox <[email protected]>
1 parent ed80790 commit e5b08e6

File tree

4 files changed

+64
-13
lines changed

4 files changed

+64
-13
lines changed

src/io/fs/fs.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package fs
1010
import (
1111
"internal/oserror"
1212
"time"
13+
"unicode/utf8"
1314
)
1415

1516
// An FS provides access to a hierarchical file system.
@@ -32,15 +33,22 @@ type FS interface {
3233

3334
// ValidPath reports whether the given path name
3435
// is valid for use in a call to Open.
35-
// Path names passed to open are unrooted, slash-separated
36-
// sequences of path elements, like “x/y/z”.
37-
// Path names must not contain a “.” or “..” or empty element,
36+
//
37+
// Path names passed to open are UTF-8-encoded,
38+
// unrooted, slash-separated sequences of path elements, like “x/y/z”.
39+
// Path names must not contain an element that is “.” or “..” or the empty string,
3840
// except for the special case that the root directory is named “.”.
39-
// Leading and trailing slashes (like “/x” or “x/”) are not allowed.
41+
// Paths must not start or end with a slash: “/x” and “x/” are invalid.
4042
//
41-
// Paths are slash-separated on all systems, even Windows.
42-
// Backslashes must not appear in path names.
43+
// Note that paths are slash-separated on all systems, even Windows.
44+
// Paths containing other characters such as backslash and colon
45+
// are accepted as valid, but those characters must never be
46+
// interpreted by an FS implementation as path element separators.
4347
func ValidPath(name string) bool {
48+
if !utf8.ValidString(name) {
49+
return false
50+
}
51+
4452
if name == "." {
4553
// special case
4654
return true
@@ -50,9 +58,6 @@ func ValidPath(name string) bool {
5058
for {
5159
i := 0
5260
for i < len(name) && name[i] != '/' {
53-
if name[i] == '\\' {
54-
return false
55-
}
5661
i++
5762
}
5863
elem := name[:i]

src/io/fs/fs_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ var isValidPathTests = []struct {
3333
{"x/..", false},
3434
{"x/../y", false},
3535
{"x//y", false},
36-
{`x\`, false},
37-
{`x\y`, false},
38-
{`\x`, false},
36+
{`x\`, true},
37+
{`x\y`, true},
38+
{`x:y`, true},
39+
{`\x`, true},
3940
}
4041

4142
func TestValidPath(t *testing.T) {

src/os/file.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,10 +620,21 @@ func DirFS(dir string) fs.FS {
620620
return dirFS(dir)
621621
}
622622

623+
func containsAny(s, chars string) bool {
624+
for i := 0; i < len(s); i++ {
625+
for j := 0; j < len(chars); j++ {
626+
if s[i] == chars[j] {
627+
return true
628+
}
629+
}
630+
}
631+
return false
632+
}
633+
623634
type dirFS string
624635

625636
func (dir dirFS) Open(name string) (fs.File, error) {
626-
if !fs.ValidPath(name) {
637+
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
627638
return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
628639
}
629640
f, err := Open(string(dir) + "/" + name)

src/os/os_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2719,6 +2719,40 @@ func TestDirFS(t *testing.T) {
27192719
if err := fstest.TestFS(DirFS("./testdata/dirfs"), "a", "b", "dir/x"); err != nil {
27202720
t.Fatal(err)
27212721
}
2722+
2723+
// Test that Open does not accept backslash as separator.
2724+
d := DirFS(".")
2725+
_, err := d.Open(`testdata\dirfs`)
2726+
if err == nil {
2727+
t.Fatalf(`Open testdata\dirfs succeeded`)
2728+
}
2729+
}
2730+
2731+
func TestDirFSPathsValid(t *testing.T) {
2732+
if runtime.GOOS == "windows" {
2733+
t.Skipf("skipping on Windows")
2734+
}
2735+
2736+
d := t.TempDir()
2737+
if err := os.WriteFile(filepath.Join(d, "control.txt"), []byte(string("Hello, world!")), 0644); err != nil {
2738+
t.Fatal(err)
2739+
}
2740+
if err := os.WriteFile(filepath.Join(d, `e:xperi\ment.txt`), []byte(string("Hello, colon and backslash!")), 0644); err != nil {
2741+
t.Fatal(err)
2742+
}
2743+
2744+
fsys := os.DirFS(d)
2745+
err := fs.WalkDir(fsys, ".", func(path string, e fs.DirEntry, err error) error {
2746+
if fs.ValidPath(e.Name()) {
2747+
t.Logf("%q ok", e.Name())
2748+
} else {
2749+
t.Errorf("%q INVALID", e.Name())
2750+
}
2751+
return nil
2752+
})
2753+
if err != nil {
2754+
t.Fatal(err)
2755+
}
27222756
}
27232757

27242758
func TestReadFileProc(t *testing.T) {

0 commit comments

Comments
 (0)