Skip to content

Commit

Permalink
refactor(fsx): introduce RealPathMapper, OverlayFS (#21)
Browse files Browse the repository at this point in the history
This diff modifies how fsx works by introducing two new concepts:

1. the `OverlayFS`, which encapsulates the common logic used by
`ChdirFS` and `ContainedFS` into a single codebase;

2. the `RealPathMapper`, which implements the path mapping policies used
by `ChdirFS` and `ContainedFS`.

As a result, we can re-implement (and deprecate) `ChdirFS` and
`ContainedFS` in light of these two new concepts. More in detail:

1. the `ChdirFS` path mapping policy is implemented by the new
`ChdirPathMapper` type;

2. the `ContainedFS` by the new `ContainedDirPathMapper` type.

This change is important because it allows us to clarify the semantics
of the base path used by a `RealPathMapper` in terms of whether such a
base directory is relative or absolute.

For `ChdirPathMapper` and `ContainedDirPathMapper`, we define two
constructors: one of them ensures the base directory is absolute, while
the other one does not bother with that.

In turn, this is important because of Unix domain sockets, where there
are limitations on the maximum socket name (i.e., file path) length.
Thus, when using absolute paths in `rbmk sh`, it is easier to hit the
limit and not being able to create the sockets.

This seems to suggest that we could switch to use relative paths *iff*
we guarantee that `rbmk COMMAND` would not `chdir` for any `COMMAND` in
`rbmk` except the `sh` command itself. However, baking this assumption
in `fsx` would have been quite optimistic and backward, and made `fsx`
itself harder to reuse.

On the contrary, by making the choice explicit in the constructors, we
clearly document (with code that one needs to invoke) what are the
expectations in terms of changing paths and, all in all, it seems
explicit is generally better than implict.
  • Loading branch information
bassosimone authored Dec 22, 2024
1 parent dc5ccc6 commit c799866
Show file tree
Hide file tree
Showing 7 changed files with 1,075 additions and 291 deletions.
111 changes: 6 additions & 105 deletions fsx/chdirfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,118 +6,19 @@

package fsx

import (
"io/fs"
"net"
"path/filepath"
"time"
)

// NewChdirFS creates a new [FS] where each file name is
// prefixed with the given directory path.
//
// Deprecated: use [NewOverlayFS] with [NewRelativeChdirPathMapper] instead.
func NewChdirFS(dep FS, path string) *ChdirFS {
return &ChdirFS{basepath: path, dep: dep}
return &ChdirFS{NewOverlayFS(dep, NewRelativeChdirPathMapper(path))}
}

// ChdirFS is the [FS] type returned by [NewChdirFS].
//
// The zero value IS NOT ready to use; construct using [NewChdirFS].
type ChdirFS struct {
// basepath is the base path.
basepath string

// dep is the dependency [FS].
dep FS
}

// Ensure [basePathFS] implements [FS].
var _ FS = &ChdirFS{}

// realPath returns the real path of a given file name or an error.
func (rfs *ChdirFS) realPath(name string) string {
return filepath.Join(rfs.basepath, name)
}

// Chmod implements [FS].
func (rfs *ChdirFS) Chmod(name string, mode fs.FileMode) error {
return rfs.dep.Chmod(rfs.realPath(name), mode)
}

// Chown implements [FS].
func (rfs *ChdirFS) Chown(name string, uid, gid int) error {
return rfs.dep.Chown(rfs.realPath(name), uid, gid)
}

// Chtimes implements [FS].
func (rfs *ChdirFS) Chtimes(name string, atime, mtime time.Time) error {
return rfs.dep.Chtimes(rfs.realPath(name), atime, mtime)
}

// Create implements [FS].
func (rfs *ChdirFS) Create(name string) (File, error) {
return rfs.dep.Create(rfs.realPath(name))
}

// DialUnix implements [FS].
//
// See also the limitations documented in the top-level package docs.
func (rfs *ChdirFS) DialUnix(name string) (net.Conn, error) {
return rfs.dep.DialUnix(rfs.realPath(name))
}

// ListenUnix implements [FS].
//
// See also the limitations documented in the top-level package docs.
func (rfs *ChdirFS) ListenUnix(name string) (net.Listener, error) {
return rfs.dep.ListenUnix(rfs.realPath(name))
}

// Lstat implements [FS].
func (rfs *ChdirFS) Lstat(name string) (fs.FileInfo, error) {
return rfs.dep.Lstat(rfs.realPath(name))
}

// Mkdir implements [FS].
func (rfs *ChdirFS) Mkdir(name string, mode fs.FileMode) error {
return rfs.dep.Mkdir(rfs.realPath(name), mode)
}

// MkdirAll implements [FS].
func (rfs *ChdirFS) MkdirAll(name string, mode fs.FileMode) error {
return rfs.dep.MkdirAll(rfs.realPath(name), mode)
}

// Open implements [FS].
func (rfs *ChdirFS) Open(name string) (File, error) {
return rfs.dep.Open(rfs.realPath(name))
}

// OpenFile implements [FS].
func (rfs *ChdirFS) OpenFile(name string, flag int, mode fs.FileMode) (File, error) {
return rfs.dep.OpenFile(rfs.realPath(name), flag, mode)
}

// ReadDir implements [FS].
func (rfs *ChdirFS) ReadDir(name string) ([]fs.DirEntry, error) {
return rfs.dep.ReadDir(rfs.realPath(name))
}

// Remove implements [FS].
func (rfs *ChdirFS) Remove(name string) error {
return rfs.dep.Remove(rfs.realPath(name))
}

// RemoveAll implements [FS].
func (rfs *ChdirFS) RemoveAll(name string) error {
return rfs.dep.RemoveAll(rfs.realPath(name))
}

// Rename implements [FS].
func (rfs *ChdirFS) Rename(oldname, newname string) error {
return rfs.dep.Rename(rfs.realPath(oldname), rfs.realPath(newname))
}

// Stat implements [FS].
func (rfs *ChdirFS) Stat(name string) (fs.FileInfo, error) {
return rfs.dep.Stat(rfs.realPath(name))
// Deprecated: use [NewOverlayFS] with [NewRelativeChdirPathMapper] instead.
type ChdirFS struct {
*OverlayFS
}
191 changes: 6 additions & 185 deletions fsx/containedfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@

package fsx

import (
"io/fs"
"net"
"path/filepath"
"strings"
"time"
)

// NewRelativeFS is a deprecated alias for [NewContainedFS].
//
// Deprecated: use [NewContainedFS] instead.
Expand All @@ -37,188 +29,17 @@ type RelativeFS = ContainedFS
// Note: This implementation cannot prevent symlink traversal
// attacks. The caller must ensure the base directory does not
// contain symlinks if this is a security requirement.
//
// Deprecated: use [NewOverlayFS] with [NewRelativeContainedDirPathMapper] instead.
func NewContainedFS(dep FS, path string) *ContainedFS {
return &ContainedFS{basepath: path, dep: dep}
return &ContainedFS{NewOverlayFS(dep, NewRelativeContainedDirPathMapper(path))}
}

// ContainedFS is the [FS] type returned by [NewContainedFS].
//
// The zero value IS NOT ready to use; construct using [NewContainedFS].
type ContainedFS struct {
// basepath is the base path.
basepath string

// dep is the dependency [FS].
dep FS
}

// Ensure [basePathFS] implements [FS].
var _ FS = &ContainedFS{}

// realPath returns the real path of a given file name or an error.
func (rfs *ContainedFS) realPath(name string) (string, error) {
// 1. entirely reject absolute path names
if filepath.IsAbs(name) {
return "", fs.ErrNotExist
}

// 2. clean the path and make sure it is not outside the base path
bpath := filepath.Clean(rfs.basepath)
fullpath := filepath.Clean(filepath.Join(bpath, name))
if !strings.HasPrefix(fullpath, bpath) {
return name, fs.ErrNotExist
}
return fullpath, nil
}

// Chmod implements [FS].
func (rfs *ContainedFS) Chmod(name string, mode fs.FileMode) error {
name, err := rfs.realPath(name)
if err != nil {
return &fs.PathError{Op: "chmod", Path: name, Err: err}
}
return rfs.dep.Chmod(name, mode)
}

// Chown implements [FS].
func (rfs *ContainedFS) Chown(name string, uid, gid int) error {
name, err := rfs.realPath(name)
if err != nil {
return &fs.PathError{Op: "chown", Path: name, Err: err}
}
return rfs.dep.Chown(name, uid, gid)
}

// Chtimes implements [FS].
func (rfs *ContainedFS) Chtimes(name string, atime, mtime time.Time) error {
name, err := rfs.realPath(name)
if err != nil {
return &fs.PathError{Op: "chtimes", Path: name, Err: err}
}
return rfs.dep.Chtimes(name, atime, mtime)
}

// Create implements [FS].
func (rfs *ContainedFS) Create(name string) (File, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "create", Path: name, Err: err}
}
return rfs.dep.Create(name)
}

