//
// Syd: rock-solid application kernel
// src/parsers/syd.rs: syd(2) nom parsers
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

//! syd(2) api parsers and utility functions.

use std::{ops::RangeInclusive, str::FromStr};

use btoi::{btoi, btoi_radix};
use dur::Duration;
use fixedbitset::FixedBitSet;
use memchr::arch::all::is_prefix;
use nix::errno::Errno;
use nom::{
    branch::alt,
    bytes::complete::{tag, tag_no_case, take_while1},
    character::complete::{char, digit1, one_of},
    combinator::{all_consuming, map, opt, recognize},
    error::{Error, ErrorKind},
    multi::separated_list1,
    sequence::preceded,
    Finish, IResult, Parser,
};

use crate::{
    confine::SydMountAttrFlags,
    hash::SydHashSet,
    landlock::{AccessFs, AccessNet},
    landlock_policy::LandlockPolicy,
    mount::api::MountAttrFlags,
    path::XPathBuf,
    sandbox::{Action, Capability, SANDBOX_CAPS},
};

// Valid Netlink families.
//
// Note, this list must be sorted because it's binary searched.
const NETLINK_FAMILIES: &[&str] = &[
    "all",
    "audit",
    "connector",
    "crypto",
    "dnrtmsg",
    "ecryptfs",
    "fib_lookup",
    "firewall",
    "generic",
    "inet_diag",
    "ip6_fw",
    "iscsi",
    "kobject_uevent",
    "netfilter",
    "nflog",
    "rdma",
    "route",
    "scsitransport",
    "selinux",
    "smc",
    "sock_diag",
    "usersock",
    "xfrm",
];

// Represents a parsed "bind" command: operation and mount details.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct BindCmd {
    // One of '+', '-', '^'
    pub(crate) op: char,
    // Mount source.
    pub(crate) src: String,
    // Mount destination.
    pub(crate) dst: String,
    // Mount flags.
    pub(crate) opt: MountAttrFlags,
    // Optional filesystem specific data.
    pub(crate) dat: Option<String>,
}

// Represents a parsed "force" command.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ForceCmd {
    // One of '+', '-', '^'
    pub(crate) op: char,
    // Source path (required for '+' and '-')
    pub(crate) src: Option<String>,
    // Hex string (required for '+')
    pub(crate) key: Option<String>,
    // Action (optional; default is Deny)
    pub(crate) act: Option<Action>,
}

// Represents a parsed "setuid" or "setgid" command.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SetIdCmd {
    // Either 'u' for uid or 'g' for gid
    pub(crate) id: char,
    // One of '+', '-', '^'
    pub(crate) op: char,
    // Source user/group (for '+' and '-' and '^' with src)
    pub(crate) src: Option<String>,
    // Destination user/group (for '+' and '-')
    pub(crate) dst: Option<String>,
}

/// Network port range
pub type PortRange = RangeInclusive<u16>;

/// Set of paths
pub type PathSet = SydHashSet<XPathBuf>;

/// Fixed bit set of port ranges
pub type PortSet = FixedBitSet;

/// landlock(7) access control rule
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LandlockRule {
    /// landlock(7) filesystem rule
    Fs((AccessFs, String)),
    /// landlock(7) network rule
    Net((AccessNet, PortRange)),
}

/// Array of Landlock access control rules
pub type LandlockFilter = Vec<LandlockRule>;

/// Represents a Landlock rule operation.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum LandlockOp {
    /// Add operation
    Add,
    /// Remove-all operation
    Rem,
}

impl TryFrom<char> for LandlockOp {
    type Error = Errno;

    fn try_from(c: char) -> Result<Self, Self::Error> {
        match c {
            '+' => Ok(Self::Add),
            '-' | '^' => Ok(Self::Rem),
            _ => Err(Errno::EINVAL),
        }
    }
}

/// Parsed filesystem sandbox rule.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct FsCmd {
    pub(crate) action: Action,
    pub(crate) op: char,
    pub(crate) fs_type: String,
}

/// Parsed "allow/lock" command.
#[derive(Debug, PartialEq, Eq)]
pub struct LandlockCmd {
    /// Access filter
    pub filter: LandlockFilter,
    /// Operation: add or remove.
    pub op: LandlockOp,
}

// Pattern for seccomp rule: either a filesystem path or an IP-based address.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ScmpPattern {
    Path(String),
    Addr(String),
    Host(String),
}

// Parsed seccomp rule command.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ScmpCmd {
    pub(crate) action: Action,
    pub(crate) filter: Capability,
    pub(crate) op: char,
    pub(crate) pat: ScmpPattern,
}

// Operation for Netlink families.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum NetlinkOp {
    Clear,
    Add(Vec<String>),
    Del(Vec<String>),
}

// Parsed Netlink command.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct NetlinkCmd {
    pub(crate) op: NetlinkOp,
}

impl NetlinkCmd {
    pub fn new(op: NetlinkOp) -> Self {
        NetlinkCmd { op }
    }
}

// Parse a "bind" command string, returning `BindCmd` or `Errno::EINVAL`.
//
// Accepts: `bind<mod><src>:<dst>(:<opt>)?`
//   - `<mod>` is '+', '-', or '^'
//   - `<src>` is a nonempty sequence of characters except ':'
//   - `<dst>` is a nonempty sequence of characters except ':'
//   - `<opt>` (optional) is a nonempty sequence of characters (no newlines)
//
// Additional validation:
//   * `<dst>` must start with '/'; `<src>` may be a filesystem type (no leading '/')
//   * Neither `<src>` nor `<dst>` may contain ".."
//   * If `<opt>` is present, split on commas: known flags -> MsFlags; unknown accumulate into `dat`.
pub(crate) fn parse_bind_cmd(command: &str) -> Result<BindCmd, Errno> {
    // Inner nom parser: returns (remaining, (op, src_str, dst_str, opt_str?))
    #[expect(clippy::type_complexity)]
    fn inner(input: &str) -> IResult<&str, (char, &str, &str, Option<&str>)> {
        // Sequence: "bind", one of '+','-','^', <src> (no ':'), ":", <dst> (no ':'), optional ":" + <opt>
        (
            tag("bind"),
            alt((char('+'), char('-'), char('^'))),
            take_while1(|c| c != ':'), // src
            char(':'),                 // consume colon
            take_while1(|c| c != ':'), // dst
            opt(preceded(char(':'), take_while1(|_| true))),
        )
            .map(|(_bind, op, src, _, dst, opt_part)| (op, src, dst, opt_part))
            .parse(input)
    }

    match inner(command).finish() {
        Ok(("", (op, src, dst, opt))) => {
            // Build MountAttrFlags and dat from opt.
            let mut flags = MountAttrFlags::empty();
            let mut dat = Vec::new();

            if let Some(opt) = opt {
                for flag in opt.split(',') {
                    // Reject invalid flags and data.
                    if flag
                        .chars()
                        .next()
                        .map(|n| n.is_whitespace())
                        .unwrap_or(true)
                    {
                        return Err(Errno::EINVAL);
                    }
                    if flag
                        .chars()
                        .last()
                        .map(|n| n.is_whitespace())
                        .unwrap_or(true)
                    {
                        return Err(Errno::EINVAL);
                    }

                    // Try to parse as a mount flag.
                    if let Some(syd_flag) = SydMountAttrFlags::from_name(flag) {
                        flags |= syd_flag.0;
                    } else {
                        // Add to auxiliary data.
                        if !dat.is_empty() {
                            dat.push(b',');
                        }
                        dat.extend_from_slice(flag.as_bytes());
                    }
                }
            }

            let dat = if dat.is_empty() {
                None
            } else {
                Some(String::from_utf8(dat).or(Err(Errno::EINVAL))?)
            };

            // Source and destination will be expanded as necessary,
            // so we perform no further handling here.
            let src = src.to_string();
            let dst = dst.to_string();

            Ok(BindCmd {
                op,
                src,
                dst,
                dat,
                opt: flags,
            })
        }
        _ => Err(Errno::EINVAL),
    }
}

