use crate::types::ValidatedGlob;
pub fn glob_match(pattern: &ValidatedGlob, input: &str) -> bool {
let pat_parts: Vec<&str> = pattern
.as_str()
.split('/')
.filter(|s| !s.is_empty())
.collect();
let inp_parts: Vec<&str> = input.split('/').filter(|s| !s.is_empty()).collect();
match_parts(&pat_parts, &inp_parts)
}
fn match_parts(pattern: &[&str], input: &[&str]) -> bool {
match (pattern, input) {
([], []) => true,
(["**"], _) => true,
([p, rest_pat @ ..], [i, rest_inp @ ..]) if *p != "**" => {
segment_match(p, i) && match_parts(rest_pat, rest_inp)
}
(["**", rest_pat @ ..], _) => {
match_parts(rest_pat, input)
|| (!input.is_empty() && match_parts(pattern, &input[1..]))
}
_ => false,
}
}
fn segment_match(pattern: &str, input: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(suffix) = pattern.strip_prefix('*') {
if !suffix.contains('*') {
return input.ends_with(suffix);
}
}
if let Some(prefix) = pattern.strip_suffix('*') {
if !prefix.contains('*') {
return input.starts_with(prefix);
}
}
if pattern.starts_with('*') && pattern.ends_with('*') && pattern.len() > 2 {
let middle = &pattern[1..pattern.len() - 1];
if !middle.contains('*') {
return input.contains(middle);
}
}
pattern == input
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ValidatedGlob;
fn g(s: &str) -> ValidatedGlob {
ValidatedGlob::parse(s).unwrap()
}
#[test]
fn exact_match() {
assert!(glob_match(&g("foo/bar"), "foo/bar"));
assert!(!glob_match(&g("foo/bar"), "foo/baz"));
}
#[test]
fn exact_match_single_segment() {
assert!(glob_match(&g("main"), "main"));
assert!(!glob_match(&g("main"), "master"));
}
#[test]
fn single_star() {
assert!(glob_match(&g("foo/*/baz"), "foo/bar/baz"));
assert!(glob_match(&g("foo/*/baz"), "foo/anything/baz"));
assert!(!glob_match(&g("foo/*/baz"), "foo/bar/qux/baz"));
}
#[test]
fn single_star_at_end() {
assert!(glob_match(&g("foo/*"), "foo/bar"));
assert!(glob_match(&g("foo/*"), "foo/anything"));
assert!(!glob_match(&g("foo/*"), "foo/bar/baz"));
}
#[test]
fn single_star_at_start() {
assert!(glob_match(&g("*/bar"), "foo/bar"));
assert!(glob_match(&g("*/bar"), "anything/bar"));
assert!(!glob_match(&g("*/bar"), "foo/baz/bar"));
}
#[test]
fn double_star_any_depth() {
assert!(glob_match(&g("foo/**"), "foo/bar"));
assert!(glob_match(&g("foo/**"), "foo/bar/baz/qux"));
assert!(glob_match(&g("foo/**"), "foo"));
}
#[test]
fn double_star_middle() {
assert!(glob_match(&g("foo/**/baz"), "foo/baz"));
assert!(glob_match(&g("foo/**/baz"), "foo/a/baz"));
assert!(glob_match(&g("foo/**/baz"), "foo/a/b/c/baz"));
assert!(!glob_match(&g("foo/**/baz"), "foo/a/b/c/qux"));
}
#[test]
fn double_star_at_start() {
assert!(glob_match(&g("**/baz"), "baz"));
assert!(glob_match(&g("**/baz"), "foo/baz"));
assert!(glob_match(&g("**/baz"), "foo/bar/baz"));
}
#[test]
fn prefix_star() {
assert!(glob_match(&g("release-*"), "release-v1"));
assert!(glob_match(&g("release-*"), "release-v2.0.0"));
assert!(glob_match(&g("release-*"), "release-"));
assert!(!glob_match(&g("release-*"), "feature-v1"));
}
#[test]
fn suffix_star() {
assert!(glob_match(&g("*-beta"), "v1-beta"));
assert!(glob_match(&g("*-beta"), "release-2.0-beta"));
assert!(!glob_match(&g("*-beta"), "v1-alpha"));
}
#[test]
fn contains_star() {
assert!(glob_match(&g("*feature*"), "my-feature-branch"));
assert!(glob_match(&g("*feature*"), "feature"));
assert!(glob_match(&g("*feature*"), "feature-x"));
assert!(glob_match(&g("*feature*"), "x-feature"));
assert!(!glob_match(&g("*feature*"), "my-branch"));
}
#[test]
fn normalise_slashes() {
assert!(glob_match(&g("foo/bar"), "foo//bar"));
assert!(glob_match(&g("foo/bar"), "foo/bar/"));
assert!(glob_match(&g("foo/bar"), "/foo/bar"));
}
#[test]
fn empty_input_matches_double_star() {
assert!(glob_match(&g("**"), ""));
assert!(glob_match(&g("**"), "a/b/c"));
}
#[test]
fn double_star_alone_matches_everything() {
assert!(glob_match(&g("**"), ""));
assert!(glob_match(&g("**"), "a"));
assert!(glob_match(&g("**"), "a/b/c"));
}
#[test]
fn no_path_traversal() {
assert!(ValidatedGlob::parse("foo/../bar").is_err());
assert!(ValidatedGlob::parse("..").is_err());
assert!(ValidatedGlob::parse("foo/..").is_err());
}
#[test]
fn refs_pattern() {
assert!(glob_match(&g("refs/heads/main"), "refs/heads/main"));
assert!(glob_match(&g("refs/heads/*"), "refs/heads/feature-x"));
assert!(glob_match(
&g("refs/heads/release-*"),
"refs/heads/release-v2"
));
assert!(!glob_match(
&g("refs/heads/release-*"),
"refs/tags/release-v2"
));
}
#[test]
fn refs_heads_all() {
assert!(glob_match(&g("refs/heads/**"), "refs/heads/main"));
assert!(glob_match(&g("refs/heads/**"), "refs/heads/feature/nested"));
assert!(!glob_match(&g("refs/heads/**"), "refs/tags/v1"));
}
#[test]
fn complex_patterns() {
assert!(glob_match(
&g("refs/heads/feature-*"),
"refs/heads/feature-123"
));
assert!(!glob_match(
&g("refs/heads/feature-*"),
"refs/heads/bugfix-123"
));
assert!(glob_match(&g("src/**"), "src/lib.rs"));
assert!(glob_match(&g("src/**"), "src/policy/mod.rs"));
}
#[test]
fn empty_pattern_after_normalization() {
}
#[test]
fn pattern_longer_than_input() {
assert!(!glob_match(&g("foo/bar/baz"), "foo/bar"));
}
#[test]
fn input_longer_than_pattern() {
assert!(!glob_match(&g("foo"), "foo/bar"));
}
#[test]
fn multiple_double_stars() {
assert!(glob_match(&g("**/**/foo"), "foo"));
assert!(glob_match(&g("**/**/foo"), "a/foo"));
assert!(glob_match(&g("**/**/foo"), "a/b/foo"));
}
}