sark_core/http/codec/
header_utils.rs1use 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}