init
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
package pathutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateAccepts(t *testing.T) {
|
||||
ok := []string{
|
||||
"index.html",
|
||||
"assets/app.js",
|
||||
"a/b/c/d/e/f/g.txt",
|
||||
".well-known/acme-challenge/token",
|
||||
"_next/static/chunks/main-abc123.js",
|
||||
"文档/说明.html",
|
||||
"a file with spaces.html",
|
||||
"weird!@#$%^&()+=[]{};'\",<>?.html",
|
||||
"..hidden", // only an exact ".." component is a traversal
|
||||
"a/..b/c", //
|
||||
"...", // not "." and not ".."
|
||||
"~tilde.html", // no special meaning below a project prefix
|
||||
strings.Repeat("a", MaxSegmentBytes),
|
||||
}
|
||||
for _, p := range ok {
|
||||
if err := Validate(p); err != nil {
|
||||
t.Errorf("Validate(%q) = %v, want nil", p, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejects(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
want error
|
||||
}{
|
||||
{"", ErrEmpty},
|
||||
{".", ErrNotRelative},
|
||||
{"..", ErrNotRelative},
|
||||
{"../etc/passwd", ErrNotRelative},
|
||||
{"a/../../etc/passwd", ErrNotRelative},
|
||||
{"a/./b", ErrNotRelative},
|
||||
{"/etc/passwd", ErrNotRelative},
|
||||
{"a//b", ErrNotRelative},
|
||||
{"a/", ErrNotRelative},
|
||||
{"/", ErrNotRelative},
|
||||
{"a/b/..", ErrNotRelative},
|
||||
{"a\x00b", ErrControlChar},
|
||||
{"a\nb", ErrControlChar},
|
||||
{"a\tb", ErrControlChar},
|
||||
{"a\x1b[31m", ErrControlChar},
|
||||
{"a\x7fb", ErrControlChar},
|
||||
{"a\\b", ErrBackslash},
|
||||
{"..\\..\\windows", ErrBackslash},
|
||||
{"a/\xff\xfe/b", ErrNotUTF8},
|
||||
{strings.Repeat("a", MaxSegmentBytes+1), ErrSegmentTooLong},
|
||||
{"ok/" + strings.Repeat("b", MaxSegmentBytes+1), ErrSegmentTooLong},
|
||||
{strings.Repeat("a/", MaxPathBytes/2) + "b", ErrTooLong},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := Validate(tc.path)
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Errorf("Validate(%q) = %v, want %v", tc.path, err, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The traversal cases are the ones that matter most, so state them again as an
|
||||
// executable claim about what a manifest can never make Join produce.
|
||||
func TestValidateBlocksEscape(t *testing.T) {
|
||||
for _, p := range []string{
|
||||
"../x", "a/../../x", "./../x", "/x", "a/b/../../../x",
|
||||
"..", "a/..", "\\..\\x", "a\\..\\..\\x",
|
||||
} {
|
||||
if err := Validate(p); err == nil {
|
||||
t.Errorf("Validate(%q) accepted a path that can escape its directory", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDetectsCollisions(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
paths []string
|
||||
want error
|
||||
}{
|
||||
{"duplicate", []string{"a.html", "a.html"}, ErrDuplicate},
|
||||
{"case only", []string{"App.js", "app.js"}, ErrCaseCollision},
|
||||
{"case in a directory", []string{"Assets/x.js", "assets/y.js"}, nil},
|
||||
{"case collision under a folded directory", []string{"Assets/x.js", "assets/X.js"}, ErrCaseCollision},
|
||||
{"file then directory", []string{"a", "a/b"}, ErrPathConflict},
|
||||
{"directory then file", []string{"a/b", "a"}, ErrPathConflict},
|
||||
{"deep file then directory", []string{"a/b/c", "a/b"}, ErrPathConflict},
|
||||
{"file then deep directory", []string{"a/b", "a/b/c/d"}, ErrPathConflict},
|
||||
{"case-folded file vs directory", []string{"A", "a/b"}, ErrPathConflict},
|
||||
{"siblings are fine", []string{"a/b", "a/c", "a/d/e"}, nil},
|
||||
{"unrelated", []string{"index.html", "assets/app.js", "assets/app.css"}, nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := NewSet(len(tc.paths))
|
||||
var err error
|
||||
for _, p := range tc.paths {
|
||||
if err = s.Add(p); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if tc.want == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
if s.Len() != len(tc.paths) {
|
||||
t.Errorf("Len = %d, want %d", s.Len(), len(tc.paths))
|
||||
}
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Fatalf("err = %v, want %v", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A rejected path must not leave a mark: the caller may report the error and
|
||||
// carry on validating the rest of the manifest.
|
||||
func TestSetRejectionLeavesNoTrace(t *testing.T) {
|
||||
s := NewSet(4)
|
||||
if err := s.Add("a/b"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Add("a"); !errors.Is(err, ErrPathConflict) {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
if err := s.Add("bad\x00path"); !errors.Is(err, ErrControlChar) {
|
||||
t.Fatalf("err = %v", err)
|
||||
}
|
||||
if s.Len() != 1 {
|
||||
t.Errorf("Len = %d, want 1", s.Len())
|
||||
}
|
||||
if err := s.Add("a/c"); err != nil {
|
||||
t.Errorf("a sibling must still be accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The message has to name the other path, or a 50,000-file manifest reports a
|
||||
// collision the operator cannot locate.
|
||||
func TestCollisionErrorNamesTheOtherPath(t *testing.T) {
|
||||
s := NewSet(2)
|
||||
if err := s.Add("Assets/App.js"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := s.Add("assets/app.js")
|
||||
if err == nil {
|
||||
t.Fatal("want a collision")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Assets/App.js") {
|
||||
t.Errorf("error %q does not name the conflicting path", err)
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzValidate(f *testing.F) {
|
||||
for _, s := range []string{"index.html", "a/b", "../x", "a\\b", "", ".", "a\x00b"} {
|
||||
f.Add(s)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, p string) {
|
||||
if Validate(p) != nil {
|
||||
return
|
||||
}
|
||||
// Anything accepted must be safe to join onto a directory. Localize is
|
||||
// the standard library's own statement of that property.
|
||||
if strings.HasPrefix(p, "/") || strings.Contains(p, "\\") {
|
||||
t.Fatalf("Validate accepted %q", p)
|
||||
}
|
||||
for _, seg := range strings.Split(p, "/") {
|
||||
if seg == "" || seg == "." || seg == ".." {
|
||||
t.Fatalf("Validate accepted %q with segment %q", p, seg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user