mirror of
https://github.com/distribution/distribution.git
synced 2026-07-16 16:59:52 +00:00
fix(s3): tags/list drops tags that are a lexical prefix of another tag
The S3 driver's Walk used a bare strings.HasPrefix to decide whether a walkInfo was under the last ErrSkipDir directory. Without a trailing "/" on the parent, a sibling whose name starts with the skipped directory's name (e.g. "0.1.20" after "0.1.2") falsely matched and was skipped, so its tag dir was never emitted to handleTag and the tag was omitted from /v2/<name>/tags/list. Replace the prefix check with isSubpath, which appends "/" to the parent so only true descendants match. Add a unit test pinning the sibling-with-lexical-prefix case. The bug is S3-only because the filesystem and inmemory drivers list directories recursively via WalkFallback and don't use this skip mechanism. It surfaced in 3.1.0 when the tags handler switched from tagService.All() (driver List with delimiter) to tagService.List() (driver Walk) for pagination support. Fixes #4891 Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
This commit is contained in:
@@ -1178,7 +1178,7 @@ func (d *driver) doWalk(parentCtx context.Context, objectCount *int64, from, sta
|
||||
|
||||
for _, walkInfo := range walkInfos {
|
||||
// skip any results under the last skip directory
|
||||
if prevSkipDir != "" && strings.HasPrefix(walkInfo.Path(), prevSkipDir) {
|
||||
if isSubpath(walkInfo.Path(), prevSkipDir) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1249,6 +1249,19 @@ func directoryDiff(prev, current string) []string {
|
||||
return paths
|
||||
}
|
||||
|
||||
func isSubpath(path, parent string) bool {
|
||||
if parent == "" {
|
||||
return false
|
||||
}
|
||||
if path == parent {
|
||||
return true
|
||||
}
|
||||
if parent == "/" {
|
||||
return strings.HasPrefix(path, "/")
|
||||
}
|
||||
return strings.HasPrefix(path, parent+"/")
|
||||
}
|
||||
|
||||
func (d *driver) s3Path(path string) string {
|
||||
return strings.TrimLeft(strings.TrimRight(d.RootDirectory, "/")+path, "/")
|
||||
}
|
||||
|
||||
@@ -292,6 +292,55 @@ func TestGetS3LogLevelFromParam(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSubpath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
parent string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "empty parent",
|
||||
path: "/tags/1.2",
|
||||
parent: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "same path",
|
||||
path: "/tags/1.2",
|
||||
parent: "/tags/1.2",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "descendant path",
|
||||
path: "/tags/1.2/current/link",
|
||||
parent: "/tags/1.2",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "sibling with lexical prefix",
|
||||
path: "/tags/1.20/current/link",
|
||||
parent: "/tags/1.2",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "root parent",
|
||||
path: "/tags/1.2",
|
||||
parent: "/",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := isSubpath(tt.path, tt.parent)
|
||||
if actual != tt.expected {
|
||||
t.Fatalf("isSubpath(%q, %q) = %t, want %t", tt.path, tt.parent, actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyRootList(t *testing.T) {
|
||||
skipCheck(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user