nmaprs 0.1.6

High-performance parallel network scanner with nmap-compatible CLI surface
Documentation
//! Parse Nmap-style `--scanflags` (TCP flag names).

use anyhow::{bail, Result};
use pnet::packet::tcp::TcpFlags;

/// Parse `--scanflags` into a TCP flags byte (OR of [`TcpFlags`] constants).
///
/// Accepts whitespace-, comma-, or pipe-separated tokens, and glued names (e.g. `SYNACK`).
pub fn parse_scanflags(s: &str) -> Result<u8> {
    let s = s.trim();
    if s.is_empty() {
        bail!("--scanflags must not be empty");
    }
    /// Longest names first so `SYN` is consumed before a trailing `ACK` in `SYNACK`.
    const KEYWORDS: &[(&str, u8)] = &[
        ("SYN", TcpFlags::SYN),
        ("ACK", TcpFlags::ACK),
        ("FIN", TcpFlags::FIN),
        ("RST", TcpFlags::RST),
        ("PSH", TcpFlags::PSH),
        ("URG", TcpFlags::URG),
        ("ECE", TcpFlags::ECE),
        ("CWR", TcpFlags::CWR),
    ];
    let normalized = s.replace(['|', ','], " ");
    let mut flags = 0u8;
    for word in normalized.split_whitespace() {
        if word.is_empty() {
            continue;
        }
        let word_u = word.to_ascii_uppercase();
        let mut sub = word_u.as_str();
        while !sub.is_empty() {
            let mut matched = false;
            for (name, bit) in KEYWORDS.iter() {
                if sub.starts_with(name) {
                    flags |= bit;
                    sub = &sub[name.len()..];
                    matched = true;
                    break;
                }
            }
            if !matched {
                bail!("unknown --scanflags token in '{word}' (unparsed suffix '{sub}')");
            }
        }
    }
    Ok(flags)
}

#[cfg(test)]
mod tests {
    use super::parse_scanflags;
    use pnet::packet::tcp::TcpFlags;

    #[test]
    fn parses_spaced_and_glued() {
        let a = parse_scanflags("SYN ACK").unwrap();
        let b = parse_scanflags("SYNACK").unwrap();
        assert_eq!(a, b);
        assert_eq!(a, TcpFlags::SYN | TcpFlags::ACK);
    }

    #[test]
    fn parses_pipe() {
        let f = parse_scanflags("URG|PSH").unwrap();
        assert_eq!(f, TcpFlags::URG | TcpFlags::PSH);
    }

    #[test]
    fn empty_errors() {
        assert!(parse_scanflags("").is_err());
        assert!(parse_scanflags("   ").is_err());
    }

    #[test]
    fn unknown_token_errors() {
        let e = parse_scanflags("SYN,QXYZ").unwrap_err();
        let s = e.to_string();
        assert!(
            s.contains("unknown") || s.contains("scanflags"),
            "unexpected error: {s}"
        );
    }

    #[test]
    fn parses_rst_syn_ece_cwr() {
        let f = parse_scanflags("RST,SYN,ECE,CWR").unwrap();
        assert_eq!(
            f,
            TcpFlags::RST | TcpFlags::SYN | TcpFlags::ECE | TcpFlags::CWR
        );
    }

    #[test]
    fn parses_comma_separated_mixed_case() {
        let f = parse_scanflags("syn,fin").unwrap();
        assert_eq!(f, TcpFlags::SYN | TcpFlags::FIN);
    }

    #[test]
    fn parses_fin_only() {
        assert_eq!(parse_scanflags("FIN").unwrap(), TcpFlags::FIN);
    }

    #[test]
    fn parses_all_common_flags() {
        let f = parse_scanflags("SYN,ACK,FIN,RST,PSH,URG,ECE,CWR").unwrap();
        assert_eq!(
            f,
            TcpFlags::SYN
                | TcpFlags::ACK
                | TcpFlags::FIN
                | TcpFlags::RST
                | TcpFlags::PSH
                | TcpFlags::URG
                | TcpFlags::ECE
                | TcpFlags::CWR
        );
    }

    #[test]
    fn parses_whitespace_and_comma_mix() {
        let f = parse_scanflags(" SYN , ACK ").unwrap();
        assert_eq!(f, TcpFlags::SYN | TcpFlags::ACK);
    }

    #[test]
    fn parses_rst_syn_scan_combo() {
        assert_eq!(
            parse_scanflags("RSTSYN").unwrap(),
            TcpFlags::RST | TcpFlags::SYN
        );
    }

    #[test]
    fn duplicate_flag_tokens_or_bits() {
        let f = parse_scanflags("SYN SYN").unwrap();
        assert_eq!(f, TcpFlags::SYN);
    }

    #[test]
    fn null_scan_flags_empty_is_error() {
        assert!(parse_scanflags("NULL").is_err());
    }

    #[test]
    fn parses_psh_only() {
        assert_eq!(parse_scanflags("PSH").unwrap(), TcpFlags::PSH);
    }

    #[test]
    fn parses_urg_fin_combo() {
        assert_eq!(
            parse_scanflags("URG FIN").unwrap(),
            TcpFlags::URG | TcpFlags::FIN
        );
    }

    #[test]
    fn parses_lowercase_glued_synack() {
        assert_eq!(
            parse_scanflags("synack").unwrap(),
            TcpFlags::SYN | TcpFlags::ACK
        );
    }

    #[test]
    fn parses_ece_only() {
        assert_eq!(parse_scanflags("ECE").unwrap(), TcpFlags::ECE);
    }

    #[test]
    fn parses_cwr_only() {
        assert_eq!(parse_scanflags("CWR").unwrap(), TcpFlags::CWR);
    }

    #[test]
    fn parses_syn_fin_rst_combo() {
        assert_eq!(
            parse_scanflags("SYN,FIN,RST").unwrap(),
            TcpFlags::SYN | TcpFlags::FIN | TcpFlags::RST
        );
    }

    #[test]
    fn parses_mixed_case_fin_ack() {
        assert_eq!(
            parse_scanflags("Fin,ack").unwrap(),
            TcpFlags::FIN | TcpFlags::ACK
        );
    }

    #[test]
    fn parses_finack_glued() {
        assert_eq!(
            parse_scanflags("FINACK").unwrap(),
            TcpFlags::FIN | TcpFlags::ACK
        );
    }

    #[test]
    fn parses_pshurgrst_glued() {
        assert_eq!(
            parse_scanflags("PSHURGRST").unwrap(),
            TcpFlags::PSH | TcpFlags::URG | TcpFlags::RST
        );
    }

    #[test]
    fn parses_pipe_separated_syn_fin() {
        assert_eq!(
            parse_scanflags("SYN|FIN").unwrap(),
            TcpFlags::SYN | TcpFlags::FIN
        );
    }

    #[test]
    fn parses_ack_rst_combo() {
        assert_eq!(
            parse_scanflags("ACK RST").unwrap(),
            TcpFlags::ACK | TcpFlags::RST
        );
    }

    #[test]
    fn parses_tab_separated_flags() {
        assert_eq!(
            parse_scanflags("SYN\tACK").unwrap(),
            TcpFlags::SYN | TcpFlags::ACK
        );
    }

    #[test]
    fn null_keyword_errors() {
        assert!(parse_scanflags("NULL").is_err());
    }

    #[test]
    fn xmas_style_fin_psh_urg() {
        assert_eq!(
            parse_scanflags("FIN,PSH,URG").unwrap(),
            TcpFlags::FIN | TcpFlags::PSH | TcpFlags::URG
        );
    }
}