Skip to main content

use_header/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// A simple header name-value pair.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Header {
7    pub name: String,
8    pub value: String,
9}
10
11/// Normalizes a header name to lowercase ASCII.
12#[must_use]
13pub fn normalize_header_name(input: &str) -> String {
14    input.trim().to_ascii_lowercase()
15}
16
17/// Returns `true` when the input is a valid HTTP field name.
18#[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/// Parses a single `Name: Value` header line.
25#[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/// Parses multiple header lines separated by newlines.
39#[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/// Returns the first matching header value, compared case-insensitively.
48#[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/// Returns `true` when the header list contains the requested name.
59#[must_use]
60pub fn has_header(headers: &[Header], name: &str) -> bool {
61    get_header(headers, name).is_some()
62}
63
64/// Sets or replaces a header value.
65pub 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
77/// Removes every header whose name matches case-insensitively.
78pub 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}