aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--crates/shirabe-external-packages/Cargo.toml1
-rw-r--r--crates/shirabe-external-packages/src/symfony/finder/finder.rs773
-rw-r--r--crates/shirabe-external-packages/src/symfony/finder/glob.rs106
-rw-r--r--crates/shirabe/tests/cli.rs1
5 files changed, 821 insertions, 61 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9d585bf..112c0dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1027,6 +1027,7 @@ name = "shirabe-external-packages"
version = "0.0.1"
dependencies = [
"anyhow",
+ "chrono",
"indexmap",
"regex",
"shirabe-php-shim",
diff --git a/crates/shirabe-external-packages/Cargo.toml b/crates/shirabe-external-packages/Cargo.toml
index 02e6a7a..f274eac 100644
--- a/crates/shirabe-external-packages/Cargo.toml
+++ b/crates/shirabe-external-packages/Cargo.toml
@@ -7,6 +7,7 @@ edition.workspace = true
shirabe-php-shim.workspace = true
shirabe-semver.workspace = true
anyhow.workspace = true
+chrono.workspace = true
indexmap.workspace = true
regex.workspace = true
diff --git a/crates/shirabe-external-packages/src/symfony/finder/finder.rs b/crates/shirabe-external-packages/src/symfony/finder/finder.rs
index 9ca17d5..95ca5a4 100644
--- a/crates/shirabe-external-packages/src/symfony/finder/finder.rs
+++ b/crates/shirabe-external-packages/src/symfony/finder/finder.rs
@@ -1,19 +1,132 @@
//! ref: composer/vendor/symfony/finder/Finder.php
+//!
+//! The iterator pipeline of `searchInDirectory()` is reproduced inline here
+//! instead of as separate `Iterator\*` classes. Entries are materialized as
+//! `PathBuf` (the SplFileInfo replacement); the relative-path information that
+//! `RecursiveDirectoryIterator` attaches to each `SplFileInfo` is carried on the
+//! private `Entry` struct so the path/exclude filters keep their exact behavior.
+use crate::composer::pcre::{CaptureKey, Preg};
+use crate::symfony::finder::glob::Glob;
+use chrono::{NaiveDate, NaiveDateTime};
+use indexmap::IndexMap;
+use shirabe_php_shim::{file_exists, glob, is_dir, preg_quote, rtrim};
+use std::cell::RefCell;
+use std::collections::HashSet;
use std::path::{Path, PathBuf};
+use std::time::UNIX_EPOCH;
+
+const IGNORE_VCS_FILES: i64 = 1;
+const IGNORE_DOT_FILES: i64 = 2;
+
+const ONLY_FILES: i64 = 1;
+const ONLY_DIRECTORIES: i64 = 2;
+
+const VCS_PATTERNS: [&str; 9] = [
+ ".svn",
+ "_svn",
+ "CVS",
+ "_darcs",
+ ".arch-params",
+ ".monotone",
+ ".bzr",
+ ".git",
+ ".hg",
+];
/// Helper trait so `Finder::exclude` accepts both single strings and slices
/// (PHP's variadic / array argument compatibility).
-pub trait IntoFinderExclude {}
-impl IntoFinderExclude for &str {}
-impl IntoFinderExclude for String {}
-impl IntoFinderExclude for &String {}
-impl IntoFinderExclude for &[String] {}
-impl IntoFinderExclude for &Vec<String> {}
-impl IntoFinderExclude for Vec<String> {}
+pub trait IntoFinderExclude {
+ fn into_exclude(self) -> Vec<String>;
+}
+impl IntoFinderExclude for &str {
+ fn into_exclude(self) -> Vec<String> {
+ vec![self.to_string()]
+ }
+}
+impl IntoFinderExclude for String {
+ fn into_exclude(self) -> Vec<String> {
+ vec![self]
+ }
+}
+impl IntoFinderExclude for &String {
+ fn into_exclude(self) -> Vec<String> {
+ vec![self.clone()]
+ }
+}
+impl IntoFinderExclude for &[String] {
+ fn into_exclude(self) -> Vec<String> {
+ self.to_vec()
+ }
+}
+impl IntoFinderExclude for &Vec<String> {
+ fn into_exclude(self) -> Vec<String> {
+ self.clone()
+ }
+}
+impl IntoFinderExclude for Vec<String> {
+ fn into_exclude(self) -> Vec<String> {
+ self
+ }
+}
-#[derive(Debug)]
-pub struct Finder;
+/// The sort strategy. Mirrors the `$sort` property which is either `false`, an
+/// `Iterator\SortableIterator::SORT_BY_*` constant, or a PHP callback.
+enum Sort {
+ None,
+ ByName,
+ ByAccessedTime,
+ Closure(RefCell<Box<dyn FnMut(&PathBuf, &PathBuf) -> i64>>),
+}
+
+/// One traversal result, replacing `Symfony\Component\Finder\SplFileInfo`.
+struct Entry {
+ pathname: PathBuf,
+ /// `getRelativePath()`: the directory of the entry relative to the search root.
+ relative_path: String,
+ /// `getRelativePathname()`: the full path of the entry relative to the search root.
+ relative_pathname: String,
+ /// `getFilename()`: the basename.
+ filename: String,
+ depth: i64,
+ is_dir: bool,
+ is_file: bool,
+}
+
+pub struct Finder {
+ mode: i64,
+ names: Vec<String>,
+ not_names: Vec<String>,
+ exclude: Vec<String>,
+ filters: Vec<RefCell<Box<dyn FnMut(&Path) -> bool>>>,
+ depths: Vec<(String, i64)>,
+ follow_links: bool,
+ reverse_sorting: bool,
+ sort: Sort,
+ ignore: i64,
+ dirs: Vec<String>,
+ dates: Vec<(String, i64)>,
+ paths: Vec<String>,
+ not_paths: Vec<String>,
+}
+
+impl std::fmt::Debug for Finder {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Finder")
+ .field("mode", &self.mode)
+ .field("names", &self.names)
+ .field("not_names", &self.not_names)
+ .field("exclude", &self.exclude)
+ .field("depths", &self.depths)
+ .field("follow_links", &self.follow_links)
+ .field("ignore", &self.ignore)
+ .field("dirs", &self.dirs)
+ .field("dates", &self.dates)
+ .field("paths", &self.paths)
+ .field("not_paths", &self.not_paths)
+ .finish_non_exhaustive()
+ }
+}
impl Default for Finder {
fn default() -> Self {
@@ -23,117 +136,654 @@ impl Default for Finder {
impl Finder {
pub fn new() -> Self {
- todo!()
+ Self {
+ mode: 0,
+ names: Vec::new(),
+ not_names: Vec::new(),
+ exclude: Vec::new(),
+ filters: Vec::new(),
+ depths: Vec::new(),
+ follow_links: false,
+ reverse_sorting: false,
+ sort: Sort::None,
+ ignore: IGNORE_VCS_FILES | IGNORE_DOT_FILES,
+ dirs: Vec::new(),
+ dates: Vec::new(),
+ paths: Vec::new(),
+ not_paths: Vec::new(),
+ }
}
pub fn create() -> Self {
- todo!()
+ Self::new()
}
pub fn files(&mut self) -> &mut Self {
- todo!()
+ self.mode = ONLY_FILES;
+
+ self
}
pub fn directories(&mut self) -> &mut Self {
- todo!()
+ self.mode = ONLY_DIRECTORIES;
+
+ self
}
- pub fn depth(&mut self, _level: i64) -> &mut Self {
- todo!()
+ pub fn depth(&mut self, level: i64) -> &mut Self {
+ // `NumberComparator` over an integer always yields the `==` operator.
+ self.depths.push(("==".to_string(), level));
+
+ self
}
- pub fn r#in(&mut self, _dirs: impl AsRef<Path>) -> &mut Self {
- todo!()
+ pub fn r#in(&mut self, dirs: impl AsRef<Path>) -> &mut Self {
+ let dir = dirs.as_ref().to_string_lossy().into_owned();
+ let mut resolved_dirs: Vec<String> = Vec::new();
+
+ if is_dir(&dir) {
+ resolved_dirs.push(self.normalize_dir(&dir));
+ } else {
+ // GLOB_ONLYDIR is emulated by retaining directory matches only.
+ // TODO(phase-c): wildcard `in()` paths depend on `shirabe_php_shim::glob`, which is
+ // still `todo!()`; only the real-directory branch above currently resolves.
+ let mut globbed: Vec<String> =
+ glob(&dir).into_iter().filter(|path| is_dir(path)).collect();
+ if !globbed.is_empty() {
+ globbed.sort();
+ for g in &globbed {
+ resolved_dirs.push(self.normalize_dir(g));
+ }
+ } else {
+ panic!("The \"{dir}\" directory does not exist.");
+ }
+ }
+
+ self.dirs.extend(resolved_dirs);
+
+ self
}
- pub fn filter(&mut self, _closure: Box<dyn FnMut(&std::path::Path) -> bool>) -> &mut Self {
- todo!()
+ pub fn filter(&mut self, closure: Box<dyn FnMut(&std::path::Path) -> bool>) -> &mut Self {
+ self.filters.push(RefCell::new(closure));
+
+ self
}
pub fn follow_links(&mut self) -> &mut Self {
- todo!()
+ self.follow_links = true;
+
+ self
}
- pub fn exclude<E: IntoFinderExclude>(&mut self, _exclude: E) -> &mut Self {
- todo!()
+ pub fn exclude<E: IntoFinderExclude>(&mut self, exclude: E) -> &mut Self {
+ self.exclude.extend(exclude.into_exclude());
+
+ self
}
- pub fn ignore_vcs(&mut self, _ignore_vcs: bool) -> &mut Self {
- todo!()
+ pub fn ignore_vcs(&mut self, ignore_vcs: bool) -> &mut Self {
+ if ignore_vcs {
+ self.ignore |= IGNORE_VCS_FILES;
+ } else {
+ self.ignore &= !IGNORE_VCS_FILES;
+ }
+
+ self
}
- pub fn ignore_dot_files(&mut self, _ignore_dot_files: bool) -> &mut Self {
- todo!()
+ pub fn ignore_dot_files(&mut self, ignore_dot_files: bool) -> &mut Self {
+ if ignore_dot_files {
+ self.ignore |= IGNORE_DOT_FILES;
+ } else {
+ self.ignore &= !IGNORE_DOT_FILES;
+ }
+
+ self
}
- pub fn not_name(&mut self, _pattern: &str) -> &mut Self {
- todo!()
+ pub fn not_name(&mut self, pattern: &str) -> &mut Self {
+ self.not_names.push(pattern.to_string());
+
+ self
}
- pub fn not_path(&mut self, _pattern: &str) -> &mut Self {
- todo!()
+ pub fn not_path(&mut self, pattern: &str) -> &mut Self {
+ self.not_paths.push(pattern.to_string());
+
+ self
}
- pub fn name(&mut self, _pattern: &str) -> &mut Self {
- todo!()
+ pub fn name(&mut self, pattern: &str) -> &mut Self {
+ self.names.push(pattern.to_string());
+
+ self
}
- pub fn sort<F>(&mut self, _comparator: F) -> &mut Self
+ pub fn sort<F>(&mut self, comparator: F) -> &mut Self
where
- F: FnMut(&PathBuf, &PathBuf) -> i64,
+ F: FnMut(&PathBuf, &PathBuf) -> i64 + 'static,
{
- todo!()
+ self.sort = Sort::Closure(RefCell::new(Box::new(comparator)));
+
+ self
}
pub fn sort_by_name(&mut self) -> &mut Self {
- todo!()
+ self.sort = Sort::ByName;
+
+ self
}
pub fn sort_by_accessed_time(&mut self) -> &mut Self {
- todo!()
+ self.sort = Sort::ByAccessedTime;
+
+ self
}
- pub fn date(&mut self, _date: &str) -> &mut Self {
- todo!()
+ pub fn date(&mut self, date: &str) -> &mut Self {
+ self.dates.push(parse_date_comparator(date));
+
+ self
}
pub fn get_iterator(&self) -> FinderIterator {
- todo!()
+ FinderIterator {
+ items: self.collect_paths(),
+ pos: 0,
+ }
}
pub fn iter(&self) -> impl Iterator<Item = PathBuf> {
- todo!();
- std::iter::empty()
+ self.get_iterator()
}
pub fn len(&self) -> usize {
- todo!()
+ self.collect_paths().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
+
+ fn normalize_dir(&self, dir: &str) -> String {
+ if dir == "/" {
+ return dir.to_string();
+ }
+
+ let dir = rtrim(dir, Some("/"));
+
+ if Preg::is_match("#^(ssh2\\.)?s?ftp://#", &dir) {
+ format!("{dir}/")
+ } else {
+ dir
+ }
+ }
+
+ fn collect_paths(&self) -> Vec<PathBuf> {
+ if self.dirs.is_empty() {
+ panic!("You must call one of in() or append() methods before iterating over a Finder.");
+ }
+
+ let mut entries: Vec<Entry> = Vec::new();
+ for dir in &self.dirs {
+ self.search_in_directory(dir, &mut entries);
+ }
+
+ if !matches!(self.sort, Sort::None) || self.reverse_sorting {
+ self.apply_sort(&mut entries);
+ }
+
+ entries.into_iter().map(|entry| entry.pathname).collect()
+ }
+
+ fn search_in_directory(&self, dir: &str, out: &mut Vec<Entry>) {
+ let mut exclude = self.exclude.clone();
+ let mut not_paths = self.not_paths.clone();
+
+ if IGNORE_VCS_FILES == (IGNORE_VCS_FILES & self.ignore) {
+ exclude.extend(VCS_PATTERNS.iter().map(|p| p.to_string()));
+ }
+
+ if IGNORE_DOT_FILES == (IGNORE_DOT_FILES & self.ignore) {
+ not_paths.push("#(^|/)\\..+(/|$)#".to_string());
+ }
+
+ let mut min_depth = 0i64;
+ let mut max_depth = i64::MAX;
+ for (operator, target) in &self.depths {
+ match operator.as_str() {
+ ">" => min_depth = target + 1,
+ ">=" => min_depth = *target,
+ "<" => max_depth = target - 1,
+ "<=" => max_depth = *target,
+ _ => {
+ min_depth = *target;
+ max_depth = *target;
+ }
+ }
+ }
+
+ let (excluded_dirs, excluded_pattern) = build_exclude(&exclude);
+
+ let mut raw: Vec<Entry> = Vec::new();
+ let root = Path::new(dir);
+ self.walk(
+ root,
+ "",
+ 0,
+ max_depth,
+ &excluded_dirs,
+ &excluded_pattern,
+ &mut raw,
+ );
+
+ let match_names: Vec<String> = self.names.iter().map(|p| to_regex_filename(p)).collect();
+ let nomatch_names: Vec<String> = self
+ .not_names
+ .iter()
+ .map(|p| to_regex_filename(p))
+ .collect();
+ let match_paths: Vec<String> = self.paths.iter().map(|p| to_regex_path(p)).collect();
+ let nomatch_paths: Vec<String> = not_paths.iter().map(|p| to_regex_path(p)).collect();
+
+ let has_name_filter = !self.names.is_empty() || !self.not_names.is_empty();
+ let has_path_filter = !self.paths.is_empty() || !not_paths.is_empty();
+
+ for entry in raw {
+ if entry.depth < min_depth {
+ continue;
+ }
+
+ if self.mode != 0 {
+ if ONLY_DIRECTORIES == (ONLY_DIRECTORIES & self.mode) && entry.is_file {
+ continue;
+ }
+ if ONLY_FILES == (ONLY_FILES & self.mode) && entry.is_dir {
+ continue;
+ }
+ }
+
+ if has_name_filter && !is_accepted(&entry.filename, &match_names, &nomatch_names) {
+ continue;
+ }
+
+ if !self.dates.is_empty() {
+ if !file_exists(&entry.pathname) {
+ continue;
+ }
+ let filedate = mtime(&entry.pathname);
+ if !self
+ .dates
+ .iter()
+ .all(|(operator, target)| comparator_test(operator, filedate, *target))
+ {
+ continue;
+ }
+ }
+
+ if !self.filters.is_empty()
+ && !self
+ .filters
+ .iter()
+ .all(|filter| (*filter.borrow_mut())(&entry.pathname))
+ {
+ continue;
+ }
+
+ if has_path_filter
+ && !is_accepted(&entry.relative_pathname, &match_paths, &nomatch_paths)
+ {
+ continue;
+ }
+
+ out.push(entry);
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn walk(
+ &self,
+ dir: &Path,
+ relative_dir: &str,
+ depth: i64,
+ max_depth: i64,
+ excluded_dirs: &HashSet<String>,
+ excluded_pattern: &Option<String>,
+ out: &mut Vec<Entry>,
+ ) {
+ // `RecursiveDirectoryIterator::SKIP_DOTS` is implicit: read_dir omits "." and "..".
+ // TODO(phase-c): unreadable directories are skipped here; the SplFileInfo-less,
+ // non-fallible iterator signatures cannot surface the AccessDeniedException that PHP
+ // throws when ignoreUnreadableDirs is false.
+ let read = match std::fs::read_dir(dir) {
+ Ok(read) => read,
+ Err(_) => return,
+ };
+
+ for entry in read {
+ let entry = match entry {
+ Ok(entry) => entry,
+ Err(_) => continue,
+ };
+
+ let filename = entry.file_name().to_string_lossy().into_owned();
+ let pathname = entry.path();
+ let relative_pathname = if relative_dir.is_empty() {
+ filename.clone()
+ } else {
+ format!("{relative_dir}/{filename}")
+ };
+
+ let metadata = std::fs::metadata(&pathname);
+ let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
+ let is_file = metadata.as_ref().map(|m| m.is_file()).unwrap_or(false);
+
+ let entry = Entry {
+ pathname: pathname.clone(),
+ relative_path: relative_dir.to_string(),
+ relative_pathname: relative_pathname.clone(),
+ filename,
+ depth,
+ is_dir,
+ is_file,
+ };
+
+ if !exclude_accept(&entry, excluded_dirs, excluded_pattern) {
+ continue;
+ }
+
+ let is_symlink = std::fs::symlink_metadata(&pathname)
+ .map(|m| m.file_type().is_symlink())
+ .unwrap_or(false);
+ let can_recurse = is_dir && (self.follow_links || !is_symlink);
+
+ out.push(entry);
+
+ if can_recurse && (max_depth == i64::MAX || depth < max_depth) {
+ self.walk(
+ &pathname,
+ &relative_pathname,
+ depth + 1,
+ max_depth,
+ excluded_dirs,
+ excluded_pattern,
+ out,
+ );
+ }
+ }
+ }
+
+ fn apply_sort(&self, entries: &mut [Entry]) {
+ let order: i64 = if self.reverse_sorting { -1 } else { 1 };
+
+ match &self.sort {
+ Sort::None => {
+ if self.reverse_sorting {
+ entries.reverse();
+ }
+ }
+ Sort::ByName => {
+ entries.sort_by(|a, b| {
+ let pa = realpath_or_pathname(&a.pathname);
+ let pb = realpath_or_pathname(&b.pathname);
+ apply_order(pa.as_bytes().cmp(pb.as_bytes()), order)
+ });
+ }
+ Sort::ByAccessedTime => {
+ entries.sort_by(|a, b| {
+ apply_order((atime(&a.pathname) - atime(&b.pathname)).cmp(&0), order)
+ });
+ }
+ Sort::Closure(comparator) => {
+ let mut comparator = comparator.borrow_mut();
+ entries.sort_by(|a, b| {
+ let result = (*comparator)(&a.pathname, &b.pathname);
+ let result = if self.reverse_sorting {
+ -result
+ } else {
+ result
+ };
+ result.cmp(&0)
+ });
+ }
+ }
+ }
}
-impl IntoIterator for &Finder {
- type Item = PathBuf;
- type IntoIter = std::vec::IntoIter<PathBuf>;
+/// Reproduces `ExcludeDirectoryFilterIterator`'s constructor split between simple
+/// directory names and `/`-containing path patterns.
+fn build_exclude(directories: &[String]) -> (HashSet<String>, Option<String>) {
+ let mut excluded_dirs = HashSet::new();
+ let mut patterns: Vec<String> = Vec::new();
- fn into_iter(self) -> Self::IntoIter {
- todo!()
+ for directory in directories {
+ let directory = rtrim(directory, Some("/"));
+ // The inner iterator is always recursive, so only `/`-containing names become patterns.
+ if directory.contains('/') {
+ patterns.push(preg_quote(&directory, Some('#')));
+ } else {
+ excluded_dirs.insert(directory);
+ }
+ }
+
+ let excluded_pattern = if patterns.is_empty() {
+ None
+ } else {
+ Some(format!("#(?:^|/)(?:{})(?:/|$)#", patterns.join("|")))
+ };
+
+ (excluded_dirs, excluded_pattern)
+}
+
+/// `ExcludeDirectoryFilterIterator::accept`.
+fn exclude_accept(
+ entry: &Entry,
+ excluded_dirs: &HashSet<String>,
+ excluded_pattern: &Option<String>,
+) -> bool {
+ if excluded_dirs.contains(&entry.filename) && entry.is_dir {
+ return false;
+ }
+
+ if let Some(pattern) = excluded_pattern {
+ let path = if entry.is_dir {
+ &entry.relative_pathname
+ } else {
+ &entry.relative_path
+ };
+ let path = path.replace('\\', "/");
+
+ return !Preg::is_match(pattern, &path);
+ }
+
+ true
+}
+
+/// `FilenameFilterIterator::toRegex`.
+fn to_regex_filename(pattern: &str) -> String {
+ if is_regex(pattern) {
+ pattern.to_string()
+ } else {
+ Glob::to_regex(pattern, true, true)
+ }
+}
+
+/// `PathFilterIterator::toRegex`.
+fn to_regex_path(pattern: &str) -> String {
+ if is_regex(pattern) {
+ pattern.to_string()
+ } else {
+ format!("/{}/", preg_quote(pattern, Some('/')))
}
}
+/// `MultiplePcreFilterIterator::isAccepted`.
+fn is_accepted(string: &str, match_regexps: &[String], nomatch_regexps: &[String]) -> bool {
+ for regex in nomatch_regexps {
+ if Preg::is_match(regex, string) {
+ return false;
+ }
+ }
+
+ if !match_regexps.is_empty() {
+ for regex in match_regexps {
+ if Preg::is_match(regex, string) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ true
+}
+
+/// `MultiplePcreFilterIterator::isRegex`.
+fn is_regex(str: &str) -> bool {
+ // PHP 8.2+ available modifiers.
+ let available_modifiers = "imsxuADUn";
+
+ let mut matches: IndexMap<CaptureKey, String> = IndexMap::new();
+ let pattern = format!("/^(.{{3,}}?)[{available_modifiers}]*$/");
+ if Preg::is_match3(&pattern, str, Some(&mut matches)) {
+ let group = matches
+ .get(&CaptureKey::ByIndex(1))
+ .cloned()
+ .unwrap_or_default();
+ let bytes = group.as_bytes();
+ let start = bytes
+ .first()
+ .map(|b| (*b as char).to_string())
+ .unwrap_or_default();
+ let end = bytes
+ .last()
+ .map(|b| (*b as char).to_string())
+ .unwrap_or_default();
+
+ if start == end {
+ return !Preg::is_match("/[*?[:alnum:] \\\\]/", &start);
+ }
+
+ for (open, close) in [("{", "}"), ("(", ")"), ("[", "]"), ("<", ">")] {
+ if start == open && end == close {
+ return true;
+ }
+ }
+ }
+
+ false
+}
+
+/// `Comparator::test`.
+fn comparator_test(operator: &str, test: i64, target: i64) -> bool {
+ match operator {
+ ">" => test > target,
+ ">=" => test >= target,
+ "<" => test < target,
+ "<=" => test <= target,
+ "!=" => test != target,
+ _ => test == target,
+ }
+}
+
+/// `DateComparator::__construct`, returning `(operator, target unix timestamp)`.
+fn parse_date_comparator(test: &str) -> (String, i64) {
+ let pattern = "#^\\s*(==|!=|[<>]=?|after|since|before|until)?\\s*(.+?)\\s*$#i";
+ let mut matches: IndexMap<CaptureKey, String> = IndexMap::new();
+ if !Preg::is_match3(pattern, test, Some(&mut matches)) {
+ panic!("Don't understand \"{test}\" as a date test.");
+ }
+
+ let date = matches
+ .get(&CaptureKey::ByIndex(2))
+ .cloned()
+ .unwrap_or_default();
+ let target = parse_datetime_to_unix(&date);
+
+ let mut operator = matches
+ .get(&CaptureKey::ByIndex(1))
+ .cloned()
+ .unwrap_or_else(|| "==".to_string());
+ if operator == "since" || operator == "after" {
+ operator = ">".to_string();
+ }
+ if operator == "until" || operator == "before" {
+ operator = "<".to_string();
+ }
+
+ (operator, target)
+}
+
+/// `(new \DateTime($s))->format('U')`.
+///
+/// TODO(phase-c): PHP's `\DateTime` accepts any strtotime() expression, but only the
+/// `Y-m-d H:i:s` / `Y-m-d` shapes produced by the callers are parsed here. The components are
+/// interpreted as UTC (not PHP's local timezone) so the timestamp round-trips with the
+/// `chrono::Utc`-derived thresholds the callers format from.
+fn parse_datetime_to_unix(s: &str) -> i64 {
+ if let Ok(datetime) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
+ return datetime.and_utc().timestamp();
+ }
+ if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
+ return date
+ .and_hms_opt(0, 0, 0)
+ .expect("midnight is always valid")
+ .and_utc()
+ .timestamp();
+ }
+
+ panic!("\"{s}\" is not a valid date.");
+}
+
+fn apply_order(ordering: std::cmp::Ordering, order: i64) -> std::cmp::Ordering {
+ if order < 0 {
+ ordering.reverse()
+ } else {
+ ordering
+ }
+}
+
+/// `$file->getRealPath() ?: $file->getPathname()`.
+fn realpath_or_pathname(path: &Path) -> String {
+ std::fs::canonicalize(path)
+ .map(|resolved| resolved.to_string_lossy().into_owned())
+ .unwrap_or_else(|_| path.to_string_lossy().into_owned())
+}
+
+fn mtime(path: &Path) -> i64 {
+ file_unix_time(path, |metadata| metadata.modified())
+}
+
+fn atime(path: &Path) -> i64 {
+ file_unix_time(path, |metadata| metadata.accessed())
+}
+
+fn file_unix_time(
+ path: &Path,
+ accessor: fn(&std::fs::Metadata) -> std::io::Result<std::time::SystemTime>,
+) -> i64 {
+ std::fs::metadata(path)
+ .ok()
+ .and_then(|metadata| accessor(&metadata).ok())
+ .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
+ .map(|duration| duration.as_secs() as i64)
+ .unwrap_or(0)
+}
+
#[derive(Debug)]
-pub struct FinderIterator;
+pub struct FinderIterator {
+ items: Vec<PathBuf>,
+ pos: usize,
+}
impl FinderIterator {
pub fn valid(&self) -> bool {
- todo!()
+ self.pos < self.items.len()
}
pub fn current(&self) -> PathBuf {
- todo!()
+ self.items[self.pos].clone()
}
}
@@ -141,7 +791,22 @@ impl Iterator for FinderIterator {
type Item = PathBuf;
fn next(&mut self) -> Option<PathBuf> {
- todo!()
+ if self.pos < self.items.len() {
+ let item = self.items[self.pos].clone();
+ self.pos += 1;
+ Some(item)
+ } else {
+ None
+ }
+ }
+}
+
+impl IntoIterator for &Finder {
+ type Item = PathBuf;
+ type IntoIter = std::vec::IntoIter<PathBuf>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.collect_paths().into_iter()
}
}
@@ -150,7 +815,7 @@ impl IntoIterator for Finder {
type IntoIter = std::vec::IntoIter<PathBuf>;
fn into_iter(self) -> Self::IntoIter {
- todo!()
+ self.collect_paths().into_iter()
}
}
@@ -159,6 +824,6 @@ impl IntoIterator for &mut Finder {
type IntoIter = std::vec::IntoIter<PathBuf>;
fn into_iter(self) -> Self::IntoIter {
- todo!()
+ self.collect_paths().into_iter()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/finder/glob.rs b/crates/shirabe-external-packages/src/symfony/finder/glob.rs
index eb79147..fe5398f 100644
--- a/crates/shirabe-external-packages/src/symfony/finder/glob.rs
+++ b/crates/shirabe-external-packages/src/symfony/finder/glob.rs
@@ -4,11 +4,105 @@
pub struct Glob;
impl Glob {
- pub fn to_regex(
- _glob: &str,
- _strict_leading_dot: bool,
- _strict_wildcard_slash: bool,
- ) -> String {
- todo!()
+ pub fn to_regex(glob: &str, strict_leading_dot: bool, strict_wildcard_slash: bool) -> String {
+ let delimiter = '#';
+ let mut first_byte = true;
+ let mut escaping = false;
+ let mut in_curlies: i64 = 0;
+ let mut regex = String::new();
+ let bytes = glob.as_bytes();
+ let size_glob = bytes.len();
+ let mut i = 0;
+ while i < size_glob {
+ let mut car = (bytes[i] as char).to_string();
+ if first_byte && strict_leading_dot && car != "." {
+ regex.push_str("(?=[^\\.])");
+ }
+
+ first_byte = car == "/";
+
+ if first_byte
+ && strict_wildcard_slash
+ && i + 2 < size_glob
+ && bytes[i + 1] == b'*'
+ && bytes[i + 2] == b'*'
+ && (i + 3 >= size_glob || bytes[i + 3] == b'/')
+ {
+ let mut new_car = String::from("[^/]++/");
+ if i + 3 >= size_glob {
+ new_car.push('?');
+ }
+
+ if strict_leading_dot {
+ new_car = format!("(?=[^\\.]){}", new_car);
+ }
+
+ new_car = format!("/(?:{})*", new_car);
+ i += 2 + usize::from(i + 3 < size_glob);
+
+ if delimiter == '/' {
+ new_car = new_car.replace('/', "\\/");
+ }
+
+ car = new_car;
+ }
+
+ if car == delimiter.to_string()
+ || car == "."
+ || car == "("
+ || car == ")"
+ || car == "|"
+ || car == "+"
+ || car == "^"
+ || car == "$"
+ {
+ regex.push('\\');
+ regex.push_str(&car);
+ } else if car == "*" {
+ regex.push_str(if escaping {
+ "\\*"
+ } else if strict_wildcard_slash {
+ "[^/]*"
+ } else {
+ ".*"
+ });
+ } else if car == "?" {
+ regex.push_str(if escaping {
+ "\\?"
+ } else if strict_wildcard_slash {
+ "[^/]"
+ } else {
+ "."
+ });
+ } else if car == "{" {
+ regex.push_str(if escaping { "\\{" } else { "(" });
+ if !escaping {
+ in_curlies += 1;
+ }
+ } else if car == "}" && in_curlies > 0 {
+ regex.push_str(if escaping { "}" } else { ")" });
+ if !escaping {
+ in_curlies -= 1;
+ }
+ } else if car == "," && in_curlies > 0 {
+ regex.push_str(if escaping { "," } else { "|" });
+ } else if car == "\\" {
+ if escaping {
+ regex.push_str("\\\\");
+ escaping = false;
+ } else {
+ escaping = true;
+ }
+
+ i += 1;
+ continue;
+ } else {
+ regex.push_str(&car);
+ }
+ escaping = false;
+ i += 1;
+ }
+
+ format!("{delimiter}^{regex}${delimiter}")
}
}
diff --git a/crates/shirabe/tests/cli.rs b/crates/shirabe/tests/cli.rs
index c0fa1d9..a267ff2 100644
--- a/crates/shirabe/tests/cli.rs
+++ b/crates/shirabe/tests/cli.rs
@@ -133,7 +133,6 @@ run_no_panic_tests! {
#[ignore = "currently panics"]
run_clear_cache => "clear-cache",
run_config => "config",
- #[ignore = "currently panics"]
run_create_project => "create-project",
run_depends => "depends",
#[ignore = "currently panics"]