aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-external-packages/src')
-rw-r--r--crates/shirabe-external-packages/src/symfony.rs1
-rw-r--r--crates/shirabe-external-packages/src/symfony/finder.rs7
-rw-r--r--crates/shirabe-external-packages/src/symfony/finder/finder.rs827
-rw-r--r--crates/shirabe-external-packages/src/symfony/finder/glob.rs466
-rw-r--r--crates/shirabe-external-packages/src/symfony/finder/spl_file_info.rs84
5 files changed, 0 insertions, 1385 deletions
diff --git a/crates/shirabe-external-packages/src/symfony.rs b/crates/shirabe-external-packages/src/symfony.rs
index 1cfe2a47..5b9849fd 100644
--- a/crates/shirabe-external-packages/src/symfony.rs
+++ b/crates/shirabe-external-packages/src/symfony.rs
@@ -1,2 +1 @@
pub mod console;
-pub mod finder;
diff --git a/crates/shirabe-external-packages/src/symfony/finder.rs b/crates/shirabe-external-packages/src/symfony/finder.rs
deleted file mode 100644
index 4b2d4b84..00000000
--- a/crates/shirabe-external-packages/src/symfony/finder.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-pub mod finder;
-pub mod glob;
-pub mod spl_file_info;
-
-pub use finder::*;
-pub use glob::*;
-pub use spl_file_info::*;
diff --git a/crates/shirabe-external-packages/src/symfony/finder/finder.rs b/crates/shirabe-external-packages/src/symfony/finder/finder.rs
deleted file mode 100644
index 4cdcf392..00000000
--- a/crates/shirabe-external-packages/src/symfony/finder/finder.rs
+++ /dev/null
@@ -1,827 +0,0 @@
-//! 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::symfony::finder::glob::Glob;
-use chrono::{NaiveDate, NaiveDateTime};
-use indexmap::{IndexMap, IndexSet};
-use shirabe_pcre::{CaptureKey, Preg};
-use shirabe_php_shim::{file_exists, glob, is_dir, php_regex, preg_quote, rtrim};
-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 {
- 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
- }
-}
-
-/// 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(std::cell::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<std::cell::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 {
- Self::new()
- }
-}
-
-impl Finder {
- pub fn new() -> Self {
- 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 {
- Self::new()
- }
-
- pub fn files(&mut self) -> &mut Self {
- self.mode = ONLY_FILES;
-
- self
- }
-
- pub fn directories(&mut self) -> &mut Self {
- self.mode = ONLY_DIRECTORIES;
-
- self
- }
-
- 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 {
- 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 {
- self.filters.push(std::cell::RefCell::new(closure));
-
- self
- }
-
- pub fn follow_links(&mut self) -> &mut Self {
- self.follow_links = true;
-
- self
- }
-
- 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 {
- 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 {
- 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 {
- self.not_names.push(pattern.to_string());
-
- self
- }
-
- 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 {
- self.names.push(pattern.to_string());
-
- self
- }
-
- pub fn sort<F>(&mut self, comparator: F) -> &mut Self
- where
- F: FnMut(&PathBuf, &PathBuf) -> i64 + 'static,
- {
- self.sort = Sort::Closure(std::cell::RefCell::new(Box::new(comparator)));
-
- self
- }
-
- pub fn sort_by_name(&mut self) -> &mut Self {
- self.sort = Sort::ByName;
-
- self
- }
-
- pub fn sort_by_accessed_time(&mut self) -> &mut Self {
- self.sort = Sort::ByAccessedTime;
-
- self
- }
-
- pub fn date(&mut self, date: &str) -> &mut Self {
- self.dates.push(parse_date_comparator(date));
-
- self
- }
-
- pub fn get_iterator(&self) -> FinderIterator {
- FinderIterator {
- items: self.collect_paths(),
- pos: 0,
- }
- }
-
- pub fn iter(&self) -> impl Iterator<Item = PathBuf> {
- self.get_iterator()
- }
-
- pub fn len(&self) -> usize {
- 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(php_regex!("#^(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: &IndexSet<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)
- });
- }
- }
- }
-}
-
-/// Reproduces `ExcludeDirectoryFilterIterator`'s constructor split between simple
-/// directory names and `/`-containing path patterns.
-fn build_exclude(directories: &[String]) -> (IndexSet<String>, Option<String>) {
- let mut excluded_dirs = IndexSet::new();
- let mut patterns: Vec<String> = Vec::new();
-
- 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: &IndexSet<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(php_regex!("/[*?[: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 {
- items: Vec<PathBuf>,
- pos: usize,
-}
-
-impl FinderIterator {
- pub fn valid(&self) -> bool {
- self.pos < self.items.len()
- }
-
- pub fn current(&self) -> PathBuf {
- self.items[self.pos].clone()
- }
-}
-
-impl Iterator for FinderIterator {
- type Item = PathBuf;
-
- fn next(&mut self) -> Option<PathBuf> {
- 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()
- }
-}
-
-impl IntoIterator for Finder {
- type Item = PathBuf;
- type IntoIter = std::vec::IntoIter<PathBuf>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.collect_paths().into_iter()
- }
-}
-
-impl IntoIterator for &mut Finder {
- type Item = PathBuf;
- type IntoIter = std::vec::IntoIter<PathBuf>;
-
- fn into_iter(self) -> Self::IntoIter {
- 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
deleted file mode 100644
index 247b772f..00000000
--- a/crates/shirabe-external-packages/src/symfony/finder/glob.rs
+++ /dev/null
@@ -1,466 +0,0 @@
-//! ref: composer/vendor/symfony/finder/Glob.php
-
-// Regex pattern compatibility:
-// PHP's Glob::toRegex builds its regex in a single character loop and emits PCRE-only
-// constructs: the look-ahead `(?=[^\.])` enforcing the strict-leading-dot rule, and a
-// possessive `[^/]++` inside the `/**/` construct. The regex crate supports neither, so
-// instead of a verbatim port of the loop, the glob is first tokenized (mirroring the
-// loop's dispatch exactly) and every no-dot constraint is then resolved by recursive
-// union expansion, producing a regex-crate-compatible pattern that matches the same
-// inputs:
-//
-// (?=[^.])c => c (branch is unmatchable when c is `\.`)
-// (?=[^.])[^/] => [^/.]
-// (?=[^.])[abc] => [[abc]&&[^.]] (character class intersection)
-// (?=[^.])[^/]*R => (?:[^/.][^/]*R | expand((?=[^.])R))
-// (?=[^.])(a|b)R => (?:expand((?=[^.])aR) | expand((?=[^.])bR))
-// (?=[^.])$ => branch dropped (no next character can exist)
-// (?=[^.])(?:$|/) => / (dir-boundary variant only)
-// [^/]++ => [^/]+ (the regex crate never backtracks)
-//
-// Unmatchable branches are dropped; a wholly unmatchable glob renders as `\b\B`, which
-// compiles and never matches, just like PHP's own output for such globs.
-
-#[derive(Debug)]
-pub struct Glob;
-
-#[derive(Debug, Clone, Copy)]
-struct Ctx {
- strict_leading_dot: bool,
- strict_wildcard_slash: bool,
-}
-
-#[derive(Debug, Clone, Copy)]
-enum Terminal {
- /// The `$` anchor of `to_regex` follows the body.
- EndAnchor,
- /// The `(?:$|/)` suffix of `to_regex_dir_boundary` ends the body.
- EndOrSlash,
-}
-
-#[derive(Debug, Clone)]
-enum Token {
- /// A literal regex piece for one glob character, escaped as needed.
- Text(String),
- /// An unescaped `*`.
- Star,
- /// An unescaped `?`.
- Question,
- /// A `/**/` (or trailing `/**`) segment, leading slash included.
- Globstar { trailing_slash_optional: bool },
- /// An `{a,b}` alternation. `closed` is false when the `}` is missing.
- Group {
- alternatives: Vec<Vec<Token>>,
- closed: bool,
- },
- /// The position of PHP's `(?=[^\.])` look-ahead.
- NoDot,
- /// End of the pattern.
- End(Terminal),
-}
-
-#[derive(Debug)]
-enum Out {
- /// Fully rendered.
- Ok(String),
- /// Cannot match anything; the branch must be dropped.
- Dead,
- /// A no-dot constraint reached the end of an isolated sub-sequence (e.g. `{a/,b}`
- /// puts a look-ahead right before the closing brace); the caller must splice in the
- /// following context and re-render.
- Escapes,
-}
-
-impl Glob {
- pub fn to_regex(glob: &str, strict_leading_dot: bool, strict_wildcard_slash: bool) -> String {
- format!(
- "#^{}$#",
- expand(
- glob,
- strict_leading_dot,
- strict_wildcard_slash,
- Terminal::EndAnchor
- )
- )
- }
-
- /// Not part of Symfony's Glob. Composer's BaseExcludeFilter derives its patterns
- /// from toRegex output by stripping the delimiters and anchors and appending the
- /// `(?=$|/)` dir-boundary look-ahead. Here that suffix has to take part in the
- /// union expansion — when a trailing `*` matches zero characters, the no-dot
- /// constraint falls onto the boundary itself — so it cannot be appended by the
- /// caller after the fact. Returns the undelimited, unanchored body with the
- /// boundary woven in as the consuming `(?:$|/)` (equivalent under boolean
- /// matching, since nothing follows it).
- pub fn to_regex_dir_boundary(
- glob: &str,
- strict_leading_dot: bool,
- strict_wildcard_slash: bool,
- ) -> String {
- expand(
- glob,
- strict_leading_dot,
- strict_wildcard_slash,
- Terminal::EndOrSlash,
- )
- }
-}
-
-fn expand(
- glob: &str,
- strict_leading_dot: bool,
- strict_wildcard_slash: bool,
- terminal: Terminal,
-) -> String {
- let ctx = Ctx {
- strict_leading_dot,
- strict_wildcard_slash,
- };
- let mut tokens = tokenize(glob, ctx);
- tokens.push(Token::End(terminal));
- match render_seq(&tokens, ctx) {
- Out::Ok(body) => body,
- Out::Dead => "\\b\\B".to_string(),
- Out::Escapes => unreachable!("the terminal token resolves every constraint"),
- }
-}
-
-// PHP iterates over bytes; iterating over chars is equivalent for valid UTF-8 because
-// every character the loop treats specially is ASCII.
-fn tokenize(glob: &str, ctx: Ctx) -> Vec<Token> {
- let chars: Vec<char> = glob.chars().collect();
- let mut first_byte = true;
- let mut escaping = false;
- // stack[0] is the root sequence (a pseudo-group with a single alternative); deeper
- // frames are open `{` groups, so PHP's $inCurlies == stack.len() - 1.
- let mut stack: Vec<Vec<Vec<Token>>> = vec![vec![Vec::new()]];
-
- fn push(stack: &mut [Vec<Vec<Token>>], token: Token) {
- stack.last_mut().unwrap().last_mut().unwrap().push(token);
- }
-
- let mut i = 0;
- while i < chars.len() {
- let car = chars[i];
- if first_byte && ctx.strict_leading_dot && car != '.' {
- push(&mut stack, Token::NoDot);
- }
-
- first_byte = car == '/';
-
- if first_byte
- && ctx.strict_wildcard_slash
- && i + 2 < chars.len()
- && chars[i + 1] == '*'
- && chars[i + 2] == '*'
- && (i + 3 >= chars.len() || chars[i + 3] == '/')
- {
- push(
- &mut stack,
- Token::Globstar {
- trailing_slash_optional: i + 3 >= chars.len(),
- },
- );
- i += 2 + usize::from(i + 3 < chars.len());
- escaping = false;
- i += 1;
- continue;
- }
-
- if car == '#'
- || car == '.'
- || car == '('
- || car == ')'
- || car == '|'
- || car == '+'
- || car == '^'
- || car == '$'
- {
- push(&mut stack, Token::Text(format!("\\{car}")));
- } else if car == '*' {
- push(
- &mut stack,
- if escaping {
- Token::Text("\\*".to_string())
- } else {
- Token::Star
- },
- );
- } else if car == '?' {
- push(
- &mut stack,
- if escaping {
- Token::Text("\\?".to_string())
- } else {
- Token::Question
- },
- );
- } else if car == '{' {
- if escaping {
- push(&mut stack, Token::Text("\\{".to_string()));
- } else {
- stack.push(vec![Vec::new()]);
- }
- } else if car == '}' && stack.len() > 1 {
- if escaping {
- push(&mut stack, Token::Text("}".to_string()));
- } else {
- let alternatives = stack.pop().unwrap();
- push(
- &mut stack,
- Token::Group {
- alternatives,
- closed: true,
- },
- );
- }
- } else if car == ',' && stack.len() > 1 {
- if escaping {
- push(&mut stack, Token::Text(",".to_string()));
- } else {
- stack.last_mut().unwrap().push(Vec::new());
- }
- } else if car == '\\' {
- if escaping {
- push(&mut stack, Token::Text("\\\\".to_string()));
- escaping = false;
- } else {
- escaping = true;
- }
- i += 1;
- continue;
- } else {
- push(&mut stack, Token::Text(car.to_string()));
- }
- escaping = false;
- i += 1;
- }
-
- // PHP leaves an unterminated `(` for an unclosed `{`; keep the broken shape so the
- // resulting pattern fails to compile just as the PCRE one does.
- while stack.len() > 1 {
- let alternatives = stack.pop().unwrap();
- push(
- &mut stack,
- Token::Group {
- alternatives,
- closed: false,
- },
- );
- }
- stack.pop().unwrap().pop().unwrap()
-}
-
-fn star(ctx: Ctx) -> &'static str {
- if ctx.strict_wildcard_slash {
- "[^/]*"
- } else {
- ".*"
- }
-}
-
-fn question(ctx: Ctx) -> &'static str {
- if ctx.strict_wildcard_slash {
- "[^/]"
- } else {
- "."
- }
-}
-
-fn globstar(ctx: Ctx, trailing_slash_optional: bool) -> String {
- // PHP emits `/(?:(?=[^\.])[^/]++/)*`; `(?=[^\.])[^/]+` collapses to `[^/.][^/]*`.
- let step = if ctx.strict_leading_dot {
- "[^/.][^/]*"
- } else {
- "[^/]+"
- };
- let slash = if trailing_slash_optional { "/?" } else { "/" };
- format!("/(?:{step}{slash})*")
-}
-
-fn render_seq(tokens: &[Token], ctx: Ctx) -> Out {
- let mut out = String::new();
- for (idx, token) in tokens.iter().enumerate() {
- match token {
- Token::Text(text) => out.push_str(text),
- Token::Star => out.push_str(star(ctx)),
- Token::Question => out.push_str(question(ctx)),
- Token::Globstar {
- trailing_slash_optional,
- } => out.push_str(&globstar(ctx, *trailing_slash_optional)),
- Token::End(Terminal::EndAnchor) => {}
- Token::End(Terminal::EndOrSlash) => out.push_str("(?:$|/)"),
- Token::NoDot => {
- return prefix(out, apply_no_dot(&tokens[idx + 1..], ctx));
- }
- Token::Group {
- alternatives,
- closed,
- } => match render_group_isolated(alternatives, *closed, ctx) {
- Out::Ok(group) => out.push_str(&group),
- Out::Dead => return Out::Dead,
- Out::Escapes => {
- // A constraint inside the group applies to what follows it; splice
- // the remainder into each alternative and re-render.
- let rest = &tokens[idx + 1..];
- let mut branches = Vec::new();
- for alternative in alternatives {
- let mut seq = alternative.clone();
- seq.extend_from_slice(rest);
- match render_seq(&seq, ctx) {
- Out::Ok(branch) => branches.push(branch),
- Out::Dead => {}
- Out::Escapes => return Out::Escapes,
- }
- }
- return match join_branches(branches) {
- Some(joined) => Out::Ok(out + &joined),
- None => Out::Dead,
- };
- }
- },
- }
- }
- Out::Ok(out)
-}
-
-fn render_group_isolated(alternatives: &[Vec<Token>], closed: bool, ctx: Ctx) -> Out {
- let mut rendered = Vec::new();
- for alternative in alternatives {
- match render_seq(alternative, ctx) {
- Out::Ok(branch) => rendered.push(branch),
- Out::Dead => {}
- Out::Escapes => {
- if !closed {
- // The unterminated `(` makes the output uncompilable either way;
- // resolve the dangling constraint as if the pattern ended here.
- let mut seq = alternative.clone();
- seq.push(Token::End(Terminal::EndAnchor));
- if let Out::Ok(branch) = render_seq(&seq, ctx) {
- rendered.push(branch);
- }
- } else {
- return Out::Escapes;
- }
- }
- }
- }
- if rendered.is_empty() {
- return Out::Dead;
- }
- let close = if closed { ")" } else { "" };
- Out::Ok(format!("({}{close}", rendered.join("|")))
-}
-
-/// Renders the remainder of the pattern under the constraint that the next matched
-/// character must not be a dot.
-fn apply_no_dot(tokens: &[Token], ctx: Ctx) -> Out {
- let Some((first, rest)) = tokens.split_first() else {
- return Out::Escapes;
- };
- match first {
- Token::Text(text) if text == "\\." => Out::Dead,
- Token::Text(text) if text == "[" => {
- if let Some((class, after)) = scan_char_class(tokens) {
- prefix(
- format!("[{class}&&[^.]]"),
- render_seq(&tokens[after..], ctx),
- )
- } else {
- prefix("[".to_string(), render_seq(rest, ctx))
- }
- }
- Token::Text(text) => prefix(text.clone(), render_seq(rest, ctx)),
- Token::Star => {
- // Either the `*` consumes at least one character, which then carries the
- // constraint, or it consumes none and the constraint moves past it.
- let head = if ctx.strict_wildcard_slash {
- "[^/.][^/]*"
- } else {
- "[^.\\n].*"
- };
- let consumed = prefix(head.to_string(), render_seq(rest, ctx));
- let skipped = apply_no_dot(rest, ctx);
- match (consumed, skipped) {
- (Out::Escapes, _) | (_, Out::Escapes) => Out::Escapes,
- (Out::Ok(a), Out::Ok(b)) => Out::Ok(format!("(?:{a}|{b})")),
- (Out::Ok(a), Out::Dead) => Out::Ok(a),
- (Out::Dead, Out::Ok(b)) => Out::Ok(b),
- (Out::Dead, Out::Dead) => Out::Dead,
- }
- }
- Token::Question => {
- let head = if ctx.strict_wildcard_slash {
- "[^/.]"
- } else {
- "[^.\\n]"
- };
- prefix(head.to_string(), render_seq(rest, ctx))
- }
- // The construct starts with a literal `/`, which satisfies the constraint.
- Token::Globstar { .. } => render_seq(tokens, ctx),
- Token::Group {
- alternatives,
- closed: true,
- } => {
- let mut branches = Vec::new();
- for alternative in alternatives {
- let mut seq = alternative.clone();
- seq.extend_from_slice(rest);
- match apply_no_dot(&seq, ctx) {
- Out::Ok(branch) => branches.push(branch),
- Out::Dead => {}
- Out::Escapes => return Out::Escapes,
- }
- }
- match join_branches(branches) {
- Some(joined) => Out::Ok(joined),
- None => Out::Dead,
- }
- }
- // The unterminated `(` makes the output uncompilable either way; skip the
- // expansion.
- Token::Group { closed: false, .. } => render_seq(tokens, ctx),
- // `(?=[^.])$`: no next character can exist.
- Token::End(Terminal::EndAnchor) => Out::Dead,
- // `(?=[^.])(?=$|/)`: the next character must exist, must not be a dot, and must
- // be either the end (impossible) or a slash — exactly one consumable `/`.
- Token::End(Terminal::EndOrSlash) => Out::Ok("/".to_string()),
- Token::NoDot => unreachable!("PHP never emits two consecutive look-aheads"),
- }
-}
-
-fn prefix(head: String, tail: Out) -> Out {
- match tail {
- Out::Ok(tail) => Out::Ok(head + &tail),
- other => other,
- }
-}
-
-fn join_branches(mut branches: Vec<String>) -> Option<String> {
- match branches.len() {
- 0 => None,
- 1 => branches.pop(),
- _ => Some(format!("(?:{})", branches.join("|"))),
- }
-}
-
-/// A `[` passes through the PHP loop untouched and opens a character class in the
-/// final regex, so a no-dot constraint has to intersect the class rather than be
-/// satisfied by the `[` itself. Collects a class spanning plain text tokens; None when
-/// no plain closing `]` follows (the `[` is then treated as a literal).
-fn scan_char_class(tokens: &[Token]) -> Option<(String, usize)> {
- let mut class = String::from("[");
- let mut idx = 1;
- loop {
- match tokens.get(idx)? {
- Token::Text(text) if text == "]" => {
- class.push(']');
- return Some((class, idx + 1));
- }
- Token::Text(text) => class.push_str(text),
- _ => return None,
- }
- idx += 1;
- }
-}
diff --git a/crates/shirabe-external-packages/src/symfony/finder/spl_file_info.rs b/crates/shirabe-external-packages/src/symfony/finder/spl_file_info.rs
deleted file mode 100644
index 6f03fe14..00000000
--- a/crates/shirabe-external-packages/src/symfony/finder/spl_file_info.rs
+++ /dev/null
@@ -1,84 +0,0 @@
-//! ref: composer/vendor/symfony/finder/SplFileInfo.php
-
-#[derive(Debug)]
-pub struct SplFileInfo {
- // The path passed to the underlying \SplFileInfo constructor.
- pathname: String,
- relative_path: String,
- relative_pathname: String,
-}
-
-impl std::fmt::Display for SplFileInfo {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.get_pathname())
- }
-}
-
-impl SplFileInfo {
- pub fn new(file: &str, relative_path: &str, relative_pathname: &str) -> Self {
- Self {
- pathname: file.to_string(),
- relative_path: relative_path.to_string(),
- relative_pathname: relative_pathname.to_string(),
- }
- }
-
- pub fn get_pathname(&self) -> String {
- self.pathname.clone()
- }
-
- pub fn get_path(&self) -> String {
- shirabe_php_shim::dirname(&self.pathname)
- }
-
- pub fn get_filename(&self) -> String {
- shirabe_php_shim::basename(&self.pathname)
- }
-
- pub fn get_basename(&self, suffix: Option<&str>) -> String {
- match suffix {
- Some(suffix) => shirabe_php_shim::basename_with_suffix(&self.pathname, suffix),
- None => shirabe_php_shim::basename(&self.pathname),
- }
- }
-
- pub fn get_extension(&self) -> String {
- // \SplFileInfo::getExtension() returns the extension (without the dot), or "" if none.
- let base = shirabe_php_shim::basename(&self.pathname);
- match base.rfind('.') {
- Some(index) => base[index + 1..].to_string(),
- None => String::new(),
- }
- }
-
- pub fn get_relative_path_name(&self) -> String {
- self.relative_pathname.clone()
- }
-
- pub fn get_relative_path(&self) -> String {
- self.relative_path.clone()
- }
-
- pub fn is_dir(&self) -> bool {
- shirabe_php_shim::is_dir(&self.pathname)
- }
-
- pub fn is_file(&self) -> bool {
- shirabe_php_shim::is_file(&self.pathname)
- }
-
- pub fn is_link(&self) -> bool {
- shirabe_php_shim::is_link(&self.pathname)
- }
-
- pub fn get_real_path(&self) -> Option<String> {
- // \SplFileInfo::getRealPath() returns the canonicalized absolute path, or false on failure.
- shirabe_php_shim::realpath(&self.pathname)
- }
-
- pub fn get_size(&self) -> i64 {
- // \SplFileInfo::getSize() returns the file size in bytes (throws on failure).
- // TODO(phase-c): PHP throws a \RuntimeException on stat failure; this returns 0 instead.
- shirabe_php_shim::filesize(&self.pathname).unwrap_or(0)
- }
-}