diff --git a/fs.go b/fs.go index f4e7932d..ecbebf04 100644 --- a/fs.go +++ b/fs.go @@ -129,15 +129,21 @@ func (m *Fs) path(p string) string { return PathJoin(cwd, p) } + // An unrestricted medium passes an absolute path through as it + // stands — only a relative one was rewritten above. No "/" prefix + // here: prefixing was a no-op for a POSIX absolute path ("//x" + // cleans to "/x") but corrupted a Windows drive-letter one — + // filepath.Clean(`/C:\models`) is `\C:\models`, a path no Win32 + // open resolves, which silently emptied every List/Read walk on + // an absolute path there. + if root == "/" { + return CleanPath(p, string(PathSeparator)) + } + // Use a leading slash to resolve all .. and . internally // before joining with the root. This is a standard way to sandbox paths. clean := CleanPath("/"+p, string(PathSeparator)) - // If root is "/", allow absolute paths through - if root == "/" { - return clean - } - // Strip leading "/" so Join works correctly with root return PathJoin(root, clean[1:]) } diff --git a/fs_internal_test.go b/fs_internal_test.go index 9d35d1fd..8ce9cf93 100644 --- a/fs_internal_test.go +++ b/fs_internal_test.go @@ -20,6 +20,15 @@ func TestFs_Fs_path_Ugly(t *T) { fsys := (&Fs{}).New("/") AssertEqual(t, PathJoin(cwd.Value.(string), "relative.txt"), fsys.path("relative.txt")) + + // An absolute path on an unrestricted medium comes back as itself — + // no "/" prefix bolted on first. The prefix was invisible on POSIX + // ("//x" cleans to "/x") and turned a Windows drive-letter path into + // `\C:\models`, which no Win32 open resolves. PathIsAbs recognises + // the drive-letter form on every platform, so this pins the contract + // everywhere, not just on a windows runner. + AssertEqual(t, `C:\models\gemma`, fsys.path(`C:\models\gemma`)) + AssertEqual(t, CleanPath("/tmp//x/../y", string(PathSeparator)), fsys.path("/tmp//x/../y")) } func TestFs_Fs_validatePath_Good(t *T) { root := t.TempDir()