parlov 0.8.0

HTTP oracle detection tool — systematic probing for RFC-compliant information leakage.
Documentation
//! CLI string parsers for the scan subcommand.
//!
//! Centralises all `--risk`, `--known-duplicate`, `--state-field`, and
//! `--alt-credential` parsing in one place. Previously split between
//! `scan_context.rs` and `vector_filter.rs`.

use http::HeaderMap;
use parlov_core::Error;
use parlov_elicit::{KnownDuplicate, RiskLevel, StateField};

use crate::util::parse_headers;

/// Parses a risk level string into a `RiskLevel`.
///
/// # Errors
///
/// Returns `Err` for any string other than `"safe"`, `"method-destructive"`,
/// or `"operation-destructive"`.
pub(crate) fn parse_risk(s: &str) -> Result<RiskLevel, Error> {
    match s {
        "safe" => Ok(RiskLevel::Safe),
        "method-destructive" => Ok(RiskLevel::MethodDestructive),
        "operation-destructive" => Ok(RiskLevel::OperationDestructive),
        other => Err(Error::Cli(format!(
            "invalid risk level '{other}'; expected safe | method-destructive | operation-destructive"
        ))),
    }
}

/// Parses a `"field=value"` string into a `KnownDuplicate`.
///
/// Splits on the first `=` only; the value may itself contain `=`.
///
/// # Errors
///
/// Returns `Err` when the input contains no `=` character.
pub(crate) fn parse_known_duplicate(s: &str) -> Result<KnownDuplicate, Error> {
    let (field, value) = s
        .split_once('=')
        .ok_or_else(|| Error::Cli(format!("known-duplicate must be 'field=value', got '{s}'")))?;
    Ok(KnownDuplicate {
        field: field.to_owned(),
        value: value.to_owned(),
    })
}

/// Parses a `"field=value"` string into a `StateField`.
///
/// Splits on the first `=` only; the value may itself contain `=`.
///
/// # Errors
///
/// Returns `Err` when the input contains no `=` character.
pub(crate) fn parse_state_field(s: &str) -> Result<StateField, Error> {
    let (field, value) = s
        .split_once('=')
        .ok_or_else(|| Error::Cli(format!("state-field must be 'field=value', got '{s}'")))?;
    Ok(StateField {
        field: field.to_owned(),
        value: value.to_owned(),
    })
}

/// Parses an alt-credential header string `"Name: Value"` into a `HeaderMap`.
///
/// # Errors
///
/// Returns `Err` for malformed header strings.
pub(crate) fn parse_alt_credential(s: &str) -> Result<HeaderMap, Error> {
    parse_headers(&[s.to_owned()])
}

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

    #[test]
    fn parse_risk_safe() {
        assert_eq!(parse_risk("safe").unwrap(), RiskLevel::Safe);
    }

    #[test]
    fn parse_risk_method_destructive() {
        assert_eq!(
            parse_risk("method-destructive").unwrap(),
            RiskLevel::MethodDestructive
        );
    }

    #[test]
    fn parse_risk_operation_destructive() {
        assert_eq!(
            parse_risk("operation-destructive").unwrap(),
            RiskLevel::OperationDestructive
        );
    }

    #[test]
    fn parse_risk_invalid_returns_err() {
        assert!(parse_risk("invalid").is_err());
    }

    #[test]
    fn parse_known_duplicate_splits_field_and_value() {
        let kd = parse_known_duplicate("email=alice@example.com").unwrap();
        assert_eq!(kd.field, "email");
        assert_eq!(kd.value, "alice@example.com");
    }

    #[test]
    fn parse_known_duplicate_splits_on_first_equals_only() {
        let kd = parse_known_duplicate("foo=bar=baz").unwrap();
        assert_eq!(kd.field, "foo");
        assert_eq!(kd.value, "bar=baz");
    }

    #[test]
    fn parse_known_duplicate_no_divider_returns_err() {
        assert!(parse_known_duplicate("nodivider").is_err());
    }

    #[test]
    fn parse_state_field_splits_correctly() {
        let sf = parse_state_field("status=invalid").unwrap();
        assert_eq!(sf.field, "status");
        assert_eq!(sf.value, "invalid");
    }
}