parlov_analysis/existence/
analyzer.rs1use parlov_core::{
4 DifferentialSet, OracleClass, OracleResult, OracleVerdict, ProbeExchange, StrategyOutcome,
5 Vector,
6};
7
8use crate::aggregation::modifiers::compute_modifiers;
9use crate::signals;
10use crate::{Analyzer, SampleDecision};
11
12use super::classifier::classify;
13
14#[derive(Clone, Copy)]
16enum AnalysisPath {
17 NotFired,
19 Unstable,
21 SameStatus,
23 FullyScored,
25}
26
27pub struct ExistenceAnalyzer;
33
34impl Analyzer for ExistenceAnalyzer {
35 fn evaluate(&self, data: &DifferentialSet) -> SampleDecision {
36 let Some(b0_exchange) = data.baseline.first() else {
37 return SampleDecision::NeedMore;
38 };
39 let Some(p0_exchange) = data.probe.first() else {
40 return SampleDecision::NeedMore;
41 };
42 let b0 = b0_exchange.response.status;
43 let p0 = p0_exchange.response.status;
44
45 if b0 == p0 {
46 let result = build_result(data);
47 let outcome =
48 classify_outcome(&result, AnalysisPath::SameStatus, &data.technique, data);
49 return SampleDecision::Complete(Box::new(result), outcome);
50 }
51
52 if data.baseline.len() < 3 {
53 return SampleDecision::NeedMore;
54 }
55
56 let stable = is_consistent(&data.baseline) && is_consistent(&data.probe);
57 if stable {
58 if is_relevant_differential(data) {
59 let result = build_result(data);
60 let outcome =
61 classify_outcome(&result, AnalysisPath::FullyScored, &data.technique, data);
62 SampleDecision::Complete(Box::new(result), outcome)
63 } else {
64 let result = not_fired_result(data);
65 let outcome =
66 classify_outcome(&result, AnalysisPath::NotFired, &data.technique, data);
67 SampleDecision::Complete(Box::new(result), outcome)
68 }
69 } else {
70 let result = unstable_result(data);
71 let outcome = classify_outcome(&result, AnalysisPath::Unstable, &data.technique, data);
72 SampleDecision::Complete(Box::new(result), outcome)
73 }
74 }
75
76 fn oracle_class(&self) -> OracleClass {
77 OracleClass::Existence
78 }
79}
80
81fn classify_outcome(
83 result: &OracleResult,
84 path: AnalysisPath,
85 technique: &parlov_core::Technique,
86 differential: &DifferentialSet,
87) -> StrategyOutcome {
88 match path {
89 AnalysisPath::NotFired | AnalysisPath::Unstable => {
90 StrategyOutcome::NoSignal(result.clone())
91 }
92 AnalysisPath::SameStatus => same_status_outcome(result, technique, differential),
93 AnalysisPath::FullyScored => match result.verdict {
94 OracleVerdict::Confirmed | OracleVerdict::Likely => {
95 StrategyOutcome::Positive(result.clone())
96 }
97 OracleVerdict::NotPresent | OracleVerdict::Inconclusive => {
98 StrategyOutcome::NoSignal(result.clone())
99 }
100 },
101 }
102}
103
104fn same_status_outcome(
113 result: &OracleResult,
114 technique: &parlov_core::Technique,
115 differential: &DifferentialSet,
116) -> StrategyOutcome {
117 let Some(base_weight) = technique.normalization_weight else {
118 return StrategyOutcome::NoSignal(result.clone());
119 };
120 debug_assert!(
121 base_weight > 0.0,
122 "normalization_weight must be positive; got {base_weight} for {}",
123 technique.id
124 );
125 let mr = compute_modifiers(technique, differential);
126 if mr.is_blocked() {
127 let reason = mr.block_reason.map_or("modifier blocked", |r| r.as_str());
128 return StrategyOutcome::Inapplicable(std::borrow::Cow::Borrowed(reason));
129 }
130 #[allow(clippy::cast_possible_truncation)]
131 let effective_weight = base_weight * mr.modifiers.total() as f32;
132 debug_assert!(
133 effective_weight > 0.0,
134 "effective weight must be positive after modifiers"
135 );
136 StrategyOutcome::Contradictory(result.clone(), effective_weight)
137}
138
139fn build_result(data: &DifferentialSet) -> OracleResult {
140 let b0 = data.baseline[0].response.status;
141 let p0 = data.probe[0].response.status;
142
143 let signals = extract_all_signals(data);
144 classify(b0, p0, signals, &data.technique)
145}
146
147fn extract_all_signals(data: &DifferentialSet) -> Vec<parlov_core::Signal> {
148 let mut out = Vec::new();
149 signals::status_code::extract_into(data, &mut out);
150 signals::header::extract_into(data, &mut out);
151 signals::metadata::extract_into(data, &mut out);
152 signals::body::extract_into(data, &mut out);
153 out
154}
155
156fn is_consistent(exchanges: &[ProbeExchange]) -> bool {
157 exchanges
158 .iter()
159 .all(|e| e.response.status == exchanges[0].response.status)
160}
161
162fn unstable_result(data: &DifferentialSet) -> OracleResult {
163 let baseline_stable = is_consistent(&data.baseline);
164 let probe_stable = is_consistent(&data.probe);
165
166 let which = match (baseline_stable, probe_stable) {
167 (false, false) => "baseline and probe sides",
168 (false, true) => "baseline side",
169 (true, false) => "probe side",
170 (true, true) => unreachable!("unstable_result called when both sides are stable"),
173 };
174
175 OracleResult {
176 class: OracleClass::Existence,
177 verdict: OracleVerdict::NotPresent,
178 severity: None,
179 confidence: 0,
180 impact_class: None,
181 reasons: vec![],
182 signals: vec![parlov_core::Signal {
183 kind: parlov_core::SignalKind::StatusCodeDiff,
184 evidence: format!("unstable: {which}"),
185 rfc_basis: None,
186 }],
187 technique_id: Some(data.technique.id.to_string()),
188 vector: Some(data.technique.vector),
189 normative_strength: Some(data.technique.strength),
190 label: None,
191 leaks: None,
192 rfc_basis: None,
193 }
194}
195
196fn is_relevant_differential(data: &DifferentialSet) -> bool {
203 let b0 = data.baseline[0].response.status;
204 let p0 = data.probe[0].response.status;
205
206 match data.technique.vector {
207 Vector::RedirectDiff => b0.is_redirection() || p0.is_redirection(),
208 Vector::StatusCodeDiff | Vector::CacheProbing | Vector::ErrorMessageGranularity => true,
210 }
211}
212
213fn not_fired_result(data: &DifferentialSet) -> OracleResult {
219 OracleResult {
220 class: OracleClass::Existence,
221 verdict: OracleVerdict::NotPresent,
222 severity: None,
223 confidence: 0,
224 impact_class: None,
225 reasons: vec![],
226 signals: vec![parlov_core::Signal {
227 kind: parlov_core::SignalKind::StatusCodeDiff,
228 evidence: "technique did not fire: no 3xx status in differential".to_owned(),
229 rfc_basis: None,
230 }],
231 technique_id: Some(data.technique.id.to_string()),
232 vector: Some(data.technique.vector),
233 normative_strength: Some(data.technique.strength),
234 label: None,
235 leaks: None,
236 rfc_basis: None,
237 }
238}
239
240#[cfg(test)]
241#[path = "analyzer_tests.rs"]
242mod tests;