Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refuse compression to zstd when using schema1 #2196

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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