// DialUnix implements [FS].
//
// See also the limitations documented in the top-level package docs.
func (rfs *ContainedFS) DialUnix(name string) (net.Conn, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "dialunix", Path: name, Err: err}
}
return rfs.dep.DialUnix(name)
}

// ListenUnix implements [FS].
//
// See also the limitations documented in the top-level package docs.
func (rfs *ContainedFS) ListenUnix(name string) (net.Listener, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "listenunix", Path: name, Err: err}
}
return rfs.dep.ListenUnix(name)
}

// Lstat implements [FS].
func (rfs *ContainedFS) Lstat(name string) (fs.FileInfo, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "lstat", Path: name, Err: err}
}
return rfs.dep.Lstat(name)
}

// Mkdir implements [FS].
func (rfs *ContainedFS) Mkdir(name string, mode fs.FileMode) error {
name, err := rfs.realPath(name)
if err != nil {
return &fs.PathError{Op: "mkdir", Path: name, Err: err}
}
return rfs.dep.Mkdir(name, mode)
}

// MkdirAll implements [FS].
func (rfs *ContainedFS) MkdirAll(name string, mode fs.FileMode) error {
name, err := rfs.realPath(name)
if err != nil {
return &fs.PathError{Op: "mkdir", Path: name, Err: err}
}
return rfs.dep.MkdirAll(name, mode)
}

