1use fn_error_context::context;
39use std::sync::Arc;
40
41use anyhow::{Context, Result};
42
43use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
44use composefs::repository::{EnsureStatus, RepositoryConfig};
45use composefs::tree::FileSystem;
46use composefs_boot::bootloader::{BootEntry as ComposefsBootEntry, get_boot_resources};
47use composefs_ctl::composefs;
48use composefs_ctl::composefs_boot;
49use composefs_ctl::composefs_oci;
50use composefs_oci::{
51 LocalFetchOpt, PullOptions, PullResult,
52 image::create_filesystem as create_composefs_filesystem, tag_image,
53};
54
55use ostree_ext::containers_image_proxy;
56
57use cap_std_ext::{
58 cap_std::{ambient_authority, fs::Dir},
59 cap_tempfile,
60 dirext::CapStdExtDirExt,
61};
62use rustix::fs::{FlockOperation, flock};
63
64use crate::bootc_composefs::boot::{
65 ExpectedBootImageIds, ensure_correct_composefs_digest, expected_boot_image_ids,
66 print_uki_dumpfile_diff_on_mismatch,
67};
68use crate::bootc_composefs::progress;
69use crate::composefs_consts::BOOTC_TAG_PREFIX;
70use crate::install::{RootSetup, State};
71use crate::lsm;
72use crate::podstorage::CStorage;
73use crate::progress_jsonl::ProgressWriter;
74
75pub(crate) fn bootc_tag_for_manifest(manifest_digest: &str) -> String {
81 format!("{BOOTC_TAG_PREFIX}{manifest_digest}")
82}
83
84pub(crate) fn open_composefs_repo(rootfs_dir: &Dir) -> Result<crate::store::ComposefsRepository> {
85 crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs")
86 .context("Failed to open composefs repository")
87}
88
89pub(crate) fn composefs_repository_config(allow_missing_fsverity: bool) -> RepositoryConfig {
94 let mut config = RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512);
95 if allow_missing_fsverity {
96 config = config.set_insecure();
97 }
98 crate::store::set_dual_erofs_formats(&mut config);
99 config
100}
101
102fn ensure_composefs_repository(
105 rootfs_dir: &Dir,
106 config: RepositoryConfig,
107) -> Result<(crate::store::ComposefsRepository, bool)> {
108 let (repo, status) =
109 crate::store::ComposefsRepository::ensure_path(rootfs_dir, "composefs", config)
110 .context("Failed to initialize composefs repository")?;
111 Ok((repo, status == EnsureStatus::Created))
112}
113
114pub(crate) struct InitializedComposefs {
115 pub(crate) repo: Arc<crate::store::ComposefsRepository>,
116 pub(crate) created: bool,
117 pub(crate) repository_insecure: bool,
118}
119
120pub(crate) fn final_repository_policy(
121 image_fsverity_policy: Option<bool>,
122 requested_relaxed: bool,
123) -> bool {
124 image_fsverity_policy == Some(true) || (image_fsverity_policy.is_none() && requested_relaxed)
125}
126
127pub(crate) fn validate_repository_policy(
131 repository_insecure: bool,
132 requested_relaxed: bool,
133 allow_missing_verity_explicit: bool,
134) -> Result<()> {
135 if repository_insecure && !requested_relaxed {
136 anyhow::bail!(
137 "Existing composefs repository is insecure, but this install requires fs-verity; refusing to continue"
138 );
139 }
140 if !repository_insecure && requested_relaxed && !allow_missing_verity_explicit {
141 anyhow::bail!(
142 "Initial insecure boot artifact conflicts with the existing strict composefs repository; explicitly pass --allow-missing-verity to permit this session"
143 );
144 }
145 Ok(())
146}
147
148pub(crate) fn finalize_fresh_repository_policy(
152 rootfs: &Dir,
153 initial_config: RepositoryConfig,
154) -> Result<()> {
155 let repo_dir = rootfs.open_dir(crate::store::COMPOSEFS)?;
156 let temp_root =
157 cap_tempfile::TempDir::new_in(rootfs).context("Creating temporary metadata directory")?;
158 let relaxed = initial_config.set_insecure();
159 let (temporary_repo, _) =
160 crate::store::ComposefsRepository::init_path(&*temp_root, ".", relaxed)?;
161 let replacement_metadata = temporary_repo.metadata().clone();
162 drop(temporary_repo);
163 let replacement_bytes = serde_json::to_vec(&replacement_metadata)?;
164
165 let lock = repo_dir
168 .reopen_as_ownedfd()
169 .context("Opening composefs repository lock")?;
170 flock(&lock, FlockOperation::LockExclusive).context("Taking composefs repository lock")?;
171 let target_metadata: composefs::repository::RepoMetadata =
172 serde_json::from_reader(repo_dir.open("meta.json")?)?;
173 anyhow::ensure!(
174 target_metadata == replacement_metadata,
175 "composefs metadata generated for policy replacement does not match the repository"
176 );
177
178 repo_dir
179 .atomic_replace_with("meta.json", |writer| -> Result<()> {
180 use std::io::Write;
181
182 writer.write_all(&replacement_bytes)?;
183 writer.flush()?;
184 writer.get_ref().as_file().sync_all()?;
185 Ok(())
186 })
187 .context("Atomically replacing composefs metadata")?;
188 Ok(())
189}
190
191pub(crate) fn tag_pulled_image(
192 rootfs_dir: &Dir,
193 pull_result: &PullResult<Sha512HashValue>,
194) -> Result<()> {
195 let repo = crate::store::ComposefsRepository::open_path(rootfs_dir, "composefs")?;
196 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
197 tag_image(&repo, &pull_result.manifest_digest, &tag)
198 .context("Tagging pulled image as bootc GC root")?;
199 Ok(())
200}
201
202pub(crate) fn initialize_composefs_repository(
203 state: &State,
204 root_setup: &RootSetup,
205 allow_missing_fsverity: bool,
206) -> Result<InitializedComposefs> {
207 const COMPOSEFS_REPO_INIT_JOURNAL_ID: &str = "5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9";
208
209 let rootfs_dir = &root_setup.physical_root;
210 let image_name = &state.source.imageref.name;
211 let transport = &state.source.imageref.transport;
212
213 tracing::info!(
214 message_id = COMPOSEFS_REPO_INIT_JOURNAL_ID,
215 bootc.operation = "repository_init",
216 bootc.source_image = %image_name,
217 bootc.transport = %transport,
218 bootc.allow_missing_fsverity = allow_missing_fsverity,
219 "Initializing composefs repository for image {}:{}",
220 transport,
221 image_name
222 );
223
224 crate::store::ensure_composefs_dir(rootfs_dir)?;
225
226 let config = composefs_repository_config(allow_missing_fsverity);
227 let (mut repo, created) = ensure_composefs_repository(rootfs_dir, config)?;
228 let repository_insecure = repo.is_insecure();
229 if allow_missing_fsverity && (created || repository_insecure) {
234 repo.set_insecure()?;
235 }
236
237 let repo = Arc::new(repo);
238
239 Ok(InitializedComposefs {
240 repo,
241 created,
242 repository_insecure,
243 })
244}
245
246pub(crate) async fn pull_install_composefs_repository(
248 state: &State,
249 root_setup: &RootSetup,
250 repo: &Arc<crate::store::ComposefsRepository>,
251 use_unified: bool,
252 fetch_imgref: Option<&containers_image_proxy::ImageReference>,
253) -> Result<PullResult<Sha512HashValue>> {
254 let rootfs_dir = &root_setup.physical_root;
255 let original_imgref: containers_image_proxy::ImageReference = state
256 .source
257 .imageref
258 .to_string()
259 .as_str()
260 .try_into()
261 .context("Parsing source image reference")?;
262 let imgref = fetch_imgref.unwrap_or(&original_imgref);
263
264 crate::store::ensure_composefs_bootc_link(rootfs_dir)?;
270
271 let pull_result = if use_unified {
272 let sepolicy = state.load_policy()?;
276 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
277 let imgstore = CStorage::create(rootfs_dir, &run, sepolicy.as_ref())?;
278 let storage_path = root_setup.physical_root_path.join(CStorage::subpath());
279
280 let r = pull_composefs_unified(
283 &imgstore,
284 storage_path.as_str(),
285 &repo,
286 &imgref,
287 false,
288 ProgressWriter::default(),
289 )
290 .await?;
291
292 imgstore
294 .ensure_labeled()
295 .context("SELinux labeling of containers-storage")?;
296 r
297 } else {
298 pull_composefs_direct(&repo, &imgref, false, ProgressWriter::default()).await?
301 };
302
303 Ok(pull_result)
304}
305
306pub(crate) struct PullRepoResult {
309 pub(crate) repo: crate::store::ComposefsRepository,
310 pub(crate) entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
311 pub(crate) id: Sha512HashValue,
312 pub(crate) boot_ids: ExpectedBootImageIds,
313 pub(crate) manifest_digest: String,
315 pub(crate) fs: FileSystem<Sha512HashValue>,
317}
318
319pub(crate) struct BootImage {
321 pub(crate) id: Sha512HashValue,
322 pub(crate) boot_ids: ExpectedBootImageIds,
323 pub(crate) fs: FileSystem<Sha512HashValue>,
324 pub(crate) entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
325}
326
327pub(crate) fn prepare_boot_image(
333 repo: &Arc<crate::store::ComposefsRepository>,
334 pull_result: &PullResult<Sha512HashValue>,
335) -> Result<BootImage> {
336 let generated_id = composefs_oci::generate_boot_image(
337 repo,
338 &pull_result.manifest_digest,
339 &composefs_oci::OciTransformOptions::default(),
340 )
341 .context("Generating bootable EROFS image")?;
342
343 let fs = create_composefs_filesystem(
344 &**repo,
345 &pull_result.config_digest,
346 Some(&pull_result.config_verity),
347 &composefs_oci::OciTransformOptions::default(),
348 )
349 .context("Creating composefs filesystem for boot entry discovery")?;
350 let entries =
351 get_boot_resources(&fs, &**repo).context("Extracting boot entries from OCI image")?;
352 let oci_img =
353 composefs_oci::oci_image::OciImage::open(repo, &pull_result.manifest_digest, None)
354 .context("Opening OCI image to read boot image refs")?;
355 let expected_ids = expected_boot_image_ids(
356 oci_img.boot_image_ref_v1().cloned(),
357 oci_img.boot_image_ref_v2().cloned(),
358 &generated_id,
359 repo.erofs_version(),
360 );
361 let recovered = print_uki_dumpfile_diff_on_mismatch(
362 ensure_correct_composefs_digest(
363 repo,
364 &pull_result.manifest_digest,
365 generated_id,
366 expected_ids,
367 &entries,
368 ),
369 repo,
370 &fs,
371 )?;
372
373 Ok(BootImage {
374 id: recovered.id,
375 boot_ids: recovered.expected_ids,
376 fs,
377 entries,
378 })
379}
380
381pub(crate) fn inspect_image_fsverity_policy(
385 repo: &Arc<crate::store::ComposefsRepository>,
386 pull_result: &PullResult<Sha512HashValue>,
387) -> Result<Option<bool>> {
388 let fs = create_composefs_filesystem(
389 &**repo,
390 &pull_result.config_digest,
391 Some(&pull_result.config_verity),
392 &composefs_oci::OciTransformOptions::default(),
393 )
394 .context("Creating composefs filesystem for image fs-verity policy inspection")?;
395 let entries = get_boot_resources(&fs, &**repo).context("Extracting boot entries")?;
396 crate::bootc_composefs::boot::image_fsverity_policy(repo, &entries)
397}
398
399async fn pull_composefs_direct(
405 repo: &Arc<crate::store::ComposefsRepository>,
406 imgref: &containers_image_proxy::ImageReference,
407 quiet: bool,
408 prog: ProgressWriter,
409) -> Result<PullResult<Sha512HashValue>> {
410 let imgref_str = imgref.to_string();
411 tracing::info!("Direct pull: fetching {imgref_str} into composefs repository");
412
413 let mut config = crate::deploy::new_proxy_config();
414 ostree_ext::container::merge_default_container_proxy_opts(&mut config)?;
415
416 let (reporter, prog_task) = progress::spawn(quiet, prog);
417
418 let pull_result = composefs_oci::pull(
419 repo,
420 &imgref_str,
421 None,
422 PullOptions {
423 img_proxy_config: Some(config),
424 progress: Some(reporter),
425 ..Default::default()
426 },
427 )
428 .await;
429
430 prog_task
434 .await
435 .context("Composefs progress task panicked")?;
436
437 pull_result.context("Pulling image into composefs repository")
438}
439
440async fn pull_composefs_unified(
455 imgstore: &CStorage,
456 storage_path: &str,
457 repo: &Arc<crate::store::ComposefsRepository>,
458 imgref: &containers_image_proxy::ImageReference,
459 quiet: bool,
460 prog: ProgressWriter,
461) -> Result<PullResult<Sha512HashValue>> {
462 let image = &imgref.name;
463
464 if imgref.transport == containers_image_proxy::Transport::ContainerStorage {
466 tracing::info!("Unified pull: copying {image} from host containers-storage");
469 imgstore
470 .pull_from_host_storage(image)
471 .await
472 .context("Copying image from host containers-storage into bootc storage")?;
473 } else {
474 let pull_ref = imgref.to_string();
477 tracing::info!("Unified pull: fetching {pull_ref} into containers-storage");
478 imgstore
479 .pull_with_progress(&pull_ref)
480 .await
481 .context("Pulling image into bootc containers-storage")?;
482 }
483
484 let cstor_imgref_str = format!("containers-storage:{image}");
487 tracing::info!("Unified pull: importing from {cstor_imgref_str} (zero-copy)");
488
489 let storage = std::path::Path::new(storage_path);
490 let (reporter, prog_task) = progress::spawn(quiet, prog);
491 let pull_opts = PullOptions {
492 local_fetch: LocalFetchOpt::ZeroCopy,
498 storage_root: Some(storage),
499 progress: Some(reporter),
500 ..Default::default()
501 };
502 let pull_result = composefs_oci::pull(repo, &cstor_imgref_str, None, pull_opts).await;
503
504 prog_task
505 .await
506 .context("Composefs progress task panicked")?;
507
508 let pull_result = pull_result.context("Importing from containers-storage into composefs")?;
509
510 Ok(pull_result)
511}
512
513#[context("Pulling composefs repository")]
530pub(crate) async fn pull_composefs_repo(
531 spec_imgref: &crate::spec::ImageReference,
532 delta: Option<&crate::delta::Delta>,
533 allow_missing_fsverity: bool,
534 use_unified: bool,
535 quiet: bool,
536 prog: ProgressWriter,
537) -> Result<PullRepoResult> {
538 const COMPOSEFS_PULL_JOURNAL_ID: &str = "4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8";
539
540 let imgref = match delta {
541 Some(delta) => {
542 delta.validate_image_reference(spec_imgref)?;
543 crate::delta::reject_unified_storage(delta, use_unified)?;
544 delta.pull_ref()?
545 }
546 None => spec_imgref.to_image_proxy_ref()?,
547 };
548
549 tracing::info!(
550 message_id = COMPOSEFS_PULL_JOURNAL_ID,
551 bootc.operation = "pull",
552 bootc.source_image = &spec_imgref.image,
553 bootc.transport = %imgref.transport,
554 bootc.allow_missing_fsverity = allow_missing_fsverity,
555 bootc.unified_storage = use_unified,
556 bootc.delta = delta.map(|d| d.path.as_str()),
557 "Pulling composefs image {imgref}",
558 );
559
560 let rootfs_dir = Dir::open_ambient_dir("/sysroot", ambient_authority())?;
561
562 let mut repo = open_composefs_repo(&rootfs_dir).context("Opening composefs repo")?;
563 if allow_missing_fsverity {
564 repo.set_insecure()?;
565 }
566
567 let repo = Arc::new(repo);
568
569 let upgrade_result =
577 composefs_oci::upgrade_repo(&repo).context("Upgrading old-format OCI images")?;
578 if upgrade_result.upgraded > 0 {
579 tracing::info!(
580 "Upgraded {} old-format OCI image(s) to current format",
581 upgrade_result.upgraded
582 );
583 }
584
585 let pull_result = if use_unified {
586 let root = Dir::open_ambient_dir("/", ambient_authority())?;
590 let sepolicy = lsm::new_sepolicy_at(&root)?;
591 let run = Dir::open_ambient_dir("/run", ambient_authority())?;
592 let imgstore = CStorage::create(&rootfs_dir, &run, sepolicy.as_ref())?;
593 let storage_path = format!("/sysroot/{}", CStorage::subpath());
594
595 pull_composefs_unified(&imgstore, &storage_path, &repo, &imgref, quiet, prog).await?
596 } else {
597 pull_composefs_direct(&repo, &imgref, quiet, prog).await?
598 };
599
600 let tag = bootc_tag_for_manifest(&pull_result.manifest_digest.to_string());
602 tag_image(&*repo, &pull_result.manifest_digest, &tag)
603 .context("Tagging pulled image as bootc GC root")?;
604
605 tracing::info!(
606 message_id = COMPOSEFS_PULL_JOURNAL_ID,
607 bootc.operation = "pull",
608 bootc.manifest_digest = %pull_result.manifest_digest,
609 bootc.manifest_verity = pull_result.manifest_verity.to_hex(),
610 bootc.config_digest = %pull_result.config_digest,
611 bootc.config_verity = pull_result.config_verity.to_hex(),
612 bootc.tag = tag,
613 "Pulled image into composefs repository",
614 );
615
616 let BootImage {
617 id,
618 boot_ids,
619 fs,
620 entries,
621 } = prepare_boot_image(&repo, &pull_result)?;
622
623 let mut repo = Arc::try_unwrap(repo).map_err(|_| {
625 anyhow::anyhow!("BUG: Arc<Repository> still has other references after pull completed")
626 })?;
627 if allow_missing_fsverity {
628 repo.set_insecure()?;
629 }
630
631 Ok(PullRepoResult {
632 repo,
633 entries,
634 id,
635 boot_ids,
636 manifest_digest: pull_result.manifest_digest.to_string(),
637 fs,
638 })
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use std::os::unix::fs::MetadataExt;
645
646 #[test]
647 fn test_bootc_tag_for_manifest() {
648 let digest = "sha256:abc123def456";
649 let tag = bootc_tag_for_manifest(digest);
650 assert_eq!(tag, "localhost/bootc-sha256:abc123def456");
651 assert!(tag.starts_with(BOOTC_TAG_PREFIX));
652 }
653
654 #[test]
655 fn test_repository_init_preserves_requested_verity_policy() {
656 for allow_missing in [true, false] {
657 let tempdir = tempfile::tempdir().unwrap();
658 let root = Dir::open_ambient_dir(tempdir.path(), ambient_authority()).unwrap();
659 let result = crate::store::ComposefsRepository::init_path(
660 &root,
661 ".",
662 composefs_repository_config(allow_missing),
663 );
664
665 match result {
666 Ok((repo, _)) => assert_eq!(repo.is_insecure(), allow_missing),
667 Err(error) if !allow_missing => {
668 let message = format!("{error:#}");
672 assert!(
673 message.to_ascii_lowercase().contains("verity"),
674 "strict initialization failed for an unrelated reason: {message}"
675 );
676 }
677 Err(error) => panic!("insecure initialization failed: {error:#}"),
678 }
679 }
680 }
681
682 #[test]
683 fn test_existing_repository_policy_is_one_way() {
684 let tempdir = tempfile::tempdir().unwrap();
685 let root = Dir::open_ambient_dir(tempdir.path(), ambient_authority()).unwrap();
686 let (strict_repo, _) = match crate::store::ComposefsRepository::init_path(
687 &root,
688 ".",
689 composefs_repository_config(false),
690 ) {
691 Ok(result) => result,
692 Err(error) if format!("{error:#}").to_ascii_lowercase().contains("verity") => {
693 return;
697 }
698 Err(error) => panic!("strict initialization failed unexpectedly: {error:#}"),
699 };
700 assert!(!strict_repo.is_insecure());
701
702 let metadata_before = std::fs::read(tempdir.path().join("meta.json")).unwrap();
703 let metadata_inode = std::fs::metadata(tempdir.path().join("meta.json"))
704 .unwrap()
705 .ino();
706 let (relaxed_repo, _) = crate::store::ComposefsRepository::init_path(
707 &root,
708 ".",
709 composefs_repository_config(true),
710 )
711 .unwrap();
712 assert!(!relaxed_repo.is_insecure());
713 assert_eq!(
714 metadata_before,
715 std::fs::read(tempdir.path().join("meta.json")).unwrap()
716 );
717 assert_eq!(
718 metadata_inode,
719 std::fs::metadata(tempdir.path().join("meta.json"))
720 .unwrap()
721 .ino()
722 );
723 assert!(validate_repository_policy(false, true, true).is_ok());
724
725 let insecure_tempdir = tempfile::tempdir().unwrap();
726 let insecure_root =
727 Dir::open_ambient_dir(insecure_tempdir.path(), ambient_authority()).unwrap();
728 let (insecure_repo, _) = crate::store::ComposefsRepository::init_path(
729 &insecure_root,
730 ".",
731 composefs_repository_config(true),
732 )
733 .unwrap();
734 assert!(insecure_repo.is_insecure());
735 assert!(validate_repository_policy(true, false, false).is_err());
736 }
737
738 #[test]
739 fn test_ensure_existing_v2_repository_preserves_metadata() {
740 let tempdir = tempfile::tempdir().unwrap();
741 let root = Dir::open_ambient_dir(tempdir.path(), ambient_authority()).unwrap();
742 crate::store::ensure_composefs_dir(&root).unwrap();
743 let mut v2_config =
744 RepositoryConfig::new(composefs::fsverity::Algorithm::SHA512).set_insecure();
745 v2_config.erofs_formats = composefs::erofs::format::FormatConfig::single(
746 composefs::erofs::format::FormatVersion::V2,
747 );
748 let (repo, _) =
749 crate::store::ComposefsRepository::init_path(&root, crate::store::COMPOSEFS, v2_config)
750 .unwrap();
751 let repo_path = tempdir.path().join(crate::store::COMPOSEFS);
752 let metadata_path = repo_path.join("meta.json");
753 let metadata_before = std::fs::read(&metadata_path).unwrap();
754 let object_path = repo_path.join("objects/sentinel");
755 std::fs::create_dir(object_path.parent().unwrap()).unwrap();
756 std::fs::write(&object_path, b"retained").unwrap();
757 drop(repo);
758
759 let (reopened, created) =
760 ensure_composefs_repository(&root, composefs_repository_config(true)).unwrap();
761 assert!(!created);
762 assert!(reopened.is_insecure());
763 assert_eq!(
764 reopened.erofs_version(),
765 composefs::erofs::format::FormatVersion::V2
766 );
767 assert_eq!(metadata_before, std::fs::read(metadata_path).unwrap());
768 assert_eq!(std::fs::read(object_path).unwrap(), b"retained");
769 }
770
771 #[test]
772 fn test_final_repository_policy_matrix() {
773 for (image_fsverity_policy, requested_relaxed, expected) in [
774 (Some(false), false, false),
775 (Some(false), true, false),
776 (Some(true), false, true),
777 (Some(true), true, true),
778 (None, false, false),
779 (None, true, true),
780 ] {
781 assert_eq!(
782 final_repository_policy(image_fsverity_policy, requested_relaxed),
783 expected
784 );
785 }
786 }
787
788 #[test]
789 fn test_existing_repository_policy_matrix() {
790 for (insecure, image_fsverity_policy, requested, explicit, expected) in [
791 (false, Some(false), false, false, true),
792 (false, Some(true), true, false, false),
793 (false, Some(true), true, true, true),
794 (false, None, false, false, true),
795 (false, None, true, false, false),
796 (false, None, true, true, true),
797 (true, Some(false), false, false, false),
798 (true, Some(true), true, false, true),
799 (true, None, false, false, false),
800 (true, None, true, false, true),
801 ] {
802 let requested_relaxed = final_repository_policy(image_fsverity_policy, requested);
803 assert_eq!(
804 validate_repository_policy(insecure, requested_relaxed, explicit).is_ok(),
805 expected,
806 "insecure={insecure} image_fsverity_policy={image_fsverity_policy:?} requested={requested} explicit={explicit}"
807 );
808 }
809 }
810
811 #[test]
812 fn test_fresh_policy_replacement_preserves_objects() {
813 let tempdir = tempfile::tempdir().unwrap();
814 let root = Dir::open_ambient_dir(tempdir.path(), ambient_authority()).unwrap();
815 crate::store::ensure_composefs_dir(&root).unwrap();
816 let (repo, _) = match crate::store::ComposefsRepository::init_path(
817 &root,
818 crate::store::COMPOSEFS,
819 composefs_repository_config(false),
820 ) {
821 Ok(repo) => repo,
822 Err(error) if format!("{error:#}").to_ascii_lowercase().contains("verity") => return,
823 Err(error) => panic!("strict repository initialization failed: {error:#}"),
824 };
825 let metadata_path = tempdir
826 .path()
827 .join(crate::store::COMPOSEFS)
828 .join("meta.json");
829 let metadata_inode = std::fs::metadata(&metadata_path).unwrap().ino();
830 let object_path = tempdir
831 .path()
832 .join(crate::store::COMPOSEFS)
833 .join("objects/sentinel");
834 std::fs::create_dir_all(object_path.parent().unwrap()).unwrap();
836 std::fs::write(&object_path, b"retained").unwrap();
837 drop(repo);
838
839 finalize_fresh_repository_policy(&root, composefs_repository_config(false)).unwrap();
840
841 let reopened =
842 crate::store::ComposefsRepository::open_path(&root, crate::store::COMPOSEFS).unwrap();
843 assert!(reopened.is_insecure());
844 assert_ne!(
845 metadata_inode,
846 std::fs::metadata(&metadata_path).unwrap().ino()
847 );
848 assert_eq!(std::fs::read(object_path).unwrap(), b"retained");
849 }
850
851 #[test]
852 fn test_fresh_policy_replacement_refuses_feature_mismatch() {
853 let tempdir = tempfile::tempdir().unwrap();
854 let root = Dir::open_ambient_dir(tempdir.path(), ambient_authority()).unwrap();
855 crate::store::ensure_composefs_dir(&root).unwrap();
856 let (repo, _) = crate::store::ComposefsRepository::init_path(
857 &root,
858 crate::store::COMPOSEFS,
859 composefs_repository_config(true),
860 )
861 .unwrap();
862 drop(repo);
863
864 let metadata_path = tempdir
865 .path()
866 .join(crate::store::COMPOSEFS)
867 .join("meta.json");
868 let mut metadata: composefs::repository::RepoMetadata =
869 serde_json::from_slice(&std::fs::read(&metadata_path).unwrap()).unwrap();
870 metadata
871 .features
872 .compatible
873 .push("review-only-feature".to_string());
874 std::fs::write(&metadata_path, serde_json::to_vec(&metadata).unwrap()).unwrap();
875 let metadata_inode = std::fs::metadata(&metadata_path).unwrap().ino();
876
877 let error = finalize_fresh_repository_policy(&root, composefs_repository_config(false))
878 .unwrap_err();
879 assert!(format!("{error:#}").contains("metadata"));
880 assert_eq!(
881 metadata_inode,
882 std::fs::metadata(&metadata_path).unwrap().ino()
883 );
884 }
885}