Skip to content
This repository was archived by the owner on Jul 3, 2025. It is now read-only.

Commit 4273bdb

Browse files
authored
feat(fsx): add NewRelativeToCwdPrefixDirPathMapper (#23)
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
1 parent ffa77aa commit 4273bdb

File tree

2 files changed

+174
-2
lines changed

2 files changed

+174
-2
lines changed

fsx/pathmappers.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package fsx
88

99
import (
1010
"io/fs"
11+
"os"
1112
"path/filepath"
1213
"strings"
1314
)
@@ -34,8 +35,9 @@ var filepathAbs = filepath.Abs
3435
// PrefixDirPathMapper is a [RealPathMapper] that prepends
3536
// a base directory to the virtual path.
3637
//
37-
// The zero value is invalid. Use [NewRelativePrefixDirPathMapper] or
38-
// [NewAbsolutePrefixDirPathMapper] to construct a new instance.
38+
// The zero value is invalid. Use [NewRelativePrefixDirPathMapper],
39+
// [NewRelativeToCwdPrefixDirPathMapper] or [NewAbsolutePrefixDirPathMapper]
40+
// to construct a new instance.
3941
type PrefixDirPathMapper struct {
4042
// baseDir is the base directory to prepend.
4143
baseDir string
@@ -77,6 +79,40 @@ func NewRelativePrefixDirPathMapper(baseDir string) *PrefixDirPathMapper {
7779
return &PrefixDirPathMapper{baseDir: baseDir}
7880
}
7981

82+
// osGetwd allows to mock [os.Getwd] in tests.
83+
var osGetwd = os.Getwd
84+
85+
// filepathRel allows to mock [filepath.Rel] in tests.
86+
var filepathRel = filepath.Rel
87+
88+
// NewRelativeToCwdPrefixDirPathMapper returns a [*PrefixDirPathMapper] in which
89+
// the given base directory is made relative to the current working directory
90+
// obtained using [os.Getwd] at the time of the call. On failure, it returns an error.
91+
//
92+
// # Usage Considerations
93+
//
94+
// Use this constructor when you know your program is not going
95+
// to invoke [os.Chdir] so you can avoid building potentially long
96+
// paths that could break Unix domain sockets as documented in
97+
// the top-level package documentation.
98+
//
99+
// This constructor explicitly addresses the `rbmk sh` use case where
100+
// [mvdan.cc/sh/v3/interp] provides us with the absolute path of the
101+
// current working directory, subcommands run as goroutines, we cannot
102+
// chdir because we're still in the same process, and we want to minimise
103+
// the length of paths because of Unix domain sockets path limitations.
104+
func NewRelativeToCwdPrefixDirPathMapper(path string) (*PrefixDirPathMapper, error) {
105+
cwd, err := osGetwd()
106+
if err != nil {
107+
return nil, err
108+
}
109+
relPath, err := filepathRel(cwd, path)
110+
if err != nil {
111+
return nil, err
112+
}
113+
return NewRelativePrefixDirPathMapper(relPath), nil
114+
}
115+
80116
// NewRelativeChdirPathMapper is a deprecated alias for [NewRelativePrefixDirPathMapper].
81117
var NewRelativeChdirPathMapper = NewRelativePrefixDirPathMapper
82118

fsx/pathmappers_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,139 @@ func TestPathMappers(t *testing.T) {
177177
})
178178
}
179179
}
180+
181+
func TestRelativeToCwdPrefixDirPathMapper(t *testing.T) {
182+
type testCase struct {
183+
// name is the name of the test case
184+
name string
185+
186+
// mockCwd is the mock to use for [os.Cwd]
187+
mockCwd func() (string, error)
188+
189+
// mockRel is the mock to use for [filepath.Rel]
190+
mockRel func(cwd, path string) (string, error)
191+
192+
// inputPath is the path passed to [NewRelativeToCwdPrefixDirPathMapper]
193+
inputPath string
194+
195+
// want is the resulting directory that we want
196+
want string
197+
198+
// wantError is the error that we expect
199+
wantError error
200+
}
201+
202+
tests := []testCase{
203+
{
204+
name: "simple relative path",
205+
mockCwd: func() (string, error) {
206+
return "/base", nil
207+
},
208+
mockRel: func(cwd, path string) (string, error) {
209+
if cwd != "/base" || path != "/base/project" {
210+
t.Fatalf("unexpected args: cwd=%q path=%q", cwd, path)
211+
}
212+
return "project", nil
213+
},
214+
inputPath: "/base/project",
215+
want: "project",
216+
},
217+
218+
{
219+
name: "nested path",
220+
mockCwd: func() (string, error) {
221+
return "/base", nil
222+
},
223+
mockRel: func(cwd, path string) (string, error) {
224+
if cwd != "/base" || path != "/base/deep/project" {
225+
t.Fatalf("unexpected args: cwd=%q path=%q", cwd, path)
226+
}
227+
return "deep/project", nil
228+
},
229+
inputPath: "/base/deep/project",
230+
want: "deep/project",
231+
},
232+
233+
{
234+
name: "getwd fails",
235+
mockCwd: func() (string, error) {
236+
return "", errors.New("getwd error")
237+
},
238+
mockRel: func(cwd, path string) (string, error) {
239+
t.Fatal("rel should not be called")
240+
return "", nil
241+
},
242+
inputPath: "/any/path",
243+
wantError: errors.New("getwd error"),
244+
},
245+
246+
{
247+
name: "rel fails",
248+
mockCwd: func() (string, error) {
249+
return "/base", nil
250+
},
251+
mockRel: func(cwd, path string) (string, error) {
252+
return "", errors.New("rel error")
253+
},
254+
inputPath: "/any/path",
255+
wantError: errors.New("rel error"),
256+
},
257+
258+
{
259+
name: "path outside base",
260+
mockCwd: func() (string, error) {
261+
return "/base", nil
262+
},
263+
mockRel: func(cwd, path string) (string, error) {
264+
if cwd != "/base" || path != "/other/path" {
265+
t.Fatalf("unexpected args: cwd=%q path=%q", cwd, path)
266+
}
267+
return "../other/path", nil
268+
},
269+
inputPath: "/other/path",
270+
want: "../other/path", // Note: this is allowed by PrefixDirPathMapper
271+
},
272+
}
273+
274+
for _, tt := range tests {
275+
t.Run(tt.name, func(t *testing.T) {
276+
// Save and restore original functions
277+
savedGetwd := osGetwd
278+
savedRel := filepathRel
279+
defer func() {
280+
osGetwd = savedGetwd
281+
filepathRel = savedRel
282+
}()
283+
284+
// Install mocks
285+
osGetwd = tt.mockCwd
286+
filepathRel = tt.mockRel
287+
288+
// Run test
289+
mapper, err := NewRelativeToCwdPrefixDirPathMapper(tt.inputPath)
290+
if err != nil {
291+
if tt.wantError == nil {
292+
t.Fatalf("unexpected error: %v", err)
293+
}
294+
if err.Error() != tt.wantError.Error() {
295+
t.Fatalf("got error %v, want %v", err, tt.wantError)
296+
}
297+
return
298+
}
299+
if tt.wantError != nil {
300+
t.Fatalf("expected error %v, got nil", tt.wantError)
301+
}
302+
303+
// Test the mapper
304+
got, err := mapper.RealPath("file.txt")
305+
if err != nil {
306+
t.Fatalf("unexpected error: %v", err)
307+
}
308+
309+
want := filepath.Join(tt.want, "file.txt")
310+
if got != want {
311+
t.Errorf("got %q, want %q", got, want)
312+
}
313+
})
314+
}
315+
}

0 commit comments

Comments
 (0)