Skip to main content

sark_core/http/codec/
header_utils.rs

1use std::ops::ControlFlow;
2
3use crate::error::{Error, Result};
4
5#[derive(Clone, Copy)]
6pub(super) enum CsvScanMode {
7    Lenient,
8    Strict,
9}
10
11pub struct Header;
12
13impl Header {
14    pub(super) fn is_ascii_ws(b: u8) -> bool {
15        b == b' ' || b == b'\t'
16    }
17
18    pub(super) fn trim_ascii_ws(value: &[u8]) -> &[u8] {
19        let mut start = 0usize;
20        let mut end = value.len();
21        while start < end && Self::is_ascii_ws(value[start]) {
22            start += 1;
23        }
24        while end > start && Self::is_ascii_ws(value[end - 1]) {
25            end -= 1;
26        }
27        &value[start..end]
28    }
29
30    pub(super) fn scan_csv_tokens(
31        value: &[u8],
32        mode: CsvScanMode,
33        mut on_token: impl FnMut(&[u8]) -> ControlFlow<()>,
34    ) -> Result<()> {
35        match mode {
36            CsvScanMode::Strict => {
37                let mut start = 0usize;
38                let mut saw_token = false;
39                let mut i = 0usize;
40                while i <= value.len() {
41                    if i == value.len() || value[i] == b',' {
42                        let token = Self::trim_ascii_ws(&value[start..i]);
43                        if token.is_empty() {
44                            return Err(Error::BadRequest("Invalid Transfer-Encoding".into()));
45                        }
46                        saw_token = true;
47                        if on_token(token).is_break() {
48                            return Ok(());
49                        }
50                        start = i.saturating_add(1);
51                    }
52                    i += 1;
53                }
54                if !saw_token {
55                    return Err(Error::BadRequest("Invalid Transfer-Encoding".into()));
56                }
57                Ok(())
58            }
59            CsvScanMode::Lenient => {
60                let mut i = 0usize;
61                while i < value.len() {
62                    while i < value.len() && (Self::is_ascii_ws(value[i]) || value[i] == b',') {
63                        i += 1;
64                    }
65                    if i >= value.len() {
66                        break;
67                    }
68                    let start = i;
69                    while i < value.len() && value[i] != b',' {
70                        i += 1;
71                    }
72                    let token = Self::trim_ascii_ws(&value[start..i]);
73                    if token.is_empty() {
74                        continue;
75                    }
76                    if on_token(token).is_break() {
77                        break;
78                    }
79                }
80                Ok(())
81            }
82        }
83    }
84
85    pub fn has_token(value: &[u8], token: &[u8]) -> bool {
86        if token.is_empty() {
87            return false;
88        }
89        let mut found = false;
90        let _ = Self::scan_csv_tokens(value, CsvScanMode::Lenient, |part| {
91            if part.eq_ignore_ascii_case(token) {
92                found = true;
93                ControlFlow::Break(())
94            } else {
95                ControlFlow::Continue(())
96            }
97        });
98        found
99    }
100
101    pub(super) fn trimmed_eq_ascii_case(value: &[u8], expected: &[u8]) -> bool {
102        Self::trim_ascii_ws(value).eq_ignore_ascii_case(expected)
103    }
104
105    pub fn content_length(value: &[u8]) -> Result<usize> {
106        if value.is_empty() {
107            return Err(Error::BadRequest("Invalid Content-Length".into()));
108        }
109        let mut len: usize = 0;
110        for &b in value {
111            if !b.is_ascii_digit() {
112                return Err(Error::BadRequest("Invalid Content-Length".into()));
113            }
114            len = len
115                .checked_mul(10)
116                .and_then(|n| n.checked_add((b - b'0') as usize))
117                .ok_or_else(|| Error::BadRequest("Invalid Content-Length".into()))?;
118        }
119        Ok(len)
120    }
121
122    pub fn has_name(headers: &[httparse::Header<'_>], name: &str) -> bool {
123        headers.iter().any(|h| h.name.eq_ignore_ascii_case(name))
124    }
125}
126
127pub trait HeaderLookup: private::SealedHeaderLookup {
128    fn header_value(&self, name: http::header::HeaderName) -> Option<&http::header::HeaderValue>;
129
130    fn has_token(&self, name: http::header::HeaderName, token: &str) -> bool {
131        match self.header_value(name) {
132            Some(v) => Header::has_token(v.as_bytes(), token.as_bytes()),
133            None => false,
134        }
135    }
136
137    fn value_eq_ascii_case(&self, name: http::header::HeaderName, expected: &str) -> bool {
138        self.header_value(name)
139            .is_some_and(|value| value.as_bytes().eq_ignore_ascii_case(expected.as_bytes()))
140    }
141}
142
143impl HeaderLookup for http::HeaderMap {
144    fn header_value(&self, name: http::header::HeaderName) -> Option<&http::header::HeaderValue> {
145        self.get(name)
146    }
147}
148
149impl HeaderLookup for crate::http::HeaderList {
150    fn header_value(&self, name: http::header::HeaderName) -> Option<&http::header::HeaderValue> {
151        self.get(name)
152    }
153}
154
155mod private {
156    pub trait SealedHeaderLookup {}
157
158    impl SealedHeaderLookup for http::HeaderMap {}
159    impl SealedHeaderLookup for crate::http::HeaderList {}
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn content_length_digits_accepted() {
168        assert_eq!(Header::content_length(b"5").unwrap(), 5);
169        assert_eq!(Header::content_length(b"0").unwrap(), 0);
170        assert_eq!(Header::content_length(b"1234").unwrap(), 1234);
171    }
172
173    #[test]
174    fn content_length_plus_prefix_rejected() {
175        assert!(Header::content_length(b"+5").is_err());
176    }
177
178    #[test]
179    fn content_length_whitespace_rejected() {
180        assert!(Header::content_length(b" 5").is_err());
181        assert!(Header::content_length(b"5 ").is_err());
182        assert!(Header::content_length(b"5\r").is_err());
183    }
184
185    #[test]
186    fn content_length_empty_rejected() {
187        assert!(Header::content_length(b"").is_err());
188    }
189
190    #[test]
191    fn content_length_non_digit_rejected() {
192        assert!(Header::content_length(b"abc").is_err());
193        assert!(Header::content_length(b"5a").is_err());
194    }
195
196    #[test]
197    fn content_length_overflow_rejected() {
198        assert!(Header::content_length(b"99999999999999999999999999").is_err());
199    }
200}