Skip to content

Commit a1b5394

Browse files
committed
os: add Root.FS
For #67002 Change-Id: Ib687c92d645b9172677e5781a3e51ef1a0427c30 Reviewed-on: https://go-review.googlesource.com/c/go/+/629518 Reviewed-by: Ian Lance Taylor <iant@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 3d56891 commit a1b5394

File tree

5 files changed

+106
-2
lines changed

5 files changed

+106
-2
lines changed

api/next/67002.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pkg os, func OpenRoot(string) (*Root, error) #67002
22
pkg os, method (*Root) Close() error #67002
33
pkg os, method (*Root) Create(string) (*File, error) #67002
4+
pkg os, method (*Root) FS() fs.FS #67002
45
pkg os, method (*Root) Lstat(string) (fs.FileInfo, error) #67002
56
pkg os, method (*Root) Mkdir(string, fs.FileMode) error #67002
67
pkg os, method (*Root) Name() string #67002

src/os/file.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,8 @@ func (f *File) SyscallConn() (syscall.RawConn, error) {
696696
// a general substitute for a chroot-style security mechanism when the directory tree
697697
// contains arbitrary content.
698698
//
699+
// Use [Root.FS] to obtain a fs.FS that prevents escapes from the tree via symbolic links.
700+
//
699701
// The directory dir must not be "".
700702
//
701703
// The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
@@ -800,7 +802,10 @@ func ReadFile(name string) ([]byte, error) {
800802
return nil, err
801803
}
802804
defer f.Close()
805+
return readFileContents(f)
806+
}
803807

808+
func readFileContents(f *File) ([]byte, error) {
804809
var size int
805810
if info, err := f.Stat(); err == nil {
806811
size64 := info.Size()

src/os/os_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3188,10 +3188,21 @@ func forceMFTUpdateOnWindows(t *testing.T, path string) {
31883188

31893189
func TestDirFS(t *testing.T) {
31903190
t.Parallel()
3191+
testDirFS(t, DirFS("./testdata/dirfs"))
3192+
}
3193+
3194+
func TestRootDirFS(t *testing.T) {
3195+
t.Parallel()
3196+
r, err := OpenRoot("./testdata/dirfs")
3197+
if err != nil {
3198+
t.Fatal(err)
3199+
}
3200+
testDirFS(t, r.FS())
3201+
}
31913202

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

3194-
fsys := DirFS("./testdata/dirfs")
31953206
if err := fstest.TestFS(fsys, "a", "b", "dir/x"); err != nil {
31963207
t.Fatal(err)
31973208
}

src/os/root.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ package os
66

77
import (
88
"errors"
9+
"internal/bytealg"
10+
"internal/stringslite"
911
"internal/testlog"
12+
"io/fs"
1013
"runtime"
14+
"slices"
1115
)
1216

1317
// Root may be used to only access files within a single directory tree.
@@ -213,3 +217,87 @@ func splitPathInRoot(s string, prefix, suffix []string) (_ []string, err error)
213217
parts = append(parts, suffix...)
214218
return parts, nil
215219
}
220+
221+
// FS returns a file system (an fs.FS) for the tree of files in the root.
222+
//
223+
// The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
224+
// [io/fs.ReadDirFS].
225+
func (r *Root) FS() fs.FS {
226+
return (*rootFS)(r)
227+
}
228+
229+
type rootFS Root
230+
231+
func (rfs *rootFS) Open(name string) (fs.File, error) {
232+
r := (*Root)(rfs)
233+
if !isValidRootFSPath(name) {
234+
return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
235+
}
236+
f, err := r.Open(name)
237+
if err != nil {
238+
return nil, err
239+
}
240+
return f, nil
241+
}
242+
243+
func (rfs *rootFS) ReadDir(name string) ([]DirEntry, error) {
244+
r := (*Root)(rfs)
245+
if !isValidRootFSPath(name) {
246+
return nil, &PathError{Op: "readdir", Path: name, Err: ErrInvalid}
247+
}
248+
249+
// This isn't efficient: We just open a regular file and ReadDir it.
250+
// Ideally, we would skip creating a *File entirely and operate directly
251+
// on the file descriptor, but that will require some extensive reworking
252+
// of directory reading in general.
253+
//
254+
// This suffices for the moment.
255+
f, err := r.Open(name)
256+
if err != nil {
257+
return nil, err
258+
}
259+
defer f.Close()
260+
dirs, err := f.ReadDir(-1)
261+
slices.SortFunc(dirs, func(a, b DirEntry) int {
262+
return bytealg.CompareString(a.Name(), b.Name())
263+
})
264+
return dirs, err
265+
}
266+
267+
func (rfs *rootFS) ReadFile(name string) ([]byte, error) {
268+
r := (*Root)(rfs)
269+
if !isValidRootFSPath(name) {
270+
return nil, &PathError{Op: "readfile", Path: name, Err: ErrInvalid}
271+
}
272+
f, err := r.Open(name)
273+
if err != nil {
274+
return nil, err
275+
}
276+
defer f.Close()
277+
return readFileContents(f)
278+
}
279+
280+
func (rfs *rootFS) Stat(name string) (FileInfo, error) {
281+
r := (*Root)(rfs)
282+
if !isValidRootFSPath(name) {
283+
return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
284+
}
285+
return r.Stat(name)
286+
}
287+
288+
// isValidRootFSPath reprots whether name is a valid filename to pass a Root.FS method.
289+
func isValidRootFSPath(name string) bool {
290+
if !fs.ValidPath(name) {
291+
return false
292+
}
293+
if runtime.GOOS == "windows" {
294+
// fs.FS paths are /-separated.
295+
// On Windows, reject the path if it contains any \ separators.
296+
// Other forms of invalid path (for example, "NUL") are handled by
297+
// Root's usual file lookup mechanisms.
298+
if stringslite.IndexByte(name, '\\') >= 0 {
299+
return false
300+
}
301+
}
302+
return true
303+
}

src/os/stat_wasip1.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
func fillFileStatFromSys(fs *fileStat, name string) {
1616
fs.name = filepathlite.Base(name)
1717
fs.size = int64(fs.sys.Size)
18-
fs.mode = FileMode(fs.sys.Mode)
1918
fs.modTime = time.Unix(0, int64(fs.sys.Mtime))
2019

2120
switch fs.sys.Filetype {

0 commit comments

Comments
 (0)