blob: ac9721aafac416093cc664837a3c76c6cbff3ffe [file] [log] [blame]
Jan Schärc53a9fc2025-04-14 11:09:27 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4// Package osimage allows reading OS images represented as OCI artifacts, and
5// contains the types for the OS image config.
6package osimage
7
8import (
9 "crypto/sha256"
10 "encoding/base64"
11 "encoding/json"
12 "fmt"
13 "io"
14
15 "github.com/klauspost/compress/zstd"
16
17 "source.monogon.dev/osbase/oci"
18 "source.monogon.dev/osbase/structfs"
19)
20
21// Image represents an OS image.
22type Image struct {
23 // Config contains the parsed config.
24 Config *Config
25 // RawConfig contains the bytes of the config.
26 RawConfig []byte
27
28 image *oci.Image
29}
30
31// Read reads the config from an OCI image and returns an [Image].
32func Read(image *oci.Image) (*Image, error) {
33 manifest := image.Manifest
34 if manifest.ArtifactType != ArtifactTypeOSImage {
35 return nil, fmt.Errorf("unexpected manifest artifact type %q", manifest.ArtifactType)
36 }
37 if manifest.Config.MediaType != MediaTypeOSImageConfig {
38 return nil, fmt.Errorf("unexpected config media type %q", manifest.Config.MediaType)
39 }
40
41 // Read the config.
42 rawConfig, err := image.ReadBlobVerified(&manifest.Config)
43 if err != nil {
44 return nil, fmt.Errorf("failed to read config: %w", err)
45 }
46 config := &Config{}
47 err = json.Unmarshal(rawConfig, &config)
48 if err != nil {
49 return nil, fmt.Errorf("failed to parse config: %w", err)
50 }
51 if config.FormatVersion != ConfigVersion {
52 return nil, fmt.Errorf("unsupported config version %q", config.FormatVersion)
53 }
54 if len(config.Payloads) != len(manifest.Layers) {
55 return nil, fmt.Errorf("number of layers %d does not match number of payloads %d", len(manifest.Layers), len(config.Payloads))
56 }
57 for i := range config.Payloads {
58 payload := &config.Payloads[i]
59 if payload.Size < 0 {
60 return nil, fmt.Errorf("payload %q has negative size", payload.Name)
61 }
62 if payload.HashChunkSize <= 0 {
63 return nil, fmt.Errorf("payload %q has invalid chunk size %d", payload.Name, payload.HashChunkSize)
64 }
65 if payload.HashChunkSize > 16*1024*1024 {
66 return nil, fmt.Errorf("payload %q has too large chunk size %d", payload.Name, payload.HashChunkSize)
67 }
68 chunks := payload.Size / payload.HashChunkSize
69 if chunks*payload.HashChunkSize < payload.Size {
70 chunks++
71 }
72 if int64(len(payload.ChunkHashesSHA256)) != chunks {
73 return nil, fmt.Errorf("payload %q has %d chunks but %d chunk hashes", payload.Name, chunks, len(payload.ChunkHashesSHA256))
74 }
75 }
76
77 osImage := &Image{
78 Config: config,
79 RawConfig: rawConfig,
80 image: image,
81 }
82 return osImage, nil
83}
84
85// Payload returns the contents of the payload of the given name.
86// All data is verified against hashes in the config before it is returned.
87func (i *Image) Payload(name string) (structfs.Blob, error) {
88 for pi := range i.Config.Payloads {
89 info := &i.Config.Payloads[pi]
90 if info.Name == name {
91 layer := &i.image.Manifest.Layers[pi]
92 blob := &payloadBlob{
93 raw: i.image.StructfsBlob(layer),
94 mediaType: layer.MediaType,
95 info: info,
96 }
97 return blob, nil
98 }
99 }
100 return nil, fmt.Errorf("payload %q not found", name)
101}
102
Jan Schär3b0c8dd2025-06-23 10:32:07 +0000103// PayloadUnverified returns the contents of the payload of the given name.
104// Data is not verified against hashes. This only works for uncompressed images.
105func (i *Image) PayloadUnverified(name string) (structfs.Blob, error) {
106 for pi, info := range i.Config.Payloads {
107 if info.Name == name {
108 layer := &i.image.Manifest.Layers[pi]
109 if layer.MediaType != MediaTypePayloadUncompressed {
110 return nil, fmt.Errorf("unsupported media type %q for unverified payload", layer.MediaType)
111 }
112 return i.image.StructfsBlob(layer), nil
113 }
114 }
115 return nil, fmt.Errorf("payload %q not found", name)
116}
117
Jan Schärc53a9fc2025-04-14 11:09:27 +0000118type payloadBlob struct {
119 raw structfs.Blob
120 mediaType string
121 info *PayloadInfo
122}
123
124func (b *payloadBlob) Open() (io.ReadCloser, error) {
125 blobReader, err := b.raw.Open()
126 if err != nil {
127 return nil, err
128 }
129 reader := &payloadReader{
130 chunkHashes: b.info.ChunkHashesSHA256,
131 blobReader: blobReader,
132 remaining: b.info.Size,
133 buf: make([]byte, b.info.HashChunkSize),
134 }
135 switch b.mediaType {
136 case MediaTypePayloadUncompressed:
137 reader.uncompressed = blobReader
138 case MediaTypePayloadZstd:
139 reader.zstdDecoder, err = zstd.NewReader(blobReader)
140 if err != nil {
141 blobReader.Close()
142 return nil, fmt.Errorf("failed to create zstd decoder: %w", err)
143 }
144 reader.uncompressed = reader.zstdDecoder
145 default:
146 blobReader.Close()
147 return nil, fmt.Errorf("unsupported media type %q", b.mediaType)
148 }
149 return reader, nil
150}
151
152func (b *payloadBlob) Size() int64 {
153 return b.info.Size
154}
155
156type payloadReader struct {
157 chunkHashes []string
158 blobReader io.ReadCloser
159 zstdDecoder *zstd.Decoder
160 uncompressed io.Reader
161 remaining int64 // number of bytes remaining in uncompressed
162 buf []byte // buffer of chunk size
163 available []byte // bytes available for reading in the last read chunk
164}
165
166func (r *payloadReader) Read(p []byte) (n int, err error) {
167 if len(r.available) != 0 {
168 n = copy(p, r.available)
169 r.available = r.available[n:]
170 return
171 }
172 if r.remaining == 0 {
173 err = io.EOF
174 return
175 }
176 chunkLen := min(r.remaining, int64(len(r.buf)))
177 chunk := r.buf[:chunkLen]
178 _, err = io.ReadFull(r.uncompressed, chunk)
179 if err != nil {
180 if err == io.EOF {
181 err = io.ErrUnexpectedEOF
182 }
183 r.remaining = 0
184 return
185 }
186 chunkHashBytes := sha256.Sum256(chunk)
187 chunkHash := base64.RawStdEncoding.EncodeToString(chunkHashBytes[:])
188 if chunkHash != r.chunkHashes[0] {
189 err = fmt.Errorf("payload failed verification against chunk hash, expected %q, got %q", r.chunkHashes[0], chunkHash)
190 r.remaining = 0
191 return
192 }
193 r.chunkHashes = r.chunkHashes[1:]
194 r.remaining -= chunkLen
195 n = copy(p, chunk)
196 r.available = chunk[n:]
197 return
198}
199
200func (r *payloadReader) Close() error {
201 if r.zstdDecoder != nil {
202 r.zstdDecoder.Close()
203 }
204 return r.blobReader.Close()
205}