mirror of https://github.com/golang/go.git
path/filepath: add Localize
Add the Localize function, which takes an io/fs slash-separated path and returns an operating system path. Localize returns an error if the path cannot be represented on the current platform. Replace internal/safefile.FromFS with Localize, which serves the same purpose as this function. The internal/safefile package remains separate from path/filepath to avoid a dependency cycle with the os package. Fixes #57151 Change-Id: I75c88047ddea17808276761da07bf79172c4f6fc Reviewed-on: https://go-review.googlesource.com/c/go/+/531677 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Lance Taylor <iant@google.com>
This commit is contained in:
parent
7b583fd1a1
commit
e596e88318
|
|
@ -0,0 +1 @@
|
||||||
|
pkg path/filepath, func Localize(string) (string, error) #57151
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
The new [`Localize`](/path/filepath#Localize) function safely converts
|
||||||
|
a slash-separated path into an operating system path.
|
||||||
|
|
@ -7,15 +7,20 @@ package safefilepath
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errInvalidPath = errors.New("invalid path")
|
var errInvalidPath = errors.New("invalid path")
|
||||||
|
|
||||||
// FromFS converts a slash-separated path into an operating-system path.
|
// Localize is filepath.Localize.
|
||||||
//
|
//
|
||||||
// FromFS returns an error if the path cannot be represented by the operating
|
// It is implemented in this package to avoid a dependency cycle
|
||||||
// system. For example, paths containing '\' and ':' characters are rejected
|
// between os and file/filepath.
|
||||||
// on Windows.
|
//
|
||||||
func FromFS(path string) (string, error) {
|
// Tests for this function are in path/filepath.
|
||||||
return fromFS(path)
|
func Localize(path string) (string, error) {
|
||||||
|
if !fs.ValidPath(path) {
|
||||||
|
return "", errInvalidPath
|
||||||
|
}
|
||||||
|
return localize(path)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
// Copyright 2022 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 safefilepath
|
|
||||||
|
|
||||||
import (
|
|
||||||
"internal/bytealg"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func fromFS(path string) (string, error) {
|
|
||||||
if runtime.GOOS == "plan9" {
|
|
||||||
if len(path) > 0 && path[0] == '#' {
|
|
||||||
return "", errInvalidPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bytealg.IndexByteString(path, 0) >= 0 {
|
|
||||||
return "", errInvalidPath
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package safefilepath
|
||||||
|
|
||||||
|
import "internal/bytealg"
|
||||||
|
|
||||||
|
func localize(path string) (string, error) {
|
||||||
|
if path[0] == '#' || bytealg.IndexByteString(path, 0) >= 0 {
|
||||||
|
return "", errInvalidPath
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
// Copyright 2022 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.
|
|
||||||
|
|
||||||
package safefilepath_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"internal/safefilepath"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PathTest struct {
|
|
||||||
path, result string
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalid = ""
|
|
||||||
|
|
||||||
var fspathtests = []PathTest{
|
|
||||||
{".", "."},
|
|
||||||
{"/a/b/c", "/a/b/c"},
|
|
||||||
{"a\x00b", invalid},
|
|
||||||
}
|
|
||||||
|
|
||||||
var winreservedpathtests = []PathTest{
|
|
||||||
{`a\b`, `a\b`},
|
|
||||||
{`a:b`, `a:b`},
|
|
||||||
{`a/b:c`, `a/b:c`},
|
|
||||||
{`NUL`, `NUL`},
|
|
||||||
{`./com1`, `./com1`},
|
|
||||||
{`a/nul/b`, `a/nul/b`},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether a reserved name with an extension is reserved or not varies by
|
|
||||||
// Windows version.
|
|
||||||
var winreservedextpathtests = []PathTest{
|
|
||||||
{"nul.txt", "nul.txt"},
|
|
||||||
{"a/nul.txt/b", "a/nul.txt/b"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var plan9reservedpathtests = []PathTest{
|
|
||||||
{`#c`, `#c`},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFromFS(t *testing.T) {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
if canWriteFile(t, "NUL") {
|
|
||||||
t.Errorf("can unexpectedly write a file named NUL on Windows")
|
|
||||||
}
|
|
||||||
if canWriteFile(t, "nul.txt") {
|
|
||||||
fspathtests = append(fspathtests, winreservedextpathtests...)
|
|
||||||
} else {
|
|
||||||
winreservedpathtests = append(winreservedpathtests, winreservedextpathtests...)
|
|
||||||
}
|
|
||||||
for i := range winreservedpathtests {
|
|
||||||
winreservedpathtests[i].result = invalid
|
|
||||||
}
|
|
||||||
for i := range fspathtests {
|
|
||||||
fspathtests[i].result = filepath.FromSlash(fspathtests[i].result)
|
|
||||||
}
|
|
||||||
case "plan9":
|
|
||||||
for i := range plan9reservedpathtests {
|
|
||||||
plan9reservedpathtests[i].result = invalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tests := fspathtests
|
|
||||||
tests = append(tests, winreservedpathtests...)
|
|
||||||
tests = append(tests, plan9reservedpathtests...)
|
|
||||||
for _, test := range tests {
|
|
||||||
got, err := safefilepath.FromFS(test.path)
|
|
||||||
if (got == "") != (err != nil) {
|
|
||||||
t.Errorf(`FromFS(%q) = %q, %v; want "" only if err != nil`, test.path, got, err)
|
|
||||||
}
|
|
||||||
if got != test.result {
|
|
||||||
t.Errorf("FromFS(%q) = %q, %v; want %q", test.path, got, err, test.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func canWriteFile(t *testing.T, name string) bool {
|
|
||||||
path := filepath.Join(t.TempDir(), name)
|
|
||||||
os.WriteFile(path, []byte("ok"), 0666)
|
|
||||||
b, _ := os.ReadFile(path)
|
|
||||||
return string(b) == "ok"
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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 unix || (js && wasm) || wasip1
|
||||||
|
|
||||||
|
package safefilepath
|
||||||
|
|
||||||
|
import "internal/bytealg"
|
||||||
|
|
||||||
|
func localize(path string) (string, error) {
|
||||||
|
if bytealg.IndexByteString(path, 0) >= 0 {
|
||||||
|
return "", errInvalidPath
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
@ -5,36 +5,31 @@
|
||||||
package safefilepath
|
package safefilepath
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"internal/bytealg"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unicode/utf8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func fromFS(path string) (string, error) {
|
func localize(path string) (string, error) {
|
||||||
if !utf8.ValidString(path) {
|
for i := 0; i < len(path); i++ {
|
||||||
return "", errInvalidPath
|
switch path[i] {
|
||||||
}
|
case ':', '\\', 0:
|
||||||
for len(path) > 1 && path[0] == '/' && path[1] == '/' {
|
return "", errInvalidPath
|
||||||
path = path[1:]
|
}
|
||||||
}
|
}
|
||||||
containsSlash := false
|
containsSlash := false
|
||||||
for p := path; p != ""; {
|
for p := path; p != ""; {
|
||||||
// Find the next path element.
|
// Find the next path element.
|
||||||
i := 0
|
var element string
|
||||||
for i < len(p) && p[i] != '/' {
|
i := bytealg.IndexByteString(p, '/')
|
||||||
switch p[i] {
|
if i < 0 {
|
||||||
case 0, '\\', ':':
|
element = p
|
||||||
return "", errInvalidPath
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
part := p[:i]
|
|
||||||
if i < len(p) {
|
|
||||||
containsSlash = true
|
|
||||||
p = p[i+1:]
|
|
||||||
} else {
|
|
||||||
p = ""
|
p = ""
|
||||||
|
} else {
|
||||||
|
containsSlash = true
|
||||||
|
element = p[:i]
|
||||||
|
p = p[i+1:]
|
||||||
}
|
}
|
||||||
if IsReservedName(part) {
|
if IsReservedName(element) {
|
||||||
return "", errInvalidPath
|
return "", errInvalidPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ package http
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"internal/safefilepath"
|
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"mime"
|
"mime"
|
||||||
|
|
@ -70,7 +69,11 @@ func mapOpenError(originalErr error, name string, sep rune, stat func(string) (f
|
||||||
// Open implements [FileSystem] using [os.Open], opening files for reading rooted
|
// Open implements [FileSystem] using [os.Open], opening files for reading rooted
|
||||||
// and relative to the directory d.
|
// and relative to the directory d.
|
||||||
func (d Dir) Open(name string) (File, error) {
|
func (d Dir) Open(name string) (File, error) {
|
||||||
path, err := safefilepath.FromFS(path.Clean("/" + name))
|
path := path.Clean("/" + name)[1:]
|
||||||
|
if path == "" {
|
||||||
|
path = "."
|
||||||
|
}
|
||||||
|
path, err := filepath.Localize(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("http: invalid or unsafe file path")
|
return nil, errors.New("http: invalid or unsafe file path")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ func CopyFS(dir string, fsys fs.FS) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fpath, err := safefilepath.FromFS(path)
|
fpath, err := safefilepath.Localize(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -747,10 +747,7 @@ func (dir dirFS) join(name string) (string, error) {
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return "", errors.New("os: DirFS with empty root")
|
return "", errors.New("os: DirFS with empty root")
|
||||||
}
|
}
|
||||||
if !fs.ValidPath(name) {
|
name, err := safefilepath.Localize(name)
|
||||||
return "", ErrInvalid
|
|
||||||
}
|
|
||||||
name, err := safefilepath.FromFS(name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", ErrInvalid
|
return "", ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ package filepath
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"internal/safefilepath"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
@ -211,6 +212,18 @@ func unixIsLocal(path string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Localize converts a slash-separated path into an operating system path.
|
||||||
|
// The input path must be a valid path as reported by [io/fs.ValidPath].
|
||||||
|
//
|
||||||
|
// Localize returns an error if the path cannot be represented by the operating system.
|
||||||
|
// For example, the path a\b is rejected on Windows, on which \ is a separator
|
||||||
|
// character and cannot be part of a filename.
|
||||||
|
//
|
||||||
|
// The path returned by Localize will always be local, as reported by IsLocal.
|
||||||
|
func Localize(path string) (string, error) {
|
||||||
|
return safefilepath.Localize(path)
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
@ -224,6 +237,9 @@ func ToSlash(path string) string {
|
||||||
// FromSlash returns the result of replacing each slash ('/') character
|
// FromSlash returns the result of replacing each slash ('/') character
|
||||||
// in path with a separator character. Multiple slashes are replaced
|
// in path with a separator character. Multiple slashes are replaced
|
||||||
// by multiple separators.
|
// by multiple separators.
|
||||||
|
//
|
||||||
|
// See also the Localize function, which converts a slash-separated path
|
||||||
|
// as used by the io/fs package to an operating system path.
|
||||||
func FromSlash(path string) string {
|
func FromSlash(path string) string {
|
||||||
if Separator == '/' {
|
if Separator == '/' {
|
||||||
return path
|
return path
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,73 @@ func TestIsLocal(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LocalizeTest struct {
|
||||||
|
path string
|
||||||
|
want string
|
||||||
|
}
|
||||||
|
|
||||||
|
var localizetests = []LocalizeTest{
|
||||||
|
{"", ""},
|
||||||
|
{".", "."},
|
||||||
|
{"..", ""},
|
||||||
|
{"a/..", ""},
|
||||||
|
{"/", ""},
|
||||||
|
{"/a", ""},
|
||||||
|
{"a\xffb", ""},
|
||||||
|
{"a/", ""},
|
||||||
|
{"a/./b", ""},
|
||||||
|
{"\x00", ""},
|
||||||
|
{"a", "a"},
|
||||||
|
{"a/b/c", "a/b/c"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var plan9localizetests = []LocalizeTest{
|
||||||
|
{"#a", ""},
|
||||||
|
{`a\b:c`, `a\b:c`},
|
||||||
|
}
|
||||||
|
|
||||||
|
var unixlocalizetests = []LocalizeTest{
|
||||||
|
{"#a", "#a"},
|
||||||
|
{`a\b:c`, `a\b:c`},
|
||||||
|
}
|
||||||
|
|
||||||
|
var winlocalizetests = []LocalizeTest{
|
||||||
|
{"#a", "#a"},
|
||||||
|
{"c:", ""},
|
||||||
|
{`a\b`, ""},
|
||||||
|
{`a:b`, ""},
|
||||||
|
{`a/b:c`, ""},
|
||||||
|
{`NUL`, ""},
|
||||||
|
{`a/NUL`, ""},
|
||||||
|
{`./com1`, ""},
|
||||||
|
{`a/nul/b`, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalize(t *testing.T) {
|
||||||
|
tests := localizetests
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "plan9":
|
||||||
|
tests = append(tests, plan9localizetests...)
|
||||||
|
case "windows":
|
||||||
|
tests = append(tests, winlocalizetests...)
|
||||||
|
for i := range tests {
|
||||||
|
tests[i].want = filepath.FromSlash(tests[i].want)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
tests = append(tests, unixlocalizetests...)
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
got, err := filepath.Localize(test.path)
|
||||||
|
wantErr := "<nil>"
|
||||||
|
if test.want == "" {
|
||||||
|
wantErr = "error"
|
||||||
|
}
|
||||||
|
if got != test.want || ((err == nil) != (test.want != "")) {
|
||||||
|
t.Errorf("IsLocal(%q) = %q, %v want %q, %v", test.path, got, err, test.want, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sep = filepath.Separator
|
const sep = filepath.Separator
|
||||||
|
|
||||||
var slashtests = []PathTest{
|
var slashtests = []PathTest{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue