Skip to main content

featherbit/plugins/util/
headers.rs

1//! Case-insensitive helpers for the context's header maps.
2//!
3//! Context headers are stored as a plain `HashMap<String, Vec<String>>`. The
4//! keys are *usually* lowercase — hyper normalises them on the way in — but a
5//! Lua script, another plugin, or a sandbox-seeded response can introduce a
6//! mixed-case name. Since HTTP header names are case-insensitive (RFC 9110
7//! §5.1), removals and lookups must be too, or they silently miss.
8
9use std::collections::HashMap;
10
11/// Removes every entry whose name matches `name` case-insensitively.
12///
13/// Returns `true` if anything was removed. A plain `map.remove(&name.to_lowercase())`
14/// only works when the stored key is already lowercase; this handles any case.
15pub fn remove_ci(map: &mut HashMap<String, Vec<String>>, name: &str) -> bool {
16    let target = name.to_ascii_lowercase();
17    let matches: Vec<String> = map
18        .keys()
19        .filter(|k| k.eq_ignore_ascii_case(&target))
20        .cloned()
21        .collect();
22    let removed = !matches.is_empty();
23    for k in matches {
24        map.remove(&k);
25    }
26    removed
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    fn map(pairs: &[(&str, &str)]) -> HashMap<String, Vec<String>> {
34        pairs
35            .iter()
36            .map(|(k, v)| (k.to_string(), vec![v.to_string()]))
37            .collect()
38    }
39
40    #[test]
41    fn test_removes_regardless_of_stored_case() {
42        let mut m = map(&[("X-Powered-By", "php"), ("content-type", "text/html")]);
43        assert!(remove_ci(&mut m, "x-powered-by"));
44        assert!(!m.contains_key("X-Powered-By"));
45        assert!(m.contains_key("content-type"));
46    }
47
48    #[test]
49    fn test_removes_regardless_of_query_case() {
50        let mut m = map(&[("x-trace", "1")]);
51        assert!(remove_ci(&mut m, "X-Trace"));
52        assert!(m.is_empty());
53    }
54
55    #[test]
56    fn test_reports_false_when_absent() {
57        let mut m = map(&[("a", "1")]);
58        assert!(!remove_ci(&mut m, "b"));
59        assert_eq!(m.len(), 1);
60    }
61}