use crate::error::ZlobError;
use crate::ffi;
use crate::flags::ZlobFlags;
use std::ffi::{c_char, CString};
use std::ops::Index;
use std::slice;
pub struct Zlob {
inner: ffi::zlob_t,
}
impl Zlob {
#[inline]
pub fn len(&self) -> usize {
self.inner.zlo_pathc
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
pub fn get(&self, index: usize) -> Option<&str> {
if index >= self.len() {
return None;
}
unsafe {
let ptr = *self.inner.zlo_pathv.add(index) as *const u8;
let len = *self.inner.zlo_pathlen.add(index);
let bytes = slice::from_raw_parts(ptr, len);
Some(std::str::from_utf8_unchecked(bytes))
}
}
#[inline]
pub fn iter(&self) -> ZlobIter<'_> {
ZlobIter {
zlob: self,
front: 0,
back: self.len(),
}
}
pub fn as_strs(&self) -> Vec<&str> {
self.iter().collect()
}
pub fn to_strings(&self) -> Vec<String> {
self.iter().map(|s| s.to_string()).collect()
}
pub unsafe fn raw_parts(&self) -> (&[*mut c_char], &[usize]) {
let pathv = slice::from_raw_parts(self.inner.zlo_pathv, self.len());
let pathlen = slice::from_raw_parts(self.inner.zlo_pathlen, self.len());
(pathv, pathlen)
}
}
impl Drop for Zlob {
fn drop(&mut self) {
unsafe {
ffi::zlobfree(&mut self.inner);
}
}
}
impl Index<usize> for Zlob {
type Output = str;
fn index(&self, index: usize) -> &Self::Output {
self.get(index).expect("index out of bounds")
}
}
impl<'a> IntoIterator for &'a Zlob {
type Item = &'a str;
type IntoIter = ZlobIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
unsafe impl Send for Zlob {}
unsafe impl Sync for Zlob {}
pub struct ZlobIter<'a> {
zlob: &'a Zlob,
front: usize,
back: usize,
}
impl<'a> Iterator for ZlobIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.front >= self.back {
return None;
}
let item = self.zlob.get(self.front)?;
self.front += 1;
Some(item)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.back - self.front;
(remaining, Some(remaining))
}
fn count(self) -> usize {
self.back - self.front
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
if n >= self.back - self.front {
self.front = self.back;
return None;
}
self.front += n;
self.next()
}
fn last(mut self) -> Option<Self::Item> {
self.next_back()
}
}
impl ExactSizeIterator for ZlobIter<'_> {
fn len(&self) -> usize {
self.back - self.front
}
}
impl DoubleEndedIterator for ZlobIter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.front >= self.back {
return None;
}
self.back -= 1;
self.zlob.get(self.back)
}
fn nth_back(&mut self, n: usize) -> Option<Self::Item> {
if n >= self.back - self.front {
self.front = self.back;
return None;
}
self.back -= n;
self.next_back()
}
}
impl std::iter::FusedIterator for ZlobIter<'_> {}
pub fn zlob(pattern: &str, flags: ZlobFlags) -> Result<Option<Zlob>, ZlobError> {
let pattern_c = CString::new(pattern).map_err(|_| ZlobError::Aborted)?;
let mut inner = ffi::zlob_t::default();
let result = unsafe { ffi::zlob(pattern_c.as_ptr(), flags.bits(), None, &mut inner) };
match ZlobError::from_code(result) {
Ok(true) => Ok(Some(Zlob { inner })),
Ok(false) => Ok(None), Err(err) => Err(err),
}
}
pub fn zlob_at(
base_path: &str,
pattern: &str,
flags: ZlobFlags,
) -> Result<Option<Zlob>, ZlobError> {
let base_path_c = CString::new(base_path).map_err(|_| ZlobError::Aborted)?;
let pattern_c = CString::new(pattern).map_err(|_| ZlobError::Aborted)?;
let mut inner = ffi::zlob_t::default();
let result = unsafe {
ffi::zlob_at(
base_path_c.as_ptr(),
pattern_c.as_ptr(),
flags.bits(),
None,
&mut inner,
)
};
match ZlobError::from_code(result) {
Ok(true) => Ok(Some(Zlob { inner })),
Ok(false) => Ok(None), Err(err) => Err(err),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zlob_basic() {
let result = zlob("Cargo.toml", ZlobFlags::empty());
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(&result[0], "Cargo.toml");
}
#[test]
fn test_zlob_no_match() {
let result = zlob("nonexistent_file_12345.xyz", ZlobFlags::empty());
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_zlob_iterator() {
let result = zlob("*.toml", ZlobFlags::empty()).unwrap().unwrap();
let paths: Vec<&str> = result.iter().collect();
assert!(paths.contains(&"Cargo.toml"));
}
#[test]
fn test_zlob_double_ended_iterator() {
let result = zlob("*.toml", ZlobFlags::empty()).unwrap().unwrap();
let mut iter = result.iter();
let first = iter.next();
let last = iter.next_back();
assert!(first.is_some());
assert!(last.is_none());
let result = zlob("src/*.rs", ZlobFlags::empty()).unwrap().unwrap();
let mut iter = result.iter();
let first = iter.next().unwrap();
let last = iter.next_back().unwrap();
assert_ne!(first, last);
}
#[test]
fn test_zlob_exact_size_iterator() {
let result = zlob("*.toml", ZlobFlags::empty()).unwrap().unwrap();
let iter = result.iter();
assert_eq!(iter.len(), 1);
assert_eq!(result.len(), 1);
}
#[test]
fn test_zlob_brace_expansion() {
let result = zlob("*.toml", ZlobFlags::empty()).unwrap().unwrap();
assert!(result.len() >= 1);
let result = zlob("*.lock", ZlobFlags::empty()).unwrap().unwrap();
assert!(result.len() >= 1);
let result = zlob("*.{toml,lock}", ZlobFlags::empty()).unwrap();
assert!(result.is_none());
let result = zlob("*.{toml,lock}", ZlobFlags::BRACE).unwrap().unwrap();
assert!(matches!(result.len(), 2));
}
#[test]
fn test_recursive_globbing() {
let result = zlob(
"**/*.{rs,toml}",
ZlobFlags::BRACE | ZlobFlags::DOUBLESTAR_RECURSIVE,
)
.unwrap()
.unwrap();
for path in &result {
println!("{}", path);
}
assert!(result.len() >= 8);
}
#[test]
fn test_extglob_filesystem_negation() {
use std::fs::File;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let dir_path = dir.path();
File::create(dir_path.join("app.js")).unwrap();
File::create(dir_path.join("app.ts")).unwrap();
File::create(dir_path.join("app.css")).unwrap();
File::create(dir_path.join("app.html")).unwrap();
let pattern = dir_path.join("app.!(js|ts)");
let pattern_str = pattern.to_str().unwrap();
let result = zlob(pattern_str, ZlobFlags::EXTGLOB).unwrap().unwrap();
assert_eq!(result.len(), 2);
let paths: Vec<&str> = result.iter().collect();
assert!(paths.iter().any(|p| p.ends_with("app.css")));
assert!(paths.iter().any(|p| p.ends_with("app.html")));
assert!(!paths.iter().any(|p| p.ends_with("app.js")));
assert!(!paths.iter().any(|p| p.ends_with("app.ts")));
}
#[test]
fn test_extglob_filesystem_exactly_one() {
use std::fs::File;
use tempfile::tempdir;
let dir = tempdir().unwrap();
let dir_path = dir.path();
File::create(dir_path.join("foo.txt")).unwrap();
File::create(dir_path.join("bar.txt")).unwrap();
File::create(dir_path.join("baz.txt")).unwrap();
File::create(dir_path.join("qux.txt")).unwrap();
let pattern = dir_path.join("@(foo|bar).txt");
let pattern_str = pattern.to_str().unwrap();
let result = zlob(pattern_str, ZlobFlags::EXTGLOB).unwrap().unwrap();
assert_eq!(result.len(), 2);
let paths: Vec<&str> = result.iter().collect();
assert!(paths.iter().any(|p| p.ends_with("foo.txt")));
assert!(paths.iter().any(|p| p.ends_with("bar.txt")));
assert!(!paths.iter().any(|p| p.ends_with("baz.txt")));
assert!(!paths.iter().any(|p| p.ends_with("qux.txt")));
}
}