use std::fs; use std::os::unix::fs::symlink; use std::path::Path; use alt_controller_image::rootfs::{CopyTree, InitrdOem, RootfsFinalization, ServiceName}; use tempfile::tempdir; #[test] fn finalization_copies_trees_generates_oem_recipe_and_enables_services() { let fixture = tempdir().expect("fixture directory"); let source = fixture.path().join("overlay"); fs::create_dir_all(source.join("nested")).expect("create source tree"); fs::write(source.join("nested/controller.conf"), "mode = controller\n").expect("write source file"); symlink("nested/controller.conf", source.join("controller.conf")) .expect("create relative source symlink"); let rootfs = tempdir().expect("rootfs directory"); let finalization = RootfsFinalization::new( vec![CopyTree::new(&source, "etc/controller").expect("valid copy destination")], InitrdOem::new(["rootfs", "qemu"], ["ext4", "virtio_blk.ko"]), vec![ServiceName::new("chronyd.service").expect("valid service")], ); let manifest = finalization.apply(rootfs.path()).expect("finalize rootfs"); assert_eq!( fs::read_to_string(rootfs.path().join("etc/controller/nested/controller.conf")) .expect("copied file"), "mode = controller\n" ); assert_eq!( fs::read_link(rootfs.path().join("etc/controller/controller.conf")).expect("copied symlink"), Path::new("nested/controller.conf") ); assert_eq!( fs::read_to_string(rootfs.path().join("etc/initrd.mk.oem")).expect("OEM recipe"), "FEATURES += qemu rootfs\nMODULES += ext4 virtio_blk.ko\n" ); assert_eq!( fs::read_link( rootfs .path() .join("etc/systemd/system/multi-user.target.wants/chronyd.service") ) .expect("enabled service"), Path::new("/usr/lib/systemd/system/chronyd.service") ); assert_eq!( manifest.created_paths(), [ "etc/controller", "etc/controller/controller.conf", "etc/controller/nested", "etc/controller/nested/controller.conf", "etc/initrd.mk.oem", "etc/systemd/system/multi-user.target.wants/chronyd.service", ] ); } #[test] fn finalization_rejects_rootfs_escaping_destinations_and_symlinks() { assert!(CopyTree::new("fixtures/overlay", "/etc/controller").is_err()); assert!(CopyTree::new("fixtures/overlay", "../etc/controller").is_err()); let fixture = tempdir().expect("fixture directory"); let source = fixture.path().join("overlay"); fs::create_dir_all(&source).expect("create source tree"); symlink("../../outside", source.join("unsafe-link")).expect("create unsafe symlink"); let rootfs = tempdir().expect("rootfs directory"); let finalization = RootfsFinalization::new( vec![CopyTree::new(&source, "etc/controller").expect("valid destination")], InitrdOem::new(std::iter::empty::<&str>(), std::iter::empty::<&str>()), vec![], ); let error = finalization .apply(rootfs.path()) .expect_err("unsafe symlink must be rejected"); assert!(error.to_string().contains("escapes copied tree")); } #[test] fn finalization_is_idempotent_for_matching_symlinks() { let fixture = tempdir().expect("fixture directory"); let source = fixture.path().join("overlay"); fs::create_dir_all(&source).expect("create source tree"); symlink("controller.conf", source.join("controller-link")).expect("create source symlink"); fs::write(source.join("controller.conf"), "controller\n").expect("write source file"); let rootfs = tempdir().expect("rootfs directory"); let finalization = RootfsFinalization::new( vec![CopyTree::new(&source, "etc/controller").expect("valid copy destination")], InitrdOem::new(std::iter::empty::<&str>(), std::iter::empty::<&str>()), vec![ServiceName::new("chronyd.service").expect("valid service")], ); finalization.apply(rootfs.path()).expect("first finalization succeeds"); let second = finalization.apply(rootfs.path()).expect("repeat finalization succeeds"); assert_eq!( fs::read_link(rootfs.path().join("etc/controller/controller-link")).expect("copied symlink"), Path::new("controller.conf") ); assert_eq!( fs::read_link( rootfs .path() .join("etc/systemd/system/multi-user.target.wants/chronyd.service") ) .expect("enabled service"), Path::new("/usr/lib/systemd/system/chronyd.service") ); assert!(second .created_paths() .iter() .any(|path| path == "etc/controller/controller-link")); } #[test] fn finalization_replaces_a_destination_symlink_instead_of_following_it() { let fixture = tempdir().expect("fixture directory"); let source = fixture.path().join("overlay"); fs::create_dir_all(&source).expect("create source tree"); fs::write(source.join("controller.conf"), "new controller configuration\n") .expect("write source file"); let rootfs = tempdir().expect("rootfs directory"); let outside = fixture.path().join("outside.conf"); fs::write(&outside, "must not change\n").expect("write outside file"); fs::create_dir_all(rootfs.path().join("etc/controller")).expect("create destination directory"); symlink(&outside, rootfs.path().join("etc/controller/controller.conf")) .expect("create destination symlink"); let finalization = RootfsFinalization::new( vec![CopyTree::new(&source, "etc/controller").expect("valid copy destination")], InitrdOem::new(std::iter::empty::<&str>(), std::iter::empty::<&str>()), vec![], ); finalization.apply(rootfs.path()).expect("finalization replaces symlink safely"); assert_eq!(fs::read_to_string(&outside).expect("read outside file"), "must not change\n"); assert!(fs::symlink_metadata(rootfs.path().join("etc/controller/controller.conf")) .expect("inspect copied file") .file_type() .is_file()); assert_eq!( fs::read_to_string(rootfs.path().join("etc/controller/controller.conf")).expect("read copied file"), "new controller configuration\n" ); }