Skip to content

Commit dc04eb8

Browse files
lsm5mtrmac
andcommitted
image/directory: assign version based on digest algorithm
Introduce version 1.2 and dynamically assign versions based on the digest algorithms used: - Version 1.1 for sha256-only images (backward compatibility) - Version 1.2 for images using non-sha256 digest algorithms (e.g., sha512) Add validation in both ImageDestination and ImageSource to: - Assume 1.1 if no version file found in dir transport images - Accept both version 1.1 and 1.2 - Refuse unsupported future versions Co-authored-by: Miloslav Trmač <[email protected]> Signed-off-by: Lokesh Mandvekar <[email protected]>
1 parent fc54788 commit dc04eb8

File tree

5 files changed

+209
-9
lines changed

5 files changed

+209
-9
lines changed

image/directory/directory_dest.go

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

23-
const version = "Directory Transport Version: 1.1\n"
23+
const (
24+
versionPrefix = "Directory Transport Version: "
25+
)
26+
27+
// version represents a parsed directory transport version
28+
type version struct {
29+
major int
30+
minor int
31+
}
32+
33+
// Supported versions
34+
// Write version file based on digest algorithm used.
35+
// 1.1 for sha256-only images, 1.2 otherwise.
36+
var (
37+
version1_1 = version{major: 1, minor: 1}
38+
version1_2 = version{major: 1, minor: 2}
39+
maxSupportedVersion = version1_2
40+
)
41+
42+
// String formats a version as a string suitable for writing to the version file
43+
func (v version) String() string {
44+
return fmt.Sprintf("%s%d.%d\n", versionPrefix, v.major, v.minor)
45+
}
46+
47+
// parseVersion parses a version string into major and minor components.
48+
// Returns an error if the format is invalid.
49+
func parseVersion(versionStr string) (version, error) {
50+
var v version
51+
expectedFormat := versionPrefix + "%d.%d\n"
52+
n, err := fmt.Sscanf(versionStr, expectedFormat, &v.major, &v.minor)
53+
if err != nil || n != 2 || versionStr != fmt.Sprintf(expectedFormat, v.major, v.minor) {
54+
return version{}, fmt.Errorf("invalid version format")
55+
}
56+
return v, nil
57+
}
58+
59+
// isGreaterThan returns true if v is greater than other
60+
func (v version) isGreaterThan(other version) bool {
61+
if v.major != other.major {
62+
return v.major > other.major
63+
}
64+
return v.minor > other.minor
65+
}
2466

2567
// ErrNotContainerImageDir indicates that the directory doesn't match the expected contents of a directory created
2668
// using the 'dir' transport
2769
var ErrNotContainerImageDir = errors.New("not a containers image directory, don't want to overwrite important data")
2870

71+
// UnsupportedVersionError indicates that the directory uses a version newer than we support
72+
type UnsupportedVersionError struct {
73+
Version string // The unsupported version string found
74+
Path string // The path to the directory
75+
}
76+
77+
func (e UnsupportedVersionError) Error() string {
78+
return fmt.Sprintf("unsupported directory transport version %q at %s", e.Version, e.Path)
79+
}
80+
2981
type dirImageDestination struct {
3082
impl.Compat
3183
impl.PropertyMethodsInitialize
3284
stubs.IgnoresOriginalOCIConfig
3385
stubs.NoPutBlobPartialInitialize
3486
stubs.AlwaysSupportsSignatures
3587

36-
ref dirReference
88+
ref dirReference
89+
usesNonSHA256Digest bool
3790
}
3891

3992
// newImageDestination returns an ImageDestination for writing to a directory.
@@ -76,9 +129,14 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im
76129
return nil, err
77130
}
78131
// check if contents of version file is what we expect it to be
79-
if string(contents) != version {
132+
versionStr := string(contents)
133+
parsedVersion, err := parseVersion(versionStr)
134+
if err != nil {
80135
return nil, ErrNotContainerImageDir
81136
}
137+
if parsedVersion.isGreaterThan(maxSupportedVersion) {
138+
return nil, UnsupportedVersionError{Version: versionStr, Path: ref.resolvedPath}
139+
}
82140
} else {
83141
return nil, ErrNotContainerImageDir
84142
}
@@ -94,11 +152,6 @@ func newImageDestination(sys *types.SystemContext, ref dirReference) (private.Im
94152
return nil, fmt.Errorf("unable to create directory %q: %w", ref.resolvedPath, err)
95153
}
96154
}
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-
}
102155

