#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(
html_favicon_url = "https://raw.githubusercontent.com/olson-sean-k/wax/master/doc/wax-favicon.svg?sanitize=true"
)]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/olson-sean-k/wax/master/doc/wax.svg?sanitize=true"
)]
#![deny(
clippy::cast_lossless,
clippy::checked_conversions,
clippy::cloned_instead_of_copied,
clippy::explicit_into_iter_loop,
clippy::filter_map_next,
clippy::flat_map_option,
clippy::from_iter_instead_of_collect,
clippy::if_not_else,
clippy::manual_ok_or,
clippy::map_unwrap_or,
clippy::match_same_arms,
clippy::redundant_closure_for_method_calls,
clippy::redundant_else,
clippy::unreadable_literal,
clippy::unused_self
)]
mod capture;
mod diagnostics;
mod encode;
mod rule;
mod token;
mod walk;
use bstr::ByteVec;
use itertools::{Itertools as _, Position};
#[cfg(feature = "diagnostics-report")]
use miette::Diagnostic;
use regex::Regex;
use std::borrow::{Borrow, Cow};
use std::convert::Infallible;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Display, Formatter};
use std::path::{Path, PathBuf};
use std::str::{self, FromStr};
use thiserror::Error;
#[cfg(feature = "diagnostics-inspect")]
use crate::diagnostics::inspect;
#[cfg(feature = "diagnostics-report")]
use crate::diagnostics::report::{self, IteratorExt as _, ResultExt as _};
use crate::encode::CompileError;
use crate::rule::RuleError;
#[cfg(feature = "diagnostics-inspect")]
use crate::token::InvariantText;
use crate::token::{Annotation, IntoTokens, ParseError, Token, Tokenized};
pub use crate::capture::MatchedText;
#[cfg(feature = "diagnostics-inspect")]
pub use crate::diagnostics::inspect::{CapturingToken, Variance};
#[cfg(feature = "diagnostics-report")]
pub use crate::diagnostics::report::{DiagnosticResult, DiagnosticResultExt};
#[cfg(feature = "diagnostics-inspect")]
pub use crate::diagnostics::Span;
pub use crate::walk::{
FilterTarget, FilterTree, IteratorExt, LinkBehavior, Negation, Walk, WalkBehavior, WalkEntry,
WalkError,
};
#[cfg(windows)]
const PATHS_ARE_CASE_INSENSITIVE: bool = true;
#[cfg(not(windows))]
const PATHS_ARE_CASE_INSENSITIVE: bool = false;
trait ResultExt<T, E> {
fn expect_encoding(self) -> T;
}
impl<T, E> ResultExt<T, E> for Result<T, E>
where
E: Debug,
{
fn expect_encoding(self) -> T {
self.expect("unexpected encoding")
}
}
trait CharExt: Sized {
fn has_casing(self) -> bool;
}
impl CharExt for char {
fn has_casing(self) -> bool {
self.is_lowercase() != self.is_uppercase()
}
}
trait StrExt {
fn has_casing(&self) -> bool;
}
impl StrExt for str {
fn has_casing(&self) -> bool {
self.chars().any(CharExt::has_casing)
}
}
trait PositionExt<T> {
fn as_tuple(&self) -> (Position<()>, &T);
fn map<U, F>(self, f: F) -> Position<U>
where
F: FnMut(T) -> U;
fn interior_borrow<B>(&self) -> Position<&B>
where
T: Borrow<B>;
}
impl<T> PositionExt<T> for Position<T> {
fn as_tuple(&self) -> (Position<()>, &T) {
match *self {
Position::First(ref item) => (Position::First(()), item),
Position::Middle(ref item) => (Position::Middle(()), item),
Position::Last(ref item) => (Position::Last(()), item),
Position::Only(ref item) => (Position::Only(()), item),
}
}
fn map<U, F>(self, mut f: F) -> Position<U>
where
F: FnMut(T) -> U,
{
match self {
Position::First(item) => Position::First(f(item)),
Position::Middle(item) => Position::Middle(f(item)),
Position::Last(item) => Position::Last(f(item)),
Position::Only(item) => Position::Only(f(item)),
}
}
fn interior_borrow<B>(&self) -> Position<&B>
where
T: Borrow<B>,
{
match *self {
Position::First(ref item) => Position::First(item.borrow()),
Position::Middle(ref item) => Position::Middle(item.borrow()),
Position::Last(ref item) => Position::Last(item.borrow()),
Position::Only(ref item) => Position::Only(item.borrow()),
}
}
}
trait SliceExt<T> {
fn terminals(&self) -> Option<Terminals<&T>>;
}
impl<T> SliceExt<T> for [T] {
fn terminals(&self) -> Option<Terminals<&T>> {
match self.len() {
0 => None,
1 => Some(Terminals::Only(self.first().unwrap())),
_ => Some(Terminals::StartEnd(
self.first().unwrap(),
self.last().unwrap(),
)),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Terminals<T> {
Only(T),
StartEnd(T, T),
}
impl<T> Terminals<T> {
pub fn map<U, F>(self, mut f: F) -> Terminals<U>
where
F: FnMut(T) -> U,
{
match self {
Terminals::Only(only) => Terminals::Only(f(only)),
Terminals::StartEnd(start, end) => Terminals::StartEnd(f(start), f(end)),
}
}
}
pub trait Pattern<'t>: IntoTokens<'t> {
fn is_match<'p>(&self, path: impl Into<CandidatePath<'p>>) -> bool;
fn matched<'p>(&self, path: &'p CandidatePath<'_>) -> Option<MatchedText<'p>>;
#[cfg(feature = "diagnostics-inspect")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics-inspect")))]
fn variance(&self) -> Variance;
}
#[cfg_attr(feature = "diagnostics-report", derive(Diagnostic))]
#[derive(Debug, Error)]
#[error(transparent)]
pub enum GlobError<'t> {
#[cfg_attr(feature = "diagnostics-report", diagnostic(transparent))]
Build(BuildError<'t>),
#[cfg_attr(feature = "diagnostics-report", diagnostic(code = "wax::glob::walk"))]
Walk(WalkError),
}
impl<'t> GlobError<'t> {
pub fn into_owned(self) -> GlobError<'static> {
match self {
GlobError::Build(error) => GlobError::Build(error.into_owned()),
GlobError::Walk(error) => GlobError::Walk(error),
}
}
}
impl<'t> From<BuildError<'t>> for GlobError<'t> {
fn from(error: BuildError<'t>) -> Self {
GlobError::Build(error)
}
}
impl From<WalkError> for GlobError<'_> {
fn from(error: WalkError) -> Self {
GlobError::Walk(error)
}
}
#[cfg_attr(feature = "diagnostics-report", derive(Diagnostic))]
#[cfg_attr(feature = "diagnostics-report", diagnostic(transparent))]
#[derive(Debug, Error)]
#[error(transparent)]
pub struct BuildError<'t> {
kind: ErrorKind<'t>,
}
impl<'t> BuildError<'t> {
pub fn into_owned(self) -> BuildError<'static> {
let BuildError { kind } = self;
BuildError {
kind: kind.into_owned(),
}
}
}
impl<'t> From<ErrorKind<'t>> for BuildError<'t> {
fn from(kind: ErrorKind<'t>) -> Self {
BuildError { kind }
}
}
impl From<CompileError> for BuildError<'_> {
fn from(error: CompileError) -> Self {
BuildError {
kind: ErrorKind::Compile(error),
}
}
}
impl From<Infallible> for BuildError<'_> {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
impl<'t> From<ParseError<'t>> for BuildError<'t> {
fn from(error: ParseError<'t>) -> Self {
BuildError {
kind: ErrorKind::Parse(error),
}
}
}
impl<'t> From<RuleError<'t>> for BuildError<'t> {
fn from(error: RuleError<'t>) -> Self {
BuildError {
kind: ErrorKind::Rule(error),
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
#[cfg_attr(feature = "diagnostics-report", derive(Diagnostic))]
enum ErrorKind<'t> {
#[error(transparent)]
#[cfg_attr(feature = "diagnostics-report", diagnostic(transparent))]
Compile(CompileError),
#[error(transparent)]
#[cfg_attr(feature = "diagnostics-report", diagnostic(transparent))]
Parse(ParseError<'t>),
#[error(transparent)]
#[cfg_attr(feature = "diagnostics-report", diagnostic(transparent))]
Rule(RuleError<'t>),
}
impl<'t> ErrorKind<'t> {
pub fn into_owned(self) -> ErrorKind<'static> {
match self {
ErrorKind::Compile(error) => ErrorKind::Compile(error),
ErrorKind::Parse(error) => ErrorKind::Parse(error.into_owned()),
ErrorKind::Rule(error) => ErrorKind::Rule(error.into_owned()),
}
}
}
#[derive(Clone)]
pub struct CandidatePath<'b> {
text: Cow<'b, str>,
}
impl<'b> CandidatePath<'b> {
pub fn into_owned(self) -> CandidatePath<'static> {
CandidatePath {
text: self.text.into_owned().into(),
}
}
}
impl AsRef<str> for CandidatePath<'_> {
fn as_ref(&self) -> &str {
self.text.as_ref()
}
}
impl Debug for CandidatePath<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.text)
}
}
impl Display for CandidatePath<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.text)
}
}
impl<'b> From<&'b OsStr> for CandidatePath<'b> {
fn from(text: &'b OsStr) -> Self {
CandidatePath {
text: match Vec::from_os_str_lossy(text) {
Cow::Borrowed(bytes) => str::from_utf8(bytes).expect_encoding().into(),
Cow::Owned(bytes) => String::from_utf8(bytes).expect_encoding().into(),
},
}
}
}
impl<'b> From<&'b Path> for CandidatePath<'b> {
fn from(path: &'b Path) -> Self {
CandidatePath::from(path.as_os_str())
}
}
impl<'b> From<&'b str> for CandidatePath<'b> {
fn from(text: &'b str) -> Self {
CandidatePath { text: text.into() }
}
}
#[derive(Clone, Debug)]
pub struct Glob<'t> {
pub(crate) tokenized: Tokenized<'t>,
pub(crate) regex: Regex,
}
impl<'t> Glob<'t> {
fn compile<T>(tokens: impl IntoIterator<Item = T>) -> Result<Regex, CompileError>
where
T: Borrow<Token<'t>>,
{
encode::compile(tokens)
}
pub fn new(expression: &'t str) -> Result<Self, BuildError<'t>> {
let tokenized = parse_and_check(expression)?;
let regex = Glob::compile(tokenized.tokens())?;
Ok(Glob { tokenized, regex })
}
#[cfg(feature = "diagnostics-report")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics-report")))]
pub fn diagnosed(expression: &'t str) -> DiagnosticResult<'t, Self> {
parse_and_diagnose(expression).and_then_diagnose(|tokenized| {
Glob::compile(tokenized.tokens())
.into_error_diagnostic()
.map_value(|regex| Glob { tokenized, regex })
})
}
pub fn partition(self) -> (PathBuf, Self) {
let Glob { tokenized, .. } = self;
let (prefix, tokenized) = tokenized.partition();
let regex = Glob::compile(tokenized.tokens()).expect("failed to compile partitioned glob");
(prefix, Glob { tokenized, regex })
}
pub fn into_owned(self) -> Glob<'static> {
let Glob { tokenized, regex } = self;
Glob {
tokenized: tokenized.into_owned(),
regex,
}
}
pub fn walk(&self, directory: impl AsRef<Path>) -> Walk {
self.walk_with_behavior(directory, WalkBehavior::default())
}
pub fn walk_with_behavior(
&self,
directory: impl AsRef<Path>,
behavior: impl Into<WalkBehavior>,
) -> Walk {
walk::walk(self, directory, behavior)
}
#[cfg(feature = "diagnostics-report")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics-report")))]
pub fn diagnostics(&self) -> impl Iterator<Item = Box<dyn Diagnostic + '_>> {
report::diagnostics(&self.tokenized)
}
#[cfg(feature = "diagnostics-inspect")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics-inspect")))]
pub fn captures(&self) -> impl '_ + Clone + Iterator<Item = CapturingToken> {
inspect::captures(self.tokenized.tokens())
}
pub fn has_root(&self) -> bool {
self.tokenized
.tokens()
.first()
.map_or(false, Token::has_root)
}
pub fn has_semantic_literals(&self) -> bool {
token::literals(self.tokenized.tokens()).any(|(_, literal)| literal.is_semantic_literal())
}
}
impl FromStr for Glob<'static> {
type Err = BuildError<'static>;
fn from_str(expression: &str) -> Result<Self, Self::Err> {
Glob::new(expression)
.map(Glob::into_owned)
.map_err(BuildError::into_owned)
}
}
impl<'t> IntoTokens<'t> for Glob<'t> {
type Annotation = Annotation;
fn into_tokens(self) -> Vec<Token<'t, Self::Annotation>> {
let Glob { tokenized, .. } = self;
tokenized.into_tokens()
}
}
impl<'t> Pattern<'t> for Glob<'t> {
fn is_match<'p>(&self, path: impl Into<CandidatePath<'p>>) -> bool {
let path = path.into();
self.regex.is_match(path.as_ref())
}
fn matched<'p>(&self, path: &'p CandidatePath<'_>) -> Option<MatchedText<'p>> {
self.regex.captures(path.as_ref()).map(From::from)
}
#[cfg(feature = "diagnostics-inspect")]
fn variance(&self) -> Variance {
self.tokenized.variance().into()
}
}
impl<'t> TryFrom<&'t str> for Glob<'t> {
type Error = BuildError<'t>;
fn try_from(expression: &'t str) -> Result<Self, Self::Error> {
Glob::new(expression)
}
}
#[derive(Clone, Debug)]
pub struct Any<'t> {
pub(crate) token: Token<'t, ()>,
pub(crate) regex: Regex,
}
impl<'t> Any<'t> {
fn compile(token: &Token<'t, ()>) -> Result<Regex, CompileError> {
encode::compile([token])
}
}
impl<'t> IntoTokens<'t> for Any<'t> {
type Annotation = ();
fn into_tokens(self) -> Vec<Token<'t, Self::Annotation>> {
let Any { token, .. } = self;
vec![token]
}
}
impl<'t> Pattern<'t> for Any<'t> {
fn is_match<'p>(&self, path: impl Into<CandidatePath<'p>>) -> bool {
let path = path.into();
self.regex.is_match(path.as_ref())
}
fn matched<'p>(&self, path: &'p CandidatePath<'_>) -> Option<MatchedText<'p>> {
self.regex.captures(path.as_ref()).map(From::from)
}
#[cfg(feature = "diagnostics-inspect")]
fn variance(&self) -> Variance {
self.token.variance::<InvariantText>().into()
}
}
pub fn any<'t, P, I>(patterns: I) -> Result<Any<'t>, BuildError<'t>>
where
BuildError<'t>: From<<I::Item as TryInto<P>>::Error>,
P: Pattern<'t>,
I: IntoIterator,
I::Item: TryInto<P>,
{
let tokens = patterns
.into_iter()
.map(TryInto::try_into)
.map_ok(|pattern| {
pattern
.into_tokens()
.into_iter()
.map(Token::unannotate)
.collect::<Vec<_>>()
})
.collect::<Result<Vec<_>, _>>()?;
let token = token::any(tokens);
let regex = Any::compile(&token)?;
Ok(Any { token, regex })
}
pub fn is_match<'p>(
expression: &str,
path: impl Into<CandidatePath<'p>>,
) -> Result<bool, BuildError> {
let glob = Glob::new(expression)?;
Ok(glob.is_match(path))
}
pub fn matched<'i, 'p>(
expression: &'i str,
path: &'p CandidatePath<'_>,
) -> Result<Option<MatchedText<'p>>, BuildError<'i>> {
let glob = Glob::new(expression)?;
Ok(glob.matched(path))
}
pub fn walk(expression: &str, directory: impl AsRef<Path>) -> Result<Walk<'static>, BuildError> {
walk_with_behavior(expression, directory, WalkBehavior::default())
}
pub fn walk_with_behavior(
expression: &str,
directory: impl AsRef<Path>,
behavior: impl Into<WalkBehavior>,
) -> Result<Walk<'static>, BuildError> {
let (prefix, glob) = Glob::new(expression)?.partition();
Ok(glob
.walk_with_behavior(directory.as_ref().join(prefix), behavior)
.into_owned())
}
#[must_use]
pub fn escape(unescaped: &str) -> Cow<str> {
const ESCAPE: char = '\\';
if unescaped.chars().any(is_meta_character) {
let mut escaped = String::new();
for x in unescaped.chars() {
if is_meta_character(x) {
escaped.push(ESCAPE);
}
escaped.push(x);
}
escaped.into()
}
else {
unescaped.into()
}
}
pub const fn is_meta_character(x: char) -> bool {
matches!(
x,
'?' | '*' | '$' | ':' | '<' | '>' | '(' | ')' | '[' | ']' | '{' | '}' | ','
)
}
pub const fn is_contextual_meta_character(x: char) -> bool {
matches!(x, '-')
}
fn parse_and_check(expression: &str) -> Result<Tokenized, BuildError> {
let tokenized = token::parse(expression)?;
rule::check(&tokenized)?;
Ok(tokenized)
}
#[cfg(feature = "diagnostics-report")]
fn parse_and_diagnose(expression: &str) -> DiagnosticResult<Tokenized> {
token::parse(expression)
.into_error_diagnostic()
.and_then_diagnose(|tokenized| {
rule::check(&tokenized)
.into_error_diagnostic()
.map_value(|_| tokenized)
})
.and_then_diagnose(|tokenized| {
report::diagnostics(&tokenized)
.into_non_error_diagnostic()
.map_value(|_| tokenized)
})
}
#[cfg(test)]
mod tests {
use std::path::Path;
use crate::{Any, BuildError, CandidatePath, ErrorKind, Glob, Pattern};
#[test]
fn escape() {
assert_eq!(crate::escape(""), "");
assert_eq!(
crate::escape("?*$:<>()[]{},"),
"\\?\\*\\$\\:\\<\\>\\(\\)\\[\\]\\{\\}\\,",
);
assert_eq!(crate::escape("/usr/local/lib"), "/usr/local/lib");
assert_eq!(
crate::escape("record[D00,00].txt"),
"record\\[D00\\,00\\].txt",
);
assert_eq!(
crate::escape("Do You Remember Love?.mp4"),
"Do You Remember Love\\?.mp4",
);
assert_eq!(crate::escape("左{}右"), "左\\{\\}右");
assert_eq!(crate::escape("*中*"), "\\*中\\*");
}
#[test]
fn build_glob_with_eager_zom_tokens() {
Glob::new("*").unwrap();
Glob::new("a/*").unwrap();
Glob::new("*a").unwrap();
Glob::new("a*").unwrap();
Glob::new("a*b").unwrap();
Glob::new("/*").unwrap();
}
#[test]
fn build_glob_with_lazy_zom_tokens() {
Glob::new("$").unwrap();
Glob::new("a/$").unwrap();
Glob::new("$a").unwrap();
Glob::new("a$").unwrap();
Glob::new("a$b").unwrap();
Glob::new("/$").unwrap();
}
#[test]
fn build_glob_with_one_tokens() {
Glob::new("?").unwrap();
Glob::new("a/?").unwrap();
Glob::new("?a").unwrap();
Glob::new("a?").unwrap();
Glob::new("a?b").unwrap();
Glob::new("??a??b??").unwrap();
Glob::new("/?").unwrap();
}
#[test]
fn build_glob_with_one_and_zom_tokens() {
Glob::new("?*").unwrap();
Glob::new("*?").unwrap();
Glob::new("*/?").unwrap();
Glob::new("?*?").unwrap();
Glob::new("/?*").unwrap();
Glob::new("?$").unwrap();
}
#[test]
fn build_glob_with_tree_tokens() {
Glob::new("**").unwrap();
Glob::new("**/").unwrap();
Glob::new("/**").unwrap();
Glob::new("**/a").unwrap();
Glob::new("a/**").unwrap();
Glob::new("**/a/**/b/**").unwrap();
Glob::new("{**/a,b/c}").unwrap();
Glob::new("{a/b,c/**}").unwrap();
Glob::new("<**/a>").unwrap();
Glob::new("<a/**>").unwrap();
}
#[test]
fn build_glob_with_class_tokens() {
Glob::new("a/[xy]").unwrap();
Glob::new("a/[x-z]").unwrap();
Glob::new("a/[xyi-k]").unwrap();
Glob::new("a/[i-kxy]").unwrap();
Glob::new("a/[!xy]").unwrap();
Glob::new("a/[!x-z]").unwrap();
Glob::new("a/[xy]b/c").unwrap();
}
#[test]
fn build_glob_with_alternative_tokens() {
Glob::new("a/{x?z,y$}b*").unwrap();
Glob::new("a/{???,x$y,frob}b*").unwrap();
Glob::new("a/{???,x$y,frob}b*").unwrap();
Glob::new("a/{???,{x*z,y$}}b*").unwrap();
Glob::new("a{/**/b/,/b/**/}ca{t,b/**}").unwrap();
}
#[test]
fn build_glob_with_repetition_tokens() {
Glob::new("<a:0,1>").unwrap();
Glob::new("<a:0,>").unwrap();
Glob::new("<a:2>").unwrap();
Glob::new("<a:>").unwrap();
Glob::new("<a>").unwrap();
Glob::new("<a<b:0,>:0,>").unwrap();
Glob::new("</root:1,>").unwrap();
Glob::new("<[!.]*/:0,>[!.]*").unwrap();
}
#[test]
fn build_glob_with_literal_escaped_wildcard_tokens() {
Glob::new("a/b\\?/c").unwrap();
Glob::new("a/b\\$/c").unwrap();
Glob::new("a/b\\*/c").unwrap();
Glob::new("a/b\\*\\*/c").unwrap();
}
#[test]
fn build_glob_with_class_escaped_wildcard_tokens() {
Glob::new("a/b[?]/c").unwrap();
Glob::new("a/b[$]/c").unwrap();
Glob::new("a/b[*]/c").unwrap();
Glob::new("a/b[*][*]/c").unwrap();
}
#[test]
fn build_glob_with_literal_escaped_alternative_tokens() {
Glob::new("a/\\{\\}/c").unwrap();
Glob::new("a/{x,y\\,,z}/c").unwrap();
}
#[test]
fn build_glob_with_class_escaped_alternative_tokens() {
Glob::new("a/[{][}]/c").unwrap();
Glob::new("a/{x,y[,],z}/c").unwrap();
}
#[test]
fn build_glob_with_literal_escaped_class_tokens() {
Glob::new("a/\\[a-z\\]/c").unwrap();
Glob::new("a/[\\[]/c").unwrap();
Glob::new("a/[\\]]/c").unwrap();
Glob::new("a/[a\\-z]/c").unwrap();
}
#[test]
fn build_glob_with_flags() {
Glob::new("(?i)a/b/c").unwrap();
Glob::new("(?-i)a/b/c").unwrap();
Glob::new("a/(?-i)b/c").unwrap();
Glob::new("a/b/(?-i)c").unwrap();
Glob::new("(?i)a/(?-i)b/(?i)c").unwrap();
}
#[test]
fn build_any_combinator() {
crate::any::<Glob, _>([
Glob::new("src/**/*.rs").unwrap(),
Glob::new("doc/**/*.md").unwrap(),
Glob::new("pkg/**/PKGBUILD").unwrap(),
])
.unwrap();
crate::any::<Glob, _>(["src/**/*.rs", "doc/**/*.md", "pkg/**/PKGBUILD"]).unwrap();
}
#[test]
fn build_any_nested_combinator() {
crate::any::<Any, _>([
crate::any::<Glob, _>(["a/b", "c/d"]).unwrap(),
crate::any::<Glob, _>(["{e,f,g}", "{h,i}"]).unwrap(),
])
.unwrap();
}
#[test]
fn reject_glob_with_invalid_separator_tokens() {
assert!(Glob::new("//a").is_err());
assert!(Glob::new("a//b").is_err());
assert!(Glob::new("a/b//").is_err());
assert!(Glob::new("a//**").is_err());
assert!(Glob::new("{//}a").is_err());
assert!(Glob::new("{**//}").is_err());
}
#[test]
fn reject_glob_with_adjacent_tree_or_zom_tokens() {
assert!(Glob::new("***").is_err());
assert!(Glob::new("****").is_err());
assert!(Glob::new("**/**").is_err());
assert!(Glob::new("a{**/**,/b}").is_err());
assert!(Glob::new("**/*/***").is_err());
assert!(Glob::new("**$").is_err());
assert!(Glob::new("**/$**").is_err());
assert!(Glob::new("{*$}").is_err());
assert!(Glob::new("<*$:1,>").is_err());
}
#[test]
fn reject_glob_with_tree_adjacent_literal_tokens() {
assert!(Glob::new("**a").is_err());
assert!(Glob::new("a**").is_err());
assert!(Glob::new("a**b").is_err());
assert!(Glob::new("a*b**").is_err());
assert!(Glob::new("**/**a/**").is_err());
}
#[test]
fn reject_glob_with_adjacent_one_tokens() {
assert!(Glob::new("**?").is_err());
assert!(Glob::new("?**").is_err());
assert!(Glob::new("?**?").is_err());
assert!(Glob::new("?*?**").is_err());
assert!(Glob::new("**/**?/**").is_err());
}
#[test]
fn reject_glob_with_unescaped_meta_characters_in_class_tokens() {
assert!(Glob::new("a/[a-z-]/c").is_err());
assert!(Glob::new("a/[-a-z]/c").is_err());
assert!(Glob::new("a/[-]/c").is_err());
assert!(Glob::new("a/[---]/c").is_err());
assert!(Glob::new("a/[[]/c").is_err());
assert!(Glob::new("a/[]]/c").is_err());
}
#[test]
fn reject_glob_with_invalid_alternative_zom_tokens() {
assert!(Glob::new("*{okay,*}").is_err());
assert!(Glob::new("{okay,*}*").is_err());
assert!(Glob::new("${okay,*error}").is_err());
assert!(Glob::new("{okay,error*}$").is_err());
assert!(Glob::new("{*,okay}{okay,*}").is_err());
assert!(Glob::new("{okay,error*}{okay,*error}").is_err());
}
#[test]
fn reject_glob_with_invalid_alternative_tree_tokens() {
assert!(Glob::new("{**}").is_err());
assert!(Glob::new("slash/{**/error}").is_err());
assert!(Glob::new("{error/**}/slash").is_err());
assert!(Glob::new("slash/{okay/**,**/error}").is_err());
assert!(Glob::new("{**/okay,error/**}/slash").is_err());
assert!(Glob::new("{**/okay,prefix{error/**}}/slash").is_err());
assert!(Glob::new("{**/okay,slash/{**/error}}postfix").is_err());
assert!(Glob::new("{error/**}{okay,**/error").is_err());
}
#[test]
fn reject_glob_with_invalid_alternative_separator_tokens() {
assert!(Glob::new("/slash/{okay,/error}").is_err());
assert!(Glob::new("{okay,error/}/slash").is_err());
assert!(Glob::new("slash/{okay,/error/,okay}/slash").is_err());
assert!(Glob::new("{okay,error/}{okay,/error}").is_err());
}
#[test]
fn reject_glob_with_rooted_alternative_tokens() {
assert!(Glob::new("{okay,/}").is_err());
assert!(Glob::new("{okay,/**}").is_err());
assert!(Glob::new("{okay,/error}").is_err());
assert!(Glob::new("{okay,/**/error}").is_err());
}
#[test]
fn reject_glob_with_invalid_repetition_bounds_tokens() {
assert!(Glob::new("<a/:0,0>").is_err());
}
#[test]
fn reject_glob_with_invalid_repetition_zom_tokens() {
assert!(Glob::new("<*:0,>").is_err());
assert!(Glob::new("<a/*:0,>*").is_err());
assert!(Glob::new("*<*a:0,>").is_err());
}
#[test]
fn reject_glob_with_invalid_repetition_tree_tokens() {
assert!(Glob::new("<**:0,>").is_err());
assert!(Glob::new("</**/a/**:0,>").is_err());
assert!(Glob::new("<a/**:0,>/").is_err());
assert!(Glob::new("/**</a:0,>").is_err());
}
#[test]
fn reject_glob_with_invalid_repetition_separator_tokens() {
assert!(Glob::new("</:0,>").is_err());
assert!(Glob::new("</a/:0,>").is_err());
assert!(Glob::new("<a/:0,>/").is_err());
}
#[test]
fn reject_glob_with_rooted_repetition_tokens() {
assert!(Glob::new("</root:0,>maybe").is_err());
assert!(Glob::new("</root>").is_err());
}
#[test]
fn reject_glob_with_oversized_invariant_repetition_tokens() {
assert!(matches!(
Glob::new("<a:65536>"),
Err(BuildError {
kind: ErrorKind::Rule(_),
..
}),
));
assert!(matches!(
Glob::new("<long:16500>"),
Err(BuildError {
kind: ErrorKind::Rule(_),
..
}),
));
assert!(matches!(
Glob::new("a<long:16500>b"),
Err(BuildError {
kind: ErrorKind::Rule(_),
..
}),
));
assert!(matches!(
Glob::new("{<a:65536>,<long:16500>}"),
Err(BuildError {
kind: ErrorKind::Rule(_),
..
}),
));
}
#[test]
fn reject_glob_with_invalid_flags() {
assert!(Glob::new("(?)a").is_err());
assert!(Glob::new("(?-)a").is_err());
assert!(Glob::new("()a").is_err());
}
#[test]
fn reject_glob_with_adjacent_tokens_through_flags() {
assert!(Glob::new("/(?i)/").is_err());
assert!(Glob::new("$(?i)$").is_err());
assert!(Glob::new("*(?i)*").is_err());
assert!(Glob::new("**(?i)?").is_err());
assert!(Glob::new("a(?i)**").is_err());
assert!(Glob::new("**(?i)a").is_err());
}
#[test]
fn reject_glob_with_oversized_program() {
assert!(matches!(
Glob::new("<a*:1000000>"),
Err(BuildError {
kind: ErrorKind::Compile(_),
..
}),
));
}
#[test]
fn reject_any_combinator() {
assert!(crate::any::<Glob, _>(["{a,b,c}", "{d, e}", "f/{g,/error,h}",]).is_err())
}
#[test]
fn match_glob_with_empty_expression() {
let glob = Glob::new("").unwrap();
assert!(glob.is_match(Path::new("")));
assert!(!glob.is_match(Path::new("abc")));
}
#[test]
fn match_glob_with_only_invariant_tokens() {
let glob = Glob::new("a/b").unwrap();
assert!(glob.is_match(Path::new("a/b")));
assert!(!glob.is_match(Path::new("aa/b")));
assert!(!glob.is_match(Path::new("a/bb")));
assert!(!glob.is_match(Path::new("a/b/c")));
assert_eq!(
"a/b",
glob.matched(&CandidatePath::from(Path::new("a/b")))
.unwrap()
.complete(),
);
}
#[test]
fn match_glob_with_tree_tokens() {
let glob = Glob::new("a/**/b").unwrap();
assert!(glob.is_match(Path::new("a/b")));
assert!(glob.is_match(Path::new("a/x/b")));
assert!(glob.is_match(Path::new("a/x/y/z/b")));
assert!(!glob.is_match(Path::new("a")));
assert!(!glob.is_match(Path::new("b/a")));
assert_eq!(
"x/y/z/",
glob.matched(&CandidatePath::from(Path::new("a/x/y/z/b")))
.unwrap()
.get(1)
.unwrap(),
);
}
#[test]
fn match_glob_with_tree_and_zom_tokens() {
let glob = Glob::new("**/*.ext").unwrap();
assert!(glob.is_match(Path::new("file.ext")));
assert!(glob.is_match(Path::new("a/file.ext")));
assert!(glob.is_match(Path::new("a/b/file.ext")));
let path = CandidatePath::from(Path::new("a/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("a/", matched.get(1).unwrap());
assert_eq!("file", matched.get(2).unwrap());
}
#[test]
fn match_glob_with_eager_and_lazy_zom_tokens() {
let glob = Glob::new("$-*.*").unwrap();
assert!(glob.is_match(Path::new("prefix-file.ext")));
assert!(glob.is_match(Path::new("a-b-c.ext")));
let path = CandidatePath::from(Path::new("a-b-c.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("a", matched.get(1).unwrap());
assert_eq!("b-c", matched.get(2).unwrap());
assert_eq!("ext", matched.get(3).unwrap());
}
#[test]
fn match_glob_with_class_tokens() {
let glob = Glob::new("a/[xyi-k]/**").unwrap();
assert!(glob.is_match(Path::new("a/x/file.ext")));
assert!(glob.is_match(Path::new("a/y/file.ext")));
assert!(glob.is_match(Path::new("a/j/file.ext")));
assert!(!glob.is_match(Path::new("a/b/file.ext")));
let path = CandidatePath::from(Path::new("a/i/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("i", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_non_ascii_class_tokens() {
let glob = Glob::new("a/[金銀]/**").unwrap();
assert!(glob.is_match(Path::new("a/金/file.ext")));
assert!(glob.is_match(Path::new("a/銀/file.ext")));
assert!(!glob.is_match(Path::new("a/銅/file.ext")));
let path = CandidatePath::from(Path::new("a/金/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("金", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_literal_escaped_class_tokens() {
let glob = Glob::new("a/[\\[\\]\\-]/**").unwrap();
assert!(glob.is_match(Path::new("a/[/file.ext")));
assert!(glob.is_match(Path::new("a/]/file.ext")));
assert!(glob.is_match(Path::new("a/-/file.ext")));
assert!(!glob.is_match(Path::new("a/b/file.ext")));
let path = CandidatePath::from(Path::new("a/[/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("[", matched.get(1).unwrap());
}
#[cfg(any(unix, windows))]
#[test]
fn match_glob_with_empty_class_tokens() {
let glob = Glob::new("a[/]b").unwrap();
assert!(!glob.is_match(Path::new("a/b")));
}
#[test]
fn match_glob_with_alternative_tokens() {
let glob = Glob::new("a/{x?z,y$}b/*").unwrap();
assert!(glob.is_match(Path::new("a/xyzb/file.ext")));
assert!(glob.is_match(Path::new("a/yb/file.ext")));
assert!(!glob.is_match(Path::new("a/xyz/file.ext")));
assert!(!glob.is_match(Path::new("a/y/file.ext")));
assert!(!glob.is_match(Path::new("a/xyzub/file.ext")));
let path = CandidatePath::from(Path::new("a/xyzb/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("xyz", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_nested_alternative_tokens() {
let glob = Glob::new("a/{y$,{x?z,?z}}b/*").unwrap();
let path = CandidatePath::from(Path::new("a/xyzb/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("xyz", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_alternative_tree_tokens() {
let glob = Glob::new("a{/foo,/bar,/**/baz}/qux").unwrap();
assert!(glob.is_match(Path::new("a/foo/qux")));
assert!(glob.is_match(Path::new("a/foo/baz/qux")));
assert!(glob.is_match(Path::new("a/foo/bar/baz/qux")));
assert!(!glob.is_match(Path::new("a/foo/bar/qux")));
}
#[test]
fn match_glob_with_alternative_repetition_tokens() {
let glob = Glob::new("log-{<[0-9]:3>,<[0-9]:4>-<[0-9]:2>-<[0-9]:2>}.txt").unwrap();
assert!(glob.is_match(Path::new("log-000.txt")));
assert!(glob.is_match(Path::new("log-1970-01-01.txt")));
assert!(!glob.is_match(Path::new("log-abc.txt")));
assert!(!glob.is_match(Path::new("log-nope-no-no.txt")));
}
#[test]
fn match_glob_with_repetition_tokens() {
let glob = Glob::new("a/<[0-9]:6>/*").unwrap();
assert!(glob.is_match(Path::new("a/000000/file.ext")));
assert!(glob.is_match(Path::new("a/123456/file.ext")));
assert!(!glob.is_match(Path::new("a/00000/file.ext")));
assert!(!glob.is_match(Path::new("a/0000000/file.ext")));
assert!(!glob.is_match(Path::new("a/bbbbbb/file.ext")));
let path = CandidatePath::from(Path::new("a/999999/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("999999", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_negative_repetition_tokens() {
let glob = Glob::new("<[!.]*/>[!.]*").unwrap();
assert!(glob.is_match(Path::new("a/b/file.ext")));
assert!(!glob.is_match(Path::new(".a/b/file.ext")));
assert!(!glob.is_match(Path::new("a/.b/file.ext")));
assert!(!glob.is_match(Path::new("a/b/.file.ext")));
}
#[test]
fn match_glob_with_nested_repetition_tokens() {
let glob = Glob::new("log<-<[0-9]:3>:1,2>.txt").unwrap();
assert!(glob.is_match(Path::new("log-000.txt")));
assert!(glob.is_match(Path::new("log-123-456.txt")));
assert!(!glob.is_match(Path::new("log-abc.txt")));
assert!(!glob.is_match(Path::new("log-123-456-789.txt")));
let path = CandidatePath::from(Path::new("log-987-654.txt"));
let matched = glob.matched(&path).unwrap();
assert_eq!("-987-654", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_repeated_alternative_tokens() {
let glob = Glob::new("<{a,b}:1,>/**").unwrap();
assert!(glob.is_match(Path::new("a/file.ext")));
assert!(glob.is_match(Path::new("b/file.ext")));
assert!(glob.is_match(Path::new("aaa/file.ext")));
assert!(glob.is_match(Path::new("bbb/file.ext")));
assert!(!glob.is_match(Path::new("file.ext")));
assert!(!glob.is_match(Path::new("c/file.ext")));
let path = CandidatePath::from(Path::new("aa/file.ext"));
let matched = glob.matched(&path).unwrap();
assert_eq!("aa", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_rooted_tree_token() {
let glob = Glob::new("/**/{var,.var}/**/*.log").unwrap();
assert!(glob.is_match(Path::new("/var/log/network.log")));
assert!(glob.is_match(Path::new("/home/nobody/.var/network.log")));
assert!(!glob.is_match(Path::new("./var/cron.log")));
assert!(!glob.is_match(Path::new("mnt/var/log/cron.log")));
let path = CandidatePath::from(Path::new("/var/log/network.log"));
let matched = glob.matched(&path).unwrap();
assert_eq!("/", matched.get(1).unwrap());
}
#[test]
fn match_glob_with_flags() {
let glob = Glob::new("(?-i)photos/**/*.(?i){jpg,jpeg}").unwrap();
assert!(glob.is_match(Path::new("photos/flower.jpg")));
assert!(glob.is_match(Path::new("photos/flower.JPEG")));
assert!(!glob.is_match(Path::new("Photos/flower.jpeg")));
}
#[test]
fn match_glob_with_escaped_flags() {
let glob = Glob::new("a\\(b\\)").unwrap();
assert!(glob.is_match(Path::new("a(b)")));
}
#[test]
fn match_any_combinator() {
let any = crate::any::<Glob, _>(["src/**/*.rs", "doc/**/*.md", "pkg/**/PKGBUILD"]).unwrap();
assert!(any.is_match("src/lib.rs"));
assert!(any.is_match("doc/api.md"));
assert!(any.is_match("pkg/arch/lib-git/PKGBUILD"));
assert!(!any.is_match("img/icon.png"));
assert!(!any.is_match("doc/LICENSE.tex"));
assert!(!any.is_match("pkg/lib.rs"));
}
#[test]
fn partition_glob_with_variant_and_invariant_parts() {
let (prefix, glob) = Glob::new("a/b/x?z/*.ext").unwrap().partition();
assert_eq!(prefix, Path::new("a/b"));
assert!(glob.is_match(Path::new("xyz/file.ext")));
assert!(glob.is_match(Path::new("a/b/xyz/file.ext").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_only_variant_wildcard_parts() {
let (prefix, glob) = Glob::new("x?z/*.ext").unwrap().partition();
assert_eq!(prefix, Path::new(""));
assert!(glob.is_match(Path::new("xyz/file.ext")));
assert!(glob.is_match(Path::new("xyz/file.ext").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_only_invariant_literal_parts() {
let (prefix, glob) = Glob::new("a/b").unwrap().partition();
assert_eq!(prefix, Path::new("a/b"));
assert!(glob.is_match(Path::new("")));
assert!(glob.is_match(Path::new("a/b").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_variant_alternative_parts() {
let (prefix, glob) = Glob::new("{x,z}/*.ext").unwrap().partition();
assert_eq!(prefix, Path::new(""));
assert!(glob.is_match(Path::new("x/file.ext")));
assert!(glob.is_match(Path::new("z/file.ext").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_invariant_alternative_parts() {
let (prefix, glob) = Glob::new("{a/b}/c").unwrap().partition();
assert_eq!(prefix, Path::new("a/b/c"));
assert!(glob.is_match(Path::new("")));
assert!(glob.is_match(Path::new("a/b/c").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_invariant_repetition_parts() {
let (prefix, glob) = Glob::new("</a/b:3>/c").unwrap().partition();
assert_eq!(prefix, Path::new("/a/b/a/b/a/b/c"));
assert!(glob.is_match(Path::new("")));
assert!(glob.is_match(Path::new("/a/b/a/b/a/b/c").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_literal_dots_and_tree_tokens() {
let (prefix, glob) = Glob::new("../**/*.ext").unwrap().partition();
assert_eq!(prefix, Path::new(".."));
assert!(glob.is_match(Path::new("xyz/file.ext")));
assert!(glob.is_match(Path::new("../xyz/file.ext").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_rooted_tree_token() {
let (prefix, glob) = Glob::new("/**/*.ext").unwrap().partition();
assert_eq!(prefix, Path::new("/"));
assert!(!glob.has_root());
assert!(glob.is_match(Path::new("file.ext")));
assert!(glob.is_match(Path::new("/root/file.ext").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_rooted_zom_token() {
let (prefix, glob) = Glob::new("/*/*.ext").unwrap().partition();
assert_eq!(prefix, Path::new("/"));
assert!(!glob.has_root());
assert!(glob.is_match(Path::new("root/file.ext")));
assert!(glob.is_match(Path::new("/root/file.ext").strip_prefix(prefix).unwrap()));
}
#[test]
fn partition_glob_with_rooted_literal_token() {
let (prefix, glob) = Glob::new("/root/**/*.ext").unwrap().partition();
assert_eq!(prefix, Path::new("/root"));
assert!(!glob.has_root());
assert!(glob.is_match(Path::new("file.ext")));
assert!(glob.is_match(Path::new("/root/file.ext").strip_prefix(prefix).unwrap()));
}
#[test]
fn query_glob_has_root() {
assert!(Glob::new("/root").unwrap().has_root());
assert!(Glob::new("/**").unwrap().has_root());
assert!(Glob::new("</root:1,>").unwrap().has_root());
assert!(!Glob::new("").unwrap().has_root());
#[cfg(any(unix, windows))]
assert!(!Glob::new("[/]root").unwrap().has_root());
assert!(!Glob::new("**/").unwrap().has_root());
}
#[cfg(any(unix, windows))]
#[test]
fn query_glob_has_semantic_literals() {
assert!(Glob::new("../src/**").unwrap().has_semantic_literals());
assert!(Glob::new("*/a/../b.*").unwrap().has_semantic_literals());
assert!(Glob::new("{a,..}").unwrap().has_semantic_literals());
assert!(Glob::new("<a/..>").unwrap().has_semantic_literals());
assert!(Glob::new("<a/{b,..,c}/d>").unwrap().has_semantic_literals());
assert!(Glob::new("./*.txt").unwrap().has_semantic_literals());
}
}