Skip to content

Commit

Permalink
Move multiline attribute value parsing out of attribute parser
Browse files Browse the repository at this point in the history
  • Loading branch information
SRv6d committed Sep 29, 2024
1 parent 879bc6a commit f5a9bca
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 52 deletions.
107 changes: 56 additions & 51 deletions src/parser/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::iter::once;

use winnow::{
ascii::{newline, space0},
combinator::{delimited, peek, preceded, repeat, separated_pair, terminated},
error::{ContextError, ParserError},
combinator::{delimited, opt, preceded, repeat, separated_pair, terminated},
error::ParserError,
token::{one_of, take_while},
PResult, Parser,
};
Expand All @@ -22,28 +22,14 @@ pub fn server_message<'s>(input: &mut &'s str) -> PResult<&'s str> {
.parse_next(input)
}

// A RPSL attribute consisting of a name and one or more values.
// The name is followed by a colon and optional spaces.
// Single value attributes are limited to one line, while multi value attributes span over multiple lines.
pub fn attribute<'s>(input: &mut &'s str) -> PResult<Attribute<'s>> {
let (name, first_value) = separated_pair(
attribute_name(),
(':', space0),
terminated(attribute_value(), newline),
)
.parse_next(input)?;

if peek(continuation_char::<ContextError>())
.parse_next(input)
.is_ok()
{
let continuation_values: Vec<&str> =
repeat(1.., continuation_line(attribute_value())).parse_next(input)?;
let value = Value::unchecked_multi(once(first_value).chain(continuation_values));
return Ok(Attribute::new(name, value));
}

Ok(Attribute::new(name, Value::unchecked_single(first_value)))
// Generate an attribute parser.
// The attributes name and value are separated by a colon and optional spaces.
pub fn attribute<'s, E>() -> impl Parser<&'s str, Attribute<'s>, E>
where
E: ParserError<&'s str>,
{
separated_pair(attribute_name(), (':', space0), attribute_value_opt_multi())
.map(|(name, value)| Attribute::new(name, value))
}

/// Generate an attribute value parser that parses an ASCII sequence of letters,
Expand All @@ -66,17 +52,40 @@ fn attribute_value<'s, E>() -> impl Parser<&'s str, &'s str, E>
where
E: ParserError<&'s str>,
{
take_while(0.., |c| Value::validate_char(c).is_ok())
terminated(
take_while(0.., |c| Value::validate_char(c).is_ok()),
newline,
)
}

/// Generate a parser that extends an attributes value over multiple lines,
/// where each value is prefixed with a continuation character.
fn continuation_line<'s, P, E>(value_parser: P) -> impl Parser<&'s str, &'s str, E>
/// Generate a parser for continuation values.
fn attribute_value_continuation<'s, P, E>(value_parser: P) -> impl Parser<&'s str, Vec<&'s str>, E>
where
P: Parser<&'s str, &'s str, E>,
E: ParserError<&'s str>,
{
delimited(continuation_char(), preceded(space0, value_parser), newline)
repeat(
1..,
preceded(continuation_char(), preceded(space0, value_parser)),
)
}

/// Generate an attribute value parser that optionally parses continuation lines.
fn attribute_value_opt_multi<'s, E>() -> impl Parser<&'s str, Value<'s>, E>
where
E: ParserError<&'s str>,
{
(
attribute_value(),
opt(attribute_value_continuation(attribute_value())),
)
.map(|(first_value, continuation)| {
if let Some(continuation_values) = continuation {
Value::unchecked_multi(once(first_value).chain(continuation_values))
} else {
Value::unchecked_single(first_value)
}
})
}