// Open implements [FS].
func (rfs *ContainedFS) Open(name string) (File, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
}
return rfs.dep.Open(name)
}

// OpenFile implements [FS].
func (rfs *ContainedFS) OpenFile(name string, flag int, mode fs.FileMode) (File, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "openfile", Path: name, Err: err}
}
return rfs.dep.OpenFile(name, flag, mode)
}

// ReadDir implements [FS].
func (rfs *ContainedFS) ReadDir(name string) ([]fs.DirEntry, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
}
return rfs.dep.ReadDir(name)
}

// Remove implements [FS].
func (rfs *ContainedFS) Remove(name string) error {
name, err := rfs.realPath(name)
if err != nil {
return &fs.PathError{Op: "remove", Path: name, Err: err}
}
return rfs.dep.Remove(name)
}

// RemoveAll implements [FS].
func (rfs *ContainedFS) RemoveAll(name string) error {
name, err := rfs.realPath(name)
if err != nil {
return &fs.PathError{Op: "removeall", Path: name, Err: err}
}
return rfs.dep.RemoveAll(name)
}

// Rename implements [FS].
func (rfs *ContainedFS) Rename(oldname, newname string) error {
oldname, err := rfs.realPath(oldname)
if err != nil {
return &fs.PathError{Op: "rename", Path: oldname, Err: err}
}
newname, err = rfs.realPath(newname)
if err != nil {
return &fs.PathError{Op: "rename", Path: newname, Err: err}
}
return rfs.dep.Rename(oldname, newname)
}

// Stat implements [FS].
func (rfs *ContainedFS) Stat(name string) (fs.FileInfo, error) {
name, err := rfs.realPath(name)
if err != nil {
return nil, &fs.PathError{Op: "stat", Path: name, Err: err}
}
return rfs.dep.Stat(name)
// Deprecated: use [NewOverlayFS] with [NewRelativeContainedDirPathMapper] instead.
type ContainedFS struct {
*OverlayFS
}
2 changes: 1 addition & 1 deletion fsx/fsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const (
O_APPEND = fsmodel.O_APPEND
)

// IsNotExist combines the [os.ErrNotExist] check with
// IsNotExist combines the [os.IsNotExist] check with
// checking for the [fs.ErrNotExist] error.
func IsNotExist(err error) bool {
return errors.Is(err, fs.ErrNotExist) || os.IsNotExist(err)
Expand Down
Loading

0 comments on commit c799866

Please sign in to comment.