package manifest import ( "os" "path/filepath" "testing" "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func manifestOCI1FromFixture(t *testing.T, fixture string) *OCI1 { manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) require.NoError(t, err) m, err := OCI1FromManifest(manifest) require.NoError(t, err) return m } func TestSupportedOCI1MediaType(t *testing.T) { type testData struct { m string mustFail bool } data := []testData{ {imgspecv1.MediaTypeDescriptor, false}, {imgspecv1.MediaTypeImageConfig, false}, {imgspecv1.MediaTypeImageLayer, false}, {imgspecv1.MediaTypeImageLayerGzip, false}, {imgspecv1.MediaTypeImageLayerNonDistributable, false}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. {imgspecv1.MediaTypeImageLayerNonDistributableGzip, false}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. {imgspecv1.MediaTypeImageLayerNonDistributableZstd, false}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. {imgspecv1.MediaTypeImageLayerZstd, false}, {imgspecv1.MediaTypeImageManifest, false}, {imgspecv1.MediaTypeLayoutHeader, false}, {"application/vnd.oci.image.layer.nondistributable.v1.tar+unknown", true}, } for _, d := range data { err := SupportedOCI1MediaType(d.m) if d.mustFail { assert.NotNil(t, err) } else { assert.Nil(t, err) } } } func TestOCI1FromManifest(t *testing.T) { validManifest, err := os.ReadFile(filepath.Join("fixtures", "ociv1.manifest.json")) require.NoError(t, err) parser := func(m []byte) error { _, err := OCI1FromManifest(m) return err } // Schema mismatch is rejected testManifestFixturesAreRejected(t, parser, []string{ "schema2-to-schema1-by-docker.json", // Not "v2s2.manifest.json" yet, without mediaType the two are too similar to tell the difference. "v2list.manifest.json", "ociv1.image.index.json", }) // Extra fields are rejected testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"fsLayers", "history", "manifests"}) } func TestOCI1Clone(t *testing.T) { // This fixture should be kept updated to have all known fields set to non-empty values m := manifestOCI1FromFixture(t, "ociv1.everything.json") clone := OCI1Clone(m) assert.Equal(t, m.Manifest, clone.Manifest) } func TestOCI1UpdateLayerInfos(t *testing.T) { customCompression := compression.Algorithm{} for _, c := range []struct { name string sourceFixture string updates []types.BlobInfo expectedFixture string // or "" to indicate an expected failure }{ { name: "gzip → zstd", sourceFixture: "ociv1.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Zstd, }, { Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", Size: 16724, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Zstd, }, { Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", Size: 73109, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Zstd, }, }, expectedFixture: "ociv1.zstd.manifest.json", }, { name: "zstd → gzip", sourceFixture: "ociv1.zstd.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, }, { Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", Size: 16724, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, }, { Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", Size: 73109, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, }, }, expectedFixture: "ociv1.manifest.json", }, { name: "zstd → uncompressed", sourceFixture: "ociv1.zstd.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Decompress, }, { Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", Size: 16724, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Decompress, }, { Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", Size: 73109, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Decompress, }, }, expectedFixture: "ociv1.uncompressed.manifest.json", }, { name: "invalid compression operation", sourceFixture: "ociv1.zstd.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, }, { Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", Size: 16724, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: 42, // MUST fail here CompressionAlgorithm: &compression.Gzip, }, { Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", Size: 73109, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, }, }, expectedFixture: "", }, { name: "invalid compression algorithm", sourceFixture: "ociv1.zstd.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, }, { Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", Size: 16724, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: 42, CompressionAlgorithm: &compression.Gzip, }, { Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", Size: 73109, MediaType: imgspecv1.MediaTypeImageLayerZstd, CompressionOperation: types.Compress, CompressionAlgorithm: &customCompression, // MUST fail here }, }, expectedFixture: "", }, { name: "gzip → uncompressed", sourceFixture: "ociv1.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Decompress, }, { Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", Size: 16724, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Decompress, }, { Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", Size: 73109, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Decompress, }, }, expectedFixture: "ociv1.uncompressed.manifest.json", }, { name: "nondistributable → gzip", sourceFixture: "ociv1.nondistributable.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, }, }, expectedFixture: "ociv1.nondistributable.gzip.manifest.json", }, { name: "nondistributable → zstd", sourceFixture: "ociv1.nondistributable.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Zstd, }, }, expectedFixture: "ociv1.nondistributable.zstd.manifest.json", }, { name: "nondistributable gzip → uncompressed", sourceFixture: "ociv1.nondistributable.gzip.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: imgspecv1.MediaTypeImageLayerGzip, CompressionOperation: types.Decompress, }, }, expectedFixture: "ociv1.nondistributable.manifest.json", }, { name: "uncompressed → gzip encrypted", sourceFixture: "ociv1.uncompressed.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Size: 32654, Annotations: map[string]string{"org.opencontainers.image.enc.…": "layer1"}, MediaType: imgspecv1.MediaTypeImageLayer, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, CryptoOperation: types.Encrypt, }, { Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Size: 16724, Annotations: map[string]string{"org.opencontainers.image.enc.…": "layer2"}, MediaType: imgspecv1.MediaTypeImageLayer, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, CryptoOperation: types.Encrypt, }, { Digest: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", Size: 73109, Annotations: map[string]string{"org.opencontainers.image.enc.…": "layer2"}, MediaType: imgspecv1.MediaTypeImageLayer, CompressionOperation: types.Compress, CompressionAlgorithm: &compression.Gzip, CryptoOperation: types.Encrypt, }, }, expectedFixture: "ociv1.encrypted.manifest.json", }, { name: "gzip encrypted → uncompressed decrypted", sourceFixture: "ociv1.encrypted.manifest.json", updates: []types.BlobInfo{ { Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", Size: 32654, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip+encrypted", CompressionOperation: types.Decompress, CryptoOperation: types.Decrypt, }, { Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", Size: 16724, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip+encrypted", CompressionOperation: types.Decompress, CryptoOperation: types.Decrypt, }, { Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", Size: 73109, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip+encrypted", CompressionOperation: types.Decompress, CryptoOperation: types.Decrypt, }, }, expectedFixture: "ociv1.uncompressed.manifest.json", }, } { manifest := manifestOCI1FromFixture(t, c.sourceFixture) err := manifest.UpdateLayerInfos(c.updates) if c.expectedFixture == "" { assert.Error(t, err, c.name) } else { require.NoError(t, err, c.name) updatedManifestBytes, err := manifest.Serialize() require.NoError(t, err, c.name) expectedManifest := manifestOCI1FromFixture(t, c.expectedFixture) expectedManifestBytes, err := expectedManifest.Serialize() require.NoError(t, err, c.name) assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes), c.name) } } } func TestOCI1Inspect(t *testing.T) { // Success is tested in image.TestManifestOCI1Inspect . m := manifestOCI1FromFixture(t, "ociv1.artifact.json") _, err := m.Inspect(func(info types.BlobInfo) ([]byte, error) { require.Equal(t, m.Config.Digest, info.Digest) // This just-enough-artifact contains a zero-byte config, sanity-check that’s till the case. require.Equal(t, int64(0), m.Config.Size) return []byte{}, nil }) var expected NonImageArtifactError assert.ErrorAs(t, err, &expected) } func TestOCI1ImageID(t *testing.T) { m := manifestOCI1FromFixture(t, "ociv1.manifest.json") // These are not the real DiffID values, but they don’t actually matter in our implementation. id, err := m.ImageID([]digest.Digest{ "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", }) require.NoError(t, err) assert.Equal(t, "b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", id) m = manifestOCI1FromFixture(t, "ociv1.artifact.json") _, err = m.ImageID([]digest.Digest{}) var expected NonImageArtifactError assert.ErrorAs(t, err, &expected) } func TestOCI1CanChangeLayerCompression(t *testing.T) { m := manifestOCI1FromFixture(t, "ociv1.manifest.json") assert.True(t, m.CanChangeLayerCompression(imgspecv1.MediaTypeImageLayerGzip)) // Some projects like to use squashfs and other unspecified formats for layers; don’t touch those. assert.False(t, m.CanChangeLayerCompression("a completely unknown and quite possibly invalid MIME type")) artifact := manifestOCI1FromFixture(t, "ociv1.artifact.json") assert.False(t, artifact.CanChangeLayerCompression(imgspecv1.MediaTypeImageLayerGzip)) }