/// Parse a "force" command string, returning `ForceCmd` or `Errno::EINVAL`.
///
/// Accepts:
///   - `force^`
///   - `force-/path`
///   - `force+/path:<hashhex>[:<action>]`
///
///   * `<hashhex>` must be exactly 8, 16, 32, 40, 64, 96, or 128 hex chars.
///   * `<action>` (optional) is one of "warn", "filter", "deny", "panic", "stop", "abort", "kill", or "exit".
pub(crate) fn parse_force_cmd(input: &str) -> Result<ForceCmd, Errno> {
    // Branch for "force^"
    fn parse_clear(input: &str) -> IResult<&str, ForceCmd> {
        map(tag("force^"), |_| ForceCmd {
            op: '^',
            src: None,
            key: None,
            act: None,
        })
        .parse(input)
    }

    // Branch for "force-/path"
    fn parse_remove(input: &str) -> IResult<&str, ForceCmd> {
        let (rem, (_, src)) = (tag("force-"), take_while1(|_| true)).parse(input)?;
        Ok((
            rem,
            ForceCmd {
                op: '-',
                src: Some(src.to_string()),
                key: None,
                act: None,
            },
        ))
    }

    // Branch for "force+/path:<hex>[:<action>]"
    fn parse_add(input: &str) -> IResult<&str, ForceCmd> {
        // Basic parsing into elements.
        let (rem, (_tag, src, _colon1, key, act)) = (
            tag("force+"),
            take_while1(|c: char| c != ':'), // path (may be env var!)
            char(':'),                       // colon before hex
            take_while1(|c: char| c != ':'), // hex (may be env var!)
            opt(preceded(char(':'), take_while1(|_| true))), // optional :action
        )
            .parse(input)?;

        // Validate action.
        let act = if let Some(act) = act {
            match Action::from_str(act) {
                Ok(act) => Some(act),
                Err(_) => {
                    return Err(nom::Err::Error(Error::new(input, ErrorKind::Fail)));
                }
            }
        } else {
            None
        };

        let fc = ForceCmd {
            act,
            op: '+',
            src: Some(src.to_string()),
            key: Some(key.to_string()),
        };

        Ok((rem, fc))
    }

    // Top-level: try clear, then remove, then add
    match alt((parse_clear, parse_remove, parse_add))
        .parse(input)
        .finish()
    {
        Ok(("", cmd)) => Ok(cmd),
        _ => Err(Errno::EINVAL),
    }
}

// Parse a "setuid/setgid" command string, returning `SetIdCmd` or `Errno::EINVAL`.
//
// Accepts exactly:
//   - `setuid+<src>:<dst>`
//   - `setuid-<src>:<dst>`
//   - `setuid^<src>`
//   - `setuid^`
//   - `setgid+<src>:<dst>`
//   - `setgid-<src>:<dst>`
//   - `setgid^<src>`
//   - `setgid^`
//
//   * `<src>` and `<dst>` must be nonempty sequences without ':'.
//   * For '^' with no `<src>`, both `src` and `dst` are `None`.
pub(crate) fn parse_setid_cmd(input: &str) -> Result<SetIdCmd, Errno> {
    // Parser for "set[id][op][src]:[dst]".
    fn parse_pm(input: &str) -> IResult<&str, SetIdCmd> {
        let (rem, (_, id, _, op, src, _, dst)) = (
            tag("set"),
            one_of("ug"),
            tag("id"),
            one_of("+-"),
            take_while1(|c| c != ':'),
            char(':'),
            take_while1(|c| c != ':'),
        )
            .parse(input)?;
        Ok((
            rem,
            SetIdCmd {
                id,
                op,
                src: Some(src.to_string()),
                dst: Some(dst.to_string()),
            },
        ))
    }

    // Parser for "set[id]^([src])?".
    fn parse_caret(input: &str) -> IResult<&str, SetIdCmd> {
        let (rem, (_, id, _, _, src)) = (
            tag("set"),
            one_of("ug"),
            tag("id"),
            char('^'),
            opt(take_while1(|c| c != ':')),
        )
            .parse(input)?;
        Ok((
            rem,
            SetIdCmd {
                id,
                op: '^',
                src: src.map(str::to_string),
                dst: None,
            },
        ))
    }

    // Try plus/minus branch first, then caret branch.
    match alt((parse_pm, parse_caret)).parse(input).finish() {
        Ok(("", cmd)) => Ok(cmd),
        _ => Err(Errno::EINVAL),
    }
}

/// Parse a port range which is either a single port
/// or a closed range in format "port1-port2".
pub fn parse_port_range(input: &str) -> Result<PortRange, Errno> {
    let mut split = input.splitn(2, '-');

    let port0 = split.next().ok_or(Errno::EINVAL)?;
    let port0 = port0.parse::<u16>().or(Err(Errno::EINVAL))?;

    let ports = if let Some(port1) = split.next() {
        let port1 = port1.parse::<u16>().or(Err(Errno::EINVAL))?;
        if port1 >= port0 {
            port0..=port1
        } else {
            port1..=port0
        }
    } else {
        port0..=port0
    };

    if ports.is_empty() {
        return Err(Errno::EINVAL);
    }

    Ok(ports)
}

/// Parse an "allow/lock" command: "allow/lock/<access_list><op><arg>"
/// Returns `LandlockCmd` or `Errno::EINVAL`.
pub fn parse_landlock_cmd(input: &str) -> Result<LandlockCmd, Errno> {
    // Inner parser: match prefix, then capture access_part, op, and arg_part.
    fn inner(input: &str) -> IResult<&str, (&str, char, &str)> {
        let (rem, (_, access, op, arg)) = (
            tag("allow/lock/"),
            // access_part: one or more chars until an op character is reached.
            take_while1(|c: char| !matches!(c, '+' | '-' | '^')),
            one_of("+-^"),
            // arg_part: rest of input, must be non-empty.
            take_while1(|_| true),
        )
            .parse(input)?;
        Ok((rem, (access, op, arg)))
    }

    match inner(input).finish() {
        Ok(("", (access, op, arg))) => {
            // Determine operation and access rights.
            let op = LandlockOp::try_from(op)?;
            let (mut access_fs, access_net) = LandlockPolicy::access(access)?;

            // Validate access rights.
            let ports = parse_port_range(arg).ok();
            if access_net.contains(AccessNet::ConnectTcp) {
                // connect requires a port-range.
                if ports.is_none() {
                    return Err(Errno::EINVAL);
                }

                // bind implies BindTcp+MakeSock.
                if access_fs == AccessFs::MakeSock {
                    // bind,connect
                    access_fs = AccessFs::EMPTY;
                } else if !access_fs.is_empty() {
                    // connect,<filesystem-right>
                    return Err(Errno::EINVAL);
                }
            }

            let mut filter = LandlockFilter::new();
            if access_net == AccessNet::BindTcp && access_fs == AccessFs::MakeSock {
                // Require absolute pathnames for UNIX domain sockets.
                // Allow environment variables as well which will be treated as paths.
                // This way passing a relative UNIX domain socket path is still possible.
                let c = arg.chars().next().ok_or(Errno::EINVAL)?;
                if matches!(c, '/' | '$') {
                    filter.push(LandlockRule::Fs((access_fs, arg.into())));
                } else {
                    let ports = ports.ok_or(Errno::EINVAL)?;
                    filter.push(LandlockRule::Net((access_net, ports)));
                }
            } else if !access_fs.is_empty() {
                filter.push(LandlockRule::Fs((access_fs, arg.into())));
            } else if access_net.contains(AccessNet::ConnectTcp) {
                let ports = ports.ok_or(Errno::EINVAL)?;
                filter.push(LandlockRule::Net((access_net, ports)));
            } else {
                eprintln!("LO:4");
                return Err(Errno::EINVAL);
            }

            Ok(LandlockCmd { filter, op })
        }
        _ => Err(Errno::EINVAL),
    }
}

