use parlov_core::{NormativeStrength, Signal, SignalKind};
use super::families::{header_family, signal_kind_family, SignalContribution, SignalFamily};
struct RawWeight {
confidence: f32,
impact: u8,
family: SignalFamily,
}
pub(crate) fn weight_signal(
signal: &Signal,
strength: NormativeStrength,
reproducibility: f32,
) -> SignalContribution {
let raw = raw_weight(signal);
let normative = normative_multiplier(strength);
let adjusted_confidence = raw.confidence * normative * reproducibility;
SignalContribution {
family: raw.family,
confidence: adjusted_confidence,
impact: raw.impact,
description: signal.evidence.clone(),
}
}
fn raw_weight(signal: &Signal) -> RawWeight {
match signal.kind {
SignalKind::HeaderPresence => header_presence_weight(signal),
SignalKind::MetadataLeak => metadata_leak_weight(signal),
SignalKind::HeaderValue => header_value_weight(signal),
_ => RawWeight {
confidence: 0.0,
impact: 0,
family: signal_kind_family(signal.kind),
},
}
}
fn header_presence_weight(signal: &Signal) -> RawWeight {
let evidence = signal.evidence.to_lowercase();
if evidence.contains("content-range") {
return RawWeight { confidence: 12.0, impact: 8, family: SignalFamily::Range };
}
if evidence.contains("etag") {
return RawWeight { confidence: 10.0, impact: 5, family: SignalFamily::CacheValidator };
}
if evidence.contains("last-modified") {
return RawWeight { confidence: 8.0, impact: 5, family: SignalFamily::CacheValidator };
}
if evidence.contains("accept-ranges") {
return RawWeight { confidence: 5.0, impact: 0, family: SignalFamily::Range };
}
if evidence.contains("www-authenticate") {
return RawWeight { confidence: 8.0, impact: 8, family: SignalFamily::Auth };
}
if evidence.contains("allow") {
return RawWeight { confidence: 6.0, impact: 5, family: SignalFamily::General };
}
RawWeight { confidence: 3.0, impact: 0, family: SignalFamily::General }
}
fn metadata_leak_weight(signal: &Signal) -> RawWeight {
let evidence = signal.evidence.to_lowercase();
let family = if evidence.contains("content-range") {
SignalFamily::Range
} else {
header_family_from_evidence(&evidence)
};
if evidence.contains("content-range") && evidence.contains("size") {
return RawWeight { confidence: 5.0, impact: 15, family };
}
if evidence.contains("etag") {
return RawWeight { confidence: 3.0, impact: 5, family };
}
RawWeight { confidence: 3.0, impact: 5, family }
}
fn header_value_weight(signal: &Signal) -> RawWeight {
let evidence = signal.evidence.to_lowercase();
let family = header_family_from_evidence(&evidence);
RawWeight { confidence: 4.0, impact: 3, family }
}
fn header_family_from_evidence(evidence: &str) -> SignalFamily {
const KNOWN_HEADERS: &[&str] = &[
"content-range",
"accept-ranges",
"etag",
"last-modified",
"www-authenticate",
"allow",
];
for name in KNOWN_HEADERS {
if evidence.contains(name) {
return header_family(name);
}
}
SignalFamily::General
}
fn normative_multiplier(strength: NormativeStrength) -> f32 {
match strength {
NormativeStrength::Must | NormativeStrength::MustNot => 1.0,
NormativeStrength::Should => 0.9,
NormativeStrength::May => 0.75,
}
}
#[cfg(test)]
mod tests {
use super::*;
use parlov_core::Signal;
fn signal(kind: SignalKind, evidence: &str) -> Signal {
Signal { kind, evidence: evidence.into(), rfc_basis: None }
}
#[test]
fn etag_presence_weight() {
let s = signal(SignalKind::HeaderPresence, "etag present in baseline");
let c = weight_signal(&s, NormativeStrength::Must, 1.0);
assert!((c.confidence - 10.0).abs() < 0.01);
assert_eq!(c.impact, 5);
assert_eq!(c.family, SignalFamily::CacheValidator);
}
#[test]
fn content_range_size_leak_weight() {
let s = signal(
SignalKind::MetadataLeak,
"Content-Range leaks total resource size: 1024 bytes",
);
let c = weight_signal(&s, NormativeStrength::Must, 1.0);
assert!((c.confidence - 5.0).abs() < 0.01);
assert_eq!(c.impact, 15);
assert_eq!(c.family, SignalFamily::Range);
}
#[test]
fn normative_should_reduces_confidence() {
let s = signal(SignalKind::HeaderPresence, "etag present in baseline");
let c = weight_signal(&s, NormativeStrength::Should, 1.0);
assert!((c.confidence - 9.0).abs() < 0.01);
}
#[test]
fn normative_may_reduces_confidence() {
let s = signal(SignalKind::HeaderPresence, "etag present in baseline");
let c = weight_signal(&s, NormativeStrength::May, 1.0);
assert!((c.confidence - 7.5).abs() < 0.01);
}
#[test]
fn reproducibility_reduces_confidence() {
let s = signal(SignalKind::HeaderPresence, "etag present in baseline");
let c = weight_signal(&s, NormativeStrength::Must, 0.7);
assert!((c.confidence - 7.0).abs() < 0.01);
assert_eq!(c.impact, 5);
}
#[test]
fn www_authenticate_weight() {
let s = signal(
SignalKind::HeaderPresence,
"www-authenticate present in baseline, absent in probe",
);
let c = weight_signal(&s, NormativeStrength::Must, 1.0);
assert!((c.confidence - 8.0).abs() < 0.01);
assert_eq!(c.impact, 8);
assert_eq!(c.family, SignalFamily::Auth);
}
}