1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Header {
7 pub name: String,
8 pub value: String,
9}
10
11#[must_use]
13pub fn normalize_header_name(input: &str) -> String {
14 input.trim().to_ascii_lowercase()
15}
16
17#[must_use]
19pub fn is_valid_header_name(input: &str) -> bool {
20 let trimmed = input.trim();
21 !trimmed.is_empty() && trimmed.bytes().all(is_token_byte)
22}
23
24#[must_use]
26pub fn parse_header_line(input: &str) -> Option<Header> {
27 let (name, value) = input.split_once(':')?;
28 if !is_valid_header_name(name) {
29 return None;
30 }
31
32 Some(Header {
33 name: normalize_header_name(name),
34 value: value.trim().to_string(),
35 })
36}
37
38#[must_use]
40pub fn parse_headers(input: &str) -> Vec<Header> {
41 input
42 .lines()
43 .filter_map(|line| parse_header_line(line.trim_end_matches('\r')))
44 .collect()
45}
46
47#[must_use]
49pub fn get_header(headers: &[Header], name: &str) -> Option<String> {
50 let normalized = normalize_header_name(name);
51
52 headers
53 .iter()
54 .find(|header| normalize_header_name(&header.name) == normalized)
55 .map(|header| header.value.clone())
56}
57
58#[must_use]
60pub fn has_header(headers: &[Header], name: &str) -> bool {
61 get_header(headers, name).is_some()
62}
63
64pub fn set_header(headers: &mut Vec<Header>, name: &str, value: &str) {
66 if !is_valid_header_name(name) {
67 return;
68 }
69
70 remove_header(headers, name);
71 headers.push(Header {
72 name: normalize_header_name(name),
73 value: value.trim().to_string(),
74 });
75}
76
77pub fn remove_header(headers: &mut Vec<Header>, name: &str) {
79 let normalized = normalize_header_name(name);
80 headers.retain(|header| normalize_header_name(&header.name) != normalized);
81}
82
83fn is_token_byte(byte: u8) -> bool {
84 byte.is_ascii_alphanumeric()
85 || matches!(
86 byte,
87 b'!' | b'#'
88 | b'$'
89 | b'%'
90 | b'&'
91 | b'\''
92 | b'*'
93 | b'+'
94 | b'-'
95 | b'.'
96 | b'^'
97 | b'_'
98 | b'`'
99 | b'|'
100 | b'~'
101 )
102}