This implements fd_filestat_set_size and fd_filestat_set_times, which passes one more test in the rust wasi-testsuite. Signed-off-by: Adrian Cole <adrian@tetrate.io> Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
440 lines
13 KiB
Go
440 lines
13 KiB
Go
package sysfs
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
func NewRootFS(fs []FS, guestPaths []string) (FS, error) {
|
|
switch len(fs) {
|
|
case 0:
|
|
return UnimplementedFS{}, nil
|
|
case 1:
|
|
if StripPrefixesAndTrailingSlash(guestPaths[0]) == "" {
|
|
return fs[0], nil
|
|
}
|
|
}
|
|
|
|
ret := &CompositeFS{
|
|
string: stringFS(fs, guestPaths),
|
|
fs: make([]FS, len(fs)),
|
|
guestPaths: make([]string, len(fs)),
|
|
cleanedGuestPaths: make([]string, len(fs)),
|
|
rootGuestPaths: map[string]int{},
|
|
rootIndex: -1,
|
|
}
|
|
|
|
copy(ret.guestPaths, guestPaths)
|
|
copy(ret.fs, fs)
|
|
|
|
for i, guestPath := range guestPaths {
|
|
// Clean the prefix in the same way path matches will.
|
|
cleaned := StripPrefixesAndTrailingSlash(guestPath)
|
|
if cleaned == "" {
|
|
if ret.rootIndex != -1 {
|
|
return nil, fmt.Errorf("multiple root filesystems are invalid: %s", ret.string)
|
|
}
|
|
ret.rootIndex = i
|
|
} else if strings.HasPrefix(cleaned, "..") {
|
|
// ../ mounts are special cased and aren't returned in a directory
|
|
// listing, so we can ignore them for now.
|
|
} else if strings.Contains(cleaned, "/") {
|
|
return nil, fmt.Errorf("only single-level guest paths allowed: %s", ret.string)
|
|
} else {
|
|
ret.rootGuestPaths[cleaned] = i
|
|
}
|
|
ret.cleanedGuestPaths[i] = cleaned
|
|
}
|
|
|
|
// Ensure there is always a root match to keep runtime logic simpler.
|
|
if ret.rootIndex == -1 {
|
|
ret.rootIndex = len(fs)
|
|
ret.cleanedGuestPaths = append(ret.cleanedGuestPaths, "")
|
|
ret.fs = append(ret.fs, &fakeRootFS{})
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
type CompositeFS struct {
|
|
UnimplementedFS
|
|
// string is cached for convenience.
|
|
string string
|
|
// fs is index-correlated with cleanedGuestPaths
|
|
fs []FS
|
|
// guestPaths are the original paths supplied by the end user, cleaned as
|
|
// cleanedGuestPaths.
|
|
guestPaths []string
|
|
// cleanedGuestPaths to match in precedence order, ascending.
|
|
cleanedGuestPaths []string
|
|
// rootGuestPaths are cleanedGuestPaths that exist directly under root, such as
|
|
// "tmp".
|
|
rootGuestPaths map[string]int
|
|
// rootIndex is the index in fs that is the root filesystem
|
|
rootIndex int
|
|
}
|
|
|
|
// String implements fmt.Stringer
|
|
func (c *CompositeFS) String() string {
|
|
return c.string
|
|
}
|
|
|
|
func stringFS(fs []FS, guestPaths []string) string {
|
|
var ret strings.Builder
|
|
ret.WriteString("[")
|
|
writeMount(&ret, fs[0], guestPaths[0])
|
|
for i, f := range fs[1:] {
|
|
ret.WriteString(" ")
|
|
writeMount(&ret, f, guestPaths[i+1])
|
|
}
|
|
ret.WriteString("]")
|
|
return ret.String()
|
|
}
|
|
|
|
func writeMount(ret *strings.Builder, f FS, guestPath string) {
|
|
ret.WriteString(f.String())
|
|
ret.WriteString(":")
|
|
ret.WriteString(guestPath)
|
|
if _, ok := f.(*readFS); ok {
|
|
ret.WriteString(":ro")
|
|
}
|
|
}
|
|
|
|
// GuestPaths returns the underlying pre-open paths in original order.
|
|
func (c *CompositeFS) GuestPaths() (guestPaths []string) {
|
|
return c.guestPaths
|
|
}
|
|
|
|
// FS returns the underlying filesystems in original order.
|
|
func (c *CompositeFS) FS() (fs []FS) {
|
|
fs = make([]FS, len(c.guestPaths))
|
|
copy(fs, c.fs)
|
|
return
|
|
}
|
|
|
|
// Open implements the same method as documented on fs.FS
|
|
func (c *CompositeFS) Open(name string) (fs.File, error) {
|
|
return fsOpen(c, name)
|
|
}
|
|
|
|
// OpenFile implements FS.OpenFile
|
|
func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fs.File, err error) {
|
|
matchIndex, relativePath := c.chooseFS(path)
|
|
|
|
f, err = c.fs[matchIndex].OpenFile(relativePath, flag, perm)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Ensure the root directory listing includes any prefix mounts.
|
|
if matchIndex == c.rootIndex {
|
|
switch path {
|
|
case ".", "/", "":
|
|
if len(c.rootGuestPaths) > 0 {
|
|
f = &openRootDir{c: c, f: f.(fs.ReadDirFile)}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// An openRootDir is a root directory open for reading, which has mounts inside
|
|
// of it.
|
|
type openRootDir struct {
|
|
c *CompositeFS
|
|
f fs.ReadDirFile // the directory file itself
|
|
dirents []fs.DirEntry // the directory contents
|
|
direntsI int // the read offset, an index into the files slice
|
|
}
|
|
|
|
func (d *openRootDir) Close() error { return d.f.Close() }
|
|
|
|
func (d *openRootDir) Stat() (fs.FileInfo, error) { return d.f.Stat() }
|
|
|
|
func (d *openRootDir) Read([]byte) (int, error) {
|
|
return 0, &fs.PathError{Op: "read", Path: "/", Err: syscall.EISDIR}
|
|
}
|
|
|
|
// readDir reads the directory fully into d.dirents, replacing any entries that
|
|
// correspond to prefix matches or appending them to the end.
|
|
func (d *openRootDir) readDir() (err error) {
|
|
if d.dirents, err = d.f.ReadDir(-1); err != nil {
|
|
return
|
|
}
|
|
|
|
remaining := make(map[string]int, len(d.c.rootGuestPaths))
|
|
for k, v := range d.c.rootGuestPaths {
|
|
remaining[k] = v
|
|
}
|
|
|
|
for i, e := range d.dirents {
|
|
if fsI, ok := remaining[e.Name()]; ok {
|
|
if d.dirents[i], err = d.rootEntry(e.Name(), fsI); err != nil {
|
|
return
|
|
}
|
|
delete(remaining, e.Name())
|
|
}
|
|
}
|
|
|
|
var di fs.DirEntry
|
|
for n, fsI := range remaining {
|
|
if di, err = d.rootEntry(n, fsI); err != nil {
|
|
return
|
|
}
|
|
d.dirents = append(d.dirents, di)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (d *openRootDir) rootEntry(name string, fsI int) (fs.DirEntry, error) {
|
|
if fi, err := StatPath(d.c.fs[fsI], "."); err != nil {
|
|
return nil, err
|
|
} else {
|
|
return fs.FileInfoToDirEntry(&renamedFileInfo{name, fi}), nil
|
|
}
|
|
}
|
|
|
|
// renamedFileInfo is needed to retain the stat info for a mount, knowing the
|
|
// directory is masked. For example, we don't want to leak the underlying host
|
|
// directory name.
|
|
type renamedFileInfo struct {
|
|
name string
|
|
f fs.FileInfo
|
|
}
|
|
|
|
func (i *renamedFileInfo) Name() string { return i.name }
|
|
func (i *renamedFileInfo) Size() int64 { return i.f.Size() }
|
|
func (i *renamedFileInfo) Mode() fs.FileMode { return i.f.Mode() }
|
|
func (i *renamedFileInfo) ModTime() time.Time { return i.f.ModTime() }
|
|
func (i *renamedFileInfo) IsDir() bool { return i.f.IsDir() }
|
|
func (i *renamedFileInfo) Sys() interface{} { return i.f.Sys() }
|
|
|
|
func (d *openRootDir) ReadDir(count int) ([]fs.DirEntry, error) {
|
|
if d.dirents == nil {
|
|
if err := d.readDir(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// logic similar to go:embed
|
|
n := len(d.dirents) - d.direntsI
|
|
if n == 0 {
|
|
if count <= 0 {
|
|
return nil, nil
|
|
}
|
|
return nil, io.EOF
|
|
}
|
|
if count > 0 && n > count {
|
|
n = count
|
|
}
|
|
list := make([]fs.DirEntry, n)
|
|
for i := range list {
|
|
list[i] = d.dirents[d.direntsI+i]
|
|
}
|
|
d.direntsI += n
|
|
return list, nil
|
|
}
|
|
|
|
// Mkdir implements FS.Mkdir
|
|
func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) error {
|
|
matchIndex, relativePath := c.chooseFS(path)
|
|
return c.fs[matchIndex].Mkdir(relativePath, perm)
|
|
}
|
|
|
|
// Rename implements FS.Rename
|
|
func (c *CompositeFS) Rename(from, to string) error {
|
|
fromFS, fromPath := c.chooseFS(from)
|
|
toFS, toPath := c.chooseFS(to)
|
|
if fromFS != toFS {
|
|
return syscall.ENOSYS // not yet anyway
|
|
}
|
|
return c.fs[fromFS].Rename(fromPath, toPath)
|
|
}
|
|
|
|
// Rmdir implements FS.Rmdir
|
|
func (c *CompositeFS) Rmdir(path string) error {
|
|
matchIndex, relativePath := c.chooseFS(path)
|
|
return c.fs[matchIndex].Rmdir(relativePath)
|
|
}
|
|
|
|
// Unlink implements FS.Unlink
|
|
func (c *CompositeFS) Unlink(path string) error {
|
|
matchIndex, relativePath := c.chooseFS(path)
|
|
return c.fs[matchIndex].Unlink(relativePath)
|
|
}
|
|
|
|
// Utimes implements FS.Utimes
|
|
func (c *CompositeFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
|
|
matchIndex, relativePath := c.chooseFS(path)
|
|
return c.fs[matchIndex].Utimes(relativePath, atimeNsec, mtimeNsec)
|
|
}
|
|
|
|
// chooseFS chooses the best fs and the relative path to use for the input.
|
|
func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string) {
|
|
matchIndex = -1
|
|
matchPrefixLen := 0
|
|
pathI, pathLen := stripPrefixesAndTrailingSlash(path)
|
|
|
|
// Last is the highest precedence, so we iterate backwards. The last longest
|
|
// match wins. e.g. the pre-open "tmp" wins vs "" regardless of order.
|
|
for i := len(c.fs) - 1; i >= 0; i-- {
|
|
prefix := c.cleanedGuestPaths[i]
|
|
if eq, match := hasPathPrefix(path, pathI, pathLen, prefix); eq {
|
|
// When the input equals the prefix, there cannot be a longer match
|
|
// later. The relative path is the FS root, so return empty string.
|
|
matchIndex = i
|
|
relativePath = ""
|
|
return
|
|
} else if match {
|
|
// Check to see if this is a longer match
|
|
prefixLen := len(prefix)
|
|
if prefixLen > matchPrefixLen || matchIndex == -1 {
|
|
matchIndex = i
|
|
matchPrefixLen = prefixLen
|
|
}
|
|
} // Otherwise, keep looking for a match
|
|
}
|
|
|
|
// Now, we know the path != prefix, but it matched an existing fs, because
|
|
// setup ensures there's always a root filesystem.
|
|
|
|
// If this was a root path match the cleaned path is the relative one to
|
|
// pass to the underlying filesystem.
|
|
if matchPrefixLen == 0 {
|
|
// Avoid re-slicing when the input is already clean
|
|
if pathI == 0 && len(path) == pathLen {
|
|
relativePath = path
|
|
} else {
|
|
relativePath = path[pathI:pathLen]
|
|
}
|
|
return
|
|
}
|
|
|
|
// Otherwise, it is non-root match: the relative path is past "$prefix/"
|
|
pathI += matchPrefixLen + 1 // e.g. prefix=foo, path=foo/bar -> bar
|
|
relativePath = path[pathI:pathLen]
|
|
return
|
|
}
|
|
|
|
// hasPathPrefix compares an input path against a prefix, both cleaned by
|
|
// stripPrefixesAndTrailingSlash. This returns a pair of eq, match to allow an
|
|
// early short circuit on match.
|
|
//
|
|
// Note: This is case-sensitive because POSIX paths are compared case
|
|
// sensitively.
|
|
func hasPathPrefix(path string, pathI, pathLen int, prefix string) (eq, match bool) {
|
|
matchLen := pathLen - pathI
|
|
if prefix == "" {
|
|
return matchLen == 0, true // e.g. prefix=, path=foo
|
|
}
|
|
|
|
prefixLen := len(prefix)
|
|
// reset pathLen temporarily to represent the length to match as opposed to
|
|
// the length of the string (that may contain leading slashes).
|
|
if matchLen == prefixLen {
|
|
if pathContainsPrefix(path, pathI, prefixLen, prefix) {
|
|
return true, true // e.g. prefix=bar, path=bar
|
|
}
|
|
return false, false
|
|
} else if matchLen < prefixLen {
|
|
return false, false // e.g. prefix=fooo, path=foo
|
|
}
|
|
|
|
if path[pathI+prefixLen] != '/' {
|
|
return false, false // e.g. prefix=foo, path=fooo
|
|
}
|
|
|
|
// Not equal, but maybe a match. e.g. prefix=foo, path=foo/bar
|
|
return false, pathContainsPrefix(path, pathI, prefixLen, prefix)
|
|
}
|
|
|
|
// pathContainsPrefix is faster than strings.HasPrefix even if we didn't cache
|
|
// the index,len. See benchmarks.
|
|
func pathContainsPrefix(path string, pathI, prefixLen int, prefix string) bool {
|
|
for i := 0; i < prefixLen; i++ {
|
|
if path[pathI] != prefix[i] {
|
|
return false // e.g. prefix=bar, path=foo or foo/bar
|
|
}
|
|
pathI++
|
|
}
|
|
return true // e.g. prefix=foo, path=foo or foo/bar
|
|
}
|
|
|
|
func StripPrefixesAndTrailingSlash(path string) string {
|
|
pathI, pathLen := stripPrefixesAndTrailingSlash(path)
|
|
return path[pathI:pathLen]
|
|
}
|
|
|
|
// stripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
|
|
// result index begins with another string. A result of "." coerces to the
|
|
// empty string "" because the current directory is handled by the guest.
|
|
//
|
|
// Results are the offset/len pair which is an optimization to avoid re-slicing
|
|
// overhead, as this function is called for every path operation.
|
|
//
|
|
// Note: Relative paths should be handled by the guest, as that's what knows
|
|
// what the current directory is. However, paths that escape the current
|
|
// directory e.g. "../.." have been found in `tinygo test` and this
|
|
// implementation takes care to avoid it.
|
|
func stripPrefixesAndTrailingSlash(path string) (pathI, pathLen int) {
|
|
// strip trailing slashes
|
|
pathLen = len(path)
|
|
for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- {
|
|
}
|
|
|
|
pathI = 0
|
|
loop:
|
|
for pathI < pathLen {
|
|
switch path[pathI] {
|
|
case '/':
|
|
pathI++
|
|
case '.':
|
|
nextI := pathI + 1
|
|
if nextI < pathLen && path[nextI] == '/' {
|
|
pathI = nextI + 1
|
|
} else if nextI == pathLen {
|
|
pathI = nextI
|
|
} else {
|
|
break loop
|
|
}
|
|
default:
|
|
break loop
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
type fakeRootFS struct{ UnimplementedFS }
|
|
|
|
// OpenFile implements FS.OpenFile
|
|
func (*fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fs.File, error) {
|
|
switch path {
|
|
case ".", "/", "":
|
|
return fakeRootDir{}, nil
|
|
}
|
|
return nil, syscall.ENOENT
|
|
}
|
|
|
|
type fakeRootDir struct{}
|
|
|
|
func (fakeRootDir) Close() (err error) { return }
|
|
|
|
func (fakeRootDir) Stat() (fs.FileInfo, error) { return fakeRootDirInfo{}, nil }
|
|
|
|
func (fakeRootDir) Read([]byte) (int, error) {
|
|
return 0, &fs.PathError{Op: "read", Path: "/", Err: syscall.EISDIR}
|
|
}
|
|
|
|
type fakeRootDirInfo struct{}
|
|
|
|
func (fakeRootDirInfo) Name() string { return "/" }
|
|
func (fakeRootDirInfo) Size() int64 { return 0 }
|
|
func (fakeRootDirInfo) Mode() fs.FileMode { return fs.ModeDir | 0o500 }
|
|
func (fakeRootDirInfo) ModTime() time.Time { return time.Unix(0, 0) }
|
|
func (fakeRootDirInfo) IsDir() bool { return true }
|
|
func (fakeRootDirInfo) Sys() interface{} { return nil }
|
|
func (fakeRootDir) ReadDir(int) (dirents []fs.DirEntry, err error) { return }
|