use std::collections::BTreeSet;
use parlov_core::{DifferentialSet, Signal, SignalKind};
const NOTABLE_HEADERS: &[&str] = &[
"etag",
"last-modified",
"content-range",
"accept-ranges",
"www-authenticate",
"allow",
];
pub fn extract(data: &DifferentialSet) -> Vec<Signal> {
let Some(baseline) = data.baseline.last() else {
return vec![];
};
let Some(probe) = data.probe.last() else {
return vec![];
};
let b_headers = &baseline.response.headers;
let p_headers = &probe.response.headers;
let names: BTreeSet<&str> = b_headers
.keys()
.chain(p_headers.keys())
.map(http::HeaderName::as_str)
.filter(|n| NOTABLE_HEADERS.contains(n))
.collect();
let mut signals = Vec::new();
for name in names {
let b_val = b_headers.get(name).and_then(|v| v.to_str().ok());
let p_val = p_headers.get(name).and_then(|v| v.to_str().ok());
match (b_val, p_val) {
(Some(_), None) => signals.push(Signal {
kind: SignalKind::HeaderPresence,
evidence: format!("{name} present in baseline, absent in probe"),
rfc_basis: None,
}),
(None, Some(_)) => signals.push(Signal {
kind: SignalKind::HeaderPresence,
evidence: format!("{name} absent in baseline, present in probe"),
rfc_basis: None,
}),
(Some(b), Some(p)) if b != p => signals.push(Signal {
kind: SignalKind::HeaderValue,
evidence: format!("{name}: \"{b}\" (baseline) vs \"{p}\" (probe)"),
rfc_basis: None,
}),
_ => {}
}
}
signals
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signals::tests::single_diff_set_with_headers;
use http::{HeaderMap, HeaderName, HeaderValue};
fn headers_with(pairs: &[(&str, &str)]) -> HeaderMap {
let mut map = HeaderMap::new();
for &(name, value) in pairs {
map.insert(
HeaderName::from_bytes(name.as_bytes()).expect("valid header name"),
HeaderValue::from_str(value).expect("valid header value"),
);
}
map
}
#[test]
fn etag_present_in_baseline_only() {
let b = headers_with(&[("etag", "\"abc\"")]);
let p = HeaderMap::new();
let ds = single_diff_set_with_headers(200, 404, b, p);
let signals = extract(&ds);
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].kind, SignalKind::HeaderPresence);
assert!(signals[0].evidence.contains("etag"));
assert!(signals[0].evidence.contains("baseline"));
}
#[test]
fn www_authenticate_present_in_probe_only() {
let b = HeaderMap::new();
let p = headers_with(&[("www-authenticate", "Bearer")]);
let ds = single_diff_set_with_headers(200, 401, b, p);
let signals = extract(&ds);
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].kind, SignalKind::HeaderPresence);
assert!(signals[0].evidence.contains("www-authenticate"));
assert!(signals[0].evidence.contains("probe"));
}
#[test]
fn allow_header_different_values() {
let b = headers_with(&[("allow", "GET, HEAD")]);
let p = headers_with(&[("allow", "GET, HEAD, POST")]);
let ds = single_diff_set_with_headers(405, 405, b, p);
let signals = extract(&ds);
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].kind, SignalKind::HeaderValue);
assert!(signals[0].evidence.contains("allow"));
}
#[test]
fn identical_notable_headers_produce_no_signal() {
let b = headers_with(&[("etag", "\"abc\"")]);
let p = headers_with(&[("etag", "\"abc\"")]);
let ds = single_diff_set_with_headers(200, 200, b, p);
assert!(extract(&ds).is_empty());
}
#[test]
fn non_notable_headers_ignored() {
let b = headers_with(&[("x-custom", "foo")]);
let p = headers_with(&[("x-custom", "bar")]);
let ds = single_diff_set_with_headers(200, 200, b, p);
assert!(extract(&ds).is_empty());
}
#[test]
fn empty_exchanges_produce_no_signals() {
let ds = crate::signals::tests::diff_set_with_statuses(&[], &[]);
assert!(extract(&ds).is_empty());
}
}