leyline/audit/compare/
headers.rs1use serde::Deserialize;
2
3use super::FieldOutcome;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6#[non_exhaustive]
7pub struct HeaderOutcome {
8 pub name: String,
9 pub outcome: FieldOutcome,
10}
11
12#[derive(Deserialize, Default)]
13pub(super) struct EchoHttp1 {
14 #[serde(default)]
15 headers: Vec<String>,
16}
17
18#[derive(Deserialize, Default)]
19pub(super) struct EchoFrame {
20 #[serde(default)]
21 headers: Vec<String>,
22}
23
24pub(super) fn echoed(http2: &[EchoFrame], http1: &EchoHttp1) -> Vec<(String, String)> {
25 http2
26 .iter()
27 .flat_map(|frame| frame.headers.iter())
28 .chain(http1.headers.iter())
29 .filter_map(|line| split_line(line))
30 .collect()
31}
32
33fn split_line(line: &str) -> Option<(String, String)> {
34 if line.starts_with(':') {
35 return None;
36 }
37 let (name, value) = line.split_once(':')?;
38 let name = name.trim();
39 (!name.is_empty()).then(|| (name.to_ascii_lowercase(), value.trim().to_owned()))
40}
41
42pub(super) fn compare(
43 sent: &[(String, String)],
44 observed: &[(String, String)],
45) -> Vec<HeaderOutcome> {
46 let mut names: Vec<String> = Vec::new();
47 for name in field_names(sent) {
48 if !names.contains(&name) {
49 names.push(name);
50 }
51 }
52 names
53 .into_iter()
54 .map(|name| {
55 let outcome = header_outcome(&name, sent, observed);
56 HeaderOutcome { name, outcome }
57 })
58 .collect()
59}
60
61pub(super) fn order(sent: &[(String, String)], observed: &[(String, String)]) -> FieldOutcome {
62 if observed.is_empty() {
63 return FieldOutcome::NotReported;
64 }
65 let expected = shared_names(sent, observed).join(",");
66 let seen = shared_names(observed, sent).join(",");
67 if expected == seen {
68 FieldOutcome::Match
69 } else {
70 FieldOutcome::Mismatch {
71 expected,
72 observed: seen,
73 }
74 }
75}
76
77fn field_names(list: &[(String, String)]) -> impl Iterator<Item = String> + '_ {
78 list.iter()
79 .filter(|(name, _)| !name.starts_with(':'))
80 .map(|(name, _)| name.to_ascii_lowercase())
81}
82
83fn shared_names(from: &[(String, String)], other: &[(String, String)]) -> Vec<String> {
84 field_names(from)
85 .filter(|name| {
86 other
87 .iter()
88 .any(|(held, _)| held.eq_ignore_ascii_case(name))
89 })
90 .collect()
91}
92
93fn values<'a>(name: &str, list: &'a [(String, String)]) -> Vec<&'a str> {
94 list.iter()
95 .filter(|(held, _)| held.eq_ignore_ascii_case(name))
96 .map(|(_, value)| value.trim())
97 .collect()
98}
99
100const COOKIE: &str = "cookie";
101
102fn joined_cookie(list: &[(String, String)]) -> Vec<&str> {
103 values(COOKIE, list)
104 .into_iter()
105 .flat_map(|value| value.split(';'))
106 .map(str::trim)
107 .filter(|pair| !pair.is_empty())
108 .collect()
109}
110
111fn header_outcome(
112 name: &str,
113 sent: &[(String, String)],
114 observed: &[(String, String)],
115) -> FieldOutcome {
116 if observed.is_empty() {
117 return FieldOutcome::NotReported;
118 }
119 let (expected, seen) = if name.eq_ignore_ascii_case(COOKIE) {
120 (joined_cookie(sent), joined_cookie(observed))
121 } else {
122 (values(name, sent), values(name, observed))
123 };
124 if seen.is_empty() {
125 return FieldOutcome::Absent {
126 expected: expected.join(", "),
127 };
128 }
129 if seen == expected {
130 return FieldOutcome::Match;
131 }
132 FieldOutcome::Mismatch {
133 expected: expected.join(", "),
134 observed: seen.join(", "),
135 }
136}