use std::collections::VecDeque;
use std::path::{Component, Path, PathBuf};
mod node;
mod nodewalker;
mod parser;
mod recursivewalker;
mod token;
use node::Node;
use nodewalker::NodeWalker;
use parser::parse;
use recursivewalker::RecursiveWalker;
#[derive(Debug)]
pub struct Glob {
nodes: Vec<Node>,
}
impl Glob {
pub fn new(pattern: &str) -> anyhow::Result<Glob> {
let mut nodes: Vec<Node> = vec![];
for comp in Path::new(pattern).components() {
let token = match comp {
Component::Prefix(s) => {
nodes.clear();
Node::LiteralComponents(PathBuf::from(s.as_os_str()))
}
Component::RootDir => {
if nodes.len() == 1 && nodes[0].is_literal_prefix_component() {
} else {
nodes.clear();
}
Node::LiteralComponents(PathBuf::from("/"))
}
Component::CurDir => continue,
Component::ParentDir => Node::LiteralComponents(PathBuf::from("..")),
Component::Normal(s) => {
let s = s.to_str().expect("str input to be representable as string");
match s {
"**" => Node::RecursiveMatch,
_ => parse(s)?,
}
}
};
match (&token, nodes.last_mut()) {
(
Node::LiteralComponents(ref literal),
Some(Node::LiteralComponents(ref mut path)),
) => *path = path.join(literal),
_ => nodes.push(token),
}
}
Ok(Glob { nodes })
}
pub fn walk<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
let walker = Walker::new(path.as_ref(), &self.nodes);
let mut results: Vec<PathBuf> = walker.collect();
results.sort();
results
}
}
fn new_binary_pattern_string() -> String {
String::from(if cfg!(windows) { "^(?i-u)" } else { "^(?-u)" })
}
#[cfg(windows)]
fn normalize_slashes(path: PathBuf) -> PathBuf {
use std::ffi::OsString;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
let mut normalized: Vec<u16> = path
.into_os_string()
.encode_wide()
.map(|c| if c == b'\\' as u16 { b'/' as u16 } else { c })
.collect();
const LONG_FILE_NAME_PREFIX: [u16; 4] = [b'/' as u16, b'/' as u16, b'?' as u16, b'/' as u16];
if normalized.starts_with(&LONG_FILE_NAME_PREFIX) {
for _ in 0..LONG_FILE_NAME_PREFIX.len() {
normalized.remove(0);
}
}
OsString::from_wide(&normalized).into()
}
#[cfg(not(windows))]
fn normalize_slashes(path: PathBuf) -> PathBuf {
path
}
struct Walker<'a> {
root: &'a Path,
stack: VecDeque<NodeWalker<'a>>,
recursive: VecDeque<RecursiveWalker>,
}
impl<'a> Walker<'a> {
fn new(root: &'a Path, nodes: &'a [Node]) -> Self {
let route = NodeWalker::new(nodes);
let mut stack = VecDeque::new();
stack.push_back(route);
Self {
root,
stack,
recursive: VecDeque::new(),
}
}
}
impl<'a> Iterator for Walker<'a> {
type Item = PathBuf;
fn next(&mut self) -> Option<PathBuf> {
while let Some(mut route) = self.stack.pop_front() {
if let Some(path) = route.next(self) {
self.stack.push_front(route);
return Some(path);
}
}
while let Some(mut route) = self.recursive.pop_front() {
if let Some(path) = route.next(self) {
self.recursive.push_front(route);
return Some(path);
}
}
None
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[allow(unused)]
fn make_dirs_in(root: &TempDir, dirs: &[&str]) -> anyhow::Result<()> {
for d in dirs {
let p = root.path().join(d);
std::fs::create_dir_all(p)?;
}
Ok(())
}
fn touch_file<P: AsRef<Path>>(path: P) -> anyhow::Result<()> {
eprintln!("touch_file {}", path.as_ref().display());
let _file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path.as_ref())?;
Ok(())
}
fn touch_files_in(root: &TempDir, files: &[&str]) -> anyhow::Result<()> {
for f in files {
let p = root.path().join(f);
let d = p.parent().unwrap();
std::fs::create_dir_all(d)?;
touch_file(p)?;
}
Ok(())
}
fn make_fixture() -> anyhow::Result<TempDir> {
#[cfg(unix)]
{
std::env::set_var("TMPDIR", std::env::temp_dir().canonicalize()?);
}
Ok(TempDir::new()?)
}
#[test]
fn test_simple() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(&root, &["src/lib.rs"])?;
let glob = Glob::new("src/*.rs")?;
assert_eq!(glob.walk(root), vec![PathBuf::from("src/lib.rs")]);
Ok(())
}
#[test]
fn test_non_relative() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(&root, &["src/lib.rs"])?;
let glob = Glob::new(&format!(
"{}/src/*.rs",
normalize_slashes(root.path().to_path_buf()).display()
))?;
assert_eq!(
glob.walk(&std::env::current_dir()?),
vec![normalize_slashes(root.path().join("src/lib.rs"))]
);
Ok(())
}
#[test]
fn non_utf8_node_match() -> anyhow::Result<()> {
let node = parse("*.rs")?;
use bstr::BStr;
use bstr::B;
let pound = BStr::new(B(b"\xa3.rs"));
eprintln!("pound is {:?}", pound);
eprintln!("node is {:?}", node);
assert_eq!(node.is_match(£), true);
Ok(())
}
#[test]
fn spaces_and_parens() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(&root, &["Program Files (x86)/Foo Bar/baz.exe"])?;
let glob = Glob::new("Program Files (x86)/*")?;
assert_eq!(
glob.walk(&root.path()),
vec![PathBuf::from("Program Files (x86)/Foo Bar")]
);
let glob = Glob::new(
normalize_slashes(root.path().join("Program Files (x86)/*"))
.to_str()
.unwrap(),
)?;
assert_eq!(
glob.walk(&root),
vec![root.path().join("Program Files (x86)/Foo Bar")]
);
assert_eq!(
glob.walk(&std::env::current_dir()?),
vec![root.path().join("Program Files (x86)/Foo Bar")]
);
let glob = Glob::new(
normalize_slashes(root.path().join("Program Files (x86)/*/baz.exe"))
.to_str()
.unwrap(),
)?;
assert_eq!(
glob.walk(&std::env::current_dir()?),
vec![root.path().join("Program Files (x86)/Foo Bar/baz.exe")]
);
Ok(())
}
#[test]
#[cfg(windows)]
fn case_insensitive() -> anyhow::Result<()> {
let node = parse("foo/bar.rs")?;
use bstr::B;
use bstr::BStr;
let upper = B(b"FOO/bAr.rs");
assert_eq!(node.is_match(BStr::new(&upper)), true);
Ok(())
}
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn test_non_utf8_on_disk() -> anyhow::Result<()> {
use bstr::ByteSlice;
use bstr::B;
if nix::sys::utsname::uname()
.unwrap()
.release()
.to_str()
.unwrap()
.contains("Microsoft")
{
return Ok(());
}
let root = make_fixture()?;
let pound = B(b"\xa3.rs").to_path()?;
if touch_file(root.path().join(£)).is_ok() {
let glob = Glob::new("*.rs")?;
assert_eq!(glob.walk(root), vec![pound.to_path_buf()]);
}
Ok(())
}
#[test]
fn test_lua_toml() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(
&root,
&["simple.lua", "assets/policy-extras/bar.lua", "assets/policy-extras/shaping.toml"],
)?;
let glob = Glob::new("**/*.{lua,toml}")?;
assert_eq!(
glob.walk(&root),
vec![
PathBuf::from("assets/policy-extras/bar.lua"),
PathBuf::from("assets/policy-extras/shaping.toml"),
PathBuf::from("simple.lua"),
]
);
Ok(())
}
#[test]
fn test_more() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(
&root,
&["foo/src/foo.rs", "bar/src/bar.rs", "bar/src/.bar.rs"],
)?;
let glob = Glob::new("*/src/*.rs")?;
assert_eq!(
glob.walk(&root),
vec![
PathBuf::from("bar/src/bar.rs"),
PathBuf::from("foo/src/foo.rs")
]
);
let glob = Glob::new("foo/src/*.rs")?;
assert_eq!(glob.walk(&root), vec![PathBuf::from("foo/src/foo.rs")]);
let glob = Glob::new("*/src/.*.rs")?;
assert_eq!(glob.walk(&root), vec![PathBuf::from("bar/src/.bar.rs")]);
let glob = Glob::new("*")?;
assert_eq!(
glob.walk(&root),
vec![PathBuf::from("bar"), PathBuf::from("foo")]
);
Ok(())
}
#[test]
fn test_doublestar() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(
&root,
&[
"foo/src/foo.rs",
"bar/src/bar.rs",
"woot/woot.rs",
"woot/.woot.rs",
],
)?;
let glob = Glob::new("**/*.rs")?;
assert_eq!(
glob.walk(&root),
vec![
PathBuf::from("bar/src/bar.rs"),
PathBuf::from("foo/src/foo.rs"),
PathBuf::from("woot/woot.rs")
]
);
let glob = Glob::new("**")?;
assert_eq!(
glob.walk(&root),
vec![
PathBuf::from("bar"),
PathBuf::from("foo"),
PathBuf::from("woot")
]
);
Ok(())
}
#[test]
fn glob_up() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(
&root,
&[
"foo/src/foo.rs",
"bar/src/bar.rs",
"woot/woot.rs",
"woot/.woot.rs",
],
)?;
let glob = Glob::new("../*/*.rs")?;
assert_eq!(
glob.walk(root.path().join("woot")),
vec![PathBuf::from("../woot/woot.rs")]
);
Ok(())
}
#[test]
fn alternative() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(&root, &["foo.rs", "bar.rs"])?;
let glob = Glob::new("{foo,bar}.rs")?;
assert_eq!(
glob.walk(&root),
vec![PathBuf::from("bar.rs"), PathBuf::from("foo.rs")]
);
Ok(())
}
#[test]
fn bogus_alternative() -> anyhow::Result<()> {
assert_eq!(
format!("{}", Glob::new("{{").unwrap_err()),
"cannot start an alternative inside an alternative"
);
assert_eq!(
format!("{}", Glob::new("{").unwrap_err()),
"missing closing alternative"
);
Ok(())
}
#[test]
fn class() -> anyhow::Result<()> {
let root = make_fixture()?;
touch_files_in(&root, &["foo.o", "foo.a"])?;
let glob = Glob::new("foo.[oa]")?;
assert_eq!(
glob.walk(&root),
vec![PathBuf::from("foo.a"), PathBuf::from("foo.o")]
);
let glob = Glob::new("foo.[[:alnum:]]")?;
assert_eq!(
glob.walk(&root),
vec![PathBuf::from("foo.a"), PathBuf::from("foo.o")]
);
let glob = Glob::new("foo.[![:alnum:]]")?;
assert_eq!(glob.walk(&root), Vec::<PathBuf>::new());
Ok(())
}
}