// Parse a filesystem rule command string, returning `FsCmd` or `Errno::EINVAL`.
//
// Format: `<action>/fs<op><fs_type>`
// - `<action>`: one of allow, deny, filter, warn, stop, abort, kill, panic, exit
// - `<op>`: one of '+','-','^'
// - `<fs_type>`: non-empty string.
//
// Returns Err(EINVAL) on any parse or validation failure.
pub(crate) fn parse_fs_cmd(input: &str) -> Result<FsCmd, Errno> {
    // Inner parser: action "/" op fs_type.
    fn inner(input: &str) -> IResult<&str, (&str, char, &str)> {
        (
            take_while1(|c| c != '/'),
            char('/'),
            tag("fs"),
            one_of("+-^"),
            take_while1(|_| true), // fs_type (rest of line, must be non-empty).
        )
            .map(|(act, _slash, _caps, op, fs_type)| (act, op, fs_type))
            .parse(input)
    }

    match inner(input).finish() {
        Ok(("", (act, op, fs_type))) => {
            let action = Action::from_str(act).map_err(|_| Errno::EINVAL)?;
            let fs_type = fs_type.to_string();

            Ok(FsCmd {
                action,
                op,
                fs_type,
            })
        }
        _ => Err(Errno::EINVAL),
    }
}

// Parse a seccomp rule command string, returning `ScmpCmd` or `Errno::EINVAL`.
//
// Format: `<action>/<caps><op><pat>`
// - `<action>`: one of allow, deny, filter, warn, stop, abort, kill, panic, exit
// - `<caps>`: "all" or comma-separated valid capabilities
//    * FS caps from VALID_FS_CAPS
//    * net caps exactly one from VALID_NET_CAPS
//    * if "all" appears anywhere, becomes All
// - `<op>`: one of '+','-','^'
// - `<pat>`: non-empty string.
//    * If filter == Many(["net/bind"]) or Many(["net/connect"]):
//        attempt to parse as IP; if successful, Pattern::Addr; else Pattern::Path.
//    * Otherwise: Pattern::Path.
//
// Returns Err(EINVAL) on any parse or validation failure.
pub(crate) fn parse_scmp_cmd(input: &str) -> Result<ScmpCmd, Errno> {
    // Match a capability name via SANDBOX_CAPS iteration order.
    fn cap_name(input: &str) -> IResult<&str, &str> {
        for name in SANDBOX_CAPS.keys().rev() {
            if let Some(rest) = input.strip_prefix(name) {
                if rest.is_empty() || rest.starts_with([',', '+', '-', '^']) {
                    return Ok((rest, *name));
                }
            }
        }
        Err(nom::Err::Error(Error::new(input, ErrorKind::Tag)))
    }

    // Inner parser: action "/" caps op pat.
    #[expect(clippy::type_complexity)]
    fn inner(input: &str) -> IResult<&str, (&str, &str, char, &str)> {
        (
            take_while1(|c| c != '/'),
            char('/'),
            recognize(separated_list1(char(','), cap_name)),
            one_of("+-^"),
            take_while1(|_| true), // pat (rest of line, must be non-empty).
        )
            .map(|(act, _slash, caps, op, pat)| (act, caps, op, pat))
            .parse(input)
    }

    match inner(input).finish() {
        Ok(("", (act, caps, op, pat))) => {
            // Parse action.
            let action = Action::from_str(act).map_err(|_| Errno::EINVAL)?;

            // Determine filter.
            let mut filter = Capability::empty();

            // Split capy by comma.
            // Be strict and do _not_ trim here.
            for cap in caps.split(',') {
                // Reject empty caps.
                if cap.is_empty() {
                    return Err(Errno::EINVAL);
                }

                let cap = Capability::from_str(cap)?;

                // Reject ioctl which is for requests not paths.
                if cap.contains(Capability::CAP_IOCTL) {
                    return Err(Errno::EINVAL);
                }

                filter.insert(cap);
            }

            // Reject empty caps.
            if filter.is_empty() {
                return Err(Errno::EINVAL);
            }

            // IP address arguments are only valid for the `net` set.
            let maybe_addr = filter.intersects(Capability::CAP_NET)
                && filter.difference(Capability::CAP_NET).is_empty();

            // Pattern resolution.
            let pat = if maybe_addr {
                // Try network alias first.
                if let Ok((rem_host, host)) = host_parser(pat).finish() {
                    if rem_host.is_empty() {
                        ScmpPattern::Host(host.to_string())
                    } else {
                        // Fallback to IP or path.
                        if let Ok((rem_addr, addr)) = addr_parser(pat).finish() {
                            if rem_addr.is_empty() {
                                ScmpPattern::Addr(addr.to_string())
                            } else {
                                ScmpPattern::Path(addr.to_string())
                            }
                        } else {
                            ScmpPattern::Path(pat.to_string())
                        }
                    }
                } else if let Ok((rem_addr, addr)) = addr_parser(pat).finish() {
                    if rem_addr.is_empty() {
                        // Fallback to IP.
                        ScmpPattern::Addr(addr.to_string())
                    } else {
                        // Fallback to path.
                        ScmpPattern::Path(addr.to_string())
                    }
                } else {
                    ScmpPattern::Path(pat.to_string())
                }
            } else if pat.is_empty() {
                return Err(Errno::EINVAL);
            } else {
                // Fallback to non-empty path.
                ScmpPattern::Path(pat.to_string())
            };

            Ok(ScmpCmd {
                action,
                filter,
                op,
                pat,
            })
        }
        _ => Err(Errno::EINVAL),
    }
}

// Parse a Netlink rule command string, returning `NetlinkCmd` or `Errno::EINVAL`.
//
// Format: `allow/net/link<suffix>`
// - `<suffix>` is one of:
//     '^'                              (Clear)
//     '+' <family1>[,<family2>,...]    (Add)
//     '-' <family1>[,<family2>,...]    (Del)
//
// `<familyX>` must be one of VALID_FAMILIES.
// Entire string must match with no trailing characters.
pub(crate) fn parse_netlink_cmd(input: &str) -> Result<NetlinkCmd, Errno> {
    // Inner parser: after "allow/net/link", parse one of:
    //  - '^'                           → Clear
    //  - '+' <family_list>             → Add(family_list)
    //  - '-' <family_list>             → Del(family_list)
    fn inner(input: &str) -> IResult<&str, NetlinkOp> {
        alt((
            // Clear: single '^'
            map(char('^'), |_| NetlinkOp::Clear),
            // Add: '+' followed by validated family list
            map((char('+'), netlink_parser), |(_, fams)| {
                NetlinkOp::Add(fams)
            }),
            // Del: '-' followed by validated family list
            map((char('-'), netlink_parser), |(_, fams)| {
                NetlinkOp::Del(fams)
            }),
        ))
        .parse(input)
    }

    // First, match the prefix "allow/net/link", then parse the suffix entirely
    let mut parser = preceded(tag("allow/net/link"), all_consuming(inner));
    match parser
        .parse(input)
        .finish()
        .map(|(_, op)| NetlinkCmd::new(op))
    {
        Ok(cmd) => Ok(cmd),
        Err(_) => Err(Errno::EINVAL),
    }
}

