whyno-cli 0.3.0

Linux permission debugger
//! Path walk rendering for explain mode.
//!
//! Split from `explain.rs` for module size.
//! Writes per-component stat, ACL, flags, mount details.

use std::io::Write;

use whyno_core::state::path::PathComponent;
use whyno_core::state::SystemState;

use crate::error::OutputError;

/// Path walk section, one indexed component per line.
pub fn write_path_walk(state: &SystemState, writer: &mut dyn Write) -> Result<(), OutputError> {
    writeln!(writer, "=== Path Walk ===")?;
    for (i, comp) in state.walk.iter().enumerate() {
        write_component(i, comp, state, writer)?;
    }
    Ok(())
}

/// Stat, acl, flags, and mount detail for one indexed component.
fn write_component(
    index: usize,
    comp: &PathComponent,
    state: &SystemState,
    writer: &mut dyn Write,
) -> Result<(), OutputError> {
    writeln!(writer, "  [{index}] {}", comp.path.display())?;
    write_stat_info(comp, writer)?;
    write_acl_info(comp, writer)?;
    write_flags_info(comp, writer)?;
    write_mount_info(comp, state, writer)?;
    Ok(())
}

/// Stat line: mode (octal), uid, gid, file type, or probe status.
fn write_stat_info(comp: &PathComponent, writer: &mut dyn Write) -> Result<(), OutputError> {
    match &comp.stat {
        whyno_core::state::Probe::Known(stat) => {
            writeln!(
                writer,
                "      stat: mode={:#06o} uid={} gid={} type={:?}",
                stat.mode, stat.uid, stat.gid, stat.file_type
            )?;
        }
        whyno_core::state::Probe::Unknown => {
            writeln!(writer, "      stat: (unknown)")?;
        }
        whyno_core::state::Probe::Inaccessible => {
            writeln!(writer, "      stat: (inaccessible)")?;
        }
        _ => {
            writeln!(writer, "      stat: (unknown variant)")?;
        }
    }
    Ok(())
}

/// Acl section: entry count and per-entry detail, or probe status.
fn write_acl_info(comp: &PathComponent, writer: &mut dyn Write) -> Result<(), OutputError> {
    match &comp.acl {
        whyno_core::state::Probe::Known(acl) if !acl.0.is_empty() => {
            writeln!(writer, "      acl: {} entries", acl.0.len())?;
            for entry in &acl.0 {
                write_acl_entry(entry, writer)?;
            }
        }
        whyno_core::state::Probe::Known(_) => {
            writeln!(writer, "      acl: (none)")?;
        }
        whyno_core::state::Probe::Unknown => {
            writeln!(writer, "      acl: (unknown)")?;
        }
        whyno_core::state::Probe::Inaccessible => {
            writeln!(writer, "      acl: (inaccessible)")?;
        }
        _ => {
            writeln!(writer, "      acl: (unknown variant)")?;
        }
    }
    Ok(())
}

/// Single acl entry: tag, qualifier, and rwx perms.
fn write_acl_entry(
    entry: &whyno_core::state::acl::AclEntry,
    writer: &mut dyn Write,
) -> Result<(), OutputError> {
    writeln!(
        writer,
        "        {:?} q={:?} perms={:?}",
        entry.tag, entry.qualifier, entry.perms
    )?;
    Ok(())
}

/// Flags section: immutable and append_only probe results.
fn write_flags_info(comp: &PathComponent, writer: &mut dyn Write) -> Result<(), OutputError> {
    match &comp.flags {
        whyno_core::state::Probe::Known(flags) => {
            writeln!(
                writer,
                "      flags: immutable={} append_only={}",
                flags.immutable, flags.append_only
            )?;
        }
        whyno_core::state::Probe::Unknown => {
            writeln!(writer, "      flags: (unknown)")?;
        }
        whyno_core::state::Probe::Inaccessible => {
            writeln!(writer, "      flags: (inaccessible)")?;
        }
        _ => {
            writeln!(writer, "      flags: (unknown variant)")?;
        }
    }
    Ok(())
}

/// Mount section: mountpoint, fs_type, ro/noexec/nosuid options.
fn write_mount_info(
    comp: &PathComponent,
    state: &SystemState,
    writer: &mut dyn Write,
) -> Result<(), OutputError> {
    match comp.mount.and_then(|i| state.mounts.0.get(i)) {
        Some(entry) => {
            writeln!(
                writer,
                "      mount: {} ({}) ro={} noexec={} nosuid={}",
                entry.mountpoint.display(),
                entry.fs_type,
                entry.options.read_only,
                entry.options.noexec,
                entry.options.nosuid,
            )?;
        }
        None => {
            writeln!(writer, "      mount: (not resolved)")?;
        }
    }
    Ok(())
}

#[cfg(test)]
#[path = "explain_walk_tests.rs"]
mod tests;