Skip to content

Commit

Permalink
Merge pull request #2196 from mtrmac/schema1-zstd
Browse files Browse the repository at this point in the history
Refuse compression to zstd when using schema1
  • Loading branch information
giuseppe authored Nov 21, 2023
2 parents 293b00b + c729a29 commit 7721f70
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 6 deletions.
15 changes: 15 additions & 0 deletions manifest/docker_schema1.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/internal/manifest"
"github.com/containers/image/v5/internal/set"
compressiontypes "github.com/containers/image/v5/pkg/compression/types"
"github.com/containers/image/v5/types"
"github.com/containers/storage/pkg/regexp"
"github.com/docker/docker/api/types/versions"
Expand Down Expand Up @@ -142,6 +143,15 @@ func (m *Schema1) LayerInfos() []LayerInfo {
return layers
}

const fakeSchema1MIMEType = DockerV2Schema2LayerMediaType // Used only in schema1CompressionMIMETypeSets
var schema1CompressionMIMETypeSets = []compressionMIMETypeSet{
{
mtsUncompressed: fakeSchema1MIMEType,
compressiontypes.GzipAlgorithmName: fakeSchema1MIMEType,
compressiontypes.ZstdAlgorithmName: mtsUnsupportedMIMEType,
},
}

// UpdateLayerInfos replaces the original layers with the specified BlobInfos (size+digest+urls), in order (the root layer first, and then successive layered layers)
func (m *Schema1) UpdateLayerInfos(layerInfos []types.BlobInfo) error {
// Our LayerInfos includes empty layers (where m.ExtractedV1Compatibility[].ThrowAway), so expect them to be included here as well.
Expand All @@ -150,6 +160,11 @@ func (m *Schema1) UpdateLayerInfos(layerInfos []types.BlobInfo) error {
}
m.FSLayers = make([]Schema1FSLayers, len(layerInfos))
for i, info := range layerInfos {
// There are no MIME types in schema1, but we do a “conversion” here to reject unsupported compression algorithms,
// in a way that is consistent with the other schema implementations.
if _, err := updatedMIMEType(schema1CompressionMIMETypeSets, fakeSchema1MIMEType, info); err != nil {
return fmt.Errorf("preparing updated manifest, layer %q: %w", info.Digest, err)
}
// (docker push) sets up m.ExtractedV1Compatibility[].{Id,Parent} based on values of info.Digest,
// but (docker pull) ignores them in favor of computing DiffIDs from uncompressed data, except verifying the child->parent links and uniqueness.
// So, we don't bother recomputing the IDs in m.History.V1Compatibility.
Expand Down
101 changes: 95 additions & 6 deletions manifest/docker_schema1_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package manifest

import (
"encoding/json"
"os"
"path/filepath"
"testing"
Expand All @@ -22,6 +23,26 @@ var schema1FixtureLayerDiffIDs = []digest.Digest{
"sha256:8c064bb1f60e84fa8cc6079b6d2e76e0423389fd6aeb7e497dfdae5e05b2b25b",
}

// assertJSONEqualsFixture tests that jsonBytes is structurally equal to fixture,
// possibly ignoring ignoreFields
func assertJSONEqualsFixture(t *testing.T, jsonBytes []byte, fixture string, ignoreFields ...string) {
var contents map[string]any
err := json.Unmarshal(jsonBytes, &contents)
require.NoError(t, err)

fixtureBytes, err := os.ReadFile(filepath.Join("fixtures", fixture))
require.NoError(t, err)
var fixtureContents map[string]any

err = json.Unmarshal(fixtureBytes, &fixtureContents)
require.NoError(t, err)
for _, f := range ignoreFields {
delete(contents, f)
delete(fixtureContents, f)
}
assert.Equal(t, fixtureContents, contents)
}

func manifestSchema1FromFixture(t *testing.T, fixture string) *Schema1 {
manifest, err := os.ReadFile(filepath.Join("fixtures", fixture))
require.NoError(t, err)
Expand Down Expand Up @@ -185,7 +206,78 @@ func TestSchema1UpdateLayerInfos(t *testing.T) {
updates []types.BlobInfo
expectedFixture string // or "" to indicate an expected failure
}{
// Many more tests cases could be added here
{
name: "gzip → uncompressed",
sourceFixture: "v2s1.manifest.json",
updates: []types.BlobInfo{
{
Digest: "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
Size: 32654,
CompressionOperation: types.Decompress,
},
{
Digest: "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
Size: 16724,
CompressionOperation: types.Decompress,
},
{
Digest: "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
Size: 73109,
CompressionOperation: types.Decompress,
},
},
expectedFixture: "v2s1.manifest.json", // MIME type is not stored, and we didn’t change the digests in this test, so we should not see any changes.
},
{
name: "uncompressed → gzip",
sourceFixture: "v2s1.manifest.json",
updates: []types.BlobInfo{
{
Digest: "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
Size: 32654,
CompressionOperation: types.Compress,
CompressionAlgorithm: &compression.Gzip,
},
{
Digest: "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
Size: 16724,
CompressionOperation: types.Compress,
CompressionAlgorithm: &compression.Gzip,
},
{
Digest: "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
Size: 73109,
CompressionOperation: types.Compress,
CompressionAlgorithm: &compression.Gzip,
},
},
expectedFixture: "v2s1.manifest.json", // MIME type is not stored, and we didn’t change the digests in this test, so we should not see any changes.
},
{
name: "gzip → zstd",
sourceFixture: "v2s1.manifest.json",
updates: []types.BlobInfo{
{
Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
Size: 32654,
CompressionOperation: types.Compress,
CompressionAlgorithm: &compression.Zstd,
},
{
Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b",
Size: 16724,
CompressionOperation: types.Compress,
CompressionAlgorithm: &compression.Zstd,
},
{
Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736",
Size: 73109,
CompressionOperation: types.Compress,
CompressionAlgorithm: &compression.Zstd,
},
},
expectedFixture: "", // zstd is not supported for docker images
},
{
name: "uncompressed → gzip encrypted",
sourceFixture: "v2s1.manifest.json",
Expand Down Expand Up @@ -254,11 +346,8 @@ func TestSchema1UpdateLayerInfos(t *testing.T) {
updatedManifestBytes, err := manifest.Serialize()
require.NoError(t, err, c.name)

expectedManifest := manifestSchema1FromFixture(t, c.expectedFixture)
expectedManifestBytes, err := expectedManifest.Serialize()
require.NoError(t, err, c.name)

assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes), c.name)
// Drop "signatures" which is generated by AddDummyV2S1Signature
assertJSONEqualsFixture(t, updatedManifestBytes, c.expectedFixture, "signatures")
}
}
}
Expand Down

0 comments on commit 7721f70

Please sign in to comment.