1use std::borrow::{Borrow, Cow};
6use std::collections::{BTreeMap, BTreeSet};
7use std::fmt::Write;
8use std::hash::{Hash, Hasher};
9use std::num::NonZeroU32;
10use std::rc::Rc;
11use std::time::Instant;
12
13use crate::container::{COMPONENT_SEPARATOR, CONTENT_ANNOTATION};
14use crate::objectsource::{ContentID, ObjectMeta, ObjectMetaMap, ObjectSourceMeta};
15use crate::objgv::*;
16use crate::statistics;
17use anyhow::{anyhow, Result};
18use camino::Utf8PathBuf;
19use containers_image_proxy::oci_spec;
20use gvariant::aligned_bytes::TryAsAligned;
21use gvariant::{Marker, Structure};
22use indexmap::IndexMap;
23use ostree::{gio, glib};
24use serde::{Deserialize, Serialize};
25
26pub(crate) const MAX_CHUNKS: u32 = 64;
30const MIN_CHUNKED_LAYERS: u32 = 4;
33
34pub(crate) type RcStr = Rc<str>;
36pub(crate) type ChunkMapping = BTreeMap<RcStr, (u64, Vec<Utf8PathBuf>)>;
39const LOW_PARTITION: &str = "2ls";
42const HIGH_PARTITION: &str = "1hs";
43
44#[derive(Debug, Default)]
45pub(crate) struct Chunk {
46 pub(crate) name: String,
47 pub(crate) content: ChunkMapping,
48 pub(crate) size: u64,
49 pub(crate) packages: Vec<String>,
50}
51
52#[derive(Debug, Deserialize, Serialize)]
53pub struct ObjectSourceMetaSized {
55 #[serde(flatten)]
57 pub meta: ObjectSourceMeta,
58 pub size: u64,
60}
61
62impl Hash for ObjectSourceMetaSized {
63 fn hash<H: Hasher>(&self, state: &mut H) {
64 self.meta.identifier.hash(state);
65 }
66}
67
68impl Eq for ObjectSourceMetaSized {}
69
70impl PartialEq for ObjectSourceMetaSized {
71 fn eq(&self, other: &Self) -> bool {
72 self.meta.identifier == other.meta.identifier
73 }
74}
75
76#[derive(Debug)]
78pub struct ObjectMetaSized {
79 pub map: ObjectMetaMap,
81 pub sizes: Vec<ObjectSourceMetaSized>,
83}
84
85impl ObjectMetaSized {
86 pub fn compute_sizes(repo: &ostree::Repo, meta: ObjectMeta) -> Result<ObjectMetaSized> {
88 let cancellable = gio::Cancellable::NONE;
89 let map = meta.map;
91 let mut set = meta.set;
92 let mut sizes = BTreeMap::<&str, u64>::new();
94 for (checksum, contentid) in map.iter() {
96 let finfo = repo.query_file(checksum, cancellable)?.0;
97 let sz = sizes.entry(contentid).or_default();
98 *sz += finfo.size() as u64;
99 }
100 let sized: Result<Vec<_>> = sizes
102 .into_iter()
103 .map(|(id, size)| -> Result<ObjectSourceMetaSized> {
104 set.take(id)
105 .ok_or_else(|| anyhow!("Failed to find {} in content set", id))
106 .map(|meta| ObjectSourceMetaSized { meta, size })
107 })
108 .collect();
109 let mut sizes = sized?;
110 sizes.sort_by(|a, b| b.size.cmp(&a.size));
111 Ok(ObjectMetaSized { map, sizes })
112 }
113}
114
115#[derive(Debug, Default)]
117pub struct Chunking {
118 pub(crate) metadata_size: u64,
119 pub(crate) remainder: Chunk,
120 pub(crate) chunks: Vec<Chunk>,
121
122 pub(crate) max: u32,
123
124 processed_mapping: bool,
125 pub(crate) n_provided_components: u32,
127 pub(crate) n_sized_components: u32,
129}
130
131#[derive(Default)]
132struct Generation {
133 path: Utf8PathBuf,
134 metadata_size: u64,
135 dirtree_found: BTreeSet<RcStr>,
136 dirmeta_found: BTreeSet<RcStr>,
137}
138
139fn push_dirmeta(repo: &ostree::Repo, gen: &mut Generation, checksum: &str) -> Result<()> {
140 if gen.dirtree_found.contains(checksum) {
141 return Ok(());
142 }
143 let checksum = RcStr::from(checksum);
144 gen.dirmeta_found.insert(RcStr::clone(&checksum));
145 let child_v = repo.load_variant(ostree::ObjectType::DirMeta, checksum.borrow())?;
146 gen.metadata_size += child_v.data_as_bytes().as_ref().len() as u64;
147 Ok(())
148}
149
150fn push_dirtree(
151 repo: &ostree::Repo,
152 gen: &mut Generation,
153 checksum: &str,
154) -> Result<glib::Variant> {
155 let child_v = repo.load_variant(ostree::ObjectType::DirTree, checksum)?;
156 if !gen.dirtree_found.contains(checksum) {
157 gen.metadata_size += child_v.data_as_bytes().as_ref().len() as u64;
158 } else {
159 let checksum = RcStr::from(checksum);
160 gen.dirtree_found.insert(checksum);
161 }
162 Ok(child_v)
163}
164
165fn generate_chunking_recurse(
166 repo: &ostree::Repo,
167 gen: &mut Generation,
168 chunk: &mut Chunk,
169 dt: &glib::Variant,
170) -> Result<()> {
171 let dt = dt.data_as_bytes();
172 let dt = dt.try_as_aligned()?;
173 let dt = gv_dirtree!().cast(dt);
174 let (files, dirs) = dt.to_tuple();
175 let mut hexbuf = [0u8; 64];
177 for file in files {
178 let (name, csum) = file.to_tuple();
179 let fpath = gen.path.join(name.to_str());
180 hex::encode_to_slice(csum, &mut hexbuf)?;
181 let checksum = std::str::from_utf8(&hexbuf)?;
182 let meta = repo.query_file(checksum, gio::Cancellable::NONE)?.0;
183 let size = meta.size() as u64;
184 let entry = chunk.content.entry(RcStr::from(checksum)).or_default();
185 entry.0 = size;
186 let first = entry.1.is_empty();
187 if first {
188 chunk.size += size;
189 }
190 entry.1.push(fpath);
191 }
192 for item in dirs {
193 let (name, contents_csum, meta_csum) = item.to_tuple();
194 let name = name.to_str();
195 gen.path.push(name);
197 hex::encode_to_slice(contents_csum, &mut hexbuf)?;
198 let checksum_s = std::str::from_utf8(&hexbuf)?;
199 let dirtree_v = push_dirtree(repo, gen, checksum_s)?;
200 generate_chunking_recurse(repo, gen, chunk, &dirtree_v)?;
201 drop(dirtree_v);
202 hex::encode_to_slice(meta_csum, &mut hexbuf)?;
203 let checksum_s = std::str::from_utf8(&hexbuf)?;
204 push_dirmeta(repo, gen, checksum_s)?;
205 assert!(gen.path.pop());
207 }
208 Ok(())
209}
210
211impl Chunk {
212 fn new(name: &str) -> Self {
213 Chunk {
214 name: name.to_string(),
215 ..Default::default()
216 }
217 }
218
219 pub(crate) fn move_obj(&mut self, dest: &mut Self, checksum: &str) -> bool {
220 if let Some((name, (size, paths))) = self.content.remove_entry(checksum) {
224 let v = dest.content.insert(name, (size, paths));
225 debug_assert!(v.is_none());
226 self.size -= size;
227 dest.size += size;
228 true
229 } else {
230 false
231 }
232 }
233}
234
235impl Chunking {
236 pub fn new(repo: &ostree::Repo, rev: &str) -> Result<Self> {
238 let rev = repo.require_rev(rev)?;
240
241 let (commit_v, _) = repo.load_commit(&rev)?;
243 let commit_v = commit_v.data_as_bytes();
244 let commit_v = commit_v.try_as_aligned()?;
245 let commit = gv_commit!().cast(commit_v);
246 let commit = commit.to_tuple();
247
248 let mut gen = Generation {
250 path: Utf8PathBuf::from("/"),
251 ..Default::default()
252 };
253 let mut chunk: Chunk = Default::default();
254
255 let contents_checksum = &hex::encode(commit.6);
257 let contents_v = repo.load_variant(ostree::ObjectType::DirTree, contents_checksum)?;
258 push_dirtree(repo, &mut gen, contents_checksum)?;
259 let meta_checksum = &hex::encode(commit.7);
260 push_dirmeta(repo, &mut gen, meta_checksum.as_str())?;
261
262 generate_chunking_recurse(repo, &mut gen, &mut chunk, &contents_v)?;
263
264 let chunking = Chunking {
265 metadata_size: gen.metadata_size,
266 remainder: chunk,
267 ..Default::default()
268 };
269 Ok(chunking)
270 }
271
272 pub fn from_mapping(
274 repo: &ostree::Repo,
275 rev: &str,
276 meta: &ObjectMetaSized,
277 max_layers: &Option<NonZeroU32>,
278 prior_build_metadata: Option<&oci_spec::image::ImageManifest>,
279 ) -> Result<Self> {
280 let mut r = Self::new(repo, rev)?;
281 r.process_mapping(meta, max_layers, prior_build_metadata)?;
282 Ok(r)
283 }
284
285 fn remaining(&self) -> u32 {
286 self.max.saturating_sub(self.chunks.len() as u32)
287 }
288
289 #[allow(clippy::or_fun_call)]
292 pub fn process_mapping(
293 &mut self,
294 meta: &ObjectMetaSized,
295 max_layers: &Option<NonZeroU32>,
296 prior_build_metadata: Option<&oci_spec::image::ImageManifest>,
297 ) -> Result<()> {
298 self.max = max_layers
299 .unwrap_or(NonZeroU32::new(MAX_CHUNKS).unwrap())
300 .get();
301
302 let sizes = &meta.sizes;
303 assert!(!self.processed_mapping);
305 self.processed_mapping = true;
306 let remaining = self.remaining();
307 if remaining == 0 {
308 return Ok(());
309 }
310
311 let mut rmap = IndexMap::<ContentID, Vec<&String>>::new();
313 for (checksum, contentid) in meta.map.iter() {
314 rmap.entry(Rc::clone(contentid)).or_default().push(checksum);
315 }
316
317 self.n_provided_components = meta.sizes.len().try_into().unwrap();
319 self.n_sized_components = sizes
320 .iter()
321 .filter(|v| v.size > 0)
322 .count()
323 .try_into()
324 .unwrap();
325
326 let start = Instant::now();
328 let packing = basic_packing(
329 sizes,
330 NonZeroU32::new(self.max).unwrap(),
331 prior_build_metadata,
332 )?;
333 let duration = start.elapsed();
334 tracing::debug!("Time elapsed in packing: {:#?}", duration);
335
336 for bin in packing.into_iter() {
337 let name = match bin.len() {
338 0 => Cow::Borrowed("Reserved for new packages"),
339 1 => {
340 let first = bin[0];
341 let first_name = &*first.meta.identifier;
342 Cow::Borrowed(first_name)
343 }
344 2..=5 => {
345 let first = bin[0];
346 let first_name = &*first.meta.identifier;
347 let r = bin.iter().map(|v| &*v.meta.identifier).skip(1).fold(
348 String::from(first_name),
349 |mut acc, v| {
350 write!(acc, " and {}", v).unwrap();
351 acc
352 },
353 );
354 Cow::Owned(r)
355 }
356 n => Cow::Owned(format!("{n} components")),
357 };
358 let mut chunk = Chunk::new(&name);
359 chunk.packages = bin.iter().map(|v| String::from(&*v.meta.name)).collect();
360 for szmeta in bin {
361 for &obj in rmap.get(&szmeta.meta.identifier).unwrap() {
362 self.remainder.move_obj(&mut chunk, obj.as_str());
363 }
364 }
365 self.chunks.push(chunk);
366 }
367
368 assert_eq!(self.remainder.content.len(), 0);
369
370 Ok(())
371 }
372
373 pub(crate) fn take_chunks(&mut self) -> Vec<Chunk> {
374 let mut r = Vec::new();
375 std::mem::swap(&mut self.chunks, &mut r);
376 r
377 }
378
379 pub fn print(&self) {
381 println!("Metadata: {}", glib::format_size(self.metadata_size));
382 if self.n_provided_components > 0 {
383 println!(
384 "Components: provided={} sized={}",
385 self.n_provided_components, self.n_sized_components
386 );
387 }
388 for (n, chunk) in self.chunks.iter().enumerate() {
389 let sz = glib::format_size(chunk.size);
390 println!(
391 "Chunk {}: \"{}\": objects:{} size:{}",
392 n,
393 chunk.name,
394 chunk.content.len(),
395 sz
396 );
397 }
398 if !self.remainder.content.is_empty() {
399 let sz = glib::format_size(self.remainder.size);
400 println!(
401 "Remainder: \"{}\": objects:{} size:{}",
402 self.remainder.name,
403 self.remainder.content.len(),
404 sz
405 );
406 }
407 }
408}
409
410#[cfg(test)]
411fn components_size(components: &[&ObjectSourceMetaSized]) -> u64 {
412 components.iter().map(|k| k.size).sum()
413}
414
415#[cfg(test)]
417fn packing_size(packing: &[Vec<&ObjectSourceMetaSized>]) -> u64 {
418 packing.iter().map(|v| components_size(v)).sum()
419}
420
421fn get_partitions_with_threshold<'a>(
434 components: &[&'a ObjectSourceMetaSized],
435 limit_hs_bins: usize,
436 threshold: f64,
437) -> Option<BTreeMap<String, Vec<&'a ObjectSourceMetaSized>>> {
438 let mut partitions: BTreeMap<String, Vec<&ObjectSourceMetaSized>> = BTreeMap::new();
439 let mut med_size: Vec<&ObjectSourceMetaSized> = Vec::new();
440 let mut high_size: Vec<&ObjectSourceMetaSized> = Vec::new();
441
442 let mut sizes: Vec<u64> = components.iter().map(|a| a.size).collect();
443 let (median_size, mad_size) = statistics::median_absolute_deviation(&mut sizes)?;
444
445 let size_low_limit = 0.5 * f64::abs(median_size - threshold * mad_size);
447 let size_high_limit = median_size + threshold * mad_size;
448
449 for pkg in components {
450 let size = pkg.size as f64;
451
452 if size >= size_high_limit {
454 high_size.push(pkg);
455 }
456 else if size <= size_low_limit {
458 partitions
459 .entry(LOW_PARTITION.to_string())
460 .and_modify(|bin| bin.push(pkg))
461 .or_insert_with(|| vec![pkg]);
462 }
463 else {
465 med_size.push(pkg);
466 }
467 }
468
469 let mut remaining_pkgs: Vec<_> = if high_size.len() <= limit_hs_bins {
471 Vec::new()
472 } else {
473 high_size.drain(limit_hs_bins..).collect()
474 };
475 assert!(high_size.len() <= limit_hs_bins);
476
477 remaining_pkgs.append(&mut med_size);
479 partitions.insert(HIGH_PARTITION.to_string(), high_size);
480
481 remaining_pkgs.sort_by(|a, b| {
483 a.meta
484 .change_frequency
485 .partial_cmp(&b.meta.change_frequency)
486 .unwrap()
487 });
488 let med_sizes: Vec<u64> = remaining_pkgs.iter().map(|a| a.size).collect();
489 let med_frequencies: Vec<u64> = remaining_pkgs
490 .iter()
491 .map(|a| a.meta.change_frequency.into())
492 .collect();
493
494 let med_mean_freq = statistics::mean(&med_frequencies)?;
495 let med_stddev_freq = statistics::std_deviation(&med_frequencies)?;
496 let med_mean_size = statistics::mean(&med_sizes)?;
497 let med_stddev_size = statistics::std_deviation(&med_sizes)?;
498
499 let med_freq_low_limit = 0.5f64 * f64::abs(med_mean_freq - threshold * med_stddev_freq);
501 let med_freq_high_limit = med_mean_freq + threshold * med_stddev_freq;
502 let med_size_low_limit = 0.5f64 * f64::abs(med_mean_size - threshold * med_stddev_size);
503 let med_size_high_limit = med_mean_size + threshold * med_stddev_size;
504
505 for pkg in remaining_pkgs {
506 let size = pkg.size as f64;
507 let freq = pkg.meta.change_frequency as f64;
508
509 let size_name;
510 if size >= med_size_high_limit {
511 size_name = "hs";
512 } else if size <= med_size_low_limit {
513 size_name = "ls";
514 } else {
515 size_name = "ms";
516 }
517
518 let freq_name;
520 if freq >= med_freq_high_limit {
521 freq_name = "3hf";
522 } else if freq <= med_freq_low_limit {
523 freq_name = "5lf";
524 } else {
525 freq_name = "4mf";
526 }
527
528 let bucket = format!("{freq_name}_{size_name}");
529 partitions
530 .entry(bucket.to_string())
531 .and_modify(|bin| bin.push(pkg))
532 .or_insert_with(|| vec![pkg]);
533 }
534
535 for (name, pkgs) in &partitions {
536 tracing::debug!("{:#?}: {:#?}", name, pkgs.len());
537 }
538
539 Some(partitions)
540}
541
542fn basic_packing_with_prior_build<'a>(
553 components: &'a [ObjectSourceMetaSized],
554 bin_size: NonZeroU32,
555 prior_build: &oci_spec::image::ImageManifest,
556) -> Result<Vec<Vec<&'a ObjectSourceMetaSized>>> {
557 let before_processing_pkgs_len = components.len();
558
559 tracing::debug!("Keeping old package structure");
560
561 let curr_build: Result<Vec<Vec<String>>> = prior_build
564 .layers()
565 .iter()
566 .skip(1)
567 .map(|layer| -> Result<_> {
568 let annotation_layer = layer
569 .annotations()
570 .as_ref()
571 .and_then(|annos| annos.get(CONTENT_ANNOTATION))
572 .ok_or_else(|| anyhow!("Missing {CONTENT_ANNOTATION} on prior build"))?;
573 Ok(annotation_layer
574 .split(COMPONENT_SEPARATOR)
575 .map(ToOwned::to_owned)
576 .collect())
577 })
578 .collect();
579 let mut curr_build = curr_build?;
580
581 let prev_pkgs_set: BTreeSet<String> = curr_build
583 .iter()
584 .flat_map(|v| v.iter().cloned())
585 .filter(|name| !name.is_empty())
586 .collect();
587 let curr_pkgs_set: BTreeSet<String> = components
588 .iter()
589 .map(|pkg| pkg.meta.name.to_string())
590 .collect();
591
592 if let Some(last_bin) = curr_build.last_mut() {
594 let added = curr_pkgs_set.difference(&prev_pkgs_set);
595 last_bin.retain(|name| !name.is_empty());
596 last_bin.extend(added.into_iter().cloned());
597 } else {
598 panic!("No empty last bin for added packages");
599 }
600
601 let removed: BTreeSet<&String> = prev_pkgs_set.difference(&curr_pkgs_set).collect();
603 for bin in curr_build.iter_mut() {
604 bin.retain(|pkg| !removed.contains(pkg));
605 }
606
607 let mut name_to_component: BTreeMap<String, &ObjectSourceMetaSized> = BTreeMap::new();
609 for component in components.iter() {
610 name_to_component
611 .entry(component.meta.name.to_string())
612 .or_insert(component);
613 }
614 let mut modified_build: Vec<Vec<&ObjectSourceMetaSized>> = Vec::new();
615 for bin in curr_build {
616 let mut mod_bin = Vec::new();
617 for pkg in bin {
618 if pkg.is_empty() {
620 continue;
621 }
622 mod_bin.push(name_to_component[&pkg]);
623 }
624 modified_build.push(mod_bin);
625 }
626
627 let after_processing_pkgs_len: usize = modified_build.iter().map(|b| b.len()).sum();
629 assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len);
630 assert!(modified_build.len() <= bin_size.get() as usize);
631 Ok(modified_build)
632}
633
634fn basic_packing<'a>(
650 components: &'a [ObjectSourceMetaSized],
651 bin_size: NonZeroU32,
652 prior_build_metadata: Option<&oci_spec::image::ImageManifest>,
653) -> Result<Vec<Vec<&'a ObjectSourceMetaSized>>> {
654 const HIGH_SIZE_CUTOFF: f32 = 0.6;
655 let before_processing_pkgs_len = components.len();
656
657 anyhow::ensure!(bin_size.get() >= MIN_CHUNKED_LAYERS);
658
659 if let Some(prior_build) = prior_build_metadata {
661 return basic_packing_with_prior_build(components, bin_size, prior_build);
662 }
663
664 tracing::debug!("Creating new packing structure");
665
666 if before_processing_pkgs_len < bin_size.get() as usize {
669 let mut r = components.iter().map(|pkg| vec![pkg]).collect::<Vec<_>>();
670 if before_processing_pkgs_len > 0 {
671 let new_pkgs_bin: Vec<&ObjectSourceMetaSized> = Vec::new();
672 r.push(new_pkgs_bin);
673 }
674 return Ok(r);
675 }
676
677 let mut r = Vec::new();
678 let (components, max_freq_components) = components
680 .iter()
681 .partition::<Vec<_>, _>(|pkg| pkg.meta.change_frequency != u32::MAX);
682 if !components.is_empty() {
683 let limit_ls_bins = 1usize;
686 let limit_new_bins = 1usize;
687 let _limit_new_pkgs = 0usize;
688 let limit_max_frequency_pkgs = max_freq_components.len();
689 let limit_max_frequency_bins = limit_max_frequency_pkgs.min(1);
690 let low_and_other_bin_limit = limit_ls_bins + limit_new_bins + limit_max_frequency_bins;
691 let limit_hs_bins = (HIGH_SIZE_CUTOFF
692 * (bin_size.get() - low_and_other_bin_limit as u32) as f32)
693 .floor() as usize;
694 let limit_ms_bins =
695 (bin_size.get() - (limit_hs_bins + low_and_other_bin_limit) as u32) as usize;
696 let partitions = get_partitions_with_threshold(&components, limit_hs_bins, 2f64)
697 .expect("Partitioning components into sets");
698
699 let low_sized_component_count = partitions
701 .get(LOW_PARTITION)
702 .map(|p| p.len())
703 .unwrap_or_default();
704
705 let pkg_per_bin_ms: usize = (components.len() - limit_hs_bins - low_sized_component_count)
707 .checked_div(limit_ms_bins)
708 .ok_or_else(|| anyhow::anyhow!("number of bins should be >= {}", MIN_CHUNKED_LAYERS))?;
709
710 for (partition, pkgs) in partitions.iter() {
712 if partition == HIGH_PARTITION {
713 for pkg in pkgs {
714 r.push(vec![*pkg]);
715 }
716 } else if partition == LOW_PARTITION {
717 let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new();
718 for pkg in pkgs {
719 bin.push(*pkg);
720 }
721 r.push(bin);
722 } else {
723 let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new();
724 for (i, pkg) in pkgs.iter().enumerate() {
725 if bin.len() < pkg_per_bin_ms {
726 bin.push(*pkg);
727 } else {
728 r.push(bin.clone());
729 bin.clear();
730 bin.push(*pkg);
731 }
732 if i == pkgs.len() - 1 && !bin.is_empty() {
733 r.push(bin.clone());
734 bin.clear();
735 }
736 }
737 }
738 }
739 tracing::debug!("Bins before unoptimized build: {}", r.len());
740
741 while r.len() > (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins) {
751 for i in (limit_ls_bins + limit_hs_bins..r.len() - 1)
752 .step_by(2)
753 .rev()
754 {
755 if r.len() <= (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins)
756 {
757 break;
758 }
759 let prev = &r[i - 1];
760 let curr = &r[i];
761 let mut merge: Vec<&ObjectSourceMetaSized> = Vec::new();
762 merge.extend(prev.iter());
763 merge.extend(curr.iter());
764 r.remove(i);
765 r.remove(i - 1);
766 r.insert(i, merge);
767 }
768 }
769 tracing::debug!("Bins after optimization: {}", r.len());
770 }
771
772 if !max_freq_components.is_empty() {
773 r.push(max_freq_components);
774 }
775
776 r.push(Vec::new());
778 let after_processing_pkgs_len = r.iter().map(|b| b.len()).sum::<usize>();
779 assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len);
780 assert!(r.len() <= bin_size.get() as usize);
781 Ok(r)
782}
783
784#[cfg(test)]
785mod test {
786 use super::*;
787
788 use oci_spec::image as oci_image;
789 use std::str::FromStr;
790
791 const FCOS_CONTENTMETA: &[u8] = include_bytes!("fixtures/fedora-coreos-contentmeta.json.gz");
792 const SHA256_EXAMPLE: &str =
793 "sha256:0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff";
794
795 #[test]
796 fn test_packing_basics() -> Result<()> {
797 for v in [4, 7].map(|v| NonZeroU32::new(v).unwrap()) {
799 assert_eq!(basic_packing(&[], v, None).unwrap().len(), 0);
800 }
801 Ok(())
802 }
803
804 #[test]
805 fn test_packing_fcos() -> Result<()> {
806 let contentmeta: Vec<ObjectSourceMetaSized> =
807 serde_json::from_reader(flate2::read::GzDecoder::new(FCOS_CONTENTMETA))?;
808 let total_size = contentmeta.iter().map(|v| v.size).sum::<u64>();
809
810 let packing =
811 basic_packing(&contentmeta, NonZeroU32::new(MAX_CHUNKS).unwrap(), None).unwrap();
812 assert!(!contentmeta.is_empty());
813 assert_eq!(packing.len() as u32, MAX_CHUNKS);
815 let packed_total_size = packing_size(&packing);
817 assert_eq!(total_size, packed_total_size);
818 Ok(())
819 }
820
821 #[test]
822 fn test_packing_one_layer() -> Result<()> {
823 let contentmeta: Vec<ObjectSourceMetaSized> =
824 serde_json::from_reader(flate2::read::GzDecoder::new(FCOS_CONTENTMETA))?;
825 let r = basic_packing(&contentmeta, NonZeroU32::new(1).unwrap(), None);
826 assert!(r.is_err());
827 Ok(())
828 }
829
830 fn create_manifest(prev_expected_structure: Vec<Vec<&str>>) -> oci_spec::image::ImageManifest {
831 use std::collections::HashMap;
832
833 let mut p = prev_expected_structure
834 .iter()
835 .map(|b| {
836 b.iter()
837 .map(|p| p.split('.').collect::<Vec<&str>>()[0].to_string())
838 .collect()
839 })
840 .collect();
841 let mut metadata_with_ostree_commit = vec![vec![String::from("ostree_commit")]];
842 metadata_with_ostree_commit.append(&mut p);
843
844 let config = oci_spec::image::DescriptorBuilder::default()
845 .media_type(oci_spec::image::MediaType::ImageConfig)
846 .size(7023_u64)
847 .digest(oci_image::Digest::from_str(SHA256_EXAMPLE).unwrap())
848 .build()
849 .expect("build config descriptor");
850
851 let layers: Vec<oci_spec::image::Descriptor> = metadata_with_ostree_commit
852 .iter()
853 .map(|l| {
854 let mut buf = [0; 8];
855 let sep = COMPONENT_SEPARATOR.encode_utf8(&mut buf);
856 oci_spec::image::DescriptorBuilder::default()
857 .media_type(oci_spec::image::MediaType::ImageLayerGzip)
858 .size(100_u64)
859 .digest(oci_image::Digest::from_str(SHA256_EXAMPLE).unwrap())
860 .annotations(HashMap::from([(
861 CONTENT_ANNOTATION.to_string(),
862 l.join(sep),
863 )]))
864 .build()
865 .expect("build layer")
866 })
867 .collect();
868
869 let image_manifest = oci_spec::image::ImageManifestBuilder::default()
870 .schema_version(oci_spec::image::SCHEMA_VERSION)
871 .config(config)
872 .layers(layers)
873 .build()
874 .expect("build image manifest");
875 image_manifest
876 }
877
878 #[test]
879 fn test_advanced_packing() -> Result<()> {
880 let contentmeta_v0: Vec<ObjectSourceMetaSized> = vec![
882 vec![1, u32::MAX, 100000],
883 vec![2, u32::MAX, 99999],
884 vec![3, 30, 99998],
885 vec![4, 100, 99997],
886 vec![10, 51, 1000],
887 vec![8, 50, 500],
888 vec![9, 1, 200],
889 vec![11, 100000, 199],
890 vec![6, 30, 2],
891 vec![7, 30, 1],
892 ]
893 .iter()
894 .map(|data| ObjectSourceMetaSized {
895 meta: ObjectSourceMeta {
896 identifier: RcStr::from(format!("pkg{}.0", data[0])),
897 name: RcStr::from(format!("pkg{}", data[0])),
898 srcid: RcStr::from(format!("srcpkg{}", data[0])),
899 change_time_offset: 0,
900 change_frequency: data[1],
901 },
902 size: data[2] as u64,
903 })
904 .collect();
905
906 let packing = basic_packing(
907 &contentmeta_v0.as_slice(),
908 NonZeroU32::new(6).unwrap(),
909 None,
910 )
911 .unwrap();
912 let structure: Vec<Vec<&str>> = packing
913 .iter()
914 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
915 .collect();
916 let v0_expected_structure = vec![
917 vec!["pkg3.0"],
918 vec!["pkg4.0"],
919 vec!["pkg6.0", "pkg7.0", "pkg11.0"],
920 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
921 vec!["pkg1.0", "pkg2.0"],
922 vec![],
923 ];
924 assert_eq!(structure, v0_expected_structure);
925
926 let mut contentmeta_v1: Vec<ObjectSourceMetaSized> = contentmeta_v0;
929 contentmeta_v1[0].meta.identifier = RcStr::from("pkg1.1");
931 contentmeta_v1.remove(contentmeta_v1.len() - 1);
933 contentmeta_v1.push(ObjectSourceMetaSized {
935 meta: ObjectSourceMeta {
936 identifier: RcStr::from("pkg5.0"),
937 name: RcStr::from("pkg5"),
938 srcid: RcStr::from("srcpkg5"),
939 change_time_offset: 0,
940 change_frequency: 42,
941 },
942 size: 100000,
943 });
944
945 let image_manifest_v0 = create_manifest(v0_expected_structure);
946 let packing_derived = basic_packing(
947 &contentmeta_v1.as_slice(),
948 NonZeroU32::new(6).unwrap(),
949 Some(&image_manifest_v0),
950 )
951 .unwrap();
952 let structure_derived: Vec<Vec<&str>> = packing_derived
953 .iter()
954 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
955 .collect();
956 let v1_expected_structure = vec![
957 vec!["pkg3.0"],
958 vec!["pkg4.0"],
959 vec!["pkg6.0", "pkg11.0"],
960 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
961 vec!["pkg1.1", "pkg2.0"],
962 vec!["pkg5.0"],
963 ];
964
965 assert_eq!(structure_derived, v1_expected_structure);
966
967 let mut contentmeta_v2: Vec<ObjectSourceMetaSized> = contentmeta_v1;
970 contentmeta_v2[9].meta.identifier = RcStr::from("pkg5.1");
972 contentmeta_v2.push(ObjectSourceMetaSized {
974 meta: ObjectSourceMeta {
975 identifier: RcStr::from("pkg12.0"),
976 name: RcStr::from("pkg12"),
977 srcid: RcStr::from("srcpkg12"),
978 change_time_offset: 0,
979 change_frequency: 42,
980 },
981 size: 100000,
982 });
983
984 let image_manifest_v1 = create_manifest(v1_expected_structure);
985 let packing_derived = basic_packing(
986 &contentmeta_v2.as_slice(),
987 NonZeroU32::new(6).unwrap(),
988 Some(&image_manifest_v1),
989 )
990 .unwrap();
991 let structure_derived: Vec<Vec<&str>> = packing_derived
992 .iter()
993 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
994 .collect();
995 let v2_expected_structure = vec![
996 vec!["pkg3.0"],
997 vec!["pkg4.0"],
998 vec!["pkg6.0", "pkg11.0"],
999 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
1000 vec!["pkg1.1", "pkg2.0"],
1001 vec!["pkg5.1", "pkg12.0"],
1002 ];
1003
1004 assert_eq!(structure_derived, v2_expected_structure);
1005 Ok(())
1006 }
1007}