// Parses a comma-separated list of families, returning a Vec<String> if all are valid.
fn netlink_parser(input: &str) -> IResult<&str, Vec<String>> {
    // Grab one-or-more non-comma characters per family.
    let fam_parser = nom::bytes::complete::take_while1(|c: char| c != ',');

    // Separated by commas.
    let (rem, raw_list) = separated_list1(char(','), fam_parser).parse(input)?;

    // Validate each.
    for &fam in &raw_list {
        if NETLINK_FAMILIES.binary_search(&fam).is_err() {
            return Err(nom::Err::Error(Error::new(input, ErrorKind::Fail)));
        }
    }

    // Convert to Vec<String>.
    let vec = raw_list.iter().map(|s| s.to_string()).collect();

    Ok((rem, vec))
}

// Parser for an IP-based address pattern:
//   <hex-or-dot-or-colon>+ (e.g., IPv4 or IPv6)
//   optionally '/' + <digits>
//   then '!' or '@'
//   then <digits> (port) optionally '-' <digits> (port range)
fn addr_parser(input: &str) -> IResult<&str, &str> {
    recognize(all_consuming((
        take_while1(|c: char| c.is_ascii_hexdigit() || c == '.' || c == ':'),
        // optional /mask
        nom::combinator::opt((char('/'), digit1)),
        // separator '!' or '@'
        one_of("!@"),
        // port or port-range
        digit1,
        nom::combinator::opt((char('-'), digit1)),
    )))
    .parse(input)
}

// Parser for a network-host alias (case-insensitive):
//   "any", "any4", "any6",
//   "local", "local4", "local6",
//   "loopback", "loopback4", "loopback6",
//   "linklocal", "linklocal4", "linklocal6"
// followed by "!" or "@", then <digits>, optionally "-" <digits>.
fn host_parser(input: &str) -> IResult<&str, &str> {
    // Base aliases (case-insensitive)
    let alias_base = alt((
        tag_no_case("any"),
        tag_no_case("local"),
        tag_no_case("loopback"),
        tag_no_case("linklocal"),
    ));
    let alias_tuple = (
        alias_base,
        opt(one_of("46")),
        one_of("!@"),
        digit1,
        opt((char('-'), digit1)),
    );
    recognize(all_consuming(alias_tuple)).parse(input)
}

/// Converts a string representation of a number into a `i64` value.
///
/// The string can be in hexadecimal (prefixed with "0x"), octal
/// (prefixed with "0o"), or decimal format. If the conversion fails, it
/// returns an `Errno::EINVAL` error.
pub fn str2i64(value: &[u8]) -> Result<i64, Errno> {
    if is_prefix(value, b"0x") || is_prefix(value, b"0X") {
        btoi_radix::<i64>(&value[2..], 16)
    } else if is_prefix(value, b"0o") || is_prefix(value, b"0O") {
        btoi_radix::<i64>(&value[2..], 8)
    } else {
        btoi::<i64>(value)
    }
    .or(Err(Errno::EINVAL))
}

/// Converts a string representation of a number into a `u64` value.
///
/// The string can be in hexadecimal (prefixed with "0x"), octal
/// (prefixed with "0o"), or decimal format. If the conversion fails, it
/// returns an `Errno::EINVAL` error.
pub fn str2u64(value: &[u8]) -> Result<u64, Errno> {
    if is_prefix(value, b"0x") || is_prefix(value, b"0X") {
        btoi_radix::<u64>(&value[2..], 16)
    } else if is_prefix(value, b"0o") || is_prefix(value, b"0O") {
        btoi_radix::<u64>(&value[2..], 8)
    } else {
        btoi::<u64>(value)
    }
    .or(Err(Errno::EINVAL))
}

/// Converts a string representation of a duration to a `Duration` value.
///
/// The string may be human formatted in which case it will be parsed by
/// the dur crate.
pub fn str2secs(value: &str) -> Result<Duration, Errno> {
    if let Ok(value) = str2u64(value.as_bytes()) {
        Ok(Duration::from_secs(value.into()))
    } else {
        value.parse::<Duration>().or(Err(Errno::EINVAL))
    }
}

/// Converts a string representation of a duration to a `Duration` value.
///
/// The string may be human formatted in which case it will be parsed by
/// the dur crate.
pub fn str2micros(value: &str) -> Result<Duration, Errno> {
    if let Ok(value) = str2u64(value.as_bytes()) {
        Ok(Duration::from_micros(value.into()))
    } else {
        value.parse::<Duration>().or(Err(Errno::EINVAL))
    }
}

