1pub mod image;
14pub mod skopeo;
15pub mod tar;
16
17use std::{collections::HashMap, io::Read, sync::Arc};
18
19use anyhow::{bail, ensure, Context, Result};
20use containers_image_proxy::ImageProxyConfig;
21use oci_spec::image::{Descriptor, ImageConfiguration};
22use sha2::{Digest, Sha256};
23
24use composefs::{
25 fsverity::FsVerityHashValue,
26 repository::Repository,
27 splitstream::DigestMap,
28 util::{parse_sha256, Sha256Digest},
29};
30
31use crate::tar::get_entry;
32
33type ContentAndVerity<ObjectID> = (Sha256Digest, ObjectID);
34
35pub(crate) fn sha256_from_descriptor(descriptor: &Descriptor) -> Result<Sha256Digest> {
36 let Some(digest) = descriptor.as_digest_sha256() else {
37 bail!("Descriptor in oci config is not sha256");
38 };
39 Ok(parse_sha256(digest)?)
40}
41
42pub(crate) fn sha256_from_digest(digest: &str) -> Result<Sha256Digest> {
43 match digest.strip_prefix("sha256:") {
44 Some(rest) => Ok(parse_sha256(rest)?),
45 None => bail!("Manifest has non-sha256 digest"),
46 }
47}
48
49pub fn import_layer<ObjectID: FsVerityHashValue>(
56 repo: &Arc<Repository<ObjectID>>,
57 sha256: &Sha256Digest,
58 name: Option<&str>,
59 tar_stream: &mut impl Read,
60) -> Result<ObjectID> {
61 repo.ensure_stream(sha256, |writer| tar::split(tar_stream, writer), name)
62}
63
64pub fn ls_layer<ObjectID: FsVerityHashValue>(
69 repo: &Repository<ObjectID>,
70 name: &str,
71) -> Result<()> {
72 let mut split_stream = repo.open_stream(name, None)?;
73
74 while let Some(entry) = get_entry(&mut split_stream)? {
75 println!("{entry}");
76 }
77
78 Ok(())
79}
80
81pub async fn pull<ObjectID: FsVerityHashValue>(
84 repo: &Arc<Repository<ObjectID>>,
85 imgref: &str,
86 reference: Option<&str>,
87 img_proxy_config: Option<ImageProxyConfig>,
88) -> Result<(Sha256Digest, ObjectID)> {
89 skopeo::pull(repo, imgref, reference, img_proxy_config).await
90}
91
92pub fn open_config<ObjectID: FsVerityHashValue>(
103 repo: &Repository<ObjectID>,
104 name: &str,
105 verity: Option<&ObjectID>,
106) -> Result<(ImageConfiguration, DigestMap<ObjectID>)> {
107 let id = match verity {
108 Some(id) => id,
109 None => {
110 let sha256 = parse_sha256(name)
112 .context("Containers must be referred to by sha256 if verity is missing")?;
113 &repo
114 .check_stream(&sha256)?
115 .with_context(|| format!("Object {name} is unknown to us"))?
116 }
117 };
118 let mut stream = repo.open_stream(name, Some(id))?;
119 let config = ImageConfiguration::from_reader(&mut stream)?;
120 Ok((config, stream.refs))
121}
122
123fn hash(bytes: &[u8]) -> Sha256Digest {
124 let mut context = Sha256::new();
125 context.update(bytes);
126 context.finalize().into()
127}
128
129pub fn open_config_shallow<ObjectID: FsVerityHashValue>(
139 repo: &Repository<ObjectID>,
140 name: &str,
141 verity: Option<&ObjectID>,
142) -> Result<ImageConfiguration> {
143 match verity {
144 Some(id) => Ok(open_config(repo, name, Some(id))?.0),
146 None => {
147 let expected_hash = parse_sha256(name)
149 .context("Containers must be referred to by sha256 if verity is missing")?;
150 let mut stream = repo.open_stream(name, None)?;
151 let mut raw_config = vec![];
152 stream.read_to_end(&mut raw_config)?;
153 ensure!(hash(&raw_config) == expected_hash, "Data integrity issue");
154 Ok(ImageConfiguration::from_reader(&mut raw_config.as_slice())?)
155 }
156 }
157}
158
159pub fn write_config<ObjectID: FsVerityHashValue>(
166 repo: &Arc<Repository<ObjectID>>,
167 config: &ImageConfiguration,
168 refs: DigestMap<ObjectID>,
169) -> Result<ContentAndVerity<ObjectID>> {
170 let json = config.to_string()?;
171 let json_bytes = json.as_bytes();
172 let sha256 = hash(json_bytes);
173 let mut stream = repo.create_stream(Some(sha256), Some(refs));
174 stream.write_inline(json_bytes);
175 let id = repo.write_stream(stream, None)?;
176 Ok((sha256, id))
177}
178
179pub fn seal<ObjectID: FsVerityHashValue>(
187 repo: &Arc<Repository<ObjectID>>,
188 config_name: &str,
189 config_verity: Option<&ObjectID>,
190) -> Result<ContentAndVerity<ObjectID>> {
191 let (mut config, refs) = open_config(repo, config_name, config_verity)?;
192 let mut myconfig = config.config().clone().context("no config!")?;
193 let labels = myconfig.labels_mut().get_or_insert_with(HashMap::new);
194 let mut fs = crate::image::create_filesystem(repo, config_name, config_verity)?;
195 let id = fs.compute_image_id();
196 labels.insert("containers.composefs.fsverity".to_string(), id.to_hex());
197 config.set_config(Some(myconfig));
198 write_config(repo, &config, refs)
199}
200
201pub fn mount<ObjectID: FsVerityHashValue>(
209 repo: &Repository<ObjectID>,
210 name: &str,
211 mountpoint: &str,
212 verity: Option<&ObjectID>,
213) -> Result<()> {
214 let config = open_config_shallow(repo, name, verity)?;
215 let Some(id) = config.get_config_annotation("containers.composefs.fsverity") else {
216 bail!("Can only mount sealed containers");
217 };
218 repo.mount_at(id, mountpoint)
219}
220
221#[cfg(test)]
222mod test {
223 use std::{fmt::Write, io::Read};
224
225 use rustix::fs::CWD;
226 use sha2::{Digest, Sha256};
227
228 use composefs::{fsverity::Sha256HashValue, repository::Repository, test::tempdir};
229
230 use super::*;
231
232 fn append_data(builder: &mut ::tar::Builder<Vec<u8>>, name: &str, size: usize) {
233 let mut header = ::tar::Header::new_ustar();
234 header.set_uid(0);
235 header.set_gid(0);
236 header.set_mode(0o700);
237 header.set_entry_type(::tar::EntryType::Regular);
238 header.set_size(size as u64);
239 builder
240 .append_data(&mut header, name, std::io::repeat(0u8).take(size as u64))
241 .unwrap();
242 }
243
244 fn example_layer() -> Vec<u8> {
245 let mut builder = ::tar::Builder::new(vec![]);
246 append_data(&mut builder, "file0", 0);
247 append_data(&mut builder, "file4095", 4095);
248 append_data(&mut builder, "file4096", 4096);
249 append_data(&mut builder, "file4097", 4097);
250 builder.into_inner().unwrap()
251 }
252
253 #[test]
254 fn test_layer() {
255 let layer = example_layer();
256 let mut context = Sha256::new();
257 context.update(&layer);
258 let layer_id: [u8; 32] = context.finalize().into();
259
260 let repo_dir = tempdir();
261 let repo = Arc::new(Repository::<Sha256HashValue>::open_path(CWD, &repo_dir).unwrap());
262 let id = import_layer(&repo, &layer_id, Some("name"), &mut layer.as_slice()).unwrap();
263
264 let mut dump = String::new();
265 let mut split_stream = repo.open_stream("refs/name", Some(&id)).unwrap();
266 while let Some(entry) = tar::get_entry(&mut split_stream).unwrap() {
267 writeln!(dump, "{entry}").unwrap();
268 }
269 similar_asserts::assert_eq!(dump, "\
270/file0 0 100700 1 0 0 0 0.0 - - -
271/file4095 4095 100700 1 0 0 0 0.0 53/72beb83c78537c8970c8361e3254119fafdf1763854ecd57d3f0fe2da7c719 - 5372beb83c78537c8970c8361e3254119fafdf1763854ecd57d3f0fe2da7c719
272/file4096 4096 100700 1 0 0 0 0.0 ba/bc284ee4ffe7f449377fbf6692715b43aec7bc39c094a95878904d34bac97e - babc284ee4ffe7f449377fbf6692715b43aec7bc39c094a95878904d34bac97e
273/file4097 4097 100700 1 0 0 0 0.0 09/3756e4ea9683329106d4a16982682ed182c14bf076463a9e7f97305cbac743 - 093756e4ea9683329106d4a16982682ed182c14bf076463a9e7f97305cbac743
274");
275 }
276}