Skip to main content

http_field/
lib.rs

1//! HTTP fields (headers & trailers).
2//!
3//! Parsing follows the generic field syntax from
4//! [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112#section-5) and the field-name /
5//! field-value rules from [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#section-5.1),
6//! [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#section-5.5), and
7//! [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2).
8
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11use core::ops::Range;
12
13use winnow::{
14    error::{ContextError, ErrMode},
15    prelude::*,
16    stream::LocatingSlice,
17};
18
19mod parsing;
20
21/// Entire HTTP field line, excluding any trailing CRLF.
22///
23/// ```plain
24/// field-line = field-name ":" OWS field-value OWS
25/// ```
26///
27/// The parsed `field-value` excludes surrounding optional whitespace.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Field<'a> {
30    inner: &'a [u8],
31    name: Range<usize>,
32    value: Range<usize>,
33}
34
35/// Field name.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FieldName<'a> {
38    slice: &'a [u8],
39}
40
41/// Field value.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct FieldValue<'a> {
44    slice: &'a [u8],
45}
46
47impl<'a> Field<'a> {
48    /// Parses an entire HTTP field line from bytes, excluding the trailing CRLF.
49    pub fn try_from_slice(input: &'a [u8]) -> Result<Self, ErrMode<ContextError>> {
50        let mut located = LocatingSlice::new(input);
51        let indices = parsing::parse_field_indices.parse_next(&mut located)?;
52
53        Ok(Self {
54            inner: input,
55            name: indices.name,
56            value: indices.value,
57        })
58    }
59
60    /// Returns the full field-line bytes.
61    #[inline]
62    pub fn as_bytes(&self) -> &'a [u8] {
63        self.inner
64    }
65
66    /// Returns the byte indices of the field name.
67    #[inline]
68    pub fn name_indices(&self) -> Range<usize> {
69        self.name.clone()
70    }
71
72    /// Returns the parsed field name.
73    #[inline]
74    pub fn name(&self) -> FieldName<'a> {
75        FieldName {
76            slice: slice_range(self.inner, &self.name),
77        }
78    }
79
80    /// Returns the byte indices of the trimmed field value.
81    #[inline]
82    pub fn value_indices(&self) -> Range<usize> {
83        self.value.clone()
84    }
85
86    /// Returns the parsed field value.
87    #[inline]
88    pub fn value(&self) -> FieldValue<'a> {
89        FieldValue {
90            slice: slice_range(self.inner, &self.value),
91        }
92    }
93}
94
95impl<'a> FieldName<'a> {
96    /// Constructs field name from bytes.
97    #[inline]
98    pub fn from_slice(slice: &'a [u8]) -> Self {
99        Self { slice }
100    }
101
102    /// Parses a field name from bytes.
103    pub fn try_from_slice(
104        slice: &'a [u8],
105    ) -> Result<Self, winnow::error::ParseError<&'a [u8], ContextError>> {
106        parsing::parse_field_name.parse(slice)?;
107
108        Ok(Self { slice })
109    }
110
111    /// Returns field name as bytes.
112    #[inline]
113    pub fn as_slice(&self) -> &'a [u8] {
114        self.slice
115    }
116}
117
118impl<'a> FieldValue<'a> {
119    /// Constructs field value from bytes.
120    #[inline]
121    pub fn from_slice(slice: &'a [u8]) -> Self {
122        Self { slice }
123    }
124
125    /// Parses a field value from bytes.
126    pub fn try_from_slice(
127        slice: &'a [u8],
128    ) -> Result<Self, winnow::error::ParseError<&'a [u8], ContextError>> {
129        parsing::parse_field_value.parse(slice)?;
130
131        Ok(Self { slice })
132    }
133
134    /// Returns field value as bytes.
135    #[inline]
136    pub fn as_slice(&self) -> &'a [u8] {
137        self.slice
138    }
139}
140
141#[inline]
142fn slice_range<'a>(bytes: &'a [u8], range: &Range<usize>) -> &'a [u8] {
143    &bytes[range.start..range.end]
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn parses_field_line() {
152        let field = Field::try_from_slice(b"content-type: text/plain; charset=utf-8").unwrap();
153
154        assert_eq!(field.as_bytes(), b"content-type: text/plain; charset=utf-8");
155        assert_eq!(field.name_indices(), 0..12);
156        assert_eq!(field.name().as_slice(), b"content-type");
157        assert_eq!(field.value_indices(), 14..39);
158        assert_eq!(field.value().as_slice(), b"text/plain; charset=utf-8");
159    }
160
161    #[test]
162    fn trims_optional_whitespace_around_value() {
163        let field = Field::try_from_slice(b"accept:\t application/json \t").unwrap();
164
165        assert_eq!(field.name().as_slice(), b"accept");
166        assert_eq!(field.value().as_slice(), b"application/json");
167    }
168
169    #[test]
170    fn allows_empty_field_value() {
171        let field = Field::try_from_slice(b"x-empty:\t ").unwrap();
172
173        assert_eq!(field.value().as_slice(), b"");
174        assert_eq!(field.value_indices(), 10..10);
175    }
176
177    #[test]
178    fn validates_name_and_value_components() {
179        assert!(FieldName::try_from_slice(b"content-type").is_ok());
180        assert!(FieldName::try_from_slice(b"content type").is_err());
181
182        assert!(FieldValue::try_from_slice(b"text/plain; charset=utf-8").is_ok());
183        assert!(FieldValue::try_from_slice(b"text/plain\r").is_err());
184    }
185}