pub type Parsed<T> = Result<T, Status>;
pub type Position = std::ops::Range<(usize, usize)>;
#[cfg_attr(test, derive(Debug, PartialEq))]
pub enum Status {
Fatal(Position, String), Reject, }
pub fn make<T>(value: T) -> Parsed<T> {
Ok(value)
}
pub fn reject<T>() -> Parsed<T> {
Err(Status::Reject)
}
macro_rules! unrecoverable {
(pos=$pos:expr, $stream:ident, $($str:expr),*) => {
return Err(crate::sudoers::basic_parser::Status::Fatal($pos .. CharStream::get_pos($stream), format![$($str),*]))
};
($stream:ident, $($str:expr),*) => {
{
let pos = CharStream::get_pos($stream);
return Err(crate::sudoers::basic_parser::Status::Fatal(pos .. pos, format![$($str),*]))
}
};
($($str:expr),*) => {
return Err(crate::basic_parser::Status::Fatal(Default::default(), format![$($str),*]))
};
}
pub(super) use unrecoverable;
pub fn maybe<T>(status: Parsed<T>) -> Parsed<Option<T>> {
match status {
Ok(x) => Ok(Some(x)),
Err(Status::Reject) => Ok(None),
Err(err) => Err(err),
}
}
pub use super::char_stream::CharStream;
pub trait Parse {
fn parse(stream: &mut impl CharStream) -> Parsed<Self>
where
Self: Sized;
}
pub fn accept_if(predicate: impl Fn(char) -> bool, stream: &mut impl CharStream) -> Parsed<char> {
let c = stream.peek().ok_or(Status::Reject)?;
if predicate(c) {
stream.advance();
make(c)
} else {
reject()
}
}
#[cfg_attr(test, derive(PartialEq, Eq))]
struct LeadingWhitespace;
#[cfg_attr(test, derive(PartialEq, Eq))]
struct TrailingWhitespace;
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
struct Comment;
impl Parse for LeadingWhitespace {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
let eat_space = |stream: &mut _| accept_if(|c| "\t ".contains(c), stream);
while eat_space(stream).is_ok() {}
if stream.peek().is_some() {
make(LeadingWhitespace {})
} else {
unrecoverable!(stream, "superfluous whitespace")
}
}
}
impl Parse for TrailingWhitespace {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
loop {
let _ = LeadingWhitespace::parse(stream);
if accept_if(|c| c == '\\', stream).is_ok() {
if accept_if(|c| c == '\n', stream).is_err() {
unrecoverable!(stream, "stray escape sequence")
}
} else {
break;
}
}
make(TrailingWhitespace {})
}
}
impl Parse for Comment {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
accept_if(|c| c == '#', stream)?;
while accept_if(|c| c != '\n', stream).is_ok() {}
make(Comment {})
}
}
fn skip_trailing_whitespace(stream: &mut impl CharStream) -> Parsed<()> {
TrailingWhitespace::parse(stream)?;
make(())
}
pub fn try_syntax(syntax: char, stream: &mut impl CharStream) -> Parsed<()> {
accept_if(|c| c == syntax, stream)?;
skip_trailing_whitespace(stream)?;
make(())
}
pub fn expect_syntax(syntax: char, stream: &mut impl CharStream) -> Parsed<()> {
if try_syntax(syntax, stream).is_err() {
let str = if let Some(c) = stream.peek() {
c.to_string()
} else {
"EOF".to_string()
};
unrecoverable!(stream, "expecting '{syntax}' but found '{str}'")
}
make(())
}
pub fn is_syntax(syntax: char, stream: &mut impl CharStream) -> Parsed<bool> {
let result = maybe(try_syntax(syntax, stream))?;
make(result.is_some())
}
pub fn try_nonterminal<T: Parse>(stream: &mut impl CharStream) -> Parsed<T> {
let result = T::parse(stream)?;
skip_trailing_whitespace(stream)?;
make(result)
}
use super::ast_names::UserFriendly;
pub fn expect_nonterminal<T: Parse + UserFriendly>(stream: &mut impl CharStream) -> Parsed<T> {
let begin_pos = stream.get_pos();
match try_nonterminal(stream) {
Err(Status::Reject) => {
unrecoverable!(pos = begin_pos, stream, "expected {}", T::DESCRIPTION)
}
result => result,
}
}
pub trait Token: Sized {
const MAX_LEN: usize = 255;
fn construct(s: String) -> Result<Self, String>;
fn accept(c: char) -> bool;
fn accept_1st(c: char) -> bool {
Self::accept(c)
}
const ESCAPE: char = '\0';
fn escaped(_: char) -> bool {
false
}
}
impl<T: Token> Parse for T {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
fn accept_escaped<T: Token>(
pred: fn(char) -> bool,
stream: &mut impl CharStream,
) -> Parsed<char> {
if let Ok(c) = accept_if(pred, stream) {
Ok(c)
} else if accept_if(|c| c == T::ESCAPE, stream).is_ok() {
if let Ok(c) = accept_if(T::escaped, stream) {
Ok(c)
} else {
unrecoverable!(stream, "illegal escape sequence")
}
} else {
reject()
}
}
let start_pos = stream.get_pos();
let mut str = accept_escaped::<T>(T::accept_1st, stream)?.to_string();
while let Ok(c) = accept_escaped::<T>(T::accept, stream) {
if str.len() >= T::MAX_LEN {
unrecoverable!(stream, "token exceeds maximum length")
}
str.push(c)
}
match T::construct(str) {
Ok(result) => make(result),
Err(msg) => unrecoverable!(pos = start_pos, stream, "{msg}"),
}
}
}
impl<T: Parse> Parse for Option<T> {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
maybe(T::parse(stream))
}
}
pub(super) fn parse_list<T: Parse>(
sep_by: char,
max: usize,
stream: &mut impl CharStream,
) -> Parsed<Vec<T>>
where
T: Parse + UserFriendly,
{
let mut elems = Vec::new();
elems.push(try_nonterminal(stream)?);
while maybe(try_syntax(sep_by, stream))?.is_some() {
if elems.len() >= max {
unrecoverable!(stream, "too many items in list")
}
elems.push(expect_nonterminal(stream)?);
}
make(elems)
}
pub trait Many {
const SEP: char = ',';
const LIMIT: usize = 127;
}
impl<T: Parse + Many + UserFriendly> Parse for Vec<T> {
fn parse(stream: &mut impl CharStream) -> Parsed<Self> {
parse_list(T::SEP, T::LIMIT, stream)
}
}
pub fn parse_lines<T, Stream: CharStream>(stream: &mut Stream) -> Vec<Parsed<T>>
where
T: Parse + UserFriendly,
{
let mut result = Vec::new();
while LeadingWhitespace::parse(stream).is_ok() {
let item = expect_nonterminal(stream);
let parsed_item_ok = item.is_ok();
result.push(item);
let _ = maybe(Comment::parse(stream));
if accept_if(|c| c == '\n', stream).is_err() {
if parsed_item_ok {
let msg = if stream.peek().is_none() {
"missing line terminator at end of file"
} else {
"garbage at end of line"
};
let error = |stream: &mut Stream| unrecoverable!(stream, "{msg}");
result.push(error(stream));
}
while accept_if(|c| c != '\n', stream).is_ok() {}
}
}
result
}
#[cfg(test)]
fn expect_complete<T: Parse>(stream: &mut impl CharStream) -> Parsed<T> {
let result = expect_nonterminal(stream)?;
if let Some(c) = stream.peek() {
unrecoverable!(stream, "garbage at end of line: {c}")
}
make(result)
}
#[cfg(test)]
pub fn parse_string<T: Parse>(text: &str) -> Parsed<T> {
expect_complete(&mut text.chars().peekable())
}
#[cfg(test)]
pub fn parse_eval<T: Parse>(text: &str) -> T {
parse_string(text).unwrap()
}
#[cfg(test)]
mod test {
use super::*;
impl Token for String {
fn construct(val: String) -> Result<Self, String> {
Ok(val)
}
fn accept(c: char) -> bool {
c.is_ascii_alphanumeric()
}
}
impl Many for String {}
#[test]
fn comment_test() {
assert_eq!(parse_eval::<Comment>("# hello"), Comment);
}
#[test]
#[should_panic]
fn comment_test_fail() {
assert_eq!(parse_eval::<Comment>("# hello\nsomething"), Comment);
}
#[test]
fn lines_test() {
let input = |text: &str| parse_lines(&mut text.chars().peekable());
let s = |text: &str| Ok(text.to_string());
assert_eq!(input("hello\nworld\n"), vec![s("hello"), s("world")]);
assert_eq!(input(" hello\nworld\n"), vec![s("hello"), s("world")]);
assert_eq!(input("hello \nworld\n"), vec![s("hello"), s("world")]);
assert_eq!(input("hello\n world\n"), vec![s("hello"), s("world")]);
assert_eq!(input("hello\nworld \n"), vec![s("hello"), s("world")]);
assert_eq!(input("hello\nworld")[0..2], vec![s("hello"), s("world")]);
let Err(_) = input("hello\nworld")[2] else { panic!() };
let Err(_) = input("hello\nworld:\n")[2] else { panic!() };
}
#[test]
fn whitespace_test() {
assert_eq!(
parse_eval::<Vec<String>>("hello,something"),
vec!["hello", "something"]
);
assert_eq!(
parse_eval::<Vec<String>>("hello , something"),
vec!["hello", "something"]
);
assert_eq!(
parse_eval::<Vec<String>>("hello, something"),
vec!["hello", "something"]
);
assert_eq!(
parse_eval::<Vec<String>>("hello ,something"),
vec!["hello", "something"]
);
assert_eq!(
parse_eval::<Vec<String>>("hello\\\n,something"),
vec!["hello", "something"]
);
}
}