diff --git a/common/internal/deepcopy.go b/common/internal/deepcopy.go new file mode 100644 index 0000000000..157f6ee4ce --- /dev/null +++ b/common/internal/deepcopy.go @@ -0,0 +1,29 @@ +package internal + +import ( + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// DeepCopyDescriptor copies a Descriptor, deeply copying its contents +func DeepCopyDescriptor(original *v1.Descriptor) *v1.Descriptor { + tmp := *original + if original.URLs != nil { + tmp.URLs = slices.Clone(original.URLs) + } + if original.Annotations != nil { + tmp.Annotations = maps.Clone(original.Annotations) + } + if original.Data != nil { + tmp.Data = slices.Clone(original.Data) + } + if original.Platform != nil { + tmpPlatform := *original.Platform + if original.Platform.OSFeatures != nil { + tmpPlatform.OSFeatures = slices.Clone(original.Platform.OSFeatures) + } + tmp.Platform = &tmpPlatform + } + return &tmp +} diff --git a/common/pkg/manifests/manifests.go b/common/pkg/manifests/manifests.go index d429144a71..3dc2ddb55b 100644 --- a/common/pkg/manifests/manifests.go +++ b/common/pkg/manifests/manifests.go @@ -6,6 +6,7 @@ import ( "fmt" "os" + "github.com/containers/common/internal" "github.com/containers/image/v5/manifest" digest "github.com/opencontainers/go-digest" imgspec "github.com/opencontainers/image-spec/specs-go" @@ -39,6 +40,8 @@ type List interface { MediaType(instanceDigest digest.Digest) (string, error) SetArtifactType(instanceDigest digest.Digest, artifactType string) error ArtifactType(instanceDigest digest.Digest) (string, error) + SetSubject(subject *v1.Descriptor) error + Subject() (*v1.Descriptor, error) Serialize(mimeType string) ([]byte, error) Instances() []digest.Digest OCIv1() *v1.Index @@ -488,6 +491,26 @@ func (l *list) ArtifactType(instanceDigest digest.Digest) (string, error) { return oci.ArtifactType, nil } +// SetSubject sets the image index's subject. +// The field is specific to the OCI image index format, and is not present in Docker manifest lists. +func (l *list) SetSubject(subject *v1.Descriptor) error { + if subject != nil { + subject = internal.DeepCopyDescriptor(subject) + } + l.oci.Subject = subject + return nil +} + +// Subject retrieves the subject which might have been set on the image index. +// The field is specific to the OCI image index format, and is not present in Docker manifest lists. +func (l *list) Subject() (*v1.Descriptor, error) { + s := l.oci.Subject + if s != nil { + s = internal.DeepCopyDescriptor(s) + } + return s, nil +} + // FromBlob builds a list from an encoded manifest list or image index. func FromBlob(manifestBytes []byte) (List, error) { manifestType := manifest.GuessMIMEType(manifestBytes) diff --git a/common/pkg/manifests/manifests_test.go b/common/pkg/manifests/manifests_test.go index da9b18738b..c34259a363 100644 --- a/common/pkg/manifests/manifests_test.go +++ b/common/pkg/manifests/manifests_test.go @@ -462,3 +462,43 @@ func TestPlatform(t *testing.T) { assert.NoError(t, list.SetOSFeatures(instanceDigest, []string{})) assert.Nil(t, list.OCIv1().Manifests[0].Platform, "expected platform to be nil") } + +func TestSubject(t *testing.T) { + bytes, err := os.ReadFile(ociFixture) + if err != nil { + t.Fatalf("error loading blob: %v", err) + } + list, err := FromBlob(bytes) + if err != nil { + t.Fatalf("error parsing blob: %v", err) + } + for _, wrote := range []*v1.Descriptor{ + nil, + {}, + { + MediaType: v1.MediaTypeImageManifest, + Digest: expectedInstance, + Size: 1234, + }, + } { + err := list.SetSubject(wrote) + if err != nil { + t.Fatalf("error setting subject: %v", err) + } + b, err := list.Serialize("") + if err != nil { + t.Fatalf("error serializing list: %v", err) + } + list, err = FromBlob(b) + if err != nil { + t.Fatalf("error parsing list: %v", err) + } + read, err := list.Subject() + if err != nil { + t.Fatalf("error retrieving subject: %v", err) + } + if !reflect.DeepEqual(read, wrote) { + t.Fatalf("expected subject %v, got %v", wrote, read) + } + } +}