From 47d028a52a21a2da888aa522543e99920d3f3ef7 Mon Sep 17 00:00:00 2001 From: "Lixia (Sylvia) Lei" Date: Thu, 24 Aug 2023 19:12:07 +0800 Subject: [PATCH] feat: validate input media type for `oras.PackManifest ` (#574) Resolves: #571 Signed-off-by: Lixia (Sylvia) Lei --- errdef/errors.go | 1 + example_test.go | 4 +- pack.go | 85 +++++++--- pack_test.go | 410 +++++++++++++++++++++++++++-------------------- 4 files changed, 298 insertions(+), 202 deletions(-) diff --git a/errdef/errors.go b/errdef/errors.go index 030360ed..7adb44b1 100644 --- a/errdef/errors.go +++ b/errdef/errors.go @@ -22,6 +22,7 @@ var ( ErrAlreadyExists = errors.New("already exists") ErrInvalidDigest = errors.New("invalid digest") ErrInvalidReference = errors.New("invalid reference") + ErrInvalidMediaType = errors.New("invalid media type") ErrMissingReference = errors.New("missing reference") ErrNotFound = errors.New("not found") ErrSizeExceedsLimit = errors.New("size exceeds limit") diff --git a/example_test.go b/example_test.go index aa7ef53b..4da0f8d5 100644 --- a/example_test.go +++ b/example_test.go @@ -111,7 +111,7 @@ func Example_pushFilesToRemoteRepository() { ctx := context.Background() // 1. Add files to the file store - mediaType := "example/file" + mediaType := "application/vnd.test.file" fileNames := []string{"/tmp/myfile"} fileDescriptors := make([]v1.Descriptor, 0, len(fileNames)) for _, name := range fileNames { @@ -124,7 +124,7 @@ func Example_pushFilesToRemoteRepository() { } // 2. Pack the files and tag the packed manifest - artifactType := "example/files" + artifactType := "application/vnd.test.artifact" opts := oras.PackManifestOptions{ Layers: fileDescriptors, } diff --git a/pack.go b/pack.go index a852bb4b..08e14e19 100644 --- a/pack.go +++ b/pack.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "regexp" "time" specs "github.com/opencontainers/image-spec/specs-go" @@ -95,6 +96,12 @@ type PackManifestOptions struct { ConfigAnnotations map[string]string } +// mediaTypeRegexp checks the format of media types. +// References: +// - https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/schema/defs-descriptor.json#L7 +// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 +var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`) + // PackManifest generates an OCI Image Manifest based on the given parameters // and pushes the packed manifest to a content storage using pusher. The version // of the manifest to be packed is determined by packManifestVersion @@ -108,6 +115,8 @@ type PackManifestOptions struct { // "application/vnd.unknown.config.v1+json" will be used. // if opts.ConfigDescriptor is NOT nil, artifactType will be ignored. // +// artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838. +// // If succeeded, returns a descriptor of the packed manifest. func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { switch packManifestVersion { @@ -190,28 +199,28 @@ func packArtifact(ctx context.Context, pusher content.Pusher, artifactType strin // packManifestV1_0 packs an image manifest defined in image-spec v1.0.2. // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md -func packManifestV1_0(ctx context.Context, pusher content.Pusher, configMediaType string, opts PackManifestOptions) (ocispec.Descriptor, error) { +func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { if opts.Subject != nil { return ocispec.Descriptor{}, fmt.Errorf("subject is not supported for manifest version %v: %w", PackManifestVersion1_0, errdef.ErrUnsupported) } - if configMediaType == "" { - configMediaType = MediaTypeUnknownConfig - } + // 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) + } configDesc = *opts.ConfigDescriptor } else { - // Use an empty JSON object here, because some registries may not accept - // empty config blob. - // As of September 2022, GAR is known to return 400 on empty blob upload. - // See https://github.com/oras-project/oras-go/issues/294 for details. - configBytes := []byte("{}") - configDesc = content.NewDescriptorFromBytes(configMediaType, configBytes) - configDesc.Annotations = opts.ConfigAnnotations - // push config - if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { - return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) + if artifactType == "" { + artifactType = MediaTypeUnknownConfig + } else if err := validateMediaType(artifactType); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err) + } + var err error + configDesc, err = pushCustomEmptyConfig(ctx, pusher, artifactType, opts.ConfigAnnotations) + if err != nil { + return ocispec.Descriptor{}, err } } @@ -242,20 +251,15 @@ func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMedi configMediaType = MediaTypeUnknownConfig } + // prepare config var configDesc ocispec.Descriptor if opts.ConfigDescriptor != nil { configDesc = *opts.ConfigDescriptor } else { - // Use an empty JSON object here, because some registries may not accept - // empty config blob. - // As of September 2022, GAR is known to return 400 on empty blob upload. - // See https://github.com/oras-project/oras-go/issues/294 for details. - configBytes := []byte("{}") - configDesc = content.NewDescriptorFromBytes(configMediaType, configBytes) - configDesc.Annotations = opts.ConfigAnnotations - // push config - if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { - return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) + var err error + configDesc, err = pushCustomEmptyConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations) + if err != nil { + return ocispec.Descriptor{}, err } } @@ -286,10 +290,19 @@ func packManifestV1_1_RC4(ctx context.Context, pusher content.Pusher, artifactTy // artifactType MUST be set when config.mediaType is set to the empty value return ocispec.Descriptor{}, ErrMissingArtifactType } + if artifactType != "" { + if err := validateMediaType(artifactType); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err) + } + } + // prepare config 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) + } configDesc = *opts.ConfigDescriptor } else { // use the empty descriptor for config @@ -369,6 +382,22 @@ func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, medi return manifestDesc, nil } +// pushCustomEmptyConfig generates and pushes an empty config blob. +func pushCustomEmptyConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) { + // Use an empty JSON object here, because some registries may not accept + // empty config blob. + // As of September 2022, GAR is known to return 400 on empty blob upload. + // See https://github.com/oras-project/oras-go/issues/294 for details. + configBytes := []byte("{}") + configDesc := content.NewDescriptorFromBytes(mediaType, configBytes) + configDesc.Annotations = annotations + // push config + if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) + } + return configDesc, nil +} + // ensureAnnotationCreated ensures that annotationCreatedKey is in annotations, // and that its value conforms to RFC 3339. Otherwise returns a new annotation // map with annotationCreatedKey created. @@ -392,3 +421,11 @@ func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey copied[annotationCreatedKey] = now.Format(time.RFC3339) return copied, nil } + +// validateMediaType validates the format of mediaType. +func validateMediaType(mediaType string) error { + if !mediaTypeRegexp.MatchString(mediaType) { + return fmt.Errorf("%s: %w", mediaType, errdef.ErrInvalidMediaType) + } + return nil +} diff --git a/pack_test.go b/pack_test.go index 7faf80fa..0f8bc04b 100644 --- a/pack_test.go +++ b/pack_test.go @@ -121,7 +121,7 @@ func Test_Pack_Artifact_WithOptions(t *testing.T) { Annotations: annotations, } configBytes := []byte("{}") - configDesc := content.NewDescriptorFromBytes("testconfig", configBytes) + configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes) configAnnotations := map[string]string{"foo": "bar"} // test Pack @@ -262,7 +262,7 @@ func Test_Pack_ImageV1_1_RC2(t *testing.T) { // test Pack ctx := context.Background() - artifactType := "testconfig" + artifactType := "application/vnd.test" manifestDesc, err := Pack(ctx, s, artifactType, layers, PackOptions{PackImageManifest: true}) if err != nil { t.Fatal("Oras.Pack() error =", err) @@ -327,7 +327,7 @@ func Test_Pack_ImageV1_1_RC2_WithOptions(t *testing.T) { content.NewDescriptorFromBytes("test", []byte("goodbye world")), } configBytes := []byte("{}") - configDesc := content.NewDescriptorFromBytes("testconfig", configBytes) + configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes) configAnnotations := map[string]string{"foo": "bar"} annotations := map[string]string{ ocispec.AnnotationCreated: "2000-01-01T00:00:00Z", @@ -523,12 +523,13 @@ func Test_Pack_ImageV1_1_RC2_InvalidDateTimeFormat(t *testing.T) { } } -func Test_PackManifest_ImageV1_1_RC4(t *testing.T) { +func Test_PackManifest_ImageV1_0(t *testing.T) { s := memory.New() - // test PackManifest + // test Pack ctx := context.Background() - manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, "test", PackManifestOptions{}) + artifactType := "application/vnd.test" + manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, PackManifestOptions{}) if err != nil { t.Fatal("Oras.PackManifest() error =", err) } @@ -545,14 +546,46 @@ func Test_PackManifest_ImageV1_1_RC4(t *testing.T) { t.Fatal("Store.Fetch().Close() error =", err) } + // verify media type + got := manifest.MediaType + if got != ocispec.MediaTypeImageManifest { + t.Fatalf("got media type = %s, want %s", got, ocispec.MediaTypeImageManifest) + } + + // verify config + expectedConfigBytes := []byte("{}") + expectedConfig := ocispec.Descriptor{ + MediaType: artifactType, + Digest: digest.FromBytes(expectedConfigBytes), + Size: int64(len(expectedConfigBytes)), + } + if !reflect.DeepEqual(manifest.Config, expectedConfig) { + t.Errorf("got config = %v, want %v", manifest.Config, expectedConfig) + } + // verify layers - expectedLayers := []ocispec.Descriptor{ocispec.DescriptorEmptyJSON} + expectedLayers := []ocispec.Descriptor{} if !reflect.DeepEqual(manifest.Layers, expectedLayers) { t.Errorf("got layers = %v, want %v", manifest.Layers, expectedLayers) } + + // verify created time annotation + createdTime, ok := manifest.Annotations[ocispec.AnnotationCreated] + if !ok { + t.Errorf("Annotation %s = %v, want %v", ocispec.AnnotationCreated, ok, true) + } + _, err = time.Parse(time.RFC3339, createdTime) + if err != nil { + t.Errorf("error parsing created time: %s, error = %v", createdTime, err) + } + + // verify descriptor annotations + if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) { + t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want) + } } -func Test_PackManifest_ImageV1_1_RC4_WithOptions(t *testing.T) { +func Test_PackManifest_ImageV1_0_WithOptions(t *testing.T) { s := memory.New() // prepare test content @@ -560,31 +593,24 @@ func Test_PackManifest_ImageV1_1_RC4_WithOptions(t *testing.T) { content.NewDescriptorFromBytes("test", []byte("hello world")), content.NewDescriptorFromBytes("test", []byte("goodbye world")), } - configBytes := []byte("config") - configDesc := content.NewDescriptorFromBytes("testconfig", configBytes) + configBytes := []byte("{}") + configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes) configAnnotations := map[string]string{"foo": "bar"} annotations := map[string]string{ ocispec.AnnotationCreated: "2000-01-01T00:00:00Z", "foo": "bar", } artifactType := "application/vnd.test" - subjectManifest := []byte(`{"layers":[]}`) - subjectDesc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageManifest, - Digest: digest.FromBytes(subjectManifest), - Size: int64(len(subjectManifest)), - } // test PackManifest with ConfigDescriptor ctx := context.Background() opts := PackManifestOptions{ - Subject: &subjectDesc, Layers: layers, ConfigDescriptor: &configDesc, ConfigAnnotations: configAnnotations, ManifestAnnotations: annotations, } - manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, opts) + manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) if err != nil { t.Fatal("Oras.PackManifest() error =", err) } @@ -593,12 +619,10 @@ func Test_PackManifest_ImageV1_1_RC4_WithOptions(t *testing.T) { Versioned: specs.Versioned{ SchemaVersion: 2, // historical value. does not pertain to OCI or docker version }, - MediaType: ocispec.MediaTypeImageManifest, - ArtifactType: artifactType, - Subject: &subjectDesc, - Config: configDesc, - Layers: layers, - Annotations: annotations, + MediaType: ocispec.MediaTypeImageManifest, + Config: configDesc, + Layers: layers, + Annotations: annotations, } expectedManifestBytes, err := json.Marshal(expectedManifest) if err != nil { @@ -623,32 +647,31 @@ func Test_PackManifest_ImageV1_1_RC4_WithOptions(t *testing.T) { // verify descriptor expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) - expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType expectedManifestDesc.Annotations = expectedManifest.Annotations if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { - t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc) + t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) } - // test PackManifest with ConfigDescriptor, but without artifactType + // test PackManifest without ConfigDescriptor opts = PackManifestOptions{ - Subject: &subjectDesc, Layers: layers, - ConfigDescriptor: &configDesc, ConfigAnnotations: configAnnotations, ManifestAnnotations: annotations, } - manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_1_RC4, "", opts) + manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) if err != nil { t.Fatal("Oras.PackManifest() error =", err) } + expectedConfigDesc := content.NewDescriptorFromBytes(artifactType, configBytes) + expectedConfigDesc.Annotations = configAnnotations expectedManifest = ocispec.Manifest{ Versioned: specs.Versioned{ SchemaVersion: 2, // historical value. does not pertain to OCI or docker version }, MediaType: ocispec.MediaTypeImageManifest, - Subject: &subjectDesc, - Config: configDesc, + Config: expectedConfigDesc, Layers: layers, Annotations: annotations, } @@ -675,90 +698,95 @@ func Test_PackManifest_ImageV1_1_RC4_WithOptions(t *testing.T) { // verify descriptor expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) - expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType expectedManifestDesc.Annotations = expectedManifest.Annotations if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc) } +} - // test Pack without ConfigDescriptor - opts = PackManifestOptions{ - Subject: &subjectDesc, - Layers: layers, - ConfigAnnotations: configAnnotations, - ManifestAnnotations: annotations, - } - manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, opts) - if err != nil { - t.Fatal("Oras.PackManifest() error =", err) +func Test_PackManifest_ImageV1_0_SubjectUnsupported(t *testing.T) { + s := memory.New() + + // prepare test content + artifactType := "application/vnd.test" + subjectManifest := []byte(`{"layers":[]}`) + subjectDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(subjectManifest), + Size: int64(len(subjectManifest)), } - expectedConfigDesc := ocispec.DescriptorEmptyJSON - expectedConfigDesc.Annotations = configAnnotations - expectedManifest = ocispec.Manifest{ - Versioned: specs.Versioned{ - SchemaVersion: 2, // historical value. does not pertain to OCI or docker version - }, - MediaType: ocispec.MediaTypeImageManifest, - ArtifactType: artifactType, - Subject: &subjectDesc, - Config: expectedConfigDesc, - Layers: layers, - Annotations: annotations, + // test Pack with ConfigDescriptor + ctx := context.Background() + opts := PackManifestOptions{ + Subject: &subjectDesc, } - expectedManifestBytes, err = json.Marshal(expectedManifest) - if err != nil { - t.Fatal("failed to marshal manifest:", err) + _, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) + if wantErr := errdef.ErrUnsupported; !errors.Is(err, wantErr) { + t.Errorf("Oras.PackManifest() error = %v, wantErr %v", err, wantErr) } +} - rc, err = s.Fetch(ctx, manifestDesc) +func Test_PackManifest_ImageV1_0_NoArtifactType(t *testing.T) { + s := memory.New() + + ctx := context.Background() + manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, "", PackManifestOptions{}) if err != nil { - t.Fatal("Store.Fetch() error =", err) + t.Fatal("Oras.PackManifest() error =", err) } - got, err = io.ReadAll(rc) + + var manifest ocispec.Manifest + rc, err := s.Fetch(ctx, manifestDesc) if err != nil { - t.Fatal("Store.Fetch().Read() error =", err) + t.Fatal("Store.Fetch() error =", err) } - err = rc.Close() - if err != nil { - t.Error("Store.Fetch().Close() error =", err) + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + t.Fatal("error decoding manifest, error =", err) } - if !bytes.Equal(got, expectedManifestBytes) { - t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes)) + if err := rc.Close(); err != nil { + t.Fatal("Store.Fetch().Close() error =", err) } - // verify descriptor - expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) - expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType - expectedManifestDesc.Annotations = expectedManifest.Annotations - if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { - t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc) + // verify artifact type and config media type + if manifestDesc.ArtifactType != MediaTypeUnknownConfig { + t.Fatalf("got artifact type = %s, want %s", manifestDesc.ArtifactType, MediaTypeUnknownConfig) + } + if manifest.Config.MediaType != MediaTypeUnknownConfig { + t.Fatalf("got artifact type = %s, want %s", manifest.Config.MediaType, MediaTypeUnknownConfig) } } -func Test_PackManifest_ImageV1_1_RC4_NoArtifactType(t *testing.T) { +func Test_PackManifest_ImageV1_0_InvalidMediaType(t *testing.T) { s := memory.New() ctx := context.Background() - // test no artifact type and no config - _, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, "", PackManifestOptions{}) - if wantErr := ErrMissingArtifactType; !errors.Is(err, wantErr) { - t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) + // test invalid artifact type + valid config media type + artifactType := "random" + configBytes := []byte("{}") + configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes) + opts := PackManifestOptions{ + ConfigDescriptor: &configDesc, + } + _, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) + if err != nil { + t.Error("Oras.PackManifest() error =", err) } - // test no artifact type and config with media type empty - opts := PackManifestOptions{ - ConfigDescriptor: &ocispec.Descriptor{ - MediaType: ocispec.DescriptorEmptyJSON.MediaType, - }, + // test invalid config media type + valid artifact type + artifactType = "application/vnd.test" + configDesc = content.NewDescriptorFromBytes("random", configBytes) + opts = PackManifestOptions{ + ConfigDescriptor: &configDesc, } - _, err = PackManifest(ctx, s, PackManifestVersion1_1_RC4, "", opts) - if wantErr := ErrMissingArtifactType; !errors.Is(err, wantErr) { + _, err = PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) + if wantErr := errdef.ErrInvalidMediaType; !errors.Is(err, wantErr) { t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) } } -func Test_PackManifest_ImageV1_1_RC4_InvalidDateTimeFormat(t *testing.T) { +func Test_PackManifest_ImageV1_0_InvalidDateTimeFormat(t *testing.T) { s := memory.New() ctx := context.Background() @@ -767,19 +795,19 @@ func Test_PackManifest_ImageV1_1_RC4_InvalidDateTimeFormat(t *testing.T) { ocispec.AnnotationCreated: "2000/01/01 00:00:00", }, } - _, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, "test", opts) + _, err := PackManifest(ctx, s, PackManifestVersion1_0, "", opts) if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) { t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) } } -func Test_PackManifest_ImageV1_0(t *testing.T) { +func Test_PackManifest_ImageV1_1_RC4(t *testing.T) { s := memory.New() - // test Pack + // test PackManifest ctx := context.Background() - artifactType := "testconfig" - manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, PackManifestOptions{}) + artifactType := "application/vnd.test" + manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, PackManifestOptions{}) if err != nil { t.Fatal("Oras.PackManifest() error =", err) } @@ -796,46 +824,14 @@ func Test_PackManifest_ImageV1_0(t *testing.T) { t.Fatal("Store.Fetch().Close() error =", err) } - // verify media type - got := manifest.MediaType - if got != ocispec.MediaTypeImageManifest { - t.Fatalf("got media type = %s, want %s", got, ocispec.MediaTypeImageManifest) - } - - // verify config - expectedConfigBytes := []byte("{}") - expectedConfig := ocispec.Descriptor{ - MediaType: artifactType, - Digest: digest.FromBytes(expectedConfigBytes), - Size: int64(len(expectedConfigBytes)), - } - if !reflect.DeepEqual(manifest.Config, expectedConfig) { - t.Errorf("got config = %v, want %v", manifest.Config, expectedConfig) - } - // verify layers - expectedLayers := []ocispec.Descriptor{} + expectedLayers := []ocispec.Descriptor{ocispec.DescriptorEmptyJSON} if !reflect.DeepEqual(manifest.Layers, expectedLayers) { t.Errorf("got layers = %v, want %v", manifest.Layers, expectedLayers) } - - // verify created time annotation - createdTime, ok := manifest.Annotations[ocispec.AnnotationCreated] - if !ok { - t.Errorf("Annotation %s = %v, want %v", ocispec.AnnotationCreated, ok, true) - } - _, err = time.Parse(time.RFC3339, createdTime) - if err != nil { - t.Errorf("error parsing created time: %s, error = %v", createdTime, err) - } - - // verify descriptor annotations - if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) { - t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want) - } } -func Test_PackManifest_ImageV1_0_WithOptions(t *testing.T) { +func Test_PackManifest_ImageV1_1_RC4_WithOptions(t *testing.T) { s := memory.New() // prepare test content @@ -843,24 +839,31 @@ func Test_PackManifest_ImageV1_0_WithOptions(t *testing.T) { content.NewDescriptorFromBytes("test", []byte("hello world")), content.NewDescriptorFromBytes("test", []byte("goodbye world")), } - configBytes := []byte("{}") - configDesc := content.NewDescriptorFromBytes("testconfig", configBytes) + configBytes := []byte("config") + configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes) configAnnotations := map[string]string{"foo": "bar"} annotations := map[string]string{ ocispec.AnnotationCreated: "2000-01-01T00:00:00Z", "foo": "bar", } artifactType := "application/vnd.test" + subjectManifest := []byte(`{"layers":[]}`) + subjectDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(subjectManifest), + Size: int64(len(subjectManifest)), + } // test PackManifest with ConfigDescriptor ctx := context.Background() opts := PackManifestOptions{ + Subject: &subjectDesc, Layers: layers, ConfigDescriptor: &configDesc, ConfigAnnotations: configAnnotations, ManifestAnnotations: annotations, } - manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) + manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, opts) if err != nil { t.Fatal("Oras.PackManifest() error =", err) } @@ -869,10 +872,12 @@ func Test_PackManifest_ImageV1_0_WithOptions(t *testing.T) { Versioned: specs.Versioned{ SchemaVersion: 2, // historical value. does not pertain to OCI or docker version }, - MediaType: ocispec.MediaTypeImageManifest, - Config: configDesc, - Layers: layers, - Annotations: annotations, + MediaType: ocispec.MediaTypeImageManifest, + ArtifactType: artifactType, + Subject: &subjectDesc, + Config: configDesc, + Layers: layers, + Annotations: annotations, } expectedManifestBytes, err := json.Marshal(expectedManifest) if err != nil { @@ -897,31 +902,32 @@ func Test_PackManifest_ImageV1_0_WithOptions(t *testing.T) { // verify descriptor expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType + expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType expectedManifestDesc.Annotations = expectedManifest.Annotations if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { - t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) + t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc) } - // test PackManifest without ConfigDescriptor + // test PackManifest with ConfigDescriptor, but without artifactType opts = PackManifestOptions{ + Subject: &subjectDesc, Layers: layers, + ConfigDescriptor: &configDesc, ConfigAnnotations: configAnnotations, ManifestAnnotations: annotations, } - manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) + manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_1_RC4, "", opts) if err != nil { t.Fatal("Oras.PackManifest() error =", err) } - expectedConfigDesc := content.NewDescriptorFromBytes(artifactType, configBytes) - expectedConfigDesc.Annotations = configAnnotations expectedManifest = ocispec.Manifest{ Versioned: specs.Versioned{ SchemaVersion: 2, // historical value. does not pertain to OCI or docker version }, MediaType: ocispec.MediaTypeImageManifest, - Config: expectedConfigDesc, + Subject: &subjectDesc, + Config: configDesc, Layers: layers, Annotations: annotations, } @@ -948,67 +954,118 @@ func Test_PackManifest_ImageV1_0_WithOptions(t *testing.T) { // verify descriptor expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) - expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType + expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType expectedManifestDesc.Annotations = expectedManifest.Annotations if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc) } -} -func Test_PackManifest_ImageV1_0_SubjectUnsupported(t *testing.T) { - s := memory.New() + // test Pack without ConfigDescriptor + opts = PackManifestOptions{ + Subject: &subjectDesc, + Layers: layers, + ConfigAnnotations: configAnnotations, + ManifestAnnotations: annotations, + } + manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, opts) + if err != nil { + t.Fatal("Oras.PackManifest() error =", err) + } - // prepare test content - artifactType := "application/vnd.test" - subjectManifest := []byte(`{"layers":[]}`) - subjectDesc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageManifest, - Digest: digest.FromBytes(subjectManifest), - Size: int64(len(subjectManifest)), + expectedConfigDesc := ocispec.DescriptorEmptyJSON + expectedConfigDesc.Annotations = configAnnotations + expectedManifest = ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageManifest, + ArtifactType: artifactType, + Subject: &subjectDesc, + Config: expectedConfigDesc, + Layers: layers, + Annotations: annotations, + } + expectedManifestBytes, err = json.Marshal(expectedManifest) + if err != nil { + t.Fatal("failed to marshal manifest:", err) } - // test Pack with ConfigDescriptor - ctx := context.Background() - opts := PackManifestOptions{ - Subject: &subjectDesc, + rc, err = s.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Fetch() error =", err) } - _, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts) - if wantErr := errdef.ErrUnsupported; !errors.Is(err, wantErr) { - t.Errorf("Oras.PackManifest() error = %v, wantErr %v", err, wantErr) + got, err = io.ReadAll(rc) + if err != nil { + t.Fatal("Store.Fetch().Read() error =", err) + } + err = rc.Close() + if err != nil { + t.Error("Store.Fetch().Close() error =", err) + } + if !bytes.Equal(got, expectedManifestBytes) { + t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes)) + } + + // verify descriptor + expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) + expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType + expectedManifestDesc.Annotations = expectedManifest.Annotations + if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { + t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc) } } -func Test_PackManifest_ImageV1_0_NoArtifactType(t *testing.T) { +func Test_PackManifest_ImageV1_1_RC4_NoArtifactType(t *testing.T) { s := memory.New() ctx := context.Background() - manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, "", PackManifestOptions{}) - if err != nil { - t.Fatal("Oras.PackManifest() error =", err) + // test no artifact type and no config + _, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, "", PackManifestOptions{}) + if wantErr := ErrMissingArtifactType; !errors.Is(err, wantErr) { + t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) } - var manifest ocispec.Manifest - rc, err := s.Fetch(ctx, manifestDesc) - if err != nil { - t.Fatal("Store.Fetch() error =", err) + // test no artifact type and config with empty media type + opts := PackManifestOptions{ + ConfigDescriptor: &ocispec.Descriptor{ + MediaType: ocispec.DescriptorEmptyJSON.MediaType, + }, } - if err := json.NewDecoder(rc).Decode(&manifest); err != nil { - t.Fatal("error decoding manifest, error =", err) + _, err = PackManifest(ctx, s, PackManifestVersion1_1_RC4, "", opts) + if wantErr := ErrMissingArtifactType; !errors.Is(err, wantErr) { + t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) } - if err := rc.Close(); err != nil { - t.Fatal("Store.Fetch().Close() error =", err) +} + +func Test_PackManifest_ImageV1_1_RC4_InvalidMediaType(t *testing.T) { + s := memory.New() + + ctx := context.Background() + // test invalid artifact type + valid config media type + artifactType := "random" + configBytes := []byte("{}") + configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes) + opts := PackManifestOptions{ + ConfigDescriptor: &configDesc, + } + _, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, opts) + if wantErr := errdef.ErrInvalidMediaType; !errors.Is(err, wantErr) { + t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) } - // verify artifact type and config media type - if manifestDesc.ArtifactType != MediaTypeUnknownConfig { - t.Fatalf("got artifact type = %s, want %s", manifestDesc.ArtifactType, MediaTypeUnknownConfig) + // test invalid config media type + invalid artifact type + artifactType = "application/vnd.test" + configDesc = content.NewDescriptorFromBytes("random", configBytes) + opts = PackManifestOptions{ + ConfigDescriptor: &configDesc, } - if manifest.Config.MediaType != MediaTypeUnknownConfig { - t.Fatalf("got artifact type = %s, want %s", manifest.Config.MediaType, MediaTypeUnknownConfig) + _, err = PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, opts) + if wantErr := errdef.ErrInvalidMediaType; !errors.Is(err, wantErr) { + t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) } } -func Test_PackManifest_ImageV1_0_InvalidDateTimeFormat(t *testing.T) { +func Test_PackManifest_ImageV1_1_RC4_InvalidDateTimeFormat(t *testing.T) { s := memory.New() ctx := context.Background() @@ -1017,7 +1074,8 @@ func Test_PackManifest_ImageV1_0_InvalidDateTimeFormat(t *testing.T) { ocispec.AnnotationCreated: "2000/01/01 00:00:00", }, } - _, err := PackManifest(ctx, s, PackManifestVersion1_0, "", opts) + artifactType := "application/vnd.test" + _, err := PackManifest(ctx, s, PackManifestVersion1_1_RC4, artifactType, opts) if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) { t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr) }