hpx_emulation/fingerprint/
composer.rs1use std::collections::HashSet;
7
8use hpx::header::{HeaderMap, HeaderName, HeaderValue};
9
10pub struct HeaderComposer {
26 fingerprint_headers: Vec<(String, String)>,
27 custom_headers: Vec<(String, String)>,
28}
29
30impl HeaderComposer {
31 pub fn new() -> Self {
33 Self {
34 fingerprint_headers: Vec::new(),
35 custom_headers: Vec::new(),
36 }
37 }
38
39 pub fn with_fingerprint_headers<I, K, V>(mut self, headers: I) -> Self
41 where
42 I: IntoIterator<Item = (K, V)>,
43 K: Into<String>,
44 V: Into<String>,
45 {
46 self.fingerprint_headers = headers
47 .into_iter()
48 .map(|(k, v)| (k.into(), v.into()))
49 .collect();
50 self
51 }
52
53 pub fn with_custom_headers<I, K, V>(mut self, headers: I) -> Self
55 where
56 I: IntoIterator<Item = (K, V)>,
57 K: Into<String>,
58 V: Into<String>,
59 {
60 let new_headers: Vec<(String, String)> = headers
61 .into_iter()
62 .map(|(k, v)| (k.into(), v.into()))
63 .collect();
64 self.custom_headers = new_headers.into_iter().chain(self.custom_headers).collect();
66 self
67 }
68
69 pub fn compose(self) -> Result<HeaderMap, ComposeError> {
73 let mut headers = HeaderMap::new();
74 let mut seen: HashSet<String> = HashSet::new();
75
76 for (name, value) in self
78 .custom_headers
79 .iter()
80 .chain(self.fingerprint_headers.iter())
81 {
82 let lower_name = name.to_lowercase();
83 if seen.contains(&lower_name) {
84 continue;
85 }
86 if value.is_empty() {
87 continue;
88 }
89 seen.insert(lower_name);
90
91 let header_name = name
92 .parse::<HeaderName>()
93 .map_err(|_| ComposeError::InvalidHeaderName(name.clone()))?;
94 let header_value = value
95 .parse::<HeaderValue>()
96 .map_err(|_| ComposeError::InvalidHeaderValue(value.clone()))?;
97 headers.insert(header_name, header_value);
98 }
99
100 Ok(headers)
101 }
102}
103
104impl Default for HeaderComposer {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110#[derive(Debug, Clone)]
112pub enum ComposeError {
113 InvalidHeaderName(String),
115 InvalidHeaderValue(String),
117}
118
119impl std::fmt::Display for ComposeError {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 ComposeError::InvalidHeaderName(name) => write!(f, "Invalid header name: {name}"),
123 ComposeError::InvalidHeaderValue(value) => {
124 write!(f, "Invalid header value: {value}")
125 }
126 }
127 }
128}
129
130impl std::error::Error for ComposeError {}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_custom_overrides_fingerprint() {
138 let composer = HeaderComposer::new()
139 .with_fingerprint_headers(vec![
140 ("user-agent", "fingerprint-ua"),
141 ("accept", "text/html"),
142 ])
143 .with_custom_headers(vec![("user-agent", "custom-ua")]);
144
145 let headers = composer.compose().unwrap();
146 assert_eq!(headers.get("user-agent").unwrap(), "custom-ua");
147 assert_eq!(headers.get("accept").unwrap(), "text/html");
148 }
149
150 #[test]
151 fn test_case_insensitive_dedup() {
152 let composer = HeaderComposer::new()
153 .with_fingerprint_headers(vec![("User-Agent", "fp-ua")])
154 .with_custom_headers(vec![("user-agent", "custom-ua")]);
155
156 let headers = composer.compose().unwrap();
157 assert_eq!(headers.get("user-agent").unwrap(), "custom-ua");
158 }
159
160 #[test]
161 fn test_empty_value_skipped() {
162 let composer = HeaderComposer::new()
163 .with_fingerprint_headers(vec![("x-empty", ""), ("x-valid", "value")]);
164
165 let headers = composer.compose().unwrap();
166 assert!(headers.get("x-empty").is_none());
167 assert_eq!(headers.get("x-valid").unwrap(), "value");
168 }
169}