1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! Utilities for parsing information from headers
|
7 + |
|
8 + | use aws_smithy_types::date_time::Format;
|
9 + | use aws_smithy_types::primitive::Parse;
|
10 + | use aws_smithy_types::DateTime;
|
11 + | use http_02x::header::{HeaderMap, HeaderName, HeaderValue};
|
12 + | use std::borrow::Cow;
|
13 + | use std::error::Error;
|
14 + | use std::fmt;
|
15 + | use std::str::FromStr;
|
16 + |
|
17 + | /// An error was encountered while parsing a header
|
18 + | #[derive(Debug)]
|
19 + | pub struct ParseError {
|
20 + | message: Cow<'static, str>,
|
21 + | source: Option<Box<dyn Error + Send + Sync + 'static>>,
|
22 + | }
|
23 + |
|
24 + | impl ParseError {
|
25 + | /// Create a new parse error with the given `message`
|
26 + | pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
|
27 + | Self {
|
28 + | message: message.into(),
|
29 + | source: None,
|
30 + | }
|
31 + | }
|
32 + |
|
33 + | /// Attach a source to this error.
|
34 + | pub fn with_source(self, source: impl Into<Box<dyn Error + Send + Sync + 'static>>) -> Self {
|
35 + | Self {
|
36 + | source: Some(source.into()),
|
37 + | ..self
|
38 + | }
|
39 + | }
|
40 + | }
|
41 + |
|
42 + | impl fmt::Display for ParseError {
|
43 + | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
44 + | write!(f, "output failed to parse in headers: {}", self.message)
|
45 + | }
|
46 + | }
|
47 + |
|
48 + | impl Error for ParseError {
|
49 + | fn source(&self) -> Option<&(dyn Error + 'static)> {
|
50 + | self.source.as_ref().map(|err| err.as_ref() as _)
|
51 + | }
|
52 + | }
|
53 + |
|
54 + | /// Read all the dates from the header map at `key` according the `format`
|
55 + | ///
|
56 + | /// This is separate from `read_many` below because we need to invoke `DateTime::read` to take advantage
|
57 + | /// of comma-aware parsing
|
58 + | pub fn many_dates<'a>(
|
59 + | values: impl Iterator<Item = &'a str>,
|
60 + | format: Format,
|
61 + | ) -> Result<Vec<DateTime>, ParseError> {
|
62 + | let mut out = vec![];
|
63 + | for header in values {
|
64 + | let mut header = header;
|
65 + | while !header.is_empty() {
|
66 + | let (v, next) = DateTime::read(header, format, ',').map_err(|err| {
|
67 + | ParseError::new(format!("header could not be parsed as date: {}", err))
|
68 + | })?;
|
69 + | out.push(v);
|
70 + | header = next;
|
71 + | }
|
72 + | }
|
73 + | Ok(out)
|
74 + | }
|
75 + |
|
76 + | /// Returns an iterator over pairs where the first element is the unprefixed header name that
|
77 + | /// starts with the input `key` prefix, and the second element is the full header name.
|
78 + | pub fn headers_for_prefix<'a>(
|
79 + | header_names: impl Iterator<Item = &'a str>,
|
80 + | key: &'a str,
|
81 + | ) -> impl Iterator<Item = (&'a str, &'a str)> {
|
82 + | let lower_key = key.to_ascii_lowercase();
|
83 + | header_names
|
84 + | .filter(move |k| k.starts_with(&lower_key))
|
85 + | .map(move |k| (&k[key.len()..], k))
|
86 + | }
|
87 + |
|
88 + | /// Convert a `HeaderValue` into a `Vec<T>` where `T: FromStr`
|
89 + | pub fn read_many_from_str<'a, T: FromStr>(
|
90 + | values: impl Iterator<Item = &'a str>,
|
91 + | ) -> Result<Vec<T>, ParseError>
|
92 + | where
|
93 + | T::Err: Error + Send + Sync + 'static,
|
94 + | {
|
95 + | read_many(values, |v: &str| {
|
96 + | v.parse().map_err(|err| {
|
97 + | ParseError::new("failed during `FromString` conversion").with_source(err)
|
98 + | })
|
99 + | })
|
100 + | }
|
101 + |
|
102 + | /// Convert a `HeaderValue` into a `Vec<T>` where `T: Parse`
|
103 + | pub fn read_many_primitive<'a, T: Parse>(
|
104 + | values: impl Iterator<Item = &'a str>,
|
105 + | ) -> Result<Vec<T>, ParseError> {
|
106 + | read_many(values, |v: &str| {
|
107 + | T::parse_smithy_primitive(v)
|
108 + | .map_err(|err| ParseError::new("failed reading a list of primitives").with_source(err))
|
109 + | })
|
110 + | }
|
111 + |
|
112 + | /// Read many comma / header delimited values from HTTP headers for `FromStr` types
|
113 + | fn read_many<'a, T>(
|
114 + | values: impl Iterator<Item = &'a str>,
|
115 + | f: impl Fn(&str) -> Result<T, ParseError>,
|
116 + | ) -> Result<Vec<T>, ParseError> {
|
117 + | let mut out = vec![];
|
118 + | for header in values {
|
119 + | let mut header = header.as_bytes();
|
120 + | while !header.is_empty() {
|
121 + | let (v, next) = read_one(header, &f)?;
|
122 + | out.push(v);
|
123 + | header = next;
|
124 + | }
|
125 + | }
|
126 + | Ok(out)
|
127 + | }
|
128 + |
|
129 + | /// Read exactly one or none from a headers iterator
|
130 + | ///
|
131 + | /// This function does not perform comma splitting like `read_many`
|
132 + | pub fn one_or_none<'a, T: FromStr>(
|
133 + | mut values: impl Iterator<Item = &'a str>,
|
134 + | ) -> Result<Option<T>, ParseError>
|
135 + | where
|
136 + | T::Err: Error + Send + Sync + 'static,
|
137 + | {
|
138 + | let first = match values.next() {
|
139 + | Some(v) => v,
|
140 + | None => return Ok(None),
|
141 + | };
|
142 + | match values.next() {
|
143 + | None => T::from_str(first.trim())
|
144 + | .map_err(|err| ParseError::new("failed to parse string").with_source(err))
|
145 + | .map(Some),
|
146 + | Some(_) => Err(ParseError::new(
|
147 + | "expected a single value but found multiple",
|
148 + | )),
|
149 + | }
|
150 + | }
|
151 + |
|
152 + | /// Given an HTTP request, set a request header if that header was not already set.
|
153 + | pub fn set_request_header_if_absent<V>(
|
154 + | request: http_02x::request::Builder,
|
155 + | key: HeaderName,
|
156 + | value: V,
|
157 + | ) -> http_02x::request::Builder
|
158 + | where
|
159 + | HeaderValue: TryFrom<V>,
|
160 + | <HeaderValue as TryFrom<V>>::Error: Into<http_02x::Error>,
|
161 + | {
|
162 + | if !request
|
163 + | .headers_ref()
|
164 + | .map(|map| map.contains_key(&key))
|
165 + | .unwrap_or(false)
|
166 + | {
|
167 + | request.header(key, value)
|
168 + | } else {
|
169 + | request
|
170 + | }
|
171 + | }
|
172 + |
|
173 + | /// Given an HTTP response, set a response header if that header was not already set.
|
174 + | pub fn set_response_header_if_absent<V>(
|
175 + | response: http_02x::response::Builder,
|
176 + | key: HeaderName,
|
177 + | value: V,
|
178 + | ) -> http_02x::response::Builder
|
179 + | where
|
180 + | HeaderValue: TryFrom<V>,
|
181 + | <HeaderValue as TryFrom<V>>::Error: Into<http_02x::Error>,
|
182 + | {
|
183 + | if !response
|
184 + | .headers_ref()
|
185 + | .map(|map| map.contains_key(&key))
|
186 + | .unwrap_or(false)
|
187 + | {
|
188 + | response.header(key, value)
|
189 + | } else {
|
190 + | response
|
191 + | }
|
192 + | }
|
193 + |
|
194 + | /// Functions for parsing multiple comma-delimited header values out of a
|
195 + | /// single header. This parsing adheres to
|
196 + | /// [RFC-7230's specification of header values](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6).
|
197 + | mod parse_multi_header {
|
198 + | use super::ParseError;
|
199 + | use std::borrow::Cow;
|
200 + |
|
201 + | fn trim(s: Cow<'_, str>) -> Cow<'_, str> {
|
202 + | match s {
|
203 + | Cow::Owned(s) => Cow::Owned(s.trim().into()),
|
204 + | Cow::Borrowed(s) => Cow::Borrowed(s.trim()),
|
205 + | }
|
206 + | }
|
207 + |
|
208 + | fn replace<'a>(value: Cow<'a, str>, pattern: &str, replacement: &str) -> Cow<'a, str> {
|
209 + | if value.contains(pattern) {
|
210 + | Cow::Owned(value.replace(pattern, replacement))
|
211 + | } else {
|
212 + | value
|
213 + | }
|
214 + | }
|
215 + |
|
216 + | /// Reads a single value out of the given input, and returns a tuple containing
|
217 + | /// the parsed value and the remainder of the slice that can be used to parse
|
218 + | /// more values.
|
219 + | pub(crate) fn read_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> {
|
220 + | for (index, &byte) in input.iter().enumerate() {
|
221 + | let current_slice = &input[index..];
|
222 + | match byte {
|
223 + | b' ' | b'\t' => { /* skip whitespace */ }
|
224 + | b'"' => return read_quoted_value(¤t_slice[1..]),
|
225 + | _ => {
|
226 + | let (value, rest) = read_unquoted_value(current_slice)?;
|
227 + | return Ok((trim(value), rest));
|
228 + | }
|
229 + | }
|
230 + | }
|
231 + |
|
232 + | // We only end up here if the entire header value was whitespace or empty
|
233 + | Ok((Cow::Borrowed(""), &[]))
|
234 + | }
|
235 + |
|
236 + | fn read_unquoted_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> {
|
237 + | let next_delim = input.iter().position(|&b| b == b',').unwrap_or(input.len());
|
238 + | let (first, next) = input.split_at(next_delim);
|
239 + | let first = std::str::from_utf8(first)
|
240 + | .map_err(|_| ParseError::new("header was not valid utf-8"))?;
|
241 + | Ok((Cow::Borrowed(first), then_comma(next).unwrap()))
|
242 + | }
|
243 + |
|
244 + | /// Reads a header value that is surrounded by quotation marks and may have escaped
|
245 + | /// quotes inside of it.
|
246 + | fn read_quoted_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> {
|
247 + | for index in 0..input.len() {
|
248 + | match input[index] {
|
249 + | b'"' if index == 0 || input[index - 1] != b'\\' => {
|
250 + | let mut inner = Cow::Borrowed(
|
251 + | std::str::from_utf8(&input[0..index])
|
252 + | .map_err(|_| ParseError::new("header was not valid utf-8"))?,
|
253 + | );
|
254 + | inner = replace(inner, "\\\"", "\"");
|
255 + | inner = replace(inner, "\\\\", "\\");
|
256 + | let rest = then_comma(&input[(index + 1)..])?;
|
257 + | return Ok((inner, rest));
|
258 + | }
|
259 + | _ => {}
|
260 + | }
|
261 + | }
|
262 + | Err(ParseError::new(
|
263 + | "header value had quoted value without end quote",
|
264 + | ))
|
265 + | }
|
266 + |
|
267 + | fn then_comma(s: &[u8]) -> Result<&[u8], ParseError> {
|
268 + | if s.is_empty() {
|
269 + | Ok(s)
|
270 + | } else if s.starts_with(b",") {
|
271 + | Ok(&s[1..])
|
272 + | } else {
|
273 + | Err(ParseError::new("expected delimiter `,`"))
|
274 + | }
|
275 + | }
|
276 + | }
|
277 + |
|
278 + | /// Read one comma delimited value for `FromStr` types
|
279 + | fn read_one<'a, T>(
|
280 + | s: &'a [u8],
|
281 + | f: &impl Fn(&str) -> Result<T, ParseError>,
|
282 + | ) -> Result<(T, &'a [u8]), ParseError> {
|
283 + | let (value, rest) = parse_multi_header::read_value(s)?;
|
284 + | Ok((f(&value)?, rest))
|
285 + | }
|
286 + |
|
287 + | /// Conditionally quotes and escapes a header value if the header value contains a comma or quote.
|
288 + | pub fn quote_header_value<'a>(value: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
|
289 + | let value = value.into();
|
290 + | if value.trim().len() != value.len()
|
291 + | || value.contains('"')
|
292 + | || value.contains(',')
|
293 + | || value.contains('(')
|
294 + | || value.contains(')')
|
295 + | {
|
296 + | Cow::Owned(format!(
|
297 + | "\"{}\"",
|
298 + | value.replace('\\', "\\\\").replace('"', "\\\"")
|
299 + | ))
|
300 + | } else {
|
301 + | value
|
302 + | }
|
303 + | }
|
304 + |
|
305 + | /// Given two [`HeaderMap`]s, merge them together and return the merged `HeaderMap`. If the
|
306 + | /// two `HeaderMap`s share any keys, values from the right `HeaderMap` be appended to the left `HeaderMap`.
|
307 + | pub fn append_merge_header_maps(
|
308 + | mut lhs: HeaderMap<HeaderValue>,
|
309 + | rhs: HeaderMap<HeaderValue>,
|
310 + | ) -> HeaderMap<HeaderValue> {
|
311 + | let mut last_header_name_seen = None;
|
312 + | for (header_name, header_value) in rhs.into_iter() {
|
313 + | // For each yielded item that has None provided for the `HeaderName`,
|
314 + | // then the associated header name is the same as that of the previously
|
315 + | // yielded item. The first yielded item will have `HeaderName` set.
|
316 + | // https://docs.rs/http/latest/http/header/struct.HeaderMap.html#method.into_iter-2
|
317 + | match (&mut last_header_name_seen, header_name) {
|
318 + | (_, Some(header_name)) => {
|
319 + | lhs.append(header_name.clone(), header_value);
|
320 + | last_header_name_seen = Some(header_name);
|
321 + | }
|
322 + | (Some(header_name), None) => {
|
323 + | lhs.append(header_name.clone(), header_value);
|
324 + | }
|
325 + | (None, None) => unreachable!(),
|
326 + | };
|
327 + | }
|
328 + |
|
329 + | lhs
|
330 + | }
|
331 + |
|
332 + | #[cfg(test)]
|
333 + | mod test {
|
334 + | use super::quote_header_value;
|
335 + | use crate::header::{
|
336 + | append_merge_header_maps, headers_for_prefix, many_dates, read_many_from_str,
|
337 + | read_many_primitive, set_request_header_if_absent, set_response_header_if_absent,
|
338 + | ParseError,
|
339 + | };
|
340 + | use aws_smithy_runtime_api::http::Request;
|
341 + | use aws_smithy_types::error::display::DisplayErrorContext;
|
342 + | use aws_smithy_types::{date_time::Format, DateTime};
|
343 + | use http_02x::header::{HeaderMap, HeaderName, HeaderValue};
|
344 + | use std::collections::HashMap;
|
345 + |
|
346 + | #[test]
|
347 + | fn put_on_request_if_absent() {
|
348 + | let builder = http_02x::Request::builder().header("foo", "bar");
|
349 + | let builder = set_request_header_if_absent(builder, HeaderName::from_static("foo"), "baz");
|
350 + | let builder =
|
351 + | set_request_header_if_absent(builder, HeaderName::from_static("other"), "value");
|
352 + | let req = builder.body(()).expect("valid request");
|
353 + | assert_eq!(
|
354 + | req.headers().get_all("foo").iter().collect::<Vec<_>>(),
|
355 + | vec!["bar"]
|
356 + | );
|
357 + | assert_eq!(
|
358 + | req.headers().get_all("other").iter().collect::<Vec<_>>(),
|
359 + | vec!["value"]
|
360 + | );
|
361 + | }
|
362 + |
|
363 + | #[test]
|
364 + | fn put_on_response_if_absent() {
|
365 + | let builder = http_02x::Response::builder().header("foo", "bar");
|
366 + | let builder = set_response_header_if_absent(builder, HeaderName::from_static("foo"), "baz");
|
367 + | let builder =
|
368 + | set_response_header_if_absent(builder, HeaderName::from_static("other"), "value");
|
369 + | let response = builder.body(()).expect("valid response");
|
370 + | assert_eq!(
|
371 + | response.headers().get_all("foo").iter().collect::<Vec<_>>(),
|
372 + | vec!["bar"]
|
373 + | );
|
374 + | assert_eq!(
|
375 + | response
|
376 + | .headers()
|
377 + | .get_all("other")
|
378 + | .iter()
|
379 + | .collect::<Vec<_>>(),
|
380 + | vec!["value"]
|
381 + | );
|
382 + | }
|
383 + |
|
384 + | #[test]
|
385 + | fn parse_floats() {
|
386 + | let test_request = http_02x::Request::builder()
|
387 + | .header("X-Float-Multi", "0.0,Infinity,-Infinity,5555.5")
|
388 + | .header("X-Float-Error", "notafloat")
|
389 + | .body(())
|
390 + | .unwrap();
|
391 + | assert_eq!(
|
392 + | read_many_primitive::<f32>(
|
393 + | test_request
|
394 + | .headers()
|
395 + | .get_all("X-Float-Multi")
|
396 + | .iter()
|
397 + | .map(|v| v.to_str().unwrap())
|
398 + | )
|
399 + | .expect("valid"),
|
400 + | vec![0.0, f32::INFINITY, f32::NEG_INFINITY, 5555.5]
|
401 + | );
|
402 + | let message = format!(
|
403 + | "{}",
|
404 + | DisplayErrorContext(
|
405 + | read_many_primitive::<f32>(
|
406 + | test_request
|
407 + | .headers()
|
408 + | .get_all("X-Float-Error")
|
409 + | .iter()
|
410 + | .map(|v| v.to_str().unwrap())
|
411 + | )
|
412 + | .expect_err("invalid")
|
413 + | )
|
414 + | );
|
415 + | let expected = "output failed to parse in headers: failed reading a list of primitives: failed to parse input as f32";
|
416 + | assert!(
|
417 + | message.starts_with(expected),
|
418 + | "expected '{message}' to start with '{expected}'"
|
419 + | );
|
420 + | }
|
421 + |
|
422 + | #[test]
|
423 + | fn test_many_dates() {
|
424 + | let test_request = http_02x::Request::builder()
|
425 + | .header("Empty", "")
|
426 + | .header("SingleHttpDate", "Wed, 21 Oct 2015 07:28:00 GMT")
|
427 + | .header(
|
428 + | "MultipleHttpDates",
|
429 + | "Wed, 21 Oct 2015 07:28:00 GMT,Thu, 22 Oct 2015 07:28:00 GMT",
|
430 + | )
|
431 + | .header("SingleEpochSeconds", "1234.5678")
|
432 + | .header("MultipleEpochSeconds", "1234.5678,9012.3456")
|
433 + | .body(())
|
434 + | .unwrap();
|
435 + | let read = |name: &str, format: Format| {
|
436 + | many_dates(
|
437 + | test_request
|
438 + | .headers()
|
439 + | .get_all(name)
|
440 + | .iter()
|
441 + | .map(|v| v.to_str().unwrap()),
|
442 + | format,
|
443 + | )
|
444 + | };
|
445 + | let read_valid = |name: &str, format: Format| read(name, format).expect("valid");
|
446 + | assert_eq!(
|
447 + | read_valid("Empty", Format::DateTime),
|
448 + | Vec::<DateTime>::new()
|
449 + | );
|
450 + | assert_eq!(
|
451 + | read_valid("SingleHttpDate", Format::HttpDate),
|
452 + | vec![DateTime::from_secs_and_nanos(1445412480, 0)]
|
453 + | );
|
454 + | assert_eq!(
|
455 + | read_valid("MultipleHttpDates", Format::HttpDate),
|
456 + | vec![
|
457 + | DateTime::from_secs_and_nanos(1445412480, 0),
|
458 + | DateTime::from_secs_and_nanos(1445498880, 0)
|
459 + | ]
|
460 + | );
|
461 + | assert_eq!(
|
462 + | read_valid("SingleEpochSeconds", Format::EpochSeconds),
|
463 + | vec![DateTime::from_secs_and_nanos(1234, 567_800_000)]
|
464 + | );
|
465 + | assert_eq!(
|
466 + | read_valid("MultipleEpochSeconds", Format::EpochSeconds),
|
467 + | vec![
|
468 + | DateTime::from_secs_and_nanos(1234, 567_800_000),
|
469 + | DateTime::from_secs_and_nanos(9012, 345_600_000)
|
470 + | ]
|
471 + | );
|
472 + | }
|
473 + |
|
474 + | #[test]
|
475 + | fn read_many_strings() {
|
476 + | let test_request = http_02x::Request::builder()
|
477 + | .header("Empty", "")
|
478 + | .header("Foo", " foo")
|
479 + | .header("FooTrailing", "foo ")
|
480 + | .header("FooInQuotes", "\" foo \"")
|
481 + | .header("CommaInQuotes", "\"foo,bar\",baz")
|
482 + | .header("CommaInQuotesTrailing", "\"foo,bar\",baz ")
|
483 + | .header("QuoteInQuotes", "\"foo\\\",bar\",\"\\\"asdf\\\"\",baz")
|
484 + | .header(
|
485 + | "QuoteInQuotesWithSpaces",
|
486 + | "\"foo\\\",bar\", \"\\\"asdf\\\"\", baz",
|
487 + | )
|
488 + | .header("JunkFollowingQuotes", "\"\\\"asdf\\\"\"baz")
|
489 + | .header("EmptyQuotes", "\"\",baz")
|
490 + | .header("EscapedSlashesInQuotes", "foo, \"(foo\\\\bar)\"")
|
491 + | .body(())
|
492 + | .unwrap();
|
493 + | let read = |name: &str| {
|
494 + | read_many_from_str::<String>(
|
495 + | test_request
|
496 + | .headers()
|
497 + | .get_all(name)
|
498 + | .iter()
|
499 + | .map(|v| v.to_str().unwrap()),
|
500 + | )
|
501 + | };
|
502 + | let read_valid = |name: &str| read(name).expect("valid");
|
503 + | assert_eq!(read_valid("Empty"), Vec::<String>::new());
|
504 + | assert_eq!(read_valid("Foo"), vec!["foo"]);
|
505 + | assert_eq!(read_valid("FooTrailing"), vec!["foo"]);
|
506 + | assert_eq!(read_valid("FooInQuotes"), vec![" foo "]);
|
507 + | assert_eq!(read_valid("CommaInQuotes"), vec!["foo,bar", "baz"]);
|
508 + | assert_eq!(read_valid("CommaInQuotesTrailing"), vec!["foo,bar", "baz"]);
|
509 + | assert_eq!(
|
510 + | read_valid("QuoteInQuotes"),
|
511 + | vec!["foo\",bar", "\"asdf\"", "baz"]
|
512 + | );
|
513 + | assert_eq!(
|
514 + | read_valid("QuoteInQuotesWithSpaces"),
|
515 + | vec!["foo\",bar", "\"asdf\"", "baz"]
|
516 + | );
|
517 + | assert!(read("JunkFollowingQuotes").is_err());
|
518 + | assert_eq!(read_valid("EmptyQuotes"), vec!["", "baz"]);
|
519 + | assert_eq!(
|
520 + | read_valid("EscapedSlashesInQuotes"),
|
521 + | vec!["foo", "(foo\\bar)"]
|
522 + | );
|
523 + | }
|
524 + |
|
525 + | #[test]
|
526 + | fn read_many_bools() {
|
527 + | let test_request = http_02x::Request::builder()
|
528 + | .header("X-Bool-Multi", "true,false")
|
529 + | .header("X-Bool-Multi", "true")
|
530 + | .header("X-Bool", "true")
|
531 + | .header("X-Bool-Invalid", "truth,falsy")
|
532 + | .header("X-Bool-Single", "true,false,true,true")
|
533 + | .header("X-Bool-Quoted", "true,\"false\",true,true")
|
534 + | .body(())
|
535 + | .unwrap();
|
536 + | assert_eq!(
|
537 + | read_many_primitive::<bool>(
|
538 + | test_request
|
539 + | .headers()
|
540 + | .get_all("X-Bool-Multi")
|
541 + | .iter()
|
542 + | .map(|v| v.to_str().unwrap())
|
543 + | )
|
544 + | .expect("valid"),
|
545 + | vec![true, false, true]
|
546 + | );
|
547 + |
|
548 + | assert_eq!(
|
549 + | read_many_primitive::<bool>(
|
550 + | test_request
|
551 + | .headers()
|
552 + | .get_all("X-Bool")
|
553 + | .iter()
|
554 + | .map(|v| v.to_str().unwrap())
|
555 + | )
|
556 + | .unwrap(),
|
557 + | vec![true]
|
558 + | );
|
559 + | assert_eq!(
|
560 + | read_many_primitive::<bool>(
|
561 + | test_request
|
562 + | .headers()
|
563 + | .get_all("X-Bool-Single")
|
564 + | .iter()
|
565 + | .map(|v| v.to_str().unwrap())
|
566 + | )
|
567 + | .unwrap(),
|
568 + | vec![true, false, true, true]
|
569 + | );
|
570 + | assert_eq!(
|
571 + | read_many_primitive::<bool>(
|
572 + | test_request
|
573 + | .headers()
|
574 + | .get_all("X-Bool-Quoted")
|
575 + | .iter()
|
576 + | .map(|v| v.to_str().unwrap())
|
577 + | )
|
578 + | .unwrap(),
|
579 + | vec![true, false, true, true]
|
580 + | );
|
581 + | read_many_primitive::<bool>(
|
582 + | test_request
|
583 + | .headers()
|
584 + | .get_all("X-Bool-Invalid")
|
585 + | .iter()
|
586 + | .map(|v| v.to_str().unwrap()),
|
587 + | )
|
588 + | .expect_err("invalid");
|
589 + | }
|
590 + |
|
591 + | #[test]
|
592 + | fn check_read_many_i16() {
|
593 + | let test_request = http_02x::Request::builder()
|
594 + | .header("X-Multi", "123,456")
|
595 + | .header("X-Multi", "789")
|
596 + | .header("X-Num", "777")
|
597 + | .header("X-Num-Invalid", "12ef3")
|
598 + | .header("X-Num-Single", "1,2,3,-4,5")
|
599 + | .header("X-Num-Quoted", "1, \"2\",3,\"-4\",5")
|
600 + | .body(())
|
601 + | .unwrap();
|
602 + | assert_eq!(
|
603 + | read_many_primitive::<i16>(
|
604 + | test_request
|
605 + | .headers()
|
606 + | .get_all("X-Multi")
|
607 + | .iter()
|
608 + | .map(|v| v.to_str().unwrap())
|
609 + | )
|
610 + | .expect("valid"),
|
611 + | vec![123, 456, 789]
|
612 + | );
|
613 + |
|
614 + | assert_eq!(
|
615 + | read_many_primitive::<i16>(
|
616 + | test_request
|
617 + | .headers()
|
618 + | .get_all("X-Num")
|
619 + | .iter()
|
620 + | .map(|v| v.to_str().unwrap())
|
621 + | )
|
622 + | .unwrap(),
|
623 + | vec![777]
|
624 + | );
|
625 + | assert_eq!(
|
626 + | read_many_primitive::<i16>(
|
627 + | test_request
|
628 + | .headers()
|
629 + | .get_all("X-Num-Single")
|
630 + | .iter()
|
631 + | .map(|v| v.to_str().unwrap())
|
632 + | )
|
633 + | .unwrap(),
|
634 + | vec![1, 2, 3, -4, 5]
|
635 + | );
|
636 + | assert_eq!(
|
637 + | read_many_primitive::<i16>(
|
638 + | test_request
|
639 + | .headers()
|
640 + | .get_all("X-Num-Quoted")
|
641 + | .iter()
|
642 + | .map(|v| v.to_str().unwrap())
|
643 + | )
|
644 + | .unwrap(),
|
645 + | vec![1, 2, 3, -4, 5]
|
646 + | );
|
647 + | read_many_primitive::<i16>(
|
648 + | test_request
|
649 + | .headers()
|
650 + | .get_all("X-Num-Invalid")
|
651 + | .iter()
|
652 + | .map(|v| v.to_str().unwrap()),
|
653 + | )
|
654 + | .expect_err("invalid");
|
655 + | }
|
656 + |
|
657 + | #[test]
|
658 + | fn test_prefix_headers() {
|
659 + | let test_request = Request::try_from(
|
660 + | http_02x::Request::builder()
|
661 + | .header("X-Prefix-A", "123,456")
|
662 + | .header("X-Prefix-B", "789")
|
663 + | .header("X-Prefix-C", "777")
|
664 + | .header("X-Prefix-C", "777")
|
665 + | .body(())
|
666 + | .unwrap(),
|
667 + | )
|
668 + | .unwrap();
|
669 + | let resp: Result<HashMap<String, Vec<i16>>, ParseError> =
|
670 + | headers_for_prefix(test_request.headers().iter().map(|h| h.0), "X-Prefix-")
|
671 + | .map(|(key, header_name)| {
|
672 + | let values = test_request.headers().get_all(header_name);
|
673 + | read_many_primitive(values).map(|v| (key.to_string(), v))
|
674 + | })
|
675 + | .collect();
|
676 + | let resp = resp.expect("valid");
|
677 + | assert_eq!(resp.get("a"), Some(&vec![123_i16, 456_i16]));
|
678 + | }
|
679 + |
|
680 + | #[test]
|
681 + | fn test_quote_header_value() {
|
682 + | assert_eq!("", "e_header_value(""));
|
683 + | assert_eq!("foo", "e_header_value("foo"));
|
684 + | assert_eq!("\" foo\"", "e_header_value(" foo"));
|
685 + | assert_eq!("foo bar", "e_header_value("foo bar"));
|
686 + | assert_eq!("\"foo,bar\"", "e_header_value("foo,bar"));
|
687 + | assert_eq!("\",\"", "e_header_value(","));
|
688 + | assert_eq!("\"\\\"foo\\\"\"", "e_header_value("\"foo\""));
|
689 + | assert_eq!("\"\\\"f\\\\oo\\\"\"", "e_header_value("\"f\\oo\""));
|
690 + | assert_eq!("\"(\"", "e_header_value("("));
|
691 + | assert_eq!("\")\"", "e_header_value(")"));
|
692 + | }
|
693 + |
|
694 + | #[test]
|
695 + | fn test_append_merge_header_maps_with_shared_key() {
|
696 + | let header_name = HeaderName::from_static("some_key");
|
697 + | let left_header_value = HeaderValue::from_static("lhs value");
|
698 + | let right_header_value = HeaderValue::from_static("rhs value");
|
699 + |
|
700 + | let mut left_hand_side_headers = HeaderMap::new();
|
701 + | left_hand_side_headers.insert(header_name.clone(), left_header_value.clone());
|
702 + |
|
703 + | let mut right_hand_side_headers = HeaderMap::new();
|
704 + | right_hand_side_headers.insert(header_name.clone(), right_header_value.clone());
|
705 + |
|
706 + | let merged_header_map =
|
707 + | append_merge_header_maps(left_hand_side_headers, right_hand_side_headers);
|
708 + | let actual_merged_values: Vec<_> =
|
709 + | merged_header_map.get_all(header_name).into_iter().collect();
|
710 + |
|
711 + | let expected_merged_values = vec![left_header_value, right_header_value];
|
712 + |
|
713 + | assert_eq!(actual_merged_values, expected_merged_values);
|
714 + | }
|
715 + |
|
716 + | #[test]
|
717 + | fn test_append_merge_header_maps_with_multiple_values_in_left_hand_map() {
|
718 + | let header_name = HeaderName::from_static("some_key");
|
719 + | let left_header_value_1 = HeaderValue::from_static("lhs value 1");
|
720 + | let left_header_value_2 = HeaderValue::from_static("lhs_value 2");
|
721 + | let right_header_value = HeaderValue::from_static("rhs value");
|
722 + |
|
723 + | let mut left_hand_side_headers = HeaderMap::new();
|
724 + | left_hand_side_headers.insert(header_name.clone(), left_header_value_1.clone());
|
725 + | left_hand_side_headers.append(header_name.clone(), left_header_value_2.clone());
|
726 + |
|
727 + | let mut right_hand_side_headers = HeaderMap::new();
|
728 + | right_hand_side_headers.insert(header_name.clone(), right_header_value.clone());
|
729 + |
|
730 + | let merged_header_map =
|
731 + | append_merge_header_maps(left_hand_side_headers, right_hand_side_headers);
|
732 + | let actual_merged_values: Vec<_> =
|
733 + | merged_header_map.get_all(header_name).into_iter().collect();
|
734 + |
|
735 + | let expected_merged_values =
|
736 + | vec![left_header_value_1, left_header_value_2, right_header_value];
|
737 + |
|
738 + | assert_eq!(actual_merged_values, expected_merged_values);
|
739 + | }
|
740 + |
|
741 + | #[test]
|
742 + | fn test_append_merge_header_maps_with_empty_left_hand_map() {
|
743 + | let header_name = HeaderName::from_static("some_key");
|
744 + | let right_header_value_1 = HeaderValue::from_static("rhs value 1");
|
745 + | let right_header_value_2 = HeaderValue::from_static("rhs_value 2");
|
746 + |
|
747 + | let left_hand_side_headers = HeaderMap::new();
|
748 + |
|
749 + | let mut right_hand_side_headers = HeaderMap::new();
|
750 + | right_hand_side_headers.insert(header_name.clone(), right_header_value_1.clone());
|
751 + | right_hand_side_headers.append(header_name.clone(), right_header_value_2.clone());
|
752 + |
|
753 + | let merged_header_map =
|
754 + | append_merge_header_maps(left_hand_side_headers, right_hand_side_headers);
|
755 + | let actual_merged_values: Vec<_> =
|
756 + | merged_header_map.get_all(header_name).into_iter().collect();
|
757 + |
|
758 + | let expected_merged_values = vec![right_header_value_1, right_header_value_2];
|
759 + |
|
760 + | assert_eq!(actual_merged_values, expected_merged_values);
|
761 + | }
|
762 + | }
|