oxihttp_core/
header_ext.rs1use http::{HeaderMap, HeaderValue};
4
5use crate::content_type::ContentType;
6use crate::OxiHttpError;
7
8pub trait HeaderMapExt {
10 fn content_type(&self) -> Option<ContentType>;
12
13 fn content_length(&self) -> Option<u64>;
15
16 fn authorization(&self) -> Option<&str>;
18
19 fn accept(&self) -> Option<&str>;
21
22 fn host(&self) -> Option<&str>;
24
25 fn user_agent(&self) -> Option<&str>;
27
28 fn cache_control(&self) -> Option<&str>;
30
31 fn etag(&self) -> Option<&str>;
33
34 fn if_none_match(&self) -> Option<&str>;
36
37 fn if_modified_since(&self) -> Option<&str>;
39
40 fn cookie_header(&self) -> Option<&str>;
42
43 fn location(&self) -> Option<&str>;
45
46 fn referer(&self) -> Option<&str>;
48
49 fn set_content_type(&mut self, ct: &ContentType) -> Result<(), OxiHttpError>;
51
52 fn set_content_length(&mut self, len: u64) -> Result<(), OxiHttpError>;
54
55 fn set_bearer_auth(&mut self, token: &str) -> Result<(), OxiHttpError>;
57
58 fn set_basic_auth(
60 &mut self,
61 username: &str,
62 password: Option<&str>,
63 ) -> Result<(), OxiHttpError>;
64
65 fn set_cache_control(&mut self, value: &str) -> Result<(), OxiHttpError>;
67
68 fn set_etag(&mut self, value: &str) -> Result<(), OxiHttpError>;
70
71 fn set_location(&mut self, value: &str) -> Result<(), OxiHttpError>;
73
74 fn set_cookie_header(&mut self, value: &str) -> Result<(), OxiHttpError>;
76}
77
78impl HeaderMapExt for HeaderMap {
79 fn content_type(&self) -> Option<ContentType> {
80 let val = self.get(http::header::CONTENT_TYPE)?;
81 val.to_str().ok()?.parse().ok()
82 }
83
84 fn content_length(&self) -> Option<u64> {
85 let val = self.get(http::header::CONTENT_LENGTH)?;
86 val.to_str().ok()?.parse().ok()
87 }
88
89 fn authorization(&self) -> Option<&str> {
90 self.get(http::header::AUTHORIZATION)?.to_str().ok()
91 }
92
93 fn accept(&self) -> Option<&str> {
94 self.get(http::header::ACCEPT)?.to_str().ok()
95 }
96
97 fn host(&self) -> Option<&str> {
98 self.get(http::header::HOST)?.to_str().ok()
99 }
100
101 fn user_agent(&self) -> Option<&str> {
102 self.get(http::header::USER_AGENT)?.to_str().ok()
103 }
104
105 fn cache_control(&self) -> Option<&str> {
106 self.get(http::header::CACHE_CONTROL)?.to_str().ok()
107 }
108
109 fn etag(&self) -> Option<&str> {
110 self.get(http::header::ETAG)?.to_str().ok()
111 }
112
113 fn if_none_match(&self) -> Option<&str> {
114 self.get(http::header::IF_NONE_MATCH)?.to_str().ok()
115 }
116
117 fn if_modified_since(&self) -> Option<&str> {
118 self.get(http::header::IF_MODIFIED_SINCE)?.to_str().ok()
119 }
120
121 fn cookie_header(&self) -> Option<&str> {
122 self.get(http::header::COOKIE)?.to_str().ok()
123 }
124
125 fn location(&self) -> Option<&str> {
126 self.get(http::header::LOCATION)?.to_str().ok()
127 }
128
129 fn referer(&self) -> Option<&str> {
130 self.get(http::header::REFERER)?.to_str().ok()
131 }
132
133 fn set_content_type(&mut self, ct: &ContentType) -> Result<(), OxiHttpError> {
134 let val = HeaderValue::from_str(&ct.to_string())
135 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
136 self.insert(http::header::CONTENT_TYPE, val);
137 Ok(())
138 }
139
140 fn set_content_length(&mut self, len: u64) -> Result<(), OxiHttpError> {
141 let val = HeaderValue::from_str(&len.to_string())
142 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
143 self.insert(http::header::CONTENT_LENGTH, val);
144 Ok(())
145 }
146
147 fn set_bearer_auth(&mut self, token: &str) -> Result<(), OxiHttpError> {
148 let val = HeaderValue::from_str(&format!("Bearer {token}"))
149 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
150 self.insert(http::header::AUTHORIZATION, val);
151 Ok(())
152 }
153
154 fn set_basic_auth(
155 &mut self,
156 username: &str,
157 password: Option<&str>,
158 ) -> Result<(), OxiHttpError> {
159 use std::io::Write;
160 let mut buf = Vec::new();
161 let _ = write!(buf, "{username}:");
162 if let Some(pw) = password {
163 let _ = write!(buf, "{pw}");
164 }
165 let encoded = base64_encode(&buf);
167 let val = HeaderValue::from_str(&format!("Basic {encoded}"))
168 .map_err(|e| OxiHttpError::InvalidHeader(e.to_string()))?;
169 self.insert(http::header::AUTHORIZATION, val);
170 Ok(())
171 }
172
173 fn set_cache_control(&mut self, value: &str) -> Result<(), OxiHttpError> {
174 let hv = HeaderValue::from_str(value).map_err(|_| {
175 OxiHttpError::InvalidHeader(format!("invalid Cache-Control value: {value}"))
176 })?;
177 self.insert(http::header::CACHE_CONTROL, hv);
178 Ok(())
179 }
180
181 fn set_etag(&mut self, value: &str) -> Result<(), OxiHttpError> {
182 let hv = HeaderValue::from_str(value)
183 .map_err(|_| OxiHttpError::InvalidHeader(format!("invalid ETag value: {value}")))?;
184 self.insert(http::header::ETAG, hv);
185 Ok(())
186 }
187
188 fn set_location(&mut self, value: &str) -> Result<(), OxiHttpError> {
189 let hv = HeaderValue::from_str(value)
190 .map_err(|_| OxiHttpError::InvalidHeader(format!("invalid Location value: {value}")))?;
191 self.insert(http::header::LOCATION, hv);
192 Ok(())
193 }
194
195 fn set_cookie_header(&mut self, value: &str) -> Result<(), OxiHttpError> {
196 let hv = HeaderValue::from_str(value).map_err(|_| {
197 OxiHttpError::InvalidHeader(format!("invalid Set-Cookie value: {value}"))
198 })?;
199 self.append(http::header::SET_COOKIE, hv);
200 Ok(())
201 }
202}
203
204fn base64_encode(data: &[u8]) -> String {
206 const CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
207 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
208 for chunk in data.chunks(3) {
209 let b0 = chunk[0] as u32;
210 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
211 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
212 let triple = (b0 << 16) | (b1 << 8) | b2;
213
214 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
215 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
216 if chunk.len() > 1 {
217 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
218 } else {
219 result.push('=');
220 }
221 if chunk.len() > 2 {
222 result.push(CHARS[(triple & 0x3F) as usize] as char);
223 } else {
224 result.push('=');
225 }
226 }
227 result
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_content_type_accessor() {
236 let mut headers = HeaderMap::new();
237 headers.insert(
238 http::header::CONTENT_TYPE,
239 HeaderValue::from_static("application/json"),
240 );
241 assert_eq!(headers.content_type(), Some(ContentType::Json));
242 }
243
244 #[test]
245 fn test_content_length_accessor() {
246 let mut headers = HeaderMap::new();
247 headers.insert(http::header::CONTENT_LENGTH, HeaderValue::from_static("42"));
248 assert_eq!(headers.content_length(), Some(42));
249 }
250
251 #[test]
252 fn test_set_bearer_auth() {
253 let mut headers = HeaderMap::new();
254 headers.set_bearer_auth("mytoken123").expect("set bearer");
255 assert_eq!(headers.authorization(), Some("Bearer mytoken123"));
256 }
257
258 #[test]
259 fn test_set_basic_auth() {
260 let mut headers = HeaderMap::new();
261 headers
262 .set_basic_auth("user", Some("pass"))
263 .expect("set basic");
264 let auth = headers.authorization().expect("auth present");
265 assert!(auth.starts_with("Basic "));
266 assert_eq!(auth, "Basic dXNlcjpwYXNz");
268 }
269
270 #[test]
271 fn test_set_content_type() {
272 let mut headers = HeaderMap::new();
273 headers
274 .set_content_type(&ContentType::Json)
275 .expect("set ct");
276 assert_eq!(headers.content_type(), Some(ContentType::Json));
277 }
278
279 #[test]
280 fn test_host_accessor() {
281 let mut headers = HeaderMap::new();
282 headers.insert(http::header::HOST, HeaderValue::from_static("example.com"));
283 assert_eq!(headers.host(), Some("example.com"));
284 }
285
286 #[test]
287 fn test_base64_encode() {
288 assert_eq!(base64_encode(b""), "");
289 assert_eq!(base64_encode(b"f"), "Zg==");
290 assert_eq!(base64_encode(b"fo"), "Zm8=");
291 assert_eq!(base64_encode(b"foo"), "Zm9v");
292 assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
293 assert_eq!(base64_encode(b"user:pass"), "dXNlcjpwYXNz");
294 }
295
296 #[test]
297 fn test_cache_control_getter_setter() {
298 let mut headers = HeaderMap::new();
299 headers
300 .set_cache_control("no-store, max-age=0")
301 .expect("set cache-control");
302 assert_eq!(headers.cache_control(), Some("no-store, max-age=0"));
303 }
304
305 #[test]
306 fn test_etag_getter_setter() {
307 let mut headers = HeaderMap::new();
308 headers
309 .set_etag("\"33a64df551425fcc55e4d42a148795d9f25f89d4\"")
310 .expect("set etag");
311 assert_eq!(
312 headers.etag(),
313 Some("\"33a64df551425fcc55e4d42a148795d9f25f89d4\"")
314 );
315 }
316
317 #[test]
318 fn test_if_none_match_getter() {
319 let mut headers = HeaderMap::new();
320 headers.insert(
321 http::header::IF_NONE_MATCH,
322 HeaderValue::from_static("\"abc123\""),
323 );
324 assert_eq!(headers.if_none_match(), Some("\"abc123\""));
325 }
326
327 #[test]
328 fn test_if_modified_since_getter() {
329 let mut headers = HeaderMap::new();
330 headers.insert(
331 http::header::IF_MODIFIED_SINCE,
332 HeaderValue::from_static("Wed, 21 Oct 2015 07:28:00 GMT"),
333 );
334 assert_eq!(
335 headers.if_modified_since(),
336 Some("Wed, 21 Oct 2015 07:28:00 GMT")
337 );
338 }
339
340 #[test]
341 fn test_cookie_header_getter() {
342 let mut headers = HeaderMap::new();
343 headers.insert(
344 http::header::COOKIE,
345 HeaderValue::from_static("session=abc"),
346 );
347 assert_eq!(headers.cookie_header(), Some("session=abc"));
348 }
349
350 #[test]
351 fn test_location_getter_setter() {
352 let mut headers = HeaderMap::new();
353 headers
354 .set_location("https://example.com/new-path")
355 .expect("set location");
356 assert_eq!(headers.location(), Some("https://example.com/new-path"));
357 }
358
359 #[test]
360 fn test_referer_getter() {
361 let mut headers = HeaderMap::new();
362 headers.insert(
363 http::header::REFERER,
364 HeaderValue::from_static("https://example.com/page"),
365 );
366 assert_eq!(headers.referer(), Some("https://example.com/page"));
367 }
368
369 #[test]
370 fn test_set_cookie_header_appends() {
371 let mut headers = HeaderMap::new();
372 headers
373 .set_cookie_header("session=abc; Path=/; HttpOnly")
374 .expect("first set-cookie");
375 headers
376 .set_cookie_header("theme=dark; Path=/; Max-Age=31536000")
377 .expect("second set-cookie");
378 let values: Vec<&str> = headers
379 .get_all(http::header::SET_COOKIE)
380 .iter()
381 .filter_map(|v| v.to_str().ok())
382 .collect();
383 assert_eq!(values.len(), 2);
384 assert!(values.contains(&"session=abc; Path=/; HttpOnly"));
385 assert!(values.contains(&"theme=dark; Path=/; Max-Age=31536000"));
386 }
387}