Repository: containers/image
Pull request #2728 (2025-02-20 13:43:57)

oci/layout: add GetLocalBlobPath()
The podman artifact store needs a method to get the actual local path on disk to the blobs. This is needed so we can then bind mount the files into the container, of course the caller must ensure we treat the files as read only. The digest based nature of the store will break if the files are modified.

Luap99: @mtrmac PTAL, no idea if this is the right approach.

One thing I noticed the blob files are written with 644 perms, would it make more sense to chmod that to 444 instead? That way we can better avoid someone accidentally writing the file I think.

Patch:
diff --git a/oci/layout/oci_dest.go b/oci/layout/oci_dest.go
index 6f61ab17..2ed6b91b 100644
--- a/oci/layout/oci_dest.go
+++ b/oci/layout/oci_dest.go
@@ -326,7 +326,7 @@ type PutBlobFromLocalFileOption struct{}
 func PutBlobFromLocalFile(ctx context.Context, dest types.ImageDestination, file string, options ...PutBlobFromLocalFileOption) (digest.Digest, int64, error) {
 	d, ok := dest.(*ociImageDestination)
 	if !ok {
-		return "", -1, errors.New("internal error: PutBlobFromLocalFile called with a non-oci: destination")
+		return "", -1, errors.New("caller error: PutBlobFromLocalFile called with a non-oci: destination")
 	}
 
 	succeeded := false
diff --git a/oci/layout/oci_src.go b/oci/layout/oci_src.go
index adfd2064..49c070c2 100644
--- a/oci/layout/oci_src.go
+++ b/oci/layout/oci_src.go
@@ -16,6 +16,7 @@ import (
 	"github.com/containers/image/v5/internal/private"
 	"github.com/containers/image/v5/pkg/tlsclientconfig"
 	"github.com/containers/image/v5/types"
+	"github.com/containers/storage/pkg/fileutils"
 	"github.com/docker/go-connections/tlsconfig"
 	"github.com/opencontainers/go-digest"
 	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -214,3 +215,26 @@ func getBlobSize(resp *http.Response) int64 {
 	}
 	return size
 }
+
+// GetLocalBlobPath returns the local path to the blob file with the given digest.
+// The returned path is checked for existence so when a non existing digest is
+// given an error will be returned.
+//
+// Important: The returned path must be treated as read only, writing the file will
+// corrupt the oci layout as the digest no longer matches.
+func GetLocalBlobPath(ctx context.Context, src types.ImageSource, digest digest.Digest) (string, error) {
+	s, ok := src.(*ociImageSource)
+	if !ok {
+		return "", errors.New("caller error: GetLocalBlobPath called with a non-oci: source")
+	}
+
+	path, err := s.ref.blobPath(digest, s.sharedBlobDir)
+	if err != nil {
+		return "", err
+	}
+	if err := fileutils.Exists(path); err != nil {
+		return "", err
+	}
+
+	return path, nil
+}


Test patch:
diff --git a/oci/layout/oci_src_test.go b/oci/layout/oci_src_test.go
index 08e7b575..70c54402 100644
--- a/oci/layout/oci_src_test.go
+++ b/oci/layout/oci_src_test.go
@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -141,3 +142,37 @@ func createImageSource(t *testing.T, sys *types.SystemContext) types.ImageSource
 	require.NoError(t, err)
 	return imageSource
 }
+
+func TestGetLocalBlobPath(t *testing.T) {
+	tmpDir := loadFixture(t, "delete_image_multiple_images")
+	ref, err := NewReference(tmpDir, "latest")
+	require.NoError(t, err)
+
+	src, err := ref.NewImageSource(context.Background(), &types.SystemContext{})
+	require.NoError(t, err)
+	defer src.Close()
+
+	// success cases
+	for _, dig := range []digest.Digest{
+		"sha256:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1",
+		"sha256:557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861",
+	} {
+		path, err := GetLocalBlobPath(context.Background(), src, dig)
+		require.NoError(t, err)
+		algo, hash, _ := strings.Cut(string(dig), ":")
+		expect := filepath.Join(tmpDir, "blobs", algo, hash)
+		assert.Equal(t, expect, path)
+	}
+
+	// error cases
+	for _, dig := range []digest.Digest{
+		// Invalid digest must error.
+		"sha256:as",
+		// Valid digest but they don't exist in the oci layout thus they must error.
+		"sha256:abababababababababababababababababababababababababababababababab",
+		"sha512:a2f798327b3f25e3eff54badcb769953de235e62e3e32051d57a5e66246de4a1557ac7d133b7770216a8101268640edf4e88beab1b4e1e1bfc9b1891a1cab861",
+	} {
+		_, err = GetLocalBlobPath(context.Background(), src, dig)
+		require.Error(t, err)
+	}
+}