parlov-analysis 0.7.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Signal extractors: pure functions that observe differentials and produce typed signals.
//!
//! Each extractor takes a `&DifferentialSet` and returns `Vec<Signal>`. Extractors are
//! independent and composable — the analyzer runs all extractors unconditionally on every
//! `DifferentialSet`.

/// Body content and `Content-Type` differential extraction.
pub mod body;
/// Notable response header differential extraction.
pub mod header;
/// Metadata leak signals (`Content-Range`, `ETag`).
pub mod metadata;
/// Status code differential extraction.
pub mod status_code;

#[cfg(test)]
pub(crate) mod tests {
    use bytes::Bytes;
    use http::{HeaderMap, StatusCode};
    use parlov_core::{
        always_applicable, DifferentialSet, NormativeStrength, OracleClass, ProbeDefinition,
        ProbeExchange, ResponseSurface, SignalSurface, Technique, Vector,
    };

    /// Builds a `ProbeExchange` with the given status and empty headers/body.
    pub fn fake_exchange(status: u16) -> ProbeExchange {
        ProbeExchange {
            request: ProbeDefinition {
                url: "http://test/resource/1".to_string(),
                method: http::Method::GET,
                headers: HeaderMap::new(),
                body: None,
            },
            response: ResponseSurface {
                status: StatusCode::from_u16(status).expect("valid status"),
                headers: HeaderMap::new(),
                body: Bytes::new(),
                timing_ns: 0,
            },
        }
    }

    /// Builds a `ProbeExchange` with custom headers on the response.
    pub fn fake_exchange_with_headers(status: u16, headers: HeaderMap) -> ProbeExchange {
        ProbeExchange {
            request: ProbeDefinition {
                url: "http://test/resource/1".to_string(),
                method: http::Method::GET,
                headers: HeaderMap::new(),
                body: None,
            },
            response: ResponseSurface {
                status: StatusCode::from_u16(status).expect("valid status"),
                headers,
                body: Bytes::new(),
                timing_ns: 0,
            },
        }
    }

    /// Default `Technique` for status-code-diff tests.
    pub fn status_code_diff_technique() -> Technique {
        Technique {
            id: "test-status-diff",
            name: "Test status code diff",
            oracle_class: OracleClass::Existence,
            vector: Vector::StatusCodeDiff,
            strength: NormativeStrength::Must,
            normalization_weight: Some(0.2),
            inverted_signal_weight: None,
            method_relevant: false,
            parser_relevant: false,
            applicability: always_applicable,
            contradiction_surface: SignalSurface::Status,
        }
    }

    /// Builds a single-sample `DifferentialSet` from baseline and probe status codes.
    pub fn single_diff_set(baseline_status: u16, probe_status: u16) -> DifferentialSet {
        DifferentialSet {
            baseline: vec![fake_exchange(baseline_status)],
            probe: vec![fake_exchange(probe_status)],
            canonical: None,
            technique: status_code_diff_technique(),
        }
    }

    /// Builds a multi-sample `DifferentialSet` from status code slices.
    pub fn diff_set_with_statuses(baseline: &[u16], probe: &[u16]) -> DifferentialSet {
        DifferentialSet {
            baseline: baseline.iter().map(|&s| fake_exchange(s)).collect(),
            probe: probe.iter().map(|&s| fake_exchange(s)).collect(),
            canonical: None,
            technique: status_code_diff_technique(),
        }
    }

    /// Builds a single-sample `DifferentialSet` with custom headers on both sides.
    pub fn single_diff_set_with_headers(
        baseline_status: u16,
        probe_status: u16,
        baseline_headers: HeaderMap,
        probe_headers: HeaderMap,
    ) -> DifferentialSet {
        DifferentialSet {
            baseline: vec![fake_exchange_with_headers(
                baseline_status,
                baseline_headers,
            )],
            probe: vec![fake_exchange_with_headers(probe_status, probe_headers)],
            canonical: None,
            technique: status_code_diff_technique(),
        }
    }

    /// Builds a single-sample `DifferentialSet` with custom headers on baseline only.
    pub fn single_diff_set_with_baseline_headers(
        baseline_status: u16,
        probe_status: u16,
        baseline_headers: HeaderMap,
    ) -> DifferentialSet {
        DifferentialSet {
            baseline: vec![fake_exchange_with_headers(
                baseline_status,
                baseline_headers,
            )],
            probe: vec![fake_exchange(probe_status)],
            canonical: None,
            technique: status_code_diff_technique(),
        }
    }

    /// Builds a `ProbeExchange` with custom headers and body.
    pub fn fake_exchange_with_body(status: u16, body: &[u8]) -> ProbeExchange {
        ProbeExchange {
            request: ProbeDefinition {
                url: "http://test/resource/1".to_string(),
                method: http::Method::GET,
                headers: HeaderMap::new(),
                body: None,
            },
            response: ResponseSurface {
                status: StatusCode::from_u16(status).expect("valid status"),
                headers: HeaderMap::new(),
                body: Bytes::from(body.to_vec()),
                timing_ns: 0,
            },
        }
    }

    /// Builds a `DifferentialSet` with custom bodies on both sides.
    pub fn single_diff_set_with_bodies(
        baseline_status: u16,
        probe_status: u16,
        baseline_body: &[u8],
        probe_body: &[u8],
    ) -> DifferentialSet {
        DifferentialSet {
            baseline: vec![fake_exchange_with_body(baseline_status, baseline_body)],
            probe: vec![fake_exchange_with_body(probe_status, probe_body)],
            canonical: None,
            technique: status_code_diff_technique(),
        }
    }

    /// Builds a `ProbeExchange` with custom request URL, status, and body.
    pub fn fake_exchange_with_url_and_body(url: &str, status: u16, body: &[u8]) -> ProbeExchange {
        ProbeExchange {
            request: ProbeDefinition {
                url: url.to_string(),
                method: http::Method::GET,
                headers: HeaderMap::new(),
                body: None,
            },
            response: ResponseSurface {
                status: StatusCode::from_u16(status).expect("valid status"),
                headers: HeaderMap::new(),
                body: Bytes::from(body.to_vec()),
                timing_ns: 0,
            },
        }
    }

    /// Builds a `DifferentialSet` with per-side request URLs and response bodies.
    ///
    /// Used for tests that exercise URL-aware logic such as input-reflection detection.
    pub fn single_diff_set_with_urls_and_bodies(
        baseline_url: &str,
        probe_url: &str,
        baseline_status: u16,
        probe_status: u16,
        baseline_body: &[u8],
        probe_body: &[u8],
    ) -> DifferentialSet {
        DifferentialSet {
            baseline: vec![fake_exchange_with_url_and_body(
                baseline_url,
                baseline_status,
                baseline_body,
            )],
            probe: vec![fake_exchange_with_url_and_body(
                probe_url,
                probe_status,
                probe_body,
            )],
            canonical: None,
            technique: status_code_diff_technique(),
        }
    }
}