whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
use super::*;
use crate::operation::Operation;
use crate::state::fsflags::FsFlags;
use crate::test_helpers::StateBuilder;

fn immutable_flags() -> FsFlags {
    FsFlags {
        immutable: true,
        append_only: false,
    }
}

fn append_only_flags() -> FsFlags {
    FsFlags {
        immutable: false,
        append_only: true,
    }
}

fn no_flags() -> FsFlags {
    FsFlags {
        immutable: false,
        append_only: false,
    }
}

#[test]
fn write_to_immutable_file_fails() {
    let state = StateBuilder::new()
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags("/data.txt", 0, 0, 0o644, immutable_flags())
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_fail());
    if let LayerResult::Fail { detail, .. } = &result {
        assert!(detail.contains("immutable"));
    }
}

#[test]
fn write_to_append_only_file_fails() {
    let state = StateBuilder::new()
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags("/log.txt", 0, 0, 0o644, append_only_flags())
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_fail());
    if let LayerResult::Fail { detail, .. } = &result {
        assert!(detail.contains("append-only"));
    }
}

#[test]
fn read_from_immutable_file_passes() {
    let state = StateBuilder::new()
        .operation(Operation::Read)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags("/data.txt", 0, 0, 0o644, immutable_flags())
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_pass());
}

#[test]
fn execute_on_immutable_file_passes() {
    let state = StateBuilder::new()
        .operation(Operation::Execute)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags("/bin/tool", 0, 0, 0o755, immutable_flags())
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_pass());
}

#[test]
fn delete_targets_parent_so_unknown_flags_degrades() {
    let state = StateBuilder::new()
        .operation(Operation::Delete)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags("/locked.txt", 0, 0, 0o644, immutable_flags())
        .build();

    // delete checks the parent (index 0). parent's flags are Unknown
    // (default from component()), so this returns Degraded.
    let result = check_fs_flags(&state);
    assert!(result.is_degraded());
}

#[test]
fn delete_with_immutable_parent_fails() {
    let state = StateBuilder::new()
        .operation(Operation::Delete)
        .mount("/", "ext4", "rw")
        .component_with_flags("/", 0, 0, 0o755, immutable_flags())
        .component_file("/locked.txt", 0, 0, 0o644)
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_fail());
    if let LayerResult::Fail { detail, .. } = &result {
        assert!(detail.contains("immutable"));
    }
}

#[test]
fn unknown_flags_returns_degraded() {
    let state = StateBuilder::new()
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file("/data.txt", 0, 0, 0o644)
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_degraded());
}

#[test]
fn no_flags_set_passes() {
    let state = StateBuilder::new()
        .operation(Operation::Write)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags("/data.txt", 0, 0, 0o644, no_flags())
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_pass());
}

#[test]
fn stat_on_immutable_file_passes() {
    let state = StateBuilder::new()
        .operation(Operation::Stat)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_file_with_flags("/data.txt", 0, 0, 0o644, immutable_flags())
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_pass());
}

#[test]
fn delete_with_append_only_parent_fails() {
    let state = StateBuilder::new()
        .operation(Operation::Delete)
        .mount("/", "ext4", "rw")
        .component_with_flags("/dir", 0, 0, 0o755, append_only_flags())
        .component_file("/dir/file.txt", 0, 0, 0o644)
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_fail());
    if let LayerResult::Fail { detail, .. } = &result {
        assert!(
            detail.contains("append-only"),
            "expected append-only: {detail}"
        );
        assert!(
            detail.contains("cannot delete"),
            "expected delete context: {detail}"
        );
    }
}

#[test]
fn create_with_append_only_parent_passes() {
    let state = StateBuilder::new()
        .operation(Operation::Create)
        .mount("/", "ext4", "rw")
        .component_with_flags("/dir", 0, 0, 0o755, append_only_flags())
        .component_file("/dir/newfile.txt", 0, 0, 0o644)
        .build();

    let result = check_fs_flags(&state);
    assert!(result.is_pass());
}