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 if let Some(last_bin) = curr_build.last_mut() {
653 let added = curr_pkgs_set.difference(&prev_pkgs_set);
654 last_bin.retain(|name| !name.is_empty());
655 last_bin.extend(added.into_iter().cloned());
656 } else {
657 tracing::debug!("No empty last bin for added packages.");
658 return Ok(None);
659 }
660
661 let removed: BTreeSet<&String> = prev_pkgs_set.difference(&curr_pkgs_set).collect();
663 for bin in curr_build.iter_mut() {
664 bin.retain(|pkg| !removed.contains(pkg));
665 }
666
667 let last_idx = curr_build.len().saturating_sub(1);
671 curr_build = curr_build
672 .into_iter()
673 .enumerate()
674 .filter_map(|(idx, bin)| (!bin.is_empty() || idx == last_idx).then_some(bin))
675 .collect();
676
677 if (bin_size.get() as usize) < curr_build.len() {
678 tracing::debug!("bin_size = {bin_size} is too small to be compatible with the prior build");
679 return Ok(None);
680 }
681
682 let mut name_to_component: BTreeMap<String, &ObjectSourceMetaSized> = BTreeMap::new();
684 for component in components.iter() {
685 name_to_component
686 .entry(component.meta.name.to_string())
687 .or_insert(component);
688 }
689 let mut modified_build: Vec<Vec<&ObjectSourceMetaSized>> = Vec::new();
690 for bin in curr_build {
691 let mut mod_bin = Vec::new();
692 for pkg in bin {
693 if pkg.is_empty() {
695 continue;
696 }
697 mod_bin.push(name_to_component[&pkg]);
698 }
699 modified_build.push(mod_bin);
700 }
701
702 let after_processing_pkgs_len: usize = modified_build.iter().map(|b| b.len()).sum();
704 assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len);
705 assert!(modified_build.len() <= bin_size.get() as usize);
706 Ok(Some(modified_build))
707}
708
709fn basic_packing<'a>(
725 components: &'a [ObjectSourceMetaSized],
726 bin_size: NonZeroU32,
727 prior_build_metadata: Option<&oci_spec::image::ImageManifest>,
728) -> Result<Vec<Vec<&'a ObjectSourceMetaSized>>> {
729 const HIGH_SIZE_CUTOFF: f32 = 0.6;
730 let before_processing_pkgs_len = components.len();
731
732 anyhow::ensure!(bin_size.get() >= MIN_CHUNKED_LAYERS);
733
734 if let Some(prior_build) = prior_build_metadata {
736 if let Some(packing) = basic_packing_with_prior_build(components, bin_size, prior_build)? {
737 tracing::debug!("Keeping old package structure");
738 return Ok(packing);
739 } else {
740 tracing::debug!(
741 "Failed to use old package structure; discarding info from prior build."
742 );
743 }
744 }
745
746 tracing::debug!("Creating new packing structure");
747
748 if before_processing_pkgs_len < bin_size.get() as usize {
751 let mut r = components.iter().map(|pkg| vec![pkg]).collect::<Vec<_>>();
752 if before_processing_pkgs_len > 0 {
753 let new_pkgs_bin: Vec<&ObjectSourceMetaSized> = Vec::new();
754 r.push(new_pkgs_bin);
755 }
756 return Ok(r);
757 }
758
759 let mut r = Vec::new();
760 let (components, max_freq_components) = components
762 .iter()
763 .partition::<Vec<_>, _>(|pkg| pkg.meta.change_frequency != u32::MAX);
764 if !components.is_empty() {
765 let limit_ls_bins = 1usize;
768 let limit_new_bins = 1usize;
769 let _limit_new_pkgs = 0usize;
770 let limit_max_frequency_pkgs = max_freq_components.len();
771 let limit_max_frequency_bins = limit_max_frequency_pkgs.min(1);
772 let low_and_other_bin_limit = limit_ls_bins + limit_new_bins + limit_max_frequency_bins;
773 let limit_hs_bins = (HIGH_SIZE_CUTOFF
774 * (bin_size.get() - low_and_other_bin_limit as u32) as f32)
775 .floor() as usize;
776 let limit_ms_bins =
777 (bin_size.get() - (limit_hs_bins + low_and_other_bin_limit) as u32) as usize;
778 let partitions = get_partitions_with_threshold(&components, limit_hs_bins, 2f64)
779 .expect("Partitioning components into sets");
780
781 let low_sized_component_count = partitions
783 .get(LOW_PARTITION)
784 .map(|p| p.len())
785 .unwrap_or_default();
786
787 let pkg_per_bin_ms: usize = (components.len() - limit_hs_bins - low_sized_component_count)
789 .checked_div(limit_ms_bins)
790 .ok_or_else(|| anyhow::anyhow!("number of bins should be >= {}", MIN_CHUNKED_LAYERS))?;
791
792 for (partition, pkgs) in partitions.iter() {
794 if partition == HIGH_PARTITION {
795 for pkg in pkgs {
796 r.push(vec![*pkg]);
797 }
798 } else if partition == LOW_PARTITION {
799 let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new();
800 for pkg in pkgs {
801 bin.push(*pkg);
802 }
803 r.push(bin);
804 } else {
805 let mut bin: Vec<&ObjectSourceMetaSized> = Vec::new();
806 for (i, pkg) in pkgs.iter().enumerate() {
807 if bin.len() < pkg_per_bin_ms {
808 bin.push(*pkg);
809 } else {
810 r.push(bin.clone());
811 bin.clear();
812 bin.push(*pkg);
813 }
814 if i == pkgs.len() - 1 && !bin.is_empty() {
815 r.push(bin.clone());
816 bin.clear();
817 }
818 }
819 }
820 }
821 tracing::debug!("Bins before unoptimized build: {}", r.len());
822
823 while r.len() > (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins) {
833 for i in (limit_ls_bins + limit_hs_bins..r.len() - 1)
834 .step_by(2)
835 .rev()
836 {
837 if r.len() <= (bin_size.get() as usize - limit_new_bins - limit_max_frequency_bins)
838 {
839 break;
840 }
841 let prev = &r[i - 1];
842 let curr = &r[i];
843 let mut merge: Vec<&ObjectSourceMetaSized> = Vec::new();
844 merge.extend(prev.iter());
845 merge.extend(curr.iter());
846 r.remove(i);
847 r.remove(i - 1);
848 r.insert(i, merge);
849 }
850 }
851 tracing::debug!("Bins after optimization: {}", r.len());
852 }
853
854 if !max_freq_components.is_empty() {
855 r.push(max_freq_components);
856 }
857
858 r.push(Vec::new());
860 let after_processing_pkgs_len = r.iter().map(|b| b.len()).sum::<usize>();
861 assert_eq!(after_processing_pkgs_len, before_processing_pkgs_len);
862 assert!(r.len() <= bin_size.get() as usize);
863 Ok(r)
864}
865
866#[cfg(test)]
867mod test {
868 use super::*;
869
870 use oci_spec::image as oci_image;
871 use std::str::FromStr;
872
873 const FCOS_CONTENTMETA: &[u8] = include_bytes!("fixtures/fedora-coreos-contentmeta.json.gz");
874 const SHA256_EXAMPLE: &str =
875 "sha256:0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff";
876
877 #[test]
878 fn test_packing_basics() -> Result<()> {
879 for v in [4, 7].map(|v| NonZeroU32::new(v).unwrap()) {
881 assert_eq!(basic_packing(&[], v, None).unwrap().len(), 0);
882 }
883 Ok(())
884 }
885
886 #[test]
887 fn test_packing_fcos() -> Result<()> {
888 let contentmeta: Vec<ObjectSourceMetaSized> =
889 serde_json::from_reader(flate2::read::GzDecoder::new(FCOS_CONTENTMETA))?;
890 let total_size = contentmeta.iter().map(|v| v.size).sum::<u64>();
891
892 let packing =
893 basic_packing(&contentmeta, NonZeroU32::new(MAX_CHUNKS).unwrap(), None).unwrap();
894 assert!(!contentmeta.is_empty());
895 assert_eq!(packing.len() as u32, MAX_CHUNKS);
897 let packed_total_size = packing_size(&packing);
899 assert_eq!(total_size, packed_total_size);
900 Ok(())
901 }
902
903 #[test]
904 fn test_packing_one_layer() -> Result<()> {
905 let contentmeta: Vec<ObjectSourceMetaSized> =
906 serde_json::from_reader(flate2::read::GzDecoder::new(FCOS_CONTENTMETA))?;
907 let r = basic_packing(&contentmeta, NonZeroU32::new(1).unwrap(), None);
908 assert!(r.is_err());
909 Ok(())
910 }
911
912 fn create_manifest(prev_expected_structure: Vec<Vec<&str>>) -> oci_spec::image::ImageManifest {
913 use std::collections::HashMap;
914
915 let mut p = prev_expected_structure
916 .iter()
917 .map(|b| {
918 b.iter()
919 .map(|p| p.split('.').collect::<Vec<&str>>()[0].to_string())
920 .collect()
921 })
922 .collect();
923 let mut metadata_with_ostree_commit = vec![vec![String::from("ostree_commit")]];
924 metadata_with_ostree_commit.append(&mut p);
925
926 let config = oci_spec::image::DescriptorBuilder::default()
927 .media_type(oci_spec::image::MediaType::ImageConfig)
928 .size(7023_u64)
929 .digest(oci_image::Digest::from_str(SHA256_EXAMPLE).unwrap())
930 .build()
931 .expect("build config descriptor");
932
933 let layers: Vec<oci_spec::image::Descriptor> = metadata_with_ostree_commit
934 .iter()
935 .map(|l| {
936 let mut buf = [0; 8];
937 let sep = COMPONENT_SEPARATOR.encode_utf8(&mut buf);
938 oci_spec::image::DescriptorBuilder::default()
939 .media_type(oci_spec::image::MediaType::ImageLayerGzip)
940 .size(100_u64)
941 .digest(oci_image::Digest::from_str(SHA256_EXAMPLE).unwrap())
942 .annotations(HashMap::from([(
943 CONTENT_ANNOTATION.to_string(),
944 l.join(sep),
945 )]))
946 .build()
947 .expect("build layer")
948 })
949 .collect();
950
951 oci_spec::image::ImageManifestBuilder::default()
952 .schema_version(oci_spec::image::SCHEMA_VERSION)
953 .config(config)
954 .layers(layers)
955 .build()
956 .expect("build image manifest")
957 }
958
959 #[test]
960 fn test_advanced_packing() -> Result<()> {
961 let contentmeta_v0: Vec<ObjectSourceMetaSized> = vec![
963 vec![1, u32::MAX, 100000],
964 vec![2, u32::MAX, 99999],
965 vec![3, 30, 99998],
966 vec![4, 100, 99997],
967 vec![10, 51, 1000],
968 vec![8, 50, 500],
969 vec![9, 1, 200],
970 vec![11, 100000, 199],
971 vec![6, 30, 2],
972 vec![7, 30, 1],
973 ]
974 .iter()
975 .map(|data| ObjectSourceMetaSized {
976 meta: ObjectSourceMeta {
977 identifier: RcStr::from(format!("pkg{}.0", data[0])),
978 name: RcStr::from(format!("pkg{}", data[0])),
979 srcid: RcStr::from(format!("srcpkg{}", data[0])),
980 change_time_offset: 0,
981 change_frequency: data[1],
982 },
983 size: data[2] as u64,
984 })
985 .collect();
986
987 let packing = basic_packing(
988 &contentmeta_v0.as_slice(),
989 NonZeroU32::new(6).unwrap(),
990 None,
991 )
992 .unwrap();
993 let structure: Vec<Vec<&str>> = packing
994 .iter()
995 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
996 .collect();
997 let v0_expected_structure = vec![
998 vec!["pkg3.0"],
999 vec!["pkg4.0"],
1000 vec!["pkg6.0", "pkg7.0", "pkg11.0"],
1001 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
1002 vec!["pkg1.0", "pkg2.0"],
1003 vec![],
1004 ];
1005 assert_eq!(structure, v0_expected_structure);
1006
1007 let mut contentmeta_v1: Vec<ObjectSourceMetaSized> = contentmeta_v0;
1010 contentmeta_v1[0].meta.identifier = RcStr::from("pkg1.1");
1012 contentmeta_v1.remove(contentmeta_v1.len() - 1);
1014 contentmeta_v1.push(ObjectSourceMetaSized {
1016 meta: ObjectSourceMeta {
1017 identifier: RcStr::from("pkg5.0"),
1018 name: RcStr::from("pkg5"),
1019 srcid: RcStr::from("srcpkg5"),
1020 change_time_offset: 0,
1021 change_frequency: 42,
1022 },
1023 size: 100000,
1024 });
1025
1026 let image_manifest_v0 = create_manifest(v0_expected_structure);
1027 let packing_derived = basic_packing(
1028 &contentmeta_v1.as_slice(),
1029 NonZeroU32::new(6).unwrap(),
1030 Some(&image_manifest_v0),
1031 )
1032 .unwrap();
1033 let structure_derived: Vec<Vec<&str>> = packing_derived
1034 .iter()
1035 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
1036 .collect();
1037 let v1_expected_structure = vec![
1038 vec!["pkg3.0"],
1039 vec!["pkg4.0"],
1040 vec!["pkg6.0", "pkg11.0"],
1041 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
1042 vec!["pkg1.1", "pkg2.0"],
1043 vec!["pkg5.0"],
1044 ];
1045
1046 assert_eq!(structure_derived, v1_expected_structure);
1047
1048 let mut contentmeta_v2: Vec<ObjectSourceMetaSized> = contentmeta_v1;
1051 contentmeta_v2[9].meta.identifier = RcStr::from("pkg5.1");
1053 contentmeta_v2.push(ObjectSourceMetaSized {
1055 meta: ObjectSourceMeta {
1056 identifier: RcStr::from("pkg12.0"),
1057 name: RcStr::from("pkg12"),
1058 srcid: RcStr::from("srcpkg12"),
1059 change_time_offset: 0,
1060 change_frequency: 42,
1061 },
1062 size: 100000,
1063 });
1064
1065 let image_manifest_v1 = create_manifest(v1_expected_structure);
1066 let packing_derived = basic_packing(
1067 &contentmeta_v2.as_slice(),
1068 NonZeroU32::new(6).unwrap(),
1069 Some(&image_manifest_v1),
1070 )
1071 .unwrap();
1072 let structure_derived: Vec<Vec<&str>> = packing_derived
1073 .iter()
1074 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
1075 .collect();
1076 let v2_expected_structure = vec![
1077 vec!["pkg3.0"],
1078 vec!["pkg4.0"],
1079 vec!["pkg6.0", "pkg11.0"],
1080 vec!["pkg9.0", "pkg8.0", "pkg10.0"],
1081 vec!["pkg1.1", "pkg2.0"],
1082 vec!["pkg5.1", "pkg12.0"],
1083 ];
1084
1085 assert_eq!(structure_derived, v2_expected_structure);
1086 Ok(())
1087 }
1088
1089 #[test]
1090 fn test_advanced_packing_with_prior_exclusive_components() -> Result<()> {
1091 let contentmeta: Vec<ObjectSourceMetaSized> = [
1092 (1, 100, 50000),
1093 (2, 200, 40000),
1094 (3, 300, 30000),
1095 (4, 400, 20000),
1096 (5, 500, 10000),
1097 (6, 600, 5000),
1098 ]
1099 .iter()
1100 .map(|&(id, freq, size)| ObjectSourceMetaSized {
1101 meta: ObjectSourceMeta {
1102 identifier: RcStr::from(format!("pkg{id}.0")),
1103 name: RcStr::from(format!("pkg{id}")),
1104 srcid: RcStr::from(format!("srcpkg{id}")),
1105 change_time_offset: 0,
1106 change_frequency: freq,
1107 },
1108 size,
1109 })
1110 .collect();
1111
1112 let regular_components = contentmeta[2..].to_vec();
1113 let prior_structure = vec![
1114 vec!["pkg1.0"],
1115 vec!["pkg2.0"],
1116 vec!["pkg3.0", "pkg4.0"],
1117 vec!["pkg5.0", "pkg6.0"],
1118 vec![],
1119 ];
1120 let prior_build = create_manifest(prior_structure);
1121
1122 let packing = basic_packing_with_prior_build(
1123 ®ular_components,
1124 NonZeroU32::new(3).unwrap(),
1125 &prior_build,
1126 )?
1127 .expect("prior layout should remain reusable after exclusive bins are removed");
1128 let structure: Vec<Vec<&str>> = packing
1129 .iter()
1130 .map(|bin| bin.iter().map(|pkg| &*pkg.meta.identifier).collect())
1131 .collect();
1132
1133 assert_eq!(
1134 structure,
1135 vec![vec!["pkg3.0", "pkg4.0"], vec!["pkg5.0", "pkg6.0"], vec![],]
1136 );
1137 Ok(())
1138 }
1139
1140 fn setup_exclusive_test(
1141 component_data: &[(u32, u32, u64)],
1142 max_layers: u32,
1143 num_fake_objects: Option<usize>,
1144 ) -> Result<(
1145 Vec<ObjectSourceMetaSized>,
1146 ObjectMetaSized,
1147 BTreeMap<ContentID, Vec<(Utf8PathBuf, String)>>,
1148 Chunking,
1149 )> {
1150 let contentmeta: Vec<ObjectSourceMetaSized> = component_data
1152 .iter()
1153 .map(|&(id, freq, size)| ObjectSourceMetaSized {
1154 meta: ObjectSourceMeta {
1155 identifier: RcStr::from(format!("pkg{id}.0")),
1156 name: RcStr::from(format!("pkg{id}")),
1157 srcid: RcStr::from(format!("srcpkg{id}")),
1158 change_time_offset: 0,
1159 change_frequency: freq,
1160 },
1161 size,
1162 })
1163 .collect();
1164
1165 let mut object_map = IndexMap::new();
1167
1168 for (i, component) in contentmeta.iter().enumerate() {
1169 let checksum = format!("checksum_{i}");
1170 object_map.insert(checksum, component.meta.identifier.clone());
1171 }
1172
1173 let regular_meta = ObjectMetaSized {
1174 map: object_map,
1175 sizes: contentmeta.clone(),
1176 };
1177
1178 let specific_contentmeta = BTreeMap::new();
1180
1181 let mut chunking = Chunking::default();
1183 chunking.max = max_layers;
1184 chunking.remainder = Chunk::new("remainder");
1185
1186 if let Some(num_objects) = num_fake_objects {
1188 for i in 0..num_objects {
1189 let checksum = format!("checksum_{i}");
1190 chunking.remainder.content.insert(
1191 RcStr::from(checksum),
1192 (
1193 1000,
1194 vec![Utf8PathBuf::from(format!("/path/to/checksum_{i}"))],
1195 ),
1196 );
1197 chunking.remainder.size += 1000;
1198 }
1199 }
1200
1201 Ok((contentmeta, regular_meta, specific_contentmeta, chunking))
1202 }
1203
1204 #[test]
1205 fn test_exclusive_chunks() -> Result<()> {
1206 let component_data = [
1208 (1, 100, 50000),
1209 (2, 200, 40000),
1210 (3, 300, 30000),
1211 (4, 400, 20000),
1212 (5, 500, 10000),
1213 ];
1214
1215 let (contentmeta, regular_meta, mut specific_contentmeta, mut chunking) =
1216 setup_exclusive_test(&component_data, 8, Some(5))?;
1217
1218 specific_contentmeta.insert(
1220 contentmeta[0].meta.identifier.clone(),
1221 vec![(
1222 Utf8PathBuf::from("/path/to/checksum_0"),
1223 "checksum_0".to_string(),
1224 )],
1225 );
1226 specific_contentmeta.insert(
1227 contentmeta[1].meta.identifier.clone(),
1228 vec![(
1229 Utf8PathBuf::from("/path/to/checksum_1"),
1230 "checksum_1".to_string(),
1231 )],
1232 );
1233
1234 chunking.process_mapping(
1235 ®ular_meta,
1236 &Some(NonZeroU32::new(8).unwrap()),
1237 None,
1238 Some(&specific_contentmeta),
1239 )?;
1240
1241 assert!(chunking.chunks.len() >= 2);
1243 assert_eq!(chunking.chunks[0].name, "pkg1.0");
1244 assert_eq!(chunking.chunks[1].name, "pkg2.0");
1245 assert_eq!(chunking.chunks[0].packages, vec!["pkg1.0".to_string()]);
1246 assert_eq!(chunking.chunks[1].packages, vec!["pkg2.0".to_string()]);
1247
1248 Ok(())
1249 }
1250
1251 #[test]
1252 fn test_exclusive_chunks_with_regular_packing() -> Result<()> {
1253 let component_data = [
1255 (1, 100, 50000), (2, 200, 40000), (3, 300, 30000), (4, 400, 20000), (5, 500, 10000), (6, 600, 5000), ];
1262
1263 let (contentmeta, regular_meta, mut specific_contentmeta, mut chunking) =
1264 setup_exclusive_test(&component_data, 8, Some(6))?;
1265
1266 specific_contentmeta.insert(
1268 contentmeta[0].meta.identifier.clone(),
1269 vec![(
1270 Utf8PathBuf::from("/path/to/checksum_0"),
1271 "checksum_0".to_string(),
1272 )],
1273 );
1274 specific_contentmeta.insert(
1275 contentmeta[1].meta.identifier.clone(),
1276 vec![(
1277 Utf8PathBuf::from("/path/to/checksum_1"),
1278 "checksum_1".to_string(),
1279 )],
1280 );
1281
1282 chunking.process_mapping(
1283 ®ular_meta,
1284 &Some(NonZeroU32::new(8).unwrap()),
1285 None,
1286 Some(&specific_contentmeta),
1287 )?;
1288
1289 assert!(chunking.chunks.len() >= 2);
1291 assert_eq!(chunking.chunks[0].name, "pkg1.0");
1292 assert_eq!(chunking.chunks[1].name, "pkg2.0");
1293 assert_eq!(chunking.chunks[0].packages, vec!["pkg1.0".to_string()]);
1294 assert_eq!(chunking.chunks[1].packages, vec!["pkg2.0".to_string()]);
1295
1296 for chunk in &chunking.chunks[2..] {
1298 assert!(!chunk.packages.contains(&"pkg1.0".to_string()));
1299 assert!(!chunk.packages.contains(&"pkg2.0".to_string()));
1300 }
1301
1302 Ok(())
1303 }
1304
1305 #[test]
1306 fn test_exclusive_chunks_isolation() -> Result<()> {
1307 let component_data = [(1, 100, 50000), (2, 200, 40000), (3, 300, 30000)];
1309
1310 let (contentmeta, regular_meta, mut specific_contentmeta, mut chunking) =
1311 setup_exclusive_test(&component_data, 8, Some(3))?;
1312
1313 specific_contentmeta.insert(
1315 contentmeta[0].meta.identifier.clone(),
1316 vec![(
1317 Utf8PathBuf::from("/path/to/checksum_0"),
1318 "checksum_0".to_string(),
1319 )],
1320 );
1321
1322 chunking.process_mapping(
1323 ®ular_meta,
1324 &Some(NonZeroU32::new(8).unwrap()),
1325 None,
1326 Some(&specific_contentmeta),
1327 )?;
1328
1329 assert!(!chunking.chunks.is_empty());
1331 assert_eq!(chunking.chunks[0].name, "pkg1.0");
1332 assert_eq!(chunking.chunks[0].packages, vec!["pkg1.0".to_string()]);
1333
1334 let mut found_pkg2 = false;
1336 let mut found_pkg3 = false;
1337 for chunk in &chunking.chunks[1..] {
1338 if chunk.packages.contains(&"pkg2".to_string()) {
1339 found_pkg2 = true;
1340 assert!(!chunk.packages.contains(&"pkg1.0".to_string()));
1341 }
1342 if chunk.packages.contains(&"pkg3".to_string()) {
1343 found_pkg3 = true;
1344 assert!(!chunk.packages.contains(&"pkg1.0".to_string()));
1345 }
1346 }
1347 assert!(found_pkg2 && found_pkg3);
1348
1349 Ok(())
1350 }
1351
1352 #[test]
1353 fn test_process_mapping_specific_components_contain_correct_objects() -> Result<()> {
1354 let packages = [
1361 (1, 100, 50000), (2, 200, 40000), (3, 300, 30000), (4, 400, 20000), (5, 500, 10000), ];
1367
1368 let (contentmeta, mut system_metadata, mut specific_contentmeta, mut chunking) =
1369 setup_exclusive_test(&packages, 8, None)?;
1370
1371 let mut system_objects_map = IndexMap::new();
1374
1375 let pkg1_objects = ["checksum_1_a", "checksum_1_b", "checksum_1_c"];
1377
1378 let pkg2_objects = ["checksum_2_a", "checksum_2_b"];
1380
1381 let pkg3_objects = ["checksum_3_a", "checksum_3_b"];
1383 for obj in &pkg3_objects {
1384 system_objects_map.insert(obj.to_string(), contentmeta[2].meta.identifier.clone());
1385 }
1386
1387 let pkg4_objects = ["checksum_4_a"];
1389 for obj in &pkg4_objects {
1390 system_objects_map.insert(obj.to_string(), contentmeta[3].meta.identifier.clone());
1391 }
1392
1393 let pkg5_objects = ["checksum_5_a", "checksum_5_b", "checksum_5_c"];
1395 for obj in &pkg5_objects {
1396 system_objects_map.insert(obj.to_string(), contentmeta[4].meta.identifier.clone());
1397 }
1398
1399 system_metadata.map = system_objects_map;
1401
1402 specific_contentmeta.insert(
1404 contentmeta[0].meta.identifier.clone(),
1405 pkg1_objects
1406 .iter()
1407 .map(|obj| {
1408 (
1409 Utf8PathBuf::from(format!("/path/to/{obj}")),
1410 obj.to_string(),
1411 )
1412 })
1413 .collect(),
1414 );
1415 specific_contentmeta.insert(
1416 contentmeta[1].meta.identifier.clone(),
1417 pkg2_objects
1418 .iter()
1419 .map(|obj| {
1420 (
1421 Utf8PathBuf::from(format!("/path/to/{obj}")),
1422 obj.to_string(),
1423 )
1424 })
1425 .collect(),
1426 );
1427
1428 for (checksum, _) in &system_metadata.map {
1432 chunking.remainder.content.insert(
1433 RcStr::from(checksum.as_str()),
1434 (
1435 1000,
1436 vec![Utf8PathBuf::from(format!("/path/to/{checksum}"))],
1437 ),
1438 );
1439 chunking.remainder.size += 1000;
1440 }
1441 for paths in specific_contentmeta.values() {
1442 for (p, checksum) in paths {
1443 chunking
1444 .remainder
1445 .content
1446 .insert(RcStr::from(checksum.as_str()), (1000, vec![p.clone()]));
1447 }
1448 }
1449
1450 chunking.process_mapping(
1454 &system_metadata,
1455 &Some(NonZeroU32::new(8).unwrap()),
1456 None,
1457 Some(&specific_contentmeta),
1458 )?;
1459
1460 assert!(
1462 chunking.chunks.len() >= 2,
1463 "Should have at least 2 chunks for specific components"
1464 );
1465
1466 let specific_component_1_layer = &chunking.chunks[0];
1468 assert_eq!(specific_component_1_layer.name, "pkg1.0");
1469 assert_eq!(specific_component_1_layer.packages, vec!["pkg1.0"]);
1470 assert_eq!(specific_component_1_layer.content.len(), 3);
1471 for obj in &pkg1_objects {
1472 assert!(
1473 specific_component_1_layer.content.contains_key(*obj),
1474 "Specific component 1 layer should contain {obj}"
1475 );
1476 }
1477
1478 let specific_component_2_layer = &chunking.chunks[1];
1480 assert_eq!(specific_component_2_layer.name, "pkg2.0");
1481 assert_eq!(specific_component_2_layer.packages, vec!["pkg2.0"]);
1482 assert_eq!(specific_component_2_layer.content.len(), 2);
1483 for obj in &pkg2_objects {
1484 assert!(
1485 specific_component_2_layer.content.contains_key(*obj),
1486 "Specific component 2 layer should contain {obj}"
1487 );
1488 }
1489
1490 for specific_layer in &chunking.chunks[0..2] {
1492 for obj in pkg3_objects
1493 .iter()
1494 .chain(&pkg4_objects)
1495 .chain(&pkg5_objects)
1496 {
1497 assert!(
1498 !specific_layer.content.contains_key(*obj),
1499 "Specific component layer '{}' should NOT contain regular package object {}",
1500 specific_layer.name,
1501 obj
1502 );
1503 }
1504 }
1505
1506 let regular_package_layers = &chunking.chunks[2..];
1508 for regular_layer in regular_package_layers {
1509 for obj in pkg1_objects.iter().chain(&pkg2_objects) {
1510 assert!(
1511 !regular_layer.content.contains_key(*obj),
1512 "Regular package layer should NOT contain specific component object {obj}"
1513 );
1514 }
1515 }
1516
1517 let mut found_regular_objects = BTreeSet::new();
1519 for regular_layer in regular_package_layers {
1520 for obj in regular_layer.content.keys() {
1521 found_regular_objects.insert(obj.as_ref());
1522 }
1523 }
1524
1525 for obj in pkg3_objects
1526 .iter()
1527 .chain(&pkg4_objects)
1528 .chain(&pkg5_objects)
1529 {
1530 assert!(
1531 found_regular_objects.contains(*obj),
1532 "Regular package object {obj} should be in some regular layer"
1533 );
1534 }
1535
1536 assert_eq!(
1538 chunking.remainder.content.len(),
1539 0,
1540 "All objects should be moved from remainder"
1541 );
1542 assert_eq!(chunking.remainder.size, 0, "Remainder size should be 0");
1543
1544 Ok(())
1545 }
1546}