blob: 128c4d1179d4df62c57a03b1ec65592ceac0c8bb [file] [log] [blame]
Jan Schärb48174d2025-04-14 10:13:02 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package oci
5
6import (
7 "encoding/json"
8 "fmt"
9 "io"
10 "os"
11 "path"
12 "path/filepath"
13
14 "github.com/opencontainers/go-digest"
15 ocispec "github.com/opencontainers/image-spec/specs-go"
16 ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
17
18 "source.monogon.dev/osbase/structfs"
19)
20
21// ReadLayout reads an image from an OS path to an OCI layout directory.
22func ReadLayout(path string) (*Image, error) {
23 // Read the oci-layout marker file.
24 layoutBytes, err := os.ReadFile(filepath.Join(path, "oci-layout"))
25 if err != nil {
26 return nil, err
27 }
28 layout := ocispecv1.ImageLayout{}
29 err = json.Unmarshal(layoutBytes, &layout)
30 if err != nil {
31 return nil, fmt.Errorf("failed to parse oci-layout: %w", err)
32 }
33 if layout.Version != "1.0.0" {
34 return nil, fmt.Errorf("unknown oci-layout version %q", layout.Version)
35 }
36
37 // Read the index.
38 imageIndexBytes, err := os.ReadFile(filepath.Join(path, "index.json"))
39 if err != nil {
40 return nil, err
41 }
42 imageIndex := ocispecv1.Index{}
43 err = json.Unmarshal(imageIndexBytes, &imageIndex)
44 if err != nil {
45 return nil, fmt.Errorf("failed to parse index.json: %w", err)
46 }
47 if imageIndex.MediaType != ocispecv1.MediaTypeImageIndex {
48 return nil, fmt.Errorf("unknown index.json mediaType %q", imageIndex.MediaType)
49 }
50 if len(imageIndex.Manifests) == 0 {
51 return nil, fmt.Errorf("index.json contains no manifests")
52 }
53 if len(imageIndex.Manifests) != 1 {
54 return nil, fmt.Errorf("index.json files containing multiple manifests are not supported")
55 }
56 manifestDescriptor := &imageIndex.Manifests[0]
57 if manifestDescriptor.MediaType != ocispecv1.MediaTypeImageManifest {
58 return nil, fmt.Errorf("unexpected manifest media type %q", manifestDescriptor.MediaType)
59 }
60
61 // Read the image manifest.
62 imageManifestPath, err := layoutBlobPath(path, manifestDescriptor)
63 if err != nil {
64 return nil, err
65 }
66 imageManifestBytes, err := os.ReadFile(imageManifestPath)
67 if err != nil {
68 return nil, fmt.Errorf("failed to read image manifest: %w", err)
69 }
70
71 blobs := &layoutBlobs{path: path}
72 return NewImage(imageManifestBytes, string(manifestDescriptor.Digest), blobs)
73}
74
75type layoutBlobs struct {
76 path string
77}
78
79func (r *layoutBlobs) Blob(descriptor *ocispecv1.Descriptor) (io.ReadCloser, error) {
80 blobPath, err := layoutBlobPath(r.path, descriptor)
81 if err != nil {
82 return nil, err
83 }
84 return os.Open(blobPath)
85}
86
87func layoutBlobPath(layoutPath string, descriptor *ocispecv1.Descriptor) (string, error) {
88 algorithm, encoded, err := ParseDigest(string(descriptor.Digest))
89 if err != nil {
90 return "", fmt.Errorf("failed to parse digest in image manifest: %w", err)
91 }
92 return filepath.Join(layoutPath, "blobs", algorithm, encoded), nil
93}
94
95// CreateLayout builds an OCI layout from an Image.
96func CreateLayout(image *Image) (structfs.Tree, error) {
97 // Build the index.
98 artifactType := image.Manifest.Config.MediaType
99 if artifactType == ocispecv1.MediaTypeImageConfig {
100 artifactType = ""
101 }
102 imageIndex := ocispecv1.Index{
103 Versioned: ocispec.Versioned{SchemaVersion: 2},
104 MediaType: ocispecv1.MediaTypeImageIndex,
105 Manifests: []ocispecv1.Descriptor{{
106 MediaType: ocispecv1.MediaTypeImageManifest,
107 ArtifactType: artifactType,
108 Digest: digest.Digest(image.ManifestDigest),
109 Size: int64(len(image.RawManifest)),
110 }},
111 }
112 imageIndexBytes, err := json.MarshalIndent(imageIndex, "", "\t")
113 if err != nil {
114 return nil, fmt.Errorf("failed to marshal image index: %w", err)
115 }
116 imageIndexBytes = append(imageIndexBytes, '\n')
117
118 root := structfs.Tree{
119 structfs.File("oci-layout", structfs.Bytes(`{"imageLayoutVersion": "1.0.0"}`+"\n")),
120 structfs.File("index.json", structfs.Bytes(imageIndexBytes)),
121 }
122
123 algorithm, encoded, err := ParseDigest(image.ManifestDigest)
124 if err != nil {
125 return nil, fmt.Errorf("failed to parse manifest digest: %w", err)
126 }
127 imageManifestPath := path.Join("blobs", algorithm, encoded)
128 err = root.PlaceFile(imageManifestPath, structfs.Bytes(image.RawManifest))
129 if err != nil {
130 return nil, err
131 }
132
133 hasBlob := map[string]bool{}
134 for descriptor := range image.Descriptors() {
135 algorithm, encoded, err := ParseDigest(string(descriptor.Digest))
136 if err != nil {
137 return nil, fmt.Errorf("failed to parse digest in image manifest: %w", err)
138 }
139 blobPath := path.Join("blobs", algorithm, encoded)
140 if hasBlob[blobPath] {
141 // If multiple blobs have the same hash, we only need the first one.
142 continue
143 }
144 hasBlob[blobPath] = true
145 err = root.PlaceFile(blobPath, image.StructfsBlob(descriptor))
146 if err != nil {
147 return nil, err
148 }
149 }
150
151 return root, nil
152}