103156
d := &dirImageDestination{
104157
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
@@ -159,6 +212,9 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io.
159212
return private.UploadedBlob{}, err
160213
}
161214
blobDigest := digester.Digest()
215+
if blobDigest.Algorithm() != digest.Canonical { // compare the special case in layerPath
216+
d.usesNonSHA256Digest = true
217+
}
162218
if inputInfo.Size != -1 && size != inputInfo.Size {
163219
return private.UploadedBlob{}, fmt.Errorf("Size mismatch when copying %s, expected %d, got %d", blobDigest, inputInfo.Size, size)
164220
}
@@ -258,6 +314,14 @@ func (d *dirImageDestination) PutSignaturesWithFormat(ctx context.Context, signa
258314
// - Uploaded data MAY be visible to others before CommitWithOptions() is called
259315
// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed)
260316
func (d *dirImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error {
317+
versionToWrite := version1_1
318+
if d.usesNonSHA256Digest {
319+
versionToWrite = version1_2
320+
}
321+
err := os.WriteFile(d.ref.versionPath(), []byte(versionToWrite.String()), 0o644)
322+
if err != nil {
323+
return fmt.Errorf("writing version file %q: %w", d.ref.versionPath(), err)
324+
}
261325
return nil
262326
}
263327

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 TestParseVersion(t *testing.T) {
18+
for _, c := range []struct {
19+
input string
20+
expected version
21+
shouldError bool
22+
}{
23+
{
24+
input: "Directory Transport Version: 1.1\n",
25+
expected: version{major: 1, minor: 1},
26+
},
27+
{
28+
input: "Directory Transport Version: 1.2\n",
29+
expected: version{major: 1, minor: 2},
30+
},
31+
{
32+
input: "Invalid prefix 1.1\n",
33+
shouldError: true,
34+
},
35+
{
36+
input: "Directory Transport Version: 1.1",
37+
shouldError: true,
38+
},
39+
{
40+
input: "Directory Transport Version: abc\n",
41+
shouldError: true,
42+
},
43+
} {
44+
t.Run(c.input, func(t *testing.T) {
45+
v, err := parseVersion(c.input)
46+
if c.shouldError {
47+
assert.Error(t, err)
48+
} else {
49+
require.NoError(t, err)
50+
assert.Equal(t, c.expected, v)
51+
}
52+
})
53+
}
54+
}
55+
56+
func TestVersionComparison(t *testing.T) {
57+
assert.False(t, version1_1.isGreaterThan(version1_1))
58+
assert.False(t, version1_1.isGreaterThan(version1_2))
59+
assert.True(t, version1_2.isGreaterThan(version1_1))
60+
assert.True(t, version{major: 2, minor: 0}.isGreaterThan(version1_2))
61+
assert.True(t, version{major: 1, minor: 3}.isGreaterThan(version1_2))
62+
}
63+
64+
func TestVersionAssignment(t *testing.T) {
65+
for _, c := range []struct {
66+
name string
67+
algorithms []digest.Algorithm
68+
expectedVersion version
69+
}{
70+
{
71+
name: "SHA256 only gets version 1.1",
72+
algorithms: []digest.Algorithm{digest.SHA256},
73+
expectedVersion: version1_1,
74+
},
75+
{
76+
name: "SHA512 only gets version 1.2",
77+
algorithms: []digest.Algorithm{digest.SHA512},
78+
expectedVersion: version1_2,
79+
},
80+
{
81+
name: "Mixed SHA256 and SHA512 gets version 1.2",
82+
algorithms: []digest.Algorithm{digest.SHA256, digest.SHA512},
83+
expectedVersion: version1_2,
84+
},
85+
} {
86+
t.Run(c.name, func(t *testing.T) {
87+
ref, tmpDir := refToTempDir(t)
88+
cache := memory.New()
89+
90+
dest, err := ref.NewImageDestination(context.Background(), nil)
91+
require.NoError(t, err)
92+
defer dest.Close()
93+
94+
for i, algo := range c.algorithms {
95+
blobData := []byte("test-blob-" + algo.String() + "-" + string(rune(i)))
96+
var blobDigest digest.Digest
97+
if algo == digest.SHA256 {
98+
blobDigest = ""
99+
} else {
100+
blobDigest = algo.FromBytes(blobData)
101+
}
102+
_, err = dest.PutBlob(context.Background(), bytes.NewReader(blobData), types.BlobInfo{Digest: blobDigest, Size: int64(len(blobData))}, cache, false)
103+
require.NoError(t, err)
104+
}
105+
106+
err = dest.Commit(context.Background(), nil)
107+
require.NoError(t, err)
108+
109+
versionBytes, err := os.ReadFile(filepath.Join(tmpDir, "version"))
110+
require.NoError(t, err)
111+
assert.Equal(t, c.expectedVersion.String(), string(versionBytes))
112+
})
113+
}
114+
}

image/directory/directory_src.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ type dirImageSource struct {
2727
// newImageSource returns an ImageSource reading from an existing directory.
2828
// The caller must call .Close() on the returned ImageSource.
2929
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,

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_test.go

Lines changed: 3 additions & 1 deletion
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()

0 commit comments

Comments
 (0)