github.com/tetratelabs/wazero@v1.2.1/internal/sysfs/rootfs.go (about)

     1  package sysfs
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/fs"
     7  	"strings"
     8  	"syscall"
     9  
    10  	"github.com/tetratelabs/wazero/internal/fsapi"
    11  )
    12  
    13  func NewRootFS(fs []fsapi.FS, guestPaths []string) (fsapi.FS, error) {
    14  	switch len(fs) {
    15  	case 0:
    16  		return fsapi.UnimplementedFS{}, nil
    17  	case 1:
    18  		if StripPrefixesAndTrailingSlash(guestPaths[0]) == "" {
    19  			return fs[0], nil
    20  		}
    21  	}
    22  
    23  	ret := &CompositeFS{
    24  		string:            stringFS(fs, guestPaths),
    25  		fs:                make([]fsapi.FS, len(fs)),
    26  		guestPaths:        make([]string, len(fs)),
    27  		cleanedGuestPaths: make([]string, len(fs)),
    28  		rootGuestPaths:    map[string]int{},
    29  		rootIndex:         -1,
    30  	}
    31  
    32  	copy(ret.guestPaths, guestPaths)
    33  	copy(ret.fs, fs)
    34  
    35  	for i, guestPath := range guestPaths {
    36  		// Clean the prefix in the same way path matches will.
    37  		cleaned := StripPrefixesAndTrailingSlash(guestPath)
    38  		if cleaned == "" {
    39  			if ret.rootIndex != -1 {
    40  				return nil, fmt.Errorf("multiple root filesystems are invalid: %s", ret.string)
    41  			}
    42  			ret.rootIndex = i
    43  		} else if strings.HasPrefix(cleaned, "..") {
    44  			// ../ mounts are special cased and aren't returned in a directory
    45  			// listing, so we can ignore them for now.
    46  		} else if strings.Contains(cleaned, "/") {
    47  			return nil, fmt.Errorf("only single-level guest paths allowed: %s", ret.string)
    48  		} else {
    49  			ret.rootGuestPaths[cleaned] = i
    50  		}
    51  		ret.cleanedGuestPaths[i] = cleaned
    52  	}
    53  
    54  	// Ensure there is always a root match to keep runtime logic simpler.
    55  	if ret.rootIndex == -1 {
    56  		ret.rootIndex = len(fs)
    57  		ret.cleanedGuestPaths = append(ret.cleanedGuestPaths, "")
    58  		ret.fs = append(ret.fs, &fakeRootFS{})
    59  	}
    60  	return ret, nil
    61  }
    62  
    63  type CompositeFS struct {
    64  	fsapi.UnimplementedFS
    65  	// string is cached for convenience.
    66  	string string
    67  	// fs is index-correlated with cleanedGuestPaths
    68  	fs []fsapi.FS
    69  	// guestPaths are the original paths supplied by the end user, cleaned as
    70  	// cleanedGuestPaths.
    71  	guestPaths []string
    72  	// cleanedGuestPaths to match in precedence order, ascending.
    73  	cleanedGuestPaths []string
    74  	// rootGuestPaths are cleanedGuestPaths that exist directly under root, such as
    75  	// "tmp".
    76  	rootGuestPaths map[string]int
    77  	// rootIndex is the index in fs that is the root filesystem
    78  	rootIndex int
    79  }
    80  
    81  // String implements fmt.Stringer
    82  func (c *CompositeFS) String() string {
    83  	return c.string
    84  }
    85  
    86  func stringFS(fs []fsapi.FS, guestPaths []string) string {
    87  	var ret strings.Builder
    88  	ret.WriteString("[")
    89  	writeMount(&ret, fs[0], guestPaths[0])
    90  	for i, f := range fs[1:] {
    91  		ret.WriteString(" ")
    92  		writeMount(&ret, f, guestPaths[i+1])
    93  	}
    94  	ret.WriteString("]")
    95  	return ret.String()
    96  }
    97  
    98  func writeMount(ret *strings.Builder, f fsapi.FS, guestPath string) {
    99  	ret.WriteString(f.String())
   100  	ret.WriteString(":")
   101  	ret.WriteString(guestPath)
   102  	if _, ok := f.(*readFS); ok {
   103  		ret.WriteString(":ro")
   104  	}
   105  }
   106  
   107  // GuestPaths returns the underlying pre-open paths in original order.
   108  func (c *CompositeFS) GuestPaths() (guestPaths []string) {
   109  	return c.guestPaths
   110  }
   111  
   112  // FS returns the underlying filesystems in original order.
   113  func (c *CompositeFS) FS() (fs []fsapi.FS) {
   114  	fs = make([]fsapi.FS, len(c.guestPaths))
   115  	copy(fs, c.fs)
   116  	return
   117  }
   118  
   119  // OpenFile implements the same method as documented on api.FS
   120  func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fsapi.File, err syscall.Errno) {
   121  	matchIndex, relativePath := c.chooseFS(path)
   122  
   123  	f, err = c.fs[matchIndex].OpenFile(relativePath, flag, perm)
   124  	if err != 0 {
   125  		return
   126  	}
   127  
   128  	// Ensure the root directory listing includes any prefix mounts.
   129  	if matchIndex == c.rootIndex {
   130  		switch path {
   131  		case ".", "/", "":
   132  			if len(c.rootGuestPaths) > 0 {
   133  				f = &openRootDir{path: path, c: c, f: f}
   134  			}
   135  		}
   136  	}
   137  	return
   138  }
   139  
   140  // An openRootDir is a root directory open for reading, which has mounts inside
   141  // of it.
   142  type openRootDir struct {
   143  	fsapi.DirFile
   144  
   145  	path     string
   146  	c        *CompositeFS
   147  	f        fsapi.File     // the directory file itself
   148  	dirents  []fsapi.Dirent // the directory contents
   149  	direntsI int            // the read offset, an index into the files slice
   150  }
   151  
   152  // Ino implements the same method as documented on fsapi.File
   153  func (d *openRootDir) Ino() (uint64, syscall.Errno) {
   154  	return d.f.Ino()
   155  }
   156  
   157  // Stat implements the same method as documented on fsapi.File
   158  func (d *openRootDir) Stat() (fsapi.Stat_t, syscall.Errno) {
   159  	return d.f.Stat()
   160  }
   161  
   162  // Seek implements the same method as documented on fsapi.File
   163  func (d *openRootDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
   164  	if offset != 0 || whence != io.SeekStart {
   165  		errno = syscall.ENOSYS
   166  		return
   167  	}
   168  	d.dirents = nil
   169  	d.direntsI = 0
   170  	return d.f.Seek(offset, whence)
   171  }
   172  
   173  // Readdir implements the same method as documented on fsapi.File
   174  func (d *openRootDir) Readdir(count int) (dirents []fsapi.Dirent, errno syscall.Errno) {
   175  	if d.dirents == nil {
   176  		if errno = d.readdir(); errno != 0 {
   177  			return
   178  		}
   179  	}
   180  
   181  	// logic similar to go:embed
   182  	n := len(d.dirents) - d.direntsI
   183  	if n == 0 {
   184  		return
   185  	}
   186  	if count > 0 && n > count {
   187  		n = count
   188  	}
   189  	dirents = make([]fsapi.Dirent, n)
   190  	for i := range dirents {
   191  		dirents[i] = d.dirents[d.direntsI+i]
   192  	}
   193  	d.direntsI += n
   194  	return
   195  }
   196  
   197  func (d *openRootDir) readdir() (errno syscall.Errno) {
   198  	// readDir reads the directory fully into d.dirents, replacing any entries that
   199  	// correspond to prefix matches or appending them to the end.
   200  	if d.dirents, errno = d.f.Readdir(-1); errno != 0 {
   201  		return
   202  	}
   203  
   204  	remaining := make(map[string]int, len(d.c.rootGuestPaths))
   205  	for k, v := range d.c.rootGuestPaths {
   206  		remaining[k] = v
   207  	}
   208  
   209  	for i := range d.dirents {
   210  		e := d.dirents[i]
   211  		if fsI, ok := remaining[e.Name]; ok {
   212  			if d.dirents[i], errno = d.rootEntry(e.Name, fsI); errno != 0 {
   213  				return
   214  			}
   215  			delete(remaining, e.Name)
   216  		}
   217  	}
   218  
   219  	var di fsapi.Dirent
   220  	for n, fsI := range remaining {
   221  		if di, errno = d.rootEntry(n, fsI); errno != 0 {
   222  			return
   223  		}
   224  		d.dirents = append(d.dirents, di)
   225  	}
   226  	return
   227  }
   228  
   229  // Sync implements the same method as documented on fsapi.File
   230  func (d *openRootDir) Sync() syscall.Errno {
   231  	return d.f.Sync()
   232  }
   233  
   234  // Datasync implements the same method as documented on fsapi.File
   235  func (d *openRootDir) Datasync() syscall.Errno {
   236  	return d.f.Datasync()
   237  }
   238  
   239  // Chmod implements the same method as documented on fsapi.File
   240  func (d *openRootDir) Chmod(fs.FileMode) syscall.Errno {
   241  	return syscall.ENOSYS
   242  }
   243  
   244  // Chown implements the same method as documented on fsapi.File
   245  func (d *openRootDir) Chown(int, int) syscall.Errno {
   246  	return syscall.ENOSYS
   247  }
   248  
   249  // Utimens implements the same method as documented on fsapi.File
   250  func (d *openRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno {
   251  	return syscall.ENOSYS
   252  }
   253  
   254  // Close implements fs.File
   255  func (d *openRootDir) Close() syscall.Errno {
   256  	return d.f.Close()
   257  }
   258  
   259  func (d *openRootDir) rootEntry(name string, fsI int) (fsapi.Dirent, syscall.Errno) {
   260  	if st, errno := d.c.fs[fsI].Stat("."); errno != 0 {
   261  		return fsapi.Dirent{}, errno
   262  	} else {
   263  		return fsapi.Dirent{Name: name, Ino: st.Ino, Type: st.Mode.Type()}, 0
   264  	}
   265  }
   266  
   267  // Lstat implements the same method as documented on api.FS
   268  func (c *CompositeFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) {
   269  	matchIndex, relativePath := c.chooseFS(path)
   270  	return c.fs[matchIndex].Lstat(relativePath)
   271  }
   272  
   273  // Stat implements the same method as documented on api.FS
   274  func (c *CompositeFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) {
   275  	matchIndex, relativePath := c.chooseFS(path)
   276  	return c.fs[matchIndex].Stat(relativePath)
   277  }
   278  
   279  // Mkdir implements the same method as documented on api.FS
   280  func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) syscall.Errno {
   281  	matchIndex, relativePath := c.chooseFS(path)
   282  	return c.fs[matchIndex].Mkdir(relativePath, perm)
   283  }
   284  
   285  // Chmod implements the same method as documented on api.FS
   286  func (c *CompositeFS) Chmod(path string, perm fs.FileMode) syscall.Errno {
   287  	matchIndex, relativePath := c.chooseFS(path)
   288  	return c.fs[matchIndex].Chmod(relativePath, perm)
   289  }
   290  
   291  // Chown implements the same method as documented on api.FS
   292  func (c *CompositeFS) Chown(path string, uid, gid int) syscall.Errno {
   293  	matchIndex, relativePath := c.chooseFS(path)
   294  	return c.fs[matchIndex].Chown(relativePath, uid, gid)
   295  }
   296  
   297  // Lchown implements the same method as documented on api.FS
   298  func (c *CompositeFS) Lchown(path string, uid, gid int) syscall.Errno {
   299  	matchIndex, relativePath := c.chooseFS(path)
   300  	return c.fs[matchIndex].Lchown(relativePath, uid, gid)
   301  }
   302  
   303  // Rename implements the same method as documented on api.FS
   304  func (c *CompositeFS) Rename(from, to string) syscall.Errno {
   305  	fromFS, fromPath := c.chooseFS(from)
   306  	toFS, toPath := c.chooseFS(to)
   307  	if fromFS != toFS {
   308  		return syscall.ENOSYS // not yet anyway
   309  	}
   310  	return c.fs[fromFS].Rename(fromPath, toPath)
   311  }
   312  
   313  // Readlink implements the same method as documented on api.FS
   314  func (c *CompositeFS) Readlink(path string) (string, syscall.Errno) {
   315  	matchIndex, relativePath := c.chooseFS(path)
   316  	return c.fs[matchIndex].Readlink(relativePath)
   317  }
   318  
   319  // Link implements the same method as documented on api.FS
   320  func (c *CompositeFS) Link(oldName, newName string) syscall.Errno {
   321  	fromFS, oldNamePath := c.chooseFS(oldName)
   322  	toFS, newNamePath := c.chooseFS(newName)
   323  	if fromFS != toFS {
   324  		return syscall.ENOSYS // not yet anyway
   325  	}
   326  	return c.fs[fromFS].Link(oldNamePath, newNamePath)
   327  }
   328  
   329  // Utimens implements the same method as documented on api.FS
   330  func (c *CompositeFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno {
   331  	matchIndex, relativePath := c.chooseFS(path)
   332  	return c.fs[matchIndex].Utimens(relativePath, times, symlinkFollow)
   333  }
   334  
   335  // Symlink implements the same method as documented on api.FS
   336  func (c *CompositeFS) Symlink(oldName, link string) (err syscall.Errno) {
   337  	fromFS, oldNamePath := c.chooseFS(oldName)
   338  	toFS, linkPath := c.chooseFS(link)
   339  	if fromFS != toFS {
   340  		return syscall.ENOSYS // not yet anyway
   341  	}
   342  	return c.fs[fromFS].Symlink(oldNamePath, linkPath)
   343  }
   344  
   345  // Truncate implements the same method as documented on api.FS
   346  func (c *CompositeFS) Truncate(path string, size int64) syscall.Errno {
   347  	matchIndex, relativePath := c.chooseFS(path)
   348  	return c.fs[matchIndex].Truncate(relativePath, size)
   349  }
   350  
   351  // Rmdir implements the same method as documented on api.FS
   352  func (c *CompositeFS) Rmdir(path string) syscall.Errno {
   353  	matchIndex, relativePath := c.chooseFS(path)
   354  	return c.fs[matchIndex].Rmdir(relativePath)
   355  }
   356  
   357  // Unlink implements the same method as documented on api.FS
   358  func (c *CompositeFS) Unlink(path string) syscall.Errno {
   359  	matchIndex, relativePath := c.chooseFS(path)
   360  	return c.fs[matchIndex].Unlink(relativePath)
   361  }
   362  
   363  // chooseFS chooses the best fs and the relative path to use for the input.
   364  func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string) {
   365  	matchIndex = -1
   366  	matchPrefixLen := 0
   367  	pathI, pathLen := stripPrefixesAndTrailingSlash(path)
   368  
   369  	// Last is the highest precedence, so we iterate backwards. The last longest
   370  	// match wins. e.g. the pre-open "tmp" wins vs "" regardless of order.
   371  	for i := len(c.fs) - 1; i >= 0; i-- {
   372  		prefix := c.cleanedGuestPaths[i]
   373  		if eq, match := hasPathPrefix(path, pathI, pathLen, prefix); eq {
   374  			// When the input equals the prefix, there cannot be a longer match
   375  			// later. The relative path is the fsapi.FS root, so return empty
   376  			// string.
   377  			matchIndex = i
   378  			relativePath = ""
   379  			return
   380  		} else if match {
   381  			// Check to see if this is a longer match
   382  			prefixLen := len(prefix)
   383  			if prefixLen > matchPrefixLen || matchIndex == -1 {
   384  				matchIndex = i
   385  				matchPrefixLen = prefixLen
   386  			}
   387  		} // Otherwise, keep looking for a match
   388  	}
   389  
   390  	// Now, we know the path != prefix, but it matched an existing fs, because
   391  	// setup ensures there's always a root filesystem.
   392  
   393  	// If this was a root path match the cleaned path is the relative one to
   394  	// pass to the underlying filesystem.
   395  	if matchPrefixLen == 0 {
   396  		// Avoid re-slicing when the input is already clean
   397  		if pathI == 0 && len(path) == pathLen {
   398  			relativePath = path
   399  		} else {
   400  			relativePath = path[pathI:pathLen]
   401  		}
   402  		return
   403  	}
   404  
   405  	// Otherwise, it is non-root match: the relative path is past "$prefix/"
   406  	pathI += matchPrefixLen + 1 // e.g. prefix=foo, path=foo/bar -> bar
   407  	relativePath = path[pathI:pathLen]
   408  	return
   409  }
   410  
   411  // hasPathPrefix compares an input path against a prefix, both cleaned by
   412  // stripPrefixesAndTrailingSlash. This returns a pair of eq, match to allow an
   413  // early short circuit on match.
   414  //
   415  // Note: This is case-sensitive because POSIX paths are compared case
   416  // sensitively.
   417  func hasPathPrefix(path string, pathI, pathLen int, prefix string) (eq, match bool) {
   418  	matchLen := pathLen - pathI
   419  	if prefix == "" {
   420  		return matchLen == 0, true // e.g. prefix=, path=foo
   421  	}
   422  
   423  	prefixLen := len(prefix)
   424  	// reset pathLen temporarily to represent the length to match as opposed to
   425  	// the length of the string (that may contain leading slashes).
   426  	if matchLen == prefixLen {
   427  		if pathContainsPrefix(path, pathI, prefixLen, prefix) {
   428  			return true, true // e.g. prefix=bar, path=bar
   429  		}
   430  		return false, false
   431  	} else if matchLen < prefixLen {
   432  		return false, false // e.g. prefix=fooo, path=foo
   433  	}
   434  
   435  	if path[pathI+prefixLen] != '/' {
   436  		return false, false // e.g. prefix=foo, path=fooo
   437  	}
   438  
   439  	// Not equal, but maybe a match. e.g. prefix=foo, path=foo/bar
   440  	return false, pathContainsPrefix(path, pathI, prefixLen, prefix)
   441  }
   442  
   443  // pathContainsPrefix is faster than strings.HasPrefix even if we didn't cache
   444  // the index,len. See benchmarks.
   445  func pathContainsPrefix(path string, pathI, prefixLen int, prefix string) bool {
   446  	for i := 0; i < prefixLen; i++ {
   447  		if path[pathI] != prefix[i] {
   448  			return false // e.g. prefix=bar, path=foo or foo/bar
   449  		}
   450  		pathI++
   451  	}
   452  	return true // e.g. prefix=foo, path=foo or foo/bar
   453  }
   454  
   455  func StripPrefixesAndTrailingSlash(path string) string {
   456  	pathI, pathLen := stripPrefixesAndTrailingSlash(path)
   457  	return path[pathI:pathLen]
   458  }
   459  
   460  // stripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
   461  // result index begins with another string. A result of "." coerces to the
   462  // empty string "" because the current directory is handled by the guest.
   463  //
   464  // Results are the offset/len pair which is an optimization to avoid re-slicing
   465  // overhead, as this function is called for every path operation.
   466  //
   467  // Note: Relative paths should be handled by the guest, as that's what knows
   468  // what the current directory is. However, paths that escape the current
   469  // directory e.g. "../.." have been found in `tinygo test` and this
   470  // implementation takes care to avoid it.
   471  func stripPrefixesAndTrailingSlash(path string) (pathI, pathLen int) {
   472  	// strip trailing slashes
   473  	pathLen = len(path)
   474  	for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- {
   475  	}
   476  
   477  	pathI = 0
   478  loop:
   479  	for pathI < pathLen {
   480  		switch path[pathI] {
   481  		case '/':
   482  			pathI++
   483  		case '.':
   484  			nextI := pathI + 1
   485  			if nextI < pathLen && path[nextI] == '/' {
   486  				pathI = nextI + 1
   487  			} else if nextI == pathLen {
   488  				pathI = nextI
   489  			} else {
   490  				break loop
   491  			}
   492  		default:
   493  			break loop
   494  		}
   495  	}
   496  	return
   497  }
   498  
   499  type fakeRootFS struct {
   500  	fsapi.UnimplementedFS
   501  }
   502  
   503  // OpenFile implements the same method as documented on api.FS
   504  func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) {
   505  	switch path {
   506  	case ".", "/", "":
   507  		return fakeRootDir{}, 0
   508  	}
   509  	return nil, syscall.ENOENT
   510  }
   511  
   512  type fakeRootDir struct {
   513  	fsapi.DirFile
   514  }
   515  
   516  // Ino implements the same method as documented on fsapi.File
   517  func (fakeRootDir) Ino() (uint64, syscall.Errno) {
   518  	return 0, 0
   519  }
   520  
   521  // Stat implements the same method as documented on fsapi.File
   522  func (fakeRootDir) Stat() (fsapi.Stat_t, syscall.Errno) {
   523  	return fsapi.Stat_t{Mode: fs.ModeDir, Nlink: 1}, 0
   524  }
   525  
   526  // Readdir implements the same method as documented on fsapi.File
   527  func (fakeRootDir) Readdir(int) (dirents []fsapi.Dirent, errno syscall.Errno) {
   528  	return // empty
   529  }
   530  
   531  // Sync implements the same method as documented on fsapi.File
   532  func (fakeRootDir) Sync() syscall.Errno {
   533  	return 0
   534  }
   535  
   536  // Datasync implements the same method as documented on fsapi.File
   537  func (fakeRootDir) Datasync() syscall.Errno {
   538  	return 0
   539  }
   540  
   541  // Chmod implements the same method as documented on fsapi.File
   542  func (fakeRootDir) Chmod(fs.FileMode) syscall.Errno {
   543  	return syscall.ENOSYS
   544  }
   545  
   546  // Chown implements the same method as documented on fsapi.File
   547  func (fakeRootDir) Chown(int, int) syscall.Errno {
   548  	return syscall.ENOSYS
   549  }
   550  
   551  // Utimens implements the same method as documented on fsapi.File
   552  func (fakeRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno {
   553  	return syscall.ENOSYS
   554  }
   555  
   556  // Close implements the same method as documented on fsapi.File
   557  func (fakeRootDir) Close() syscall.Errno {
   558  	return 0
   559  }