diff --git a/go.mod b/go.mod index 7aada81..4217770 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,8 @@ module github.com/go-git/go-billy/v6 -// go-git supports the last 3 stable Go versions. -go 1.24.0 +go 1.25.0 require ( - github.com/cyphar/filepath-securejoin v0.5.0 github.com/stretchr/testify v1.11.1 golang.org/x/sys v0.37.0 ) diff --git a/go.sum b/go.sum index 508dd26..316bb52 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw= -github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/osfs/os.go b/osfs/os.go index a5c9e19..2c4ba69 100644 --- a/osfs/os.go +++ b/osfs/os.go @@ -22,63 +22,19 @@ const ( var Default = &ChrootOS{} // New returns a new OS filesystem. -// By default paths are deduplicated, but still enforced -// under baseDir. For more info refer to WithDeduplicatePath. func New(baseDir string, opts ...Option) billy.Filesystem { - o := &options{ - deduplicatePath: true, - } + o := &options{} for _, opt := range opts { opt(o) } if o.Type == BoundOSFS { - return newBoundOS(baseDir, o.deduplicatePath) + return newBoundOS(baseDir) } return newChrootOS(baseDir) } -// WithBoundOS returns the option of using a Bound filesystem OS. -func WithBoundOS() Option { - return func(o *options) { - o.Type = BoundOSFS - } -} - -// WithChrootOS returns the option of using a Chroot filesystem OS. -func WithChrootOS() Option { - return func(o *options) { - o.Type = ChrootOSFS - } -} - -// WithDeduplicatePath toggles the deduplication of the base dir in the path. -// This occurs when absolute links are being used. -// Assuming base dir /base/dir and an absolute symlink /base/dir/target: -// -// With DeduplicatePath (default): /base/dir/target -// Without DeduplicatePath: /base/dir/base/dir/target -// -// This option is only used by the BoundOS OS type. -func WithDeduplicatePath(enabled bool) Option { - return func(o *options) { - o.deduplicatePath = enabled - } -} - -type options struct { - Type - deduplicatePath bool -} - -type Type int - -const ( - ChrootOSFS Type = iota - BoundOSFS -) - func tempFile(dir, prefix string) (billy.File, error) { f, err := os.CreateTemp(dir, prefix) if err != nil { diff --git a/osfs/os_bound.go b/osfs/os_bound.go index 1ddf42c..cc68eca 100644 --- a/osfs/os_bound.go +++ b/osfs/os_bound.go @@ -20,18 +20,26 @@ package osfs import ( + "errors" "fmt" "io/fs" + gofs "io/fs" "os" "path/filepath" - "strings" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-git/go-billy/v6" + "github.com/go-git/go-billy/v6/util" ) var ( dotPrefixes = []string{"./", ".\\"} + + // ErrPathEscapesParent represents when an action leads to scaping from the + // given dir the filesystem is bound to. + // + // The upstream version of this Error is not public: + // https://github.com/golang/go/blob/45d6bc76af641853a0bea31c77912bf9fd52ed79/src/os/file.go#L421 + ErrPathEscapesParent = errors.New("path escapes from parent") ) // BoundOS is a fs implementation based on the OS filesystem which is bound to @@ -46,193 +54,301 @@ var ( // 3. Readlink and Lstat ensures that the link file is located within the base // dir, evaluating any symlinks that file or base dir may contain. type BoundOS struct { - baseDir string - deduplicatePath bool + baseDir string + root *os.Root + rootError error } -func newBoundOS(d string, deduplicatePath bool) billy.Filesystem { - return &BoundOS{baseDir: d, deduplicatePath: deduplicatePath} +func newBoundOS(d string) billy.Filesystem { + r, err := os.OpenRoot(d) + return &BoundOS{baseDir: d, root: r, rootError: err} } func (fs *BoundOS) Capabilities() billy.Capability { return billy.DefaultCapabilities & billy.SyncCapability } -func (fs *BoundOS) Create(filename string) (billy.File, error) { - return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode) +func (fs *BoundOS) Create(name string) (billy.File, error) { + return fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode) } -func (fs *BoundOS) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) { - filename = fs.expandDot(filename) - fn, err := fs.abs(filename) +func (fs *BoundOS) OpenFile(name string, flag int, perm gofs.FileMode) (billy.File, error) { + root, err := fs.fsRoot() if err != nil { return nil, err } - return openFile(fn, flag, perm, fs.createDir) -} + // When not creating, read symlink links so that they can be made + // relative and therefore work. + if flag&os.O_CREATE == 0 { + fi, err := fs.root.Lstat(name) + if err == nil && fi.Mode()&gofs.ModeSymlink != 0 { + fn, err := root.Readlink(name) + if err != nil { + return nil, err + } + name = fn + } + } -func (fs *BoundOS) ReadDir(path string) ([]fs.DirEntry, error) { - path = fs.expandDot(path) - dir, err := fs.abs(path) + if filepath.IsAbs(name) { + fn, err := filepath.Rel(fs.baseDir, name) + if err != nil { + return nil, err + } + name = fn + } + + if flag&os.O_CREATE != 0 { + if err = fs.createDir(root, name); err != nil { + return nil, translateError(err, name) + } + } + + f, err := root.OpenFile(name, flag, perm) if err != nil { return nil, err } - - return os.ReadDir(dir) + return &file{File: f}, nil } -func (fs *BoundOS) Rename(from, to string) error { - if from == "." || from == fs.baseDir { - return billy.ErrBaseDirCannotBeRenamed +func (fs *BoundOS) ReadDir(name string) ([]fs.DirEntry, error) { + if filepath.IsAbs(name) { + fn, err := filepath.Rel(fs.baseDir, name) + if err != nil { + return nil, err + } + name = fn } - from = fs.expandDot(from) - _, err := fs.Lstat(from) + if name == "" { + name = "." + } + + root, err := fs.fsRoot() if err != nil { - return err + return nil, err } - f, err := fs.abs(from) + f, err := root.Open(name) if err != nil { - return err + return nil, translateError(err, name) } - to = fs.expandDot(to) - t, err := fs.abs(to) + e, err := f.ReadDir(-1) if err != nil { - return err + return nil, translateError(err, name) + } + return e, nil +} + +func (fs *BoundOS) Rename(from, to string) error { + if from == "." || from == fs.baseDir { + return billy.ErrBaseDirCannotBeRenamed } - // MkdirAll for target name. - if err := fs.createDir(t); err != nil { + root, err := fs.fsRoot() + if err != nil { return err } - return os.Rename(f, t) + // Ensure directory the new link will be created exists. + err = root.MkdirAll(filepath.Dir(to), defaultDirectoryMode) + if err == nil { + err = root.Rename(from, to) + } + + return translateError(err, to) } -func (fs *BoundOS) MkdirAll(path string, perm fs.FileMode) error { - path = fs.expandDot(path) - dir, err := fs.abs(path) +func (fs *BoundOS) MkdirAll(name string, _ fs.FileMode) error { + root, err := fs.fsRoot() if err != nil { return err } - return os.MkdirAll(dir, perm) + + // os.Root errors when perm contains bits other than the nine least-significant bits (0o777). + err = root.MkdirAll(name, 0o777) + return translateError(err, name) } -func (fs *BoundOS) Open(filename string) (billy.File, error) { - return fs.OpenFile(filename, os.O_RDONLY, 0) +func (fs *BoundOS) Open(name string) (billy.File, error) { + return fs.OpenFile(name, os.O_RDONLY, 0) } -func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) { - filename = fs.expandDot(filename) - filename, err := fs.abs(filename) +func (fs *BoundOS) Stat(name string) (os.FileInfo, error) { + if filepath.IsAbs(name) { + fn, err := filepath.Rel(fs.baseDir, name) + if err != nil { + return nil, err + } + name = fn + } + + if name == "" { + name = "." + } + + root, err := fs.fsRoot() if err != nil { return nil, err } - return os.Stat(filename) + + fi, err := root.Stat(name) + if err != nil { + return nil, translateError(err, name) + } + + return fi, nil } -func (fs *BoundOS) Remove(filename string) error { - if filename == "." || filename == fs.baseDir { +func (fs *BoundOS) Remove(name string) error { + if name == "." || name == fs.baseDir { return billy.ErrBaseDirCannotBeRemoved } - fn, err := fs.abs(filename) + if filepath.IsAbs(name) { + fn, err := filepath.Rel(fs.baseDir, name) + if err != nil { + return err + } + name = fn + } + + root, err := fs.fsRoot() if err != nil { return err } - return os.Remove(fn) + + err = root.Remove(name) + if err == nil { + return nil + } + + return translateError(err, name) } // TempFile creates a temporary file. If dir is empty, the file -// will be created within the OS Temporary dir. If dir is provided -// it must descend from the current base dir. +// will be created within a .tmp dir. +// +// If dir is outside the bound dir, [os.ErrPermission] is returned. func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) { - if dir != "" { - var err error - dir, err = fs.abs(dir) + if filepath.IsAbs(dir) { + path, err := filepath.Rel(fs.baseDir, dir) if err != nil { return nil, err } + dir = path } - return tempFile(dir, prefix) + return util.TempFile(fs, dir, prefix) } func (fs *BoundOS) Join(elem ...string) string { return filepath.Join(elem...) } -func (fs *BoundOS) RemoveAll(path string) error { - if path == "." || path == fs.baseDir { +func (fs *BoundOS) RemoveAll(name string) error { + if name == "." || name == fs.baseDir { return billy.ErrBaseDirCannotBeRemoved } - path = fs.expandDot(path) - dir, err := fs.abs(path) + if filepath.IsAbs(name) { + fn, err := filepath.Rel(fs.baseDir, name) + if err != nil { + return err + } + name = fn + } + + root, err := fs.fsRoot() if err != nil { return err } - return os.RemoveAll(dir) + + return root.RemoveAll(name) } -func (fs *BoundOS) Symlink(target, link string) error { - link = fs.expandDot(link) - ln, err := fs.abs(link) +func (fs *BoundOS) Symlink(oldname, newname string) error { + if filepath.IsAbs(newname) { + fn, err := filepath.Rel(fs.baseDir, newname) + if err != nil { + return err + } + newname = fn + } + + root, err := fs.fsRoot() if err != nil { return err } - // MkdirAll for containing dir. - if err := fs.createDir(ln); err != nil { + + err = fs.createDir(root, newname) + if err != nil { return err } - return os.Symlink(target, ln) + + return root.Symlink(oldname, newname) } -func (fs *BoundOS) expandDot(p string) string { - if p == "." { - return fs.baseDir - } - for _, prefix := range dotPrefixes { - if strings.HasPrefix(p, prefix) { - return filepath.Join(fs.baseDir, strings.TrimPrefix(p, prefix)) +func (fs *BoundOS) Lstat(name string) (os.FileInfo, error) { + if filepath.IsAbs(name) { + fn, err := filepath.Rel(fs.baseDir, name) + if err != nil { + return nil, err } + name = fn } - return p -} -func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) { - filename = fs.expandDot(filename) - filename = filepath.Clean(filename) - if !filepath.IsAbs(filename) { - filename = filepath.Join(fs.baseDir, filename) - } - if ok, err := fs.insideBaseDirEval(filename); !ok { + root, err := fs.fsRoot() + if err != nil { return nil, err } - return os.Lstat(filename) -} -func (fs *BoundOS) Readlink(link string) (string, error) { - link = fs.expandDot(link) - if !filepath.IsAbs(link) { - link = filepath.Clean(filepath.Join(fs.baseDir, link)) + fi, err := root.Lstat(name) + if err != nil { + return nil, translateError(err, name) } - if ok, err := fs.insideBaseDirEval(link); !ok { + + return fi, nil +} + +func (fs *BoundOS) Readlink(name string) (string, error) { + root, err := fs.fsRoot() + if err != nil { return "", err } - return os.Readlink(link) + + lnk, err := root.Readlink(name) + if err != nil { + return "", translateError(err, name) + } + + return lnk, nil } // Chroot returns a new BoundOS filesystem, with the base dir set to the // result of joining the provided path with the underlying base dir. func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) { - joined, err := securejoin.SecureJoin(fs.baseDir, path) - if err != nil { + fi, err := fs.root.Lstat(path) + if errors.Is(err, os.ErrNotExist) { + err := fs.root.MkdirAll(path, defaultDirectoryMode) + if err != nil { + return nil, fmt.Errorf("failed to auto create dir: %w", err) + } + } else if err != nil { return nil, err } + if fi != nil && !fi.IsDir() { + return nil, fmt.Errorf("cannot chroot: path is not dir") + } + + root, err := fs.root.OpenRoot(path) + if err != nil { + return nil, fmt.Errorf("unable to chroot: %w", err) + } + + joined := filepath.Join(fs.baseDir, root.Name()) return New(joined, WithBoundOS()), nil } @@ -243,10 +359,10 @@ func (fs *BoundOS) Root() string { return fs.baseDir } -func (fs *BoundOS) createDir(fullpath string) error { +func (fs *BoundOS) createDir(root *os.Root, fullpath string) error { dir := filepath.Dir(fullpath) if dir != "." { - if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil { + if err := root.MkdirAll(dir, defaultDirectoryMode); err != nil { return err } } @@ -254,49 +370,18 @@ func (fs *BoundOS) createDir(fullpath string) error { return nil } -// abs transforms filename to an absolute path, taking into account the base dir. -// Relative paths won't be allowed to ascend the base dir, so `../file` will become -// `/working-dir/file`. -// -// Note that if filename is a symlink, the returned address will be the target of the -// symlink. -func (fs *BoundOS) abs(filename string) (string, error) { - if filename == fs.baseDir { - filename = string(filepath.Separator) - } +func (fs *BoundOS) fsRoot() (*os.Root, error) { + return fs.root, fs.rootError +} - path, err := securejoin.SecureJoin(fs.baseDir, filename) - if err != nil { - return "", err +func translateError(err error, file string) error { + if err == nil { + return nil } - if fs.deduplicatePath { - vol := filepath.VolumeName(fs.baseDir) - dup := filepath.Join(fs.baseDir, fs.baseDir[len(vol):]) - if strings.HasPrefix(path, dup+string(filepath.Separator)) { - return fs.abs(path[len(dup):]) - } + if errors.Unwrap(err).Error() == ErrPathEscapesParent.Error() { + return fmt.Errorf("%w: %q", ErrPathEscapesParent, file) } - return path, nil -} -// insideBaseDirEval checks whether filename is contained within -// a dir that is within the fs.baseDir, by first evaluating any symlinks -// that either filename or fs.baseDir may contain. -func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) { - if fs.baseDir == "/" || fs.baseDir == "" || fs.baseDir == filename { - return true, nil - } - dir, err := filepath.EvalSymlinks(filepath.Dir(filename)) - if dir == "" || os.IsNotExist(err) { - dir = filepath.Dir(filename) - } - wd, err := filepath.EvalSymlinks(fs.baseDir) - if wd == "" || os.IsNotExist(err) { - wd = fs.baseDir - } - if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) { - return false, fmt.Errorf("%q: path outside base dir %q: %w", filename, fs.baseDir, os.ErrNotExist) - } - return true, nil + return err } diff --git a/osfs/os_bound_test.go b/osfs/os_bound_test.go index 259232b..b3c338a 100644 --- a/osfs/os_bound_test.go +++ b/osfs/os_bound_test.go @@ -20,10 +20,8 @@ package osfs import ( - "fmt" "os" "path/filepath" - "runtime" "strings" "testing" @@ -34,11 +32,11 @@ import ( func TestBoundOSCapabilities(t *testing.T) { dir := t.TempDir() - fs := newBoundOS(dir, true) - _, ok := fs.(billy.Capable) + fs := newBoundOS(dir) + c, ok := fs.(billy.Capable) assert.True(t, ok) - caps := billy.Capabilities(fs) + caps := c.Capabilities() assert.Equal(t, billy.DefaultCapabilities&billy.SyncCapability, caps) } @@ -55,7 +53,7 @@ func TestOpen(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "test-file", }, @@ -64,7 +62,7 @@ func TestOpen(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "./test-file", }, @@ -73,9 +71,10 @@ func TestOpen(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "rel-above-cwd"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "../../rel-above-cwd", + wantErr: ErrPathEscapesParent.Error(), }, { name: "file: rel path to below cwd", @@ -84,7 +83,7 @@ func TestOpen(t *testing.T) { require.NoError(t, err) err = os.WriteFile(filepath.Join(dir, "sub/rel-below-cwd"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "sub/rel-below-cwd", }, @@ -93,7 +92,7 @@ func TestOpen(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "abs-test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "abs-test-file", makeAbs: true, @@ -101,51 +100,77 @@ func TestOpen(t *testing.T) { { name: "file: abs outside cwd", before: func(dir string) billy.Filesystem { - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "/some/path/outside/cwd", - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent.Error(), }, { - name: "symlink: same dir", + name: "symlink: same dir abs", before: func(dir string) billy.Filesystem { target := filepath.Join(dir, "target-file") err := os.WriteFile(target, []byte("anything"), 0o600) require.NoError(t, err) err = os.Symlink(target, filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", }, + { + name: "symlink: same dir rel", + before: func(dir string) billy.Filesystem { + target := filepath.Join(dir, "target-file") + err := os.WriteFile(target, []byte("anything"), 0o600) + require.NoError(t, err) + err = os.Symlink("target-file", filepath.Join(dir, "symlink")) + require.NoError(t, err) + return newBoundOS(dir) + }, + filename: "symlink", + }, + { + name: "symlink: symlink to symlink", + before: func(dir string) billy.Filesystem { + target := filepath.Join(dir, "target-file") + err := os.WriteFile(target, []byte("anything"), 0o600) + require.NoError(t, err) + err = os.Symlink("target-file", filepath.Join(dir, "symlink")) + require.NoError(t, err) + err = os.Symlink("symlink", filepath.Join(dir, "symlink2")) + require.NoError(t, err) + return newBoundOS(dir) + }, + filename: "symlink2", + }, { name: "symlink: rel outside cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("../../../../../../outside/cwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: true, - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent.Error(), }, { name: "symlink: abs outside cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("/some/path/outside/cwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: true, - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent.Error(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) if tt.before != nil { fs = tt.before(dir) @@ -180,6 +205,7 @@ func Test_Symlink(t *testing.T) { name string link string target string + makeAbs bool before func(dir string) billy.Filesystem wantStatErr string }{ @@ -188,6 +214,12 @@ func Test_Symlink(t *testing.T) { link: "symlink", target: filepath.FromSlash("/etc/passwd"), }, + { + name: "abs link to abs valid target", + link: "symlink", + target: filepath.FromSlash("/etc/passwd"), + makeAbs: true, + }, { name: "dot link to abs valid target", link: "./symlink", @@ -211,7 +243,7 @@ func Test_Symlink(t *testing.T) { { name: "auto create dir", link: "new-dir/symlink", - target: filepath.FromSlash("../../../some/random/path"), + target: filepath.FromSlash("/etc/passwd"), }, { name: "keep dir filemode if exists", @@ -219,7 +251,7 @@ func Test_Symlink(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.Mkdir(filepath.Join(dir, "new-dir"), 0o701) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, target: filepath.FromSlash("../../../some/random/path"), }, @@ -228,7 +260,7 @@ func Test_Symlink(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) if tt.before != nil { fs = tt.before(dir) @@ -243,10 +275,16 @@ func Test_Symlink(t *testing.T) { diBefore, _ := os.Lstat(filepath.Dir(link)) - err = fs.Symlink(tt.target, tt.link) + lnk := tt.link + if tt.makeAbs { + lnk = link + } + + err = fs.Symlink(tt.target, lnk) require.NoError(t, err) fi, err := os.Lstat(link) + if tt.wantStatErr != "" { require.ErrorContains(t, err, tt.wantStatErr) } else { @@ -271,33 +309,40 @@ func Test_Symlink(t *testing.T) { func TestTempFile(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) + // No dir provided means bound dir + `/.tmp`. f, err := fs.TempFile("", "prefix") require.NoError(t, err) assert.NotNil(f) - assert.Contains(f.Name(), os.TempDir()) + prefix := filepath.Join(".tmp", "prefix") + assert.True(strings.HasPrefix(f.Name(), filepath.Join(dir, prefix)), f.Name(), prefix) require.NoError(t, f.Close()) f, err = fs.TempFile("/above/cwd", "prefix") - require.ErrorContains(t, err, fmt.Sprint(dir, filepath.FromSlash("/above/cwd/prefix"))) + require.ErrorIs(t, err, ErrPathEscapesParent) assert.Nil(f) - tempDir := os.TempDir() + f, err = fs.TempFile("../../../above/cwd", "prefix") + require.ErrorIs(t, err, ErrPathEscapesParent) + assert.Nil(f) + + tempDir := filepath.Join(dir, "/tmp") // For windows, volume name must be removed. if v := filepath.VolumeName(tempDir); v != "" { tempDir = strings.TrimPrefix(tempDir, v) } + // Full path aligned with bound dir, works as expected. f, err = fs.TempFile(tempDir, "prefix") - require.ErrorContains(t, err, filepath.Join(dir, tempDir, "prefix")) - assert.Nil(f) + require.NoError(t, err) + assert.NotNil(f) } func TestChroot(t *testing.T) { assert := assert.New(t) tmp := t.TempDir() - fs := newBoundOS(tmp, true) + fs := newBoundOS(tmp) f, err := fs.Chroot("test") require.NoError(t, err) @@ -309,7 +354,7 @@ func TestChroot(t *testing.T) { func TestRoot(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) root := fs.Root() assert.Equal(dir, root) @@ -323,14 +368,14 @@ func TestReadLink(t *testing.T) { expected string makeExpectedAbs bool before func(dir string) billy.Filesystem - wantErr string + wantErr error }{ { name: "symlink: pointing to abs outside cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("/etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", expected: filepath.FromSlash("/etc/passwd"), @@ -338,18 +383,19 @@ func TestReadLink(t *testing.T) { { name: "file: rel pointing to abs above cwd", filename: "../../file", - wantErr: "path outside base dir", + wantErr: ErrPathEscapesParent, }, { name: "symlink: abs symlink pointing outside cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("/etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: true, expected: filepath.FromSlash("/etc/passwd"), + wantErr: ErrPathEscapesParent, }, { name: "symlink: dir pointing outside cwd", @@ -367,11 +413,11 @@ func TestReadLink(t *testing.T) { err = os.WriteFile(filepath.Join(outside, "file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(cwd, true) + return newBoundOS(cwd) }, filename: "current-dir/symlink/file", makeAbs: true, - wantErr: "path outside base dir", + wantErr: ErrPathEscapesParent, }, { name: "symlink: within cwd + baseDir symlink", @@ -391,7 +437,7 @@ func TestReadLink(t *testing.T) { require.NoError(t, err) err = os.Symlink(filepath.Join(cwdTarget, "file"), filepath.Join(cwdAlt, "symlink-file")) require.NoError(t, err) - return newBoundOS(cwd, true) + return newBoundOS(cwd) }, filename: "symlink-file", expected: filepath.Join("cwd-target/file"), @@ -418,17 +464,17 @@ func TestReadLink(t *testing.T) { require.NoError(t, err) err = os.Symlink(filepath.Join(cwdTarget, "file"), filepath.Join(outside, "symlink-file")) require.NoError(t, err) - return newBoundOS(cwd, true) + return newBoundOS(cwd) }, filename: "symlink-outside/symlink-file", - wantErr: "path outside base dir", + wantErr: ErrPathEscapesParent, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) if tt.before != nil { fs = tt.before(dir) @@ -445,8 +491,8 @@ func TestReadLink(t *testing.T) { } got, err := fs.Readlink(filename) - if tt.wantErr != "" { - require.ErrorContains(t, err, tt.wantErr) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) assert.Empty(got) } else { require.NoError(t, err) @@ -462,14 +508,14 @@ func TestLstat(t *testing.T) { filename string makeAbs bool before func(dir string) billy.Filesystem - wantErr string + wantErr error }{ { name: "rel symlink: pointing to abs outside cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("/etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", }, @@ -478,7 +524,7 @@ func TestLstat(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.Symlink("../../../../../../../../etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", }, @@ -487,7 +533,7 @@ func TestLstat(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.Symlink("/etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: true, @@ -497,7 +543,7 @@ func TestLstat(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.Symlink("../../../../../../../../etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: false, @@ -520,7 +566,7 @@ func TestLstat(t *testing.T) { require.NoError(t, err) err = os.Symlink(filepath.Join(cwdTarget, "file"), filepath.Join(cwdAlt, "symlink-file")) require.NoError(t, err) - return newBoundOS(cwd, true) + return newBoundOS(cwd) }, filename: "symlink-file", makeAbs: false, @@ -547,28 +593,28 @@ func TestLstat(t *testing.T) { err = os.Symlink(filepath.Join(cwdTarget, "file"), filepath.Join(outside, "symlink-file")) require.NoError(t, err) - return newBoundOS(cwd, true) + return newBoundOS(cwd) }, filename: "symlink-outside/symlink-file", makeAbs: false, - wantErr: "path outside base dir", + wantErr: ErrPathEscapesParent, }, { name: "path: rel pointing to abs above cwd", filename: "../../file", - wantErr: "path outside base dir", + wantErr: ErrPathEscapesParent, }, { name: "path: abs pointing outside cwd", filename: "/etc/passwd", - wantErr: "path outside base dir", + wantErr: ErrPathEscapesParent, }, { name: "file: rel", before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "test-file", }, @@ -577,7 +623,7 @@ func TestLstat(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "./test-file", }, @@ -586,7 +632,7 @@ func TestLstat(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "test-file", makeAbs: true, @@ -596,7 +642,7 @@ func TestLstat(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) if tt.before != nil { fs = tt.before(dir) @@ -607,8 +653,8 @@ func TestLstat(t *testing.T) { filename = filepath.Join(dir, filename) } fi, err := fs.Lstat(filename) - if tt.wantErr != "" { - require.ErrorContains(t, err, tt.wantErr) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) assert.Nil(fi) } else { require.NoError(t, err) @@ -625,27 +671,27 @@ func TestStat(t *testing.T) { filename string makeAbs bool before func(dir string) billy.Filesystem - wantErr string + wantErr error }{ { name: "rel symlink: pointing to abs outside cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("/etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { name: "rel symlink: pointing to rel path above cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("../../../../../../../../etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { @@ -653,46 +699,46 @@ func TestStat(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.Symlink("/etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: true, - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { name: "abs symlink: pointing to rel outside cwd", before: func(dir string) billy.Filesystem { err := os.Symlink("../../../../../../../../etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: false, - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { name: "path: rel pointing to abs above cwd", filename: "../../file", - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { name: "path: abs pointing outside cwd", filename: "/etc/passwd", - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { name: "rel file", before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "test-file", }, { name: "rel dot dir", before: func(dir string) billy.Filesystem { - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: ".", }, @@ -701,7 +747,7 @@ func TestStat(t *testing.T) { before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "test-file", makeAbs: true, @@ -711,7 +757,7 @@ func TestStat(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) if tt.before != nil { fs = tt.before(dir) @@ -723,8 +769,8 @@ func TestStat(t *testing.T) { } fi, err := fs.Stat(filename) - if tt.wantErr != "" { - require.ErrorContains(t, err, tt.wantErr) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) assert.Nil(fi) } else { require.NoError(t, err) @@ -740,40 +786,41 @@ func TestRemove(t *testing.T) { filename string makeAbs bool before func(dir string) billy.Filesystem - wantErr string + after func(t *testing.T, dir string) + wantErr error }{ { name: "path: rel pointing outside cwd w forward slash", filename: "/some/path/outside/cwd", - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { name: "path: rel pointing outside cwd", filename: "../../../../path/outside/cwd", - wantErr: notFoundError(), + wantErr: ErrPathEscapesParent, }, { name: "inexistent dir", before: func(dir string) billy.Filesystem { - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "inexistent", - wantErr: notFoundError(), + wantErr: os.ErrNotExist, }, { name: "same dot dir", before: func(dir string) billy.Filesystem { - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: ".", - wantErr: "base dir cannot be removed", + wantErr: billy.ErrBaseDirCannotBeRemoved, }, { name: "same dir file", before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "test-file", }, @@ -785,31 +832,49 @@ func TestRemove(t *testing.T) { require.NoError(t, err) err = os.Symlink(target, filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", }, + { + name: "rel path to file below cwd", + before: func(dir string) billy.Filesystem { + p := filepath.Join(dir, "sub") + err := os.MkdirAll(p, 0o777) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(dir, "sub", "rel-below-cwd"), []byte("anything"), 0o600) + require.NoError(t, err) + return newBoundOS(dir) + }, + filename: "./sub/rel-below-cwd", + }, { name: "rel path to file above cwd", before: func(dir string) billy.Filesystem { - err := os.WriteFile(filepath.Join(dir, "rel-above-cwd"), []byte("anything"), 0o600) + p := filepath.Join(dir, "sub") + err := os.MkdirAll(p, 0o777) require.NoError(t, err) - return newBoundOS(dir, true) + + err = os.WriteFile(filepath.Join(dir, "rel-above-cwd"), []byte("anything"), 0o600) + require.NoError(t, err) + return newBoundOS(p) }, - filename: "../../rel-above-cwd", + filename: "../rel-above-cwd", + wantErr: ErrPathEscapesParent, }, { name: "abs file", before: func(dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "abs-test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "abs-test-file", makeAbs: true, }, { - name: "abs symlink: pointing outside is forced to descend", + name: "abs symlink: pointing outside is deleted", before: func(dir string) billy.Filesystem { cwd := filepath.Join(dir, "current-dir") outsideFile := filepath.Join(dir, "outside-cwd/file") @@ -822,16 +887,19 @@ func TestRemove(t *testing.T) { require.NoError(t, err) err = os.Symlink(outsideFile, filepath.Join(cwd, "remove-abs-symlink")) require.NoError(t, err) - return newBoundOS(cwd, true) + return newBoundOS(cwd) + }, + after: func(t *testing.T, dir string) { + _, err := os.Stat(filepath.Join(dir, "outside-cwd/file")) + require.NoError(t, err) }, filename: "remove-abs-symlink", - wantErr: notFoundError(), }, { - name: "rel symlink: pointing outside is forced to descend", + name: "rel symlink: pointing outside is deleted", before: func(dir string) billy.Filesystem { cwd := filepath.Join(dir, "current-dir") - outsideFile := filepath.Join(dir, "outside-cwd", "file2") + outsideFile := filepath.Join(dir, "outside-cwd", "file") err := os.Mkdir(cwd, 0o700) require.NoError(t, err) @@ -839,18 +907,21 @@ func TestRemove(t *testing.T) { require.NoError(t, err) err = os.WriteFile(outsideFile, []byte("anything"), 0o600) require.NoError(t, err) - err = os.Symlink(filepath.Join("..", "outside-cwd", "file2"), filepath.Join(cwd, "remove-abs-symlink2")) + err = os.Symlink(filepath.Join("..", "outside-cwd", "file"), filepath.Join(cwd, "remove-rel-symlink")) + require.NoError(t, err) + return newBoundOS(cwd) + }, + after: func(t *testing.T, dir string) { + _, err := os.Stat(filepath.Join(dir, "outside-cwd/file")) require.NoError(t, err) - return newBoundOS(cwd, true) }, filename: "remove-rel-symlink", - wantErr: notFoundError(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) if tt.before != nil { fs = tt.before(dir) @@ -862,11 +933,15 @@ func TestRemove(t *testing.T) { } err := fs.Remove(filename) - if tt.wantErr != "" { - require.ErrorContains(t, err, tt.wantErr) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) } else { require.NoError(t, err) } + + if tt.after != nil { + tt.after(t, dir) + } }) } } @@ -884,7 +959,7 @@ func TestRemoveAll(t *testing.T) { before: func(t *testing.T, dir string) billy.Filesystem { err := os.MkdirAll(filepath.Join(dir, "parent", "children"), 0o700) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "parent", }, @@ -897,7 +972,7 @@ func TestRemoveAll(t *testing.T) { before: func(t *testing.T, dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "test-file", }, @@ -909,7 +984,7 @@ func TestRemoveAll(t *testing.T) { require.NoError(t, err) err = os.Symlink(target, filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", }, @@ -918,16 +993,17 @@ func TestRemoveAll(t *testing.T) { before: func(t *testing.T, dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "rel-above-cwd"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "../../rel-above-cwd", + wantErr: ErrPathEscapesParent.Error(), }, { name: "abs file", before: func(t *testing.T, dir string) billy.Filesystem { err := os.WriteFile(filepath.Join(dir, "abs-test-file"), []byte("anything"), 0o600) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "abs-test-file", makeAbs: true, @@ -937,7 +1013,7 @@ func TestRemoveAll(t *testing.T) { before: func(t *testing.T, dir string) billy.Filesystem { err := os.Symlink("/etc/passwd", filepath.Join(dir, "symlink")) require.NoError(t, err) - return newBoundOS(dir, true) + return newBoundOS(dir) }, filename: "symlink", makeAbs: true, @@ -947,7 +1023,7 @@ func TestRemoveAll(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs, ok := newBoundOS(dir, true).(*BoundOS) + fs, ok := newBoundOS(dir).(*BoundOS) assert.True(ok) if tt.before != nil { @@ -999,7 +1075,7 @@ func TestJoin(t *testing.T) { for _, tt := range tests { t.Run(tt.wanted, func(t *testing.T) { assert := assert.New(t) - fs := newBoundOS(t.TempDir(), true) + fs := newBoundOS(t.TempDir()) got := fs.Join(tt.elems...) assert.Equal(tt.wanted, got) @@ -1007,198 +1083,10 @@ func TestJoin(t *testing.T) { } } -func TestAbs(t *testing.T) { - tests := []struct { - name string - cwd string - filename string - makeAbs bool - expected string - makeExpectedAbs bool - wantErr string - deduplicatePath bool - before func(dir string) - }{ - { - name: "path: same dir rel file", - cwd: "/working/dir", - filename: "./file", - expected: filepath.FromSlash("/working/dir/file"), - }, - { - name: "path: descending rel file", - cwd: "/working/dir", - filename: "file", - expected: filepath.FromSlash("/working/dir/file"), - }, - { - name: "path: ascending rel file 1", - cwd: "/working/dir", - filename: "../file", - expected: filepath.FromSlash("/working/dir/file"), - }, - { - name: "path: ascending rel file 2", - cwd: "/working/dir", - filename: "../../file", - expected: filepath.FromSlash("/working/dir/file"), - }, - { - name: "path: ascending rel file 3", - cwd: "/working/dir", - filename: "/../../file", - expected: filepath.FromSlash("/working/dir/file"), - }, - { - name: "path: abs file within cwd", - cwd: filepath.FromSlash("/working/dir"), - filename: filepath.FromSlash("/working/dir/abs-file"), - expected: filepath.FromSlash("/working/dir/abs-file"), - deduplicatePath: true, - }, - { - name: "path: abs file within cwd wo deduplication", - cwd: filepath.FromSlash("/working/dir"), - filename: filepath.FromSlash("/working/dir/abs-file"), - expected: filepath.FromSlash("/working/dir/working/dir/abs-file"), - }, - { - name: "path: abs file within cwd", - cwd: "/working/dir", - filename: "/outside/dir/abs-file", - expected: filepath.FromSlash("/working/dir/outside/dir/abs-file"), - }, - { - name: "abs symlink: within cwd w abs descending target", - filename: "ln-cwd-cwd", - makeAbs: true, - expected: "within-cwd", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink(filepath.Join(dir, "within-cwd"), filepath.Join(dir, "ln-cwd-cwd")) - require.NoError(t, err) - }, - deduplicatePath: true, - }, - { - name: "abs symlink: within cwd w rel descending target", - filename: "ln-rel-cwd-cwd", - makeAbs: true, - expected: "within-cwd", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink("within-cwd", filepath.Join(dir, "ln-rel-cwd-cwd")) - require.NoError(t, err) - }, - deduplicatePath: true, - }, - { - name: "abs symlink: within cwd w abs ascending target", - filename: "ln-cwd-up", - makeAbs: true, - expected: "/some/outside/dir", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink("/some/outside/dir", filepath.Join(dir, "ln-cwd-up")) - require.NoError(t, err) - }, - deduplicatePath: true, - }, - { - name: "abs symlink: within cwd w rel ascending target", - filename: "ln-rel-cwd-up", - makeAbs: true, - expected: "outside-cwd", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink("../../outside-cwd", filepath.Join(dir, "ln-rel-cwd-up")) - require.NoError(t, err) - }, - deduplicatePath: true, - }, - { - name: "rel symlink: within cwd w abs descending target", - filename: "ln-cwd-cwd", - expected: "within-cwd", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink(filepath.Join(dir, "within-cwd"), filepath.Join(dir, "ln-cwd-cwd")) - require.NoError(t, err) - }, - deduplicatePath: true, - }, - { - name: "rel symlink: within cwd w rel descending target", - filename: "ln-rel-cwd-cwd2", - expected: "within-cwd", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink("within-cwd", filepath.Join(dir, "ln-rel-cwd-cwd2")) - require.NoError(t, err) - }, - }, - { - name: "rel symlink: within cwd w abs ascending target", - filename: "ln-cwd-up2", - expected: "/outside/path/up", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink("/outside/path/up", filepath.Join(dir, "ln-cwd-up2")) - require.NoError(t, err) - }, - }, - { - name: "rel symlink: within cwd w rel ascending target", - filename: "ln-rel-cwd-up2", - expected: "outside", - makeExpectedAbs: true, - before: func(dir string) { - err := os.Symlink("../../../../outside", filepath.Join(dir, "ln-rel-cwd-up2")) - require.NoError(t, err) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert := assert.New(t) - cwd := tt.cwd - if cwd == "" { - cwd = t.TempDir() - } - - fs, ok := newBoundOS(cwd, tt.deduplicatePath).(*BoundOS) - assert.True(ok) - - if tt.before != nil { - tt.before(cwd) - } - - filename := tt.filename - if tt.makeAbs { - filename = filepath.Join(cwd, filename) - } - - expected := tt.expected - if tt.makeExpectedAbs { - expected = filepath.Join(cwd, expected) - } - - got, err := fs.abs(filename) - if tt.wantErr != "" { - require.ErrorContains(t, err, tt.wantErr) - } else { - require.NoError(t, err) - } - - assert.Equal(expected, got) - }) - } -} - func TestReadDir(t *testing.T) { assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) f, err := os.Create(filepath.Join(dir, "file1")) require.NoError(t, err) @@ -1223,35 +1111,25 @@ func TestReadDir(t *testing.T) { err = os.Symlink("/some/path/outside/cwd", filepath.Join(dir, "symlink")) require.NoError(t, err) dirs, err = fs.ReadDir("symlink") - require.ErrorContains(t, err, notFoundError()) + require.ErrorIs(t, err, ErrPathEscapesParent) assert.Nil(dirs) } -func TestInsideBaseDirEval(t *testing.T) { - assert := assert.New(t) - - fs := BoundOS{baseDir: "/"} - b, err := fs.insideBaseDirEval("a") - assert.True(b) - require.NoError(t, err) - - fs = BoundOS{baseDir: ""} - b, err = fs.insideBaseDirEval(filepath.Join("a", "b", "c")) - assert.True(b) - require.NoError(t, err) -} - func TestMkdirAll(t *testing.T) { assert := assert.New(t) root := t.TempDir() cwd := filepath.Join(root, "cwd") + + err := os.MkdirAll(cwd, 0o700) + require.NoError(t, err) + target := "abc" targetAbs := filepath.Join(cwd, target) - fs := newBoundOS(cwd, true) + fs := newBoundOS(cwd) // Even if CWD is changed outside of the fs instance, // the base dir must still be observed. - err := os.Chdir(os.TempDir()) + err = os.Chdir(os.TempDir()) require.NoError(t, err) err = fs.MkdirAll(target, 0o700) @@ -1267,7 +1145,7 @@ func TestMkdirAll(t *testing.T) { require.NoError(t, err) err = fs.MkdirAll(filepath.Join(cwd, "symlink", "new-dir"), 0o700) - require.NoError(t, err) + require.ErrorIs(t, err, ErrPathEscapesParent) // For windows, the volume name must be removed from the path or // it will lead to an invalid path. @@ -1275,13 +1153,13 @@ func TestMkdirAll(t *testing.T) { root = root[len(vol):] } - mustExist(filepath.Join(cwd, root, "outside", "new-dir")) + fi, err = os.Stat(filepath.Join(root, "outside", "new-dir")) + require.Nil(t, fi, "dir must not be created") } func TestRename(t *testing.T) { - assert := assert.New(t) dir := t.TempDir() - fs := newBoundOS(dir, true) + fs := newBoundOS(dir) oldFile := "old-file" newFile := filepath.Join("newdir", "newfile") @@ -1300,27 +1178,11 @@ func TestRename(t *testing.T) { fi, err := os.Stat(filepath.Join(dir, newFile)) require.NoError(t, err) - assert.NotNil(fi) + assert.NotNil(t, fi) err = fs.Rename(filepath.FromSlash("/tmp/outside/cwd/file1"), newFile) - require.ErrorIs(t, err, os.ErrNotExist) + assert.ErrorIs(t, err, ErrPathEscapesParent) err = fs.Rename(oldFile, filepath.FromSlash("/tmp/outside/cwd/file2")) - require.ErrorIs(t, err, os.ErrNotExist) -} - -func mustExist(filename string) { - fi, err := os.Stat(filename) - if err != nil || fi == nil { - panic(fmt.Sprintf("file %s should exist", filename)) - } -} - -func notFoundError() string { - switch runtime.GOOS { - case "windows": - return "The system cannot find the " // {path,file} specified - default: - return "no such file or directory" - } + assert.ErrorIs(t, err, ErrPathEscapesParent) } diff --git a/osfs/os_options.go b/osfs/os_options.go index 2f235c6..99b971d 100644 --- a/osfs/os_options.go +++ b/osfs/os_options.go @@ -1,3 +1,28 @@ package osfs type Option func(*options) + +// WithBoundOS returns the option of using a Bound filesystem OS. +func WithBoundOS() Option { + return func(o *options) { + o.Type = BoundOSFS + } +} + +// WithChrootOS returns the option of using a Chroot filesystem OS. +func WithChrootOS() Option { + return func(o *options) { + o.Type = ChrootOSFS + } +} + +type options struct { + Type +} + +type Type int + +const ( + ChrootOSFS Type = iota + BoundOSFS +)