Skip to main content

hpx_emulation/fingerprint/
composer.rs

1//! Header composition with deduplication and priority.
2//!
3//! `HeaderComposer` builds HTTP header maps with proper deduplication:
4//! custom headers take priority over fingerprint defaults.
5
6use std::collections::HashSet;
7
8use hpx::header::{HeaderMap, HeaderName, HeaderValue};
9
10/// Composes HTTP headers with priority-based deduplication.
11///
12/// Custom headers (higher priority) override fingerprint headers (lower priority)
13/// when they share the same header name. Header names are compared case-insensitively.
14///
15/// # Example
16///
17/// ```ignore
18/// let composer = HeaderComposer::new()
19///     .with_fingerprint_headers(fp_headers)
20///     .with_custom_headers(vec![("user-agent", "custom-ua")]);
21///
22/// let headers = composer.compose();
23/// // The custom user-agent overrides the fingerprint one.
24/// ```
25pub struct HeaderComposer {
26    fingerprint_headers: Vec<(String, String)>,
27    custom_headers: Vec<(String, String)>,
28}
29
30impl HeaderComposer {
31    /// Creates a new empty `HeaderComposer`.
32    pub fn new() -> Self {
33        Self {
34            fingerprint_headers: Vec::new(),
35            custom_headers: Vec::new(),
36        }
37    }
38
39    /// Adds fingerprint-derived headers (lower priority).
40    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    /// Adds custom headers (higher priority, override fingerprint headers).
54    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        // Prepend new headers so they have higher priority
65        self.custom_headers = new_headers.into_iter().chain(self.custom_headers).collect();
66        self
67    }
68
69    /// Composes the final `HeaderMap` with deduplication.
70    ///
71    /// Returns an error if any header name or value is invalid.
72    pub fn compose(self) -> Result<HeaderMap, ComposeError> {
73        let mut headers = HeaderMap::new();
74        let mut seen: HashSet<String> = HashSet::new();
75
76        // Custom headers first (higher priority)
77        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/// Error type for header composition.
111#[derive(Debug, Clone)]
112pub enum ComposeError {
113    /// Invalid header name.
114    InvalidHeaderName(String),
115    /// Invalid header value.
116    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}