aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/go/build/deps_test.go2
-rw-r--r--src/internal/safefilepath/path_windows.go98
-rw-r--r--src/path/filepath/path.go17
-rw-r--r--src/path/filepath/path_nonwindows.go9
-rw-r--r--src/path/filepath/path_test.go67
-rw-r--r--src/path/filepath/path_windows.go194
6 files changed, 269 insertions, 118 deletions
diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go
index 3e8fb9ea11..1ec6ee7c27 100644
--- a/src/go/build/deps_test.go
+++ b/src/go/build/deps_test.go
@@ -153,7 +153,7 @@ var depsRules = `
unicode, fmt !< net, os, os/signal;
- os/signal, STR
+ os/signal, internal/safefilepath, STR
< path/filepath
< io/ioutil;
diff --git a/src/internal/safefilepath/path_windows.go b/src/internal/safefilepath/path_windows.go
index 909c150edc..7cfd6ce2ea 100644
--- a/src/internal/safefilepath/path_windows.go
+++ b/src/internal/safefilepath/path_windows.go
@@ -20,15 +20,10 @@ func fromFS(path string) (string, error) {
for p := path; p != ""; {
// Find the next path element.
i := 0
- dot := -1
for i < len(p) && p[i] != '/' {
switch p[i] {
case 0, '\\', ':':
return "", errInvalidPath
- case '.':
- if dot < 0 {
- dot = i
- }
}
i++
}
@@ -39,22 +34,8 @@ func fromFS(path string) (string, error) {
} else {
p = ""
}
- // Trim the extension and look for a reserved name.
- base := part
- if dot >= 0 {
- base = part[:dot]
- }
- if isReservedName(base) {
- if dot < 0 {
- return "", errInvalidPath
- }
- // The path element is a reserved name with an extension.
- // Some Windows versions consider this a reserved name,
- // while others do not. Use FullPath to see if the name is
- // reserved.
- if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` {
- return "", errInvalidPath
- }
+ if IsReservedName(part) {
+ return "", errInvalidPath
}
}
if containsSlash {
@@ -70,23 +51,88 @@ func fromFS(path string) (string, error) {
return path, nil
}
-// isReservedName reports if name is a Windows reserved device name.
+// IsReservedName reports if name is a Windows reserved device name.
// It does not detect names with an extension, which are also reserved on some Windows versions.
//
// For details, search for PRN in
// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
-func isReservedName(name string) bool {
- if 3 <= len(name) && len(name) <= 4 {
+func IsReservedName(name string) bool {
+ // Device names can have arbitrary trailing characters following a dot or colon.
+ base := name
+ for i := 0; i < len(base); i++ {
+ switch base[i] {
+ case ':', '.':
+ base = base[:i]
+ }
+ }
+ // Trailing spaces in the last path element are ignored.
+ for len(base) > 0 && base[len(base)-1] == ' ' {
+ base = base[:len(base)-1]
+ }
+ if !isReservedBaseName(base) {
+ return false
+ }
+ if len(base) == len(name) {
+ return true
+ }
+ // The path element is a reserved name with an extension.
+ // Some Windows versions consider this a reserved name,
+ // while others do not. Use FullPath to see if the name is
+ // reserved.
+ if p, _ := syscall.FullPath(name); len(p) >= 4 && p[:4] == `\\.\` {
+ return true
+ }
+ return false
+}
+
+func isReservedBaseName(name string) bool {
+ if len(name) == 3 {
switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
case "CON", "PRN", "AUX", "NUL":
- return len(name) == 3
+ return true
+ }
+ }
+ if len(name) >= 4 {
+ switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
case "COM", "LPT":
- return len(name) == 4 && '1' <= name[3] && name[3] <= '9'
+ if len(name) == 4 && '1' <= name[3] && name[3] <= '9' {
+ return true
+ }
+ // Superscript ¹, ², and ³ are considered numbers as well.
+ switch name[3:] {
+ case "\u00b2", "\u00b3", "\u00b9":
+ return true
+ }
+ return false
}
}
+
+ // Passing CONIN$ or CONOUT$ to CreateFile opens a console handle.
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles
+ //
+ // While CONIN$ and CONOUT$ aren't documented as being files,
+ // they behave the same as CON. For example, ./CONIN$ also opens the console input.
+ if len(name) == 6 && name[5] == '$' && equalFold(name, "CONIN$") {
+ return true
+ }
+ if len(name) == 7 && name[6] == '$' && equalFold(name, "CONOUT$") {
+ return true
+ }
return false
}
+func equalFold(a, b string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := 0; i < len(a); i++ {
+ if toUpper(a[i]) != toUpper(b[i]) {
+ return false
+ }
+ }
+ return true
+}
+
func toUpper(c byte) byte {
if 'a' <= c && c <= 'z' {
return c - ('a' - 'A')
diff --git a/src/path/filepath/path.go b/src/path/filepath/path.go
index 326ca006b2..485ef4e2b8 100644
--- a/src/path/filepath/path.go
+++ b/src/path/filepath/path.go
@@ -15,7 +15,6 @@ import (
"errors"
"io/fs"
"os"
- "runtime"
"sort"
"strings"
)
@@ -163,21 +162,7 @@ func Clean(path string) string {
out.append('.')
}
- if runtime.GOOS == "windows" && out.volLen == 0 && out.buf != nil {
- // If a ':' appears in the path element at the start of a Windows path,
- // insert a .\ at the beginning to avoid converting relative paths
- // like a/../c: into c:.
- for _, c := range out.buf {
- if os.IsPathSeparator(c) {
- break
- }
- if c == ':' {
- out.prepend('.', Separator)
- break
- }
- }
- }
-
+ postClean(&out) // avoid creating absolute paths on Windows
return FromSlash(out.string())
}
diff --git a/src/path/filepath/path_nonwindows.go b/src/path/filepath/path_nonwindows.go
new file mode 100644
index 0000000000..db69f0228b
--- /dev/null
+++ b/src/path/filepath/path_nonwindows.go
@@ -0,0 +1,9 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !windows
+
+package filepath
+
+func postClean(out *lazybuf) {}
diff --git a/src/path/filepath/path_test.go b/src/path/filepath/path_test.go
index 1a2e53a795..0a2286648f 100644
--- a/src/path/filepath/path_test.go
+++ b/src/path/filepath/path_test.go
@@ -115,6 +115,9 @@ var wincleantests = []PathTest{
{`a/../c:/a`, `.\c:\a`},
{`a/../../c:`, `..\c:`},
{`foo:bar`, `foo:bar`},
+
+ // Don't allow cleaning to create a Root Local Device path like \??\a.
+ {`/a/../??/a`, `\.\??\a`},
}
func TestClean(t *testing.T) {
@@ -176,8 +179,28 @@ var islocaltests = []IsLocalTest{
var winislocaltests = []IsLocalTest{
{"NUL", false},
{"nul", false},
+ {"nul ", false},
{"nul.", false},
+ {"a/nul:", false},
+ {"a/nul : a", false},
+ {"com0", true},
{"com1", false},
+ {"com2", false},
+ {"com3", false},
+ {"com4", false},
+ {"com5", false},
+ {"com6", false},
+ {"com7", false},
+ {"com8", false},
+ {"com9", false},
+ {"com¹", false},
+ {"com²", false},
+ {"com³", false},
+ {"com¹ : a", false},
+ {"cOm1", false},
+ {"lpt1", false},
+ {"LPT1", false},
+ {"lpt³", false},
{"./nul", false},
{`\`, false},
{`\a`, false},
@@ -383,6 +406,7 @@ var winjointests = []JoinTest{
{[]string{`\\a\`, `b`, `c`}, `\\a\b\c`},
{[]string{`//`, `a`}, `\\a`},
{[]string{`a:\b\c`, `x\..\y:\..\..\z`}, `a:\b\z`},
+ {[]string{`\`, `??\a`}, `\.\??\a`},
}
func TestJoin(t *testing.T) {
@@ -953,6 +977,8 @@ var winisabstests = []IsAbsTest{
{`\\host\share\`, true},
{`\\host\share\foo`, true},
{`//host/share/foo/bar`, true},
+ {`\\?\a\b\c`, true},
+ {`\??\a\b\c`, true},
}
func TestIsAbs(t *testing.T) {
@@ -1422,7 +1448,8 @@ type VolumeNameTest struct {
var volumenametests = []VolumeNameTest{
{`c:/foo/bar`, `c:`},
{`c:`, `c:`},
- {`2:`, ``},
+ {`c:\`, `c:`},
+ {`2:`, `2:`},
{``, ``},
{`\\\host`, `\\\host`},
{`\\\host\`, `\\\host`},
@@ -1442,12 +1469,23 @@ var volumenametests = []VolumeNameTest{
{`//host/share//foo///bar////baz`, `\\host\share`},
{`\\host\share\foo\..\bar`, `\\host\share`},
{`//host/share/foo/../bar`, `\\host\share`},
+ {`//.`, `\\.`},
+ {`//./`, `\\.\`},
{`//./NUL`, `\\.\NUL`},
- {`//?/NUL`, `\\?\NUL`},
+ {`//?/`, `\\?`},
+ {`//./a/b`, `\\.\a`},
+ {`//?/`, `\\?`},
+ {`//?/`, `\\?`},
{`//./C:`, `\\.\C:`},
+ {`//./C:/`, `\\.\C:`},
{`//./C:/a/b/c`, `\\.\C:`},
{`//./UNC/host/share/a/b/c`, `\\.\UNC\host\share`},
{`//./UNC/host`, `\\.\UNC\host`},
+ {`//./UNC/host\`, `\\.\UNC\host\`},
+ {`//./UNC`, `\\.\UNC`},
+ {`//./UNC/`, `\\.\UNC\`},
+ {`\\?\x`, `\\?`},
+ {`\??\x`, `\??`},
}
func TestVolumeName(t *testing.T) {
@@ -1720,3 +1758,28 @@ func TestIssue51617(t *testing.T) {
t.Errorf("got directories %v, want %v", saw, want)
}
}
+
+func TestEscaping(t *testing.T) {
+ dir1 := t.TempDir()
+ dir2 := t.TempDir()
+ chdir(t, dir1)
+
+ for _, p := range []string{
+ filepath.Join(dir2, "x"),
+ } {
+ if !filepath.IsLocal(p) {
+ continue
+ }
+ f, err := os.Create(p)
+ if err != nil {
+ f.Close()
+ }
+ ents, err := os.ReadDir(dir2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range ents {
+ t.Fatalf("found: %v", e.Name())
+ }
+ }
+}
diff --git a/src/path/filepath/path_windows.go b/src/path/filepath/path_windows.go
index 4dca9e0f55..c490424f20 100644
--- a/src/path/filepath/path_windows.go
+++ b/src/path/filepath/path_windows.go
@@ -5,6 +5,8 @@
package filepath
import (
+ "internal/safefilepath"
+ "os"
"strings"
"syscall"
)
@@ -20,34 +22,6 @@ func toUpper(c byte) byte {
return c
}
-// isReservedName reports if name is a Windows reserved device name or a console handle.
-// It does not detect names with an extension, which are also reserved on some Windows versions.
-//
-// For details, search for PRN in
-// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
-func isReservedName(name string) bool {
- if 3 <= len(name) && len(name) <= 4 {
- switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
- case "CON", "PRN", "AUX", "NUL":
- return len(name) == 3
- case "COM", "LPT":
- return len(name) == 4 && '1' <= name[3] && name[3] <= '9'
- }
- }
- // Passing CONIN$ or CONOUT$ to CreateFile opens a console handle.
- // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles
- //
- // While CONIN$ and CONOUT$ aren't documented as being files,
- // they behave the same as CON. For example, ./CONIN$ also opens the console input.
- if len(name) == 6 && name[5] == '$' && strings.EqualFold(name, "CONIN$") {
- return true
- }
- if len(name) == 7 && name[6] == '$' && strings.EqualFold(name, "CONOUT$") {
- return true
- }
- return false
-}
-
func isLocal(path string) bool {
if path == "" {
return false
@@ -68,25 +42,8 @@ func isLocal(path string) bool {
if part == "." || part == ".." {
hasDots = true
}
- // Trim the extension and look for a reserved name.
- base, _, hasExt := strings.Cut(part, ".")
- if isReservedName(base) {
- if !hasExt {
- return false
- }
- // The path element is a reserved name with an extension. Some Windows
- // versions consider this a reserved name, while others do not. Use
- // FullPath to see if the name is reserved.
- //
- // FullPath will convert references to reserved device names to their
- // canonical form: \\.\${DEVICE_NAME}
- //
- // FullPath does not perform this conversion for paths which contain
- // a reserved device name anywhere other than in the last element,
- // so check the part rather than the full path.
- if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` {
- return false
- }
+ if safefilepath.IsReservedName(part) {
+ return false
}
}
if hasDots {
@@ -118,40 +75,99 @@ func IsAbs(path string) (b bool) {
// volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere.
//
-// See: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
+// See:
+// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
+// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
func volumeNameLen(path string) int {
- if len(path) < 2 {
- return 0
- }
- // with drive letter
- c := path[0]
- if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
+ switch {
+ case len(path) >= 2 && path[1] == ':':
+ // Path starts with a drive letter.
+ //
+ // Not all Windows functions necessarily enforce the requirement that
+ // drive letters be in the set A-Z, and we don't try to here.
+ //
+ // We don't handle the case of a path starting with a non-ASCII character,
+ // in which case the "drive letter" might be multiple bytes long.
return 2
- }
- // UNC and DOS device paths start with two slashes.
- if !isSlash(path[0]) || !isSlash(path[1]) {
+
+ case len(path) == 0 || !isSlash(path[0]):
+ // Path does not have a volume component.
return 0
+
+ case pathHasPrefixFold(path, `\\.\UNC`):
+ // We're going to treat the UNC host and share as part of the volume
+ // prefix for historical reasons, but this isn't really principled;
+ // Windows's own GetFullPathName will happily remove the first
+ // component of the path in this space, converting
+ // \\.\unc\a\b\..\c into \\.\unc\a\c.
+ return uncLen(path, len(`\\.\UNC\`))
+
+ case pathHasPrefixFold(path, `\\.`):
+ // Path starts with \\., and is a Local Device path.
+ //
+ // We currently treat the next component after the \\.\ prefix
+ // as part of the volume name, although there doesn't seem to be
+ // a principled reason to do this.
+ if len(path) == 3 {
+ return 3 // exactly \\.
+ }
+ _, rest, ok := cutPath(path[4:])
+ if !ok {
+ return len(path)
+ }
+ return len(path) - len(rest) - 1
+
+ case pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
+ // Path starts with \\?\ or \??\, and is a Root Local Device path.
+ //
+ // While Windows usually treats / and \ as equivalent,
+ // /??/ does not seem to be recognized as a Root Local Device path.
+ // We treat it as one anyway here to be safe.
+ return 3
+
+ case len(path) >= 2 && isSlash(path[1]):
+ // Path starts with \\, and is a UNC path.
+ return uncLen(path, 2)
}
- rest := path[2:]
- p1, rest, _ := cutPath(rest)
- p2, rest, ok := cutPath(rest)
- if !ok {
- return len(path)
+ return 0
+}
+
+// pathHasPrefixFold tests whether the path s begins with prefix,
+// ignoring case and treating all path separators as equivalent.
+// If s is longer than prefix, then s[len(prefix)] must be a path separator.
+func pathHasPrefixFold(s, prefix string) bool {
+ if len(s) < len(prefix) {
+ return false
}
- if p1 != "." && p1 != "?" {
- // This is a UNC path: \\${HOST}\${SHARE}\
- return len(path) - len(rest) - 1
+ for i := 0; i < len(prefix); i++ {
+ if isSlash(prefix[i]) {
+ if !isSlash(s[i]) {
+ return false
+ }
+ } else if toUpper(prefix[i]) != toUpper(s[i]) {
+ return false
+ }
}
- // This is a DOS device path.
- if len(p2) == 3 && toUpper(p2[0]) == 'U' && toUpper(p2[1]) == 'N' && toUpper(p2[2]) == 'C' {
- // This is a DOS device path that links to a UNC: \\.\UNC\${HOST}\${SHARE}\
- _, rest, _ = cutPath(rest) // host
- _, rest, ok = cutPath(rest) // share
- if !ok {
- return len(path)
+ if len(s) > len(prefix) && !isSlash(s[len(prefix)]) {
+ return false
+ }
+ return true
+}
+
+// uncLen returns the length of the volume prefix of a UNC path.
+// prefixLen is the prefix prior to the start of the UNC host;
+// for example, for "//host/share", the prefixLen is len("//")==2.
+func uncLen(path string, prefixLen int) int {
+ count := 0
+ for i := prefixLen; i < len(path); i++ {
+ if isSlash(path[i]) {
+ count++
+ if count == 2 {
+ return i
+ }
}
}
- return len(path) - len(rest) - 1
+ return len(path)
}
// cutPath slices path around the first path separator.
@@ -238,6 +254,12 @@ func join(elem []string) string {
for len(e) > 0 && isSlash(e[0]) {
e = e[1:]
}
+ // If the path is \ and the next path element is ??,
+ // add an extra .\ to create \.\?? rather than \??\
+ // (a Root Local Device path).
+ if b.Len() == 1 && pathHasPrefixFold(e, "??") {
+ b.WriteString(`.\`)
+ }
case lastChar == ':':
// If the path ends in a colon, keep the path relative to the current directory
// on a drive and don't add a separator. Preserve leading slashes in the next
@@ -304,3 +326,29 @@ func isUNC(path string) bool {
func sameWord(a, b string) bool {
return strings.EqualFold(a, b)
}
+
+// postClean adjusts the results of Clean to avoid turning a relative path
+// into an absolute or rooted one.
+func postClean(out *lazybuf) {
+ if out.volLen != 0 || out.buf == nil {
+ return
+ }
+ // If a ':' appears in the path element at the start of a path,
+ // insert a .\ at the beginning to avoid converting relative paths
+ // like a/../c: into c:.
+ for _, c := range out.buf {
+ if os.IsPathSeparator(c) {
+ break
+ }
+ if c == ':' {
+ out.prepend('.', Separator)
+ return
+ }
+ }
+ // If a path begins with \??\, insert a \. at the beginning
+ // to avoid converting paths like \a\..\??\c:\x into \??\c:\x
+ // (equivalent to c:\x).
+ if len(out.buf) >= 3 && os.IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
+ out.prepend(Separator, '.')
+ }
+}