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

sandbox: move path functions to fspath package #193

Merged
merged 3 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading