From b36af23f85a84a170d3dd8a1ed8a5ea681af96e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Tue, 28 Nov 2023 22:07:28 +0100 Subject: [PATCH 1/3] Fix a doc string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should not change behavior. Signed-off-by: Miloslav Trmač --- internal/manifest/manifest.go | 2 +- manifest/manifest.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 1dbcc14182..1b6caa1db4 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -14,7 +14,7 @@ import ( const ( // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 DockerV2Schema1MediaType = "application/vnd.docker.distribution.manifest.v1+json" - // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 with a JWS signature + // DockerV2Schema1SignedMediaType MIME type represents Docker manifest schema 1 with a JWS signature DockerV2Schema1SignedMediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws" // DockerV2Schema2MediaType MIME type represents Docker manifest schema 2 DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json" diff --git a/manifest/manifest.go b/manifest/manifest.go index 959aac935e..828b8da0b7 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -16,7 +16,7 @@ import ( const ( // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 DockerV2Schema1MediaType = manifest.DockerV2Schema1MediaType - // DockerV2Schema1MediaType MIME type represents Docker manifest schema 1 with a JWS signature + // DockerV2Schema1SignedMediaType MIME type represents Docker manifest schema 1 with a JWS signature DockerV2Schema1SignedMediaType = manifest.DockerV2Schema1SignedMediaType // DockerV2Schema2MediaType MIME type represents Docker manifest schema 2 DockerV2Schema2MediaType = manifest.DockerV2Schema2MediaType From d69367336337568107e838f8729b8b188a85df27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Tue, 28 Nov 2023 22:08:31 +0100 Subject: [PATCH 2/3] Generalize handling of schema-agnostic transports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We already have a loop which filters out encryption-incompatible MIME types, so maintain a list of all MIME types instead of pre-computing a filtered list. Should not change behavior. Signed-off-by: Miloslav Trmač --- copy/manifest.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/copy/manifest.go b/copy/manifest.go index 6f01cf5cc3..c3afd83757 100644 --- a/copy/manifest.go +++ b/copy/manifest.go @@ -19,8 +19,8 @@ import ( // Include v2s1 signed but not v2s1 unsigned, because docker/distribution requires a signature even if the unsigned MIME type is used. var preferredManifestMIMETypes = []string{manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1SignedMediaType} -// ociEncryptionMIMETypes lists manifest MIME types that are known to support OCI encryption. -var ociEncryptionMIMETypes = []string{v1.MediaTypeImageManifest} +// allManifestMIMETypes lists all possible manifest MIME types. +var allManifestMIMETypes = []string{v1.MediaTypeImageManifest, manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1SignedMediaType, manifest.DockerV2Schema1MediaType} // orderedSet is a list of strings (MIME types or platform descriptors in our case), with each string appearing at most once. type orderedSet struct { @@ -87,7 +87,7 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest otherMIMETypeCandidates: []string{}, }, nil } - destSupportedManifestMIMETypes = ociEncryptionMIMETypes + destSupportedManifestMIMETypes = allManifestMIMETypes } supportedByDest := set.New[string]() for _, t := range destSupportedManifestMIMETypes { @@ -96,7 +96,7 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest } } if supportedByDest.Empty() { - if len(destSupportedManifestMIMETypes) == 0 { // Coverage: This should never happen, empty values were replaced by ociEncryptionMIMETypes + if len(destSupportedManifestMIMETypes) == 0 { // Coverage: This should never happen, empty values were replaced by allManifestMIMETypes return manifestConversionPlan{}, errors.New("internal error: destSupportedManifestMIMETypes is empty") } // We know, and have verified, that destSupportedManifestMIMETypes is not empty, so encryption must have been involved. @@ -108,8 +108,8 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest return manifestConversionPlan{}, fmt.Errorf("encryption required together with format %s, which does not support encryption", in.forceManifestMIMEType) } - if len(in.destSupportedManifestMIMETypes) == 0 { // 2. destination accepts anything and we have chosen ociEncryptionMIMETypes - // Coverage: This should never happen, ociEncryptionMIMETypes all support encryption + if len(in.destSupportedManifestMIMETypes) == 0 { // 2. destination accepts anything and we have chosen allManifestTypes + // Coverage: This should never happen, allManifestTypes includes OCI, which supports encryption return manifestConversionPlan{}, errors.New("internal error: in.destSupportedManifestMIMETypes is empty but supportedByDest is empty as well") } // 3. destination does not support encryption. @@ -156,7 +156,7 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest } logrus.Debugf("Manifest has MIME type %s, ordered candidate list [%s]", srcType, strings.Join(prioritizedTypes.list, ", ")) - if len(prioritizedTypes.list) == 0 { // Coverage: destSupportedManifestMIMETypes and supportedByDest, which is a subset, is not empty (or we would have exited above), so this should never happen. + if len(prioritizedTypes.list) == 0 { // Coverage: destSupportedManifestMIMETypes and supportedByDest, which is a subset, is not empty (or we would have exited above), so this should never happen. return manifestConversionPlan{}, errors.New("Internal error: no candidate MIME types") } res := manifestConversionPlan{ From 1f52275ea262563ff8b8e081f55d59a98d964df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Tue, 28 Nov 2023 22:38:21 +0100 Subject: [PATCH 3/3] Trigger a conversion to OCI when compressing to Zstd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ... even if the destination does not restrict manifest schemas. Signed-off-by: Miloslav Trmač --- copy/manifest.go | 70 +++++++++--- copy/manifest_test.go | 176 ++++++++++++++++++++++++++--- copy/single.go | 1 + internal/manifest/manifest.go | 24 ++++ internal/manifest/manifest_test.go | 50 ++++++++ 5 files changed, 286 insertions(+), 35 deletions(-) diff --git a/copy/manifest.go b/copy/manifest.go index c3afd83757..8844ac8e7e 100644 --- a/copy/manifest.go +++ b/copy/manifest.go @@ -6,8 +6,10 @@ import ( "fmt" "strings" + internalManifest "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/internal/set" "github.com/containers/image/v5/manifest" + compressiontypes "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/image/v5/types" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" @@ -51,9 +53,10 @@ type determineManifestConversionInputs struct { destSupportedManifestMIMETypes []string // MIME types supported by the destination, per types.ImageDestination.SupportedManifestMIMETypes() - forceManifestMIMEType string // User’s choice of forced manifest MIME type - requiresOCIEncryption bool // Restrict to manifest formats that can support OCI encryption - cannotModifyManifestReason string // The reason the manifest cannot be modified, or an empty string if it can + forceManifestMIMEType string // User’s choice of forced manifest MIME type + requestedCompressionFormat *compressiontypes.Algorithm // Compression algorithm to use, if the user _explictily_ requested one. + requiresOCIEncryption bool // Restrict to manifest formats that can support OCI encryption + cannotModifyManifestReason string // The reason the manifest cannot be modified, or an empty string if it can } // manifestConversionPlan contains the decisions made by determineManifestConversion. @@ -80,8 +83,10 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest destSupportedManifestMIMETypes = []string{in.forceManifestMIMEType} } + restrictiveCompressionRequired := in.requestedCompressionFormat != nil && !internalManifest.CompressionAlgorithmIsUniversallySupported(*in.requestedCompressionFormat) if len(destSupportedManifestMIMETypes) == 0 { - if !in.requiresOCIEncryption || manifest.MIMETypeSupportsEncryption(srcType) { + if (!in.requiresOCIEncryption || manifest.MIMETypeSupportsEncryption(srcType)) && + (!restrictiveCompressionRequired || internalManifest.MIMETypeSupportsCompressionAlgorithm(srcType, *in.requestedCompressionFormat)) { return manifestConversionPlan{ // Anything goes; just use the original as is, do not try any conversions. preferredMIMEType: srcType, otherMIMETypeCandidates: []string{}, @@ -91,30 +96,61 @@ func determineManifestConversion(in determineManifestConversionInputs) (manifest } supportedByDest := set.New[string]() for _, t := range destSupportedManifestMIMETypes { - if !in.requiresOCIEncryption || manifest.MIMETypeSupportsEncryption(t) { - supportedByDest.Add(t) + if in.requiresOCIEncryption && !manifest.MIMETypeSupportsEncryption(t) { + continue } + if restrictiveCompressionRequired && !internalManifest.MIMETypeSupportsCompressionAlgorithm(t, *in.requestedCompressionFormat) { + continue + } + supportedByDest.Add(t) } if supportedByDest.Empty() { if len(destSupportedManifestMIMETypes) == 0 { // Coverage: This should never happen, empty values were replaced by allManifestMIMETypes return manifestConversionPlan{}, errors.New("internal error: destSupportedManifestMIMETypes is empty") } - // We know, and have verified, that destSupportedManifestMIMETypes is not empty, so encryption must have been involved. - if !in.requiresOCIEncryption { // Coverage: This should never happen, destSupportedManifestMIMETypes was not empty, so we should have filtered for encryption. - return manifestConversionPlan{}, errors.New("internal error: supportedByDest is empty but destSupportedManifestMIMETypes is not, and not encrypting") - } + // We know, and have verified, that destSupportedManifestMIMETypes is not empty, so some filtering of supported MIME types must have been involved. + // destSupportedManifestMIMETypes has three possible origins: if in.forceManifestMIMEType != "" { // 1. forceManifestType specified - return manifestConversionPlan{}, fmt.Errorf("encryption required together with format %s, which does not support encryption", - in.forceManifestMIMEType) + switch { + case in.requiresOCIEncryption && restrictiveCompressionRequired: + return manifestConversionPlan{}, fmt.Errorf("compression using %s, and encryption, required together with format %s, which does not support both", + in.requestedCompressionFormat.Name(), in.forceManifestMIMEType) + case in.requiresOCIEncryption: + return manifestConversionPlan{}, fmt.Errorf("encryption required together with format %s, which does not support encryption", + in.forceManifestMIMEType) + case restrictiveCompressionRequired: + return manifestConversionPlan{}, fmt.Errorf("compression using %s required together with format %s, which does not support it", + in.requestedCompressionFormat.Name(), in.forceManifestMIMEType) + default: + return manifestConversionPlan{}, errors.New("internal error: forceManifestMIMEType was rejected for an unknown reason") + } } if len(in.destSupportedManifestMIMETypes) == 0 { // 2. destination accepts anything and we have chosen allManifestTypes - // Coverage: This should never happen, allManifestTypes includes OCI, which supports encryption - return manifestConversionPlan{}, errors.New("internal error: in.destSupportedManifestMIMETypes is empty but supportedByDest is empty as well") + if !restrictiveCompressionRequired { + // Coverage: This should never happen. + // If we have not rejected for encryption reasons, we must have rejected due to encryption, but + // allManifestTypes includes OCI, which supports encryption. + return manifestConversionPlan{}, errors.New("internal error: in.destSupportedManifestMIMETypes is empty but supportedByDest is empty as well") + } + // This can legitimately happen when the user asks for completely unsupported formats like Bzip2 or Xz. + return manifestConversionPlan{}, fmt.Errorf("compression using %s required, but none of the known manifest formats support it", in.requestedCompressionFormat.Name()) + } + // 3. destination accepts a restricted list of mime types + destMIMEList := strings.Join(destSupportedManifestMIMETypes, ", ") + switch { + case in.requiresOCIEncryption && restrictiveCompressionRequired: + return manifestConversionPlan{}, fmt.Errorf("compression using %s, and encryption, required but the destination only supports MIME types [%s], none of which support both", + in.requestedCompressionFormat.Name(), destMIMEList) + case in.requiresOCIEncryption: + return manifestConversionPlan{}, fmt.Errorf("encryption required but the destination only supports MIME types [%s], none of which support encryption", + destMIMEList) + case restrictiveCompressionRequired: + return manifestConversionPlan{}, fmt.Errorf("compression using %s required but the destination only supports MIME types [%s], none of which support it", + in.requestedCompressionFormat.Name(), destMIMEList) + default: // Coverage: This should never happen, we only filter for in.requiresOCIEncryption || restrictiveCompressionRequired + return manifestConversionPlan{}, errors.New("internal error: supportedByDest is empty but destSupportedManifestMIMETypes is not, and we are neither encrypting nor requiring a restrictive compression algorithm") } - // 3. destination does not support encryption. - return manifestConversionPlan{}, fmt.Errorf("encryption required but the destination only supports MIME types [%s], none of which support encryption", - strings.Join(destSupportedManifestMIMETypes, ", ")) } // destSupportedManifestMIMETypes is a static guess; a particular registry may still only support a subset of the types. diff --git a/copy/manifest_test.go b/copy/manifest_test.go index 0a7d6540af..7f22c0fe31 100644 --- a/copy/manifest_test.go +++ b/copy/manifest_test.go @@ -8,6 +8,7 @@ import ( "github.com/containers/image/v5/internal/testing/mocks" "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/pkg/compression" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -216,10 +217,11 @@ func TestDetermineManifestConversion(t *testing.T) { }, res, c.description) } - // When encryption is required: + // When encryption or zstd is required: + // In both of these cases, we we are restricted to OCI for _, c := range []struct { description string - in determineManifestConversionInputs // with requiresOCIEncryption implied + in determineManifestConversionInputs // with requiresOCIEncryption or requestedCompressionFormat: zstd implied expected manifestConversionPlan // Or {} to expect a failure }{ { // Destination accepts anything - no conversion necessary @@ -234,7 +236,7 @@ func TestDetermineManifestConversion(t *testing.T) { otherMIMETypeCandidates: []string{}, }, }, - { // Destination accepts anything - need to convert for encryption + { // Destination accepts anything - need to convert to OCI "s2→anything", determineManifestConversionInputs{ srcMIMEType: manifest.DockerV2Schema2MediaType, @@ -246,7 +248,7 @@ func TestDetermineManifestConversion(t *testing.T) { otherMIMETypeCandidates: []string{}, }, }, - // Destination accepts an encrypted format + // Destination accepts OCI { "OCI→OCI", determineManifestConversionInputs{ @@ -271,7 +273,7 @@ func TestDetermineManifestConversion(t *testing.T) { otherMIMETypeCandidates: []string{}, }, }, - // Destination does not accept an encrypted format + // Destination does not accept OCI { "OCI→s2", determineManifestConversionInputs{ @@ -289,9 +291,9 @@ func TestDetermineManifestConversion(t *testing.T) { manifestConversionPlan{}, }, // Whatever the input is, with cannotModifyManifestReason we return "keep the original as is". - // Still, encryption is necessarily going to fail… + // Still, encryption/compression is necessarily going to fail… { - "OCI→OCI cannotModifyManifestReason", + "OCI cannotModifyManifestReason", determineManifestConversionInputs{ srcMIMEType: v1.MediaTypeImageManifest, destSupportedManifestMIMETypes: supportS1S2OCI, @@ -304,7 +306,7 @@ func TestDetermineManifestConversion(t *testing.T) { }, }, { - "s2→OCI cannotModifyManifestReason", + "s2 cannotModifyManifestReason", determineManifestConversionInputs{ srcMIMEType: manifest.DockerV2Schema2MediaType, destSupportedManifestMIMETypes: supportS1S2OCI, @@ -316,7 +318,7 @@ func TestDetermineManifestConversion(t *testing.T) { otherMIMETypeCandidates: []string{}, }, }, - // forceManifestMIMEType to a type that supports encryption + // forceManifestMIMEType to a type that supports OCI features { "OCI→OCI forced", determineManifestConversionInputs{ @@ -343,7 +345,7 @@ func TestDetermineManifestConversion(t *testing.T) { otherMIMETypeCandidates: []string{}, }, }, - // forceManifestMIMEType to a type that does not support encryption + // forceManifestMIMEType to a type that does not support OCI features { "OCI→s2 forced", determineManifestConversionInputs{ @@ -363,16 +365,154 @@ func TestDetermineManifestConversion(t *testing.T) { manifestConversionPlan{}, }, } { - in := c.in - in.requiresOCIEncryption = true - res, err := determineManifestConversion(in) - if c.expected.preferredMIMEType != "" { - require.NoError(t, err, c.description) - assert.Equal(t, c.expected, res, c.description) - } else { - assert.Error(t, err, c.description) + for _, restriction := range []struct { + description string + edit func(in *determineManifestConversionInputs) + }{ + { + description: "encrypted", + edit: func(in *determineManifestConversionInputs) { + in.requiresOCIEncryption = true + }, + }, + { + description: "zstd", + edit: func(in *determineManifestConversionInputs) { + in.requestedCompressionFormat = &compression.Zstd + }, + }, + { + description: "zstd:chunked", + edit: func(in *determineManifestConversionInputs) { + in.requestedCompressionFormat = &compression.ZstdChunked + }, + }, + { + description: "encrypted+zstd", + edit: func(in *determineManifestConversionInputs) { + in.requiresOCIEncryption = true + in.requestedCompressionFormat = &compression.Zstd + }, + }, + } { + desc := c.description + " / " + restriction.description + + in := c.in + restriction.edit(&in) + res, err := determineManifestConversion(in) + if c.expected.preferredMIMEType != "" { + require.NoError(t, err, desc) + assert.Equal(t, c.expected, res, desc) + } else { + assert.Error(t, err, desc) + } } } + + // When encryption using a completely unsupported algorithm is required: + for _, c := range []struct { + description string + in determineManifestConversionInputs // with requiresOCIEncryption or requestedCompressionFormat: zstd implied + }{ + { // Destination accepts anything + "OCI→anything", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: nil, + }, + }, + { // Destination accepts anything - need to convert to OCI + "s2→anything", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: nil, + }, + }, + // Destination only supports some formats + { + "OCI→OCI", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + }, + }, + { + "s2→OCI", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + }, + }, + { + "OCI→s2", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2, + }, + }, + { + "s2→s2", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2, + }, + }, + // cannotModifyManifestReason + { + "OCI cannotModifyManifestReason", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + cannotModifyManifestReason: "Preserving digests", + }, + }, + { + "s2 cannotModifyManifestReason", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + cannotModifyManifestReason: "Preserving digests", + }, + }, + // forceManifestMIMEType + { + "OCI→OCI forced", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: v1.MediaTypeImageManifest, + }, + }, + { + "s2→OCI forced", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: v1.MediaTypeImageManifest, + }, + }, + { + "OCI→s2 forced", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: manifest.DockerV2Schema2MediaType, + }, + }, + { + "s2→s2 forced", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: manifest.DockerV2Schema2MediaType, + }, + }, + } { + in := c.in + in.requestedCompressionFormat = &compression.Xz + _, err := determineManifestConversion(in) + assert.Error(t, err, c.description) + } } // fakeUnparsedImage is an implementation of types.UnparsedImage which only returns itself as a MIME type in Manifest, diff --git a/copy/single.go b/copy/single.go index 67ca43f7bc..b9ea05970c 100644 --- a/copy/single.go +++ b/copy/single.go @@ -167,6 +167,7 @@ func (c *copier) copySingleImage(ctx context.Context, unparsedImage *image.Unpar srcMIMEType: ic.src.ManifestMIMEType, destSupportedManifestMIMETypes: ic.c.dest.SupportedManifestMIMETypes(), forceManifestMIMEType: c.options.ForceManifestMIMEType, + requestedCompressionFormat: ic.compressionFormat, requiresOCIEncryption: destRequiresOciEncryption, cannotModifyManifestReason: ic.cannotModifyManifestReason, }) diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 1b6caa1db4..6f7bc8bbe6 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -3,6 +3,7 @@ package manifest import ( "encoding/json" + compressiontypes "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/libtrust" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -165,3 +166,26 @@ func NormalizedMIMEType(input string) string { return DockerV2Schema1SignedMediaType } } + +// CompressionAlgorithmIsUniversallySupported returns true if MIMETypeSupportsCompressionAlgorithm(mimeType, algo) returns true for all mimeType values. +func CompressionAlgorithmIsUniversallySupported(algo compressiontypes.Algorithm) bool { + switch algo.Name() { // Should this use InternalUnstableUndocumentedMIMEQuestionMark() ? + case compressiontypes.GzipAlgorithmName: + return true + default: + return false + } +} + +// MIMETypeSupportsCompressionAlgorithm returns true if mimeType can represent algo. +func MIMETypeSupportsCompressionAlgorithm(mimeType string, algo compressiontypes.Algorithm) bool { + if CompressionAlgorithmIsUniversallySupported(algo) { + return true + } + switch algo.Name() { // Should this use InternalUnstableUndocumentedMIMEQuestionMark() ? + case compressiontypes.ZstdAlgorithmName, compressiontypes.ZstdChunkedAlgorithmName: + return mimeType == imgspecv1.MediaTypeImageManifest + default: // Includes Bzip2AlgorithmName and XzAlgorithmName, which are defined names but are not supported anywhere + return false + } +} diff --git a/internal/manifest/manifest_test.go b/internal/manifest/manifest_test.go index 8dc9879192..0b01549670 100644 --- a/internal/manifest/manifest_test.go +++ b/internal/manifest/manifest_test.go @@ -1,10 +1,12 @@ package manifest import ( + "fmt" "os" "path/filepath" "testing" + "github.com/containers/image/v5/pkg/compression" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" @@ -132,3 +134,51 @@ func TestNormalizedMIMEType(t *testing.T) { assert.Equal(t, DockerV2Schema1SignedMediaType, res, c) } } + +func TestCompressionAlgorithmIsUniversallySupported(t *testing.T) { + for _, algo := range []compression.Algorithm{compression.Gzip} { + res := CompressionAlgorithmIsUniversallySupported(algo) + assert.True(t, res, algo.Name()) + } + + for _, algo := range []compression.Algorithm{ + compression.Bzip2, + compression.Xz, + compression.Zstd, + compression.ZstdChunked, + } { + res := CompressionAlgorithmIsUniversallySupported(algo) + assert.False(t, res, algo.Name()) + } +} + +func TestMIMETypeSupportsCompressionAlgorithm(t *testing.T) { + allMIMETypes := []string{imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType, DockerV2Schema1SignedMediaType, DockerV2Schema1MediaType} + + for _, algo := range []compression.Algorithm{compression.Gzip} { + for _, mt := range allMIMETypes { + res := MIMETypeSupportsCompressionAlgorithm(mt, algo) + assert.True(t, res, fmt.Sprintf("%s, %s", mt, algo.Name())) + } + } + + for _, algo := range []compression.Algorithm{ + compression.Bzip2, + compression.Xz, + } { + for _, mt := range allMIMETypes { + res := MIMETypeSupportsCompressionAlgorithm(mt, algo) + assert.False(t, res, fmt.Sprintf("%s, %s", mt, algo.Name())) + } + } + + for _, algo := range []compression.Algorithm{ + compression.Zstd, + compression.ZstdChunked, + } { + for _, mt := range allMIMETypes { + res := MIMETypeSupportsCompressionAlgorithm(mt, algo) + assert.Equal(t, mt == imgspecv1.MediaTypeImageManifest, res, fmt.Sprintf("%s, %s", mt, algo.Name())) + } + } +}