archive: fix listing entries with a leading slash as if they were in the root

The check that an entry returned by an archiver is a direct child of
the directory being listed normalised a parent of "/" to the root, so
an entry named "/x" passed as a child of the root while "x/" and
"dir//x" were rejected.

Decide by stripping the directory prefix and checking what is left
with sanitize.Leaf, which rejects an empty name, ".", ".." and any
name containing a "/". This also covers the leading slash case.
This commit is contained in:
Nick Craig-Wood
2026-09-05 12:14:46 +01:00
parent da352a2a1b
commit b88e237e8c
2 changed files with 32 additions and 7 deletions
+8 -7
View File
@@ -36,6 +36,7 @@ import (
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/fspath"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/lib/sanitize"
)
// Register with Fs
@@ -529,14 +530,14 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
// isDirectChild reports whether remote names an entry directly in dir
// ("" being the root), with no ".." or other components in between.
func isDirectChild(dir, remote string) bool {
if remote == "" || remote == "." || remote == ".." || strings.HasSuffix(remote, "/") {
return false
if dir != "" {
var ok bool
remote, ok = strings.CutPrefix(remote, dir+"/")
if !ok {
return false
}
}
parent := path.Dir(remote)
if parent == "." || parent == "/" {
parent = ""
}
return parent == dir && path.Clean(remote) == remote
return sanitize.Leaf(remote) == nil
}
// NewObject creates a new remote archive file object
+24
View File
@@ -379,3 +379,27 @@ func TestArchiveEscapingArchiver(t *testing.T) {
_, err = f.NewObject(ctx, "test.escaping/file.txt")
assert.ErrorIs(t, err, fs.ErrorObjectNotFound)
}
// TestIsDirectChild checks the guard which decides whether an entry
// returned by an archiver belongs directly in the directory listed.
func TestIsDirectChild(t *testing.T) {
for _, test := range []struct {
dir, remote string
want bool
}{
{"", "a.txt", true},
{"", "/a.txt", false},
{"", "a.txt/", false},
{"", "../a.txt", false},
{"", "sub/a.txt", false},
{"d", "d/a.txt", true},
{"d", "d", false},
{"d", "d/", false},
{"d", "d//a.txt", false},
{"d", "d/../a.txt", false},
{"d", "dd/a.txt", false},
{"d", "a.txt", false},
} {
assert.Equal(t, test.want, isDirectChild(test.dir, test.remote), "dir=%q remote=%q", test.dir, test.remote)
}
}