/// Generate a parser for a single continuation character.
Expand All @@ -91,6 +100,7 @@ where
mod tests {
use proptest::prelude::*;
use rstest::*;
use winnow::error::ContextError;

use super::*;

Expand Down Expand Up @@ -131,7 +141,7 @@ mod tests {
#[case] expected: Attribute,
#[case] remaining: &str,
) {
let parsed = attribute(given).unwrap();
let parsed = attribute::<ContextError>().parse_next(given).unwrap();
assert_eq!(parsed, expected);
assert_eq!(*given, remaining);
}
Expand Down Expand Up @@ -159,7 +169,7 @@ mod tests {
#[case] expected: Attribute,
#[case] remaining: &str,
) {
let parsed = attribute(given).unwrap();
let parsed = attribute::<ContextError>().parse_next(given).unwrap();
assert_eq!(parsed, expected);
assert_eq!(*given, remaining);
}
Expand Down Expand Up @@ -207,27 +217,22 @@ mod tests {
#[case(
&mut "This is an example remark\n",
"This is an example remark",
"\n"
""
)]
#[case(
&mut "Concerning abuse and spam ... mailto: [email protected]\n",
"Concerning abuse and spam ... mailto: [email protected]",
"\n"
""
)]
#[case(
&mut "+49 176 07071964\n",
"+49 176 07071964",
"\n"
""
)]
#[case(
&mut "* Equinix FR5, Kleyerstr, Frankfurt am Main\n",
"* Equinix FR5, Kleyerstr, Frankfurt am Main",
"\n"
)]
#[case(
&mut "\n",
"",
"\n"
""
)]
fn attribute_value_valid(
#[case] given: &mut &str,
Expand All @@ -241,38 +246,38 @@ mod tests {
}

proptest! {
/// Any non extended ASCII is not returned by the value parser.
/// Parsing any non extended ASCII returns an error.
#[test]
fn attribute_value_non_extended_ascii_not_parsed(s in r"[^\x00-\xFF]+") {
fn attribute_value_non_extended_ascii_is_err(s in r"[^\x00-\xFF]+") {
let mut parser = attribute_value::<ContextError>();
let parsed = parser.parse_next(&mut s.as_str()).unwrap();
prop_assert_eq!(parsed, "");
assert!(parser.parse_next(&mut s.as_str()).is_err());
}
}

#[rstest]
#[case(
&mut " continuation value prefixed by a space\n",
"continuation value prefixed by a space",
vec!["continuation value prefixed by a space"],
""
)]
#[case(
&mut "\t continuation value prefixed by a tab\n",
"continuation value prefixed by a tab",
vec!["continuation value prefixed by a tab"],
""
)]
#[case(
&mut "+ continuation value prefixed by a plus\n",
"continuation value prefixed by a plus",
vec!["continuation value prefixed by a plus"],
""
)]
fn continuation_line_valid(
fn attribute_value_continuation_valid(
#[case] given: &mut &str,
#[case] expected: &str,
#[case] expected: Vec<&str>,
#[case] remaining: &str,
) {
let mut parser = continuation_line::<_, ContextError>(attribute_value());
let parsed = parser.parse_next(given).unwrap();
let value_parser = attribute_value::<ContextError>();
let mut continuation_parser = attribute_value_continuation::<_, ContextError>(value_parser);
let parsed = continuation_parser.parse_next(given).unwrap();
assert_eq!(parsed, expected);
assert_eq!(*given, remaining);
}
Expand Down
2 changes: 1 addition & 1 deletion src/parser/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{Object, ParseError};
/// As per [RFC 2622](https://datatracker.ietf.org/doc/html/rfc2622#section-2), an RPSL object
/// is textually represented as a list of attribute-value pairs that ends when a blank line is encountered.
fn object_block<'s>(input: &mut &'s str) -> PResult<Object<'s>> {
let (attributes, source) = terminated(repeat(1.., component::attribute), newline)
let (attributes, source) = terminated(repeat(1.., component::attribute()), newline)
.with_taken()
.parse_next(input)?;
Ok(Object::from_parsed(source, attributes))
Expand Down

0 comments on commit f5a9bca

Please sign in to comment.