diff --git a/pkg/registry/chart.go b/pkg/registry/chart.go index b00fc616d..d188c3f52 100644 --- a/pkg/registry/chart.go +++ b/pkg/registry/chart.go @@ -18,8 +18,10 @@ package registry // import "helm.sh/helm/v4/pkg/registry" import ( "bytes" + "fmt" "strings" "time" + "unicode" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" @@ -65,14 +67,31 @@ 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 + + for _, r := range s { + if r > unicode.MaxASCII { + // Escape non-ASCII using standard unicode escaping + fmt.Fprintf(&result, "\\u%04x", r) + } else { + result.WriteRune(r) + } + } + 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{} - 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 +100,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 +109,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..e15508557 100644 --- a/pkg/registry/chart_test.go +++ b/pkg/registry/chart_test.go @@ -272,3 +272,143 @@ 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", + }, + { + name: "Chinese characters", + input: "张伟", + expected: "\\u5f20\\u4f1f", + }, + } + + 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") + } +}