blob: edea5b062020c87a48b4ff86f864bcf551edac6a [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"
Jan Schärb48174d2025-04-14 10:13:02 +000011 "path/filepath"
12
13 "github.com/opencontainers/go-digest"
14 ocispec "github.com/opencontainers/image-spec/specs-go"
15 ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
16
17 "source.monogon.dev/osbase/structfs"
18)
19
Jan Schär2963b682025-07-17 17:03:44 +020020// ReadLayoutIndex reads the index from an OS path to an OCI layout directory.
21func ReadLayoutIndex(path string) (*Index, error) {
Jan Schärb48174d2025-04-14 10:13:02 +000022 // Read the oci-layout marker file.
23 layoutBytes, err := os.ReadFile(filepath.Join(path, "oci-layout"))
24 if err != nil {
25 return nil, err
26 }
27 layout := ocispecv1.ImageLayout{}
28 err = json.Unmarshal(layoutBytes, &layout)
29 if err != nil {
30 return nil, fmt.Errorf("failed to parse oci-layout: %w", err)
31 }
32 if layout.Version != "1.0.0" {
33 return nil, fmt.Errorf("unknown oci-layout version %q", layout.Version)
34 }
35
36 // Read the index.
Jan Schär2963b682025-07-17 17:03:44 +020037 indexBytes, err := os.ReadFile(filepath.Join(path, "index.json"))
Jan Schärb48174d2025-04-14 10:13:02 +000038 if err != nil {
39 return nil, err
40 }
Jan Schär2963b682025-07-17 17:03:44 +020041 blobs := &layoutBlobs{path: path}
42 ref, err := NewRef(indexBytes, ocispecv1.MediaTypeImageIndex, "", blobs)
Jan Schärb48174d2025-04-14 10:13:02 +000043 if err != nil {
Jan Schär2963b682025-07-17 17:03:44 +020044 return nil, err
Jan Schärb48174d2025-04-14 10:13:02 +000045 }
Jan Schär2963b682025-07-17 17:03:44 +020046 return ref.(*Index), nil
47}
48
49// ReadLayout reads a manifest from an OS path to an OCI layout directory.
50// It expects the index to point to exactly one manifest, which is common.
51func ReadLayout(path string) (Ref, error) {
52 index, err := ReadLayoutIndex(path)
53 if err != nil {
54 return nil, err
Jan Schärb48174d2025-04-14 10:13:02 +000055 }
Jan Schär2963b682025-07-17 17:03:44 +020056
57 if len(index.Manifest.Manifests) == 0 {
Jan Schärb48174d2025-04-14 10:13:02 +000058 return nil, fmt.Errorf("index.json contains no manifests")
59 }
Jan Schär2963b682025-07-17 17:03:44 +020060 if len(index.Manifest.Manifests) != 1 {
Jan Schärb48174d2025-04-14 10:13:02 +000061 return nil, fmt.Errorf("index.json files containing multiple manifests are not supported")
62 }
Jan Schär2963b682025-07-17 17:03:44 +020063 return index.Ref(&index.Manifest.Manifests[0])
Jan Schärb48174d2025-04-14 10:13:02 +000064}
65
66type layoutBlobs struct {
67 path string
68}
69
70func (r *layoutBlobs) Blob(descriptor *ocispecv1.Descriptor) (io.ReadCloser, error) {
Jan Schär2963b682025-07-17 17:03:44 +020071 algorithm, encoded, err := ParseDigest(string(descriptor.Digest))
72 if err != nil {
73 return nil, fmt.Errorf("failed to parse digest in descriptor: %w", err)
74 }
75 return os.Open(filepath.Join(r.path, "blobs", algorithm, encoded))
76}
77
78func (r *layoutBlobs) Manifest(descriptor *ocispecv1.Descriptor) ([]byte, error) {
79 blob, err := r.Blob(descriptor)
Jan Schärb48174d2025-04-14 10:13:02 +000080 if err != nil {
81 return nil, err
82 }
Jan Schär2963b682025-07-17 17:03:44 +020083 defer blob.Close()
84 manifestBytes := make([]byte, descriptor.Size)
85 _, err = io.ReadFull(blob, manifestBytes)
Jan Schärb48174d2025-04-14 10:13:02 +000086 if err != nil {
Jan Schär2963b682025-07-17 17:03:44 +020087 return nil, fmt.Errorf("failed to read manifest: %w", err)
Jan Schärb48174d2025-04-14 10:13:02 +000088 }
Jan Schär2963b682025-07-17 17:03:44 +020089 return manifestBytes, nil
Jan Schärb48174d2025-04-14 10:13:02 +000090}
91
Jan Schär2963b682025-07-17 17:03:44 +020092func (r *layoutBlobs) Blobs(_ *ocispecv1.Descriptor) (Blobs, error) {
93 return r, nil
94}
95
96// CreateLayout builds an OCI layout from a Ref.
97func CreateLayout(ref Ref) (structfs.Tree, error) {
Jan Schärb48174d2025-04-14 10:13:02 +000098 // Build the index.
Jan Schär2963b682025-07-17 17:03:44 +020099 artifactType := ""
100 if image, ok := ref.(*Image); ok {
101 // According to the OCI spec, the artifactType is the config descriptor
102 // mediaType, and is only set when the descriptor references the image
103 // manifest of an artifact.
104 artifactType = image.Manifest.Config.MediaType
105 if artifactType == ocispecv1.MediaTypeImageConfig {
106 artifactType = ""
107 }
Jan Schärb48174d2025-04-14 10:13:02 +0000108 }
109 imageIndex := ocispecv1.Index{
110 Versioned: ocispec.Versioned{SchemaVersion: 2},
111 MediaType: ocispecv1.MediaTypeImageIndex,
112 Manifests: []ocispecv1.Descriptor{{
113 MediaType: ocispecv1.MediaTypeImageManifest,
114 ArtifactType: artifactType,
Jan Schär2963b682025-07-17 17:03:44 +0200115 Digest: digest.Digest(ref.Digest()),
116 Size: int64(len(ref.RawManifest())),
Jan Schärb48174d2025-04-14 10:13:02 +0000117 }},
118 }
119 imageIndexBytes, err := json.MarshalIndent(imageIndex, "", "\t")
120 if err != nil {
121 return nil, fmt.Errorf("failed to marshal image index: %w", err)
122 }
123 imageIndexBytes = append(imageIndexBytes, '\n')
124
125 root := structfs.Tree{
126 structfs.File("oci-layout", structfs.Bytes(`{"imageLayoutVersion": "1.0.0"}`+"\n")),
127 structfs.File("index.json", structfs.Bytes(imageIndexBytes)),
128 }
129
Jan Schär2963b682025-07-17 17:03:44 +0200130 hasBlob := make(map[string]bool)
131 blobDirs := make(map[string]*structfs.Node)
132 addBlob := func(digest string, blob structfs.Blob) error {
133 if hasBlob[digest] {
134 // If multiple blobs have the same digest, we only need the first one.
135 return nil
136 }
137 hasBlob[digest] = true
138 algorithm, encoded, err := ParseDigest(digest)
139 if err != nil {
140 return fmt.Errorf("failed to parse manifest digest: %w", err)
141 }
142 blobDir, ok := blobDirs[algorithm]
143 if !ok {
144 blobDir = structfs.Dir(algorithm, nil)
145 err = root.Place("blobs", blobDir)
146 if err != nil {
147 return err
148 }
149 blobDirs[algorithm] = blobDir
150 }
151 // root.PlaceFile is not used here because then running time would be
152 // quadratic in the number of blobs.
153 blobDir.Children = append(blobDir.Children, structfs.File(encoded, blob))
154 return nil
Jan Schärb48174d2025-04-14 10:13:02 +0000155 }
Jan Schär2963b682025-07-17 17:03:44 +0200156 err = WalkRefs(string(imageIndex.Manifests[0].Digest), ref, func(digest string, ref Ref) error {
157 err := addBlob(digest, structfs.Bytes(ref.RawManifest()))
158 if err != nil {
159 return err
160 }
161 if image, ok := ref.(*Image); ok {
162 for descriptor := range image.Descriptors() {
163 err := addBlob(string(descriptor.Digest), image.StructfsBlob(descriptor))
164 if err != nil {
165 return err
166 }
167 }
168 }
169 return nil
170 })
Jan Schärb48174d2025-04-14 10:13:02 +0000171 if err != nil {
172 return nil, err
173 }
174
Jan Schärb48174d2025-04-14 10:13:02 +0000175 return root, nil
176}