util: add RFC 8941 parser

This commit is contained in:
anna 2023-01-25 18:40:39 +01:00
parent a583875cd7
commit 78fbbdfedd
Signed by: fef
GPG key ID: EC22E476DC2D3D84
2 changed files with 912 additions and 0 deletions

909
src/util/header.rs Normal file
View file

@ -0,0 +1,909 @@
use bytes::Bytes;
use reqwest::header::HeaderValue;
use std::ops::RangeBounds;
use crate::core::*;
use crate::util::slice::SliceCursor;
use crate::util::transcode;
pub trait ParseHeader<'a>: Sized {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self>;
fn parse_from_header(header: &'a HeaderValue) -> Result<Self> {
Self::parse_from_ascii(header.as_bytes())
}
}
#[derive(Debug, PartialEq)]
pub struct Dictionary<'a>(Vec<(&'a str, Member<'a>)>);
#[derive(Debug, PartialEq)]
pub struct List<'a>(Vec<Member<'a>>);
#[derive(Debug, PartialEq)]
pub enum Member<'a> {
Item(Item<'a>),
InnerList(InnerList<'a>),
}
#[derive(Debug, PartialEq)]
pub struct InnerList<'a> {
items: Vec<Item<'a>>,
params: Vec<(&'a str, BareItem<'a>)>,
}
#[derive(Debug, PartialEq)]
pub struct Item<'a> {
bare_item: BareItem<'a>,
params: Vec<(&'a str, BareItem<'a>)>,
}
#[derive(Debug, PartialEq)]
pub enum BareItem<'a> {
Integer(i64),
Decimal(f32),
String(StringItem<'a>),
Token(TokenItem<'a>),
ByteSequence(ByteSequenceItem<'a>),
Boolean(bool),
}
#[derive(Debug, PartialEq)]
pub struct StringItem<'a>(&'a str);
#[derive(Debug, PartialEq)]
pub struct TokenItem<'a>(&'a str);
#[derive(Debug, PartialEq)]
pub struct ByteSequenceItem<'a>(&'a str);
impl<'a> ParseHeader<'a> for Dictionary<'a> {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
Parser::new(header)?.parse_dictionary()
}
}
impl<'a> Dictionary<'a> {
pub fn get(&self, key: &'a str) -> Option<&Member<'a>> {
self.0.iter().find_map(|(k, v)| key.eq(*k).then_some(v))
}
pub fn nth(&self, index: usize) -> Option<&(&'a str, Member<'a>)> {
self.0.get(index)
}
}
impl<'a> ParseHeader<'a> for List<'a> {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
Parser::new(header)?.parse_list()
}
}
impl<'a> List<'a> {
pub fn nth(&self, index: usize) -> Option<&Member<'a>> {
self.0.get(index)
}
pub fn iter(&self) -> impl Iterator<Item = &Member<'a>> {
self.0.iter()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl<'a> ParseHeader<'a> for Item<'a> {
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
Parser::new(header)?.parse_item()
}
}
impl<'a> Item<'a> {
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
where
K: Into<&'a str>,
{
let key = key.into();
self.params
.iter()
.find_map(|(k, v)| key.eq(*k).then_some(v))
}
pub fn has_param<K>(&self, key: K) -> bool
where
K: Into<&'a str>,
{
let key = key.into();
self.params.iter().any(|(k, _)| key.eq(*k))
}
pub fn as_integer(&self) -> Option<i64> {
self.bare_item.as_integer()
}
pub fn as_decimal(&self) -> Option<f32> {
self.bare_item.as_decimal()
}
pub fn as_string(&self) -> Option<String> {
self.bare_item.as_string()
}
pub fn as_token(&self) -> Option<&'a str> {
self.bare_item.as_token()
}
pub fn as_byte_sequence(&self) -> Option<Bytes> {
self.bare_item.as_byte_sequence()
}
pub fn as_boolean(&self) -> Option<bool> {
self.bare_item.as_boolean()
}
}
impl<'a> BareItem<'a> {
pub fn as_integer(&self) -> Option<i64> {
match self {
BareItem::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_decimal(&self) -> Option<f32> {
match self {
BareItem::Decimal(d) => Some(*d),
_ => None,
}
}
pub fn as_string(&self) -> Option<String> {
match self {
BareItem::String(s) => Some(remove_escapes_stupid(s.0)),
_ => None,
}
}
pub fn as_token(&self) -> Option<&'a str> {
match self {
BareItem::Token(t) => Some(t.0),
_ => None,
}
}
pub fn as_byte_sequence(&self) -> Option<Bytes> {
match self {
BareItem::ByteSequence(bs) => Some(bs.into()),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
BareItem::Boolean(b) => Some(*b),
_ => None,
}
}
}
impl<'a> Member<'a> {
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
where
K: Into<&'a str>,
{
match self {
Member::Item(i) => i.param(key),
Member::InnerList(l) => l.param(key),
}
}
pub fn has_param<K>(&self, key: K) -> bool
where
K: Into<&'a str>,
{
match self {
Member::Item(i) => i.has_param(key),
Member::InnerList(l) => l.has_param(key),
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
Member::Item(i) => i.as_integer(),
_ => None,
}
}
pub fn as_decimal(&self) -> Option<f32> {
match self {
Member::Item(i) => i.as_decimal(),
_ => None,
}
}
pub fn as_string(&self) -> Option<String> {
match self {
Member::Item(i) => i.as_string(),
_ => None,
}
}
pub fn as_token(&self) -> Option<&'a str> {
match self {
Member::Item(i) => i.as_token(),
_ => None,
}
}
pub fn as_byte_sequence(&self) -> Option<Bytes> {
match self {
Member::Item(i) => i.as_byte_sequence(),
_ => None,
}
}
pub fn as_boolean(&self) -> Option<bool> {
match self {
Member::Item(i) => i.as_boolean(),
_ => None,
}
}
pub fn as_list(&self) -> Option<&InnerList<'a>> {
match self {
Member::InnerList(l) => Some(l),
_ => None,
}
}
}
impl<'a> InnerList<'a> {
pub fn nth(&self, index: usize) -> Option<&Item<'a>> {
self.items.get(index)
}
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
self.items.iter()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
where
K: Into<&'a str>,
{
let key = key.into();
self.params
.iter()
.find_map(|(k, v)| key.eq(*k).then_some(v))
}
pub fn has_param<K>(&self, key: K) -> bool
where
K: Into<&'a str>,
{
let key = key.into();
self.params.iter().any(|(k, _)| key.eq(*k))
}
}
struct Parser<'a> {
cursor: SliceCursor<'a, u8>,
}
impl<'a> Parser<'a> {
fn new(data: &'a [u8]) -> Result<Parser> {
if data.is_ascii() {
Ok(Parser {
cursor: SliceCursor::new(data),
})
} else {
Err(Error::BadHeader(String::from(
"RFC 8941 prohibits non-ASCII characters",
)))
}
}
/// Parse a full List (section 3.1).
///
/// ```notrust
/// sf-list = list-member *( OWS "," OWS list-member )
/// ```
fn parse_list(&mut self) -> Result<List<'a>> {
let mut members = Vec::with_capacity(1);
members.push(self.parse_list_member()?);
self.skip_whitespace();
while self.skip_if(|c| c == b',') {
self.skip_whitespace();
members.push(self.parse_list_member()?);
// > Parsers MUST support Lists containing at least 1024 members.
if members.len() == 1024 {
break;
}
self.skip_whitespace();
}
Ok(List(members))
}
/// Parse a single list member.
///
/// ```notrust
/// list-member = sf-item / inner-list
/// ```
fn parse_list_member(&mut self) -> Result<Member<'a>> {
if self.cursor.peek().copied() == Some(b'(') {
self.parse_inner_list().map(Member::InnerList)
} else {
self.parse_item().map(Member::Item)
}
}
/// Parse a Dictionary (section 3.2).
///
/// ```notrust
/// sf-dictionary = dict-member *( OWS "," OWS dict-member )
/// ```
fn parse_dictionary(&mut self) -> Result<Dictionary<'a>> {
let mut members = Vec::with_capacity(1);
members.push(self.parse_dict_member()?);
self.skip_whitespace();
while self.skip_if(|c| c == b',') {
self.skip_whitespace();
members.push(self.parse_dict_member()?);
// > Parsers MUST support Dictionaries containing at least
// > 1024 key/value pairs and keys with at least 64 characters.
if members.len() == 1024 {
break;
}
self.skip_whitespace();
}
Ok(Dictionary(members))
}
/// Parse a Dictionary member.
///
/// ```notrust
/// dict-member = member-key ( parameters / ( "=" member-value ))
/// member-key = key
/// member-value = sf-item / inner-list
/// ```
fn parse_dict_member(&mut self) -> Result<(&'a str, Member<'a>)> {
// member-key
let key = self.parse_key()?;
let val = if self.skip_if(|c| c == b'=') {
// member-value
if self.cursor.peek().copied() == Some(b'(') {
Member::InnerList(self.parse_inner_list()?)
} else {
Member::Item(self.parse_item()?)
}
} else {
// parameters
Member::Item(Item {
bare_item: BareItem::Boolean(true),
params: self.parse_parameters()?,
})
};
Ok((key, val))
}
/// Parse an Inner List (section 3.1.1).
///
/// ```notrust
/// inner-list = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")" parameters
/// ```
fn parse_inner_list(&mut self) -> Result<InnerList<'a>> {
self.assert_next(|c| c == b'(')?;
self.skip_sp();
let mut items = Vec::new();
loop {
if self.skip_if(|c| c == b')') {
break;
}
items.push(self.parse_item()?);
// > Parsers MUST support Inner Lists containing at least 256 members.
if items.len() == 256 {
break;
}
if self.skip_sp() != 1 {
self.assert_next(|c| c == b')')?;
break;
}
}
let params = self.parse_parameters()?;
Ok(InnerList { items, params })
}
/// Parse a full Item including parameters (section 3.3).
///
/// ```notrust
/// sf-item = bare-item parameters
/// ```
fn parse_item(&mut self) -> Result<Item<'a>> {
let bare_item = self.parse_bare_item()?;
let params = self.parse_parameters()?;
Ok(Item { bare_item, params })
}
/// Parse a list of parameters (section 3.1.2).
///
/// ```notrust
/// parameters = *( ";" *SP parameter )
/// ```
fn parse_parameters(&mut self) -> Result<Vec<(&'a str, BareItem<'a>)>> {
let mut params = Vec::new();
while self.skip_if(|c| c == b';') {
self.skip_sp();
params.push(self.parse_parameter()?);
if params.len() == 256 {
break;
}
}
Ok(params)
}
/// Parse a single Parameter (section 3.1.2).
///
/// ```notrust
/// parameter = param-key [ "=" param-value ]
/// param-key = key
/// param-value = bare-item
/// ```
fn parse_parameter(&mut self) -> Result<(&'a str, BareItem<'a>)> {
let key = self.parse_key()?;
let value = if self.skip_if(|c| c == b'=') {
self.parse_bare_item()?
} else {
BareItem::Boolean(true)
};
Ok((key, value))
}
/// Parse a key for a Parameter or Dictionary.
///
/// ```notrust
/// key = ( lcalpha / "*" )
/// *( lcalpha / DIGIT / "_" / "-" / "." / "*" )
/// lcalpha = %x61-7A ; a-z
/// ```
fn parse_key(&mut self) -> Result<&'a str> {
self.chop();
self.assert_next(is_key_start)?;
self.skip_while(is_key_part);
Ok(self.chop())
}
/// Parse a bare item (section 3.3).
///
/// ```notrust
/// bare-item = sf-integer / sf-decimal / sf-string
/// / sf-token / sf-binary / sf-boolean
/// ```
fn parse_bare_item(&mut self) -> Result<BareItem<'a>> {
match self
.cursor
.peek()
.copied()
.ok_or_else(|| self.make_error("Unexpected end of header"))?
{
c if is_numeric_start(c) => self.parse_numeric(),
b'"' => self.parse_string(),
c if is_token_start(c) => self.parse_token(),
b':' => self.parse_byte_sequence(),
b'?' => self.parse_boolean(),
_ => Err(self.make_error("Unexpected character")),
}
}
/// Parse an Integer (section 3.3.1) or Decimal (section 3.3.2) item.
///
/// ```notrust
/// sf-integer = ["-"] 1*15DIGIT
/// sf-decimal = ["-"] 1*12DIGIT "." 1*3DIGIT
/// ```
fn parse_numeric(&mut self) -> Result<BareItem<'a>> {
self.chop();
self.cursor.next_if(|&c| c == b'-');
let int_digits = self.parse_digits(1..=15)?.len();
if self.cursor.next_if(|&c| c == b'.').is_some() {
if int_digits <= 12 {
self.parse_digits(1..=3)?;
Ok(BareItem::Decimal(self.chop().parse().unwrap()))
} else {
Err(self.make_error("Decimals may contain at most 12 integer digits"))
}
} else {
Ok(BareItem::Integer(self.chop().parse().unwrap()))
}
}
/// Parse a String item (section 3.3.3).
///
/// ```notrust
/// sf-string = DQUOTE *chr DQUOTE
/// chr = unescaped / escaped
/// unescaped = %x20-21 / %x23-5B / %x5D-7E
/// escaped = "\" ( DQUOTE / "\" )
/// ```
fn parse_string(&mut self) -> Result<BareItem<'a>> {
self.assert_next(|c| c == b'"')?;
self.chop();
loop {
match self.require_next()? {
b'"' => break,
b'\\' => {
self.assert_next(|c| c == b'\\' || c == b'"')?;
}
c if is_string_part(c) => continue,
_ => return Err(self.make_error("Unexpected character in string")),
}
}
let slice = self.chop();
let slice = &slice[..slice.len() - 1]; // discard the trailing "
Ok(BareItem::String(StringItem(slice)))
}
/// Parse a Token item (section 3.3.4).
///
/// ```notrust
/// sf-token = ( ALPHA / "*" ) *( tchar / ":" / "/" )
/// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
/// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
/// / DIGIT / ALPHA
/// ; any VCHAR, except delimiters
/// ```
fn parse_token(&mut self) -> Result<BareItem<'a>> {
self.chop();
self.assert_next(|c| c.is_ascii_alphabetic() || c == b'*')?;
self.skip_while(|c| is_tchar(c) || c == b':' || c == b'/');
Ok(BareItem::Token(TokenItem(self.chop())))
}
/// Parse a Byte Sequence item (section 3.3.5).
///
/// ```notrust
/// sf-binary = ":" *(base64) ":"
/// base64 = ALPHA / DIGIT / "+" / "/" / "="
/// ```
fn parse_byte_sequence(&mut self) -> Result<BareItem<'a>> {
self.assert_next(|c| c == b':')?;
self.chop();
self.skip_while(is_base64);
let slice = self.chop();
self.assert_next(|c| c == b':')?;
Ok(BareItem::ByteSequence(ByteSequenceItem(slice)))
}
/// Parse a Boolean item (section 3.3.6).
///
/// ```notrust
/// sf-boolean = "?" boolean
/// boolean = "0" / "1"
/// ```
fn parse_boolean(&mut self) -> Result<BareItem<'a>> {
self.assert_next(|c| c == b'?')?;
self.chop();
let b = self.assert_next(|c| b"01".contains(&c))? == b'1';
self.chop();
Ok(BareItem::Boolean(b))
}
fn parse_digits(&mut self, amount: impl RangeBounds<usize>) -> Result<&[u8]> {
let slice = self.cursor.next_while(|c| c.is_ascii_digit());
if amount.contains(&slice.len()) {
Ok(slice)
} else {
Err(self.make_error("Number out of permissible range"))
}
}
fn skip_sp(&mut self) -> usize {
self.cursor.next_while(|&c| c == b' ').len()
}
fn skip_whitespace(&mut self) -> usize {
self.cursor.next_while(|&c| c == b' ' || c == b'\t').len()
}
fn assert_next<F>(&mut self, predicate: F) -> Result<u8>
where
F: FnOnce(u8) -> bool,
{
let c = self.require_next()?;
if predicate(c) {
Ok(c)
} else {
Err(self.make_error(format!("Unexpected token {:?}", c as char)))
}
}
fn skip_if<F>(&mut self, predicate: F) -> bool
where
F: FnOnce(u8) -> bool,
{
self.cursor.next_if(|&c| predicate(c)).is_some()
}
fn skip_while<F>(&mut self, mut predicate: F) -> usize
where
F: FnMut(u8) -> bool,
{
self.cursor.next_while(|&c| predicate(c)).len()
}
fn require_next(&mut self) -> Result<u8> {
self.cursor
.next()
.copied()
.ok_or_else(|| self.make_error("Unexpected end of header"))
}
fn chop(&mut self) -> &'a str {
let bytes = self.cursor.chop();
// SAFETY: The parser validates the string when constructed
unsafe { std::str::from_utf8_unchecked(bytes) }
}
fn make_error<S>(&self, msg: S) -> Error
where
S: Into<String>,
{
Error::BadHeader(msg.into())
}
}
fn is_numeric_start(c: u8) -> bool {
c.is_ascii_digit() || c == b'-'
}
fn is_string_start(c: u8) -> bool {
c == b'"'
}
fn is_string_part(c: u8) -> bool {
(b'\x20'..=b'\x21').contains(&c)
|| (b'\x23'..=b'\x5b').contains(&c)
|| (b'\x5d'..=b'\x7e').contains(&c)
}
fn is_token_start(c: u8) -> bool {
c.is_ascii_alphabetic() || c == b'*'
}
fn is_tchar(c: u8) -> bool {
c.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&c)
}
fn is_byte_sequence_start(c: u8) -> bool {
c == b':'
}
fn is_base64(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'+' || c == b'/' || c == b'='
}
fn is_key_start(c: u8) -> bool {
c.is_ascii_lowercase() || c == b'*'
}
fn is_key_part(c: u8) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || b"_-.*".contains(&c)
}
fn remove_escapes_stupid(s: &str) -> String {
let mut had_escape = false;
let utf8 = s
.as_bytes()
.iter()
.filter_map(|&c| {
let should_take = c != b'\\' || had_escape;
had_escape = !had_escape && (c == b'\\');
should_take.then_some(c)
})
.collect();
// SAFETY: the data originally came from a &str slice
unsafe { String::from_utf8_unchecked(utf8) }
}
impl<'a> From<&StringItem<'a>> for String {
fn from(val: &StringItem<'a>) -> String {
remove_escapes_stupid(val.0)
}
}
impl<'a> From<&TokenItem<'a>> for String {
fn from(val: &TokenItem<'a>) -> String {
val.0.into()
}
}
impl<'a> From<&ByteSequenceItem<'a>> for Bytes {
fn from(val: &ByteSequenceItem<'a>) -> Bytes {
transcode::base64_decode(val.0).unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::transcode::base64_decode;
fn mklist(header: &'static str) -> Result<List<'static>> {
List::parse_from_ascii(header.as_bytes())
}
fn mkdict(header: &'static str) -> Result<Dictionary<'static>> {
Dictionary::parse_from_ascii(header.as_bytes())
}
fn mkitem(header: &'static str) -> Result<Item<'static>> {
Item::parse_from_ascii(header.as_bytes())
}
#[test]
fn parse_list() {
let list = mklist("sugar,tea ,\t rum").unwrap();
assert_eq!(list.nth(0).unwrap().as_token().unwrap(), "sugar");
assert_eq!(list.nth(1).unwrap().as_token().unwrap(), "tea");
assert_eq!(list.nth(2).unwrap().as_token().unwrap(), "rum");
assert!(list.nth(3).is_none());
assert!(mklist("sugar, ").is_err());
}
#[test]
fn parse_inner_list() {
let list = mklist(r#"("foo" "bar"), ("baz"), ("bat" "one")"#).unwrap();
assert_eq!(list.len(), 3);
let inner = list.nth(0).unwrap().as_list().unwrap();
assert_eq!(inner.len(), 2);
assert_eq!(inner.nth(0).unwrap().as_string(), Some("foo".into()));
assert_eq!(inner.nth(1).unwrap().as_string(), Some("bar".into()));
assert!(inner.nth(2).is_none());
let inner = list.nth(1).unwrap().as_list().unwrap();
assert_eq!(inner.len(), 1);
assert_eq!(inner.nth(0).unwrap().as_string(), Some("baz".into()));
assert!(inner.nth(1).is_none());
let inner = list.nth(2).unwrap().as_list().unwrap();
assert_eq!(inner.len(), 2);
assert_eq!(inner.nth(0).unwrap().as_string(), Some("bat".into()));
assert_eq!(inner.nth(1).unwrap().as_string(), Some("one".into()));
assert!(inner.nth(2).is_none());
assert!(list.nth(3).is_none());
let list = mklist(r#"( "a" )"#).unwrap();
assert_eq!(list.len(), 1);
let inner = list.nth(0).unwrap().as_list().unwrap();
assert_eq!(inner.len(), 1);
assert_eq!(inner.nth(0).unwrap().as_string(), Some("a".into()));
assert!(inner.nth(1).is_none());
assert!(mklist(r#"("a" "b")"#).is_err());
assert!(mklist("(\t\"a\"\t)").is_err());
}
#[test]
fn parse_dictionary() {
let dict = mkdict(r#"en="Applepie", da=:w4ZibGV0w6ZydGU=:"#).unwrap();
assert_eq!(dict.get("en"), dict.nth(0).map(|(_, v)| v));
assert_eq!(dict.get("en").unwrap().as_string(), Some("Applepie".into()));
assert_eq!(dict.get("da"), dict.nth(1).map(|(_, v)| v));
assert_eq!(
dict.get("da").unwrap().as_byte_sequence(),
Some(base64_decode("w4ZibGV0w6ZydGU=").unwrap())
);
let dict = mkdict(r#"a=?0, b, c; foo=bar"#).unwrap();
assert_eq!(dict.get("a").unwrap().as_boolean(), Some(false));
assert_eq!(dict.get("b").unwrap().as_boolean(), Some(true));
let c = dict.get("c").unwrap();
assert_eq!(c.as_boolean(), Some(true));
assert_eq!(c.param("foo").unwrap().as_token(), Some("bar"));
let dict = mkdict("rating=1.5, feelings=(joy sadness)").unwrap();
assert!(dict.get("rating").unwrap().as_decimal().unwrap() - 1.5 < 0.001);
let feelings = dict.get("feelings").unwrap().as_list().unwrap();
assert_eq!(feelings.len(), 2);
assert_eq!(feelings.nth(0).unwrap().as_token(), Some("joy"));
assert_eq!(feelings.nth(1).unwrap().as_token(), Some("sadness"));
assert!(feelings.nth(2).is_none());
}
#[test]
fn parse_item_integer() {
let item = mkitem("-0").unwrap();
assert_eq!(item.as_integer(), Some(0));
let item = mkitem("999999999999999").unwrap();
assert_eq!(item.as_integer(), Some(999999999999999));
let item = mkitem("-999999999999999").unwrap();
assert_eq!(item.as_integer(), Some(-999999999999999));
assert!(mkitem("0999999999999999").is_err());
assert!(mkitem("-0999999999999999").is_err());
assert!(mkitem("1000000000000000").is_err());
assert!(mkitem("-1000000000000000").is_err());
}
#[test]
fn parse_item_decimal() {
let item = mkitem("4.20").unwrap();
assert!((item.as_decimal().unwrap() - 4.20).abs() < 0.001);
let item = mkitem("999999999999.999").unwrap();
assert!((item.as_decimal().unwrap() - 999999999999.999).abs() < 0.001);
let item = mkitem("-999999999999.999").unwrap();
assert!((item.as_decimal().unwrap() + 999999999999.999).abs() < 0.001);
assert!(mkitem("0999999999999.999").is_err());
assert!(mkitem("-0999999999999.999").is_err());
assert!(mkitem("999999999999.9990").is_err());
assert!(mkitem("-999999999999.9990").is_err());
}
#[test]
fn parse_item_string() {
let item = mkitem("\"\"").unwrap();
assert_eq!(item.as_string(), Some("".into()));
let item = mkitem(r#""\\\"""#).unwrap();
assert_eq!(item.as_string(), Some("\\\"".into()));
assert!(mkitem("\"").is_err());
assert!(mkitem("\\").is_err());
}
#[test]
fn parse_item_token() {
let item = mkitem(r#"token-val;param="param \"value\"""#).unwrap();
assert_eq!(item.as_token(), Some("token-val"));
assert_eq!(
item.param("param").and_then(|p| p.as_string()),
Some("param \"value\"".into())
);
}
#[test]
fn parse_item_byte_sequence() {
let base64_str =
"aWYgdXIgcmVhZGluZyB0aGlzIHlvdSBzaG91bGQgcHJvYmFibHkgZ28gb3V0c2lkZSBhbmQgdG91Y2ggc29tZSBmdWNraW5nIGdyYXNzCg==";
let item = mkitem(":aWYgdXIgcmVhZGluZyB0aGlzIHlvdSBzaG91bGQgcHJvYmFibHkgZ28gb3V0c2lkZSBhbmQgdG91Y2ggc29tZSBmdWNraW5nIGdyYXNzCg==:").unwrap();
assert_eq!(
item.as_byte_sequence(),
Some(base64_decode(base64_str).unwrap())
)
}
#[test]
fn parse_item_boolean() {
let item = mkitem("?0").unwrap();
assert_eq!(item.as_boolean(), Some(false));
let item = mkitem("?1").unwrap();
assert_eq!(item.as_boolean(), Some(true));
}
#[test]
fn reject_non_ascii_item() {
assert!(mkitem("þis is not ASCII").is_err());
}
#[test]
fn reject_empty_item() {
assert!(mkitem("").is_err());
}
}

View file

@ -1,5 +1,8 @@
pub mod bear;
pub mod crypto;
/// Almost complete implementation of [RFC 8941](https://www.rfc-editor.org/rfc/rfc8941).
pub mod header;
/// Wrappers for [`reqwest`].
pub mod http;
pub mod password;
/// Cursor utilities for parsers.