diff --git a/pack.go b/pack.go index cd1c88b6..8f1533fa 100644 --- a/pack.go +++ b/pack.go @@ -101,6 +101,12 @@ type PackManifestOptions struct { // ConfigDescriptor is a pointer to the descriptor of the config blob. // If not nil, ConfigAnnotations will be ignored. + // + // When set, the caller is responsible for pushing the referenced config + // blob to the target and for populating the descriptor's Digest and Size. + // PackManifest uses the descriptor as-is and does NOT push the config blob. + // The descriptor's Digest MUST be valid, otherwise PackManifest returns an + // error wrapping [errdef.ErrInvalidDigest]. ConfigDescriptor *ocispec.Descriptor // ConfigAnnotations is the annotation map of the config descriptor. @@ -225,8 +231,8 @@ func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType s // prepare config var configDesc ocispec.Descriptor if opts.ConfigDescriptor != nil { - if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil { - return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err) + if err := validateConfigDescriptor(*opts.ConfigDescriptor); err != nil { + return ocispec.Descriptor{}, err } configDesc = *opts.ConfigDescriptor } else { @@ -272,6 +278,9 @@ func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMedi // prepare config var configDesc ocispec.Descriptor if opts.ConfigDescriptor != nil { + if err := validateConfigDescriptor(*opts.ConfigDescriptor); err != nil { + return ocispec.Descriptor{}, err + } configDesc = *opts.ConfigDescriptor } else { var err error @@ -318,8 +327,8 @@ func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType s var emptyBlobExists bool var configDesc ocispec.Descriptor if opts.ConfigDescriptor != nil { - if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil { - return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err) + if err := validateConfigDescriptor(*opts.ConfigDescriptor); err != nil { + return ocispec.Descriptor{}, err } configDesc = *opts.ConfigDescriptor } else { @@ -446,3 +455,19 @@ func validateMediaType(mediaType string) error { } return nil } + +// validateConfigDescriptor validates a caller-supplied config descriptor. +// When a config descriptor is provided to Pack or PackManifest, the caller is +// responsible for pushing the config blob and populating the descriptor's +// Digest and Size. Validating the descriptor here prevents silently producing +// a manifest that references a non-existent config blob (e.g. an empty digest), +// which some registries accept while dropping the descriptor's annotations. +func validateConfigDescriptor(config ocispec.Descriptor) error { + if err := validateMediaType(config.MediaType); err != nil { + return fmt.Errorf("invalid config mediaType format: %w", err) + } + if err := config.Digest.Validate(); err != nil { + return fmt.Errorf("invalid config descriptor digest %q: %w", config.Digest, errdef.ErrInvalidDigest) + } + return nil +} diff --git a/pack_test.go b/pack_test.go index 11780025..eb7e8aea 100644 --- a/pack_test.go +++ b/pack_test.go @@ -523,6 +523,27 @@ func Test_Pack_ImageV1_1_RC2_InvalidDateTimeFormat(t *testing.T) { } } +func Test_Pack_ImageV1_1_RC2_InvalidConfigDigest(t *testing.T) { + s := memory.New() + + ctx := context.Background() + // a config descriptor with a valid media type but an empty (invalid) digest, + // as produced when the caller forgets to push the config blob and populate + // the descriptor. See https://github.com/oras-project/oras-go/issues/1236 + configDesc := ocispec.Descriptor{ + MediaType: "application/vnd.test.config", + Annotations: map[string]string{"foo": "bar"}, + } + opts := PackOptions{ + PackImageManifest: true, + ConfigDescriptor: &configDesc, + } + _, err := Pack(ctx, s, "", nil, opts) + if wantErr := errdef.ErrInvalidDigest; !errors.Is(err, wantErr) { + t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr) + } +} + func Test_PackManifest_ImageV1_0(t *testing.T) { s := memory.New() @@ -786,6 +807,26 @@ func Test_PackManifest_ImageV1_0_InvalidMediaType(t *testing.T) { } } +func Test_PackManifest_ImageV1_0_InvalidConfigDigest(t *testing.T) { + s := memory.New() + + ctx := context.Background() + // a config descriptor with a valid media type but an empty (invalid) digest, + // as produced when the caller forgets to push the config blob and populate + // the descriptor. See https://github.com/oras-project/oras-go/issues/1236 + configDesc := ocispec.Descriptor{ + MediaType: "application/vnd.test.config", + Annotations: map[string]string{"foo": "bar"}, + } + opts := PackManifestOptions{ + ConfigDescriptor: &configDesc, + } + _, err := PackManifest(ctx, s, PackManifestVersion1_0, "", opts) + if wantErr := errdef.ErrInvalidDigest; !errors.Is(err, wantErr) { + t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) + } +} + func Test_PackManifest_ImageV1_0_InvalidDateTimeFormat(t *testing.T) { s := memory.New() @@ -1065,6 +1106,26 @@ func Test_PackManifest_ImageV1_1_InvalidMediaType(t *testing.T) { } } +func Test_PackManifest_ImageV1_1_InvalidConfigDigest(t *testing.T) { + s := memory.New() + + ctx := context.Background() + // a config descriptor with a valid media type but an empty (invalid) digest, + // as produced when the caller forgets to push the config blob and populate + // the descriptor. See https://github.com/oras-project/oras-go/issues/1236 + configDesc := ocispec.Descriptor{ + MediaType: "application/vnd.test.config", + Annotations: map[string]string{"foo": "bar"}, + } + opts := PackManifestOptions{ + ConfigDescriptor: &configDesc, + } + _, err := PackManifest(ctx, s, PackManifestVersion1_1, "application/vnd.test", opts) + if wantErr := errdef.ErrInvalidDigest; !errors.Is(err, wantErr) { + t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) + } +} + func Test_PackManifest_ImageV1_1_InvalidDateTimeFormat(t *testing.T) { s := memory.New()