Skip to content

Commit

Permalink
os: add Root.FS
Browse files Browse the repository at this point in the history
For #67002

Change-Id: Ib687c92d645b9172677e5781a3e51ef1a0427c30
Reviewed-on: https://go-review.googlesource.com/c/go/+/629518
Reviewed-by: Ian Lance Taylor <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
  • Loading branch information
neild committed Nov 20, 2024
1 parent 3d56891 commit a1b5394
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 2 deletions.
1 change: 1 addition & 0 deletions api/next/67002.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pkg os, func OpenRoot(string) (*Root, error) #67002
pkg os, method (*Root) Close() error #67002
pkg os, method (*Root) Create(string) (*File, error) #67002
pkg os, method (*Root) FS() fs.FS #67002
pkg os, method (*Root) Lstat(string) (fs.FileInfo, error) #67002
pkg os, method (*Root) Mkdir(string, fs.FileMode) error #67002
pkg os, method (*Root) Name() string #67002
Expand Down
5 changes: 5 additions & 0 deletions src/os/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,8 @@ func (f *File) SyscallConn() (syscall.RawConn, error) {
// a general substitute for a chroot-style security mechanism when the directory tree
// contains arbitrary content.
//
// Use [Root.FS] to obtain a fs.FS that prevents escapes from the tree via symbolic links.
//
// The directory dir must not be "".
//
// The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
Expand Down Expand Up @@ -800,7 +802,10 @@ func ReadFile(name string) ([]byte, error) {
return nil, err
}
defer f.Close()
return readFileContents(f)
}

func readFileContents(f *File) ([]byte, error) {
var size int
if info, err := f.Stat(); err == nil {
size64 := info.Size()
Expand Down
13 changes: 12 additions & 1 deletion src/os/os_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3188,10 +3188,21 @@ func forceMFTUpdateOnWindows(t *testing.T, path string) {

func TestDirFS(t *testing.T) {
t.Parallel()
testDirFS(t, DirFS("./testdata/dirfs"))
}

func TestRootDirFS(t *testing.T) {
t.Parallel()
r, err := OpenRoot("./testdata/dirfs")
if err != nil {
t.Fatal(err)
}
testDirFS(t, r.FS())
}

func testDirFS(t *testing.T, fsys fs.FS) {
forceMFTUpdateOnWindows(t, "./testdata/dirfs")

fsys := DirFS("./testdata/dirfs")
if err := fstest.TestFS(fsys, "a", "b", "dir/x"); err != nil {
t.Fatal(err)
}
Expand Down
88 changes: 88 additions & 0 deletions src/os/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ package os

import (
"errors"
"internal/bytealg"
"internal/stringslite"
"internal/testlog"
"io/fs"
"runtime"
"slices"
)

// Root may be used to only access files within a single directory tree.
Expand Down Expand Up @@ -213,3 +217,87 @@ func splitPathInRoot(s string, prefix, suffix []string) (_ []string, err error)
parts = append(parts, suffix...)
return parts, nil
}

// FS returns a file system (an fs.FS) for the tree of files in the root.
//
// The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
// [io/fs.ReadDirFS].
func (r *Root) FS() fs.FS {
return (*rootFS)(r)
}

type rootFS Root

func (rfs *rootFS) Open(name string) (fs.File, error) {
r := (*Root)(rfs)
if !isValidRootFSPath(name) {
return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
}
f, err := r.Open(name)
if err != nil {
return nil, err
}
return f, nil
}

func (rfs *rootFS) ReadDir(name string) ([]DirEntry, error) {
r := (*Root)(rfs)
if !isValidRootFSPath(name) {
return nil, &PathError{Op: "readdir", Path: name, Err: ErrInvalid}
}

// This isn't efficient: We just open a regular file and ReadDir it.
// Ideally, we would skip creating a *File entirely and operate directly
// on the file descriptor, but that will require some extensive reworking
// of directory reading in general.
//
// This suffices for the moment.
f, err := r.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
dirs, err := f.ReadDir(-1)
slices.SortFunc(dirs, func(a, b DirEntry) int {
return bytealg.CompareString(a.Name(), b.Name())
})
return dirs, err
}

func (rfs *rootFS) ReadFile(name string) ([]byte, error) {
r := (*Root)(rfs)
if !isValidRootFSPath(name) {
return nil, &PathError{Op: "readfile", Path: name, Err: ErrInvalid}
}
f, err := r.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return readFileContents(f)
}

func (rfs *rootFS) Stat(name string) (FileInfo, error) {
r := (*Root)(rfs)
if !isValidRootFSPath(name) {
return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
}
return r.Stat(name)
}

// isValidRootFSPath reprots whether name is a valid filename to pass a Root.FS method.
func isValidRootFSPath(name string) bool {
if !fs.ValidPath(name) {
return false
}
if runtime.GOOS == "windows" {
// fs.FS paths are /-separated.
// On Windows, reject the path if it contains any \ separators.
// Other forms of invalid path (for example, "NUL") are handled by
// Root's usual file lookup mechanisms.
if stringslite.IndexByte(name, '\\') >= 0 {
return false
}
}
return true
}
1 change: 0 additions & 1 deletion src/os/stat_wasip1.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
func fillFileStatFromSys(fs *fileStat, name string) {
fs.name = filepathlite.Base(name)
fs.size = int64(fs.sys.Size)
fs.mode = FileMode(fs.sys.Mode)
fs.modTime = time.Unix(0, int64(fs.sys.Mtime))

switch fs.sys.Filetype {
Expand Down

0 comments on commit a1b5394

Please sign in to comment.