mirror of https://github.com/golang/go.git
path/filepath: add IsLocal
IsLocal reports whether a path lexically refers to a location contained within the directory in which it is evaluated. It identifies paths that are absolute, escape a directory with ".." elements, and (on Windows) paths that reference reserved device names. For #56219. Change-Id: I35edfa3ce77b40b8e66f1fc8e0ff73cfd06f2313 Reviewed-on: https://go-review.googlesource.com/c/go/+/449239 Run-TryBot: Damien Neil <dneil@google.com> Reviewed-by: Joseph Tsai <joetsai@digital-static.net> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@google.com> Reviewed-by: Ian Lance Taylor <iant@golang.org> Reviewed-by: Joedian Reid <joedian@golang.org>
This commit is contained in:
parent
fd59c6cf8c
commit
6d0bf438e3
|
|
@ -0,0 +1 @@
|
||||||
|
pkg path/filepath, func IsLocal(string) bool #56219
|
||||||
|
|
@ -665,6 +665,13 @@ proxyHandler := &httputil.ReverseProxy{
|
||||||
<p><!-- CL 363814 --><!-- https://go.dev/issue/47209 -->
|
<p><!-- CL 363814 --><!-- https://go.dev/issue/47209 -->
|
||||||
TODO: <a href="https://go.dev/cl/363814">https://go.dev/cl/363814</a>: path/filepath, io/fs: add SkipAll; modified api/next/47209.txt
|
TODO: <a href="https://go.dev/cl/363814">https://go.dev/cl/363814</a>: path/filepath, io/fs: add SkipAll; modified api/next/47209.txt
|
||||||
</p>
|
</p>
|
||||||
|
<p><!-- https://go.dev/issue/56219 -->
|
||||||
|
The new <code>IsLocal</code> function reports whether a path is
|
||||||
|
lexically local to a directory.
|
||||||
|
For example, if <code>IsLocal(p)</code> is <code>true</code>,
|
||||||
|
then <code>Open(p)</code> will refer to a file that is lexically
|
||||||
|
within the subtree rooted at the current directory.
|
||||||
|
</p>
|
||||||
</dd>
|
</dd>
|
||||||
</dl><!-- io -->
|
</dl><!-- io -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,46 @@ func Clean(path string) string {
|
||||||
return FromSlash(out.string())
|
return FromSlash(out.string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsLocal reports whether path, using lexical analysis only, has all of these properties:
|
||||||
|
//
|
||||||
|
// - is within the subtree rooted at the directory in which path is evaluated
|
||||||
|
// - is not an absolute path
|
||||||
|
// - is not empty
|
||||||
|
// - on Windows, is not a reserved name such as "NUL"
|
||||||
|
//
|
||||||
|
// If IsLocal(path) returns true, then
|
||||||
|
// Join(base, path) will always produce a path contained within base and
|
||||||
|
// Clean(path) will always produce an unrooted path with no ".." path elements.
|
||||||
|
//
|
||||||
|
// IsLocal is a purely lexical operation.
|
||||||
|
// In particular, it does not account for the effect of any symbolic links
|
||||||
|
// that may exist in the filesystem.
|
||||||
|
func IsLocal(path string) bool {
|
||||||
|
return isLocal(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unixIsLocal(path string) bool {
|
||||||
|
if IsAbs(path) || path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hasDots := false
|
||||||
|
for p := path; p != ""; {
|
||||||
|
var part string
|
||||||
|
part, p, _ = strings.Cut(p, "/")
|
||||||
|
if part == "." || part == ".." {
|
||||||
|
hasDots = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasDots {
|
||||||
|
path = Clean(path)
|
||||||
|
}
|
||||||
|
if path == ".." || strings.HasPrefix(path, "../") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// ToSlash returns the result of replacing each separator character
|
// ToSlash returns the result of replacing each separator character
|
||||||
// in path with a slash ('/') character. Multiple separators are
|
// in path with a slash ('/') character. Multiple separators are
|
||||||
// replaced by multiple slashes.
|
// replaced by multiple slashes.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ package filepath
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
|
func isLocal(path string) bool {
|
||||||
|
return unixIsLocal(path)
|
||||||
|
}
|
||||||
|
|
||||||
// IsAbs reports whether the path is absolute.
|
// IsAbs reports whether the path is absolute.
|
||||||
func IsAbs(path string) bool {
|
func IsAbs(path string) bool {
|
||||||
return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "#")
|
return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "#")
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,60 @@ func TestClean(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IsLocalTest struct {
|
||||||
|
path string
|
||||||
|
isLocal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var islocaltests = []IsLocalTest{
|
||||||
|
{"", false},
|
||||||
|
{".", true},
|
||||||
|
{"..", false},
|
||||||
|
{"../a", false},
|
||||||
|
{"/", false},
|
||||||
|
{"/a", false},
|
||||||
|
{"/a/../..", false},
|
||||||
|
{"a", true},
|
||||||
|
{"a/../a", true},
|
||||||
|
{"a/", true},
|
||||||
|
{"a/.", true},
|
||||||
|
{"a/./b/./c", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
var winislocaltests = []IsLocalTest{
|
||||||
|
{"NUL", false},
|
||||||
|
{"nul", false},
|
||||||
|
{"nul.", false},
|
||||||
|
{"nul.txt", false},
|
||||||
|
{"com1", false},
|
||||||
|
{"./nul", false},
|
||||||
|
{"a/nul.txt/b", false},
|
||||||
|
{`\`, false},
|
||||||
|
{`\a`, false},
|
||||||
|
{`C:`, false},
|
||||||
|
{`C:\a`, false},
|
||||||
|
{`..\a`, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var plan9islocaltests = []IsLocalTest{
|
||||||
|
{"#a", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsLocal(t *testing.T) {
|
||||||
|
tests := islocaltests
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
tests = append(tests, winislocaltests...)
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "plan9" {
|
||||||
|
tests = append(tests, plan9islocaltests...)
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
if got := filepath.IsLocal(test.path); got != test.isLocal {
|
||||||
|
t.Errorf("IsLocal(%q) = %v, want %v", test.path, got, test.isLocal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sep = filepath.Separator
|
const sep = filepath.Separator
|
||||||
|
|
||||||
var slashtests = []PathTest{
|
var slashtests = []PathTest{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ package filepath
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
|
func isLocal(path string) bool {
|
||||||
|
return unixIsLocal(path)
|
||||||
|
}
|
||||||
|
|
||||||
// IsAbs reports whether the path is absolute.
|
// IsAbs reports whether the path is absolute.
|
||||||
func IsAbs(path string) bool {
|
func IsAbs(path string) bool {
|
||||||
return strings.HasPrefix(path, "/")
|
return strings.HasPrefix(path, "/")
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,73 @@ func toUpper(c byte) byte {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocal(path string) bool {
|
||||||
|
if path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if isSlash(path[0]) {
|
||||||
|
// Path rooted in the current drive.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.IndexByte(path, ':') >= 0 {
|
||||||
|
// Colons are only valid when marking a drive letter ("C:foo").
|
||||||
|
// Rejecting any path with a colon is conservative but safe.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hasDots := false // contains . or .. path elements
|
||||||
|
for p := path; p != ""; {
|
||||||
|
var part string
|
||||||
|
part, p, _ = cutPath(p)
|
||||||
|
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 hasDots {
|
||||||
|
path = Clean(path)
|
||||||
|
}
|
||||||
|
if path == ".." || strings.HasPrefix(path, `..\`) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// IsAbs reports whether the path is absolute.
|
// IsAbs reports whether the path is absolute.
|
||||||
func IsAbs(path string) (b bool) {
|
func IsAbs(path string) (b bool) {
|
||||||
l := volumeNameLen(path)
|
l := volumeNameLen(path)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue