Skip to content
This repository has been archived by the owner on Feb 17, 2024. It is now read-only.

Commit

Permalink
Merge pull request #193 from stealthrocket/fspath
Browse files Browse the repository at this point in the history
sandbox: move path functions to fspath package
  • Loading branch information
achille-roussel authored Aug 7, 2023
2 parents a92689e + b6a50db commit 3520e5f
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 256 deletions.
13 changes: 7 additions & 6 deletions internal/sandbox/dirfs_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"io/fs"
"os"

"github.com/stealthrocket/timecraft/internal/sandbox/fspath"
"golang.org/x/sys/unix"
)

Expand All @@ -14,11 +15,11 @@ func (root dirFS) Open(name string, flags int, mode fs.FileMode) (File, error) {
if err != nil {
return nil, &fs.PathError{Op: "open", Path: string(root), Err: err}
}
if name = cleanPath(name); name == "/" || name == "." { // root?
if name = fspath.Clean(name); name == "/" || name == "." { // root?
return &dirFile{fd: dirfd, name: "/"}, nil
}
defer closeTraceError(dirfd)
relPath := "/" + trimLeadingSlash(name)
relPath := "/" + fspath.TrimLeadingSlash(name)
fd, err := openat(dirfd, name, flags, uint32(mode.Perm()))
if err != nil {
return nil, &fs.PathError{Op: "open", Path: relPath, Err: err}
Expand Down Expand Up @@ -49,7 +50,7 @@ func (f *dirFile) Close() error {
}

func (f *dirFile) Open(name string, flags int, mode fs.FileMode) (File, error) {
name = cleanPath(name)
name = fspath.Clean(name)
relPath := f.join(name)
fd, err := openat(f.fd, name, flags, uint32(mode.Perm()))
if err != nil {
Expand Down Expand Up @@ -236,7 +237,7 @@ func (f *dirFile) Rename(oldName string, newDir File, newName string) error {
fd2 := int(newDir.Fd())
if err := renameat(fd1, oldName, fd2, newName); err != nil {
path1 := f.join(oldName)
path2 := joinPath(newDir.Name(), newName)
path2 := fspath.Join(newDir.Name(), newName)
return &os.LinkError{Op: "rename", Old: path1, New: path2, Err: err}
}
return nil
Expand All @@ -251,7 +252,7 @@ func (f *dirFile) Link(oldName string, newDir File, newName string, flags int) e
fd2 := int(newDir.Fd())
if err := linkat(fd1, oldName, fd2, newName, linkFlags); err != nil {
path1 := f.join(oldName)
path2 := joinPath(newDir.Name(), newName)
path2 := fspath.Join(newDir.Name(), newName)
return &os.LinkError{Op: "link", Old: path1, New: path2, Err: err}
}
return nil
Expand All @@ -272,5 +273,5 @@ func (f *dirFile) Unlink(name string) error {
}

func (f *dirFile) join(name string) string {
return joinPath(f.name, name)
return fspath.Join(f.name, name)
}
12 changes: 5 additions & 7 deletions internal/sandbox/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
"os/user"
"path"
"strconv"
"strings"
"time"

"github.com/stealthrocket/timecraft/internal/sandbox/fspath"
)

const (
Expand Down Expand Up @@ -123,11 +124,11 @@ func MkdirAll(fsys FileSystem, name string, mode fs.FileMode) error {
}

func mkdirAll(fsys FileSystem, name string, mode fs.FileMode) error {
path := cleanPath(name)
path := fspath.Clean(name)
if path == "/" || path == "." {
return nil
}
path = strings.TrimPrefix(path, "/")
path = fspath.TrimLeadingSlash(path)

d, err := OpenRoot(fsys)
if err != nil {
Expand All @@ -137,10 +138,7 @@ func mkdirAll(fsys FileSystem, name string, mode fs.FileMode) error {

for path != "" {
var dir string
dir, path = walkPath(path)
if dir == "." {
dir, path = path, ""
}
dir, path = fspath.Walk(path)

if err := d.Mkdir(dir, mode); err != nil {
if !errors.Is(err, EEXIST) {
Expand Down
150 changes: 150 additions & 0 deletions internal/sandbox/fspath/fspath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Package fspath is similar to the standard path package but provides functions
// that are more useful for path manipulation in the presence of symbolic links.
package fspath

// Depth returns the depth of a path. The root "/" has depth zero.
func Depth(path string) (depth int) {
for {
path = TrimLeadingSlash(path)
if path == "" {
return depth
}
i := IndexSlash(path)
if i < 0 {
i = len(path)
}
switch path[:i] {
case ".":
case "..":
if depth > 0 {
depth--
}
default:
depth++
}
path = path[i:]
}
}

// Join is similar to path.Join but is simplified to only join two paths and
// avoid cleaning parent directory references in the paths.
func Join(dir, name string) string {
buf := make([]byte, 0, 256)
buf = AppendClean(buf, dir)
if name = TrimLeadingSlash(name); name != "" {
if !HasTrailingSlash(string(buf)) {
buf = append(buf, '/')
}
buf = AppendClean(buf, name)
}
return string(buf)
}

// Clean is like path.Clean but it preserves parent directory references;
// this is necessary to ensure that symbolic links aren't erased from walking
// the path.
func Clean(path string) string {
buf := make([]byte, 0, 256)
buf = AppendClean(buf, path)
return string(buf)
}

// AppendClean is like cleanPath but it appends the result to the byte slice
// passed as first argument.
func AppendClean(buf []byte, path string) []byte {
if len(path) == 0 {
return buf
}

type region struct {
off, end int
}

elems := make([]region, 0, 16)
if IsAbs(path) {
elems = append(elems, region{})
}

i := 0
for {
for i < len(path) && path[i] == '/' {
i++
}
if i == len(path) {
break
}
j := i
for j < len(path) && path[j] != '/' {
j++
}
if path[i:j] != "." {
elems = append(elems, region{off: i, end: j})
}
i = j
}

if len(elems) == 0 {
return append(buf, '.')
}
if len(elems) == 1 && elems[0] == (region{}) {
return append(buf, '/')
}
for i, elem := range elems {
if i != 0 {
buf = append(buf, '/')
}
buf = append(buf, path[elem.off:elem.end]...)
}
if HasTrailingSlash(path) {
buf = append(buf, '/')
}
return buf
}

// IndexSlash is like strings.IndexByte(path, '/') but the function is simple
// enough to be inlined, which is a measurable improvement since it gets called
// very often by the other routines in this file.
func IndexSlash(path string) int {
for i := 0; i < len(path); i++ {
if path[i] == '/' {
return i
}
}
return -1
}

// Walk separates the next path element from the rest of the path.
func Walk(path string) (elem, name string) {
i := IndexSlash(path)
if i < 0 {
return path, ""
}
if i == 0 {
i = 1
}
return path[:i], TrimLeadingSlash(path[i:])
}

func HasTrailingSlash(s string) bool {
return len(s) > 0 && s[len(s)-1] == '/'
}

func TrimLeadingSlash(s string) string {
i := 0
for i < len(s) && s[i] == '/' {
i++
}
return s[i:]
}

func TrimTrailingSlash(s string) string {
i := len(s)
for i > 0 && s[i-1] == '/' {
i--
}
return s[:i]
}

func IsAbs(path string) bool {
return len(path) > 0 && path[0] == '/'
}
88 changes: 88 additions & 0 deletions internal/sandbox/fspath/fspath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package fspath_test

import (
"testing"

"github.com/stealthrocket/timecraft/internal/assert"
"github.com/stealthrocket/timecraft/internal/sandbox/fspath"
)

func TestDepth(t *testing.T) {
tests := []struct {
path string
depth int
}{
{"", 0},
{".", 0},
{"/", 0},
{"..", 0},
{"/..", 0},
{"a/b/c", 3},
{"//hello//world/", 2},
{"/../path/././to///file/..", 2},
}

for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
assert.Equal(t, fspath.Depth(test.path), test.depth)
})
}
}

func TestJoin(t *testing.T) {
tests := []struct {
dir string
name string
path string
}{
{"", "", ""},
{".", ".", "./."},
{".", "hello", "./hello"},
{"hello", ".", "hello/."},
{"/", "/", "/"},
{"..//", ".", "../."},
{"hello/world", "!", "hello/world/!"},
{"/hello", "/world", "/hello/world"},
{"/hello", "/world/", "/hello/world/"},
{"//hello", "//world", "/hello/world"},
{"//hello/", "//world//", "/hello/world/"},
{"hello/../", "../world/./", "hello/../../world/"},
{"hello", "/.", "hello/."},
}

for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
path := fspath.Join(test.dir, test.name)
assert.Equal(t, path, test.path)
})
}
}

func TestClean(t *testing.T) {
tests := []struct {
input string
output string
}{
{"", ""},
{".", "."},
{"..", ".."},
{"./", "."},
{"/././././", "/"},
{"hello/world", "hello/world"},
{"/hello/world", "/hello/world"},
{"/tmp/.././//test/", "/tmp/../test/"},
}

for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
path := fspath.Clean(test.input)
assert.Equal(t, path, test.output)
})
}
}

func BenchmarkClean(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fspath.Clean("/tmp/.././//test/")
}
}
Loading

0 comments on commit 3520e5f

Please sign in to comment.