diff --git a/pkg/registry/client.go b/pkg/registry/client.go index f2bfd13b4..d6a12d674 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -30,11 +30,13 @@ import ( "os" "sort" "strings" + "unicode/utf8" "github.com/Masterminds/semver/v3" "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" @@ -719,6 +721,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) 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 } @@ -756,6 +761,24 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu 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 func PushOptProvData(provData []byte) PushOption { return func(operation *pushOperation) { diff --git a/pkg/registry/client_test.go b/pkg/registry/client_test.go index 702dfff69..da915a941 100644 --- a/pkg/registry/client_test.go +++ b/pkg/registry/client_test.go @@ -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)) + }) + } +}