use std::collections::HashMap;
use winnow::ascii::escaped_transform;
use winnow::ascii::space1;
use winnow::branch::alt;
use winnow::bytes::one_of;
use winnow::bytes::take_till0;
use winnow::bytes::take_till1;
use winnow::combinator::opt;
use winnow::error::VerboseError;
use winnow::multi::separated0;
use winnow::sequence::delimited;
use winnow::sequence::preceded;
use winnow::IResult;
use winnow::Parser;
pub(crate) type ErrTy<'a> = VerboseError<&'a str>;
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Directive {
WS,
Source(String),
SourceAuto,
Ignore(Matcher),
Transform(Matcher, String, HashMap<String, String>),
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Matcher {
Section(String),
Literal(String, String),
Regex(String, String),
}
pub(super) fn parse_config(i: &str) -> IResult<&str, Vec<Directive>, ErrTy<'_>> {
let alternatives = (
comment.context("comment"),
source.context("source"),
ignore.context("ignore"),
transform.context("transform"),
"".map(|_| Directive::WS).context("whitespace"), );
(separated0(alt(alternatives), newline), opt(newline))
.map(|(val, _)| val)
.parse_next(i)
}
fn newline(i: &str) -> IResult<&str, (), ErrTy<'_>> {
one_of(("\n", "\r", "\r\n")).void().parse_next(i)
}
fn comment(i: &str) -> IResult<&str, Directive, ErrTy<'_>> {
('#', take_till0("\n\r"))
.void()
.map(|_| Directive::WS)
.parse_next(i)
}
fn source(i: &str) -> IResult<&str, Directive, ErrTy<'_>> {
(
"source",
space1,
alt((
"auto".map(|_| Directive::SourceAuto),
quoted_string.map(Directive::Source),
)),
)
.map(|(_, _, result)| result)
.parse_next(i)
}
fn ignore(i: &str) -> IResult<&str, Directive, ErrTy<'_>> {
("ignore", space1, matcher)
.map(|(_, _, pattern)| Directive::Ignore(pattern))
.parse_next(i)
}
fn transform(i: &str) -> IResult<&str, Directive, ErrTy<'_>> {
(
"transform",
space1,
matcher_transform,
space1,
take_till1(" \r\n"),
opt(preceded(space1, separated0(transform_arg, space1))),
)
.map(|(_, _, pattern, _, transform, args)| {
Directive::Transform(pattern, transform.to_owned(), args.unwrap_or_default())
})
.parse_next(i)
}
fn transform_arg(i: &str) -> IResult<&str, (String, String), ErrTy<'_>> {
(take_till1([' ', '=']), '=', quoted_string)
.map(|(key, _, value)| (key.to_owned(), value))
.parse_next(i)
}
fn match_section(i: &str) -> IResult<&str, Matcher, ErrTy<'_>> {
("section", space1, quoted_string)
.map(|(_, _, section)| Matcher::Section(section))
.parse_next(i)
}
fn match_regex(i: &str) -> IResult<&str, Matcher, ErrTy<'_>> {
("regex", space1, quoted_string, space1, quoted_string)
.map(|(_, _, section, _, key)| Matcher::Regex(section, key))
.parse_next(i)
}
fn match_literal(i: &str) -> IResult<&str, Matcher, ErrTy<'_>> {
(quoted_string, space1, quoted_string)
.map(|(section, _, key)| Matcher::Literal(section, key))
.parse_next(i)
}
fn matcher(i: &str) -> IResult<&str, Matcher, ErrTy<'_>> {
alt((match_section, match_regex, match_literal)).parse_next(i)
}
fn matcher_transform(i: &str) -> IResult<&str, Matcher, ErrTy<'_>> {
alt((match_regex, match_literal)).parse_next(i)
}
fn quoted_string(i: &str) -> IResult<&str, String, ErrTy<'_>> {
delimited(
'"',
escaped_transform(
take_till1("\"\\"),
'\\',
alt(("\\".value("\\"), "\"".value("\""), "n".value("\n"))),
),
'"',
)
.parse_next(i)
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
#[test]
fn check_quoted_string() {
let (rem, out) = quoted_string("\"test \\\" \\\\input\"").unwrap();
assert_eq!(rem, "");
assert_eq!(out, "test \" \\input");
let res = quoted_string("\"invalid");
assert!(matches!(res, Err(_)));
}
#[test]
fn check_matcher() {
let (rem, out) = matcher("section \"my-section\"").unwrap();
assert_eq!(rem, "");
assert!(matches!(out, Matcher::Section(s) if s == "my-section"));
let (rem, out) = matcher("\"my-section\" \"my-key\"").unwrap();
assert_eq!(rem, "");
assert!(matches!(out, Matcher::Literal(s, k) if s == "my-section" && k == "my-key"));
let (rem, out) = matcher("regex \"my-section.*\" \"my-key.*\"").unwrap();
assert_eq!(rem, "");
assert!(matches!(out, Matcher::Regex(s, k) if s == "my-section.*" && k == "my-key.*"));
}
#[test]
fn check_transform_arg() {
let (rem, out) = transform_arg("aaa=\"bbb\"").unwrap();
assert_eq!(rem, "");
assert_eq!(out.0, "aaa");
assert_eq!(out.1, "bbb");
}
#[test]
fn check_transform() {
let (rem, out) =
transform("transform regex \"s.*\" \"k.*\" transform-name arg1=\"a\" arg2=\"b\"")
.unwrap();
assert_eq!(rem, "");
assert_eq!(
out,
Directive::Transform(
Matcher::Regex("s.*".into(), "k.*".into()),
"transform-name".into(),
HashMap::from([("arg1".into(), "a".into()), ("arg2".into(), "b".into())]),
)
);
}
#[test]
fn check_transform_no_args() {
let (rem, out) = transform("transform regex \"s.*\" \"k.*\" transform-name").unwrap();
assert_eq!(rem, "");
assert_eq!(
out,
Directive::Transform(
Matcher::Regex("s.*".into(), "k.*".into()),
"transform-name".into(),
HashMap::new(),
)
);
}
const FULL_EXAMPLE: &str = indoc! {r#"
#!/path
source auto
ignore section "c"
ignore "a" "b"
transform "d" "e" unsorted-list separator=","
transform "f g" "h" keyring service="srv" user="usr"
transform "a" "b" kde-shortcut
ignore regex "a.*" "b.*"
transform regex "d.*" "e.*" kde-shortcut
# Random comment
"#};
#[test]
fn test_parse() {
let out = parse_config.parse(FULL_EXAMPLE).unwrap();
let out: Vec<_> = out.into_iter().filter(|v| *v != Directive::WS).collect();
assert_eq!(
out,
vec![
Directive::SourceAuto,
Directive::Ignore(Matcher::Section("c".into())),
Directive::Ignore(Matcher::Literal("a".into(), "b".into())),
Directive::Transform(
Matcher::Literal("d".into(), "e".into()),
"unsorted-list".into(),
HashMap::from([("separator".into(), ",".into())])
),
Directive::Transform(
Matcher::Literal("f g".into(), "h".into()),
"keyring".into(),
HashMap::from([
("service".into(), "srv".into()),
("user".into(), "usr".into())
])
),
Directive::Transform(
Matcher::Literal("a".into(), "b".into()),
"kde-shortcut".into(),
HashMap::new()
),
Directive::Ignore(Matcher::Regex("a.*".into(), "b.*".into())),
Directive::Transform(
Matcher::Regex("d.*".into(), "e.*".into()),
"kde-shortcut".into(),
HashMap::new()
),
]
)
}
#[test]
fn test_parse_newlines() {
let out = parse_config.parse("source auto\rsource \"foo\"\r\nignore section \"bar\"\nignore section \"quux\"\r\n").unwrap();
let out: Vec<_> = out.into_iter().filter(|v| *v != Directive::WS).collect();
assert_eq!(
out,
vec![
Directive::SourceAuto,
Directive::Source("foo".into()),
Directive::Ignore(Matcher::Section("bar".into())),
Directive::Ignore(Matcher::Section("quux".into()))
]
)
}
}