#![cfg_attr(docsrs, feature(doc_cfg))]
use core::ops::Range;
use winnow::{
error::{ContextError, ErrMode},
prelude::*,
stream::LocatingSlice,
};
mod parsing;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field<'a> {
inner: &'a [u8],
name: Range<usize>,
value: Range<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldName<'a> {
slice: &'a [u8],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldValue<'a> {
slice: &'a [u8],
}
impl<'a> Field<'a> {
pub fn try_from_slice(input: &'a [u8]) -> Result<Self, ErrMode<ContextError>> {
let mut located = LocatingSlice::new(input);
let indices = parsing::parse_field_indices.parse_next(&mut located)?;
Ok(Self {
inner: input,
name: indices.name,
value: indices.value,
})
}
#[inline]
pub fn as_bytes(&self) -> &'a [u8] {
self.inner
}
#[inline]
pub fn name_indices(&self) -> Range<usize> {
self.name.clone()
}
#[inline]
pub fn name(&self) -> FieldName<'a> {
FieldName {
slice: slice_range(self.inner, &self.name),
}
}
#[inline]
pub fn value_indices(&self) -> Range<usize> {
self.value.clone()
}
#[inline]
pub fn value(&self) -> FieldValue<'a> {
FieldValue {
slice: slice_range(self.inner, &self.value),
}
}
}
impl<'a> FieldName<'a> {
#[inline]
pub fn from_slice(slice: &'a [u8]) -> Self {
Self { slice }
}
pub fn try_from_slice(
slice: &'a [u8],
) -> Result<Self, winnow::error::ParseError<&'a [u8], ContextError>> {
parsing::parse_field_name.parse(slice)?;
Ok(Self { slice })
}
#[inline]
pub fn as_slice(&self) -> &'a [u8] {
self.slice
}
}
impl<'a> FieldValue<'a> {
#[inline]
pub fn from_slice(slice: &'a [u8]) -> Self {
Self { slice }
}
pub fn try_from_slice(
slice: &'a [u8],
) -> Result<Self, winnow::error::ParseError<&'a [u8], ContextError>> {
parsing::parse_field_value.parse(slice)?;
Ok(Self { slice })
}
#[inline]
pub fn as_slice(&self) -> &'a [u8] {
self.slice
}
}
#[inline]
fn slice_range<'a>(bytes: &'a [u8], range: &Range<usize>) -> &'a [u8] {
&bytes[range.start..range.end]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_field_line() {
let field = Field::try_from_slice(b"content-type: text/plain; charset=utf-8").unwrap();
assert_eq!(field.as_bytes(), b"content-type: text/plain; charset=utf-8");
assert_eq!(field.name_indices(), 0..12);
assert_eq!(field.name().as_slice(), b"content-type");
assert_eq!(field.value_indices(), 14..39);
assert_eq!(field.value().as_slice(), b"text/plain; charset=utf-8");
}
#[test]
fn trims_optional_whitespace_around_value() {
let field = Field::try_from_slice(b"accept:\t application/json \t").unwrap();
assert_eq!(field.name().as_slice(), b"accept");
assert_eq!(field.value().as_slice(), b"application/json");
}
#[test]
fn allows_empty_field_value() {
let field = Field::try_from_slice(b"x-empty:\t ").unwrap();
assert_eq!(field.value().as_slice(), b"");
assert_eq!(field.value_indices(), 10..10);
}
#[test]
fn validates_name_and_value_components() {
assert!(FieldName::try_from_slice(b"content-type").is_ok());
assert!(FieldName::try_from_slice(b"content type").is_err());
assert!(FieldValue::try_from_slice(b"text/plain; charset=utf-8").is_ok());
assert!(FieldValue::try_from_slice(b"text/plain\r").is_err());
}
}