use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
use super::Patterns;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum IntersectionError {
TooManyPatterns,
}
impl fmt::Display for IntersectionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooManyPatterns => write!(f, "pattern intersection exceeds the complexity limit"),
}
}
}
impl std::error::Error for IntersectionError {}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidPattern {
EmptySegment,
InvalidSegment(String),
MultipleGlobstars,
TooManySegments,
}
impl fmt::Display for InvalidPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptySegment => write!(f, "empty path segment"),
Self::InvalidSegment(segment) => write!(f, "invalid pattern segment: {segment:?}"),
Self::MultipleGlobstars => write!(f, "more than one ** segment"),
Self::TooManySegments => write!(f, "more than {} segments", Pattern::MAX_SEGMENTS),
}
}
}
impl std::error::Error for InvalidPattern {}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Segment {
Literal(String),
Wildcard,
Partial {
prefix: String,
suffix: String,
},
Globstar,
}
impl Segment {
fn parse(text: &str) -> Result<Self, InvalidPattern> {
match text {
"" => Err(InvalidPattern::EmptySegment),
"*" => Ok(Self::Wildcard),
"**" => Ok(Self::Globstar),
_ if text.contains('/') => Err(InvalidPattern::InvalidSegment(text.to_string())),
_ => match text.split_once('*') {
None => Ok(Self::Literal(text.to_string())),
Some((prefix, suffix)) if !suffix.contains('*') => Ok(Self::Partial {
prefix: prefix.to_string(),
suffix: suffix.to_string(),
}),
Some(_) => Err(InvalidPattern::InvalidSegment(text.to_string())),
},
}
}
fn covers(&self, other: &Self) -> bool {
match (self, other) {
(Self::Wildcard, Self::Literal(_) | Self::Partial { .. } | Self::Wildcard) => true,
(Self::Literal(a), Self::Literal(b)) => a == b,
(Self::Partial { .. }, Self::Literal(literal)) => self.matches(literal),
(
Self::Partial { prefix, suffix },
Self::Partial {
prefix: other_prefix,
suffix: other_suffix,
},
) => other_prefix.starts_with(prefix.as_str()) && other_suffix.ends_with(suffix.as_str()),
_ => false,
}
}
fn compatible(&self, other: &Self) -> bool {
match (self, other) {
(
Self::Partial { prefix, suffix },
Self::Partial {
prefix: other_prefix,
suffix: other_suffix,
},
) => {
(prefix.starts_with(other_prefix.as_str()) || other_prefix.starts_with(prefix.as_str()))
&& (suffix.ends_with(other_suffix.as_str()) || other_suffix.ends_with(suffix.as_str()))
}
_ => self.covers(other) || other.covers(self),
}
}
fn matches(&self, part: &str) -> bool {
match self {
Self::Literal(literal) => literal == part,
Self::Wildcard => true,
Self::Partial { prefix, suffix } => {
part.len() >= prefix.len() + suffix.len()
&& part.starts_with(prefix.as_str())
&& part.ends_with(suffix.as_str())
}
Self::Globstar => false,
}
}
fn intersect(&self, other: &Self) -> Vec<Self> {
match (self, other) {
(Self::Globstar, _) | (_, Self::Globstar) => Vec::new(),
(Self::Wildcard, other) => vec![other.clone()],
(this, Self::Wildcard) => vec![this.clone()],
(Self::Literal(a), Self::Literal(b)) => (a == b).then(|| self.clone()).into_iter().collect(),
(Self::Literal(literal), partial @ Self::Partial { .. })
| (partial @ Self::Partial { .. }, Self::Literal(literal)) => partial
.matches(literal)
.then(|| Self::Literal(literal.clone()))
.into_iter()
.collect(),
(
Self::Partial { prefix, suffix },
Self::Partial {
prefix: other_prefix,
suffix: other_suffix,
},
) => {
if !self.compatible(other) {
return Vec::new();
}
let prefix = if prefix.len() >= other_prefix.len() {
prefix
} else {
other_prefix
};
let suffix = if suffix.len() >= other_suffix.len() {
suffix
} else {
other_suffix
};
let mut out = vec![Self::Partial {
prefix: prefix.clone(),
suffix: suffix.clone(),
}];
for overlap in 1..=prefix.len().min(suffix.len()) {
if !prefix.is_char_boundary(prefix.len() - overlap) || !suffix.is_char_boundary(overlap) {
continue;
}
if prefix[prefix.len() - overlap..] != suffix[..overlap] {
continue;
}
let part = format!("{prefix}{}", &suffix[overlap..]);
if self.matches(&part) && other.matches(&part) {
out.push(Self::Literal(part));
}
}
out
}
}
}
}
fn intersect_run(a: &[Segment], b: &[Segment], limit: usize) -> Result<Vec<Vec<Segment>>, IntersectionError> {
debug_assert_eq!(a.len(), b.len());
let mut out: Vec<Vec<Segment>> = vec![Vec::with_capacity(a.len())];
for (a, b) in a.iter().zip(b) {
let choices = a.intersect(b);
if choices.is_empty() {
return Ok(Vec::new());
}
if out.len().checked_mul(choices.len()).is_none_or(|size| size > limit) {
return Err(IntersectionError::TooManyPatterns);
}
out = out
.iter()
.flat_map(|prefix| {
choices.iter().map(move |choice| {
let mut next = prefix.clone();
next.push(choice.clone());
next
})
})
.collect();
}
Ok(out)
}
fn insert_intersection(
out: &mut Patterns,
remaining: &mut usize,
segments: Vec<Segment>,
) -> Result<(), IntersectionError> {
if *remaining == 0 {
return Err(IntersectionError::TooManyPatterns);
}
*remaining -= 1;
if let Ok(pattern) = Pattern::new(segments) {
out.insert(pattern);
}
Ok(())
}
fn intersect_into(
a: &[Segment],
b: &[Segment],
out: &mut Patterns,
remaining: &mut usize,
) -> Result<(), IntersectionError> {
for run in intersect_run(a, b, *remaining)? {
insert_intersection(out, remaining, run)?;
}
Ok(())
}
fn expand(head: &[Segment], tail: &[Segment], len: usize) -> Vec<Segment> {
let mut out = head.to_vec();
out.extend(std::iter::repeat_n(Segment::Wildcard, len - head.len() - tail.len()));
out.extend_from_slice(tail);
out
}
impl fmt::Display for Segment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Literal(literal) => f.write_str(literal),
Self::Wildcard => f.write_str("*"),
Self::Partial { prefix, suffix } => write!(f, "{prefix}*{suffix}"),
Self::Globstar => f.write_str("**"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Specificity {
literals: usize,
exact: bool,
partials: usize,
wildcards: usize,
pinned: usize,
head: usize,
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Pattern {
text: String,
segments: Vec<Segment>,
globstar: Option<usize>,
head: usize,
}
impl Pattern {
pub const MAX_SEGMENTS: usize = 32;
pub const MAX_INTERSECTIONS: usize = 1024;
pub fn new(segments: impl IntoIterator<Item = Segment>) -> Result<Self, InvalidPattern> {
let mut segments: Vec<Segment> = segments.into_iter().collect();
if segments.len() > Self::MAX_SEGMENTS {
return Err(InvalidPattern::TooManySegments);
}
let mut globstar = None;
for (i, segment) in segments.iter().enumerate() {
match segment {
Segment::Literal(literal) if literal.is_empty() => return Err(InvalidPattern::EmptySegment),
Segment::Literal(literal) if literal.contains(['*', '/']) => {
return Err(InvalidPattern::InvalidSegment(literal.clone()));
}
Segment::Partial { prefix, suffix }
if (prefix.is_empty() && suffix.is_empty())
|| prefix.contains(['*', '/'])
|| suffix.contains(['*', '/']) =>
{
return Err(InvalidPattern::InvalidSegment(format!("{prefix}*{suffix}")));
}
Segment::Globstar if globstar.is_some() => return Err(InvalidPattern::MultipleGlobstars),
Segment::Globstar => globstar = Some(i),
_ => {}
}
}
if let Some(mut index) = globstar {
while index > 0 && segments[index - 1] == Segment::Wildcard {
segments.swap(index - 1, index);
index -= 1;
}
globstar = Some(index);
}
let mut text = String::new();
let mut head = 0;
let mut in_head = true;
for (i, segment) in segments.iter().enumerate() {
if i > 0 {
text.push('/');
}
match segment {
Segment::Literal(literal) => text.push_str(literal),
other => {
in_head = false;
text.push_str(&other.to_string());
}
}
if in_head {
head = text.len();
}
}
Ok(Self {
text,
segments,
globstar,
head,
})
}
pub fn all() -> Self {
Self::new([Segment::Globstar]).expect("** is valid")
}
pub fn literal(path: &str) -> Result<Self, InvalidPattern> {
Self::new(literal_segments(path))
}
pub fn subtree(path: &str) -> Result<Self, InvalidPattern> {
Self::new(literal_segments(path).chain([Segment::Globstar]))
}
pub fn as_str(&self) -> &str {
&self.text
}
pub fn segments(&self) -> &[Segment] {
&self.segments
}
pub fn head(&self) -> &str {
&self.text[..self.head]
}
pub fn is_literal(&self) -> bool {
self.head == self.text.len()
}
pub fn as_prefix(&self) -> Option<&str> {
match self.segments.split_last() {
Some((Segment::Globstar, head)) if head.iter().all(|s| matches!(s, Segment::Literal(_))) => {
Some(self.head())
}
_ => None,
}
}
pub fn has_globstar(&self) -> bool {
self.globstar.is_some()
}
pub fn matches(&self, path: &str) -> bool {
let parts: Vec<&str> = split_path(path).collect();
match self.globstar {
None => {
parts.len() == self.segments.len()
&& self
.segments
.iter()
.zip(&parts)
.all(|(segment, part)| segment.matches(part))
}
Some(_) => {
let (head, tail) = self.split();
parts.len() >= head.len() + tail.len()
&& head.iter().zip(&parts).all(|(segment, part)| segment.matches(part))
&& tail
.iter()
.rev()
.zip(parts.iter().rev())
.all(|(segment, part)| segment.matches(part))
}
}
}
pub fn contains(&self, other: &Self) -> bool {
match (self.globstar, other.globstar) {
(None, None) => {
self.segments.len() == other.segments.len()
&& self.segments.iter().zip(&other.segments).all(|(a, b)| a.covers(b))
}
(None, Some(_)) => false,
(Some(_), None) => {
let (head, tail) = self.split();
other.segments.len() >= head.len() + tail.len()
&& head.iter().zip(&other.segments).all(|(a, b)| a.covers(b))
&& tail
.iter()
.rev()
.zip(other.segments.iter().rev())
.all(|(a, b)| a.covers(b))
}
(Some(_), Some(_)) => {
let (head, tail) = self.split();
let (other_head, other_tail) = other.split();
let covers_run = |ours: &[Segment], theirs: &[Segment]| {
ours.iter().enumerate().all(|(i, a)| match theirs.get(i) {
Some(b) => a.covers(b),
None => *a == Segment::Wildcard,
})
};
let reversed = |run: &[Segment]| run.iter().rev().cloned().collect::<Vec<_>>();
head.len() + tail.len() <= other_head.len() + other_tail.len()
&& covers_run(head, other_head)
&& covers_run(&reversed(tail), &reversed(other_tail))
}
}
}
pub fn overlaps(&self, other: &Self) -> bool {
let compatible_run = |a: &[Segment], b: &[Segment]| a.iter().zip(b).all(|(a, b)| a.compatible(b));
let compatible_tail =
|a: &[Segment], b: &[Segment]| a.iter().rev().zip(b.iter().rev()).all(|(a, b)| a.compatible(b));
match (self.globstar, other.globstar) {
(None, None) => {
self.segments.len() == other.segments.len() && compatible_run(&self.segments, &other.segments)
}
(None, Some(_)) => other.overlaps(self),
(Some(_), None) => {
let (head, tail) = self.split();
other.segments.len() >= head.len() + tail.len()
&& compatible_run(head, &other.segments)
&& compatible_tail(tail, &other.segments)
}
(Some(_), Some(_)) => {
let (head, tail) = self.split();
let (other_head, other_tail) = other.split();
compatible_run(head, other_head) && compatible_tail(tail, other_tail)
}
}
}
pub fn specificity(&self) -> Specificity {
let count = |wanted: fn(&Segment) -> bool| self.segments.iter().filter(|s| wanted(s)).count();
Specificity {
literals: count(|s| matches!(s, Segment::Literal(_))),
exact: self.globstar.is_none(),
partials: count(|s| matches!(s, Segment::Partial { .. })),
wildcards: count(|s| matches!(s, Segment::Wildcard)),
pinned: self
.segments
.iter()
.map(|s| match s {
Segment::Partial { prefix, suffix } => prefix.len() + suffix.len(),
_ => 0,
})
.sum(),
head: self
.segments
.iter()
.take_while(|s| matches!(s, Segment::Literal(_)))
.count(),
}
}
pub fn rebase(&self, root: &str) -> Patterns {
let root: Vec<&str> = split_path(root).collect();
let mut out = Patterns::new();
let matches_run = |segments: &[Segment], parts: &[&str]| segments.iter().zip(parts).all(|(s, p)| s.matches(p));
let build = |segments: &[Segment]| Pattern::new(segments.to_vec()).expect("a rebased pattern is valid");
match self.globstar {
None => {
if root.len() <= self.segments.len() && matches_run(&self.segments, &root) {
out.insert(build(&self.segments[root.len()..]));
}
}
Some(index) => {
let (head, tail) = self.split();
if root.len() <= head.len() {
if matches_run(head, &root) {
out.insert(build(&self.segments[root.len()..]));
}
return out;
}
if !matches_run(head, &root) {
return out;
}
let rest = &root[head.len()..];
out.insert(build(&self.segments[index..]));
for consumed in 1..=tail.len().min(rest.len()) {
if matches_run(&tail[..consumed], &rest[rest.len() - consumed..]) {
out.insert(build(&tail[consumed..]));
}
}
}
}
out
}
pub fn intersect(&self, other: &Self) -> Result<Patterns, IntersectionError> {
if self.contains(other) {
return Ok(Patterns::from(other.clone()));
}
if other.contains(self) {
return Ok(Patterns::from(self.clone()));
}
let mut out = Patterns::new();
let mut remaining = Self::MAX_INTERSECTIONS;
match (self.globstar, other.globstar) {
(None, None) => {
if self.segments.len() == other.segments.len() {
intersect_into(&self.segments, &other.segments, &mut out, &mut remaining)?;
}
}
(Some(_), None) => {
let (head, tail) = self.split();
if other.segments.len() >= head.len() + tail.len() {
let stretched = expand(head, tail, other.segments.len());
intersect_into(&stretched, &other.segments, &mut out, &mut remaining)?;
}
}
(None, Some(_)) => return other.intersect(self),
(Some(_), Some(_)) => {
let (head, tail) = self.split();
let (other_head, other_tail) = other.split();
let heads = head.len().max(other_head.len());
let tails = tail.len().max(other_tail.len());
let shortest = (head.len() + tail.len()).max(other_head.len() + other_tail.len());
let long = heads + tails;
let open = long < Self::MAX_SEGMENTS;
let cap = if open { long } else { Self::MAX_SEGMENTS + 1 };
for len in shortest..cap {
let a = expand(head, tail, len);
let b = expand(other_head, other_tail, len);
intersect_into(&a, &b, &mut out, &mut remaining)?;
}
if open {
let pad = |run: &[Segment], len: usize, front: bool| -> Vec<Segment> {
let fill = std::iter::repeat_n(Segment::Wildcard, len - run.len());
if front {
run.iter().cloned().chain(fill).collect()
} else {
fill.chain(run.iter().cloned()).collect()
}
};
let fronts = intersect_run(&pad(head, heads, true), &pad(other_head, heads, true), remaining)?;
let backs = intersect_run(&pad(tail, tails, false), &pad(other_tail, tails, false), remaining)?;
if fronts
.len()
.checked_mul(backs.len())
.is_none_or(|size| size > remaining)
{
return Err(IntersectionError::TooManyPatterns);
}
for front in &fronts {
for back in &backs {
let mut segments = front.clone();
segments.push(Segment::Globstar);
segments.extend_from_slice(back);
insert_intersection(&mut out, &mut remaining, segments)?;
}
}
}
}
}
Ok(out)
}
pub fn captures(&self, matched: &Self) -> Option<Vec<Self>> {
if !self.contains(matched) {
return None;
}
let build = |segments: &[Segment]| Pattern::new(segments.to_vec()).expect("a capture is valid");
let mut out = Vec::new();
if self.globstar.is_none() {
for (segment, theirs) in self.segments.iter().zip(&matched.segments) {
if !matches!(segment, Segment::Literal(_)) {
out.push(build(std::slice::from_ref(theirs)));
}
}
return Some(out);
}
let (head, tail) = self.split();
let middle = matched.segments.len() - tail.len();
let free = matched.globstar;
let pinned = |at: usize, from_front: bool| match free {
Some(free) if from_front => at < free,
Some(free) => at > free,
None => true,
};
for (i, segment) in head.iter().enumerate() {
if !matches!(segment, Segment::Literal(_)) {
let capture = if pinned(i, true) { &matched.segments[i] } else { segment };
out.push(build(std::slice::from_ref(capture)));
}
}
if free.is_none_or(|free| free >= head.len() && free < middle) {
out.push(build(&matched.segments[head.len()..middle]));
} else {
out.push(Pattern::all());
}
for (j, segment) in tail.iter().enumerate() {
if !matches!(segment, Segment::Literal(_)) {
let at = middle + j;
let capture = if pinned(at, false) {
&matched.segments[at]
} else {
segment
};
out.push(build(std::slice::from_ref(capture)));
}
}
Some(out)
}
pub fn rooted(&self, root: &str) -> Result<Self, InvalidPattern> {
Self::new(literal_segments(root).chain(self.segments.iter().cloned()))
}
fn split(&self) -> (&[Segment], &[Segment]) {
match self.globstar {
Some(index) => (&self.segments[..index], &self.segments[index + 1..]),
None => (&self.segments, &[]),
}
}
}
fn split_path(path: &str) -> impl Iterator<Item = &str> {
path.split('/').filter(|part| !part.is_empty())
}
fn literal_segments(path: &str) -> impl Iterator<Item = Segment> + '_ {
split_path(path).map(|part| Segment::Literal(part.to_string()))
}
impl FromStr for Pattern {
type Err = InvalidPattern;
fn from_str(text: &str) -> Result<Self, InvalidPattern> {
if text.is_empty() {
return Self::new([]);
}
text.split('/')
.map(Segment::parse)
.collect::<Result<Vec<_>, _>>()
.and_then(Self::new)
}
}
impl TryFrom<&str> for Pattern {
type Error = InvalidPattern;
fn try_from(text: &str) -> Result<Self, InvalidPattern> {
text.parse()
}
}
impl TryFrom<String> for Pattern {
type Error = InvalidPattern;
fn try_from(text: String) -> Result<Self, InvalidPattern> {
text.parse()
}
}
impl Default for Pattern {
fn default() -> Self {
Self::new([]).expect("the empty pattern is valid")
}
}
impl fmt::Display for Pattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.text)
}
}
impl fmt::Debug for Pattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Pattern({:?})", self.text)
}
}
impl PartialOrd for Pattern {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Pattern {
fn cmp(&self, other: &Self) -> Ordering {
self.text.cmp(&other.text)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Pattern {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Pattern {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
impl AsRef<str> for Pattern {
fn as_ref(&self) -> &str {
&self.text
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pattern(text: &str) -> Pattern {
text.parse().unwrap_or_else(|err| panic!("{text:?}: {err}"))
}
#[test]
fn parses_and_prints_canonically() {
for text in [
"",
"a",
"a/b",
"*",
"**",
"a/*/b",
"**/transcode.pro",
"a/**/b/*",
"**/*",
"**/*.hang",
"foo*",
"foo.*.hang",
] {
assert_eq!(pattern(text).to_string(), text);
}
assert_eq!(
pattern("a/*/**/b").segments(),
&[
Segment::Literal("a".into()),
Segment::Globstar,
Segment::Wildcard,
Segment::Literal("b".into()),
]
);
}
#[test]
fn rejects_bad_syntax() {
assert_eq!("/a".parse::<Pattern>(), Err(InvalidPattern::EmptySegment));
assert_eq!("a/".parse::<Pattern>(), Err(InvalidPattern::EmptySegment));
assert_eq!("a//b".parse::<Pattern>(), Err(InvalidPattern::EmptySegment));
assert_eq!("/".parse::<Pattern>(), Err(InvalidPattern::EmptySegment));
assert_eq!("**/**".parse::<Pattern>(), Err(InvalidPattern::MultipleGlobstars));
assert_eq!(
"***".parse::<Pattern>(),
Err(InvalidPattern::InvalidSegment("***".into()))
);
assert_eq!(
"a*b*c".parse::<Pattern>(),
Err(InvalidPattern::InvalidSegment("a*b*c".into()))
);
assert_eq!(
"*a*".parse::<Pattern>(),
Err(InvalidPattern::InvalidSegment("*a*".into()))
);
assert_eq!(
"*.hang".parse::<Pattern>().unwrap().segments(),
&[Segment::Partial {
prefix: String::new(),
suffix: ".hang".into()
}]
);
assert_eq!(
Pattern::new([Segment::Partial {
prefix: String::new(),
suffix: String::new()
}]),
Err(InvalidPattern::InvalidSegment("*".into()))
);
assert_eq!(
Pattern::new([Segment::Partial {
prefix: "a/".into(),
suffix: String::new()
}]),
Err(InvalidPattern::InvalidSegment("a/*".into()))
);
let deep = ["a"; Pattern::MAX_SEGMENTS + 1].join("/");
assert_eq!(deep.parse::<Pattern>(), Err(InvalidPattern::TooManySegments));
let max = ["a"; Pattern::MAX_SEGMENTS].join("/");
assert!(max.parse::<Pattern>().is_ok());
assert_eq!(
Pattern::new([Segment::Literal("a/b".into())]),
Err(InvalidPattern::InvalidSegment("a/b".into()))
);
assert_eq!(
Pattern::new([Segment::Literal(String::new())]),
Err(InvalidPattern::EmptySegment)
);
}
#[test]
fn literal_and_subtree_normalize_paths() {
assert_eq!(Pattern::literal("/foo//bar/").unwrap(), pattern("foo/bar"));
assert_eq!(Pattern::literal("").unwrap(), Pattern::default());
assert_eq!(Pattern::subtree("foo").unwrap(), pattern("foo/**"));
assert_eq!(Pattern::subtree("/").unwrap(), Pattern::all());
assert_eq!(Pattern::literal("a/*"), Err(InvalidPattern::InvalidSegment("*".into())));
assert_eq!(Pattern::literal("**"), Err(InvalidPattern::InvalidSegment("**".into())));
}
#[test]
fn as_prefix_accepts_literals_then_globstar() {
assert_eq!(Pattern::all().as_prefix(), Some(""));
assert_eq!(pattern("foo/**").as_prefix(), Some("foo"));
assert_eq!(pattern("foo/bar/**").as_prefix(), Some("foo/bar"));
assert_eq!(pattern("foo").as_prefix(), None);
assert_eq!(Pattern::default().as_prefix(), None);
assert_eq!(pattern("foo/*").as_prefix(), None);
assert_eq!(pattern("*/foo/**").as_prefix(), None);
assert_eq!(pattern("foo/**/bar").as_prefix(), None);
assert_eq!(pattern("foo*/**").as_prefix(), None);
}
#[test]
fn matches_whole_segments() {
let cases = [
("", "", true),
("", "a", false),
("a", "a", true),
("a", "a/b", false),
("a", "ab", false),
("*", "a", true),
("*", "", false),
("*", "a/b", false),
("**", "", true),
("**", "a/b/c", true),
("a/**", "a", true),
("a/**", "a/b/c", true),
("a/**", "b", false),
("**/c", "c", true),
("**/c", "a/b/c", true),
("**/c", "a/c/b", false),
("a/**/c", "a/c", true),
("a/**/c", "a/x/y/c", true),
("a/**/c", "a", false),
("a/*/c", "a/x/c", true),
("a/*/c", "a/c", false),
("a/*/**", "a", false),
("a/*/**", "a/b", true),
("**/transcode.pro", "pid/foo.hang/transcode.pro", true),
("**/transcode.pro", "pid/foo.transcode.pro", false),
("**/*.hang", "pid/cam.hang", true),
("**/*.hang", ".hang", true),
("**/*.hang", "pid/cam.hang/x", false),
("foo*", "foo", true),
("foo*", "foobar", true),
("foo*", "fo", false),
("foo.*.hang", "foo..hang", true),
("foo.*.hang", "foo.1.hang", true),
("foo.*.hang", "foo.hang", false),
("a*a", "a", false),
("a*a", "aa", true),
];
for (text, path, expected) in cases {
assert_eq!(pattern(text).matches(path), expected, "{text} vs {path}");
}
assert!(pattern("a/b").matches("/a//b/"));
}
#[test]
fn contains_is_containment() {
let cases = [
("**", "**", true),
("**", "", true),
("**", "a/*/b", true),
("", "**", false),
("*", "a", true),
("a", "*", false),
("a/**", "a", true),
("a/**", "a/b/**", true),
("a/**", "**", false),
("a/**", "**/a", false),
("**/a", "a", true),
("**/a", "**/b/a", true),
("**/a", "a/**", false),
("*/**", "**", false),
("*/**", "a/**", true),
("*/*/**", "a/**", false),
("*/*/**", "a/b/**", true),
("a/**/c", "a/c", true),
("a/**/c", "a/x/c", true),
("a/**/c", "a/**/x/c", true),
("a/*/**/*", "a/**/b", false),
("a/*/c", "a/b/c", true),
("a/*/c", "a/**/c", false),
("*", "*.hang", true),
("*.hang", "*", false),
("*.hang", "cam.hang", true),
("*.hang", "cam.hang2", false),
("*.hang", "*.hang", true),
("*.hang", "cam*.hang", true),
("*.hang", "cam*hang", false),
("foo*", "foo.*.hang", true),
("foo.*", "foo*", false),
("**/*.hang", "pid/*/cam.hang", true),
];
for (outer, inner, expected) in cases {
assert_eq!(
pattern(outer).contains(&pattern(inner)),
expected,
"{outer} contains {inner}"
);
}
}
#[test]
fn overlaps_is_symmetric_intersection() {
let cases = [
("a", "a", true),
("a", "b", false),
("a", "*", true),
("a", "a/*", false),
("a/**", "**/b", true),
("a/**", "b/**", false),
("a/*", "*/b", true),
("a/*", "b/*", false),
("*/*", "a/**", true),
("*", "a/**", true),
("*", "a/*/**", false),
("**", "", true),
("a/**/b", "**/c", false),
("a/**/b", "**/*", true),
("*.hang", "cam*", true),
("*.hang", "cam.msf", false),
("*.hang", "*.msf", false),
("foo*", "foo.bar*", true),
("foo*", "fob*", false),
("a*b", "ab", true),
("ab*", "*ab", true),
("a/**/b", "x/**", false),
("a/**/b", "**/x", false),
];
for (a, b, expected) in cases {
assert_eq!(pattern(a).overlaps(&pattern(b)), expected, "{a} overlaps {b}");
assert_eq!(pattern(b).overlaps(&pattern(a)), expected, "{b} overlaps {a}");
}
}
#[test]
fn specificity_ranks_by_what_is_pinned_down() {
let ranked = ["a/b/c", "a/b", "a/*.hang", "a/*", "a/**", "*.hang", "*", "**"];
for pair in ranked.windows(2) {
assert!(
pattern(pair[0]).specificity() > pattern(pair[1]).specificity(),
"{} should outrank {}",
pair[0],
pair[1]
);
}
assert!(pattern("a/**").specificity() > pattern("**/a").specificity());
assert!(pattern("cam*.hang").specificity() > pattern("*.hang").specificity());
assert!(pattern("*.hang").specificity() > pattern("*").specificity());
assert_eq!(pattern("*/a").specificity(), pattern("*/b").specificity());
assert_eq!(pattern("*/a/**").specificity(), pattern("*/**/a").specificity());
}
#[test]
fn rebase_is_set_valued() {
let cases: &[(&str, &str, &[&str])] = &[
("**", "a", &["**"]),
("**/a", "a", &["", "**/a"]),
("a/**", "a", &["**"]),
("a/**", "a/b", &["**"]),
("a/**", "b", &[]),
("a/b", "a", &["b"]),
("a/b", "a/b", &[""]),
("a/b", "a/b/c", &[]),
("*/b", "a", &["b"]),
("a/*/c", "a/x", &["c"]),
("a/**/b/c", "a/b", &["**/b/c", "c"]),
("a/**/b", "a/b/b", &["**/b", ""]),
("**/b/c", "b", &["**/b/c", "c"]),
("", "", &[""]),
("", "a", &[]),
("**", "", &["**"]),
("*.hang/**", "cam.hang", &["**"]),
("*.hang/**", "cam.msf", &[]),
("**/*.hang", "a.hang", &["", "**/*.hang"]),
];
for (text, root, expected) in cases {
let got = pattern(text).rebase(root);
let expected: Patterns = expected.iter().map(|e| pattern(e)).collect();
assert_eq!(got, expected, "{text} rebased at {root}");
}
}
#[test]
fn rooted_inverts_rebase() {
assert_eq!(pattern("**").rooted("a/b").unwrap(), pattern("a/b/**"));
assert_eq!(pattern("").rooted("a").unwrap(), pattern("a"));
assert_eq!(pattern("*/c").rooted("").unwrap(), pattern("*/c"));
assert_eq!(
pattern("a").rooted("*"),
Err(InvalidPattern::InvalidSegment("*".into()))
);
let deep = ["a"; Pattern::MAX_SEGMENTS].join("/");
assert_eq!(pattern("b").rooted(&deep), Err(InvalidPattern::TooManySegments));
}
#[test]
fn head_is_the_literal_prefix() {
assert_eq!(pattern("a/b/*/c").head(), "a/b");
assert_eq!(pattern("**/a").head(), "");
assert_eq!(pattern("a/b").head(), "a/b");
assert_eq!(pattern("").head(), "");
assert_eq!(pattern("a/b*/c").head(), "a");
assert!(!pattern("a/b*").is_literal());
assert!(pattern("a/b").is_literal());
assert!(!pattern("a/*").is_literal());
assert!(pattern("a/**").has_globstar());
assert!(!pattern("a/*").has_globstar());
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trips_as_text() {
let p = pattern("a/*/**");
let json = serde_json::to_string(&p).unwrap();
assert_eq!(json, "\"a/**/*\"");
assert_eq!(serde_json::from_str::<Pattern>(&json).unwrap(), p);
assert!(serde_json::from_str::<Pattern>("\"a//b\"").is_err());
}
#[test]
fn ordering_is_by_text() {
let mut list = [pattern("b"), pattern("**"), pattern("a/*"), pattern("a")];
list.sort();
let texts: Vec<_> = list.iter().map(ToString::to_string).collect();
assert_eq!(texts, ["**", "a", "a/*", "b"]);
}
}