Skip to content

Commit 24c3af7

Browse files
authored
Merge pull request #475 from lsm5/digest-redux
image/internal: sha512 support for skopeo copy
2 parents 46ca9db + 5ef64fc commit 24c3af7

File tree

8 files changed

+252
-21
lines changed

8 files changed

+252
-21
lines changed

image/directory/directory_dest.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
"go.podman.io/storage/pkg/fileutils"
2121
)
2222

23-
const version = "Directory Transport Version: 1.1\n"
24-
2523
// ErrNotContainerImageDir indicates that the directory doesn't match the expected contents of a directory created
2624
// using the 'dir' transport
2725
var ErrNotContainerImageDir = errors.New("not a containers image directory, don't want to overwrite important data")
@@ -33,7 +31,8 @@ type dirImageDestination struct {
3331
stubs.NoPutBlobPartialInitialize
3432
stubs.AlwaysSupportsSignatures
3533

36-
ref dirReference
34+
ref dirReference
35+
usesNonSHA256Digest bool
3736
}
3837

3938
// newImageDestination returns an ImageDestination for writing to a directory.
@@ -76,9 +75,14 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im
7675
return nil, err
7776
}
7877
// check if contents of version file is what we expect it to be
79-
if string(contents) != version {
78+
versionStr := string(contents)
79+
parsedVersion, err := parseVersion(versionStr)
80+
if err != nil {
8081
return nil, ErrNotContainerImageDir
8182
}
83+
if parsedVersion.isGreaterThan(maxSupportedVersion) {
84+
return nil, UnsupportedVersionError{Version: versionStr, Path: ref.resolvedPath}
85+
}
8286
} else {
8387
return nil, ErrNotContainerImageDir
8488
}
@@ -94,11 +98,6 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im
9498
return nil, fmt.Errorf("unable to create directory %q: %w", ref.resolvedPath, err)
9599
}
96100
}
97-
// create version file
98-
err = os.WriteFile(ref.versionPath(), []byte(version), 0o644)
99-
if err != nil {
100-
return nil, fmt.Errorf("creating version file %q: %w", ref.versionPath(), err)
101-
}
102101

103102
d := &dirImageDestination{
104103
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
@@ -151,13 +150,17 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io.
151150
}
152151
}()
153152

