local: don't resolve relative roots to absolute paths - fixes #9510

cleanRootPath used filepath.Abs which prepends the current directory,
but the resulting absolute path does not always refer to the same
directory as the original relative path - for example when the current
directory is shadowed by a mount or has been removed. This made
"rclone copy --links . ../dst" fail where "cp -ra . ../dst" succeeds.

rclone now cleans the path lexically on non-Windows platforms instead,
leaving relative roots relative so the OS resolves them against the live
working directory. Windows still makes the path absolute as required for
UNC long-path conversion.
This commit is contained in:
Nick Craig-Wood
2026-06-11 11:47:02 +01:00
parent a37d54b11a
commit 7bb3bcaac3
2 changed files with 38 additions and 3 deletions

View File

@@ -1726,9 +1726,18 @@ func cleanRootPath(s string, noUNC bool, enc encoder.MultiEncoder) string {
if runtime.GOOS == "windows" {
s = vol + s
}
s2, err := filepath.Abs(s)
if err == nil {
s = s2
// UNC paths on Windows must be absolute, so make the path absolute
// there. On other platforms filepath.Abs would prepend the current
// directory, but the resulting absolute string is not guaranteed to
// refer to the same directory as the original relative path - for
// example when the current directory is shadowed by a mount or has been
// removed - so just clean it lexically instead.
if runtime.GOOS == "windows" {
if s2, err := filepath.Abs(s); err == nil {
s = s2
}
} else {
s = filepath.Clean(s)
}
if !noUNC {
// Convert to UNC. It does nothing on non windows platforms.

View File

@@ -30,3 +30,29 @@ func TestCleanWindows(t *testing.T) {
}
}
}
// Relative roots must stay relative so the OS resolves them against the
// live working directory rather than a canonicalised string that may no
// longer refer to the same directory (#9510).
var testsRelative = [][2]string{
{".", "."},
{"./", "."},
{"sub/dir", "sub/dir"},
{"sub/dir/", "sub/dir"},
{"./sub/dir", "sub/dir"},
{"sub/../dir", "dir"},
{"..", ".."},
}
func TestCleanRootPathRelative(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skipf("non-windows only")
}
for _, test := range testsRelative {
got := cleanRootPath(test[0], true, encoder.OS)
expect := test[1]
if got != expect {
t.Fatalf("got %q, expected %q", got, expect)
}
}
}