#![no_std]
#![doc(html_root_url = "https://docs.rs/glob-parent/0.1.0")]
#![allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[must_use]
pub fn glob_parent(glob: &str) -> String {
glob_parent_with_options(glob, true)
}
#[must_use]
pub fn glob_parent_with_options(glob: &str, flip_backslashes: bool) -> String {
let mut s = glob.to_string();
if flip_backslashes && cfg!(windows) && !s.contains('/') {
s = s.replace('\\', "/");
}
if is_enclosure(&s) {
s.push('/');
}
s.push('a');
loop {
s = posix_dirname(&s);
if !is_globby(&s) {
break;
}
}
unescape_glob(&s)
}
#[must_use]
pub fn is_glob(s: &str) -> bool {
if s.is_empty() {
return false;
}
if is_extglob(s) {
return true;
}
let units: Vec<u16> = s.encode_utf16().collect();
is_glob_strict(&units)
}
#[must_use]
pub fn is_extglob(s: &str) -> bool {
if s.is_empty() {
return false;
}
let units: Vec<u16> = s.encode_utf16().collect();
let len = units.len();
let mut start = 0;
while start < len {
let mut hit: Option<(usize, bool)> = None;
let mut p = start;
while p < len {
if units[p] == cu('\\') && units.get(p + 1).is_some_and(|&n| !is_line_terminator(n)) {
hit = Some((p, false));
break;
}
if is_extglob_char(units[p])
&& units.get(p + 1) == Some(&cu('('))
&& close_paren_before_line_terminator(&units[p + 2..])
{
hit = Some((p, true));
break;
}
p += 1;
}
match hit {
None => return false,
Some((_, true)) => return true,
Some((p, false)) => start = p + 2,
}
}
false
}
const fn cu(c: char) -> u16 {
c as u16
}
fn is_line_terminator(u: u16) -> bool {
matches!(u, 0x000A | 0x000D | 0x2028 | 0x2029)
}
fn is_extglob_char(u: u16) -> bool {
u == cu('@') || u == cu('?') || u == cu('!') || u == cu('+') || u == cu('*')
}
fn close_paren_before_line_terminator(units: &[u16]) -> bool {
for &u in units {
if u == cu(')') {
return true;
}
if is_line_terminator(u) {
return false;
}
}
false
}
fn find_from(units: &[u16], c: u16, from: usize) -> i64 {
if from > units.len() {
return -1;
}
units[from..]
.iter()
.position(|&x| x == c)
.map_or(-1, |p| (from + p) as i64)
}
#[allow(clippy::too_many_lines)]
fn is_glob_strict(units: &[u16]) -> bool {
let len = units.len();
if units.first() == Some(&cu('!')) {
return true;
}
let mut index: usize = 0;
let mut pipe_index: i64 = -2;
let mut close_square: i64 = -2;
let mut close_curly: i64 = -2;
let mut close_paren: i64 = -2;
let mut back_slash: i64 = -2;
while index < len {
let c = units[index];
if c == cu('*') {
return true;
}
if units.get(index + 1) == Some(&cu('?'))
&& (c == cu(']') || c == cu('.') || c == cu('+') || c == cu(')'))
{
return true;
}
if close_square != -1 && c == cu('[') && units.get(index + 1) != Some(&cu(']')) {
if close_square < index as i64 {
close_square = find_from(units, cu(']'), index);
}
if close_square > index as i64 {
if back_slash == -1 || back_slash > close_square {
return true;
}
back_slash = find_from(units, cu('\\'), index);
if back_slash == -1 || back_slash > close_square {
return true;
}
}
}
if close_curly != -1 && c == cu('{') && units.get(index + 1) != Some(&cu('}')) {
close_curly = find_from(units, cu('}'), index);
if close_curly > index as i64 {
back_slash = find_from(units, cu('\\'), index);
if back_slash == -1 || back_slash > close_curly {
return true;
}
}
}
if close_paren != -1
&& c == cu('(')
&& units.get(index + 1) == Some(&cu('?'))
&& matches!(units.get(index + 2).copied(), Some(x) if x == cu(':') || x == cu('!') || x == cu('='))
&& units.get(index + 3) != Some(&cu(')'))
{
close_paren = find_from(units, cu(')'), index);
if close_paren > index as i64 {
back_slash = find_from(units, cu('\\'), index);
if back_slash == -1 || back_slash > close_paren {
return true;
}
}
}
if pipe_index != -1 && c == cu('(') && units.get(index + 1) != Some(&cu('|')) {
if pipe_index < index as i64 {
pipe_index = find_from(units, cu('|'), index);
}
if pipe_index != -1 && units.get(pipe_index as usize + 1) != Some(&cu(')')) {
close_paren = find_from(units, cu(')'), pipe_index as usize);
if close_paren > pipe_index {
back_slash = find_from(units, cu('\\'), pipe_index as usize);
if back_slash == -1 || back_slash > close_paren {
return true;
}
}
}
}
if c == cu('\\') {
let open = units.get(index + 1).copied();
index += 2;
let close = if open == Some(cu('{')) {
Some(cu('}'))
} else if open == Some(cu('(')) {
Some(cu(')'))
} else if open == Some(cu('[')) {
Some(cu(']'))
} else {
None
};
if let Some(close) = close {
let n = find_from(units, close, index);
if n != -1 {
index = n as usize + 1;
}
}
if units.get(index) == Some(&cu('!')) {
return true;
}
} else {
index += 1;
}
}
false
}
fn posix_dirname(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
if len == 0 {
return ".".to_string();
}
let has_root = chars[0] == '/';
let mut end: i64 = -1;
let mut matched_slash = true;
let mut i = len as i64 - 1;
while i >= 1 {
if chars[i as usize] == '/' {
if !matched_slash {
end = i;
break;
}
} else {
matched_slash = false;
}
i -= 1;
}
if end == -1 {
return if has_root { "/" } else { "." }.to_string();
}
if has_root && end == 1 {
return "//".to_string();
}
chars[..end as usize].iter().collect()
}
fn is_enclosure(s: &str) -> bool {
let chars: Vec<char> = s.chars().collect();
let n = chars.len();
if n == 0 {
return false;
}
let start = match chars[n - 1] {
'}' => '{',
']' => '[',
_ => return false,
};
let Some(found) = chars.iter().position(|&c| c == start) else {
return false;
};
chars[found + 1..n - 1].contains(&'/')
}
fn is_globby(s: &str) -> bool {
let chars: Vec<char> = s.chars().collect();
if open_paren_tail(&chars) {
return true;
}
if matches!(chars.first(), Some('{' | '[')) {
return true;
}
for i in 1..chars.len() {
if matches!(chars[i], '{' | '[') && chars[i - 1] != '\\' {
return true;
}
}
is_glob(s)
}
fn open_paren_tail(chars: &[char]) -> bool {
let mut last = None;
for (i, &c) in chars.iter().enumerate() {
if c == '(' || c == ')' {
last = Some((i, c));
}
}
matches!(last, Some((i, '(')) if i + 1 < chars.len())
}
fn unescape_glob(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\'
&& matches!(
chars.get(i + 1),
Some('!' | '*' | '?' | '|' | '[' | ']' | '(' | ')' | '{' | '}')
)
{
out.push(chars[i + 1]);
i += 2;
} else {
out.push(chars[i]);
i += 1;
}
}
out
}