fix(registry): hint on non-ASCII annotation mismatch

Signed-off-by: kkh <kkhdevs@gmail.com>
pull/31701/head
kkh 9 months ago
parent 9dd3f87e02
commit 4f8b820f0f

@ -18,11 +18,8 @@ package registry // import "helm.sh/helm/v4/pkg/registry"
import ( import (
"bytes" "bytes"
"strconv"
"strings" "strings"
"time" "time"
"unicode/utf16"
"unicode/utf8"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/chart/v2/loader"
@ -62,7 +59,7 @@ annotations:
} }
// Add chart annotation // Add chart annotation
ociAnnotations[chartAnnotationKey] = escapeNonASCII(chartAnnotationValue) ociAnnotations[chartAnnotationKey] = chartAnnotationValue
} }
return ociAnnotations return ociAnnotations
@ -120,44 +117,8 @@ func addToMap(inputMap map[string]string, newKey string, newValue string) map[st
// Add item to map if its // Add item to map if its
if len(strings.TrimSpace(newValue)) > 0 { if len(strings.TrimSpace(newValue)) > 0 {
inputMap[newKey] = escapeNonASCII(newValue) inputMap[newKey] = newValue
} }
return inputMap return inputMap
} }
func escapeNonASCII(value string) string {
if value == "" {
return value
}
var escaped strings.Builder
escaped.Grow(len(value))
for _, r := range value {
if r < utf8.RuneSelf {
escaped.WriteRune(r)
continue
}
if r <= 0xFFFF {
writeEscapedRune(&escaped, r)
continue
}
high, low := utf16.EncodeRune(r)
writeEscapedRune(&escaped, high)
writeEscapedRune(&escaped, low)
}
return escaped.String()
}
func writeEscapedRune(builder *strings.Builder, r rune) {
builder.WriteString("\\u")
hex := strconv.FormatInt(int64(r), 16)
for i := len(hex); i < 4; i++ {
builder.WriteByte('0')
}
builder.WriteString(hex)
}

@ -63,20 +63,6 @@ func TestGenerateOCIChartAnnotations(t *testing.T) {
"org.opencontainers.image.url": "https://helm.sh", "org.opencontainers.image.url": "https://helm.sh",
}, },
}, },
{
"Chart values with non-ASCII",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Description: "OCI Helm Chart for Kr\u00f6pke",
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.created": nowString,
"org.opencontainers.image.description": "OCI Helm Chart for Kr\\u00f6pke",
},
},
{ {
"Maintainer without email", "Maintainer without email",
&chart.Metadata{ &chart.Metadata{
@ -212,22 +198,6 @@ func TestGenerateOCIAnnotations(t *testing.T) {
"anotherkey": "anothervalue", "anotherkey": "anothervalue",
}, },
}, },
{
"Custom annotations with non-ASCII values",
&chart.Metadata{
Name: "oci",
Version: "0.0.1",
Annotations: map[string]string{
"extrakey": "Kr\u00f6pke",
},
},
map[string]string{
"org.opencontainers.image.title": "oci",
"org.opencontainers.image.version": "0.0.1",
"org.opencontainers.image.created": nowString,
"extrakey": "Kr\\u00f6pke",
},
},
{ {
"Verify Chart Name and Version cannot be overridden from annotations", "Verify Chart Name and Version cannot be overridden from annotations",
&chart.Metadata{ &chart.Metadata{
@ -261,36 +231,6 @@ func TestGenerateOCIAnnotations(t *testing.T) {
} }
} }
func TestEscapeNonASCII(t *testing.T) {
tests := []struct {
name string
input string
expect string
}{
{
name: "ASCII only",
input: "alpha-._:@/+ 123",
expect: "alpha-._:@/+ 123",
},
{
name: "Latin-1 characters",
input: "Kr\u00f6pke",
expect: "Kr\\u00f6pke",
},
{
name: "Emoji",
input: "chart \U0001f600",
expect: "chart \\ud83d\\ude00",
},
}
for _, tt := range tests {
if got := escapeNonASCII(tt.input); got != tt.expect {
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
}
}
}
func TestGenerateOCICreatedAnnotations(t *testing.T) { func TestGenerateOCICreatedAnnotations(t *testing.T) {
nowTime := time.Now() nowTime := time.Now()

@ -30,11 +30,13 @@ import (
"os" "os"
"sort" "sort"
"strings" "strings"
"unicode/utf8"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/opencontainers/image-spec/specs-go" "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2" "oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote"
@ -715,6 +717,9 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
manifestDescriptor, err = oras.ExtendedCopy(ctx, memoryStore, parsedRef.String(), repository, parsedRef.String(), oras.DefaultExtendedCopyOptions) manifestDescriptor, err = oras.ExtendedCopy(ctx, memoryStore, parsedRef.String(), repository, parsedRef.String(), oras.DefaultExtendedCopyOptions)
if err != nil { if err != nil {
if hasNonASCIIAnnotationValues(ociAnnotations) && errors.Is(err, content.ErrMismatchedDigest) {
return nil, fmt.Errorf("manifest digest mismatch while pushing; the registry may be rewriting non-ASCII OCI annotation values. Consider using ASCII-only metadata/annotations or a registry that preserves annotations: %w", err)
}
return nil, err return nil, err
} }
@ -752,6 +757,24 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
return result, err return result, err
} }
func hasNonASCIIAnnotationValues(annotations map[string]string) bool {
for key, value := range annotations {
if containsNonASCII(key) || containsNonASCII(value) {
return true
}
}
return false
}
func containsNonASCII(value string) bool {
for i := 0; i < len(value); i++ {
if value[i] >= utf8.RuneSelf {
return true
}
}
return false
}
// PushOptProvData returns a function that sets the prov bytes setting on push // PushOptProvData returns a function that sets the prov bytes setting on push
func PushOptProvData(provData []byte) PushOption { func PushOptProvData(provData []byte) PushOption {
return func(operation *pushOperation) { return func(operation *pushOperation) {

@ -166,3 +166,54 @@ func TestWarnIfHostHasPath(t *testing.T) {
}) })
} }
} }
func TestHasNonASCIIAnnotationValues(t *testing.T) {
t.Parallel()
tests := []struct {
name string
annotations map[string]string
want bool
}{
{
name: "ascii only",
annotations: map[string]string{
"org.opencontainers.image.title": "chart",
"custom": "alpha-._:@/+ 123",
},
want: false,
},
{
name: "non-ascii value",
annotations: map[string]string{
"org.opencontainers.image.description": "Kröpke",
},
want: true,
},
{
name: "non-ascii key",
annotations: map[string]string{
"\uC124\uBA85": "chart",
},
want: true,
},
{
name: "emoji value",
annotations: map[string]string{
"note": "chart \U0001F600",
},
want: true,
},
{
name: "empty map",
annotations: map[string]string{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, hasNonASCIIAnnotationValues(tt.annotations))
})
}
}

Loading…
Cancel
Save