From b778e8e78b7ac8b9016b50db41c4b5300596322e Mon Sep 17 00:00:00 2001 From: dualfroz Date: Wed, 2 Sep 2026 11:39:06 +0200 Subject: [PATCH] fix: strip nested BasePathFs base segment from BasePathFile.Name BasePathFile.Name trimmed only filepath.Clean(path) from the wrapped file name. filepath.Clean drops the trailing separator, and a nested BasePathFile reports a name whose base segment is preceded by a leading separator (for example "/123/x"), which the cleaned base ("123") does not match. The base then leaked into Name and callers double-prepended it. Trim the separator-prefixed form too so nested BasePathFs report a name that round-trips. Fixes #428 --- basepath.go | 11 ++++++++++- basepath_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/basepath.go b/basepath.go index 47438dd..aa1e38c 100644 --- a/basepath.go +++ b/basepath.go @@ -37,7 +37,16 @@ type BasePathFile struct { func (f *BasePathFile) Name() string { sourcename := f.File.Name() - return strings.TrimPrefix(sourcename, filepath.Clean(f.path)) + base := filepath.Clean(f.path) + name := strings.TrimPrefix(sourcename, base) + if name == sourcename { + // filepath.Clean drops the leading separator from the base path, so a + // name produced by a wrapped (nested) BasePathFile can still carry the + // base preceded by a separator. Strip that form as well, otherwise the + // base segment leaks into the reported name. + name = strings.TrimPrefix(sourcename, string(filepath.Separator)+base) + } + return name } func (f *BasePathFile) ReadDir(n int) ([]fs.DirEntry, error) { diff --git a/basepath_test.go b/basepath_test.go index 2ecc440..603b48e 100644 --- a/basepath_test.go +++ b/basepath_test.go @@ -307,3 +307,31 @@ func TestBasePathTempFile(t *testing.T) { t.Fatalf("TempFile realpath leaked: expected %s, got %s", expected, actual) } } + +// Regression test for #428: a nested BasePathFs must strip its own base +// segment from BasePathFile.Name(), even though filepath.Clean drops the +// leading separator that the wrapped (inner) file name carries. +func TestNestedBasePathFileName(t *testing.T) { + baseFs := &MemMapFs{} + if err := baseFs.MkdirAll("/files/123", 0o777); err != nil { + t.Fatal(err) + } + fs1 := NewBasePathFs(baseFs, "/files") + fs2 := NewBasePathFs(fs1, "123") + + f, err := fs2.OpenFile("/afero-test.txt", os.O_CREATE|os.O_RDWR, 0o644) + if err != nil { + t.Fatalf("open through nested BasePathFs failed: %v", err) + } + defer f.Close() + + if expected, actual := "/afero-test.txt", f.Name(); expected != actual { + t.Fatalf("nested BasePathFile.Name() = %q, want %q", actual, expected) + } + + // The returned name must round-trip: using it via fs2 resolves back to + // the same file instead of double-prepending the base segment. + if err := fs2.Chmod(f.Name(), 0o600); err != nil { + t.Fatalf("chmod via returned name failed: %v", err) + } +}