diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index feab52532..01f8fe1f9 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -3,7 +3,7 @@ use crate::{ decks::DeckID, - err::{AnkiError, ParseError, ParseErrorKind, Result}, + err::{ParseError, ParseErrorKind as FailKind, Result}, notetype::NoteTypeID, }; use lazy_static::lazy_static; @@ -11,19 +11,18 @@ use nom::{ branch::alt, bytes::complete::{escaped, is_not, tag}, character::complete::{anychar, char, none_of, one_of}, - combinator::{all_consuming, map, verify}, + combinator::{all_consuming, map}, error::ErrorKind as NomErrorKind, multi::many0, sequence::{delimited, preceded, separated_pair}, }; use regex::{Captures, Regex}; use std::borrow::Cow; -use ParseErrorKind::*; type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err>>; type ParseResult<'a, O> = std::result::Result>>; -fn parse_failure(input: &str, kind: ParseErrorKind) -> nom::Err> { +fn parse_failure(input: &str, kind: FailKind) -> nom::Err> { nom::Err::Failure(ParseError::Anki(input, kind)) } @@ -132,9 +131,9 @@ fn group_inner(input: &str) -> IResult> { // before adding the node, if the length is even then the node // must not be a boolean if node == Node::And { - return Err(parse_failure(input, MisplacedAnd)); + return Err(parse_failure(input, FailKind::MisplacedAnd)); } else if node == Node::Or { - return Err(parse_failure(input, MisplacedOr)); + return Err(parse_failure(input, FailKind::MisplacedOr)); } } else { // if the length is odd, the next item must be a boolean. if it's @@ -154,11 +153,11 @@ fn group_inner(input: &str) -> IResult> { } if nodes.is_empty() { - Err(parse_failure(input, EmptyGroup)) + Err(parse_failure(input, FailKind::EmptyGroup)) } else if nodes.last().unwrap() == &Node::And { - Err(parse_failure(input, MisplacedAnd)) + Err(parse_failure(input, FailKind::MisplacedAnd)) } else if nodes.last().unwrap() == &Node::Or { - Err(parse_failure(input, MisplacedOr)) + Err(parse_failure(input, FailKind::MisplacedOr)) } else { // chomp any trailing whitespace let (remaining, _) = whitespace0(remaining)?; @@ -201,44 +200,38 @@ fn quoted_term(s: &str) -> IResult { /// eg deck:"foo bar" - quotes must come after the : fn partially_quoted_term(s: &str) -> IResult { let (remaining, (key, val)) = separated_pair( - escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(": \u{3000}")), + escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(" \u{3000}")), char(':'), quoted_term_str, )(s)?; - if key.is_empty() { - Err(parse_failure(s, MissingKey)) - } else { - Ok(( - remaining, - Node::Search(search_node_for_text_with_argument(key, val)?), - )) - } + Ok((remaining, Node::Search(search_node_for_text_with_argument(key, val)?))) } /// Unquoted text, terminated by whitespace or unescaped ", ( or ) fn unquoted_term(s: &str) -> IResult { - if let Ok((tail, term)) = verify::<_, _, _, ParseError, _, _>( - escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}")), - |s: &str| !s.is_empty(), - )(s) + match escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}"))(s) { - if tail.starts_with('\\') { - // trailing backslash followed by whitespace or nothing - let seq = (if tail.len() > 1 { &tail[0..2] } else { r"\" }).to_string(); - Err(parse_failure(s, UnknownEscape(seq))) - } else if term.eq_ignore_ascii_case("and") { - Ok((tail, Node::And)) - } else if term.eq_ignore_ascii_case("or") { - Ok((tail, Node::Or)) - } else { - Ok((tail, Node::Search(search_node_for_text(term)?))) - } - } else if s.starts_with('\\') { - // leading backslash followed by whitespace or nothing - let seq = (if s.len() > 1 { &s[0..2] } else { r"\" }).to_string(); - Err(parse_failure(s, UnknownEscape(seq))) - } else { - Err(nom::Err::Error(ParseError::Nom(s, NomErrorKind::Verify))) + Ok((tail, term)) => { + if term.is_empty() { + Err(nom::Err::Error(ParseError::Nom(s, NomErrorKind::Verify))) + } else if term.eq_ignore_ascii_case("and") { + Ok((tail, Node::And)) + } else if term.eq_ignore_ascii_case("or") { + Ok((tail, Node::Or)) + } else { + Ok((tail, Node::Search(search_node_for_text(term)?))) + } + }, + Err(err) => { + if let nom::Err::Error((c, NomErrorKind::NoneOf)) = err { + Err(parse_failure(s, FailKind::UnknownEscape(format!("\\{}", c)))) + } else if "\"() \u{3000}".contains(s.chars().next().unwrap()) { + Err(nom::Err::Error(ParseError::Nom(s, NomErrorKind::IsNot))) + } else { + // input ends in an odd number of backslashes + Err(parse_failure(s, FailKind::UnknownEscape('\\'.to_string()))) + } + }, } } @@ -251,23 +244,26 @@ fn quoted_term_str(s: &str) -> IResult<&str> { if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) { Ok((remaining, inner)) } else { - Err(parse_failure(s, UnclosedQuote)) + Err(parse_failure(s, FailKind::UnclosedQuote)) } } else { Err(parse_failure( s, match opened.chars().next().unwrap() { - '"' => EmptyQuote, - // '\' followed by nothing - '\\' => UnclosedQuote, - _ => unreachable!(), + '"' => FailKind::EmptyQuote, + // no unescaped " and a trailing \ + _ => FailKind::UnclosedQuote, }, )) } } /// Determine if text is a qualified search, and handle escaped chars. +/// Expect well-formed input: unempty and no trailing \. fn search_node_for_text(s: &str) -> ParseResult { + if s.is_empty() { + return Err(parse_failure(s, FailKind::Other(Some("Unexpected search error.".to_string())))); + } if let Ok((tail, head)) = escaped::<_, ParseError, _, _, _, _>(is_not(r":\"), '\\', anychar)(s) { if tail.is_empty() { @@ -276,9 +272,8 @@ fn search_node_for_text(s: &str) -> ParseResult { search_node_for_text_with_argument(head, &tail[1..]) } } else { - // escaped only fails on "\" and leading ':' - // "\" cannot be passed as an argument by a calling parser - Err(parse_failure(s, MissingKey)) + // trailing \ should not be passed, so error must be leading ':' + Err(parse_failure(s, FailKind::MissingKey)) } } @@ -322,12 +317,12 @@ fn parse_template(s: &str) -> ParseResult { fn parse_flag(s: &str) -> ParseResult { if let Ok(flag) = s.parse::() { if flag > 4 { - Err(parse_failure(s, InvalidFlag)) + Err(parse_failure(s, FailKind::InvalidFlag)) } else { Ok(SearchNode::Flag(flag)) } } else { - Err(parse_failure(s, InvalidEdited)) + Err(parse_failure(s, FailKind::InvalidEdited)) } } @@ -341,7 +336,7 @@ fn parse_prop(s: &str) -> ParseResult> { tag("ease"), tag("pos"), ))(s) - .map_err(|_| parse_failure(s, InvalidPropProperty))?; + .map_err(|_| parse_failure(s, FailKind::InvalidPropProperty))?; let (num, operator) = alt::<&str, &str, ParseError, _>(( tag("<="), @@ -351,19 +346,19 @@ fn parse_prop(s: &str) -> ParseResult> { tag("<"), tag(">"), ))(tail) - .map_err(|_| parse_failure(s, InvalidPropOperator))?; + .map_err(|_| parse_failure(s, FailKind::InvalidPropOperator))?; let kind = if prop == "ease" { if let Ok(f) = num.parse::() { PropertyKind::Ease(f) } else { - return Err(parse_failure(s, InvalidPropFloat)); + return Err(parse_failure(s, FailKind::InvalidPropFloat)); } } else if prop == "due" { if let Ok(i) = num.parse::() { PropertyKind::Due(i) } else { - return Err(parse_failure(s, InvalidPropInteger)); + return Err(parse_failure(s, FailKind::InvalidPropInteger)); } } else if let Ok(u) = num.parse::() { match prop { @@ -374,7 +369,7 @@ fn parse_prop(s: &str) -> ParseResult> { _ => unreachable!(), } } else { - return Err(parse_failure(s, InvalidPropUnsigned)); + return Err(parse_failure(s, FailKind::InvalidPropUnsigned)); }; Ok(SearchNode::Property { @@ -388,7 +383,7 @@ fn parse_added(s: &str) -> ParseResult { if let Ok(days) = s.parse::() { Ok(SearchNode::AddedInDays(days.max(1))) } else { - Err(parse_failure(s, InvalidAdded)) + Err(parse_failure(s, FailKind::InvalidAdded)) } } @@ -397,7 +392,7 @@ fn parse_edited(s: &str) -> ParseResult { if let Ok(days) = s.parse::() { Ok(SearchNode::EditedInDays(days.max(1))) } else { - Err(parse_failure(s, InvalidEdited)) + Err(parse_failure(s, FailKind::InvalidEdited)) } } @@ -412,17 +407,17 @@ fn parse_rated(s: &str) -> ParseResult { if u < 5 { Some(u) } else { - return Err(parse_failure(s, InvalidRatedEase)); + return Err(parse_failure(s, FailKind::InvalidRatedEase)); } } else { - return Err(parse_failure(s, InvalidRatedEase)); + return Err(parse_failure(s, FailKind::InvalidRatedEase)); } } else { None }; Ok(SearchNode::Rated { days, ease }) } else { - Err(parse_failure(s, InvalidRatedDays)) + Err(parse_failure(s, FailKind::InvalidRatedDays)) } } @@ -438,7 +433,7 @@ fn parse_state(s: &str) -> ParseResult { "buried-manually" => UserBuried, "buried-sibling" => SchedBuried, "suspended" => Suspended, - _ => return Err(parse_failure(s, InvalidState)), + _ => return Err(parse_failure(s, FailKind::InvalidState)), })) } @@ -446,7 +441,7 @@ fn parse_did(s: &str) -> ParseResult { if let Ok(did) = s.parse() { Ok(SearchNode::DeckID(did)) } else { - Err(parse_failure(s, InvalidDid)) + Err(parse_failure(s, FailKind::InvalidDid)) } } @@ -454,7 +449,7 @@ fn parse_mid(s: &str) -> ParseResult { if let Ok(mid) = s.parse() { Ok(SearchNode::NoteTypeID(mid)) } else { - Err(parse_failure(s, InvalidMid)) + Err(parse_failure(s, FailKind::InvalidMid)) } } @@ -467,7 +462,7 @@ fn check_id_list(s: &str) -> ParseResult<&str> { if RE.is_match(s) { Ok(s) } else { - Err(parse_failure(s, InvalidIdList)) + Err(parse_failure(s, FailKind::InvalidIdList)) } } @@ -481,10 +476,10 @@ fn parse_dupes(s: &str) -> ParseResult { text: unescape_quotes(text), }) } else { - Err(parse_failure(s, InvalidDupesText)) + Err(parse_failure(s, FailKind::InvalidDupeText)) } } else { - Err(parse_failure(s, InvalidDupesMid)) + Err(parse_failure(s, FailKind::InvalidDupeMid)) } } @@ -516,7 +511,7 @@ fn unescape_quotes(s: &str) -> Cow { /// Unescape chars with special meaning to the parser. fn unescape(txt: &str) -> ParseResult> { if let Some(seq) = invalid_escape_sequence(txt) { - Err(parse_failure(txt, UnknownEscape(seq))) + Err(parse_failure(txt, FailKind::UnknownEscape(seq))) } else { Ok(if is_parser_escape(txt) { lazy_static! {