#[cfg(not(has_core_iter_Iterator_reduce))]
use crate::util::compat::features::IteratorFoldSelfExt as _;
use std::prelude::v1::*;
use regex::Regex;
#[derive(Debug)]
pub enum Error {
RegexError(regex::Error),
RegexTooBig,
}
impl From<regex::Error> for Error {
fn from(e: regex::Error) -> Self {
Self::RegexError(e)
}
}
pub(super) const PATH_VALUE_REGEX_S: &str = r"[0-9a-zA-Z_\-.~%!$&'()*+,;=@:]+";
pub(super) const QS_VALUE_REGEX_S: &str = r"[0-9a-zA-Z_\-.~%!$'()*+,;=@:]+";
pub(super) const START_RE: &str = r"\A";
lazy_static::lazy_static! {
static ref PLACEHOLDER_REGEX: Regex = Regex::new(r"\{.+?\}").unwrap();
}
#[cfg_attr(not(has_core_iter_Iterator_reduce), allow(unstable_name_collisions))]
pub(super) fn query_string_regex(s: &str) -> Result<Vec<Regex>, Error> {
let kv_regex_s = PLACEHOLDER_REGEX
.split(s)
.map(|literal| {
literal
.split('&')
.map(|part| regex::escape(part))
.reduce(|mut acc, escaped_part| {
acc.push('&');
acc.push_str(escaped_part.as_str());
acc
})
.unwrap()
})
.reduce(|mut acc, escaped| {
acc.push_str(QS_VALUE_REGEX_S);
acc.push_str(escaped.as_str());
acc
})
.unwrap();
let mut kv_iter = kv_regex_s.split('&');
kv_iter.try_fold(
Vec::with_capacity(core::cmp::max(kv_iter.size_hint().0, 8)),
|mut acc, kv_literal| {
let mut kv = START_RE.to_string();
kv.push_str(kv_literal);
Regex::new(kv.as_str())
.map(|regex| {
acc.push(regex);
acc
})
.map_err(Error::from)
},
)
}
#[cfg_attr(not(has_core_iter_Iterator_reduce), allow(unstable_name_collisions))]
pub(super) fn path_regex(path: &str) -> Result<Regex, Error> {
let path_without_dup_fslashes = coalesce_chars(path, '/');
let regex_literal = PLACEHOLDER_REGEX
.split(path_without_dup_fslashes.as_str())
.map(|literal| literal.to_string()) .reduce(|mut acc, literal| {
acc.push_str(PATH_VALUE_REGEX_S);
acc.push_str(literal.as_str());
acc
})
.unwrap();
let required_capacity = regex_literal
.len()
.checked_add(START_RE.len())
.ok_or(Error::RegexTooBig)?;
let mut final_regex = String::with_capacity(required_capacity);
final_regex.push_str(START_RE);
final_regex.push_str(®ex_literal);
Ok(Regex::new(final_regex.as_str())?)
}
pub(super) fn split_path_n_qs(s: &str) -> (&str, Option<&str>) {
s.find('?')
.map(|idx| (&s[..idx], Some(&s[idx + 1..])))
.unwrap_or((s, None))
}
pub(super) fn coalesce_chars(s: &str, coalescing_char: char) -> String {
enum Last {
Missed,
Matched,
}
let mut last = Last::Missed;
s.chars()
.fold(String::with_capacity(s.len()), |mut acc, c| {
if c == coalescing_char {
if let Last::Missed = last {
acc.push(c);
last = Last::Matched;
}
} else {
acc.push(c);
last = Last::Missed;
}
acc
})
}
#[cfg(test)]
mod test {
use super::*;
mod coalesce_chars {
use super::*;
#[test]
fn remove_duped_forward_slashes() -> Result<(), Error> {
let pattern = "/a//b///c////d/////e/";
assert_eq!(coalesce_chars(pattern, '/').as_str(), "/a/b/c/d/e/");
Ok(())
}
}
mod split_path_n_qs {
use super::*;
#[test]
fn strips_question_mark() -> Result<(), Error> {
let pattern = "/abc?param1=1¶m2=2";
let (path, qs) = split_path_n_qs(pattern);
assert_eq!(path, "/abc");
assert_eq!(qs, Some("param1=1¶m2=2"));
Ok(())
}
}
mod query_string_regex {
use super::*;
#[test]
fn builds_correct_qs_param_regexes() -> Result<(), Error> {
let qs_patterns = "fmt={fmt}&hardcoded=1&lang{num}={lang}";
let regexes = query_string_regex(qs_patterns)?;
let regexes_s = regexes.iter().map(Regex::as_str).collect::<Vec<_>>();
assert!(regexes_s.contains(&format!(r"{}fmt={}", START_RE, QS_VALUE_REGEX_S).as_str()));
assert!(regexes_s.contains(&format!(r"{}hardcoded=1", START_RE).as_str()));
assert!(regexes_s.contains(
&format!(r"{s}lang{qs}={qs}", s = START_RE, qs = QS_VALUE_REGEX_S).as_str()
));
Ok(())
}
#[test]
fn matches_qs_parameters() -> Result<(), Error> {
let qs_patterns = ["fmt={fmt}", "hardcoded=1", "lang{num}={lang}"];
let qs_examples = ["fmt=json", "hardcoded=1", "lang_1=ca"];
let qs_counter_ex = ["fmt-json", "hardcoded=", "a_lang_1=ca"];
let regexes = query_string_regex(qs_patterns.join("&").as_str())?;
for regex in regexes {
assert!(qs_examples.iter().any(|&qs| { regex.is_match(qs) }));
assert!(qs_counter_ex.iter().all(|&qs| { !regex.is_match(qs) }));
}
Ok(())
}
}
mod path_regex {
use super::*;
#[test]
fn match_fail() -> Result<(), Error> {
let pattern = "/abc";
let regex = path_regex(pattern)?;
assert!(!regex.is_match("/aaa"));
Ok(())
}
#[test]
fn match_prefix() -> Result<(), Error> {
let pattern = "/abc";
let regex = path_regex(pattern)?;
assert!(regex.is_match("/abc"));
assert!(regex.is_match("/abcd"));
Ok(())
}
#[test]
fn match_special_chars() -> Result<(), Error> {
let pattern = "/foo/{wildcard}/bar";
let regex = path_regex(pattern)?;
assert!(regex.is_match("/foo/a@b/bar"));
assert!(regex.is_match("/foo/a:b/bar"));
assert!(regex.is_match("/foo/a%b/bar"));
assert!(regex.is_match("/foo/a$b/bar"));
assert!(regex.is_match("/foo/a()b/bar"));
Ok(())
}
#[test]
fn match_exact() -> Result<(), Error> {
let pattern = "/abc$";
let regex = path_regex(pattern)?;
assert!(regex.is_match("/abc"));
assert!(!regex.is_match("/abcd"));
Ok(())
}
#[test]
fn match_dollar_sign_at_end() -> Result<(), Error> {
let pattern = r"/abc\$";
let regex = path_regex(pattern)?;
assert!(regex.is_match("/abc$"));
assert!(!regex.is_match("/abcd"));
Ok(())
}
#[test]
fn match_double_forward_slashes() -> Result<(), Error> {
let patterns = [
("/foo//bar", "/foo/bar"),
("/foo///bar", "/foo/bar"),
("///foo///bar///", "/foo/bar/"),
("/foo/bar///", "/foo/bar/"),
("/foo/ /bar", "/foo/ /bar"),
];
for (pattern, expected) in patterns.iter() {
let regex = path_regex(pattern)?;
assert!(regex.is_match(expected));
}
Ok(())
}
}
}