From b88e237e8c6006ca0426c0364f5020340efb38fd Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 2 Sep 2026 15:42:43 +0100 Subject: [PATCH] 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. --- backend/archive/archive.go | 15 ++++++++------- backend/archive/archive_internal_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/backend/archive/archive.go b/backend/archive/archive.go index 25e489581..d60d7f012 100644 --- a/backend/archive/archive.go +++ b/backend/archive/archive.go @@ -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 diff --git a/backend/archive/archive_internal_test.go b/backend/archive/archive_internal_test.go index 75509e6f7..4d3cf2a0a 100644 --- a/backend/archive/archive_internal_test.go +++ b/backend/archive/archive_internal_test.go @@ -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) + } +}