1use super::model::{
4 Descriptor, ImageConfiguration, ImageIndex, ImageManifest, OCI_IMAGE_CONFIG, OCI_IMAGE_INDEX,
5 OCI_IMAGE_MANIFEST, OCI_LAYER_TAR, OCI_REF_NAME, OciImageConfig, Platform, ResolvedImageConfig,
6 RootFs, RuntimeConfiguration,
7};
8use crate::{
9 BundlePlan, Digest, Result,
10 error::io,
11 hash::{HashingWriter, sha256_bytes},
12 rootfs::{
13 TarBuilder, ensure_directory, guard_output, output_parent, path_exists, publish_directory,
14 },
15};
16use std::{
17 collections::BTreeMap,
18 io::{BufWriter, Write},
19 path::{Path, PathBuf},
20};
21
22const OCI_LAYOUT_VERSION: &str = "1.0.0";
23
24#[derive(Debug)]
25pub struct OciLayoutBuilder {
26 output: PathBuf,
27 image: OciImageConfig,
28}
29
30impl OciLayoutBuilder {
31 pub fn new(output: impl Into<PathBuf>) -> OciLayoutBuilder {
32 OciLayoutBuilder {
33 output: output.into(),
34 image: OciImageConfig::default(),
35 }
36 }
37
38 pub fn image(mut self, image: OciImageConfig) -> OciLayoutBuilder {
39 self.image = image;
40 self
41 }
42
43 pub fn apply(&self, plan: &BundlePlan) -> Result<OciReport> {
44 guard_output(&self.output)?;
45 let parent = output_parent(&self.output);
46 std::fs::create_dir_all(parent).map_err(|error| io(parent, error))?;
47 let stage = tempfile::Builder::new()
48 .prefix(".elfpak-oci-")
49 .tempdir_in(parent)
50 .map_err(|error| io(parent, error))?;
51
52 if path_exists(&self.output) {
53 ensure_directory(&self.output)?;
54 }
55
56 set_directory_mode(stage.path(), 0o755)?;
57 let report = build_layout_into(stage.path(), plan, &self.image)?;
58 publish_directory(stage, &self.output)?;
59 Ok(report)
60 }
61}
62
63#[derive(Debug)]
64pub struct OciReport {
65 layer_digest: Digest,
66 layer_size: u64,
67 config_digest: Digest,
68 config_size: u64,
69 manifest_digest: Digest,
70 manifest_size: u64,
71 platform: String,
72 image: ResolvedImageConfig,
73}
74
75impl OciReport {
76 pub fn layer_digest(&self) -> &Digest {
77 &self.layer_digest
78 }
79
80 pub fn layer_size(&self) -> u64 {
81 self.layer_size
82 }
83
84 pub fn config_digest(&self) -> &Digest {
85 &self.config_digest
86 }
87
88 pub fn config_size(&self) -> u64 {
89 self.config_size
90 }
91
92 pub fn manifest_digest(&self) -> &Digest {
93 &self.manifest_digest
94 }
95
96 pub fn manifest_size(&self) -> u64 {
97 self.manifest_size
98 }
99
100 pub fn platform(&self) -> &str {
101 &self.platform
102 }
103
104 pub fn image(&self) -> &ResolvedImageConfig {
105 &self.image
106 }
107}
108
109pub(crate) fn build_layout_into(
110 root: &Path,
111 plan: &BundlePlan,
112 image: &OciImageConfig,
113) -> Result<OciReport> {
114 let image = image.resolve(plan)?;
115 let blobs = root.join("blobs/sha256");
116 std::fs::create_dir_all(&blobs).map_err(|error| io(&blobs, error))?;
117
118 let (layer_digest, layer_size) = write_layer(&blobs, plan)?;
119 let layer_descriptor = descriptor(OCI_LAYER_TAR, &layer_digest, layer_size);
120
121 let config = ImageConfiguration {
122 architecture: image.architecture.clone(),
123 os: image.os.clone(),
124 config: RuntimeConfiguration {
125 user: image.user.clone(),
126 env: image.env.clone(),
127 entrypoint: image.entrypoint.clone(),
128 cmd: image.cmd.clone(),
129 working_dir: image.working_dir.clone(),
130 labels: image.labels.clone(),
131 },
132 rootfs: RootFs {
133 kind: "layers",
134 diff_ids: vec![oci_digest(&layer_digest)],
135 },
136 };
137 let config_bytes = serde_json::to_vec(&config).expect("OCI configuration is serializable");
138 let (config_digest, config_size) = write_blob(&blobs, &config_bytes)?;
139
140 let manifest = ImageManifest {
141 schema_version: 2,
142 media_type: OCI_IMAGE_MANIFEST,
143 config: descriptor(OCI_IMAGE_CONFIG, &config_digest, config_size),
144 layers: vec![layer_descriptor],
145 };
146 let manifest_bytes = serde_json::to_vec(&manifest).expect("OCI manifest is serializable");
147 let (manifest_digest, manifest_size) = write_blob(&blobs, &manifest_bytes)?;
148
149 let index = ImageIndex {
150 schema_version: 2,
151 media_type: OCI_IMAGE_INDEX,
152 manifests: vec![Descriptor {
153 media_type: OCI_IMAGE_MANIFEST,
154 digest: oci_digest(&manifest_digest),
155 size: manifest_size,
156 annotations: Some(BTreeMap::from([(
157 OCI_REF_NAME.to_string(),
158 image.tag.clone(),
159 )])),
160 platform: Some(Platform {
161 architecture: image.architecture.clone(),
162 os: image.os.clone(),
163 }),
164 }],
165 };
166 write_json_document(&root.join("index.json"), &index)?;
167 write_json_document(
168 &root.join("oci-layout"),
169 &serde_json::json!({ "imageLayoutVersion": OCI_LAYOUT_VERSION }),
170 )?;
171
172 Ok(OciReport {
173 layer_digest,
174 layer_size,
175 config_digest,
176 config_size,
177 manifest_digest,
178 manifest_size,
179 platform: format!("{}/{}", image.os, image.architecture),
180 image,
181 })
182}
183
184fn write_layer(blobs: &Path, plan: &BundlePlan) -> Result<(Digest, u64)> {
185 let mut stage = tempfile::NamedTempFile::new_in(blobs).map_err(|error| io(blobs, error))?;
186 let stage_path = stage.path().to_path_buf();
187 let writer = BufWriter::new(stage.as_file_mut());
188 let writer = HashingWriter::new(writer);
189 let (writer, _) = TarBuilder::new(&stage_path).write_to(writer, plan)?;
190 let (mut writer, digest, size) = writer.finish();
191 writer.flush().map_err(|error| io(&stage_path, error))?;
192 drop(writer);
193 stage
194 .as_file()
195 .sync_all()
196 .map_err(|error| io(&stage_path, error))?;
197 let destination = blobs.join(&digest.0);
198 stage
199 .persist(&destination)
200 .map_err(|error| io(&destination, error.error))?;
201 Ok((digest, size))
202}
203
204fn descriptor(media_type: &'static str, digest: &Digest, size: u64) -> Descriptor {
205 Descriptor {
206 media_type,
207 digest: oci_digest(digest),
208 size,
209 annotations: None,
210 platform: None,
211 }
212}
213
214fn oci_digest(digest: &Digest) -> String {
215 format!("sha256:{digest}")
216}
217
218fn write_blob(blobs: &Path, bytes: &[u8]) -> Result<(Digest, u64)> {
219 let digest = sha256_bytes(bytes);
220 let destination = blobs.join(&digest.0);
221 std::fs::write(&destination, bytes).map_err(|error| io(&destination, error))?;
222 Ok((digest, bytes.len() as u64))
223}
224
225fn write_json_document(path: &Path, value: &impl serde::Serialize) -> Result<()> {
226 let mut bytes = serde_json::to_vec(value).expect("OCI metadata is serializable");
227 bytes.push(b'\n');
228 std::fs::write(path, bytes).map_err(|error| io(path, error))
229}
230
231fn set_directory_mode(path: &Path, mode: u32) -> Result<()> {
232 use std::os::unix::fs::PermissionsExt;
233
234 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
235 .map_err(|error| io(path, error))
236}