Skip to main content

aws_smithy_runtime_api/http/
header_parse.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Low-level parsing of Smithy values out of HTTP header text.
7//!
8//! <div class="warning">
9//!
10//! Apart from [`ParseError`], which is re-exported as
11//! [`aws_smithy_runtime_api::http::ParseError`](crate::http::ParseError), the items in this
12//! module are an implementation detail shared between `aws-smithy-http` and
13//! `aws-smithy-schema`. They are **not** a stable API and may change in a minor release.
14//! Depend on [`aws_smithy_http::header`] instead.
15//!
16//! </div>
17//!
18//! These primitives operate on iterators of raw header bytes (or `&str`) rather than on any
19//! HTTP library's types, which is why they can live here without pulling an HTTP crate or
20//! `aws-smithy-http`'s dependency tree into schema-based serialization.
21//!
22//! [`aws_smithy_http::header`]: https://docs.rs/aws-smithy-http/latest/aws_smithy_http/header/index.html
23
24use aws_smithy_types::date_time::Format;
25use aws_smithy_types::primitive::Parse;
26use aws_smithy_types::DateTime;
27use std::borrow::Cow;
28use std::error::Error;
29use std::fmt;
30use std::str::FromStr;
31
32/// An error was encountered while parsing a header
33#[derive(Debug)]
34pub struct ParseError {
35    message: Cow<'static, str>,
36    source: Option<Box<dyn Error + Send + Sync + 'static>>,
37}
38
39impl ParseError {
40    /// Create a new parse error with the given `message`
41    pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
42        Self {
43            message: message.into(),
44            source: None,
45        }
46    }
47
48    /// Attach a source to this error.
49    pub fn with_source(self, source: impl Into<Box<dyn Error + Send + Sync + 'static>>) -> Self {
50        Self {
51            source: Some(source.into()),
52            ..self
53        }
54    }
55}
56
57impl fmt::Display for ParseError {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "output failed to parse in headers: {}", self.message)
60    }
61}
62
63impl Error for ParseError {
64    fn source(&self) -> Option<&(dyn Error + 'static)> {
65        self.source.as_ref().map(|err| err.as_ref() as _)
66    }
67}
68
69pub(crate) const NON_UTF8_HEADER: &str = "header was not valid utf-8";
70
71/// Interpret raw header bytes as UTF-8, or fail with a [`ParseError`].
72fn str_from_utf8(bytes: &[u8]) -> Result<&str, ParseError> {
73    std::str::from_utf8(bytes).map_err(|_| ParseError::new(NON_UTF8_HEADER))
74}
75
76/// Read all the dates from the header map at `key` according the `format`
77///
78/// This is separate from `read_many_bytes` below because we need to invoke `DateTime::read` to take
79/// advantage of comma-aware parsing
80pub fn many_dates<'a>(
81    values: impl Iterator<Item = &'a str>,
82    format: Format,
83) -> Result<Vec<DateTime>, ParseError> {
84    many_dates_bytes(values.map(str::as_bytes), format)
85}
86
87/// Read all the dates from raw header values according to the `format`
88///
89/// Like [`many_dates`], but accepts raw bytes. A value that is not valid UTF-8 produces a
90/// [`ParseError`]: every Smithy protocol encodes timestamps as ASCII, so such a value is
91/// always malformed.
92pub fn many_dates_bytes<'a>(
93    values: impl Iterator<Item = &'a [u8]>,
94    format: Format,
95) -> Result<Vec<DateTime>, ParseError> {
96    let mut out = vec![];
97    for header in values {
98        let mut header = str_from_utf8(header)?;
99        while !header.is_empty() {
100            let (v, next) = DateTime::read(header, format, ',').map_err(|err| {
101                ParseError::new(format!("header could not be parsed as date: {err}"))
102            })?;
103            out.push(v);
104            header = next;
105        }
106    }
107    Ok(out)
108}
109
110/// Returns an iterator over pairs where the first element is the unprefixed header name that
111/// starts with the input `key` prefix, and the second element is the full header name.
112///
113/// `header_names` must be normalized (lowercase) names, as HTTP libraries produce them. A name
114/// matches when it starts with `key` lowercased, so a modeled prefix such as `X-Meta-` matches
115/// the wire name `x-meta-foo`.
116///
117/// This is done without allocating a lowercased copy of `key`, because it runs per prefix-bound
118/// member on every response. A prefix with no uppercase letters is already its own lowercase
119/// form, so a plain byte comparison is the whole answer; only an uppercase prefix pays for
120/// lowercasing each byte as it compares.
121pub fn headers_for_prefix<'a>(
122    header_names: impl Iterator<Item = &'a str>,
123    key: &'a str,
124) -> impl Iterator<Item = (&'a str, &'a str)> {
125    let prefix = key.as_bytes();
126    let has_uppercase = prefix.iter().any(u8::is_ascii_uppercase);
127    header_names
128        .filter(move |k| {
129            let Some(head) = k.as_bytes().get(..prefix.len()) else {
130                return false;
131            };
132            if has_uppercase {
133                head.iter()
134                    .zip(prefix)
135                    .all(|(h, p)| *h == p.to_ascii_lowercase())
136            } else {
137                head == prefix
138            }
139        })
140        // Splitting at `key.len()` is a character boundary: those bytes just compared equal
141        // (ignoring ASCII case) to `key`'s bytes, so they agree with `key`'s boundaries.
142        .map(move |k| (&k[key.len()..], k))
143}
144
145/// Convert a `HeaderValue` into a `Vec<T>` where `T: FromStr`
146pub fn read_many_from_str<'a, T: FromStr>(
147    values: impl Iterator<Item = &'a str>,
148) -> Result<Vec<T>, ParseError>
149where
150    T::Err: Error + Send + Sync + 'static,
151{
152    read_many_from_str_bytes(values.map(str::as_bytes))
153}
154
155/// Convert raw header values into a `Vec<T>` where `T: FromStr`
156///
157/// Like [`read_many_from_str`], but accepts raw bytes. A value that is not valid UTF-8
158/// produces a [`ParseError`].
159pub fn read_many_from_str_bytes<'a, T: FromStr>(
160    values: impl Iterator<Item = &'a [u8]>,
161) -> Result<Vec<T>, ParseError>
162where
163    T::Err: Error + Send + Sync + 'static,
164{
165    read_many_bytes(values, |v: &str| {
166        v.parse().map_err(|err| {
167            ParseError::new("failed during `FromString` conversion").with_source(err)
168        })
169    })
170}
171
172/// Convert a `HeaderValue` into a `Vec<T>` where `T: Parse`
173pub fn read_many_primitive<'a, T: Parse>(
174    values: impl Iterator<Item = &'a str>,
175) -> Result<Vec<T>, ParseError> {
176    read_many_primitive_bytes(values.map(str::as_bytes))
177}
178
179/// Convert raw header values into a `Vec<T>` where `T: Parse`
180///
181/// Like [`read_many_primitive`], but accepts raw bytes. A value that is not valid UTF-8
182/// produces a [`ParseError`]: Smithy primitives are ASCII, so such a value is always
183/// malformed.
184pub fn read_many_primitive_bytes<'a, T: Parse>(
185    values: impl Iterator<Item = &'a [u8]>,
186) -> Result<Vec<T>, ParseError> {
187    read_many_bytes(values, |v: &str| {
188        T::parse_smithy_primitive(v)
189            .map_err(|err| ParseError::new("failed reading a list of primitives").with_source(err))
190    })
191}
192
193/// Read many comma / header delimited values from raw HTTP header bytes
194fn read_many_bytes<'a, T>(
195    values: impl Iterator<Item = &'a [u8]>,
196    f: impl Fn(&str) -> Result<T, ParseError>,
197) -> Result<Vec<T>, ParseError> {
198    let mut out = vec![];
199    for header in values {
200        let mut header = header;
201        while !header.is_empty() {
202            let (v, next) = read_one(header, &f)?;
203            out.push(v);
204            header = next;
205        }
206    }
207    Ok(out)
208}
209
210/// Read comma / header delimited values, keeping only the first, and require that the
211/// values held at most one item.
212///
213/// This is the cardinality rule a scalar (non-list) member bound to a header follows. It
214/// exists separately from [`read_many_bytes`] so a scalar read does not allocate a `Vec`
215/// that is immediately discarded.
216///
217/// Every token is still parsed, so a malformed later value reports its own parse error
218/// rather than being masked by the cardinality error — matching the behavior of parsing
219/// into a `Vec` and then checking its length.
220fn one_of_many_bytes<'a, T>(
221    values: impl Iterator<Item = &'a [u8]>,
222    f: impl Fn(&str) -> Result<T, ParseError>,
223) -> Result<Option<T>, ParseError> {
224    let mut first = None;
225    let mut count = 0usize;
226    for header in values {
227        let mut header = header;
228        while !header.is_empty() {
229            let (value, next) = read_one(header, &f)?;
230            count += 1;
231            if first.is_none() {
232                first = Some(value);
233            }
234            header = next;
235        }
236    }
237    if count > 1 {
238        return Err(ParseError::new(format!(
239            "expected one item but found {count}"
240        )));
241    }
242    Ok(first)
243}
244
245/// Read a single comma-delimited primitive from raw header values
246///
247/// This is the scalar counterpart to [`read_many_primitive_bytes`]: it applies the same
248/// tokenizing and parsing but returns at most one value and fails if the header carried
249/// more than one. `Ok(None)` means the values produced no tokens at all, which the
250/// generated legacy path reports as an absent member.
251pub fn one_primitive_or_none_bytes<'a, T: Parse>(
252    values: impl Iterator<Item = &'a [u8]>,
253) -> Result<Option<T>, ParseError> {
254    one_of_many_bytes(values, |v: &str| {
255        T::parse_smithy_primitive(v)
256            .map_err(|err| ParseError::new("failed reading a list of primitives").with_source(err))
257    })
258}
259
260/// Read a single comma-delimited `FromStr` value from raw header values
261///
262/// This is the scalar counterpart to [`read_many_from_str_bytes`]. See
263/// [`one_primitive_or_none_bytes`] for the cardinality and `Ok(None)` semantics.
264pub fn one_from_str_or_none_bytes<'a, T: FromStr>(
265    values: impl Iterator<Item = &'a [u8]>,
266) -> Result<Option<T>, ParseError>
267where
268    T::Err: Error + Send + Sync + 'static,
269{
270    one_of_many_bytes(values, |v: &str| {
271        v.parse().map_err(|err| {
272            ParseError::new("failed during `FromString` conversion").with_source(err)
273        })
274    })
275}
276
277/// Read a single date from raw header values according to `format`
278///
279/// This is the scalar counterpart to [`many_dates_bytes`], and like it uses
280/// [`DateTime::read`] so an HTTP-date containing a comma is not mistaken for two values.
281/// See [`one_primitive_or_none_bytes`] for the cardinality and `Ok(None)` semantics.
282pub fn one_date_or_none_bytes<'a>(
283    values: impl Iterator<Item = &'a [u8]>,
284    format: Format,
285) -> Result<Option<DateTime>, ParseError> {
286    let mut first = None;
287    let mut count = 0usize;
288    for header in values {
289        let mut header = str_from_utf8(header)?;
290        while !header.is_empty() {
291            let (value, next) = DateTime::read(header, format, ',').map_err(|err| {
292                ParseError::new(format!("header could not be parsed as date: {err}"))
293            })?;
294            count += 1;
295            if first.is_none() {
296                first = Some(value);
297            }
298            header = next;
299        }
300    }
301    if count > 1 {
302        return Err(ParseError::new(format!(
303            "expected one item but found {count}"
304        )));
305    }
306    Ok(first)
307}
308
309/// Conditionally quotes and escapes a header value if the header value contains a comma or quote.
310pub fn quote_header_value<'a>(value: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
311    let value = value.into();
312    if value.trim().len() != value.len()
313        || value.contains('"')
314        || value.contains(',')
315        || value.contains('(')
316        || value.contains(')')
317    {
318        Cow::Owned(format!(
319            "\"{}\"",
320            value.replace('\\', "\\\\").replace('"', "\\\"")
321        ))
322    } else {
323        value
324    }
325}
326
327/// Read exactly one or none from a headers iterator
328///
329/// This function does not perform comma splitting like [`read_many_from_str`]
330pub fn one_or_none<'a, T: FromStr>(
331    values: impl Iterator<Item = &'a str>,
332) -> Result<Option<T>, ParseError>
333where
334    T::Err: Error + Send + Sync + 'static,
335{
336    one_or_none_bytes(values.map(str::as_bytes))
337}
338
339/// Read exactly one or none from a raw header bytes iterator
340///
341/// Like [`one_or_none`], but accepts raw bytes. A value that is not valid UTF-8 produces a
342/// [`ParseError`].
343///
344/// This function does not perform comma splitting like [`read_many_from_str_bytes`].
345pub fn one_or_none_bytes<'a, T: FromStr>(
346    mut values: impl Iterator<Item = &'a [u8]>,
347) -> Result<Option<T>, ParseError>
348where
349    T::Err: Error + Send + Sync + 'static,
350{
351    let first = match values.next() {
352        Some(v) => v,
353        None => return Ok(None),
354    };
355    match values.next() {
356        // Checked before the UTF-8 conversion so that the "multiple values" error keeps
357        // precedence, matching `one_or_none`.
358        None => T::from_str(str_from_utf8(first)?.trim())
359            .map_err(|err| ParseError::new("failed to parse string").with_source(err))
360            .map(Some),
361        Some(_) => Err(ParseError::new(
362            "expected a single value but found multiple",
363        )),
364    }
365}
366
367/// Functions for parsing multiple comma-delimited header values out of a
368/// single header. This parsing adheres to
369/// [RFC-7230's specification of header values](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6).
370mod parse_multi_header {
371    use super::ParseError;
372    use std::borrow::Cow;
373
374    fn trim(s: Cow<'_, str>) -> Cow<'_, str> {
375        match s {
376            Cow::Owned(s) => Cow::Owned(s.trim().into()),
377            Cow::Borrowed(s) => Cow::Borrowed(s.trim()),
378        }
379    }
380
381    fn replace<'a>(value: Cow<'a, str>, pattern: &str, replacement: &str) -> Cow<'a, str> {
382        if value.contains(pattern) {
383            Cow::Owned(value.replace(pattern, replacement))
384        } else {
385            value
386        }
387    }
388
389    /// Reads a single value out of the given input, and returns a tuple containing
390    /// the parsed value and the remainder of the slice that can be used to parse
391    /// more values.
392    pub(crate) fn read_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> {
393        for (index, &byte) in input.iter().enumerate() {
394            let current_slice = &input[index..];
395            match byte {
396                b' ' | b'\t' => { /* skip whitespace */ }
397                b'"' => return read_quoted_value(&current_slice[1..]),
398                _ => {
399                    let (value, rest) = read_unquoted_value(current_slice)?;
400                    return Ok((trim(value), rest));
401                }
402            }
403        }
404
405        // We only end up here if the entire header value was whitespace or empty
406        Ok((Cow::Borrowed(""), &[]))
407    }
408
409    fn read_unquoted_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> {
410        let next_delim = input.iter().position(|&b| b == b',').unwrap_or(input.len());
411        let (first, next) = input.split_at(next_delim);
412        let first =
413            std::str::from_utf8(first).map_err(|_| ParseError::new(super::NON_UTF8_HEADER))?;
414        Ok((Cow::Borrowed(first), then_comma(next).unwrap()))
415    }
416
417    /// Reads a header value that is surrounded by quotation marks and may have escaped
418    /// quotes inside of it.
419    fn read_quoted_value(input: &[u8]) -> Result<(Cow<'_, str>, &[u8]), ParseError> {
420        for index in 0..input.len() {
421            match input[index] {
422                b'"' if index == 0 || input[index - 1] != b'\\' => {
423                    let mut inner = Cow::Borrowed(
424                        std::str::from_utf8(&input[0..index])
425                            .map_err(|_| ParseError::new(super::NON_UTF8_HEADER))?,
426                    );
427                    inner = replace(inner, "\\\"", "\"");
428                    inner = replace(inner, "\\\\", "\\");
429                    let rest = then_comma(&input[(index + 1)..])?;
430                    return Ok((inner, rest));
431                }
432                _ => {}
433            }
434        }
435        Err(ParseError::new(
436            "header value had quoted value without end quote",
437        ))
438    }
439
440    fn then_comma(s: &[u8]) -> Result<&[u8], ParseError> {
441        if s.is_empty() {
442            Ok(s)
443        } else if s.starts_with(b",") {
444            Ok(&s[1..])
445        } else {
446            Err(ParseError::new("expected delimiter `,`"))
447        }
448    }
449}
450
451/// Read one comma delimited value for `FromStr` types
452fn read_one<'a, T>(
453    s: &'a [u8],
454    f: &impl Fn(&str) -> Result<T, ParseError>,
455) -> Result<(T, &'a [u8]), ParseError> {
456    let (value, rest) = parse_multi_header::read_value(s)?;
457    Ok((f(&value)?, rest))
458}
459
460#[cfg(test)]
461mod test {
462    //! These exercise the parsers directly through iterators. `aws-smithy-http` keeps its own
463    //! copy of the equivalent HTTP-typed tests, which additionally proves that the re-exports
464    //! from `aws_smithy_http::header` still behave identically.
465
466    use super::{
467        headers_for_prefix, many_dates, many_dates_bytes, one_date_or_none_bytes,
468        one_from_str_or_none_bytes, one_or_none, one_or_none_bytes, one_primitive_or_none_bytes,
469        read_many_from_str, read_many_from_str_bytes, read_many_primitive,
470        read_many_primitive_bytes, ParseError,
471    };
472    use aws_smithy_types::error::display::DisplayErrorContext;
473    use aws_smithy_types::{date_time::Format, DateTime};
474    use std::collections::HashMap;
475
476    #[test]
477    fn parse_floats() {
478        assert_eq!(
479            read_many_primitive::<f32>(["0.0,Infinity,-Infinity,5555.5"].into_iter())
480                .expect("valid"),
481            vec![0.0, f32::INFINITY, f32::NEG_INFINITY, 5555.5]
482        );
483        let message = format!(
484            "{}",
485            DisplayErrorContext(
486                read_many_primitive::<f32>(["notafloat"].into_iter()).expect_err("invalid")
487            )
488        );
489        let expected = "output failed to parse in headers: failed reading a list of primitives: failed to parse input as f32";
490        assert!(
491            message.starts_with(expected),
492            "expected '{message}' to start with '{expected}'"
493        );
494    }
495
496    #[test]
497    fn test_many_dates() {
498        assert_eq!(
499            many_dates([""].into_iter(), Format::DateTime).expect("valid"),
500            Vec::<DateTime>::new()
501        );
502        assert_eq!(
503            many_dates(
504                ["Wed, 21 Oct 2015 07:28:00 GMT"].into_iter(),
505                Format::HttpDate
506            )
507            .expect("valid"),
508            vec![DateTime::from_secs_and_nanos(1445412480, 0)]
509        );
510        assert_eq!(
511            many_dates(
512                ["Wed, 21 Oct 2015 07:28:00 GMT,Thu, 22 Oct 2015 07:28:00 GMT"].into_iter(),
513                Format::HttpDate
514            )
515            .expect("valid"),
516            vec![
517                DateTime::from_secs_and_nanos(1445412480, 0),
518                DateTime::from_secs_and_nanos(1445498880, 0)
519            ]
520        );
521        assert_eq!(
522            many_dates(["1234.5678"].into_iter(), Format::EpochSeconds).expect("valid"),
523            vec![DateTime::from_secs_and_nanos(1234, 567_800_000)]
524        );
525        assert_eq!(
526            many_dates(["1234.5678,9012.3456"].into_iter(), Format::EpochSeconds).expect("valid"),
527            vec![
528                DateTime::from_secs_and_nanos(1234, 567_800_000),
529                DateTime::from_secs_and_nanos(9012, 345_600_000)
530            ]
531        );
532    }
533
534    // A lone 0xE9 is a valid HTTP header octet (obs-text per RFC 7230) but is not valid UTF-8.
535    const NON_UTF8_VALUE: &[u8] = b"value-\xe9";
536
537    #[test]
538    fn bytes_helpers_agree_with_str_helpers_on_valid_utf8() {
539        assert_eq!(
540            one_or_none::<String>(["  foo  "].into_iter()).unwrap(),
541            one_or_none_bytes::<String>([b"  foo  ".as_slice()].into_iter()).unwrap(),
542        );
543        assert_eq!(
544            read_many_from_str::<String>(["\"foo,bar\",baz"].into_iter()).unwrap(),
545            read_many_from_str_bytes::<String>([b"\"foo,bar\",baz".as_slice()].into_iter())
546                .unwrap(),
547        );
548        assert_eq!(
549            read_many_primitive::<i16>(["1,2", "3"].into_iter()).unwrap(),
550            read_many_primitive_bytes::<i16>([b"1,2".as_slice(), b"3".as_slice()].into_iter())
551                .unwrap(),
552        );
553        assert_eq!(
554            many_dates(
555                ["Mon, 16 Dec 2019 23:48:18 GMT"].into_iter(),
556                Format::HttpDate
557            )
558            .unwrap(),
559            many_dates_bytes(
560                [b"Mon, 16 Dec 2019 23:48:18 GMT".as_slice()].into_iter(),
561                Format::HttpDate
562            )
563            .unwrap(),
564        );
565    }
566
567    #[test]
568    fn bytes_helpers_reject_non_utf8() {
569        let expected = "header was not valid utf-8";
570
571        let err = one_or_none_bytes::<String>([NON_UTF8_VALUE].into_iter()).expect_err("non-utf8");
572        assert!(err.to_string().contains(expected), "{err}");
573
574        let err =
575            read_many_from_str_bytes::<String>([NON_UTF8_VALUE].into_iter()).expect_err("non-utf8");
576        assert!(err.to_string().contains(expected), "{err}");
577
578        let err =
579            read_many_primitive_bytes::<i16>([NON_UTF8_VALUE].into_iter()).expect_err("non-utf8");
580        assert!(err.to_string().contains(expected), "{err}");
581
582        let err =
583            many_dates_bytes([NON_UTF8_VALUE].into_iter(), Format::HttpDate).expect_err("non-utf8");
584        assert!(err.to_string().contains(expected), "{err}");
585    }
586
587    #[test]
588    fn one_or_none_bytes_reports_multiple_before_non_utf8() {
589        // `one_or_none` checks for multiple values before inspecting the first one; the bytes
590        // variant must keep that precedence.
591        let err = one_or_none_bytes::<String>([NON_UTF8_VALUE, b"second".as_slice()].into_iter())
592            .expect_err("multiple values");
593        assert!(
594            err.to_string()
595                .contains("expected a single value but found multiple"),
596            "{err}"
597        );
598    }
599
600    #[test]
601    fn read_many_strings() {
602        let read = |value: &str| read_many_from_str::<String>([value].into_iter());
603        let read_valid = |value: &str| read(value).expect("valid");
604        assert_eq!(read_valid(""), Vec::<String>::new());
605        assert_eq!(read_valid("  foo"), vec!["foo"]);
606        assert_eq!(read_valid("foo   "), vec!["foo"]);
607        assert_eq!(read_valid("\"  foo  \""), vec!["  foo  "]);
608        assert_eq!(read_valid("\"foo,bar\",baz"), vec!["foo,bar", "baz"]);
609        assert_eq!(read_valid("\"foo,bar\",baz  "), vec!["foo,bar", "baz"]);
610        assert_eq!(
611            read_valid("\"foo\\\",bar\",\"\\\"asdf\\\"\",baz"),
612            vec!["foo\",bar", "\"asdf\"", "baz"]
613        );
614        assert_eq!(
615            read_valid("\"foo\\\",bar\", \"\\\"asdf\\\"\", baz"),
616            vec!["foo\",bar", "\"asdf\"", "baz"]
617        );
618        assert!(read("\"\\\"asdf\\\"\"baz").is_err());
619        assert_eq!(read_valid("\"\",baz"), vec!["", "baz"]);
620        assert_eq!(
621            read_valid("foo, \"(foo\\\\bar)\""),
622            vec!["foo", "(foo\\bar)"]
623        );
624    }
625
626    #[test]
627    fn read_many_bools() {
628        assert_eq!(
629            read_many_primitive::<bool>(["true,false", "true"].into_iter()).expect("valid"),
630            vec![true, false, true]
631        );
632        assert_eq!(
633            read_many_primitive::<bool>(["true"].into_iter()).unwrap(),
634            vec![true]
635        );
636        assert_eq!(
637            read_many_primitive::<bool>(["true,false,true,true"].into_iter()).unwrap(),
638            vec![true, false, true, true]
639        );
640        assert_eq!(
641            read_many_primitive::<bool>(["true,\"false\",true,true"].into_iter()).unwrap(),
642            vec![true, false, true, true]
643        );
644        read_many_primitive::<bool>(["truth,falsy"].into_iter()).expect_err("invalid");
645    }
646
647    #[test]
648    fn check_read_many_i16() {
649        assert_eq!(
650            read_many_primitive::<i16>(["123,456", "789"].into_iter()).expect("valid"),
651            vec![123, 456, 789]
652        );
653        assert_eq!(
654            read_many_primitive::<i16>(["777"].into_iter()).unwrap(),
655            vec![777]
656        );
657        assert_eq!(
658            read_many_primitive::<i16>(["1,2,3,-4,5"].into_iter()).unwrap(),
659            vec![1, 2, 3, -4, 5]
660        );
661        assert_eq!(
662            read_many_primitive::<i16>(["1, \"2\",3,\"-4\",5"].into_iter()).unwrap(),
663            vec![1, 2, 3, -4, 5]
664        );
665        read_many_primitive::<i16>(["12ef3"].into_iter()).expect_err("invalid");
666    }
667
668    #[test]
669    fn test_prefix_headers() {
670        // Normalized (lowercased) wire names, as `Headers::iter` yields them, matched against a
671        // mixed-case modeled prefix.
672        let names = ["x-prefix-a", "x-prefix-b", "x-prefix-c", "other"];
673        let values: HashMap<&str, Vec<&str>> = [
674            ("x-prefix-a", vec!["123,456"]),
675            ("x-prefix-b", vec!["789"]),
676            ("x-prefix-c", vec!["777", "777"]),
677            ("other", vec!["1"]),
678        ]
679        .into_iter()
680        .collect();
681        let resp: Result<HashMap<String, Vec<i16>>, ParseError> =
682            headers_for_prefix(names.into_iter(), "x-prefix-")
683                .map(|(key, header_name)| {
684                    read_many_primitive(values[header_name].iter().copied())
685                        .map(|v| (key.to_string(), v))
686                })
687                .collect();
688        let resp = resp.expect("valid");
689        assert_eq!(resp.get("a"), Some(&vec![123_i16, 456_i16]));
690        assert_eq!(resp.get("b"), Some(&vec![789_i16]));
691        assert_eq!(resp.get("c"), Some(&vec![777_i16, 777_i16]));
692        assert_eq!(resp.get("other"), None);
693    }
694
695    #[test]
696    fn prefix_matching_lowercases_the_modeled_prefix() {
697        let names = ["x-meta-foo", "x-meta-bar"];
698        let matched: Vec<_> = headers_for_prefix(names.into_iter(), "X-Meta-").collect();
699        assert_eq!(
700            matched,
701            vec![("foo", "x-meta-foo"), ("bar", "x-meta-bar")],
702            "a mixed-case modeled prefix must match normalized wire names"
703        );
704    }
705
706    #[test]
707    fn every_spelling_of_a_prefix_matches_the_same_normalized_names() {
708        // Names arrive normalized to lowercase, so the prefix is matched as if lowercased —
709        // the behavior of the original `to_ascii_lowercase` + `starts_with`, without the
710        // allocation. A shorter name must not panic on the split.
711        for prefix in ["X-Meta-", "x-meta-", "X-META-", "x-Meta-"] {
712            let names = ["x-meta-foo", "x-meta-bar", "x-other", "x-me", "x-metadata"];
713            let matched: Vec<_> = headers_for_prefix(names.into_iter(), prefix).collect();
714            assert_eq!(
715                matched,
716                vec![("foo", "x-meta-foo"), ("bar", "x-meta-bar")],
717                "prefix {prefix:?} must match the same names"
718            );
719        }
720    }
721
722    #[test]
723    fn a_prefix_with_no_letters_matches_exactly() {
724        let names = ["1-2-a", "1-2-", "1-3-a"];
725        let matched: Vec<_> = headers_for_prefix(names.into_iter(), "1-2-").collect();
726        assert_eq!(matched, vec![("a", "1-2-a"), ("", "1-2-")]);
727    }
728
729    #[test]
730    fn a_prefix_matches_the_whole_name_and_yields_an_empty_key() {
731        let names = ["x-meta-"];
732        let matched: Vec<_> = headers_for_prefix(names.into_iter(), "x-meta-").collect();
733        assert_eq!(matched, vec![("", "x-meta-")]);
734    }
735
736    /// The scalar helpers must agree with "parse into a `Vec`, then require `len() <= 1`",
737    /// which is what the generated legacy header path does. They exist only to avoid the
738    /// discarded `Vec`, not to change any outcome.
739    #[test]
740    fn scalar_helpers_agree_with_the_many_value_helpers() {
741        let cases: &[&[&[u8]]] = &[
742            &[],
743            &[b""],
744            &[b"", b""],
745            &[b"5"],
746            &[b" 5 "],
747            &[b"5", b""],
748            &[b"5,6"],
749            &[b"5", b"6"],
750            &[b"notanint"],
751            &[b"5,notanint"],
752        ];
753        for values in cases {
754            let many = read_many_primitive_bytes::<i32>(values.iter().copied());
755            let one = one_primitive_or_none_bytes::<i32>(values.iter().copied());
756            match many {
757                Ok(mut parsed) if parsed.len() <= 1 => assert_eq!(
758                    one.as_ref().ok().and_then(|v| v.as_ref()),
759                    parsed.pop().as_ref(),
760                    "values {values:?} must produce the same single value"
761                ),
762                Ok(parsed) => {
763                    let message = format!(
764                        "{}",
765                        DisplayErrorContext(&one.expect_err("cardinality must fail"))
766                    );
767                    assert!(
768                        message.contains(&format!("expected one item but found {}", parsed.len())),
769                        "values {values:?} must report the legacy cardinality message, got '{message}'"
770                    );
771                }
772                // A parse error must stay a parse error and keep precedence over cardinality.
773                Err(expected) => {
774                    let expected = format!("{}", DisplayErrorContext(&expected));
775                    let actual = format!(
776                        "{}",
777                        DisplayErrorContext(&one.expect_err("parse error must propagate"))
778                    );
779                    assert_eq!(
780                        expected, actual,
781                        "values {values:?} must report the same error"
782                    );
783                }
784            }
785        }
786    }
787
788    #[test]
789    fn scalar_from_str_helper_agrees_with_the_many_value_helper() {
790        assert_eq!(
791            one_from_str_or_none_bytes::<String>([b"a".as_slice()].into_iter()).expect("valid"),
792            Some("a".to_string())
793        );
794        // Unlike `one_or_none_bytes`, this path splits on commas, so a comma-bearing value
795        // is two items and therefore a cardinality error.
796        let err = one_from_str_or_none_bytes::<String>([b"a,b".as_slice()].into_iter())
797            .expect_err("two items");
798        assert!(
799            format!("{}", DisplayErrorContext(&err)).contains("expected one item but found 2"),
800            "expected a cardinality error"
801        );
802        assert_eq!(
803            one_from_str_or_none_bytes::<String>([b"".as_slice()].into_iter()).expect("valid"),
804            None,
805            "an empty value produces no tokens, which the legacy path reports as absent"
806        );
807    }
808
809    #[test]
810    fn scalar_date_helper_agrees_with_many_dates() {
811        // The comma inside an HTTP-date must not be read as a value delimiter; this is why
812        // dates need their own helper rather than the comma tokenizer.
813        let http_date = b"Wed, 21 Oct 2015 07:28:00 GMT".as_slice();
814        assert_eq!(
815            one_date_or_none_bytes([http_date].into_iter(), Format::HttpDate).expect("valid"),
816            Some(many_dates_bytes([http_date].into_iter(), Format::HttpDate).expect("valid")[0])
817        );
818        assert_eq!(
819            one_date_or_none_bytes([b"".as_slice()].into_iter(), Format::HttpDate).expect("valid"),
820            None
821        );
822        let two = b"Wed, 21 Oct 2015 07:28:00 GMT,Wed, 21 Oct 2015 07:28:01 GMT".as_slice();
823        assert_eq!(
824            many_dates_bytes([two].into_iter(), Format::HttpDate)
825                .expect("valid")
826                .len(),
827            2,
828            "precondition: this value really does hold two dates"
829        );
830        let err =
831            one_date_or_none_bytes([two].into_iter(), Format::HttpDate).expect_err("two items");
832        assert!(
833            format!("{}", DisplayErrorContext(&err)).contains("expected one item but found 2"),
834            "expected a cardinality error"
835        );
836    }
837
838    #[test]
839    fn scalar_helpers_reject_non_utf8() {
840        let non_utf8 = b"value-\xe9".as_slice();
841        for message in [
842            format!(
843                "{}",
844                DisplayErrorContext(
845                    &one_primitive_or_none_bytes::<i32>([non_utf8].into_iter())
846                        .expect_err("not utf-8")
847                )
848            ),
849            format!(
850                "{}",
851                DisplayErrorContext(
852                    &one_from_str_or_none_bytes::<String>([non_utf8].into_iter())
853                        .expect_err("not utf-8")
854                )
855            ),
856            format!(
857                "{}",
858                DisplayErrorContext(
859                    &one_date_or_none_bytes([non_utf8].into_iter(), Format::HttpDate)
860                        .expect_err("not utf-8")
861                )
862            ),
863        ] {
864            assert!(
865                message.contains(super::NON_UTF8_HEADER),
866                "expected '{message}' to report the non-UTF-8 reason"
867            );
868        }
869    }
870}