154-
digester, stream := putblobdigest.DigestIfCanonicalUnknown(stream, inputInfo)
153+
digester, stream := putblobdigest.DigestIfUnknown(stream, inputInfo)
154+
155155
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
156156
size, err := io.Copy(blobFile, stream)
157157
if err != nil {
158158
return private.UploadedBlob{}, err
159159
}
160160
blobDigest := digester.Digest()
161+
if blobDigest.Algorithm() != digest.Canonical { // compare the special case in layerPath
162+
d.usesNonSHA256Digest = true
163+
}
161164
if inputInfo.Size != -1 && size != inputInfo.Size {
162165
return private.UploadedBlob{}, fmt.Errorf("Size mismatch when copying %s, expected %d, got %d", blobDigest, inputInfo.Size, size)
163166
}
@@ -257,6 +260,14 @@ func (d *dirImageDestination) PutSignaturesWithFormat(ctx context.Context, signa
257260
// - Uploaded data MAY be visible to others before CommitWithOptions() is called
258261
// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed)
259262
func (d *dirImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error {
263+
versionToWrite := version1_1
264+
if d.usesNonSHA256Digest {
265+
versionToWrite = version1_2
266+
}
267+
err := os.WriteFile(d.ref.versionPath(), []byte(versionToWrite.String()), 0o644)
268+
if err != nil {
269+
return fmt.Errorf("writing version file %q: %w", d.ref.versionPath(), err)
270+
}
260271
return nil
261272
}
262273

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package directory
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/opencontainers/go-digest"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"go.podman.io/image/v5/pkg/blobinfocache/memory"
14+
"go.podman.io/image/v5/types"
15+
)
16+
17+
func TestVersionAssignment(t *testing.T) {
18+
for _, c := range []struct {
19+
name string
20+
algorithms []digest.Algorithm
21+
expectedVersion version
22+
}{
23+
{
24+
name: "SHA256 only gets version 1.1",
25+
algorithms: []digest.Algorithm{digest.SHA256},
26+
expectedVersion: version1_1,
27+
},
28+
{
29+
name: "SHA512 only gets version 1.2",
30+
algorithms: []digest.Algorithm{digest.SHA512},
31+
expectedVersion: version1_2,
32+
},
33+
{
34+
name: "Mixed SHA256 and SHA512 gets version 1.2",
35+
algorithms: []digest.Algorithm{digest.SHA256, digest.SHA512},
36+
expectedVersion: version1_2,
37+
},
38+
} {
39+
t.Run(c.name, func(t *testing.T) {
40+
ref, tmpDir := refToTempDir(t)
41+
cache := memory.New()
42+
43+
dest, err := ref.NewImageDestination(context.Background(), nil)
44+
require.NoError(t, err)
45+
defer dest.Close()
46+
47+
for i, algo := range c.algorithms {
48+
blobData := []byte("test-blob-" + algo.String() + "-" + string(rune(i)))
49+
var blobDigest digest.Digest
50+
if algo == digest.SHA256 {
51+
blobDigest = ""
52+
} else {
53+
blobDigest = algo.FromBytes(blobData)
54+
}
55+
_, err = dest.PutBlob(context.Background(), bytes.NewReader(blobData), types.BlobInfo{Digest: blobDigest, Size: int64(len(blobData))}, cache, false)
56+
require.NoError(t, err)
57+
}
58+
59+
err = dest.Commit(context.Background(), nil)
60+
require.NoError(t, err)
61+
62+
versionBytes, err := os.ReadFile(filepath.Join(tmpDir, "version"))
63+
require.NoError(t, err)
64+
assert.Equal(t, c.expectedVersion.String(), string(versionBytes))
65+
})
66+
}
67+
}

image/directory/directory_src.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,24 @@ type dirImageSource struct {
2626

2727
// newImageSource returns an ImageSource reading from an existing directory.
2828
// The caller must call .Close() on the returned ImageSource.
29-
func newImageSource(ref dirReference) private.ImageSource {
29+
func newImageSource(ref dirReference) (private.ImageSource, error) {
30+
versionPath := ref.versionPath()
31+
contents, err := os.ReadFile(versionPath)
32+
if err != nil {
33+
if !os.IsNotExist(err) {
34+
return nil, fmt.Errorf("reading version file %q: %w", versionPath, err)
35+
}
36+
} else {
37+
versionStr := string(contents)
38+
parsedVersion, err := parseVersion(versionStr)
39+
if err != nil {
40+
return nil, fmt.Errorf("invalid version file content: %q", versionStr)
41+
}
42+
if parsedVersion.isGreaterThan(maxSupportedVersion) {
43+
return nil, UnsupportedVersionError{Version: versionStr, Path: ref.resolvedPath}
44+
}
45+
}
46+
3047
s := &dirImageSource{
3148
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
3249
HasThreadSafeGetBlob: false,
@@ -36,7 +53,7 @@ func newImageSource(ref dirReference) private.ImageSource {
3653
ref: ref,
3754
}
3855
s.Compat = impl.AddCompat(s)
39-
return s
56+
return s, nil
4057
}
4158

4259
// Reference returns the reference used to set up this source, _as specified by the user_

image/directory/directory_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"io"
88
"os"
9+
"path/filepath"
910
"testing"
1011

1112
"github.com/opencontainers/go-digest"
@@ -203,6 +204,8 @@ func TestGetPutSignatures(t *testing.T) {
203204

204205
func TestSourceReference(t *testing.T) {
205206
ref, tmpDir := refToTempDir(t)
207+
err := os.WriteFile(filepath.Join(tmpDir, "version"), []byte("Directory Transport Version: 1.1\n"), 0o644)
208+
require.NoError(t, err)
206209

207210
src, err := ref.NewImageSource(context.Background(), nil)
208211
require.NoError(t, err)

image/directory/directory_transport.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (ref dirReference) NewImage(ctx context.Context, sys *types.SystemContext)
146146
// NewImageSource returns a types.ImageSource for this reference.
147147
// The caller must call .Close() on the returned ImageSource.
148148
func (ref dirReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) {
149-
return newImageSource(ref), nil
149+
return newImageSource(ref)
150150
}
151151

152152
// NewImageDestination returns a types.ImageDestination for this reference.
@@ -172,12 +172,19 @@ func (ref dirReference) manifestPath(instanceDigest *digest.Digest) (string, err
172172
}
173173

174174
// layerPath returns a path for a layer tarball within a directory using our conventions.
175-
func (ref dirReference) layerPath(digest digest.Digest) (string, error) {
176-
if err := digest.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly.
175+
func (ref dirReference) layerPath(d digest.Digest) (string, error) {
176+
if err := d.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly.
177177
return "", err
178178
}
179-
// FIXME: Should we keep the digest identification?
180-
return filepath.Join(ref.path, digest.Encoded()), nil
179+
180+
var filename string
181+
if d.Algorithm() == digest.Canonical {
182+
filename = d.Encoded()
183+
} else {
184+
filename = d.Algorithm().String() + "-" + d.Encoded()
185+
}
186+
187+
return filepath.Join(ref.path, filename), nil
181188
}
182189

183190
// signaturePath returns a path for a signature within a directory using our conventions.

image/directory/directory_transport_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,9 @@ func TestReferenceNewImageNoValidManifest(t *testing.T) {
172172
}
173173

174174
func TestReferenceNewImageSource(t *testing.T) {
175-
ref, _ := refToTempDir(t)
175+
ref, tmpDir := refToTempDir(t)
176+
err := os.WriteFile(filepath.Join(tmpDir, "version"), []byte("Directory Transport Version: 1.1\n"), 0o644)
177+
require.NoError(t, err)
176178
src, err := ref.NewImageSource(context.Background(), nil)
177179
assert.NoError(t, err)
178180
defer src.Close()
@@ -209,14 +211,21 @@ func TestReferenceManifestPath(t *testing.T) {
209211
}
210212

211213
func TestReferenceLayerPath(t *testing.T) {
212-
const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
214+
const hex256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
215+
const hex512 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
213216

214217
ref, tmpDir := refToTempDir(t)
215218
dirRef, ok := ref.(dirReference)
216219
require.True(t, ok)
217-
res, err := dirRef.layerPath("sha256:" + hex)
220+
221+
res, err := dirRef.layerPath("sha256:" + hex256)
218222
require.NoError(t, err)
219-
assert.Equal(t, tmpDir+"/"+hex, res)
223+
assert.Equal(t, tmpDir+"/"+hex256, res)
224+
225+
res, err = dirRef.layerPath("sha512:" + hex512)
226+
require.NoError(t, err)
227+
assert.Equal(t, tmpDir+"/sha512-"+hex512, res)
228+
220229
_, err = dirRef.layerPath(digest.Digest("sha256:../hello"))
221230
assert.Error(t, err)
222231
}

image/directory/version.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package directory
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
const (
8+
versionPrefix = "Directory Transport Version: "
9+
)
10+
11+
// version represents a parsed directory transport version
12+
type version struct {
13+
major int
14+
minor int
15+
}
16+
17+
// Supported versions
18+
// Write version file based on digest algorithm used.
19+
// 1.1 for sha256-only images, 1.2 otherwise.
20+
var (
21+
version1_1 = version{major: 1, minor: 1}
22+
version1_2 = version{major: 1, minor: 2}
23+
maxSupportedVersion = version1_2
24+
)
25+
26+
// String formats a version as a string suitable for writing to the version file
27+
func (v version) String() string {
28+
return fmt.Sprintf("%s%d.%d\n", versionPrefix, v.major, v.minor)
29+
}
30+
31+
// parseVersion parses a version string into major and minor components.
32+
// Returns an error if the format is invalid.
33+
func parseVersion(versionStr string) (version, error) {
34+
var v version
35+
expectedFormat := versionPrefix + "%d.%d\n"
36+
// Sscanf parsing is a bit loose (treats spaces specially), but a strict check immediately follows
37+
n, err := fmt.Sscanf(versionStr, expectedFormat, &v.major, &v.minor)
38+
if err != nil || n != 2 || versionStr != v.String() {
39+
return version{}, fmt.Errorf("invalid version format")
40+
}
41+
return v, nil
42+
}
43+
44+
// TODO: Potential refactor for better interoperability with `cmp`
45+
// https://github.com/containers/container-libs/pull/475#discussion_r2571131267
46+
// isGreaterThan returns true if v is greater than other
47+
func (v version) isGreaterThan(other version) bool {
48+
if v.major != other.major {
49+
return v.major > other.major
50+
}
51+
return v.minor > other.minor
52+
}
53+
54+
// UnsupportedVersionError indicates that the directory uses a version newer than we support
55+
type UnsupportedVersionError struct {
56+
Version string // The unsupported version string found
57+
Path string // The path to the directory
58+
}
59+
60+
func (e UnsupportedVersionError) Error() string {
61+
return fmt.Sprintf("unsupported directory transport version %q at %s", e.Version, e.Path)
62+
}

image/directory/version_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package directory
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestParseVersion(t *testing.T) {
11+
for _, c := range []struct {
12+
input string
13+
expected version
14+
shouldError bool
15+
}{
16+
{
17+
input: "Directory Transport Version: 1.1\n",
18+
expected: version{major: 1, minor: 1},
19+
},
20+
{
21+
input: "Directory Transport Version: 1.2\n",
22+
expected: version{major: 1, minor: 2},
23+
},
24+
{
25+
input: "Invalid prefix 1.1\n",
26+
shouldError: true,
27+
},
28+
{
29+
input: "Directory Transport Version: 1.1",
30+
shouldError: true,
31+
},
32+
{
33+
input: "Directory Transport Version: abc\n",
34+
shouldError: true,
35+
},
36+
} {
37+
t.Run(c.input, func(t *testing.T) {
38+
v, err := parseVersion(c.input)
39+
if c.shouldError {
40+
assert.Error(t, err)
41+
} else {
42+
require.NoError(t, err)
43+
assert.Equal(t, c.expected, v)
44+
}
45+
})
46+
}
47+
}
48+
49+
func TestVersionComparison(t *testing.T) {
50+
assert.False(t, version1_1.isGreaterThan(version1_1))
51+
assert.False(t, version1_1.isGreaterThan(version1_2))
52+
assert.True(t, version1_2.isGreaterThan(version1_1))
53+
assert.True(t, version{major: 2, minor: 0}.isGreaterThan(version1_2))
54+
assert.True(t, version{major: 1, minor: 3}.isGreaterThan(version1_2))
55+
}

0 commit comments

Comments
 (0)