1use std::collections::HashMap;
2use std::fs::metadata;
3use std::io::Write;
4use std::path::PathBuf;
5
6use anyhow::{Context, Error, Result};
7use indexmap::IndexMap;
8use log::{debug, warn};
9use oci_spec::image::{
10 DescriptorBuilder, ImageConfiguration, ImageIndexBuilder, ImageManifestBuilder, MediaType,
11 PlatformBuilder, SCHEMA_VERSION,
12};
13use serde::Serialize;
14use sha256::{digest, try_digest};
15#[derive(Debug, Default)]
16pub struct Builder {
17 configs: Vec<(ImageConfiguration, String)>,
18 layers: Vec<(PathBuf, String)>,
19}
20
21#[derive(Serialize, Debug)]
22struct OciLayout {
23 #[serde(rename = "imageLayoutVersion")]
24 image_layout_version: String,
25}
26
27impl Default for OciLayout {
28 fn default() -> Self {
29 Self {
30 image_layout_version: "1.0.0".to_string(),
31 }
32 }
33}
34
35#[derive(Serialize, Debug)]
36struct DockerManifest {
37 #[serde(rename = "Config")]
38 config: String,
39 #[serde(rename = "RepoTags")]
40 repo_tags: Vec<String>,
41 #[serde(rename = "Layers")]
42 layers: Vec<String>,
43}
44
45pub const WASM_LAYER_MEDIA_TYPE: &str =
46 "application/vnd.bytecodealliance.wasm.component.layer.v0+wasm";
47
48impl Builder {
49 pub fn add_config(&mut self, config: ImageConfiguration, name: String) -> &mut Self {
50 self.configs.push((config, name));
51 self
52 }
53
54 pub fn add_layer(&mut self, layer: &PathBuf) -> &mut Self {
55 self.layers.push((layer.to_owned(), "".to_string()));
56 self
57 }
58
59 pub fn add_layer_with_media_type(&mut self, layer: &PathBuf, media_type: String) -> &mut Self {
60 self.layers.push((layer.to_owned(), media_type));
61 self
62 }
63
64 pub fn build<W: Write>(&mut self, w: W) -> Result<(), Error> {
65 let mut tb = tar::Builder::new(w);
66 let mut manifests = Vec::new();
67 let mut layer_digests = IndexMap::new();
69
70 if self.configs.len() > 1 {
71 anyhow::bail!("only one config is supported");
72 }
73
74 let mut mfst = DockerManifest {
75 config: "".to_string(),
76 repo_tags: Vec::new(),
77 layers: Vec::new(),
78 };
79
80 for layer in self.layers.iter() {
81 let dgst = try_digest(layer.0.as_path()).context("failed to digest layer")?;
82 let meta = metadata(layer.0.clone()).context("could not get layer metadata")?;
83 let oci_digest = "sha256:".to_owned() + &dgst;
84
85 let mut media_type = MediaType::ImageLayer;
86 if !layer.1.is_empty() {
87 media_type = MediaType::Other(layer.1.clone());
88 }
89 let desc = DescriptorBuilder::default()
90 .media_type(media_type)
92 .digest(&oci_digest)
93 .size(meta.len() as i64)
94 .build()
95 .context("failed to build descriptor")?;
96 layer_digests.insert(oci_digest, desc);
97
98 let mut th = tar::Header::new_gnu();
99 th.set_mode(0o444);
100 th.set_size(meta.len());
101 let p = "blobs/sha256/".to_owned() + &dgst;
102 th.set_path(&p).context("could not set path for layer")?;
103 th.set_cksum();
104 let f = std::fs::File::open(layer.0.clone()).context("could not open layer")?;
105 tb.append(&th, f)?;
106
107 mfst.layers.push(p.to_string());
108 }
109
110 for config in self.configs.iter() {
111 let s = config.0.to_string().context("failed to serialize config")?;
112 let b = s.as_bytes();
113 let dgst = digest(b);
114 let mut th = tar::Header::new_gnu();
115 th.set_mode(0o444);
116 th.set_size(b.len() as u64);
117 let p = "blobs/sha256/".to_owned() + &dgst;
118 th.set_path(&p).context("could not set path for config")?;
119 th.set_cksum();
120 tb.append(&th, b)?;
121
122 mfst.config = p.to_string();
123
124 let desc = DescriptorBuilder::default()
125 .media_type(MediaType::ImageConfig)
126 .size(b.len() as i64)
127 .digest("sha256:".to_owned() + &dgst)
128 .build()
129 .context("failed to build descriptor")?;
130
131 let mut layers = Vec::new();
133 for (_k, v) in layer_digests.iter() {
134 layers.push(v.clone());
135 }
136
137 for id in config.0.rootfs().diff_ids().iter() {
138 debug!("id: {}", id);
139 if layer_digests.get(id).is_none() {
140 warn!("rootfs diff with id {} not found in layers", id);
141 }
142 }
143
144 let mut annotations = HashMap::new();
145 if config.1.contains(':') {
146 let split = config.1.split(':').collect::<Vec<&str>>()[1];
147 annotations.insert(
148 "org.opencontainers.image.ref.name".to_string(),
149 split.to_string(),
150 );
151 }
152 mfst.repo_tags.push(config.1.clone());
153 annotations.insert("io.containerd.image.name".to_string(), config.1.clone());
154
155 let manifest = ImageManifestBuilder::default()
156 .schema_version(SCHEMA_VERSION)
157 .media_type(MediaType::ImageManifest);
158
159 let manifest = manifest
160 .layers(layers)
161 .config(desc)
162 .annotations(annotations.clone())
163 .build()
164 .context("failed to build manifest")?
165 .to_string()
166 .context("failed to serialize manifest")?;
167 let b = manifest.as_bytes();
168 let dgst = digest(b);
169
170 let mut th = tar::Header::new_gnu();
171 th.set_mode(0o444);
172 th.set_size(b.len() as u64);
173 th.set_path("blobs/sha256/".to_owned() + &dgst)
174 .context("could not set path for manifest")?;
175 th.set_cksum();
176 tb.append(&th, b)?;
177
178 let platform = PlatformBuilder::default()
179 .os(config.0.os().clone())
180 .architecture(config.0.architecture().clone())
181 .build()
182 .context("failed to build platform")?;
183
184 let desc = DescriptorBuilder::default()
185 .media_type(MediaType::ImageManifest)
186 .size(b.len() as i64)
187 .platform(platform)
188 .annotations(annotations)
189 .digest("sha256:".to_owned() + &dgst)
190 .build()
191 .context("failed to build descriptor")?;
192
193 manifests.push(desc);
194 }
195
196 let idx = ImageIndexBuilder::default()
197 .schema_version(SCHEMA_VERSION)
198 .media_type(MediaType::ImageIndex)
199 .manifests(manifests)
200 .build()
201 .context("failed to build index")?;
202
203 let s = idx.to_string().context("failed to serialize index")?;
204 let b = s.as_bytes();
205
206 let mut th = tar::Header::new_gnu();
207 th.set_path("index.json")
208 .context("could not set path to index.json")?;
209 th.set_size(b.len() as u64);
210 th.set_mode(0o644);
211 th.set_cksum();
212
213 tb.append(&th, b)?;
214
215 let layout = serde_json::to_string(&OciLayout::default())
216 .context("failed to serialize oci-layout")?;
217 let b = layout.as_bytes();
218
219 let mut th = tar::Header::new_gnu();
220 th.set_path("oci-layout")
221 .context("could not set path for oci-layout file")?;
222 th.set_size(b.len() as u64);
223 th.set_mode(0o644);
224 th.set_cksum();
225 tb.append(&th, b)?;
226
227 let mfst_data =
228 serde_json::to_string(&vec![&mfst]).context("failed to serialize manifest")?;
229 let mut th = tar::Header::new_gnu();
230 th.set_path("manifest.json")?;
231 th.set_mode(0o644);
232 th.set_size(mfst_data.as_bytes().len() as u64);
233 th.set_cksum();
234 tb.append(&th, mfst_data.as_bytes())?;
235
236 tb.finish()?;
237
238 Ok(())
239 }
240}