use bytes::Bytes;
use http::HeaderMap;
use parlov_core::{DifferentialSet, ProbeExchange, SignalSurface, Technique};
const BODY_SURFACE_MISMATCH_THRESHOLD: f64 = 0.10;
const HEADER_SURFACE_MISMATCH_THRESHOLD: f64 = 0.50;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SurfaceDecision {
Reached(f64),
Blocked,
}
impl SurfaceDecision {
#[must_use]
pub fn confidence(self) -> f64 {
match self {
Self::Reached(c) => c,
Self::Blocked => 0.0,
}
}
}
#[must_use]
pub fn surface_relevance(technique: &Technique, differential: &DifferentialSet) -> SurfaceDecision {
let Some((b, p)) = first_pair(differential) else {
return SurfaceDecision::Reached(1.0);
};
match technique.contradiction_surface {
SignalSurface::Status => {
if body_diff_ratio(&b.response.body, &p.response.body) > BODY_SURFACE_MISMATCH_THRESHOLD
|| header_diff_ratio(&b.response.headers, &p.response.headers)
> HEADER_SURFACE_MISMATCH_THRESHOLD
{
SurfaceDecision::Blocked
} else {
SurfaceDecision::Reached(1.0)
}
}
SignalSurface::Body
| SignalSurface::Headers
| SignalSurface::Timing
| SignalSurface::Composite => SurfaceDecision::Reached(1.0),
}
}
fn first_pair(differential: &DifferentialSet) -> Option<(&ProbeExchange, &ProbeExchange)> {
let b = differential.baseline.first()?;
let p = differential.probe.first()?;
Some((b, p))
}
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn body_diff_ratio(a: &Bytes, b: &Bytes) -> f64 {
let max_len = a.len().max(b.len());
if max_len == 0 {
return 0.0;
}
let min_len = a.len().min(b.len());
let differing_positions = a.iter().zip(b.iter()).filter(|(x, y)| x != y).count();
let length_diff = max_len - min_len;
(differing_positions + length_diff) as f64 / max_len as f64
}
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn header_diff_ratio(a: &HeaderMap, b: &HeaderMap) -> f64 {
let mut all_keys: std::collections::HashSet<&http::HeaderName> =
std::collections::HashSet::new();
all_keys.extend(a.keys());
all_keys.extend(b.keys());
if all_keys.is_empty() {
return 0.0;
}
let diverged = all_keys
.iter()
.filter(|k| {
let av: Vec<&[u8]> = a
.get_all(**k)
.iter()
.map(http::HeaderValue::as_bytes)
.collect();
let bv: Vec<&[u8]> = b
.get_all(**k)
.iter()
.map(http::HeaderValue::as_bytes)
.collect();
av != bv
})
.count();
diverged as f64 / all_keys.len() as f64
}
#[cfg(test)]
#[path = "surface_tests.rs"]
mod tests;