mirror of
https://github.com/golang/go
synced 2024-09-06 00:19:45 +00:00
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
1
api/next/56219.txt
Normal file
1
api/next/56219.txt
Normal file
|
@ -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 -->
|
||||
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><!-- 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>
|
||||
</dl><!-- io -->
|
||||
|
||||
|
|
|
@ -172,6 +172,46 @@ func Clean(path string) 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
|
||||
// in path with a slash ('/') character. Multiple separators are
|
||||
// replaced by multiple slashes.
|
||||
|
|
|
@ -6,6 +6,10 @@ package filepath
|
|||
|
||||
import "strings"
|
||||
|
||||
func isLocal(path string) bool {
|
||||
return unixIsLocal(path)
|
||||
}
|
||||
|
||||
// IsAbs reports whether the path is absolute.
|
||||
func IsAbs(path string) bool {
|
||||
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
|
||||
|
||||
var slashtests = []PathTest{
|
||||
|
|
|
@ -8,6 +8,10 @@ package filepath
|
|||
|
||||
import "strings"
|
||||
|
||||
func isLocal(path string) bool {
|
||||
return unixIsLocal(path)
|
||||
}
|
||||
|
||||
// IsAbs reports whether the path is absolute.
|
||||
func IsAbs(path string) bool {
|
||||
return strings.HasPrefix(path, "/")
|
||||
|
|
|
@ -20,6 +20,73 @@ func toUpper(c byte) byte {
|
|||
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.
|
||||
func IsAbs(path string) (b bool) {
|
||||
l := volumeNameLen(path)
|
||||
|
|
Loading…
Reference in a new issue