//
// Syd: rock-solid application kernel
// src/syd-lock.rs: Run a command under Landlock
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    env,
    ffi::OsString,
    ops::RangeInclusive,
    os::unix::process::CommandExt,
    process::{Command, ExitCode},
};

use nix::errno::Errno;
use syd::{
    config::*,
    err::{SydError, SydResult},
    landlock::{RulesetStatus, ABI},
    lock_enabled,
    path::XPathBuf,
};

fn main() -> SydResult<ExitCode> {
    use lexopt::prelude::*;

    syd::set_sigpipe_dfl()?;

    // Parse CLI options.
    //
    // Note, option parsing is POSIXly correct:
    // POSIX recommends that no more options are parsed after the first
    // positional argument. The other arguments are then all treated as
    // positional arguments.
    // See: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
    let mut opt_abick = false;
    let mut opt_check = false;
    let mut opt_verbose = false;
    let mut opt_cmd = env::var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));
    let mut opt_arg = Vec::new();
    let mut path_ro = Vec::new();
    let mut path_rw = Vec::new();
    let mut port_bind = Vec::new();
    let mut port_conn = Vec::new();

    let mut parser = lexopt::Parser::from_env();
    while let Some(arg) = parser.next()? {
        match arg {
            Short('h') => {
                help();
                return Ok(ExitCode::SUCCESS);
            }
            Short('A') => opt_abick = true,
            Short('V') => opt_check = true,
            Short('v') => opt_verbose = true,
            Short('r') => {
                let path = parser.value().map(XPathBuf::from)?;
                if !path.starts_with(b"/") {
                    eprintln!("syd-lock: -r requires an absolute path as argument!");
                    return Ok(ExitCode::FAILURE);
                }
                path_ro.push(path);
            }
            Short('w') => {
                let path = parser.value().map(XPathBuf::from)?;
                if !path.starts_with(b"/") {
                    eprintln!("syd-lock: -w requires an absolute path as argument!");
                    return Ok(ExitCode::FAILURE);
                }
                path_rw.push(path);
            }
            Short('b') => {
                let port = parser.value()?.parse::<String>()?;
                let parts: Vec<&str> = port.splitn(2, '-').collect();
                let port_range: RangeInclusive<u16> = if parts.len() == 2 {
                    parts[0].parse().or::<SydError>(Err(Errno::EINVAL.into()))?
                        ..=parts[1].parse().or::<SydError>(Err(Errno::EINVAL.into()))?
                } else {
                    let p = parts[0].parse().or::<SydError>(Err(Errno::EINVAL.into()))?;
                    p..=p
                };
                port_bind.push(port_range);
            }
            Short('c') => {
                let port = parser.value()?.parse::<String>()?;
                let parts: Vec<&str> = port.splitn(2, '-').collect();
                let port_range: RangeInclusive<u16> = if parts.len() == 2 {
                    parts[0].parse().or::<SydError>(Err(Errno::EINVAL.into()))?
                        ..=parts[1].parse().or::<SydError>(Err(Errno::EINVAL.into()))?
                } else {
                    let p = parts[0].parse().or::<SydError>(Err(Errno::EINVAL.into()))?;
                    p..=p
                };
                port_conn.push(port_range);
            }
            Value(prog) => {
                opt_cmd = prog;
                opt_arg.extend(parser.raw_args()?);
            }
            _ => return Err(arg.unexpected().into()),
        }
    }

    if opt_abick && opt_check {
        eprintln!("-A and -V are mutually exclusive!");
        return Err(Errno::EINVAL.into());
    }

    let abi = ABI::new_current();
    if opt_abick {
        let abi = abi as i32 as u8;
        print!("{abi}");
        return Ok(ExitCode::from(abi));
    } else if opt_check {
        if abi == ABI::Unsupported {
            println!("Landlock is not supported.");
            return Ok(ExitCode::from(127));
        }

        let state = lock_enabled(abi);
        let state_verb = match state {
            0 => "fully enforced",
            1 => "partially enforced",
            2 => "not enforced",
            _ => "unsupported",
        };
        println!("Landlock ABI {} is {state_verb}.", abi as i32);
        return Ok(ExitCode::from(state));
    }

    // Set up Landlock sandbox.
    macro_rules! vprintln {
        ($($arg:tt)*) => {
            if opt_verbose {
                eprintln!($($arg)*);
            }
        };
    }
    match syd::landlock_operation(abi, &path_ro, &path_rw, &port_bind, &port_conn, true, true) {
        Ok(status) => match status.ruleset {
            RulesetStatus::FullyEnforced => {
                vprintln!("syd-lock: Landlock ABI {} is fully enforced.", abi as i32)
            }
            RulesetStatus::PartiallyEnforced => {
                vprintln!(
                    "syd-lock: Landlock ABI {} is partially enforced.",
                    abi as i32
                )
            }
            RulesetStatus::NotEnforced => {
                eprintln!("syd-lock: Landlock ABI {} is not enforced!", abi as i32);
                return Ok(ExitCode::FAILURE);
            }
        },
        Err(error) => {
            eprintln!("syd-lock: Landlock ABI {} unsupported: {error}", abi as i32);
            return Ok(ExitCode::FAILURE);
        }
    };

    // Execute command, /bin/sh by default.
    Ok(ExitCode::from(
        127 + Command::new(opt_cmd)
            .args(opt_arg)
            .exec()
            .raw_os_error()
            .unwrap_or(0) as u8,
    ))
}

fn help() {
    println!("Usage: syd-lock [-hvAV] [-r path]... [-w path]... [-b port]... [-c port]... {{command [args...]}}");
    println!("Run a command under Landlock.");
    println!("Use -r to specify a read-only path, may be repeated.");
    println!("Use -w to specify a read-write path, may be repeated.");
    println!("Use -b to specify a bind port, may be repeated.");
    println!("Use -c to specify a connect port, may be repeated.");
    println!("Use -v to increase verbosity.");
    println!("Use -A to exit with Landlock ABI version, rather than running a command.");
    println!("Use -V to check for Landlock support, rather than running a command.");
}
