Compare commits

...
Author SHA1 Message Date
Nick Craig-Wood e426492549 local: fix hang listing the root of a Windows network share - fixes #10280
Since v1.75.0 using a bare UNC share such as \\server\share as the
root of a local remote hung forever on Windows.

This was caused by the check that names stay inside the root. This
calls filepath.Rel which never returns on Windows when the base is a
bare UNC share and the target is the same share with a trailing
separator, which is what filepath.Join makes of the root and "".

This fixes it by treating the root and the root with a trailing
separator as the root itself without calling filepath.Rel.
2026-11-07 18:56:55 +00:00
2 changed files with 48 additions and 2 deletions

No files matched your search

+14 -2
View File
@@ -887,13 +887,25 @@ func (f *Fs) nativePath(name string) string {
// happen depending on the encoding.
func (f *Fs) localPath(name string) (string, error) {
localPath := filepath.Join(f.root, f.nativePath(name))
rel, err := filepath.Rel(f.root, localPath)
rel, err := f.relToRoot(localPath)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", fserrors.NoRetryError(fmt.Errorf("%q: %w", name, errPathEscapes))
}
return localPath, nil
}
// relToRoot returns localPath relative to f.root.
func (f *Fs) relToRoot(localPath string) (string, error) {
// On Windows filepath.Rel loops forever if the base is a bare UNC
// share and the target is the same share with a trailing
// separator, eg `\\server\share` and `\\server\share\`, which is
// what filepath.Join makes of f.root and "".
if localPath == f.root || localPath == f.root+string(filepath.Separator) {
return ".", nil
}
return filepath.Rel(f.root, localPath)
}
// Put the Object to the local filesystem
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
// Temporary Object under construction - info filled in by Update()
@@ -1594,7 +1606,7 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read
// f.root is the trusted destination the user chose, so it is created (and
// followed if it is itself a symlink) before being opened.
func (f *Fs) osRoot(localPath string) (root *os.Root, rel string, err error) {
rel, err = filepath.Rel(f.root, localPath)
rel, err = f.relToRoot(localPath)
if err != nil {
return nil, "", err
}
@@ -8,6 +8,7 @@ import (
"runtime"
"syscall"
"testing"
"time"
"github.com/rclone/rclone/fs/operations"
"github.com/rclone/rclone/fstest"
@@ -38,3 +39,36 @@ func TestRmdirWindows(t *testing.T) {
err = operations.Rmdir(context.Background(), r.Flocal, "testdir")
assert.NoError(t, err)
}
// TestLocalPathUNCShareRoot checks localPath works when the root is a
// bare UNC share, where filepath.Rel would never return.
func TestLocalPathUNCShareRoot(t *testing.T) {
for _, root := range []string{`\\server\share`, `\\?\UNC\server\share`} {
f := &Fs{root: root}
for _, test := range []struct {
name string
want string
}{
{name: "", want: root + `\`},
{name: "file.txt", want: root + `\file.txt`},
{name: "sub/file.txt", want: root + `\sub\file.txt`},
} {
type result struct {
path string
err error
}
done := make(chan result, 1)
go func() {
path, err := f.localPath(test.name)
done <- result{path, err}
}()
select {
case res := <-done:
require.NoError(t, res.err, root, test.name)
assert.Equal(t, test.want, res.path, root, test.name)
case <-time.After(10 * time.Second):
t.Fatalf("localPath(%q) with root %q did not return", test.name, root)
}
}
}
}