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::{Result, anyhow};
18use camino::{Utf8Path, 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, Clone, 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, generation: &mut Generation, checksum: &str) -> Result<()> {
140 if generation.dirtree_found.contains(checksum) {
141 return Ok(());
142 }
143 let checksum = RcStr::from(checksum);
144 generation.dirmeta_found.insert(RcStr::clone(&checksum));
145 let child_v = repo.load_variant(ostree::ObjectType::DirMeta, checksum.borrow())?;
146 generation.metadata_size += child_v.data_as_bytes().as_ref().len() as u64;
147 Ok(())
148}
149
150fn push_dirtree(
151 repo: &ostree::Repo,
152 generation: &mut Generation,
153 checksum: &str,
154) -> Result<glib::Variant> {
155 let child_v = repo.load_variant(ostree::ObjectType::DirTree, checksum)?;
156 if !generation.dirtree_found.contains(checksum) {
157 generation.metadata_size += child_v.data_as_bytes().as_ref().len() as u64;
158 } else {
159 let checksum = RcStr::from(checksum);
160 generation.dirtree_found.insert(checksum);
161 }
162 Ok(child_v)
163}
164
165fn generate_chunking_recurse(
166 repo: &ostree::Repo,
167 generation: &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 = generation.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 generation.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, generation, checksum_s)?;
200 generate_chunking_recurse(repo, generation, 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, generation, checksum_s)?;
205 assert!(generation.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 pub(crate) fn move_path(&mut self, dest: &mut Self, checksum: &str, path: &Utf8Path) {
235 if let Some((_size, paths)) = self.content.get_mut(checksum) {
236 let path_index = paths.iter().position(|p| *p == path);
237 if let Some(index) = path_index {
238 let removed_path = paths.remove(index);
239
240 let dest_entry = dest
241 .content
242 .entry(RcStr::from(checksum))
243 .or_insert((0, Vec::new()));
244 dest_entry.1.push(removed_path);
245
246 if paths.is_empty() {
247 self.content.remove(checksum);
248 }
249 }
250 }
251 }
252}
253
254impl Chunking {
255 fn create_content_id_map(
257 map: &IndexMap<String, ContentID>,
258 ) -> IndexMap<ContentID, Vec<&String>> {
259 let mut rmap = IndexMap::<ContentID, Vec<&String>>::new();
260 for (checksum, contentid) in map.iter() {
261 rmap.entry(Rc::clone(contentid)).or_default().push(checksum);
262 }
263 rmap
264 }
265
266 pub fn new(repo: &ostree::Repo, rev: &str) -> Result<Self> {
268 let rev = repo.require_rev(rev)?;
270
271 let (commit_v, _) = repo.load_commit(&rev)?;
273 let commit_v = commit_v.data_as_bytes();
274 let commit_v = commit_v.try_as_aligned()?;
275 let commit = gv_commit!().cast(commit_v);
276 let commit = commit.to_tuple();
277
278 let mut generation = Generation {
280 path: Utf8PathBuf::from("/"),
281 ..Default::default()
282 };
283 let mut chunk: Chunk = Default::default();
284
285 let contents_checksum = &hex::encode(commit.6);
287 let contents_v = repo.load_variant(ostree::ObjectType::DirTree, contents_checksum)?;
288 push_dirtree(repo, &mut generation, contents_checksum)?;
289 let meta_checksum = &hex::encode(commit.7);
290 push_dirmeta(repo, &mut generation, meta_checksum.as_str())?;
291
292 generate_chunking_recurse(repo, &mut generation, &mut chunk, &contents_v)?;
293
294 let chunking = Chunking {
295 metadata_size: generation.metadata_size,
296 remainder: chunk,
297 ..Default::default()
298 };
299 Ok(chunking)
300 }
301
302 pub fn from_mapping(
304 repo: &ostree::Repo,
305 rev: &str,
306 meta: &ObjectMetaSized,
307 max_layers: &Option<NonZeroU32>,
308 prior_build_metadata: Option<&oci_spec::image::ImageManifest>,
309 specific_contentmeta: Option<&BTreeMap<ContentID, Vec<(Utf8PathBuf, String)>>>,
310 ) -> Result<Self> {
311 let mut r = Self::new(repo, rev)?;
312 r.process_mapping(meta, max_layers, prior_build_metadata, specific_contentmeta)?;
313 Ok(r)
314 }
315
316 fn remaining(&self) -> u32 {
317 self.max.saturating_sub(self.chunks.len() as u32)
318 }
319
320 #[allow(clippy::or_fun_call)]
323 pub fn process_mapping(
324 &mut self,
325 meta: &ObjectMetaSized,
326 max_layers: &Option<NonZeroU32>,
327 prior_build_metadata: Option<&oci_spec::image::ImageManifest>,
328 specific_contentmeta: Option<&BTreeMap<ContentID, Vec<(Utf8PathBuf, String)>>>,
329 ) -> Result<()> {
330 self.max = max_layers
331 .unwrap_or(NonZeroU32::new(MAX_CHUNKS).unwrap())
332 .get();
333
334 let sizes = &meta.sizes;
335 assert!(!self.processed_mapping);
337 self.processed_mapping = true;
338 let remaining = self.remaining();
339 if remaining == 0 {
340 return Ok(());
341 }
342
343 let mut processed_specific_components = BTreeSet::new();
345 if let Some(specific_meta) = specific_contentmeta {
346 for (component, files) in specific_meta {
347 let mut chunk = Chunk::new(&component);
348 chunk.packages = vec![component.to_string()];
349
350 for (path, checksum) in files {
352 self.remainder
353 .move_path(&mut chunk, checksum.as_str(), path);
354 }
355
356 self.chunks.push(chunk);
357 processed_specific_components.insert(component.clone());
358 }
359 }
360
361 self.n_provided_components = meta.sizes.len().try_into().unwrap();
363 self.n_sized_components = sizes
364 .iter()
365 .filter(|v| v.size > 0)
366 .count()
367 .try_into()
368 .unwrap();
369
370 let regular_sizes: Vec<ObjectSourceMetaSized> = sizes
372 .iter()
373 .filter(|component| {
374 !processed_specific_components.contains(&*component.meta.identifier)
375 })
376 .cloned()
377 .collect();
378
379 let rmap = Self::create_content_id_map(&meta.map);
380
381 if let Some(remaining) = NonZeroU32::new(self.remaining()) {
383 let start = Instant::now();
384 let packing = basic_packing(®ular_sizes, remaining, prior_build_metadata)?;
385 let duration = start.elapsed();
386 tracing::debug!("Time elapsed in packing: {:#?}", duration);
387
388 for bin in packing.into_iter() {
389 let name = match bin.len() {
390 0 => Cow::Borrowed("Reserved for new packages"),
391 1 => {
392 let first = bin[0];
393 let first_name = &*first.meta.identifier;
394 Cow::Borrowed(first_name)
395 }
396 2..=5 => {
397 let first = bin[0];
398 let first_name = &*first.meta.identifier;
399 let r = bin.iter().map(|v| &*v.meta.identifier).skip(1).fold(
400 String::from(first_name),
401 |mut acc, v| {
402 write!(acc, " and {v}").unwrap();
403 acc
404 },
405 );
406 Cow::Owned(r)
407 }
408 n => Cow::Owned(format!("{n} components")),
409 };
410 let mut chunk = Chunk::new(&name);
411 chunk.packages = bin.iter().map(|v| String::from(&*v.meta.name)).collect();
412 for szmeta in bin {
413 for &obj in rmap.get(&szmeta.meta.identifier).unwrap() {
414 self.remainder.move_obj(&mut chunk, obj.as_str());
415 }
416 }
417 self.chunks.push(chunk);
418 }
419 }
420
421 if !processed_specific_components.is_empty() || !regular_sizes.is_empty() {
423 assert_eq!(self.remainder.content.len(), 0);
424 }
425
426 Ok(())
427 }
428
429 pub(crate) fn take_chunks(&mut self) -> Vec<Chunk> {
430 let mut r = Vec::new();
431 std::mem::swap(&mut self.chunks, &mut r);
432 r
433 }
434
435 pub fn print(&self) {
437 println!("Metadata: {}", glib::format_size(self.metadata_size));
438 if self.n_provided_components > 0 {
439 println!(
440 "Components: provided={} sized={}",
441 self.n_provided_components, self.n_sized_components
442 );
443 }
444 for (n, chunk) in self.chunks.iter().enumerate() {
445 let sz = glib::format_size(chunk.size);
446 println!(
447 "Chunk {}: \"{}\": objects:{} size:{}",
448 n,
449 chunk.name,
450 chunk.content.len(),
451 sz
452 );
453 }
454 if !self.remainder.content.is_empty() {
455 let sz = glib::format_size(self.remainder.size);
456 println!(
457 "Remainder: \"{}\": objects:{} size:{}",
458 self.remainder.name,
459 self.remainder.content.len(),
460 sz
461 );
462 }
463 }
464}
465
466#[cfg(test)]
467fn components_size(components: &[&ObjectSourceMetaSized]) -> u64 {
468 components.iter().map(|k| k.size).sum()
469}
470
471#[cfg(test)]
473fn packing_size(packing: &[Vec<&ObjectSourceMetaSized>]) -> u64 {
474 packing.iter().map(|v| components_size(v)).sum()
475}
476
477fn get_partitions_with_threshold<'a>(
490 components: &[&'a ObjectSourceMetaSized],
491 limit_hs_bins: usize,
492 threshold: f64,
493) -> Option<BTreeMap<String, Vec<&'a ObjectSourceMetaSized>>> {
494 let mut partitions: BTreeMap<String, Vec<&ObjectSourceMetaSized>> = BTreeMap::new();
495 let mut med_size: Vec<&ObjectSourceMetaSized> = Vec::new();
496 let mut high_size: Vec<&ObjectSourceMetaSized> = Vec::new();
497
498 let mut sizes: Vec<u64> = components.iter().map(|a| a.size).collect();
499 let (median_size, mad_size) = statistics::median_absolute_deviation(&mut sizes)?;
500
501 let size_low_limit = 0.5 * f64::abs(median_size - threshold * mad_size);
503 let size_high_limit = median_size + threshold * mad_size;
504
505 for pkg in components {
506 let size = pkg.size as f64;
507
508 if size >= size_high_limit {
510 high_size.push(pkg);
511 }
512 else if size <= size_low_limit {
514 partitions
515 .entry(LOW_PARTITION.to_string())
516 .and_modify(|bin| bin.push(pkg))
517 .or_insert_with(|| vec![pkg]);
518 }
519 else {
521 med_size.push(pkg);
522 }
523 }
524
525 let mut remaining_pkgs: Vec<_> = if high_size.len() <= limit_hs_bins {
527 Vec::new()
528 } else {
529 high_size.drain(limit_hs_bins..).collect()
530 };
531 assert!(high_size.len() <= limit_hs_bins);
532
533 remaining_pkgs.append(&mut med_size);
535 partitions.insert(HIGH_PARTITION.to_string(), high_size);
536
537 remaining_pkgs.sort_by(|a, b| {
539 a.meta
540 .change_frequency
541 .partial_cmp(&b.meta.change_frequency)
542 .unwrap()
543 });
544 let med_sizes: Vec<u64> = remaining_pkgs.iter().map(|a| a.size).collect();
545 let med_frequencies: Vec<u64> = remaining_pkgs
546 .iter()
547 .map(|a| a.meta.change_frequency.into())
548 .collect();
549
550 let med_mean_freq = statistics::mean(&med_frequencies)?;
551 let med_stddev_freq = statistics::std_deviation(&med_frequencies)?;
552 let med_mean_size = statistics::mean(&med_sizes)?;
553 let med_stddev_size = statistics::std_deviation(&med_sizes)?;
554
555 let med_freq_low_limit = 0.5f64 * f64::abs(med_mean_freq - threshold * med_stddev_freq);
557 let med_freq_high_limit = med_mean_freq + threshold * med_stddev_freq;
558 let med_size_low_limit = 0.5f64 * f64::abs(med_mean_size - threshold * med_stddev_size);
559 let med_size_high_limit = med_mean_size + threshold * med_stddev_size;
560
561 for pkg in remaining_pkgs {
562 let size = pkg.size as f64;
563 let freq = pkg.meta.change_frequency as f64;
564
565 let size_name;
566 if size >= med_size_high_limit {
567 size_name = "hs";
568 } else if size <= med_size_low_limit {
569 size_name = "ls";
570 } else {
571 size_name = "ms";
572 }
573
574 let freq_name;
576 if freq >= med_freq_high_limit {
577 freq_name = "3hf";
578 } else if freq <= med_freq_low_limit {
579 freq_name = "5lf";
580 } else {
581 freq_name = "4mf";
582 }
583
584 let bucket = format!("{freq_name}_{size_name}");
585 partitions
586 .entry(bucket.to_string())
587 .and_modify(|bin| bin.push(pkg))
588 .or_insert_with(|| vec![pkg]);
589 }
590
591 for (name, pkgs) in &partitions {
592 tracing::debug!("{:#?}: {:#?}", name, pkgs.len());
593 }
594
595 Some(partitions)
596}
597
598fn basic_packing_with_prior_build<'a>(
612 components: &'a [ObjectSourceMetaSized],
613 bin_size: NonZeroU32,
614 prior_build: &oci_spec::image::ImageManifest,
615) -> Result<Option<Vec<Vec<&'a ObjectSourceMetaSized>>>> {
616 let before_processing_pkgs_len = components.len();
617
618 tracing::debug!("Attempting to use old package structure");
619
620 let curr_build: Result<Vec<Vec<String>>> = prior_build
623 .layers()
624 .iter()
625 .skip(1)
626 .map(|layer| -> Result<_> {
627 let annotation_layer = layer
628 .annotations()
629 .as_ref()
630 .and_then(|annos| annos.get(CONTENT_ANNOTATION))
631 .ok_or_else(|| anyhow!("Missing {CONTENT_ANNOTATION} on prior build"))?;
632 Ok(annotation_layer
633 .split(COMPONENT_SEPARATOR)
634 .map(ToOwned::to_owned)
635 .collect())
636 })
637 .collect();
638 let mut curr_build = curr_build?;
639
640 let prev_pkgs_set: BTreeSet<String> = curr_build
642 .iter()
643 .flat_map(|v| v.iter().cloned())
644 .filter(|name| !name.is_empty())
645 .collect();
646 let curr_pkgs_set: BTreeSet<String> = components
647 .iter()
648 .map(|pkg| pkg.meta.name.to_string())
649 .collect();
650
651 let mut prior_bins = BTreeMap::new();
655 let mut split_names = BTreeSet::new();
656 for (idx, bin) in curr_build.iter().enumerate() {
657 for name in bin.iter().filter(|name| !name.is_empty()) {
658 if prior_bins.insert(name, idx).is_some_and(|prev| prev != idx) {
659 split_names.insert(name.clone());
660 }
661 }
662 }
663 for bin in &mut curr_build {
664 bin.retain(|name| !split_names.contains(name));
665 }
666
667 if let Some(last_bin) = curr_build.last_mut() {
669 let added = curr_pkgs_set.difference(&prev_pkgs_set);
670 last_bin.retain(|name| !name.is_empty());
671 last_bin.extend(added.into_iter().cloned());
672 last_bin.extend(split_names.intersection(&curr_pkgs_set).cloned());
673 } else {
674 tracing::debug!("No empty last bin for added packages.");
675 return Ok(None);
676 }
677
678 let removed: BTreeSet<&String> = prev_pkgs_set.difference(&curr_pkgs_set).collect();
680 for bin in curr_build.iter_mut() {
681 bin.retain(|pkg| !removed.contains(pkg));
682 }
683
684 let last_idx = curr_build.len().saturating_sub(1);
688 curr_build = curr_build
689 .into_iter()
690 .enumerate()
691 .filter_map(|(idx, bin)| (!bin.is_empty() || idx == last_idx).then_some(bin))
692 .collect();
693
694 if (bin_size.get() as usize) < curr_build.len() {
695 tracing::debug!("bin_size = {bin_size} is too small to be compatible with the prior build");
696 return Ok(None);
697 }
698
699 let mut name_to_components: BTreeMap<String, Vec<&ObjectSourceMetaSized>> = BTreeMap::new();
701 for component in components.iter() {
702 name_to_components
703 .entry(component.meta.name.to_string())
704 .or_default()
705 .push(component);
706 }
707 let mut modified_build: Vec<Vec<&ObjectSourceMetaSized>> = Vec::new();
708 for bin in curr_build {
709 let mut mod_bin = Vec::new();
710 for pkg in bin {
711 if pkg.is_empty() {
713 continue;
714 }
715 if let Some(components) = name_to_components.remove(&pkg) {
718 mod_bin.extend(components);
719 }
720 }
721 modified_build.push(mod_bin);
722 }
723
724 let after_processing_pkgs_len: usize = modified_build.iter().map(|b| b.len()).sum();
726 assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len);
727 assert!(modified_build.len() <= bin_size.get() as usize);
728 Ok(Some(modified_build))
729}
730
731fn basic_packing<'a>(
747 components: &'a [ObjectSourceMetaSized],
748 bin_size: NonZeroU32,
749 prior_build_metadata: Option<&oci_spec::image::ImageManifest>,
750) -> Result<Vec<Vec<&'a ObjectSourceMetaSized>>> {
751 const HIGH_SIZE_CUTOFF: f32 = 0.6;
752 let before_processing_pkgs_len = components.len();
753
754 anyhow::ensure!(bin_size.get() >= MIN_CHUNKED_LAYERS);
755
756 if let Some(prior_build) = prior_build_metadata {
758 if let Some(packing) = basic_packing_with_prior_build(components, bin_size, prior_build)? {
759 tracing::debug!("Keeping old package structure");
760 return Ok(packing);
761 } else {
762 tracing::debug!(
763 "Failed to use old package structure; discarding info from prior build."
764 );
765 }
766 }
767
768 tracing::debug!("Creating new packing structure");
769
770 if before_processing_pkgs_len < bin_size.get() as usize {
773 let mut r = components.iter().map(|pkg| vec![pkg]).collect::<Vec<_>>();
774 if before_processing_pkgs_len > 0 {
775 let new_pkgs_bin: Vec<&ObjectSourceMetaSized> = Vec::new();
776 r.push(new_pkgs_bin);
777 }
778 return Ok(r);
779 }
780
781 let mut r = Vec::new();
782 let (components, max_freq_components) = components
784 .iter()
785 .partition::<Vec<_>, _>(|pkg| pkg.meta.change_frequency != u32::MAX);
786 if !components.is_empty() {
787 let limit_ls_bins = 1usize;
790 let limit_new_bins = 1usize;
791 let _limit_new_pkgs = 0usize;
792 let limit_max_frequency_pkgs = max_freq_components.len();
793 let limit_max_frequency_bins = limit_max_frequency_pkgs.min(1);
794 let low_and_other_bin_limit = limit_ls_bins + limit_new_bins + limit_max_frequency_bins;
795 let limit_hs_bins = (HIGH_SIZE_CUTOFF
796 * (bin_size.get() - low_and_other_bin_limit as u32) as f32)
797 .floor() as usize;
798 let limit_ms_bins =
799 (bin_size.get() - (limit_hs_bins + low_and_other_bin_limit) as u32) as usize;
800 let partitions = get_partitions_with_threshold(&components, limit_hs_bins, 2f64)
801 .expect("Partitioning components into sets");
802
803 let low_sized_component_count = partitions
805 .get(LOW_PARTITION)
806 .map(|p| p.len())
807 .unwrap_or_default();
808
809 let pkg_per_bin_ms: usize = (components.len() - limit_hs_bins - low_sized_component_count)
811 .checked_div(limit_ms_bins)
812 .ok_or_else(|| anyhow::anyhow!("number of bins should be >= {}", MIN_CHUNKED_LAYERS))?;
813
814 for (partition, pkgs) in partitions.iter() {
816 if partition == HIGH_PARTITION {
817 for pkg in pkgs {
818 r.push(vec![*pkg]);
819 }
820 } else if partition == LOW_PARTITION {
821 let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new();
822 for pkg in pkgs {
823 bin.push(*pkg);
824 }
825 r.push(bin);
826 } else {
827 let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new();
828 for (i, pkg) in pkgs.iter().enumerate() {
829 if bin.len() < pkg_per_bin_ms {
830 bin.push(*pkg);
831 } else {
832 r.push(bin.clone());
833 bin.clear();
834 bin.push(*pkg);
835 }
836 if i == pkgs.len() - 1 && !bin.is_empty() {
837 r.push(bin.clone());
838 bin.clear();
839 }
840 }
841 }
842 }
843 tracing::debug!("Bins before unoptimized build: {}", r.len());
844
845 while r.len() > (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins) {
855 for i in (limit_ls_bins + limit_hs_bins..r.len() - 1)
856 .step_by(2)
857 .rev()
858 {
859 if r.len() <= (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins)
860 {
861 break;
862 }
863 let prev = &r[i - 1];
864 let curr = &r[i];
865 let mut merge: Vec<&ObjectSourceMetaSized> = Vec::new();
866 merge.extend(prev.iter());
867 merge.extend(curr.iter());
868 r.remove(i);
869 r.remove(i - 1);
870 r.insert(i, merge);
871 }
872 }
873 tracing::debug!("Bins after optimization: {}", r.len());
874 }
875
876 if !max_freq_components.is_empty() {
877 r.push(max_freq_components);
878 }
879
880 r.push(Vec::new());
882 let after_processing_pkgs_len = r.iter().map(|b| b.len()).sum::<usize>();
883 assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len);
884 assert!(r.len() <= bin_size.get() as usize);
885 Ok(r)
886}
887
888#[cfg(test)]
889mod test {
890 use super::*;
891
892 use oci_spec::image as oci_image;
893 use std::str::FromStr;
894
895 const FCOS_CONTENTMETA: &[u8] = include_bytes!("fixtures/fedora-coreos-contentmeta.json.gz");
896 const SHA256_EXAMPLE: &str =
897 "sha256:0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff";
898
899 #[test]
900 fn test_packing_basics() -> Result<()> {
901 for v in [4, 7].map(|v| NonZeroU32::new(v).unwrap()) {
903 assert_eq!(basic_packing(&[], v, None).unwrap().len(), 0);
904 }
905 Ok(())
906 }
907
908 #[test]
909 fn test_packing_fcos() -> Result<()> {
910 let contentmeta: Vec<ObjectSourceMetaSized> =
911 serde_json::from_reader(flate2::read::GzDecoder::new(FCOS_CONTENTMETA))?;
912 let total_size = contentmeta.iter().map(|v| v.size).sum::<u64>();
913
914 let packing =
915 basic_packing(&contentmeta, NonZeroU32::new(MAX_CHUNKS).unwrap(), None).unwrap();
916 assert!(!contentmeta.is_empty());
917 assert_eq!(packing.len() as u32, MAX_CHUNKS);
919 let packed_total_size = packing_size(&packing);
921 assert_eq!(total_size, packed_total_size);
922 Ok(())
923 }
924
925 #[test]
926 fn test_packing_one_layer() -> Result<()> {
927 let contentmeta: Vec<ObjectSourceMetaSized> =
928 serde_json::from_reader(flate2::read::GzDecoder::new(FCOS_CONTENTMETA))?;
929 let r = basic_packing(&contentmeta, NonZeroU32::new(1).unwrap(), None);
930 assert!(r.is_err());
931 Ok(())
932 }
933
934 fn create_manifest(prev_expected_structure: Vec<Vec<&str>>) -> oci_spec::image::ImageManifest {
935 use std::collections::HashMap;
936
937 let mut p = prev_expected_structure
938 .iter()
939 .map(|b| {
940 b.iter()
941 .map(|p| p.split('.').collect::<Vec<&str>>()[0].to_string())
942 .collect()
943 })
944 .collect();
945 let mut metadata_with_ostree_commit = vec![vec![String::from("ostree_commit")]];
946 metadata_with_ostree_commit.append(&mut p);
947
948 let config = oci_spec::image::DescriptorBuilder::default()
949 .media_type(oci_spec::image::MediaType::ImageConfig)
950 .size(7023_u64)
951 .digest(oci_image::Digest::from_str(SHA256_EXAMPLE).unwrap())
952 .build()
953 .expect("build config descriptor");
954
955 let layers: Vec<oci_spec::image::Descriptor> = metadata_with_ostree_commit
956 .iter()
957 .map(|l| {
958 let mut buf = [0; 8];
959 let sep = COMPONENT_SEPARATOR.encode_utf8(&mut buf);
960 oci_spec::image::DescriptorBuilder::default()
961 .media_type(oci_spec::image::MediaType::ImageLayerGzip)
962 .size(100_u64)
963 .digest(oci_image::Digest::from_str(SHA256_EXAMPLE).unwrap())
964 .annotations(HashMap::from([(
965 CONTENT_ANNOTATION.to_string(),
966 l.join(sep),
967 )]))
968 .build()
969 .expect("build layer")
970 })
971 .collect();
972
973 oci_spec::image::ImageManifestBuilder::default()
974 .schema_version(oci_spec::image::SCHEMA_VERSION)
975 .config(config)
976 .layers(layers)
977 .build()
978 .expect("build image manifest")
979 }
980
981 #[test]
982 fn test_advanced_packing() -> Result<()> {
983 let contentmeta_v0: Vec<ObjectSourceMetaSized> = vec![
985 vec![1, u32::MAX, 100000],
986 vec![2, u32::MAX, 99999],
987 vec![3, 30, 99998],
988 vec![4, 100, 99997],
989 vec![10, 51, 1000],
990 vec![8, 50, 500],
991 vec![9, 1, 200],
992 vec![11, 100000, 199],
993 vec![6, 30, 2],
994 vec![7, 30, 1],
995 ]
996 .iter()
997 .map(|data| ObjectSourceMetaSized {
998 meta: ObjectSourceMeta {
999 identifier: RcStr::from(format!("pkg{}.0", data[0])),
1000 name: RcStr::from(format!("pkg{}", data[0])),
1001 srcid: RcStr::from(format!("srcpkg{}", data[0])),
1002 change_time_offset: 0,
1003 change_frequency: data[1],
1004 },
1005 size: data[2] as u64,
1006 })
1007 .collect();
1008
1009 let packing = basic_packing(
1010 &contentmeta_v0.as_slice(),
1011 NonZeroU32::new(6).unwrap(),
1012 None,
1013 )
1014 .unwrap();
1015 let structure: Vec<Vec<&str>> = packing
1016 .iter()
1017 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
1018 .collect();
1019 let v0_expected_structure = vec![
1020 vec!["pkg3.0"],
1021 vec!["pkg4.0"],
1022 vec!["pkg6.0", "pkg7.0", "pkg11.0"],
1023 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
1024 vec!["pkg1.0", "pkg2.0"],
1025 vec![],
1026 ];
1027 assert_eq!(structure, v0_expected_structure);
1028
1029 let mut contentmeta_v1: Vec<ObjectSourceMetaSized> = contentmeta_v0;
1032 contentmeta_v1[0].meta.identifier = RcStr::from("pkg1.1");
1034 contentmeta_v1.remove(contentmeta_v1.len() - 1);
1036 contentmeta_v1.push(ObjectSourceMetaSized {
1038 meta: ObjectSourceMeta {
1039 identifier: RcStr::from("pkg5.0"),
1040 name: RcStr::from("pkg5"),
1041 srcid: RcStr::from("srcpkg5"),
1042 change_time_offset: 0,
1043 change_frequency: 42,
1044 },
1045 size: 100000,
1046 });
1047
1048 let image_manifest_v0 = create_manifest(v0_expected_structure);
1049 let packing_derived = basic_packing(
1050 &contentmeta_v1.as_slice(),
1051 NonZeroU32::new(6).unwrap(),
1052 Some(&image_manifest_v0),
1053 )
1054 .unwrap();
1055 let structure_derived: Vec<Vec<&str>> = packing_derived
1056 .iter()
1057 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
1058 .collect();
1059 let v1_expected_structure = vec![
1060 vec!["pkg3.0"],
1061 vec!["pkg4.0"],
1062 vec!["pkg6.0", "pkg11.0"],
1063 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
1064 vec!["pkg1.1", "pkg2.0"],
1065 vec!["pkg5.0"],
1066 ];
1067
1068 assert_eq!(structure_derived, v1_expected_structure);
1069
1070 let mut contentmeta_v2: Vec<ObjectSourceMetaSized> = contentmeta_v1;
1073 contentmeta_v2[9].meta.identifier = RcStr::from("pkg5.1");
1075 contentmeta_v2.push(ObjectSourceMetaSized {
1077 meta: ObjectSourceMeta {
1078 identifier: RcStr::from("pkg12.0"),
1079 name: RcStr::from("pkg12"),
1080 srcid: RcStr::from("srcpkg12"),
1081 change_time_offset: 0,
1082 change_frequency: 42,
1083 },
1084 size: 100000,
1085 });
1086
1087 let image_manifest_v1 = create_manifest(v1_expected_structure);
1088 let packing_derived = basic_packing(
1089 &contentmeta_v2.as_slice(),
1090 NonZeroU32::new(6).unwrap(),
1091 Some(&image_manifest_v1),
1092 )
1093 .unwrap();
1094 let structure_derived: Vec<Vec<&str>> = packing_derived
1095 .iter()
1096 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
1097 .collect();
1098 let v2_expected_structure = vec![
1099 vec!["pkg3.0"],
1100 vec!["pkg4.0"],
1101 vec!["pkg6.0", "pkg11.0"],
1102 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
1103 vec!["pkg1.1", "pkg2.0"],
1104 vec!["pkg5.1", "pkg12.0"],
1105 ];
1106
1107 assert_eq!(structure_derived, v2_expected_structure);
1108 Ok(())
1109 }
1110
1111 #[test]
1112 fn test_advanced_packing_with_prior_exclusive_components() -> Result<()> {
1113 let contentmeta: Vec<ObjectSourceMetaSized> = [
1114 (1, 100, 50000),
1115 (2, 200, 40000),
1116 (3, 300, 30000),
1117 (4, 400, 20000),
1118 (5, 500, 10000),
1119 (6, 600, 5000),
1120 ]
1121 .iter()
1122 .map(|&(id, freq, size)| ObjectSourceMetaSized {
1123 meta: ObjectSourceMeta {
1124 identifier: RcStr::from(format!("pkg{id}.0")),
1125 name: RcStr::from(format!("pkg{id}")),
1126 srcid: RcStr::from(format!("srcpkg{id}")),
1127 change_time_offset: 0,
1128 change_frequency: freq,
1129 },
1130 size,
1131 })
1132 .collect();
1133
1134 let regular_components = contentmeta[2..].to_vec();
1135 let prior_structure = vec![
1136 vec!["pkg1.0"],
1137 vec!["pkg2.0"],
1138 vec!["pkg3.0", "pkg4.0"],
1139 vec!["pkg5.0", "pkg6.0"],
1140 vec![],
1141 ];
1142 let prior_build = create_manifest(prior_structure);
1143
1144 let packing = basic_packing_with_prior_build(
1145 ®ular_components,
1146 NonZeroU32::new(3).unwrap(),
1147 &prior_build,
1148 )?
1149 .expect("prior layout should remain reusable after exclusive bins are removed");
1150 let structure: Vec<Vec<&str>> = packing
1151 .iter()
1152 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
1153 .collect();
1154
1155 assert_eq!(
1156 structure,
1157 vec![vec!["pkg3.0", "pkg4.0"], vec!["pkg5.0", "pkg6.0"], vec![],]
1158 );
1159 Ok(())
1160 }
1161
1162 #[test]
1163 fn test_prior_packing_duplicate_names() -> Result<()> {
1164 for (current_count, prior_structure, expected_structure) in [
1166 (
1167 2,
1168 vec![vec!["pkg1.0"], vec!["pkg2.0"], vec![]],
1169 vec![vec!["pkg1.0", "pkg1.1"], vec!["pkg2.0"], vec![]],
1170 ),
1171 (
1172 2,
1173 vec![vec!["pkg1.0", "pkg1.1"], vec!["pkg2.0"], vec![]],
1174 vec![vec!["pkg1.0", "pkg1.1"], vec!["pkg2.0"], vec![]],
1175 ),
1176 (
1177 1,
1178 vec![vec!["pkg1.0", "pkg1.1"], vec!["pkg2.0"], vec![]],
1179 vec![vec!["pkg1.0"], vec!["pkg2.0"], vec![]],
1180 ),
1181 (
1182 0,
1183 vec![vec!["pkg1.0"], vec!["pkg2.0"], vec!["pkg1.1"], vec![]],
1184 vec![vec!["pkg2.0"], vec![]],
1185 ),
1186 (
1187 2,
1188 vec![vec!["pkg2.0"], vec![]],
1189 vec![vec!["pkg2.0"], vec!["pkg1.0", "pkg1.1"]],
1190 ),
1191 (
1192 3,
1193 vec![vec!["pkg1.0"], vec!["pkg2.0"], vec![]],
1194 vec![vec!["pkg1.0", "pkg1.1", "pkg1.2"], vec!["pkg2.0"], vec![]],
1195 ),
1196 (
1197 2,
1198 vec![vec!["pkg1.0", "pkg2.0"], vec!["pkg1.1"], vec![]],
1199 vec![vec!["pkg2.0"], vec!["pkg1.0", "pkg1.1"]],
1200 ),
1201 (
1202 1,
1203 vec![vec!["pkg1.0"], vec!["pkg2.0"], vec!["pkg1.1"]],
1204 vec![vec!["pkg2.0"], vec!["pkg1.0"]],
1205 ),
1206 ] {
1207 let mut components = Vec::new();
1208 for i in 0..current_count {
1209 components.push(ObjectSourceMetaSized {
1210 meta: ObjectSourceMeta {
1211 identifier: RcStr::from(format!("pkg1.{i}")),
1212 name: RcStr::from("pkg1"),
1213 srcid: RcStr::from("srcpkg1"),
1214 change_time_offset: 0,
1215 change_frequency: 1,
1216 },
1217 size: 100,
1218 });
1219 }
1220 components.push(ObjectSourceMetaSized {
1221 meta: ObjectSourceMeta {
1222 identifier: RcStr::from("pkg2.0"),
1223 name: RcStr::from("pkg2"),
1224 srcid: RcStr::from("srcpkg2"),
1225 change_time_offset: 0,
1226 change_frequency: 1,
1227 },
1228 size: 200,
1229 });
1230 components.sort_by(|a, b| b.size.cmp(&a.size));
1231 let prior = create_manifest(prior_structure);
1232 let bin_size = NonZeroU32::new(4).unwrap();
1233 let reused = basic_packing_with_prior_build(&components, bin_size, &prior)?
1234 .expect("duplicate names should preserve the reusable prior layout");
1235 let structure: Vec<Vec<&str>> = reused
1236 .iter()
1237 .map(|bin| bin.iter().map(|c| c.meta.identifier.as_ref()).collect())
1238 .collect();
1239 assert_eq!(structure, expected_structure);
1240 let packing = basic_packing(&components, bin_size, Some(&prior))?;
1241 let packed: BTreeSet<_> = packing
1242 .iter()
1243 .flatten()
1244 .map(|c| c.meta.identifier.as_ref())
1245 .collect();
1246 let expected: BTreeSet<_> = components
1247 .iter()
1248 .map(|c| c.meta.identifier.as_ref())
1249 .collect();
1250 assert_eq!(packed, expected);
1251 assert_eq!(
1252 packing.iter().map(Vec::len).sum::<usize>(),
1253 components.len()
1254 );
1255 assert_eq!(
1256 packing_size(&packing),
1257 components.iter().map(|c| c.size).sum::<u64>()
1258 );
1259 assert!(packing.len() <= bin_size.get() as usize);
1260 }
1261 Ok(())
1262 }
1263
1264 #[test]
1265 fn test_process_mapping_duplicate_names() -> Result<()> {
1266 let (_, mut meta, _, mut chunking) =
1267 setup_exclusive_test(&[(1, 1, 1000), (2, 1, 1000)], 4, Some(2))?;
1268 meta.sizes[1].meta.name = meta.sizes[0].meta.name.clone();
1271 let prior = create_manifest(vec![vec!["pkg1.0"], vec!["pkg1.1"], vec![]]);
1272 chunking.process_mapping(&meta, &NonZeroU32::new(4), Some(&prior), None)?;
1273 assert!(chunking.remainder.content.is_empty());
1274 assert_eq!(chunking.remainder.size, 0);
1275 let objects: BTreeSet<_> = chunking
1276 .chunks
1277 .iter()
1278 .flat_map(|chunk| chunk.content.keys().map(|s| s.as_ref()))
1279 .collect();
1280 assert_eq!(objects, BTreeSet::from(["checksum_0", "checksum_1"]));
1281 assert_eq!(chunking.chunks.iter().map(|c| c.size).sum::<u64>(), 2000);
1282 Ok(())
1283 }
1284
1285 fn setup_exclusive_test(
1286 component_data: &[(u32, u32, u64)],
1287 max_layers: u32,
1288 num_fake_objects: Option<usize>,
1289 ) -> Result<(
1290 Vec<ObjectSourceMetaSized>,
1291 ObjectMetaSized,
1292 BTreeMap<ContentID, Vec<(Utf8PathBuf, String)>>,
1293 Chunking,
1294 )> {
1295 let contentmeta: Vec<ObjectSourceMetaSized> = component_data
1297 .iter()
1298 .map(|&(id, freq, size)| ObjectSourceMetaSized {
1299 meta: ObjectSourceMeta {
1300 identifier: RcStr::from(format!("pkg{id}.0")),
1301 name: RcStr::from(format!("pkg{id}")),
1302 srcid: RcStr::from(format!("srcpkg{id}")),
1303 change_time_offset: 0,
1304 change_frequency: freq,
1305 },
1306 size,
1307 })
1308 .collect();
1309
1310 let mut object_map = IndexMap::new();
1312
1313 for (i, component) in contentmeta.iter().enumerate() {
1314 let checksum = format!("checksum_{i}");
1315 object_map.insert(checksum, component.meta.identifier.clone());
1316 }
1317
1318 let regular_meta = ObjectMetaSized {
1319 map: object_map,
1320 sizes: contentmeta.clone(),
1321 };
1322
1323 let specific_contentmeta = BTreeMap::new();
1325
1326 let mut chunking = Chunking::default();
1328 chunking.max = max_layers;
1329 chunking.remainder = Chunk::new("remainder");
1330
1331 if let Some(num_objects) = num_fake_objects {
1333 for i in 0..num_objects {
1334 let checksum = format!("checksum_{i}");
1335 chunking.remainder.content.insert(
1336 RcStr::from(checksum),
1337 (
1338 1000,
1339 vec![Utf8PathBuf::from(format!("/path/to/checksum_{i}"))],
1340 ),
1341 );
1342 chunking.remainder.size += 1000;
1343 }
1344 }
1345
1346 Ok((contentmeta, regular_meta, specific_contentmeta, chunking))
1347 }
1348
1349 #[test]
1350 fn test_exclusive_chunks() -> Result<()> {
1351 let component_data = [
1353 (1, 100, 50000),
1354 (2, 200, 40000),
1355 (3, 300, 30000),
1356 (4, 400, 20000),
1357 (5, 500, 10000),
1358 ];
1359
1360 let (contentmeta, regular_meta, mut specific_contentmeta, mut chunking) =
1361 setup_exclusive_test(&component_data, 8, Some(5))?;
1362
1363 specific_contentmeta.insert(
1365 contentmeta[0].meta.identifier.clone(),
1366 vec![(
1367 Utf8PathBuf::from("/path/to/checksum_0"),
1368 "checksum_0".to_string(),
1369 )],
1370 );
1371 specific_contentmeta.insert(
1372 contentmeta[1].meta.identifier.clone(),
1373 vec![(
1374 Utf8PathBuf::from("/path/to/checksum_1"),
1375 "checksum_1".to_string(),
1376 )],
1377 );
1378
1379 chunking.process_mapping(
1380 ®ular_meta,
1381 &Some(NonZeroU32::new(8).unwrap()),
1382 None,
1383 Some(&specific_contentmeta),
1384 )?;
1385
1386 assert!(chunking.chunks.len() >= 2);
1388 assert_eq!(chunking.chunks[0].name, "pkg1.0");
1389 assert_eq!(chunking.chunks[1].name, "pkg2.0");
1390 assert_eq!(chunking.chunks[0].packages, vec!["pkg1.0".to_string()]);
1391 assert_eq!(chunking.chunks[1].packages, vec!["pkg2.0".to_string()]);
1392
1393 Ok(())
1394 }
1395
1396 #[test]
1397 fn test_exclusive_chunks_with_regular_packing() -> Result<()> {
1398 let component_data = [
1400 (1, 100, 50000), (2, 200, 40000), (3, 300, 30000), (4, 400, 20000), (5, 500, 10000), (6, 600, 5000), ];
1407
1408 let (contentmeta, regular_meta, mut specific_contentmeta, mut chunking) =
1409 setup_exclusive_test(&component_data, 8, Some(6))?;
1410
1411 specific_contentmeta.insert(
1413 contentmeta[0].meta.identifier.clone(),
1414 vec![(
1415 Utf8PathBuf::from("/path/to/checksum_0"),
1416 "checksum_0".to_string(),
1417 )],
1418 );
1419 specific_contentmeta.insert(
1420 contentmeta[1].meta.identifier.clone(),
1421 vec![(
1422 Utf8PathBuf::from("/path/to/checksum_1"),
1423 "checksum_1".to_string(),
1424 )],
1425 );
1426
1427 chunking.process_mapping(
1428 ®ular_meta,
1429 &Some(NonZeroU32::new(8).unwrap()),
1430 None,
1431 Some(&specific_contentmeta),
1432 )?;
1433
1434 assert!(chunking.chunks.len() >= 2);
1436 assert_eq!(chunking.chunks[0].name, "pkg1.0");
1437 assert_eq!(chunking.chunks[1].name, "pkg2.0");
1438 assert_eq!(chunking.chunks[0].packages, vec!["pkg1.0".to_string()]);
1439 assert_eq!(chunking.chunks[1].packages, vec!["pkg2.0".to_string()]);
1440
1441 for chunk in &chunking.chunks[2..] {
1443 assert!(!chunk.packages.contains(&"pkg1.0".to_string()));
1444 assert!(!chunk.packages.contains(&"pkg2.0".to_string()));
1445 }
1446
1447 Ok(())
1448 }
1449
1450 #[test]
1451 fn test_exclusive_chunks_isolation() -> Result<()> {
1452 let component_data = [(1, 100, 50000), (2, 200, 40000), (3, 300, 30000)];
1454
1455 let (contentmeta, regular_meta, mut specific_contentmeta, mut chunking) =
1456 setup_exclusive_test(&component_data, 8, Some(3))?;
1457
1458 specific_contentmeta.insert(
1460 contentmeta[0].meta.identifier.clone(),
1461 vec![(
1462 Utf8PathBuf::from("/path/to/checksum_0"),
1463 "checksum_0".to_string(),
1464 )],
1465 );
1466
1467 chunking.process_mapping(
1468 ®ular_meta,
1469 &Some(NonZeroU32::new(8).unwrap()),
1470 None,
1471 Some(&specific_contentmeta),
1472 )?;
1473
1474 assert!(!chunking.chunks.is_empty());
1476 assert_eq!(chunking.chunks[0].name, "pkg1.0");
1477 assert_eq!(chunking.chunks[0].packages, vec!["pkg1.0".to_string()]);
1478
1479 let mut found_pkg2 = false;
1481 let mut found_pkg3 = false;
1482 for chunk in &chunking.chunks[1..] {
1483 if chunk.packages.contains(&"pkg2".to_string()) {
1484 found_pkg2 = true;
1485 assert!(!chunk.packages.contains(&"pkg1.0".to_string()));
1486 }
1487 if chunk.packages.contains(&"pkg3".to_string()) {
1488 found_pkg3 = true;
1489 assert!(!chunk.packages.contains(&"pkg1.0".to_string()));
1490 }
1491 }
1492 assert!(found_pkg2 && found_pkg3);
1493
1494 Ok(())
1495 }
1496
1497 #[test]
1498 fn test_process_mapping_specific_components_contain_correct_objects() -> Result<()> {
1499 let packages = [
1506 (1, 100, 50000), (2, 200, 40000), (3, 300, 30000), (4, 400, 20000), (5, 500, 10000), ];
1512
1513 let (contentmeta, mut system_metadata, mut specific_contentmeta, mut chunking) =
1514 setup_exclusive_test(&packages, 8, None)?;
1515
1516 let mut system_objects_map = IndexMap::new();
1519
1520 let pkg1_objects = ["checksum_1_a", "checksum_1_b", "checksum_1_c"];
1522
1523 let pkg2_objects = ["checksum_2_a", "checksum_2_b"];
1525
1526 let pkg3_objects = ["checksum_3_a", "checksum_3_b"];
1528 for obj in &pkg3_objects {
1529 system_objects_map.insert(obj.to_string(), contentmeta[2].meta.identifier.clone());
1530 }
1531
1532 let pkg4_objects = ["checksum_4_a"];
1534 for obj in &pkg4_objects {
1535 system_objects_map.insert(obj.to_string(), contentmeta[3].meta.identifier.clone());
1536 }
1537
1538 let pkg5_objects = ["checksum_5_a", "checksum_5_b", "checksum_5_c"];
1540 for obj in &pkg5_objects {
1541 system_objects_map.insert(obj.to_string(), contentmeta[4].meta.identifier.clone());
1542 }
1543
1544 system_metadata.map = system_objects_map;
1546
1547 specific_contentmeta.insert(
1549 contentmeta[0].meta.identifier.clone(),
1550 pkg1_objects
1551 .iter()
1552 .map(|obj| {
1553 (
1554 Utf8PathBuf::from(format!("/path/to/{obj}")),
1555 obj.to_string(),
1556 )
1557 })
1558 .collect(),
1559 );
1560 specific_contentmeta.insert(
1561 contentmeta[1].meta.identifier.clone(),
1562 pkg2_objects
1563 .iter()
1564 .map(|obj| {
1565 (
1566 Utf8PathBuf::from(format!("/path/to/{obj}")),
1567 obj.to_string(),
1568 )
1569 })
1570 .collect(),
1571 );
1572
1573 for (checksum, _) in &system_metadata.map {
1577 chunking.remainder.content.insert(
1578 RcStr::from(checksum.as_str()),
1579 (
1580 1000,
1581 vec![Utf8PathBuf::from(format!("/path/to/{checksum}"))],
1582 ),
1583 );
1584 chunking.remainder.size += 1000;
1585 }
1586 for paths in specific_contentmeta.values() {
1587 for (p, checksum) in paths {
1588 chunking
1589 .remainder
1590 .content
1591 .insert(RcStr::from(checksum.as_str()), (1000, vec![p.clone()]));
1592 }
1593 }
1594
1595 chunking.process_mapping(
1599 &system_metadata,
1600 &Some(NonZeroU32::new(8).unwrap()),
1601 None,
1602 Some(&specific_contentmeta),
1603 )?;
1604
1605 assert!(
1607 chunking.chunks.len() >= 2,
1608 "Should have at least 2 chunks for specific components"
1609 );
1610
1611 let specific_component_1_layer = &chunking.chunks[0];
1613 assert_eq!(specific_component_1_layer.name, "pkg1.0");
1614 assert_eq!(specific_component_1_layer.packages, vec!["pkg1.0"]);
1615 assert_eq!(specific_component_1_layer.content.len(), 3);
1616 for obj in &pkg1_objects {
1617 assert!(
1618 specific_component_1_layer.content.contains_key(*obj),
1619 "Specific component 1 layer should contain {obj}"
1620 );
1621 }
1622
1623 let specific_component_2_layer = &chunking.chunks[1];
1625 assert_eq!(specific_component_2_layer.name, "pkg2.0");
1626 assert_eq!(specific_component_2_layer.packages, vec!["pkg2.0"]);
1627 assert_eq!(specific_component_2_layer.content.len(), 2);
1628 for obj in &pkg2_objects {
1629 assert!(
1630 specific_component_2_layer.content.contains_key(*obj),
1631 "Specific component 2 layer should contain {obj}"
1632 );
1633 }
1634
1635 for specific_layer in &chunking.chunks[0..2] {
1637 for obj in pkg3_objects
1638 .iter()
1639 .chain(&pkg4_objects)
1640 .chain(&pkg5_objects)
1641 {
1642 assert!(
1643 !specific_layer.content.contains_key(*obj),
1644 "Specific component layer '{}' should NOT contain regular package object {}",
1645 specific_layer.name,
1646 obj
1647 );
1648 }
1649 }
1650
1651 let regular_package_layers = &chunking.chunks[2..];
1653 for regular_layer in regular_package_layers {
1654 for obj in pkg1_objects.iter().chain(&pkg2_objects) {
1655 assert!(
1656 !regular_layer.content.contains_key(*obj),
1657 "Regular package layer should NOT contain specific component object {obj}"
1658 );
1659 }
1660 }
1661
1662 let mut found_regular_objects = BTreeSet::new();
1664 for regular_layer in regular_package_layers {
1665 for obj in regular_layer.content.keys() {
1666 found_regular_objects.insert(obj.as_ref());
1667 }
1668 }
1669
1670 for obj in pkg3_objects
1671 .iter()
1672 .chain(&pkg4_objects)
1673 .chain(&pkg5_objects)
1674 {
1675 assert!(
1676 found_regular_objects.contains(*obj),
1677 "Regular package object {obj} should be in some regular layer"
1678 );
1679 }
1680
1681 assert_eq!(
1683 chunking.remainder.content.len(),
1684 0,
1685 "All objects should be moved from remainder"
1686 );
1687 assert_eq!(chunking.remainder.size, 0, "Remainder size should be 0");
1688
1689 Ok(())
1690 }
1691}