arcbox-agent 0.9.0

Guest agent for ArcBox VMs
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::Command;

use super::super::appledouble::{AppleDouble, Attr};
use super::super::xattrs::{Mode, Store};
use super::Identity;

const STORE: Store = Store::new(64);

fn run(command: &mut Command) {
    let output = command.output().unwrap();
    assert!(output.status.success(), "{command:?}: {output:?}");
}

struct Mount(PathBuf);

impl Mount {
    fn image(image: &Path, target: &Path, filesystem: &str) -> Self {
        run(Command::new("mount")
            .args(["-t", filesystem, "-o", "loop"])
            .arg(image)
            .arg(target));
        Self(target.to_owned())
    }

    fn overlay(root: &Path) -> Self {
        let target = root.join("merged");
        let options = format!(
            "lowerdir={},upperdir={},workdir={}",
            root.join("lower").display(),
            root.join("upper").display(),
            root.join("work").display()
        );
        run(Command::new("mount")
            .args(["-t", "overlay", "overlay", "-o", &options])
            .arg(&target));
        Self(target)
    }
}

impl Drop for Mount {
    fn drop(&mut self) {
        run(Command::new("umount").arg(&self.0));
    }
}

fn content() -> AppleDouble {
    AppleDouble {
        resource_fork: Some(vec![9u8; 100]),
        ..AppleDouble::default()
    }
}

#[test]
#[ignore = "requires root, mount, mkfs.ext4, and mkfs.btrfs"]
fn mounted_roots_preserve_remounts_and_distinguish_filesystems() {
    for filesystem in ["ext4", "btrfs"] {
        let dir = tempfile::tempdir().unwrap();
        let target = dir.path().join("mounted");
        fs::create_dir(&target).unwrap();
        let images = [dir.path().join("first.img"), dir.path().join("second.img")];
        for image in &images {
            fs::File::create(image)
                .unwrap()
                .set_len(128 * 1024 * 1024)
                .unwrap();
            let force = if filesystem == "ext4" { "-F" } else { "-f" };
            run(Command::new(format!("mkfs.{filesystem}"))
                .args([force, "-q"])
                .arg(image));
        }
        let mount = Mount::image(&images[0], &target, filesystem);
        let first = Identity::of(&target).unwrap();
        STORE.store(&target, &content(), Mode::Replace).unwrap();
        let link = dir.path().join("link");
        symlink(&target, &link).unwrap();
        let link_identity = Identity::of(&link).unwrap();
        STORE.store(&link, &content(), Mode::Replace).unwrap();
        drop(mount);

        let mount = Mount::image(&images[0], &target, filesystem);
        assert_eq!(
            Identity::of(&target).unwrap(),
            first,
            "{filesystem} remount"
        );
        assert_eq!(STORE.load(&target).unwrap(), Some(content()));
        drop(mount);

        let _mount = Mount::image(&images[1], &target, filesystem);
        let second = Identity::of(&target).unwrap();
        assert_ne!(
            first, second,
            "{filesystem} roots belong to different filesystems"
        );
        assert_eq!(STORE.load(&target).unwrap(), None);
        assert_eq!(Identity::of(&link).unwrap(), link_identity);
        assert_eq!(STORE.load(&link).unwrap(), Some(content()));
    }
}

#[test]
#[ignore = "requires root and overlayfs mount support"]
fn overlay_copy_up_and_remount_preserve_the_side_entry() {
    let dir = tempfile::tempdir().unwrap();
    for name in ["lower", "upper", "work", "merged"] {
        fs::create_dir(dir.path().join(name)).unwrap();
    }
    fs::write(dir.path().join("lower/file"), b"lower content").unwrap();
    let target = dir.path().join("merged/file");
    let mount = Mount::overlay(dir.path());
    let first = Identity::of(&target).unwrap();
    let content = AppleDouble {
        attrs: vec![Attr {
            name: b"note".to_vec(),
            value: b"copy up".to_vec(),
        }],
        ..content()
    };
    STORE.store(&target, &content, Mode::Replace).unwrap();
    assert_eq!(Identity::of(&target).unwrap(), first);
    assert_eq!(STORE.load(&target).unwrap(), Some(content.clone()));
    drop(mount);

    let _mount = Mount::overlay(dir.path());
    assert_eq!(Identity::of(&target).unwrap(), first);
    assert_eq!(STORE.load(&target).unwrap(), Some(content));
}