From e77cafcadafb1d2dbf7c2ae63ce33e0de347a083 Mon Sep 17 00:00:00 2001 From: bassg0navy Date: Sat, 29 Nov 2025 04:08:59 -0600 Subject: [PATCH 1/5] fix: escape non-ASCII characters in OCI annotation values; fixes #31507 Signed-off-by: bassg0navy --- pkg/registry/chart.go | 32 +++++++-- pkg/registry/chart_test.go | 135 +++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 7 deletions(-) diff --git a/pkg/registry/chart.go b/pkg/registry/chart.go index b00fc616d..f17ec34d0 100644 --- a/pkg/registry/chart.go +++ b/pkg/registry/chart.go @@ -18,6 +18,7 @@ package registry // import "helm.sh/helm/v4/pkg/registry" import ( "bytes" + "fmt" "strings" "time" @@ -65,14 +66,31 @@ annotations: return ociAnnotations } +func escapeNonASCII(s string) string { + var result strings.Builder + result.Grow(len(s)) // Pre-allocate for efficiency + + for _, r := range s { + if r > 127 { + // Escape non-ASCII as \uXXXX (lowercase hex) + fmt.Fprintf(&result, "\\u%04x", r) + } else { + result.WriteRune(r) + } + } + return result.String() +} + // generateChartOCIAnnotations will generate OCI annotations from the provided chart +// Non-ASCII characters in annotation values are escaped as \uXXXX sequences +// to ensure compatibility with registries that strictly follow OCI spec recommendations. func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { chartOCIAnnotations := map[string]string{} - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description) - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name) - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version) - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, escapeNonASCII(meta.Description)) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, escapeNonASCII(meta.Name)) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, escapeNonASCII(meta.Version)) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, escapeNonASCII(meta.Home)) if len(creationTime) == 0 { creationTime = time.Now().UTC().Format(time.RFC3339) @@ -81,7 +99,7 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime) if len(meta.Sources) > 0 { - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0]) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, escapeNonASCII(meta.Sources[0])) } if len(meta.Maintainers) > 0 { @@ -90,12 +108,12 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ for maintainerIdx, maintainer := range meta.Maintainers { if len(maintainer.Name) > 0 { - maintainerSb.WriteString(maintainer.Name) + maintainerSb.WriteString(escapeNonASCII(maintainer.Name)) } if len(maintainer.Email) > 0 { maintainerSb.WriteString(" (") - maintainerSb.WriteString(maintainer.Email) + maintainerSb.WriteString(escapeNonASCII(maintainer.Email)) maintainerSb.WriteString(")") } diff --git a/pkg/registry/chart_test.go b/pkg/registry/chart_test.go index 77ccdaab7..d195f9a0a 100644 --- a/pkg/registry/chart_test.go +++ b/pkg/registry/chart_test.go @@ -272,3 +272,138 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { } } + +func TestEscapeNonASCII(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "ASCII only - no change", + input: "John Smith", + expected: "John Smith", + }, + { + name: "German umlaut", + input: "Jan-Otto Kröpke", + expected: "Jan-Otto Kr\\u00f6pke", + }, + { + name: "Multiple umlauts", + input: "Müller Schröder", + expected: "M\\u00fcller Schr\\u00f6der", + }, + { + name: "French accents", + input: "François Müller", + expected: "Fran\\u00e7ois M\\u00fcller", + }, + { + name: "Spanish tilde", + input: "José Muñoz", + expected: "Jos\\u00e9 Mu\\u00f1oz", + }, + { + name: "Empty string", + input: "", + expected: "", + }, + { + name: "Numbers and punctuation", + input: "v1.2.3-beta+build.123", + expected: "v1.2.3-beta+build.123", + }, + { + name: "Email with umlaut in name", + input: "kröpke@example.com", + expected: "kr\\u00f6pke@example.com", + }, + { + name: "Nordic characters", + input: "Øystein Ålander", + expected: "\\u00d8ystein \\u00c5lander", + }, + { + name: "Mixed ASCII and non-ASCII", + input: "Abc Déf Ghï", + expected: "Abc D\\u00e9f Gh\\u00ef", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := escapeNonASCII(tt.input) + if result != tt.expected { + t.Errorf("escapeNonASCII(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestGenerateChartOCIAnnotations_WithNonASCII(t *testing.T) { + meta := &chart.Metadata{ + Name: "test-chart", + Description: "A Helm chart for Kröpke", + Version: "1.0.0", + Home: "https://example.com", + Sources: []string{"https://github.com/example/test"}, + Maintainers: []*chart.Maintainer{ + { + Name: "Jan-Otto Kröpke", + Email: "github@jkroepke.de", + }, + { + Name: "François Müller", + Email: "francois@example.com", + }, + }, + } + + annotations := generateChartOCIAnnotations(meta, "2025-01-01T00:00:00Z") + + // Check description is escaped + expectedDesc := "A Helm chart for Kr\\u00f6pke" + if annotations["org.opencontainers.image.description"] != expectedDesc { + t.Errorf("Description = %q, want %q", + annotations["org.opencontainers.image.description"], expectedDesc) + } + + // Check authors are escaped + expectedAuthors := "Jan-Otto Kr\\u00f6pke (github@jkroepke.de), Fran\\u00e7ois M\\u00fcller (francois@example.com)" + if annotations["org.opencontainers.image.authors"] != expectedAuthors { + t.Errorf("Authors = %q, want %q", + annotations["org.opencontainers.image.authors"], expectedAuthors) + } + + // Check title (ASCII only, should be unchanged) + if annotations["org.opencontainers.image.title"] != "test-chart" { + t.Errorf("Title = %q, want %q", + annotations["org.opencontainers.image.title"], "test-chart") + } +} + +func TestGenerateChartOCIAnnotations_ASCIIOnly(t *testing.T) { + meta := &chart.Metadata{ + Name: "test-chart", + Description: "A simple Helm chart", + Version: "1.0.0", + Maintainers: []*chart.Maintainer{ + { + Name: "John Smith", + Email: "john@example.com", + }, + }, + } + + annotations := generateChartOCIAnnotations(meta, "2025-01-01T00:00:00Z") + + // ASCII-only values should be unchanged + if annotations["org.opencontainers.image.description"] != "A simple Helm chart" { + t.Errorf("Description should be unchanged for ASCII-only input") + } + + if annotations["org.opencontainers.image.authors"] != "John Smith (john@example.com)" { + t.Errorf("Authors should be unchanged for ASCII-only input") + } +} \ No newline at end of file From 3b532c64dd0abdbcb127bf2b3483d79301ab687e Mon Sep 17 00:00:00 2001 From: bassg0navy Date: Sat, 29 Nov 2025 13:09:20 -0600 Subject: [PATCH 2/5] refactor: use max ascii for clarity Signed-off-by: bassg0navy --- pkg/registry/chart.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/registry/chart.go b/pkg/registry/chart.go index f17ec34d0..46a25d62e 100644 --- a/pkg/registry/chart.go +++ b/pkg/registry/chart.go @@ -21,6 +21,7 @@ import ( "fmt" "strings" "time" + "unicode" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" @@ -71,8 +72,8 @@ func escapeNonASCII(s string) string { result.Grow(len(s)) // Pre-allocate for efficiency for _, r := range s { - if r > 127 { - // Escape non-ASCII as \uXXXX (lowercase hex) + if r > unicode.MaxASCII { + // Escape non-ASCII using standard unicode escaping fmt.Fprintf(&result, "\\u%04x", r) } else { result.WriteRune(r) From 8988b467390660ee571e5f217b33a35f4b5f071d Mon Sep 17 00:00:00 2001 From: bassg0navy Date: Sat, 29 Nov 2025 13:15:49 -0600 Subject: [PATCH 3/5] refactor: abbreviate and move up comment Signed-off-by: bassg0navy --- pkg/registry/chart.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/registry/chart.go b/pkg/registry/chart.go index 46a25d62e..634fbfe12 100644 --- a/pkg/registry/chart.go +++ b/pkg/registry/chart.go @@ -67,6 +67,8 @@ annotations: return ociAnnotations } +// Non-ASCII characters in annotation values are escaped +// to ensure compatibility with registries that strictly follow OCI spec func escapeNonASCII(s string) string { var result strings.Builder result.Grow(len(s)) // Pre-allocate for efficiency @@ -83,8 +85,6 @@ func escapeNonASCII(s string) string { } // generateChartOCIAnnotations will generate OCI annotations from the provided chart -// Non-ASCII characters in annotation values are escaped as \uXXXX sequences -// to ensure compatibility with registries that strictly follow OCI spec recommendations. func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { chartOCIAnnotations := map[string]string{} From 3a27773483cb404bdb05e55d6e0e252f69dfe820 Mon Sep 17 00:00:00 2001 From: bassg0navy Date: Sat, 29 Nov 2025 23:36:23 -0600 Subject: [PATCH 4/5] feat: add chinese character test Signed-off-by: bassg0navy --- pkg/registry/chart_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/registry/chart_test.go b/pkg/registry/chart_test.go index d195f9a0a..071cb513f 100644 --- a/pkg/registry/chart_test.go +++ b/pkg/registry/chart_test.go @@ -329,6 +329,11 @@ func TestEscapeNonASCII(t *testing.T) { input: "Abc Déf Ghï", expected: "Abc D\\u00e9f Gh\\u00ef", }, + { + name: "Chinese characters", + input: "张伟", + expected: "\\u5f20\\u4f1f", + }, } for _, tt := range tests { From f165de5faba082af4de03602d48d2b52b92d7c62 Mon Sep 17 00:00:00 2001 From: bassg0navy Date: Sun, 30 Nov 2025 07:42:06 -0600 Subject: [PATCH 5/5] refactor: add newlines suggested by gofmt Signed-off-by: bassg0navy --- pkg/registry/chart.go | 2 +- pkg/registry/chart_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/registry/chart.go b/pkg/registry/chart.go index 634fbfe12..d188c3f52 100644 --- a/pkg/registry/chart.go +++ b/pkg/registry/chart.go @@ -83,7 +83,7 @@ func escapeNonASCII(s string) string { } return result.String() } - + // generateChartOCIAnnotations will generate OCI annotations from the provided chart func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { chartOCIAnnotations := map[string]string{} diff --git a/pkg/registry/chart_test.go b/pkg/registry/chart_test.go index 071cb513f..e15508557 100644 --- a/pkg/registry/chart_test.go +++ b/pkg/registry/chart_test.go @@ -411,4 +411,4 @@ func TestGenerateChartOCIAnnotations_ASCIIOnly(t *testing.T) { if annotations["org.opencontainers.image.authors"] != "John Smith (john@example.com)" { t.Errorf("Authors should be unchanged for ASCII-only input") } -} \ No newline at end of file +}