Skip to content

Commit 7722d0f

Browse files
hirochachachabradfitz
authored andcommitted
path/filepath: handle ".." in normalizing a path on Windows
Current code assumes there are not ".." in the Clean(path). That's not true. Clean doesn't handle leading "..", so we need to stop normalization if we see "..". Fixes #16793 Change-Id: I0a7901bedac17f1210b134d593ebd9f5e8483775 Reviewed-on: https://go-review.googlesource.com/27410 Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Alex Brainman <[email protected]> Run-TryBot: Ian Lance Taylor <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 1319a0f commit 7722d0f

File tree

4 files changed

+159
-8
lines changed

4 files changed

+159
-8
lines changed

src/path/filepath/export_windows_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44

55
package filepath
66

7-
var ToNorm = toNorm
7+
var (
8+
ToNorm = toNorm
9+
NormBase = normBase
10+
)

src/path/filepath/path_test.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ func TestEvalSymlinks(t *testing.T) {
845845
if p, err := filepath.EvalSymlinks(path); err != nil {
846846
t.Errorf("EvalSymlinks(%q) error: %v", d.path, err)
847847
} else if filepath.Clean(p) != filepath.Clean(dest) {
848-
t.Errorf("Clean(%q)=%q, want %q", path, p, dest)
848+
t.Errorf("EvalSymlinks(%q)=%q, want %q", path, p, dest)
849849
}
850850

851851
// test EvalSymlinks(".")
@@ -877,6 +877,34 @@ func TestEvalSymlinks(t *testing.T) {
877877
t.Errorf(`EvalSymlinks(".") in %q directory returns %q, want "." or %q`, d.path, p, want)
878878
}()
879879

880+
// test EvalSymlinks(".."+path)
881+
func() {
882+
defer func() {
883+
err := os.Chdir(wd)
884+
if err != nil {
885+
t.Fatal(err)
886+
}
887+
}()
888+
889+
err := os.Chdir(simpleJoin(tmpDir, "test"))
890+
if err != nil {
891+
t.Error(err)
892+
return
893+
}
894+
895+
path := simpleJoin("..", d.path)
896+
dest := simpleJoin("..", d.dest)
897+
if filepath.IsAbs(d.dest) || os.IsPathSeparator(d.dest[0]) {
898+
dest = d.dest
899+
}
900+
901+
if p, err := filepath.EvalSymlinks(path); err != nil {
902+
t.Errorf("EvalSymlinks(%q) error: %v", d.path, err)
903+
} else if filepath.Clean(p) != filepath.Clean(dest) {
904+
t.Errorf("EvalSymlinks(%q)=%q, want %q", path, p, dest)
905+
}
906+
}()
907+
880908
// test EvalSymlinks where parameter is relative path
881909
func() {
882910
defer func() {
@@ -894,7 +922,7 @@ func TestEvalSymlinks(t *testing.T) {
894922
if p, err := filepath.EvalSymlinks(d.path); err != nil {
895923
t.Errorf("EvalSymlinks(%q) error: %v", d.path, err)
896924
} else if filepath.Clean(p) != filepath.Clean(d.dest) {
897-
t.Errorf("Clean(%q)=%q, want %q", d.path, p, d.dest)
925+
t.Errorf("EvalSymlinks(%q)=%q, want %q", d.path, p, d.dest)
898926
}
899927
}()
900928
}

src/path/filepath/path_windows_test.go

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,106 @@ func TestToNorm(t *testing.T) {
309309
for _, test := range tests {
310310
got, err := filepath.ToNorm(test.arg, stubBase)
311311
if err != nil {
312-
t.Errorf("unexpected toNorm error, arg: %s, err: %v\n", test.arg, err)
312+
t.Errorf("toNorm(%s) failed: %v\n", test.arg, err)
313313
} else if got != test.want {
314-
t.Errorf("toNorm error, arg: %s, want: %s, got: %s\n", test.arg, test.want, got)
314+
t.Errorf("toNorm(%s) returns %s, but %s expected\n", test.arg, got, test.want)
315+
}
316+
}
317+
318+
testPath := `{{tmp}}\test\foo\bar`
319+
320+
testsDir := []struct {
321+
wd string
322+
arg string
323+
want string
324+
}{
325+
// test absolute paths
326+
{".", `{{tmp}}\test\foo\bar`, `{{tmp}}\test\foo\bar`},
327+
{".", `{{tmp}}\.\test/foo\bar`, `{{tmp}}\test\foo\bar`},
328+
{".", `{{tmp}}\test\..\test\foo\bar`, `{{tmp}}\test\foo\bar`},
329+
{".", `{{tmp}}\TEST\FOO\BAR`, `{{tmp}}\test\foo\bar`},
330+
331+
// test relative paths begin with drive letter
332+
{`{{tmp}}\test`, `{{tmpvol}}.`, `{{tmpvol}}.`},
333+
{`{{tmp}}\test`, `{{tmpvol}}..`, `{{tmpvol}}..`},
334+
{`{{tmp}}\test`, `{{tmpvol}}foo\bar`, `{{tmpvol}}foo\bar`},
335+
{`{{tmp}}\test`, `{{tmpvol}}.\foo\bar`, `{{tmpvol}}foo\bar`},
336+
{`{{tmp}}\test`, `{{tmpvol}}foo\..\foo\bar`, `{{tmpvol}}foo\bar`},
337+
{`{{tmp}}\test`, `{{tmpvol}}FOO\BAR`, `{{tmpvol}}foo\bar`},
338+
339+
// test relative paths begin with '\'
340+
{".", `{{tmpnovol}}\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
341+
{".", `{{tmpnovol}}\.\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
342+
{".", `{{tmpnovol}}\test\..\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
343+
{".", `{{tmpnovol}}\TEST\FOO\BAR`, `{{tmpnovol}}\test\foo\bar`},
344+
345+
// test relative paths begin without '\'
346+
{`{{tmp}}\test`, ".", `.`},
347+
{`{{tmp}}\test`, "..", `..`},
348+
{`{{tmp}}\test`, `foo\bar`, `foo\bar`},
349+
{`{{tmp}}\test`, `.\foo\bar`, `foo\bar`},
350+
{`{{tmp}}\test`, `foo\..\foo\bar`, `foo\bar`},
351+
{`{{tmp}}\test`, `FOO\BAR`, `foo\bar`},
352+
}
353+
354+
cwd, err := os.Getwd()
355+
if err != nil {
356+
t.Fatal(err)
357+
}
358+
359+
defer func() {
360+
err := os.Chdir(cwd)
361+
if err != nil {
362+
t.Fatal(err)
363+
}
364+
}()
365+
366+
tmp, err := ioutil.TempDir("", "testToNorm")
367+
if err != nil {
368+
t.Fatal(err)
369+
}
370+
defer os.RemoveAll(tmp)
371+
372+
// ioutil.TempDir might return "non-canonical" name.
373+
tmp, err = filepath.EvalSymlinks(tmp)
374+
if err != nil {
375+
t.Fatal(err)
376+
}
377+
378+
err = os.MkdirAll(strings.Replace(testPath, "{{tmp}}", tmp, -1), 0777)
379+
if err != nil {
380+
t.Fatal(err)
381+
}
382+
383+
tmpVol := filepath.VolumeName(tmp)
384+
tmpNoVol := tmp[len(tmpVol):]
385+
386+
for _, test := range testsDir {
387+
wd := strings.Replace(strings.Replace(strings.Replace(test.wd, "{{tmp}}", tmp, -1), "{{tmpvol}}", tmpVol, -1), "{{tmpnovol}}", tmpNoVol, -1)
388+
arg := strings.Replace(strings.Replace(strings.Replace(test.arg, "{{tmp}}", tmp, -1), "{{tmpvol}}", tmpVol, -1), "{{tmpnovol}}", tmpNoVol, -1)
389+
want := strings.Replace(strings.Replace(strings.Replace(test.want, "{{tmp}}", tmp, -1), "{{tmpvol}}", tmpVol, -1), "{{tmpnovol}}", tmpNoVol, -1)
390+
391+
if test.wd == "." {
392+
err := os.Chdir(cwd)
393+
if err != nil {
394+
t.Error(err)
395+
396+
continue
397+
}
398+
} else {
399+
err := os.Chdir(wd)
400+
if err != nil {
401+
t.Error(err)
402+
403+
continue
404+
}
405+
}
406+
407+
got, err := filepath.ToNorm(arg, filepath.NormBase)
408+
if err != nil {
409+
t.Errorf("toNorm(%s) failed: %v (wd=%s)\n", arg, err, wd)
410+
} else if got != want {
411+
t.Errorf("toNorm(%s) returns %s, but %s expected (wd=%s)\n", arg, got, want, wd)
315412
}
316413
}
317414
}

src/path/filepath/symlink_windows.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func normVolumeName(path string) string {
2222
return strings.ToUpper(volume)
2323
}
2424

25-
// normBase retruns the last element of path.
25+
// normBase returns the last element of path with correct case.
2626
func normBase(path string) (string, error) {
2727
p, err := syscall.UTF16PtrFromString(path)
2828
if err != nil {
@@ -40,7 +40,24 @@ func normBase(path string) (string, error) {
4040
return syscall.UTF16ToString(data.FileName[:]), nil
4141
}
4242

43-
func toNorm(path string, base func(string) (string, error)) (string, error) {
43+
// baseIsDotDot returns whether the last element of path is "..".
44+
// The given path should be 'Clean'-ed in advance.
45+
func baseIsDotDot(path string) bool {
46+
i := strings.LastIndexByte(path, Separator)
47+
return path[i+1:] == ".."
48+
}
49+
50+
// toNorm returns the normalized path that is guranteed to be unique.
51+
// It should accept the following formats:
52+
// * UNC paths (e.g \\server\share\foo\bar)
53+
// * absolute paths (e.g C:\foo\bar)
54+
// * relative paths begin with drive letter (e.g C:foo\bar, C:..\foo\bar, C:.., C:.)
55+
// * relative paths begin with '\' (e.g \foo\bar)
56+
// * relative paths begin without '\' (e.g foo\bar, ..\foo\bar, .., .)
57+
// The returned normalized path will be in the same form (of 5 listed above) as the input path.
58+
// If two paths A and B are indicating the same file with the same format, toNorm(A) should be equal to toNorm(B).
59+
// The normBase parameter should be equal to the normBase func, except for in tests. See docs on the normBase func.
60+
func toNorm(path string, normBase func(string) (string, error)) (string, error) {
4461
if path == "" {
4562
return path, nil
4663
}
@@ -58,7 +75,13 @@ func toNorm(path string, base func(string) (string, error)) (string, error) {
5875
var normPath string
5976

6077
for {
61-
name, err := base(volume + path)
78+
if baseIsDotDot(path) {
79+
normPath = path + `\` + normPath
80+
81+
break
82+
}
83+
84+
name, err := normBase(volume + path)
6285
if err != nil {
6386
return "", err
6487
}

0 commit comments

Comments
 (0)