fix: keep unread nested archive limits out of java capabilities

- java embeds the shared `cataloging.ArchiveSearchConfig` but only reads the two `Include*` booleans,
  and setting depth or byte limits there fails `CreateSBOM`
- a `capabilities-fields:` annotation on an embedded struct now limits which of its fields are
  listed, so the docs stop advertising settings that error
- building the java cataloger directly with those fields set now warns instead of debug logging

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-10-21 11:45:41 -04:00
parent 36d417d52a
commit 72835436c4
5 changed files with 59 additions and 7 deletions
@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"github.com/anchore/syft/internal/capabilities/internal"
@@ -31,6 +32,10 @@ type ConfigInfo struct {
var appConfigAnnotationPattern = regexp.MustCompile(`^//\s*app-config:\s*(.+)$`)
// embeddedFieldsAnnotationPattern matches "// capabilities-fields: A, B" on an embedded struct, which limits
// the embedded fields listed to the ones named (for a shared type the cataloger reads only part of).
var embeddedFieldsAnnotationPattern = regexp.MustCompile(`^//\s*capabilities-fields:\s*(.+)$`)
// DiscoverConfigs walks the cataloger directory and discovers all configuration structs
// Returns map where key is "packageName.StructName" (e.g., "golang.CatalogerConfig")
func DiscoverConfigs(repoRoot string) (map[string]ConfigInfo, error) {
@@ -160,6 +165,9 @@ func extractCatalogerConfigFieldsRecursive(structType *ast.StructType, visitedTy
if len(field.Names) == 0 {
// this is an embedded field - resolve and extract its fields
embeddedFields := resolveEmbeddedStructFields(field.Type, visitedTypes)
if only := embeddedFieldAllowlist(field.Doc); only != nil {
embeddedFields = slices.DeleteFunc(embeddedFields, func(f ConfigField) bool { return !only[f.Name] })
}
fields = append(fields, embeddedFields...)
continue
}
@@ -195,6 +203,26 @@ func extractCatalogerConfigFieldsRecursive(structType *ast.StructType, visitedTy
return fields
}
// embeddedFieldAllowlist returns the field names from a "capabilities-fields:" annotation, or nil when there
// is none (every exported embedded field is listed).
func embeddedFieldAllowlist(commentGroup *ast.CommentGroup) map[string]bool {
if commentGroup == nil {
return nil
}
for _, comment := range commentGroup.List {
matches := embeddedFieldsAnnotationPattern.FindStringSubmatch(comment.Text)
if len(matches) < 2 {
continue
}
only := make(map[string]bool)
for _, name := range strings.Split(matches[1], ",") {
only[strings.TrimSpace(name)] = true
}
return only
}
return nil
}
// resolveEmbeddedStructFields resolves an embedded struct type and extracts its fields recursively
func resolveEmbeddedStructFields(fieldType ast.Expr, visitedTypes map[string]bool) []ConfigField {
// extract the type name from the expression
@@ -457,3 +457,32 @@ func TestDiscoverAllowedConfigStructs(t *testing.T) {
t.Logf(" - %s", configType)
}
}
func TestEmbeddedFieldAllowlist(t *testing.T) {
comments := func(lines ...string) *ast.CommentGroup {
g := &ast.CommentGroup{}
for _, l := range lines {
g.List = append(g.List, &ast.Comment{Text: l})
}
return g
}
tests := []struct {
name string
doc *ast.CommentGroup
want map[string]bool
}{
{name: "no doc lists every field", doc: nil, want: nil},
{name: "no annotation lists every field", doc: comments("// Deprecated: use something else."), want: nil},
{
name: "annotation limits to the named fields",
doc: comments("// Deprecated: use something else.", "// capabilities-fields: IncludeIndexedArchives, IncludeUnindexedArchives"),
want: map[string]bool{"IncludeIndexedArchives": true, "IncludeUnindexedArchives": true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, embeddedFieldAllowlist(tt.doc))
})
}
}