/// Converts a string representation of a number into a `u32` value.
///
/// The string can be in hexadecimal (prefixed with "0x"), octal
/// (prefixed with "0o"), or decimal format. If the conversion fails, it
/// returns an `Errno::EINVAL` error.
pub fn str2u32(value: &[u8]) -> Result<u32, Errno> {
    if is_prefix(value, b"0x") || is_prefix(value, b"0X") {
        btoi_radix::<u32>(&value[2..], 16)
    } else if is_prefix(value, b"0o") || is_prefix(value, b"0O") {
        btoi_radix::<u32>(&value[2..], 8)
    } else {
        btoi::<u32>(value)
    }
    .or(Err(Errno::EINVAL))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_str2i64_decimal() {
        assert_eq!(str2i64(b"0"), Ok(0));
        assert_eq!(str2i64(b"123"), Ok(123));
        assert_eq!(str2i64(b"-456"), Ok(-456));
        assert_eq!(str2i64(b"9223372036854775807"), Ok(i64::MAX));
        assert_eq!(str2i64(b"-9223372036854775808"), Ok(i64::MIN));
    }

    #[test]
    fn test_str2i64_hexadecimal() {
        assert_eq!(str2i64(b"0x0"), Ok(0));
        assert_eq!(str2i64(b"0x1a"), Ok(26));
        assert_eq!(str2i64(b"0xFF"), Ok(255));
        assert_eq!(str2i64(b"0XDeAdBeEf"), Ok(0xDEADBEEF));
    }

    #[test]
    fn test_str2i64_octal() {
        assert_eq!(str2i64(b"0o0"), Ok(0));
        assert_eq!(str2i64(b"0o17"), Ok(15));
        assert_eq!(str2i64(b"0o777"), Ok(511));
        assert_eq!(str2i64(b"0O755"), Ok(493));
    }

    #[test]
    fn test_str2i64_invalid() {
        assert_eq!(str2i64(b""), Err(Errno::EINVAL));
        assert_eq!(str2i64(b"abc"), Err(Errno::EINVAL));
        assert_eq!(str2i64(b"12.34"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_str2u64_decimal() {
        assert_eq!(str2u64(b"0"), Ok(0));
        assert_eq!(str2u64(b"123"), Ok(123));
        assert_eq!(str2u64(b"18446744073709551615"), Ok(u64::MAX));
    }

    #[test]
    fn test_str2u64_hexadecimal() {
        assert_eq!(str2u64(b"0x0"), Ok(0));
        assert_eq!(str2u64(b"0xdeadbeef"), Ok(0xDEADBEEF));
        assert_eq!(str2u64(b"0XCAFEBABE"), Ok(0xCAFEBABE));
    }

    #[test]
    fn test_str2u64_octal() {
        assert_eq!(str2u64(b"0o0"), Ok(0));
        assert_eq!(str2u64(b"0o777"), Ok(511));
        assert_eq!(str2u64(b"0O644"), Ok(420));
    }

    #[test]
    fn test_str2u64_invalid() {
        assert_eq!(str2u64(b""), Err(Errno::EINVAL));
        assert_eq!(str2u64(b"-1"), Err(Errno::EINVAL));
        assert_eq!(str2u64(b"abc"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_str2u32_decimal() {
        assert_eq!(str2u32(b"0"), Ok(0));
        assert_eq!(str2u32(b"123"), Ok(123));
        assert_eq!(str2u32(b"4294967295"), Ok(u32::MAX));
    }

    #[test]
    fn test_str2u32_hexadecimal() {
        assert_eq!(str2u32(b"0x0"), Ok(0));
        assert_eq!(str2u32(b"0xFFFFFFFF"), Ok(u32::MAX));
        assert_eq!(str2u32(b"0xcafe"), Ok(0xCAFE));
    }

    #[test]
    fn test_str2u32_octal() {
        assert_eq!(str2u32(b"0o0"), Ok(0));
        assert_eq!(str2u32(b"0o777"), Ok(511));
    }

    #[test]
    fn test_str2u32_invalid() {
        assert_eq!(str2u32(b""), Err(Errno::EINVAL));
        assert_eq!(str2u32(b"-1"), Err(Errno::EINVAL));
        assert_eq!(str2u32(b"4294967296"), Err(Errno::EINVAL)); // overflow
    }

    #[test]
    fn test_str2secs_numeric() {
        assert_eq!(str2secs("0"), Ok(Duration::from_secs(0)));
        assert_eq!(str2secs("60"), Ok(Duration::from_secs(60)));
        assert_eq!(str2secs("3600"), Ok(Duration::from_secs(3600)));
    }

    #[test]
    fn test_str2secs_hex() {
        assert_eq!(str2secs("0x3c"), Ok(Duration::from_secs(60)));
    }

    #[test]
    fn test_str2secs_invalid() {
        assert_eq!(str2secs(""), Err(Errno::EINVAL));
        assert_eq!(str2secs("abc"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_str2micros_numeric() {
        assert_eq!(str2micros("0"), Ok(Duration::from_micros(0)));
        assert_eq!(str2micros("1000"), Ok(Duration::from_micros(1000)));
        assert_eq!(str2micros("1000000"), Ok(Duration::from_micros(1_000_000)));
    }

    #[test]
    fn test_str2micros_hex() {
        assert_eq!(str2micros("0x3e8"), Ok(Duration::from_micros(1000)));
    }

    #[test]
    fn test_str2micros_invalid() {
        assert_eq!(str2micros(""), Err(Errno::EINVAL));
        assert_eq!(str2micros("abc"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_bind_parse_bind_plus_root_readonly() {
        let cmd = "bind+/:/:ro";
        let bc = parse_bind_cmd(cmd).unwrap();
        assert_eq!(bc.op, '+');
        assert_eq!(bc.src, "/");
        assert_eq!(bc.dst, "/");
        assert!(bc.opt.contains(MountAttrFlags::MOUNT_ATTR_RDONLY));
        assert!(bc.dat.is_none());
    }

    #[test]
    fn test_parse_bind_parse_bind_minus_tmpfs_no_opts() {
        let cmd = "bind-/mnt/data:/data";
        let bc = parse_bind_cmd(cmd).unwrap();
        assert_eq!(bc.op, '-');
        assert_eq!(bc.src, "/mnt/data");
        assert_eq!(bc.dst, "/data");
        assert!(bc.opt.is_empty());
        assert!(bc.dat.is_none());
    }

    #[test]
    fn test_parse_bind_parse_bind_caret_multi_opts_and_dat() {
        let cmd =
            "bind^overlay:/tmp/target:lowerdir=/tmp/lower,upperdir=/tmp/upper,workdir=/tmp/work";
        let bc = parse_bind_cmd(cmd).unwrap();
        assert_eq!(bc.op, '^');
        assert_eq!(bc.src, "overlay");
        assert_eq!(bc.dst, "/tmp/target");
        // All three flags are unknown, so go into dat_buf
        assert!(bc.opt.is_empty());
        assert_eq!(
            bc.dat.unwrap(),
            "lowerdir=/tmp/lower,upperdir=/tmp/upper,workdir=/tmp/work",
        );
    }

    #[test]
    fn test_parse_bind_parse_bind_known_and_unknown_opts() {
        let cmd = "bind+tmpfs:/tmp:ro,nosuid,size=10M";
        let bc = parse_bind_cmd(cmd).unwrap();
        assert_eq!(bc.op, '+');
        assert_eq!(bc.src, "tmpfs");
        assert_eq!(bc.dst, "/tmp");
        assert!(bc.opt.contains(MountAttrFlags::MOUNT_ATTR_RDONLY));
        assert!(bc.opt.contains(MountAttrFlags::MOUNT_ATTR_NOSUID));
        // "size=10M" is unknown -> goes into dat_buf
        assert_eq!(bc.dat.unwrap(), "size=10M");
    }

    #[test]
    fn test_parse_bind_parse_bind_empty_parts_fails() {
        assert_eq!(parse_bind_cmd("bind+::"), Err(Errno::EINVAL));
        assert_eq!(parse_bind_cmd("bind+/src::opt"), Err(Errno::EINVAL));
        assert_eq!(parse_bind_cmd("bind+:/dst:opt"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_bind_parse_bind_bad_mod_fails() {
        assert_eq!(parse_bind_cmd("bind*src:/dst"), Err(Errno::EINVAL));
        assert_eq!(parse_bind_cmd("bind=src:/dst"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_bind_parse_bind_missing_prefix_fails() {
        assert_eq!(parse_bind_cmd("stat"), Err(Errno::EINVAL));
        assert_eq!(parse_bind_cmd("bindsrc:/dst"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_bind_parse_bind_options_spacing_fails() {
        assert_eq!(parse_bind_cmd("bind+src:/dst: ro"), Err(Errno::EINVAL));
        assert_eq!(parse_bind_cmd("bind+src:/dst:ro "), Err(Errno::EINVAL));
        assert_eq!(
            parse_bind_cmd("bind+src:/dst:ro, nosuid"),
            Err(Errno::EINVAL)
        );
        assert_eq!(
            parse_bind_cmd("bind+src:/dst:ro,nosuid "),
            Err(Errno::EINVAL)
        );
        assert_eq!(
            parse_bind_cmd("bind+src:/dst:  ro,  nosuid"),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_force_parse_clear_force() {
        let fc = parse_force_cmd("force^").unwrap();
        assert_eq!(fc.op, '^');
        assert_eq!(fc.src, None);
        assert_eq!(fc.key, None);
        assert_eq!(fc.act, None);
    }

    #[test]
    fn test_parse_force_parse_remove_force() {
        let fc = parse_force_cmd("force-/usr/bin/foo").unwrap();
        assert_eq!(fc.op, '-');
        assert_eq!(fc.src.unwrap(), "/usr/bin/foo");
        assert_eq!(fc.key, None);
        assert_eq!(fc.act, None);
    }

    #[test]
    fn test_parse_force_parse_add_force_minimal() {
        let fc = parse_force_cmd("force+/usr/bin/bar:abcd1234").unwrap();
        assert_eq!(fc.op, '+');
        assert_eq!(fc.src.unwrap(), "/usr/bin/bar");
        assert_eq!(fc.key.unwrap(), "abcd1234".to_string());
        assert_eq!(fc.act, None);
    }

    #[test]
    fn test_parse_force_parse_add_force_with_action() {
        let fc = parse_force_cmd("force+/bin/prog:0123456789abcdef:warn").unwrap();
        assert_eq!(fc.op, '+');
        assert_eq!(fc.src.unwrap(), "/bin/prog");
        assert_eq!(fc.key.unwrap(), "0123456789abcdef".to_string());
        assert_eq!(fc.act.unwrap(), Action::Warn);
    }

    #[test]
    fn test_parse_force_parse_add_force_long_hash_and_filter() {
        let long_hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
        let cmd = format!("force+/lib/x:{long_hash}:filter");
        let fc = parse_force_cmd(&cmd).unwrap();
        assert_eq!(fc.op, '+');
        assert_eq!(fc.src.unwrap(), "/lib/x");
        assert_eq!(fc.key.unwrap(), long_hash.to_string());
        assert_eq!(fc.act.unwrap(), Action::Filter);
    }

    #[test]
    fn test_parse_force_parse_force_invalid_op() {
        assert_eq!(parse_force_cmd("force*=stuff"), Err(Errno::EINVAL));
        assert_eq!(parse_force_cmd("force?"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_force_parse_force_add_missing_parts_fails() {
        assert_eq!(parse_force_cmd("force+"), Err(Errno::EINVAL));
        assert_eq!(parse_force_cmd("force+/path"), Err(Errno::EINVAL));
        assert_eq!(parse_force_cmd("force+/path:"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_force_parse_force_remove_missing_path_fails() {
        assert_eq!(parse_force_cmd("force-"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_force_parse_force_add_invalid_action_fails() {
        assert_eq!(
            parse_force_cmd("force+/x:abcd1234:invalid"),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_force_parse_force_extra_chars_after_fails() {
        assert_eq!(parse_force_cmd("force^extra"), Err(Errno::EINVAL));
        assert_eq!(
            parse_force_cmd("force+/path:abcd1234:warn:extra"),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_setid_parse_setuid_add() {
        let cmd = parse_setid_cmd("setuid+alice:bob").unwrap();
        assert_eq!(
            cmd,
            SetIdCmd {
                id: 'u',
                op: '+',
                src: Some("alice".into()),
                dst: Some("bob".into()),
            }
        );
    }

    #[test]
    fn test_parse_setid_parse_setgid_remove() {
        let cmd = parse_setid_cmd("setgid-john:doe").unwrap();
        assert_eq!(
            cmd,
            SetIdCmd {
                id: 'g',
                op: '-',
                src: Some("john".into()),
                dst: Some("doe".into()),
            }
        );
    }

    #[test]
    fn test_parse_setid_parse_setuid_clear_all() {
        let cmd = parse_setid_cmd("setuid^").unwrap();
        assert_eq!(
            cmd,
            SetIdCmd {
                id: 'u',
                op: '^',
                src: None,
                dst: None,
            }
        );
    }

    #[test]
    fn test_parse_setid_parse_setgid_clear_src() {
        let cmd = parse_setid_cmd("setgid^wheel").unwrap();
        assert_eq!(
            cmd,
            SetIdCmd {
                id: 'g',
                op: '^',
                src: Some("wheel".into()),
                dst: None,
            }
        );
    }

    #[test]
    fn test_parse_setid_parse_setid_invalid_prefix() {
        assert_eq!(parse_setid_cmd("setxid+user:group"), Err(Errno::EINVAL));
        assert_eq!(parse_setid_cmd("setuid*user:group"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_setid_parse_setid_missing_parts() {
        assert_eq!(parse_setid_cmd("setuid+alice"), Err(Errno::EINVAL)); // missing ":dst"
        assert_eq!(parse_setid_cmd("setuid-alice"), Err(Errno::EINVAL)); // missing ":dst"
    }

    #[test]
    fn test_parse_landlock_parse_all_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/all+/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((LandlockPolicy::access_fs_from_set("all"), "/trusted".into(),))
        );
    }

    #[test]
    fn test_parse_landlock_parse_all_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/all-/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((LandlockPolicy::access_fs_from_set("all"), "/trusted".into(),))
        );
    }

    #[test]
    fn test_parse_landlock_parse_all_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/all^/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((LandlockPolicy::access_fs_from_set("all"), "/trusted".into(),))
        );
    }

    #[test]
    fn test_parse_landlock_parse_rpath_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/rpath+/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("rpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_rpath_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/rpath-/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("rpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_rpath_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/rpath^/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("rpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_wpath_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/wpath+/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("wpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_wpath_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/wpath-/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("wpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_wpath_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/wpath^/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("wpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_cpath_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/cpath+/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("cpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_cpath_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/cpath-/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("cpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_cpath_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/cpath^/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("cpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_dpath_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/dpath+/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("dpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_dpath_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/dpath-/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("dpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_dpath_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/dpath^/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("dpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_spath_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/spath+/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("spath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_spath_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/spath-/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("spath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_spath_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/spath^/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("spath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_tpath_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/tpath+/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("tpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_tpath_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/tpath-/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("tpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_tpath_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/tpath^/trusted").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                LandlockPolicy::access_fs_from_set("tpath"),
                "/trusted".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_net_inet_plus_path() {
        let cmd = parse_landlock_cmd("allow/lock/inet+1024-65535").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Net((LandlockPolicy::access_net_from_set("inet"), 1024..=65535,))
        );
    }

    #[test]
    fn test_parse_landlock_parse_net_inet_minus_path() {
        let cmd = parse_landlock_cmd("allow/lock/inet-1024-65535").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Net((LandlockPolicy::access_net_from_set("inet"), 1024..=65535,))
        );
    }

    #[test]
    fn test_parse_landlock_parse_net_inet_caret_path() {
        let cmd = parse_landlock_cmd("allow/lock/inet^1024-65535").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Net((LandlockPolicy::access_net_from_set("inet"), 1024..=65535,))
        );
    }

    #[test]
    fn test_parse_landlock_parse_many_fs_rights_minus() {
        let cmd = parse_landlock_cmd("allow/lock/read,write,exec-/var/log").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((
                AccessFs::ReadFile | AccessFs::WriteFile | AccessFs::Execute,
                "/var/log".into(),
            ))
        );
    }

    #[test]
    fn test_parse_landlock_parse_many_net_rights_caret() {
        let cmd = parse_landlock_cmd("allow/lock/bind,connect^1000-2000").unwrap();
        assert_eq!(cmd.op, LandlockOp::Rem);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Net((AccessNet::BindTcp | AccessNet::ConnectTcp, 1000..=2000,))
        );
    }

    #[test]
    fn test_parse_landlock_parse_single_right_write_plus() {
        let cmd = parse_landlock_cmd("allow/lock/write+tmp").unwrap();
        assert_eq!(cmd.op, LandlockOp::Add);
        assert_eq!(
            cmd.filter.first().cloned().unwrap(),
            LandlockRule::Fs((AccessFs::WriteFile, "tmp".into(),))
        );
    }

    #[test]
    fn test_parse_landlock_parse_invalid_prefix() {
        assert_eq!(
            parse_landlock_cmd("allow/lockx/write+/tmp"),
            Err(Errno::EINVAL)
        );
        assert_eq!(
            parse_landlock_cmd("deny/lock/read+/tmp"),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_landlock_parse_invalid_rights() {
        assert_eq!(
            parse_landlock_cmd("allow/lock/invalid+/tmp"),
            Err(Errno::EINVAL)
        );
        assert_eq!(
            parse_landlock_cmd("allow/lock/read,foo+/tmp"),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_landlock_parse_missing_op_or_arg_fails() {
        assert_eq!(parse_landlock_cmd("allow/lock/all"), Err(Errno::EINVAL));
        assert_eq!(parse_landlock_cmd("allow/lock/all+"), Err(Errno::EINVAL));
        assert_eq!(
            parse_landlock_cmd("allow/lock/read,write-"),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_scmp_parse_all_fs_plus_path() {
        let cmd = parse_scmp_cmd("allow/all+/usr/bin").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_GLOB,
                op: '+',
                pat: ScmpPattern::Path("/usr/bin".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_all_x() {
        let cmd = parse_scmp_cmd("allow/all-x+/home/***").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_ALL_X,
                op: '+',
                pat: ScmpPattern::Path("/home/***".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_all_x_minus_op() {
        let cmd = parse_scmp_cmd("deny/all-x-/tmp/***").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Deny,
                filter: Capability::CAP_ALL_X,
                op: '-',
                pat: ScmpPattern::Path("/tmp/***".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_all_x_caret_op() {
        let cmd = parse_scmp_cmd("filter/all-x^/var/***").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Filter,
                filter: Capability::CAP_ALL_X,
                op: '^',
                pat: ScmpPattern::Path("/var/***".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_all_x_with_read() {
        let cmd = parse_scmp_cmd("allow/all-x,read+/data").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_ALL_X,
                op: '+',
                pat: ScmpPattern::Path("/data".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_read_with_all_x() {
        let cmd = parse_scmp_cmd("warn/read,all-x-/secure").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Warn,
                filter: Capability::CAP_ALL_X,
                op: '-',
                pat: ScmpPattern::Path("/secure".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_all_x_with_exec() {
        // all-x combined with exec should give CAP_GLOB (since all-x = all - exec)
        let cmd = parse_scmp_cmd("allow/all-x,exec+/bin").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_GLOB,
                op: '+',
                pat: ScmpPattern::Path("/bin".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_all_vs_all_x_distinction() {
        // Ensure `all` is correctly parsed when not followed by `-x`
        let cmd = parse_scmp_cmd("allow/all+/path").unwrap();
        assert_eq!(cmd.filter, Capability::CAP_GLOB);

        let cmd = parse_scmp_cmd("allow/all-x+/path").unwrap();
        assert_eq!(cmd.filter, Capability::CAP_ALL_X);
    }

    #[test]
    fn test_parse_scmp_parse_all_with_others() {
        let cmd = parse_scmp_cmd("deny/all,read+/tmp").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Deny,
                filter: Capability::CAP_GLOB,
                op: '+',
                pat: ScmpPattern::Path("/tmp".into())
            }
        );

        let cmd = parse_scmp_cmd("allow/write,truncate,all-/tmp").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_GLOB,
                op: '-',
                pat: ScmpPattern::Path("/tmp".into())
            }
        );

        let cmd = parse_scmp_cmd("filter/all,chdir^/tmp").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Filter,
                filter: Capability::CAP_GLOB,
                op: '^',
                pat: ScmpPattern::Path("/tmp".into())
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_many_fs_minus_path() {
        let cmd = parse_scmp_cmd("deny/read,write-/var/log").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Deny,
                filter: Capability::CAP_READ | Capability::CAP_WRITE,
                op: '-',
                pat: ScmpPattern::Path("/var/log".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_single_net_bind_plus_addr() {
        let cmd = parse_scmp_cmd("filter/net/bind+10.0.0.0/24!80-90").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Filter,
                filter: Capability::CAP_NET_BIND,
                op: '+',
                pat: ScmpPattern::Addr("10.0.0.0/24!80-90".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_single_net_bind_plus_path() {
        let cmd = parse_scmp_cmd("warn/net/bind+/some/dir").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Warn,
                filter: Capability::CAP_NET_BIND,
                op: '+',
                pat: ScmpPattern::Path("/some/dir".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_single_net_connect_minus_addr() {
        let cmd = parse_scmp_cmd("warn/net/connect-2001:db8::1@22").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Warn,
                filter: Capability::CAP_NET_CONNECT,
                op: '-',
                pat: ScmpPattern::Addr("2001:db8::1@22".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_single_net_connect_minus_path() {
        let cmd = parse_scmp_cmd("exit/net/connect-/var/run/socket").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Exit,
                filter: Capability::CAP_NET_CONNECT,
                op: '-',
                pat: ScmpPattern::Path("/var/run/socket".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_net_sendfd_plus_path() {
        let cmd = parse_scmp_cmd("exit/net/sendfd+/tmp/socket").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Exit,
                filter: Capability::CAP_NET_SENDFD,
                op: '+',
                pat: ScmpPattern::Path("/tmp/socket".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_invalid_action() {
        assert_eq!(parse_scmp_cmd("block/all+/path"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_scmp_parse_invalid_caps_fails() {
        assert_eq!(parse_scmp_cmd("allow/foo+/path"), Err(Errno::EINVAL));
        assert_eq!(parse_scmp_cmd("deny/read,foo+/path"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_scmp_parse_net_combo_with_fs() {
        let cmd = parse_scmp_cmd("allow/net/bind,read+/file").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_NET_BIND | Capability::CAP_READ,
                op: '+',
                pat: ScmpPattern::Path("/file".into()),
            }
        );

        let cmd = parse_scmp_cmd("kill/read,net/connect-/file").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Kill,
                filter: Capability::CAP_NET_CONNECT | Capability::CAP_READ,
                op: '-',
                pat: ScmpPattern::Path("/file".into()),
            }
        );

        let cmd = parse_scmp_cmd("panic/read,net/sendfd,write^/file").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Panic,
                filter: Capability::CAP_NET_SENDFD | Capability::CAP_READ | Capability::CAP_WRITE,
                op: '^',
                pat: ScmpPattern::Path("/file".into()),
            }
        );

        let cmd =
            parse_scmp_cmd("filter/net/bind,read,net/sendfd,write,net/connect+/file").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Filter,
                filter: Capability::CAP_NET | Capability::CAP_READ | Capability::CAP_WRITE,
                op: '+',
                pat: ScmpPattern::Path("/file".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_multiple_net() {
        let cmd = parse_scmp_cmd("allow/net/bind,net/connect+1.2.3.4!80").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_NET_BIND | Capability::CAP_NET_CONNECT,
                op: '+',
                pat: ScmpPattern::Addr("1.2.3.4!80".into()),
            }
        );

        let cmd = parse_scmp_cmd("abort/read,net/bind,net/connect-1.2.3.4!80").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Abort,
                filter: Capability::CAP_NET_BIND
                    | Capability::CAP_NET_CONNECT
                    | Capability::CAP_READ,
                op: '-',
                pat: ScmpPattern::Path("1.2.3.4!80".into()),
            }
        );

        let cmd = parse_scmp_cmd("stop/net/bind,net/connect,net/sendfd^1.2.3.4!80").unwrap();
        assert_eq!(
            cmd,
            ScmpCmd {
                action: Action::Stop,
                filter: Capability::CAP_NET,
                op: '^',
                pat: ScmpPattern::Addr("1.2.3.4!80".into()),
            }
        );
    }

    #[test]
    fn test_parse_scmp_parse_invalid_addr_fails() {
        assert_eq!(
            parse_scmp_cmd("allow/net/bind+not_ip"),
            Ok(ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_NET_BIND,
                op: '+',
                pat: ScmpPattern::Path("not_ip".into()),
            })
        );
        assert_eq!(
            parse_scmp_cmd("allow/net/connect+1.2.3.4!port"),
            Ok(ScmpCmd {
                action: Action::Allow,
                filter: Capability::CAP_NET_CONNECT,
                op: '+',
                pat: ScmpPattern::Path("1.2.3.4!port".into()),
            })
        );
    }

    #[test]
    fn test_parse_scmp_parse_missing_parts_fails() {
        assert_eq!(parse_scmp_cmd("allow/all"), Err(Errno::EINVAL));
        assert_eq!(parse_scmp_cmd("deny/net/bind+"), Err(Errno::EINVAL));
        assert_eq!(parse_scmp_cmd("warn/stat,path+/file"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_scmp_test_parse_netlink_parse_clear() {
        let cmd = parse_netlink_cmd("allow/net/link^").unwrap();
        assert_eq!(cmd.op, NetlinkOp::Clear);
    }

    #[test]
    fn test_parse_netlink_parse_add_single_family() {
        let cmd = parse_netlink_cmd("allow/net/link+route").unwrap();
        assert_eq!(cmd.op, NetlinkOp::Add(vec!["route".into()]));
    }

    #[test]
    fn test_parse_netlink_parse_add_multiple_families() {
        let cmd = parse_netlink_cmd("allow/net/link+route,usersock,firewall").unwrap();
        assert_eq!(
            cmd.op,
            NetlinkOp::Add(vec!["route".into(), "usersock".into(), "firewall".into()])
        );
    }

    #[test]
    fn test_parse_netlink_parse_del_single_family() {
        let cmd = parse_netlink_cmd("allow/net/link-fib_lookup").unwrap();
        assert_eq!(cmd.op, NetlinkOp::Del(vec!["fib_lookup".into()]));
    }

    #[test]
    fn test_parse_netlink_parse_del_multiple_families() {
        let cmd = parse_netlink_cmd("allow/net/link-selinux,sock_diag,crypto").unwrap();
        assert_eq!(
            cmd.op,
            NetlinkOp::Del(vec!["selinux".into(), "sock_diag".into(), "crypto".into()])
        );
    }

    #[test]
    fn test_parse_netlink_invalid_family_name_fails() {
        assert_eq!(parse_netlink_cmd("allow/net/link+foo"), Err(Errno::EINVAL));
        assert_eq!(
            parse_netlink_cmd("allow/net/link-bar,unknown"),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_netlink_missing_suffix_fails() {
        assert_eq!(parse_netlink_cmd("allow/net/link"), Err(Errno::EINVAL));
        assert_eq!(parse_netlink_cmd("allow/net/link "), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_netlink_trailing_characters_fail() {
        assert_eq!(
            parse_netlink_cmd("allow/net/link^extra"),
            Err(Errno::EINVAL)
        );
        assert_eq!(
            parse_netlink_cmd("allow/net/link+route "),
            Err(Errno::EINVAL)
        );
    }

    #[test]
    fn test_parse_netlink_empty_family_list_fails() {
        assert_eq!(parse_netlink_cmd("allow/net/link+"), Err(Errno::EINVAL));
        assert_eq!(parse_netlink_cmd("allow/net/link-"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_port_range_single_port() {
        assert_eq!(parse_port_range("80"), Ok(80..=80));
        assert_eq!(parse_port_range("443"), Ok(443..=443));
        assert_eq!(parse_port_range("0"), Ok(0..=0));
        assert_eq!(parse_port_range("65535"), Ok(65535..=65535));
    }

    #[test]
    fn test_parse_port_range_range() {
        assert_eq!(parse_port_range("1024-65535"), Ok(1024..=65535));
        assert_eq!(parse_port_range("80-443"), Ok(80..=443));
        assert_eq!(parse_port_range("0-1023"), Ok(0..=1023));
    }

    #[test]
    fn test_parse_port_range_reversed_normalizes() {
        // Reversed range should be auto-normalized
        assert_eq!(parse_port_range("100-50"), Ok(50..=100));
        assert_eq!(parse_port_range("65535-1024"), Ok(1024..=65535));
    }

    #[test]
    fn test_parse_port_range_same_port() {
        assert_eq!(parse_port_range("8080-8080"), Ok(8080..=8080));
    }

    #[test]
    fn test_parse_port_range_invalid() {
        assert_eq!(parse_port_range(""), Err(Errno::EINVAL));
        assert_eq!(parse_port_range("abc"), Err(Errno::EINVAL));
        assert_eq!(parse_port_range("-1"), Err(Errno::EINVAL));
        assert_eq!(parse_port_range("65536"), Err(Errno::EINVAL));
        assert_eq!(parse_port_range("80-abc"), Err(Errno::EINVAL));
        assert_eq!(parse_port_range("abc-80"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_fs_cmd_allow_plus() {
        let cmd = parse_fs_cmd("allow/fs+ext4").unwrap();
        assert_eq!(cmd.action, Action::Allow);
        assert_eq!(cmd.op, '+');
        assert_eq!(cmd.fs_type, "ext4");
    }

    #[test]
    fn test_parse_fs_cmd_deny_minus() {
        let cmd = parse_fs_cmd("deny/fs-tmpfs").unwrap();
        assert_eq!(cmd.action, Action::Deny);
        assert_eq!(cmd.op, '-');
        assert_eq!(cmd.fs_type, "tmpfs");
    }

    #[test]
    fn test_parse_fs_cmd_filter_caret() {
        let cmd = parse_fs_cmd("filter/fs^btrfs").unwrap();
        assert_eq!(cmd.action, Action::Filter);
        assert_eq!(cmd.op, '^');
        assert_eq!(cmd.fs_type, "btrfs");
    }

    #[test]
    fn test_parse_fs_cmd_all_actions() {
        let actions = [
            ("allow", Action::Allow),
            ("deny", Action::Deny),
            ("filter", Action::Filter),
            ("warn", Action::Warn),
            ("stop", Action::Stop),
            ("abort", Action::Abort),
            ("kill", Action::Kill),
            ("panic", Action::Panic),
            ("exit", Action::Exit),
        ];
        for (act_str, act_enum) in actions {
            let input = format!("{}/fs+xfs", act_str);
            let cmd = parse_fs_cmd(&input).unwrap();
            assert_eq!(cmd.action, act_enum);
        }
    }

    #[test]
    fn test_parse_fs_cmd_invalid_action() {
        assert_eq!(parse_fs_cmd("block/fs+ext4"), Err(Errno::EINVAL));
        assert_eq!(parse_fs_cmd("reject/fs-tmpfs"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_fs_cmd_missing_fs_type() {
        assert_eq!(parse_fs_cmd("allow/fs+"), Err(Errno::EINVAL));
        assert_eq!(parse_fs_cmd("deny/fs-"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_fs_cmd_invalid_operator() {
        assert_eq!(parse_fs_cmd("allow/fs*ext4"), Err(Errno::EINVAL));
        assert_eq!(parse_fs_cmd("allow/fs=tmpfs"), Err(Errno::EINVAL));
    }

    #[test]
    fn test_parse_fs_cmd_missing_prefix() {
        assert_eq!(parse_fs_cmd("allow+ext4"), Err(Errno::EINVAL));
        assert_eq!(parse_fs_cmd("fs+ext4"), Err(Errno::EINVAL));
    }
}
