proxima/src/parse.rs

150 lines
3.8 KiB
Rust

//! Parsers for parts of the config file.
// TODO: comments
use super::{ Ports, Effect, Pattern, Rule };
use nom::{
sequence as seq,
multi as mul,
character::complete as chr,
bytes::complete::{self as byt, tag, take_till},
Parser, error::ParseError, combinator::opt,
};
/// The result of running a parser.
pub type PResult<'i, T> = nom::IResult<&'i str, T>;
fn around <I, O, P, E, A, B> (a: A, b: B) -> impl Parser<I, O, E>
where E: ParseError<I>,
A: Parser<I, P, E> + Clone,
B: Parser<I, O, E>,
{
seq::delimited(a.clone(), b, a)
}
/// Parse a set of [`Ports`].
pub fn ports (s: &str) -> PResult<'_, Ports> {
let single = chr::u16;
let either = {
let delim = around(chr::space1, chr::char('|'));
seq::delimited(
chr::char('('),
mul::separated_list1(delim, single),
chr::char(')'),
)
};
let any = byt::tag("*");
let single = single.map(Ports::Single);
let either = either.map(Ports::Either);
let any = any.map(|_| Ports::Any);
single.or(either)
.or(any)
.parse(s)
}
/// Parse an [`Effect`].
pub fn effect (s: &str) -> PResult<'_, Effect> {
let redirect = {
let spaced = |x| seq::delimited(chr::space1, x, chr::space1);
seq::preceded(
spaced(tag("==>")),
target,
).map(Effect::Redirect)
};
let proxy = {
let spaced = |x| seq::delimited(chr::space1, x, chr::space1);
let ssl = opt(seq::delimited(
seq::preceded(chr::space1, chr::char('[')),
tag("ssl"),
chr::char(']'),
)).map(|o| o.is_some());
let internal = seq::terminated(chr::u16, chr::space0);
seq::preceded(
spaced(tag("-->")),
internal.and(ssl),
).map(|(port, ssl)| Effect::Proxy { ssl, port })
};
redirect.or(proxy)
.parse(s)
}
fn target (s: &str) -> PResult<'_, String> {
take_till(|c: char| !(c == '.' || c.is_alphanumeric()))
.map(str::to_string)
.parse(s)
}
/// Parse a [`Pattern`].
pub fn pattern (s: &str) -> PResult<'_, Pattern> {
let spaced = |x| seq::delimited(chr::space1, x, chr::space1);
seq::separated_pair(target, spaced(chr::char(':')), ports)
.map(|(domain, ports)| Pattern { host: domain, ports })
.parse(s)
}
/// Parse a [`Rule`].
pub fn rule (s: &str) -> PResult<'_, Rule> {
pattern.and(effect)
.map(|(p, e)| Rule (p, e))
.parse(s)
}
#[cfg(test)]
mod tests {
use super::*;
/// Test whether a pattern containing an Any portspec gets parsed
/// correctly.
#[test]
fn simple_pattern () {
let input = "example.com : *";
let (_, Pattern { host: domain, ports }) = pattern(input).unwrap();
assert!(domain == "example.com");
assert!(match ports {
Ports::Any => true,
_ => false,
})
}
/// Test whether an Either portspec is parsed correctly.
#[test]
fn either_ports () {
let input = "(69 | 420)";
assert!(match ports(input).unwrap() {
("", Ports::Either(p)) => p == [69, 420],
_ => false,
})
}
/// Test whether target names are parsed correctly.
#[test]
fn targets () {
let inputs = ["example.com", "im.badat.dev", "riley.lgbt", "toot.site", "a.b.c.d.e.f.g.h"];
// Each of these should be considered valid
for input in inputs {
target(input).unwrap();
}
}
/// Test whether a simple rule gets parsed correctly.
#[test]
fn simple_rule () {
let input = "example.gay : * --> 3000";
let (_, Rule (_, effect)) = rule(input).unwrap();
assert!(match effect {
Effect::Proxy { port: 3000, ssl } if !ssl => true,
_ => false,
});
}
}