Skip to content

Commit

Permalink
feat(fsx): add NewRelativeToCwdPrefixDirPathMapper (#23)
Browse files Browse the repository at this point in the history
This commit adds the NewRelativeToCwdPrefixDirPathMapper constructor for
PrefixDirPathMapper.

It seems this constructor should completely address the use case of
`rbmk sh` along with the Unix domain socket total path length
restrictions.

I initially planned on implementing this inside the `rbmk sh`
implementaion but then decided to implement it here because it could be
useful for other use cases beyond just my specific use case of `rbmk
sh`. Namely:

1. cases where one is using `fsx`

2. the rest of the code does not call `os.Chdir`

3. we're fine with using relative paths
  • Loading branch information
bassosimone authored Dec 22, 2024
1 parent ffa77aa commit 4273bdb
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 2 deletions.
40 changes: 38 additions & 2 deletions fsx/pathmappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package fsx

import (
"io/fs"
"os"
"path/filepath"
"strings"
)
Expand All @@ -34,8 +35,9 @@ var filepathAbs = filepath.Abs
// PrefixDirPathMapper is a [RealPathMapper] that prepends
// a base directory to the virtual path.
//
// The zero value is invalid. Use [NewRelativePrefixDirPathMapper] or
// [NewAbsolutePrefixDirPathMapper] to construct a new instance.
// The zero value is invalid. Use [NewRelativePrefixDirPathMapper],
// [NewRelativeToCwdPrefixDirPathMapper] or [NewAbsolutePrefixDirPathMapper]
// to construct a new instance.
type PrefixDirPathMapper struct {
// baseDir is the base directory to prepend.
baseDir string
Expand Down Expand Up @@ -77,6 +79,40 @@ func NewRelativePrefixDirPathMapper(baseDir string) *PrefixDirPathMapper {
return &PrefixDirPathMapper{baseDir: baseDir}
}

// osGetwd allows to mock [os.Getwd] in tests.
var osGetwd = os.Getwd

// filepathRel allows to mock [filepath.Rel] in tests.
var filepathRel = filepath.Rel

// NewRelativeToCwdPrefixDirPathMapper returns a [*PrefixDirPathMapper] in which
// the given base directory is made relative to the current working directory
// obtained using [os.Getwd] at the time of the call. On failure, it returns an error.
//
// # Usage Considerations
//
// Use this constructor when you know your program is not going
// to invoke [os.Chdir] so you can avoid building potentially long
// paths that could break Unix domain sockets as documented in
// the top-level package documentation.
//
// This constructor explicitly addresses the `rbmk sh` use case where
// [mvdan.cc/sh/v3/interp] provides us with the absolute path of the
// current working directory, subcommands run as goroutines, we cannot
// chdir because we're still in the same process, and we want to minimise
// the length of paths because of Unix domain sockets path limitations.
func NewRelativeToCwdPrefixDirPathMapper(path string) (*PrefixDirPathMapper, error) {
cwd, err := osGetwd()
if err != nil {
return nil, err
}
relPath, err := filepathRel(cwd, path)
if err != nil {
return nil, err
}
return NewRelativePrefixDirPathMapper(relPath), nil
}

// NewRelativeChdirPathMapper is a deprecated alias for [NewRelativePrefixDirPathMapper].
var NewRelativeChdirPathMapper = NewRelativePrefixDirPathMapper

Expand Down
136 changes: 136 additions & 0 deletions fsx/pathmappers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,139 @@ func TestPathMappers(t *testing.T) {
})
}
}

func TestRelativeToCwdPrefixDirPathMapper(t *testing.T) {
type testCase struct {
// name is the name of the test case
name string

// mockCwd is the mock to use for [os.Cwd]
mockCwd func() (string, error)

// mockRel is the mock to use for [filepath.Rel]
mockRel func(cwd, path string) (string, error)

// inputPath is the path passed to [NewRelativeToCwdPrefixDirPathMapper]
inputPath string

// want is the resulting directory that we want
want string

// wantError is the error that we expect
wantError error
}

tests := []testCase{
{
name: "simple relative path",
mockCwd: func() (string, error) {
return "/base", nil
},
mockRel: func(cwd, path string) (string, error) {
if cwd != "/base" || path != "/base/project" {
t.Fatalf("unexpected args: cwd=%q path=%q", cwd, path)
}
return "project", nil
},
inputPath: "/base/project",
want: "project",
},

{
name: "nested path",
mockCwd: func() (string, error) {
return "/base", nil
},
mockRel: func(cwd, path string) (string, error) {
if cwd != "/base" || path != "/base/deep/project" {
t.Fatalf("unexpected args: cwd=%q path=%q", cwd, path)
}
return "deep/project", nil
},
inputPath: "/base/deep/project",
want: "deep/project",
},

{
name: "getwd fails",
mockCwd: func() (string, error) {
return "", errors.New("getwd error")
},
mockRel: func(cwd, path string) (string, error) {
t.Fatal("rel should not be called")
return "", nil
},
inputPath: "/any/path",
wantError: errors.New("getwd error"),
},

{
name: "rel fails",
mockCwd: func() (string, error) {
return "/base", nil
},
mockRel: func(cwd, path string) (string, error) {
return "", errors.New("rel error")
},
inputPath: "/any/path",
wantError: errors.New("rel error"),
},

{
name: "path outside base",
mockCwd: func() (string, error) {
return "/base", nil
},
mockRel: func(cwd, path string) (string, error) {
if cwd != "/base" || path != "/other/path" {
t.Fatalf("unexpected args: cwd=%q path=%q", cwd, path)
}
return "../other/path", nil
},
inputPath: "/other/path",
want: "../other/path", // Note: this is allowed by PrefixDirPathMapper
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save and restore original functions
savedGetwd := osGetwd
savedRel := filepathRel
defer func() {
osGetwd = savedGetwd
filepathRel = savedRel
}()

// Install mocks
osGetwd = tt.mockCwd
filepathRel = tt.mockRel

// Run test
mapper, err := NewRelativeToCwdPrefixDirPathMapper(tt.inputPath)
if err != nil {
if tt.wantError == nil {
t.Fatalf("unexpected error: %v", err)
}
if err.Error() != tt.wantError.Error() {
t.Fatalf("got error %v, want %v", err, tt.wantError)
}
return
}
if tt.wantError != nil {
t.Fatalf("expected error %v, got nil", tt.wantError)
}

// Test the mapper
got, err := mapper.RealPath("file.txt")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

want := filepath.Join(tt.want, "file.txt")
if got != want {
t.Errorf("got %q, want %q", got, want)
}
})
}
}

0 comments on commit 4273bdb

Please sign in to comment.