1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3use std::{
191 cell::RefCell,
192 collections::{BTreeMap, HashSet},
193 error::Error,
194 ffi::OsStr,
195 fmt,
196 hash::{Hash, Hasher},
197};
198
199use memchr::{memchr, memchr2, memchr3, memmem};
200
201mod set;
202#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
203mod suffix_word;
204mod sweep;
205
206pub use set::{PatternSet, PatternSetError};
207use sweep::{SweepEngine, SweepState};
208
209#[derive(Debug, Clone)]
211pub struct Pattern {
212 alternatives: Vec<CompiledAlternative>,
213 alternative_fast_path: Option<Box<AlternativeFastPath>>,
214 path_filter: Option<Box<PathFilter>>,
217 walker_path_viability: WalkerPathViability,
218 walker_path_problem_offset: Option<usize>,
219 options: PatternOptions,
220}
221
222#[derive(Debug, Clone)]
229struct PathFilter {
230 compiled: Vec<CompiledAlternative>,
238 programs: Vec<usize>,
243 whole: Vec<usize>,
246}
247
248impl PathFilter {
249 fn compile(
250 alternatives: &[CompiledAlternative],
251 options: PatternOptions,
252 budget: &mut IrBudget,
253 ) -> Result<Option<Self>, PatternError> {
254 let is_program = |alternative: &CompiledAlternative| {
255 options.extglob && alternative.extglob.is_some() && !alternative.raw.starts_with(b"./")
256 };
257 let needs_copy = |alternative: &CompiledAlternative| {
258 alternative.raw.starts_with(b"./")
259 || alternative.tokens.windows(2).any(|tokens| {
260 matches!(
261 tokens,
262 [Token::Separator, Token::Any | Token::Star | Token::Class(_)]
263 )
264 })
265 };
266 let plain_needs_copy = alternatives
267 .iter()
268 .any(|alternative| !is_program(alternative) && needs_copy(alternative));
269 let programs = alternatives.iter().filter(|item| is_program(item)).count();
270 let plain = alternatives.len() - programs;
271 let (copies, whole) = if plain_needs_copy {
273 (plain, 0)
274 } else {
275 (0, plain)
276 };
277 let mut filter = Self {
278 compiled: Vec::with_capacity(copies),
279 programs: Vec::with_capacity(programs),
280 whole: Vec::with_capacity(whole),
281 };
282 for (index, alternative) in alternatives.iter().enumerate() {
283 if is_program(alternative) {
284 filter.programs.push(index);
285 } else if plain_needs_copy {
286 filter
289 .compiled
290 .push(Pattern::compile_path_filter_alternative(
291 alternative,
292 options,
293 budget,
294 )?);
295 } else {
296 filter.whole.push(index);
297 }
298 }
299 Ok((!filter.compiled.is_empty() || !filter.programs.is_empty()).then_some(filter))
300 }
301
302 fn is_match(
305 &self,
306 alternatives: &[CompiledAlternative],
307 options: PatternOptions,
308 whole_options: PatternOptions,
309 path: &[u8],
310 ) -> bool {
311 let shared = |indices: &[usize], options: PatternOptions| {
312 indices.iter().any(|&index| {
313 Pattern::match_alternatives(
314 std::slice::from_ref(&alternatives[index]),
315 options,
316 path,
317 )
318 })
319 };
320 Pattern::match_alternatives(&self.compiled, options, path)
321 || shared(&self.programs, options)
322 || shared(&self.whole, whole_options)
323 }
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct PatternOptions {
359 braces: bool,
360 recursive_double_star: bool,
361 extglob: bool,
362 match_hidden: bool,
363 case_insensitive: bool,
364 escape: bool,
365 component_wildcards: bool,
366 root_component_wildcards: bool,
367 candidate_root_component_wildcard: bool,
370 candidate_starts_component: bool,
376}
377
378impl Default for PatternOptions {
379 fn default() -> Self {
380 Self::CONSERVATIVE
381 }
382}
383
384impl PatternOptions {
385 const CONSERVATIVE: Self = Self {
388 braces: false,
389 recursive_double_star: false,
390 extglob: false,
391 match_hidden: false,
392 case_insensitive: false,
393 escape: true,
394 component_wildcards: false,
395 root_component_wildcards: false,
396 candidate_root_component_wildcard: false,
397 candidate_starts_component: true,
398 };
399
400 #[must_use]
457 pub const fn walker() -> Self {
458 Self::CONSERVATIVE
459 .recursive_double_star(true)
460 .braces(true)
461 .extglob(true)
462 }
463
464 #[must_use]
466 pub const fn braces(mut self, enabled: bool) -> Self {
467 self.braces = enabled;
468 self
469 }
470
471 #[must_use]
506 pub const fn recursive_double_star(mut self, enabled: bool) -> Self {
507 self.recursive_double_star = enabled;
508 self
509 }
510
511 #[must_use]
513 pub const fn extglob(mut self, enabled: bool) -> Self {
514 self.extglob = enabled;
515 self
516 }
517
518 #[must_use]
520 pub const fn match_hidden(mut self, enabled: bool) -> Self {
521 self.match_hidden = enabled;
522 self
523 }
524
525 #[must_use]
527 pub const fn case_insensitive(mut self, enabled: bool) -> Self {
528 self.case_insensitive = enabled;
529 self
530 }
531
532 #[must_use]
534 pub const fn escape(mut self, enabled: bool) -> Self {
535 self.escape = enabled;
536 self
537 }
538}
539
540#[derive(Debug, Clone, PartialEq, Eq)]
561pub struct PatternError {
562 offset: usize,
563 message: &'static str,
564}
565
566impl PatternError {
567 #[must_use]
579 pub const fn new(offset: usize, message: &'static str) -> Self {
580 Self { offset, message }
581 }
582
583 pub const fn offset(&self) -> usize {
585 self.offset
586 }
587
588 pub const fn message(&self) -> &'static str {
593 self.message
594 }
595}
596
597impl fmt::Display for PatternError {
598 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
599 write!(formatter, "{} at byte {}", self.message, self.offset)
600 }
601}
602
603impl Error for PatternError {}
604
605#[doc(hidden)]
611#[derive(Debug, Clone, Copy, PartialEq, Eq)]
612#[non_exhaustive]
613pub enum WalkerPathViability {
614 Viable,
617 ParentComponent,
619 Root,
621 EmptyComponent,
624 TrailingDot,
626 DotComponent,
628}
629
630impl Pattern {
631 #[must_use]
637 pub fn has_wildcards(pattern: impl AsRef<[u8]>, options: PatternOptions) -> bool {
638 let pattern = pattern.as_ref();
639 if memchr3(b'*', b'?', b'[', pattern).is_some() {
640 return true;
641 }
642 if options.braces && memchr(b'{', pattern).is_some() {
643 return true;
644 }
645 if !options.extglob {
646 return false;
647 }
648 let mut offset = 0;
649 while let Some(found) = memchr(b'(', &pattern[offset..]) {
650 let index = offset + found;
651 if index > 0 && matches!(pattern[index - 1], b'?' | b'*' | b'+' | b'@' | b'!') {
652 return true;
653 }
654 offset = index + 1;
655 }
656 false
657 }
658
659 pub fn compile(
678 pattern: impl AsRef<[u8]>,
679 options: PatternOptions,
680 ) -> Result<Self, PatternError> {
681 let pattern = pattern.as_ref();
682 let source_provenance = SourceProvenance::Contiguous { source_start: 0 };
686 let mut budget = IrBudget::new();
687 let mut provenance_budget = ProvenanceBudget::new();
688 let mut compiled = Self::compile_within(
689 pattern,
690 options,
691 &mut budget,
692 &mut provenance_budget,
693 Some(&source_provenance),
694 pattern.starts_with(b"./"),
695 ComponentBounds::WHOLE,
696 )?;
697 let (viability, offset) = walker_path_analysis(&compiled.alternatives, &mut budget)?;
698 compiled.walker_path_viability = viability;
699 compiled.walker_path_problem_offset = offset;
700 compiled.release_compile_state();
701 Ok(compiled)
702 }
703
704 fn release_compile_state(&mut self) {
710 let copies = self
711 .path_filter
712 .iter_mut()
713 .flat_map(|filter| filter.compiled.iter_mut());
714 for alternative in self.alternatives.iter_mut().chain(copies) {
715 alternative.release_compile_state();
716 }
717 }
718
719 #[doc(hidden)]
726 #[must_use]
727 pub const fn walker_path_viability(&self) -> WalkerPathViability {
728 self.walker_path_viability
729 }
730
731 #[doc(hidden)]
738 #[must_use]
739 pub const fn walker_path_problem_offset(&self) -> Option<usize> {
740 self.walker_path_problem_offset
741 }
742
743 fn compile_within(
751 pattern: &[u8],
752 options: PatternOptions,
753 budget: &mut IrBudget,
754 provenance_budget: &mut ProvenanceBudget,
755 walker_source_provenance: Option<&SourceProvenance>,
756 leading_dot_is_normalized: bool,
757 bounds: ComponentBounds,
758 ) -> Result<Self, PatternError> {
759 if options.braces {
760 let parse_options = PatternOptions {
761 braces: false,
762 ..options
763 };
764 ensure_source_brace_compiled_ir_lower_bound(
765 pattern,
766 parse_options,
767 budget,
768 provenance_budget,
769 )?;
770 let expanded = expand_brace_alternatives_with_provenance(
771 pattern,
772 walker_source_provenance,
773 options.escape,
774 provenance_budget,
775 )?;
776 ensure_brace_compiled_ir_lower_bound(&expanded, parse_options, budget)?;
777 let mut alternatives = Vec::with_capacity(expanded.len());
780 for alternative in expanded {
781 let leading_dot_is_normalized =
786 leading_dot_is_normalized || alternative.bytes.starts_with(b"./");
787 let compiled = Self::compile_within(
788 &alternative.bytes,
789 parse_options,
790 budget,
791 provenance_budget,
792 alternative.source_provenance.as_ref(),
793 leading_dot_is_normalized,
794 bounds,
795 )?;
796 alternatives.extend(compiled.alternatives);
797 }
798 return Self::from_alternatives(alternatives, options, budget);
799 }
800 let mut tokens = Vec::new();
801 let mut literals = Vec::new();
802 let mut walker_path = WalkerPathShapeBuilder::new(leading_dot_is_normalized);
807 let mut index = 0;
808 let mut charged = 0;
812 let mut star_run_end = 0;
815 let mut star_run_is_recursive = false;
816
817 while index < pattern.len() {
818 budget.charge(tokens.len() - charged, 0)?;
819 charged = tokens.len();
820 if pattern[index] == b'*' && index >= star_run_end {
821 star_run_end = index
822 + pattern[index..]
823 .iter()
824 .take_while(|&&byte| byte == b'*')
825 .count();
826 star_run_is_recursive = options.recursive_double_star
827 && star_run_is_whole_component(
828 pattern,
829 index,
830 star_run_end,
831 options.escape,
832 bounds,
833 );
834 }
835 match pattern[index] {
836 b'/' => {
837 flush_literals(&mut tokens, &mut literals);
838 tokens.push(Token::Separator);
839 walker_path.separator(
840 walker_source_provenance.and_then(|provenance| provenance.offset_at(index)),
841 );
842 index += 1;
843 }
844 b'*' if star_run_is_recursive && pattern.get(index + 1) == Some(&b'*') => {
845 flush_literals(&mut tokens, &mut literals);
846 if pattern.get(index + 2) == Some(&b'/') {
847 tokens.push(Token::RecursivePrefix);
848 walker_path.wildcard();
849 walker_path.separator(
850 walker_source_provenance
851 .and_then(|provenance| provenance.offset_at(index + 2)),
852 );
853 index += 3;
854 } else {
855 tokens.push(Token::RecursiveStar);
856 walker_path.wildcard();
857 index += 2;
858 }
859 }
860 b'*' => {
861 flush_literals(&mut tokens, &mut literals);
862 tokens.push(Token::Star);
863 walker_path.wildcard();
864 index += 1;
865 }
866 b'?' => {
867 flush_literals(&mut tokens, &mut literals);
868 tokens.push(Token::Any);
869 walker_path.wildcard();
870 index += 1;
871 }
872 b'[' => {
873 flush_literals(&mut tokens, &mut literals);
874 let (class, next) =
875 parse_class(pattern, index, options.escape).map_err(|mut error| {
876 if let Some(source_offset) = walker_source_provenance
877 .and_then(|provenance| provenance.offset_at(error.offset))
878 {
879 error.offset = source_offset;
880 }
881 error
882 })?;
883 budget.charge(class.members.len(), 0)?;
886 tokens.push(Token::Class(class));
887 walker_path.wildcard();
888 index = next;
889 }
890 b'\\' if options.escape => {
891 if let Some(&escaped) = pattern.get(index + 1) {
892 literals.push(escaped);
893 walker_path.escaped();
894 index += 2;
895 } else {
896 literals.push(b'\\');
899 walker_path.escaped();
900 index += 1;
901 }
902 }
903 byte => {
904 literals.push(byte);
905 walker_path.literal(
906 byte,
907 walker_source_provenance.and_then(|provenance| provenance.offset_at(index)),
908 );
909 index += 1;
910 }
911 }
912 }
913 flush_literals(&mut tokens, &mut literals);
914 budget.charge(tokens.len() - charged, 0)?;
915
916 let extglob = compile_extglob(
917 pattern,
918 options,
919 budget,
920 provenance_budget,
921 walker_source_provenance,
922 leading_dot_is_normalized,
923 )?;
924 let fast_path = FastPath::compile(&tokens, options);
925 let sweep = compile_sweep(
926 &tokens,
927 &fast_path,
928 extglob.as_deref(),
929 options,
930 budget,
931 None,
932 )?;
933 let walker_path_shape = walker_path.finish();
934 Self::from_alternatives(
935 vec![CompiledAlternative {
936 extglob,
937 raw: pattern.to_vec(),
938 fast_path,
939 prefilter: Prefilter::compile(&tokens),
940 sweep,
941 tokens,
942 walker_path_shape,
943 }],
944 options,
945 budget,
946 )
947 }
948
949 pub fn validate(
955 pattern: impl AsRef<[u8]>,
956 options: PatternOptions,
957 ) -> Result<(), PatternError> {
958 Self::compile(pattern, options).map(|_| ())
959 }
960
961 #[must_use]
987 pub fn is_match(&self, path: impl AsRef<[u8]>) -> bool {
988 let path = path.as_ref();
989 if let [alternative] = self.alternatives.as_slice()
990 && (!self.options.extglob || alternative.extglob.is_none())
991 && let Some(fast_path) = &alternative.fast_path
992 {
993 return fast_path.is_match(path, self.options);
994 }
995 if let Some(fast_path) = &self.alternative_fast_path {
996 return fast_path.is_match(path, self.options, &self.alternatives);
997 }
998 Self::match_alternatives(&self.alternatives, self.options, path)
999 }
1000
1001 #[doc(hidden)]
1009 #[cfg(any(test, feature = "unstable-test-hooks"))]
1010 #[must_use]
1011 pub fn engines_agree(&self, path: impl AsRef<[u8]>) -> bool {
1012 let path = path.as_ref();
1013 let answers = |pattern: &Self| {
1014 (
1015 pattern.is_match(path),
1016 pattern.is_match_path(path),
1017 pattern.is_match_glob_path(path),
1018 )
1019 };
1020 let mut sweep_only = self.clone();
1021 sweep_only.strip_engines(true, false, false);
1022 let mut memoized = self.clone();
1023 memoized.strip_engines(true, true, true);
1024 let compiled = answers(self);
1025 compiled == answers(&sweep_only) && compiled == answers(&memoized)
1026 }
1027
1028 #[cfg(any(test, feature = "unstable-test-hooks"))]
1034 fn strip_engines(&mut self, fast_paths: bool, sweeps: bool, prefilters: bool) {
1035 if fast_paths {
1036 self.alternative_fast_path = None;
1037 }
1038 let options = self.options;
1039 for alternative in self.compiled_alternatives_mut() {
1040 alternative.strip_engines(options, fast_paths, sweeps, prefilters);
1041 }
1042 }
1043
1044 #[cfg(any(test, feature = "unstable-test-hooks"))]
1047 fn compiled_alternatives_mut(&mut self) -> impl Iterator<Item = &mut CompiledAlternative> {
1048 self.alternatives.iter_mut().chain(
1049 self.path_filter
1050 .iter_mut()
1051 .flat_map(|filter| filter.compiled.iter_mut()),
1052 )
1053 }
1054
1055 fn match_alternatives(
1060 alternatives: &[CompiledAlternative],
1061 options: PatternOptions,
1062 path: &[u8],
1063 ) -> bool {
1064 alternatives.iter().any(|alternative| {
1065 if options.extglob
1066 && let Some(program) = &alternative.extglob
1067 {
1068 match_extglob_program(program, path, options)
1069 } else if let Some(fast_path) = &alternative.fast_path
1070 && (!options.component_wildcards || fast_path.supports_component_wildcards())
1071 {
1072 fast_path.is_match(path, options)
1073 } else {
1074 !alternative
1079 .prefilter
1080 .rejects(path, options.case_insensitive)
1081 && if let Some(sweep) = &alternative.sweep {
1082 sweep.is_match(path, options)
1083 } else {
1084 Self::matches_general(&alternative.tokens, path, options)
1085 }
1086 }
1087 })
1088 }
1089
1090 fn matches_general(tokens: &[Token], path: &[u8], options: PatternOptions) -> bool {
1098 SCRATCH.with(|cell| match cell.try_borrow_mut() {
1099 Ok(mut scratch) => {
1100 let matched = Self::matches_with_scratch(tokens, path, options, &mut scratch);
1101 if scratch.visited.len() > RETAINED_SCRATCH_WORDS {
1104 scratch.visited.truncate(RETAINED_SCRATCH_WORDS);
1105 scratch.visited.shrink_to_fit();
1106 }
1107 matched
1108 }
1109 Err(_) => Self::matches_with_scratch(tokens, path, options, &mut Scratch::default()),
1111 })
1112 }
1113
1114 fn matches_with_scratch(
1115 tokens: &[Token],
1116 path: &[u8],
1117 options: PatternOptions,
1118 scratch: &mut Scratch,
1119 ) -> bool {
1120 if scratch.generation == u64::MAX {
1122 scratch.scans.clear();
1123 scratch.generation = 0;
1124 }
1125 scratch.generation += 1;
1126 let Scratch {
1127 visited,
1128 deferred,
1129 scans,
1130 generation,
1131 } = scratch;
1132 deferred.clear();
1133 if scans.len() < tokens.len() {
1134 scans.resize(tokens.len(), StarScans::default());
1135 }
1136 let mut failed = FailedStates::new(tokens, path, visited);
1137 let (skip_token, skip_literal) =
1138 skipping_star(tokens, options).unwrap_or((usize::MAX, &[]));
1139 let mut work = StarWork {
1140 scans,
1141 generation: *generation,
1142 skip_token,
1143 skip_literal,
1144 };
1145 if skip_token == usize::MAX {
1148 Self::matches_from::<false>(tokens, path, options, &mut failed, deferred, &mut work)
1149 } else {
1150 Self::matches_from::<true>(tokens, path, options, &mut failed, deferred, &mut work)
1151 }
1152 }
1153
1154 #[must_use]
1188 pub fn filter_paths<'a, T>(&self, paths: impl IntoIterator<Item = &'a T>) -> Vec<&'a T>
1189 where
1190 T: AsRef<[u8]> + ?Sized + 'a,
1191 {
1192 paths
1193 .into_iter()
1194 .filter(|path| self.is_match_path(path.as_ref()))
1195 .collect()
1196 }
1197
1198 #[must_use]
1220 pub fn is_match_path(&self, path: impl AsRef<[u8]>) -> bool {
1221 self.matches_path_filter(path.as_ref())
1222 }
1223
1224 #[must_use]
1251 pub fn is_match_glob_path(&self, path: impl AsRef<[u8]>) -> bool {
1252 let path = without_leading_dot_slash(path.as_ref());
1253 let options = PatternOptions {
1254 component_wildcards: true,
1255 root_component_wildcards: true,
1256 ..self.options
1257 };
1258 if self
1259 .alternatives
1260 .iter()
1261 .any(|alternative| alternative.raw.starts_with(b"./"))
1262 && let Some(filter) = &self.path_filter
1263 {
1264 return filter.is_match(&self.alternatives, options, options, path);
1267 }
1268 if let [alternative] = self.alternatives.as_slice()
1269 && (!options.extglob || alternative.extglob.is_none())
1270 && let Some(fast_path) = &alternative.fast_path
1271 && fast_path.supports_component_wildcards()
1272 {
1273 return fast_path.is_match(path, options);
1274 }
1275 if let Some(fast_path) = &self.alternative_fast_path {
1276 return fast_path.is_match(path, options, &self.alternatives);
1277 }
1278 Self::match_alternatives(&self.alternatives, options, path)
1279 }
1280
1281 #[doc(hidden)]
1290 #[must_use]
1291 pub fn is_match_crossing_path(&self, path: impl AsRef<[u8]>) -> bool {
1292 let path = without_leading_dot_slash(path.as_ref());
1293 if let Some(filter) = &self.path_filter
1296 && self
1297 .alternatives
1298 .iter()
1299 .any(|alternative| alternative.raw.starts_with(b"./"))
1300 {
1301 return filter.is_match(&self.alternatives, self.options, self.options, path);
1308 }
1309 self.is_match(path)
1310 }
1311
1312 #[must_use]
1336 pub fn filter_paths_at<'a, T>(
1337 &self,
1338 base_path: impl AsRef<[u8]>,
1339 paths: impl IntoIterator<Item = &'a T>,
1340 ) -> Vec<&'a T>
1341 where
1342 T: AsRef<[u8]> + ?Sized + 'a,
1343 {
1344 let base_path = base_path.as_ref();
1345 paths
1346 .into_iter()
1347 .filter(|path| {
1348 path_after_base(base_path, path.as_ref())
1349 .is_some_and(|relative| self.is_match_path(relative))
1350 })
1351 .collect()
1352 }
1353
1354 #[must_use]
1370 pub fn filter_path_indices<'a, T>(&self, paths: impl IntoIterator<Item = &'a T>) -> Vec<usize>
1371 where
1372 T: AsRef<[u8]> + ?Sized + 'a,
1373 {
1374 paths
1375 .into_iter()
1376 .enumerate()
1377 .filter_map(|(index, path)| self.is_match_path(path.as_ref()).then_some(index))
1378 .collect()
1379 }
1380
1381 #[must_use]
1397 pub fn filter_path_indices_at<'a, T>(
1398 &self,
1399 base_path: impl AsRef<[u8]>,
1400 paths: impl IntoIterator<Item = &'a T>,
1401 ) -> Vec<usize>
1402 where
1403 T: AsRef<[u8]> + ?Sized + 'a,
1404 {
1405 let base_path = base_path.as_ref();
1406 paths
1407 .into_iter()
1408 .enumerate()
1409 .filter_map(|(index, path)| {
1410 path_after_base(base_path, path.as_ref())
1411 .is_some_and(|relative| self.is_match_path(relative))
1412 .then_some(index)
1413 })
1414 .collect()
1415 }
1416
1417 fn matches_path_filter(&self, path: &[u8]) -> bool {
1418 let path = without_leading_dot_slash(path);
1419 let Some(filter) = &self.path_filter else {
1420 return self.is_match(path);
1421 };
1422 let component = PatternOptions {
1423 component_wildcards: true,
1424 ..self.options
1425 };
1426 filter.is_match(&self.alternatives, component, self.options, path)
1427 }
1428
1429 fn from_alternatives(
1430 alternatives: Vec<CompiledAlternative>,
1431 options: PatternOptions,
1432 budget: &mut IrBudget,
1433 ) -> Result<Self, PatternError> {
1434 let alternative_fast_path =
1435 AlternativeFastPath::compile(&alternatives, options, budget)?.map(Box::new);
1436 let path_filter = PathFilter::compile(&alternatives, options, budget)?.map(Box::new);
1437 Ok(Self {
1438 alternatives,
1439 alternative_fast_path,
1440 path_filter,
1441 walker_path_viability: WalkerPathViability::Viable,
1442 walker_path_problem_offset: None,
1443 options,
1444 })
1445 }
1446
1447 fn compile_path_filter_alternative(
1448 alternative: &CompiledAlternative,
1449 options: PatternOptions,
1450 budget: &mut IrBudget,
1451 ) -> Result<CompiledAlternative, PatternError> {
1452 let leading_dot_slash = alternative.raw.starts_with(b"./")
1453 && matches!(alternative.tokens.as_slice(), [Token::Literal(dot), Token::Separator, ..] if dot == b".");
1454 let raw = if leading_dot_slash {
1455 alternative.raw[2..].to_vec()
1456 } else {
1457 alternative.raw.clone()
1458 };
1459 let source_tokens = if leading_dot_slash {
1460 &alternative.tokens[2..]
1461 } else {
1462 &alternative.tokens
1463 };
1464 let tokens = source_tokens.to_vec();
1465 let fast_path = FastPath::compile(
1466 &tokens,
1467 PatternOptions {
1468 component_wildcards: true,
1469 ..options
1470 },
1471 );
1472 budget.charge(tokens.len(), 0)?;
1473 let mut provenance_budget = ProvenanceBudget::new();
1474 let extglob = compile_extglob(&raw, options, budget, &mut provenance_budget, None, false)?;
1475 let built = alternative.sweep.as_ref().filter(|_| !leading_dot_slash);
1478 let sweep = compile_sweep(
1479 &tokens,
1480 &fast_path,
1481 extglob.as_deref(),
1482 options,
1483 budget,
1484 built,
1485 )?;
1486 Ok(CompiledAlternative {
1487 extglob,
1488 raw,
1489 fast_path,
1490 prefilter: Prefilter::compile(&tokens),
1491 sweep,
1492 tokens,
1493 walker_path_shape: alternative.walker_path_shape.clone(),
1494 })
1495 }
1496
1497 fn matches_from<const SKIP: bool>(
1512 tokens: &[Token],
1513 path: &[u8],
1514 options: PatternOptions,
1515 failed: &mut FailedStates<'_>,
1516 deferred: &mut Vec<(usize, usize)>,
1517 work: &mut StarWork<'_>,
1518 ) -> bool {
1519 let mut state = (0_usize, 0_usize);
1520
1521 loop {
1522 let (token_index, path_index) = state;
1523 let advanced = if token_index == tokens.len() {
1524 if path_index == path.len() {
1525 return true;
1526 }
1527 None
1528 } else if !failed.insert(token_index, path_index) {
1529 None
1530 } else {
1531 match &tokens[token_index] {
1532 Token::Literal(literal) => {
1533 Self::advance_literal(token_index, path_index, path, options, literal)
1534 }
1535 Token::Separator => {
1536 if path.get(path_index).is_some_and(|byte| is_separator(*byte)) {
1537 Some((token_index + 1, path_index + 1))
1538 } else if path_index == path.len()
1539 && token_index + 2 == tokens.len()
1540 && matches!(tokens.get(token_index + 1), Some(Token::RecursiveStar))
1541 {
1542 return true;
1543 } else {
1544 None
1545 }
1546 }
1547 Token::Any => {
1548 Self::advance_one(tokens, token_index, path_index, path, options, |_| true)
1549 }
1550 Token::Class(class) => {
1551 Self::advance_one(tokens, token_index, path_index, path, options, |byte| {
1552 class.matches(byte, options.case_insensitive)
1553 })
1554 }
1555 Token::Star => Self::advance_star::<SKIP>(
1556 token_index,
1557 path_index,
1558 path,
1559 options,
1560 StarSemantics::ordinary(!Self::component_wildcard(
1561 tokens,
1562 token_index,
1563 options,
1564 )),
1565 deferred,
1566 work,
1567 ),
1568 Token::RecursiveStar => Self::advance_star::<SKIP>(
1569 token_index,
1570 path_index,
1571 path,
1572 options,
1573 StarSemantics::ordinary(true),
1574 deferred,
1575 work,
1576 ),
1577 Token::RecursivePrefix => Self::advance_star::<SKIP>(
1578 token_index,
1579 path_index,
1580 path,
1581 options,
1582 StarSemantics::RECURSIVE_PREFIX,
1583 deferred,
1584 work,
1585 ),
1586 }
1587 };
1588
1589 debug_assert!(
1590 deferred.len() <= tokens.len(),
1591 "the work list holds at most one deferred repetition per token"
1592 );
1593 match advanced.or_else(|| deferred.pop()) {
1594 Some(next) => state = next,
1595 None => return false,
1596 }
1597 }
1598 }
1599
1600 fn component_wildcard(tokens: &[Token], token_index: usize, options: PatternOptions) -> bool {
1601 options.component_wildcards
1602 && (options.root_component_wildcards
1603 || token_index == 0 && options.candidate_root_component_wildcard
1604 || token_index > 0 && matches!(tokens[token_index - 1], Token::Separator))
1605 }
1606
1607 fn advance_literal(
1609 token_index: usize,
1610 path_index: usize,
1611 path: &[u8],
1612 options: PatternOptions,
1613 literal: &[u8],
1614 ) -> Option<(usize, usize)> {
1615 let candidate = path.get(path_index..path_index + literal.len())?;
1616 literal
1617 .iter()
1618 .zip(candidate)
1619 .all(|(&expected, &actual)| bytes_equal(expected, actual, options.case_insensitive))
1620 .then_some((token_index + 1, path_index + literal.len()))
1621 }
1622
1623 fn advance_one(
1627 tokens: &[Token],
1628 token_index: usize,
1629 path_index: usize,
1630 path: &[u8],
1631 options: PatternOptions,
1632 accepts: impl FnOnce(u8) -> bool,
1633 ) -> Option<(usize, usize)> {
1634 let &byte = path.get(path_index)?;
1635 (accepts(byte)
1636 && (!Self::component_wildcard(tokens, token_index, options) || !is_separator(byte))
1637 && (options.match_hidden
1638 || byte != b'.'
1639 || !at_component_start(path, path_index, options)))
1640 .then_some((token_index + 1, path_index + 1))
1641 }
1642
1643 fn advance_star<const SKIP: bool>(
1651 token_index: usize,
1652 path_index: usize,
1653 path: &[u8],
1654 options: PatternOptions,
1655 semantics: StarSemantics,
1656 deferred: &mut Vec<(usize, usize)>,
1657 work: &mut StarWork<'_>,
1658 ) -> Option<(usize, usize)> {
1659 if let Some(next) = Self::next_star_position::<SKIP>(
1660 token_index,
1661 path_index,
1662 path,
1663 options,
1664 semantics.recursive,
1665 work,
1666 ) {
1667 deferred.push((token_index, next));
1668 }
1669 let hidden_stop_allowed = !semantics.blocks_hidden_stop
1670 || options.match_hidden
1671 || path.get(path_index) != Some(&b'.')
1672 || !at_component_start(path, path_index, options);
1673 let component_stop_allowed =
1674 !semantics.stops_at_component_start || at_component_start(path, path_index, options);
1675 (hidden_stop_allowed && component_stop_allowed).then_some((token_index + 1, path_index))
1676 }
1677
1678 #[inline]
1693 fn next_star_position<const SKIP: bool>(
1694 token_index: usize,
1695 path_index: usize,
1696 path: &[u8],
1697 options: PatternOptions,
1698 recursive: bool,
1699 work: &mut StarWork<'_>,
1700 ) -> Option<usize> {
1701 if SKIP && work.skip_token == token_index {
1702 return Self::skip_to_literal(token_index, path_index, path, options, recursive, work);
1703 }
1704
1705 let &byte = path.get(path_index)?;
1706 star_consumes_byte(path, path_index, byte, options, recursive).then_some(path_index + 1)
1707 }
1708
1709 #[inline(never)]
1712 fn skip_to_literal(
1713 token_index: usize,
1714 path_index: usize,
1715 path: &[u8],
1716 options: PatternOptions,
1717 recursive: bool,
1718 work: &mut StarWork<'_>,
1719 ) -> Option<usize> {
1720 let literal = work.skip_literal;
1721 let scans = work.scans(token_index);
1722 if scans.stalled >= STALLED_SKIPS {
1723 let &byte = path.get(path_index)?;
1724 return star_consumes_byte(path, path_index, byte, options, recursive)
1725 .then_some(path_index + 1);
1726 }
1727 if let Some(cached) = scans.skip.get(path_index) {
1728 return cached;
1729 }
1730 let barrier = star_barrier(path, path_index, options, recursive, scans);
1731 let start = path_index + 1;
1732 if start > barrier {
1733 return scans.skip.record(path_index, path_index, None);
1734 }
1735 match next_literal_start(path, start, literal, &mut scans.literal) {
1736 None => scans.skip.record(path_index, usize::MAX, None),
1738 Some(found) if found > barrier => scans.skip.record(path_index, barrier, None),
1741 Some(found) => {
1742 scans.stalled = if found == start {
1743 scans.stalled.saturating_add(1)
1744 } else {
1745 0
1746 };
1747 scans.skip.record(path_index, found - 1, Some(found))
1748 }
1749 }
1750 }
1751}
1752
1753enum FailedStates<'scratch> {
1764 Inline {
1765 width: usize,
1766 states: u128,
1767 },
1768 Heap {
1769 width: usize,
1770 states: &'scratch mut [u64],
1771 },
1772}
1773
1774impl<'scratch> FailedStates<'scratch> {
1775 fn new(tokens: &[Token], path: &[u8], scratch: &'scratch mut Vec<u64>) -> Self {
1776 let width = path.len() + 1;
1777 let state_count = tokens.len().saturating_mul(width);
1778 if state_count <= u128::BITS as usize {
1779 return Self::Inline { width, states: 0 };
1780 }
1781 let words = state_count.div_ceil(u64::BITS as usize);
1782 if scratch.len() < words {
1783 scratch.resize(words, 0);
1784 }
1785 let states = &mut scratch[..words];
1786 states.fill(0);
1787 Self::Heap { width, states }
1788 }
1789
1790 fn insert(&mut self, token_index: usize, path_index: usize) -> bool {
1791 match self {
1792 Self::Inline { width, states } => {
1793 let mask = 1_u128 << (token_index * *width + path_index);
1794 if *states & mask != 0 {
1795 false
1796 } else {
1797 *states |= mask;
1798 true
1799 }
1800 }
1801 Self::Heap { width, states } => {
1802 let state = token_index * *width + path_index;
1803 let word = &mut states[state / u64::BITS as usize];
1804 let mask = 1_u64 << (state % u64::BITS as usize);
1805 if *word & mask != 0 {
1806 false
1807 } else {
1808 *word |= mask;
1809 true
1810 }
1811 }
1812 }
1813 }
1814}
1815
1816#[derive(Default)]
1822struct Scratch {
1823 visited: Vec<u64>,
1824 deferred: Vec<(usize, usize)>,
1825 scans: Vec<StarScans>,
1826 generation: u64,
1827}
1828
1829struct StarWork<'scratch> {
1831 scans: &'scratch mut [StarScans],
1835 generation: u64,
1836 skip_token: usize,
1840 skip_literal: &'scratch [u8],
1842}
1843
1844#[derive(Clone, Copy)]
1845struct StarSemantics {
1846 recursive: bool,
1847 blocks_hidden_stop: bool,
1848 stops_at_component_start: bool,
1852}
1853
1854impl StarSemantics {
1855 const RECURSIVE_PREFIX: Self = Self {
1856 recursive: true,
1857 blocks_hidden_stop: false,
1858 stops_at_component_start: true,
1859 };
1860
1861 const fn ordinary(recursive: bool) -> Self {
1862 Self {
1863 recursive,
1864 blocks_hidden_stop: true,
1865 stops_at_component_start: false,
1866 }
1867 }
1868}
1869
1870impl StarWork<'_> {
1871 fn scans(&mut self, token_index: usize) -> &mut StarScans {
1872 let scans = &mut self.scans[token_index];
1873 if scans.generation != self.generation {
1874 *scans = StarScans {
1875 generation: self.generation,
1876 ..StarScans::default()
1877 };
1878 }
1879 scans
1880 }
1881}
1882
1883const RETAINED_SCRATCH_WORDS: usize = 8 * 1024;
1887
1888thread_local! {
1889 static SCRATCH: RefCell<Scratch> = const {
1890 RefCell::new(Scratch {
1891 visited: Vec::new(),
1892 deferred: Vec::new(),
1893 scans: Vec::new(),
1894 generation: 0,
1895 })
1896 };
1897}
1898
1899#[cfg(test)]
1904fn scratch_capacities() -> (usize, usize, usize) {
1905 SCRATCH.with(|cell| {
1906 let scratch = cell.borrow();
1907 (
1908 scratch.visited.capacity(),
1909 scratch.deferred.capacity(),
1910 scratch.scans.capacity(),
1911 )
1912 })
1913}
1914
1915#[derive(Clone, Copy, Default)]
1924struct StarScans {
1925 generation: u64,
1926 skip: SkipDecision,
1927 separator: FirstAtOrAfter,
1928 component_dot: FirstAtOrAfter,
1929 literal: FirstAtOrAfter,
1930 stalled: u8,
1933}
1934
1935const STALLED_SKIPS: u8 = 4;
1943
1944#[derive(Clone, Copy)]
1950struct SkipDecision {
1951 from: usize,
1952 upto: usize,
1953 jump: usize,
1955}
1956
1957impl SkipDecision {
1958 fn get(self, index: usize) -> Option<Option<usize>> {
1959 (self.from <= index && index <= self.upto)
1960 .then(|| (self.jump != usize::MAX).then_some(self.jump))
1961 }
1962
1963 fn record(&mut self, from: usize, upto: usize, jump: Option<usize>) -> Option<usize> {
1964 *self = Self {
1965 from,
1966 upto,
1967 jump: jump.unwrap_or(usize::MAX),
1968 };
1969 jump
1970 }
1971}
1972
1973impl Default for SkipDecision {
1974 fn default() -> Self {
1975 Self {
1976 from: usize::MAX,
1977 upto: 0,
1978 jump: usize::MAX,
1979 }
1980 }
1981}
1982
1983#[derive(Clone, Copy)]
1989struct FirstAtOrAfter {
1990 from: usize,
1991 answer: usize,
1992}
1993
1994impl FirstAtOrAfter {
1995 const EMPTY: Self = Self {
1997 from: usize::MAX,
1998 answer: 0,
1999 };
2000
2001 fn get(self, index: usize) -> Option<usize> {
2002 (self.from <= index && index <= self.answer).then_some(self.answer)
2003 }
2004
2005 fn record(&mut self, index: usize, answer: usize) -> usize {
2006 *self = Self {
2007 from: index,
2008 answer,
2009 };
2010 answer
2011 }
2012}
2013
2014impl Default for FirstAtOrAfter {
2015 fn default() -> Self {
2016 Self::EMPTY
2017 }
2018}
2019
2020fn skipping_star(tokens: &[Token], options: PatternOptions) -> Option<(usize, &[u8])> {
2032 if options.case_insensitive {
2033 return None;
2034 }
2035 let index = tokens.iter().position(|token| {
2036 matches!(
2037 token,
2038 Token::Star | Token::RecursiveStar | Token::RecursivePrefix
2039 )
2040 })?;
2041 match tokens.get(index + 1) {
2042 Some(Token::Literal(literal)) if !literal.is_empty() => Some((index, literal)),
2043 _ => None,
2044 }
2045}
2046
2047fn star_consumes_byte(
2049 path: &[u8],
2050 path_index: usize,
2051 byte: u8,
2052 options: PatternOptions,
2053 recursive: bool,
2054) -> bool {
2055 (recursive || !options.component_wildcards || !is_separator(byte))
2056 && (options.match_hidden || byte != b'.' || !at_component_start(path, path_index, options))
2057}
2058
2059fn star_barrier(
2066 path: &[u8],
2067 path_index: usize,
2068 options: PatternOptions,
2069 recursive: bool,
2070 scans: &mut StarScans,
2071) -> usize {
2072 let mut barrier = path.len();
2073 if !recursive && options.component_wildcards {
2074 barrier = barrier.min(next_separator_from(path, path_index, &mut scans.separator));
2075 }
2076 if !options.match_hidden {
2077 barrier = barrier.min(next_component_dot(
2078 path,
2079 path_index,
2080 options.candidate_starts_component,
2081 &mut scans.component_dot,
2082 ));
2083 }
2084 barrier
2085}
2086
2087fn next_literal_start(
2089 path: &[u8],
2090 index: usize,
2091 literal: &[u8],
2092 cache: &mut FirstAtOrAfter,
2093) -> Option<usize> {
2094 let found = match cache.get(index) {
2095 Some(found) => found,
2096 None => {
2097 let found = path
2098 .get(index..)
2099 .and_then(|tail| find_literal(tail, literal))
2100 .map_or(usize::MAX, |offset| index + offset);
2101 cache.record(index, found)
2102 }
2103 };
2104 (found != usize::MAX).then_some(found)
2105}
2106
2107fn find_literal(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2114 const MEMMEM_HAYSTACK_FLOOR: usize = 256;
2115 const SCALAR_HAYSTACK_CEILING: usize = 32;
2116 let (&first, rest) = needle.split_first()?;
2117 if haystack.len() >= MEMMEM_HAYSTACK_FLOOR {
2118 return memmem::find(haystack, needle);
2119 }
2120 if haystack.len() <= SCALAR_HAYSTACK_CEILING {
2121 return (0..haystack.len())
2124 .find(|&start| haystack[start] == first && haystack[start + 1..].starts_with(rest));
2125 }
2126 let mut offset = 0;
2127 while let Some(hit) = memchr(first, &haystack[offset..]) {
2128 let start = offset + hit;
2129 if haystack[start + 1..].starts_with(rest) {
2130 return Some(start);
2131 }
2132 offset = start + 1;
2133 }
2134 None
2135}
2136
2137fn next_separator_from(path: &[u8], index: usize, cache: &mut FirstAtOrAfter) -> usize {
2139 if let Some(found) = cache.get(index) {
2140 return found;
2141 }
2142 let found = path
2143 .get(index..)
2144 .and_then(next_separator)
2145 .map_or(path.len(), |offset| index + offset);
2146 cache.record(index, found)
2147}
2148
2149fn next_separator(bytes: &[u8]) -> Option<usize> {
2151 if cfg!(windows) {
2152 memchr2(b'/', b'\\', bytes)
2153 } else {
2154 memchr(b'/', bytes)
2155 }
2156}
2157
2158fn next_component_dot(
2164 path: &[u8],
2165 index: usize,
2166 candidate_starts_component: bool,
2167 cache: &mut FirstAtOrAfter,
2168) -> usize {
2169 if let Some(found) = cache.get(index) {
2170 return found;
2171 }
2172 if index == 0 && candidate_starts_component && path.first() == Some(&b'.') {
2173 return cache.record(0, 0);
2174 }
2175 let start = index.saturating_sub(1);
2176 let found = match path.get(start..) {
2177 Some(window) => {
2178 let mut offset = 0;
2179 loop {
2180 let Some(hit) = next_separator(&window[offset..]) else {
2181 break path.len();
2182 };
2183 let at = offset + hit;
2184 if window.get(at + 1) == Some(&b'.') {
2185 break start + at + 1;
2186 }
2187 offset = at + 1;
2188 }
2189 }
2190 None => path.len(),
2191 };
2192 cache.record(index, found)
2193}
2194
2195pub const VERSION: &str = env!("CARGO_PKG_VERSION");
2197
2198#[derive(Debug, Clone, PartialEq, Eq)]
2199enum Token {
2200 Literal(Vec<u8>),
2201 Separator,
2202 Any,
2203 Star,
2204 RecursiveStar,
2207 RecursivePrefix,
2210 Class(Class),
2211}
2212
2213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2223struct ComponentBounds {
2224 starts: bool,
2225 ends: bool,
2226}
2227
2228impl ComponentBounds {
2229 const WHOLE: Self = Self {
2230 starts: true,
2231 ends: true,
2232 };
2233}
2234
2235fn star_run_is_whole_component(
2244 pattern: &[u8],
2245 start: usize,
2246 end: usize,
2247 escapes: bool,
2248 bounds: ComponentBounds,
2249) -> bool {
2250 starts_component_at(pattern, start, escapes, bounds) && ends_component_at(pattern, end, bounds)
2251}
2252
2253fn starts_component_at(
2256 pattern: &[u8],
2257 start: usize,
2258 escapes: bool,
2259 bounds: ComponentBounds,
2260) -> bool {
2261 let Some(before) = start.checked_sub(1) else {
2262 return bounds.starts;
2263 };
2264 let escaped = escapes
2265 && pattern[..before]
2266 .iter()
2267 .rev()
2268 .take_while(|&&byte| byte == b'\\')
2269 .count()
2270 % 2
2271 == 1;
2272 pattern[before] == b'/' && !escaped
2273}
2274
2275fn ends_component_at(pattern: &[u8], end: usize, bounds: ComponentBounds) -> bool {
2278 pattern.get(end).map_or(bounds.ends, |&byte| byte == b'/')
2279}
2280
2281#[derive(Debug, Clone, PartialEq, Eq)]
2282struct CompiledAlternative {
2287 raw: Vec<u8>,
2288 fast_path: Option<FastPath>,
2289 tokens: Vec<Token>,
2290 extglob: Option<Box<CompiledExtglob>>,
2294 prefilter: Prefilter,
2297 walker_path_shape: WalkerPathShape,
2302 sweep: Option<SweepEngine>,
2307}
2308
2309impl CompiledAlternative {
2310 fn release_compile_state(&mut self) {
2313 self.walker_path_shape = WalkerPathShape::default();
2314 if let Some(program) = &mut self.extglob {
2315 program.walker_source_provenance = None;
2316 for group in &mut program.groups {
2317 for alternative in group
2318 .alternatives
2319 .iter_mut()
2320 .chain(group.through_separator.iter_mut())
2321 {
2322 for nested in &mut alternative.compiled {
2323 nested.release_compile_state();
2324 }
2325 }
2326 }
2327 }
2328 }
2329
2330 #[cfg(any(test, feature = "unstable-test-hooks"))]
2333 fn strip_engines(
2334 &mut self,
2335 options: PatternOptions,
2336 fast_paths: bool,
2337 sweeps: bool,
2338 prefilters: bool,
2339 ) {
2340 if fast_paths {
2341 if !sweeps && sweep_is_deferred(self) {
2344 self.sweep = SweepEngine::build(&self.tokens, options).ok();
2345 }
2346 self.fast_path = None;
2347 }
2348 if sweeps {
2349 self.sweep = None;
2350 }
2351 if prefilters {
2352 self.prefilter = Prefilter::default();
2353 }
2354 if let Some(program) = &mut self.extglob {
2355 for group in &mut program.groups {
2356 for alternative in group
2357 .alternatives
2358 .iter_mut()
2359 .chain(group.through_separator.iter_mut())
2360 {
2361 for nested in &mut alternative.compiled {
2362 nested.strip_engines(options, fast_paths, sweeps, prefilters);
2363 }
2364 }
2365 }
2366 }
2367 }
2368}
2369
2370fn compile_sweep(
2388 tokens: &[Token],
2389 fast_path: &Option<FastPath>,
2390 extglob: Option<&CompiledExtglob>,
2391 options: PatternOptions,
2392 budget: &mut IrBudget,
2393 built: Option<&SweepEngine>,
2394) -> Result<Option<SweepEngine>, PatternError> {
2395 if extglob.is_some()
2396 || matches!(
2397 fast_path,
2398 Some(FastPath::LiteralTokens(_) | FastPath::DeterministicTokens(_))
2399 )
2400 {
2401 return Ok(None);
2402 }
2403 if fast_path
2404 .as_ref()
2405 .is_some_and(FastPath::supports_component_wildcards)
2406 {
2407 SweepEngine::charge(tokens, budget)?;
2408 return Ok(None);
2409 }
2410 if let Some(built) = built {
2411 SweepEngine::charge(tokens, budget)?;
2412 return Ok(Some(built.clone()));
2413 }
2414 SweepEngine::compile(tokens, options, budget)
2415}
2416
2417fn sweep_is_deferred(alternative: &CompiledAlternative) -> bool {
2420 alternative.sweep.is_none()
2421 && alternative.extglob.is_none()
2422 && alternative.fast_path.as_ref().is_some_and(|fast_path| {
2423 fast_path.supports_component_wildcards()
2424 && !matches!(
2425 fast_path,
2426 FastPath::LiteralTokens(_) | FastPath::DeterministicTokens(_)
2427 )
2428 })
2429}
2430
2431#[derive(Debug, Clone, PartialEq, Eq, Default)]
2458struct Prefilter {
2459 prefix: Vec<u8>,
2462 suffix: Vec<u8>,
2464 min_length: usize,
2466 interior: Option<Box<InteriorLiteral>>,
2470}
2471
2472#[derive(Debug, Clone)]
2500struct InteriorLiteral(memmem::Finder<'static>);
2501
2502impl PartialEq for InteriorLiteral {
2503 fn eq(&self, other: &Self) -> bool {
2504 self.0.needle() == other.0.needle()
2505 }
2506}
2507
2508impl Eq for InteriorLiteral {}
2509
2510impl Prefilter {
2511 fn compile(tokens: &[Token]) -> Self {
2512 let elidable_separator = tokens.len() >= 2
2515 && matches!(
2516 tokens[tokens.len() - 2..],
2517 [Token::Separator, Token::RecursiveStar]
2518 );
2519 let fixed = |token: &&Token| matches!(token, Token::Literal(_) | Token::Separator);
2520 let mut leading = tokens.iter().take_while(fixed).count();
2521 if elidable_separator && leading + 1 == tokens.len() {
2522 leading -= 1;
2523 }
2524 let trailing = tokens.iter().rev().take_while(fixed).count();
2525 let min_length =
2526 tokens.iter().map(token_min_length).sum::<usize>() - usize::from(elidable_separator);
2527 let prefilter = Self {
2528 prefix: run_bytes(&tokens[..leading]),
2529 suffix: run_bytes(&tokens[tokens.len() - trailing..]),
2530 min_length,
2531 interior: interior_literal(tokens, leading, trailing, elidable_separator)
2532 .map(|needle| Box::new(InteriorLiteral(memmem::Finder::new(&needle).into_owned()))),
2533 };
2534 debug_assert!(
2535 prefilter.min_length >= prefilter.prefix.len()
2536 && prefilter.min_length >= prefilter.suffix.len(),
2537 "the fixed runs are part of what the minimum length counts"
2538 );
2539 prefilter
2540 }
2541
2542 fn rejects(&self, path: &[u8], case_insensitive: bool) -> bool {
2545 if path.len() < self.min_length {
2546 return true;
2547 }
2548 let tail = path.len() - self.suffix.len();
2549 !run_matches(&self.prefix, &path[..self.prefix.len()], case_insensitive)
2550 || !run_matches(&self.suffix, &path[tail..], case_insensitive)
2551 || !case_insensitive
2552 && self
2553 .interior
2554 .as_ref()
2555 .is_some_and(|interior| interior.0.find(path).is_none())
2556 }
2557}
2558
2559fn interior_literal(
2567 tokens: &[Token],
2568 leading: usize,
2569 trailing: usize,
2570 elidable_separator: bool,
2571) -> Option<Vec<u8>> {
2572 let interior_end = tokens.len() - trailing;
2573 let mut best: Option<Vec<u8>> = None;
2574 let mut run = Vec::new();
2575 for (index, token) in tokens.iter().enumerate() {
2576 let joins = index >= leading && index < interior_end;
2577 match token {
2578 Token::Literal(literal) if joins => run.extend_from_slice(literal),
2579 Token::Separator
2580 if joins
2581 && !cfg!(windows)
2582 && !(elidable_separator && index + 2 == tokens.len()) =>
2583 {
2584 run.push(b'/');
2585 }
2586 _ => {
2587 keep_longer_run(&mut best, &mut run);
2588 }
2589 }
2590 }
2591 keep_longer_run(&mut best, &mut run);
2592 best
2593}
2594
2595fn keep_longer_run(best: &mut Option<Vec<u8>>, run: &mut Vec<u8>) {
2598 if run.len() > best.as_ref().map_or(0, Vec::len) && run.iter().any(|&byte| !is_separator(byte))
2599 {
2600 *best = Some(std::mem::take(run));
2601 } else {
2602 run.clear();
2603 }
2604}
2605
2606fn run_bytes(run: &[Token]) -> Vec<u8> {
2610 fn bytes(token: &Token) -> &[u8] {
2611 match token {
2612 Token::Literal(literal) => literal,
2613 _ => b"/",
2614 }
2615 }
2616 let mut collected = Vec::with_capacity(run.iter().map(|token| bytes(token).len()).sum());
2617 for token in run {
2618 collected.extend_from_slice(bytes(token));
2619 }
2620 collected
2621}
2622
2623fn token_min_length(token: &Token) -> usize {
2624 match token {
2625 Token::Literal(literal) => literal.len(),
2626 Token::Separator | Token::Any | Token::Class(_) => 1,
2627 Token::Star | Token::RecursiveStar | Token::RecursivePrefix => 0,
2628 }
2629}
2630
2631fn run_matches(expected: &[u8], actual: &[u8], case_insensitive: bool) -> bool {
2632 expected
2633 .iter()
2634 .zip(actual)
2635 .all(|(&expected, &actual)| run_byte_matches(expected, actual, case_insensitive))
2636}
2637
2638fn run_byte_matches(expected: u8, actual: u8, case_insensitive: bool) -> bool {
2646 if is_separator(expected) {
2647 is_separator(actual)
2648 } else {
2649 bytes_equal(expected, actual, case_insensitive)
2650 }
2651}
2652
2653#[derive(Debug, Clone, PartialEq, Eq)]
2655enum LiteralSuffix {
2656 Plain(Vec<u8>),
2657 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
2658 Packed16(Box<suffix_word::PreparedSuffix16>),
2659}
2660
2661impl LiteralSuffix {
2662 fn new(suffix: Vec<u8>, case_insensitive: bool) -> Self {
2663 #[cfg(not(all(target_arch = "aarch64", target_os = "macos")))]
2664 let _ = case_insensitive;
2665
2666 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
2667 if !case_insensitive && let Some(prepared) = suffix_word::PreparedSuffix16::new(&suffix) {
2668 return Self::Packed16(Box::new(prepared));
2669 }
2670
2671 Self::Plain(suffix)
2672 }
2673
2674 fn strip_from<'a>(&self, path: &'a [u8], case_insensitive: bool) -> Option<&'a [u8]> {
2675 match self {
2676 Self::Plain(suffix) => strip_literal_suffix(path, suffix, case_insensitive),
2677 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
2678 Self::Packed16(suffix) => {
2679 if !case_insensitive && let Some(matches) = suffix.matches(path) {
2680 return matches.then_some(&path[..path.len() - suffix.len()]);
2681 }
2682 let bytes = suffix.bytes();
2683 strip_literal_suffix(path, &bytes[16 - suffix.len()..], case_insensitive)
2684 }
2685 }
2686 }
2687}
2688
2689#[derive(Debug, Clone, PartialEq, Eq)]
2693enum AlternativeFastPath {
2694 SuffixSet(SuffixSet),
2695 ScopedSuffixSet(ScopedSuffixSet),
2696}
2697
2698impl AlternativeFastPath {
2699 fn compile(
2700 alternatives: &[CompiledAlternative],
2701 options: PatternOptions,
2702 budget: &mut IrBudget,
2703 ) -> Result<Option<Self>, PatternError> {
2704 if alternatives.len() < 2 || alternatives.iter().any(|item| item.extglob.is_some()) {
2705 return Ok(None);
2706 }
2707 if let Some(set) = SuffixSet::compile(alternatives, options.case_insensitive, budget)? {
2708 return Ok(Some(Self::SuffixSet(set)));
2709 }
2710 Ok(
2711 ScopedSuffixSet::compile(alternatives, options.case_insensitive, budget)?
2712 .map(Self::ScopedSuffixSet),
2713 )
2714 }
2715
2716 fn is_match(
2717 &self,
2718 path: &[u8],
2719 options: PatternOptions,
2720 alternatives: &[CompiledAlternative],
2721 ) -> bool {
2722 match self {
2723 Self::SuffixSet(set) => set.is_match(path, options),
2724 Self::ScopedSuffixSet(set) => {
2725 alternatives
2726 .first()
2727 .and_then(|alternative| alternative.fast_path.as_ref())
2728 .is_some_and(|fast_path| fast_path.is_match(path, options))
2729 || set.is_match(path, options)
2730 }
2731 }
2732 }
2733}
2734
2735#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2736enum SuffixSetKind {
2737 Star,
2738 Recursive,
2739}
2740
2741#[derive(Debug, Clone, PartialEq, Eq)]
2742struct SuffixSet {
2743 kind: SuffixSetKind,
2744 trie: AffixTrie,
2745}
2746
2747#[derive(Debug, Clone, PartialEq, Eq)]
2750struct ScopedSuffixSet {
2751 prefixes: AffixTrie,
2752 suffixes: AffixTrie,
2753}
2754
2755#[derive(Debug, Clone, PartialEq, Eq)]
2756struct AffixTrie {
2757 nodes: Vec<AffixNode>,
2758 edges: Vec<AffixEdge>,
2759}
2760
2761#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2762struct AffixNode {
2763 first_edge: usize,
2764 edge_count: usize,
2765 terminal: bool,
2766}
2767
2768#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2769struct AffixEdge {
2770 byte: u8,
2771 next: usize,
2772}
2773
2774#[derive(Default)]
2775struct BuildingAffixNode {
2776 edges: Vec<(u8, usize)>,
2777 terminal: bool,
2778}
2779
2780impl SuffixSet {
2781 fn compile(
2782 alternatives: &[CompiledAlternative],
2783 case_insensitive: bool,
2784 budget: &mut IrBudget,
2785 ) -> Result<Option<Self>, PatternError> {
2786 let mut suffixes = Vec::with_capacity(alternatives.len());
2787 let mut common_kind = None;
2788 for alternative in alternatives {
2789 let Some(fast_path) = alternative.fast_path.as_ref() else {
2790 return Ok(None);
2791 };
2792 let (kind, suffix) = match (fast_path, alternative.tokens.as_slice()) {
2793 (FastPath::StarSuffix { .. }, [Token::Star, Token::Literal(suffix)]) => {
2794 (SuffixSetKind::Star, suffix)
2795 }
2796 (
2797 FastPath::RecursiveSuffix { .. },
2798 [Token::RecursivePrefix, Token::Star, Token::Literal(suffix)],
2799 ) => (SuffixSetKind::Recursive, suffix),
2800 _ => return Ok(None),
2801 };
2802 if common_kind.is_some_and(|common| common != kind) {
2803 return Ok(None);
2804 }
2805 suffixes.push(suffix.as_slice());
2806 common_kind = Some(kind);
2807 }
2808 Ok(Some(Self {
2809 kind: common_kind.expect("two suffix alternatives establish a kind"),
2810 trie: AffixTrie::new(&suffixes, true, case_insensitive, budget)?,
2811 }))
2812 }
2813
2814 fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
2815 let mut node = 0;
2816 for (offset, &byte) in path.iter().rev().enumerate() {
2817 let Some(next) = self
2818 .trie
2819 .next(node, fold_ascii(byte, options.case_insensitive))
2820 else {
2821 return false;
2822 };
2823 node = next;
2824 if !self.trie.nodes[node].terminal {
2825 continue;
2826 }
2827
2828 let variable_end = path.len() - offset - 1;
2829 if self.kind == SuffixSetKind::Star
2830 && options.component_wildcards
2831 && next_separator(&path[..variable_end]).is_some()
2832 {
2833 continue;
2834 }
2835 if star_stops_before_hidden_component(path, variable_end, options) {
2836 continue;
2837 }
2838 if options.match_hidden
2839 || !contains_hidden_component_in(
2840 path,
2841 0,
2842 variable_end,
2843 options.candidate_starts_component,
2844 )
2845 {
2846 return true;
2847 }
2848 }
2849 false
2850 }
2851}
2852
2853impl ScopedSuffixSet {
2854 fn compile(
2855 alternatives: &[CompiledAlternative],
2856 case_insensitive: bool,
2857 budget: &mut IrBudget,
2858 ) -> Result<Option<Self>, PatternError> {
2859 let mut prefixes = Vec::new();
2860 let mut suffixes = Vec::new();
2861 let mut pairs = Vec::with_capacity(alternatives.len());
2862 for alternative in alternatives {
2863 let Some(fast_path) = alternative.fast_path.as_ref() else {
2864 return Ok(None);
2865 };
2866 let (
2867 FastPath::RecursivePrefixSuffix { .. },
2868 [
2869 Token::Literal(prefix),
2870 Token::Separator,
2871 Token::RecursivePrefix,
2872 Token::Star,
2873 Token::Literal(suffix),
2874 ],
2875 ) = (fast_path, alternative.tokens.as_slice())
2876 else {
2877 return Ok(None);
2878 };
2879 let prefix = literal_index(&mut prefixes, prefix, case_insensitive);
2880 let suffix = literal_index(&mut suffixes, suffix, case_insensitive);
2881 if !pairs.contains(&(prefix, suffix)) {
2882 pairs.push((prefix, suffix));
2883 }
2884 }
2885
2886 if prefixes
2890 .len()
2891 .checked_mul(suffixes.len())
2892 .is_none_or(|product| pairs.len() != product)
2893 {
2894 return Ok(None);
2895 }
2896 Ok(Some(Self {
2897 prefixes: AffixTrie::new(&prefixes, false, case_insensitive, budget)?,
2898 suffixes: AffixTrie::new(&suffixes, true, case_insensitive, budget)?,
2899 }))
2900 }
2901
2902 fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
2903 let mut prefix_node = 0;
2904 let mut prefix_len = None;
2905 for (offset, &byte) in path.iter().enumerate() {
2906 let Some(next) = self
2907 .prefixes
2908 .next(prefix_node, fold_ascii(byte, options.case_insensitive))
2909 else {
2910 return false;
2911 };
2912 prefix_node = next;
2913 if self.prefixes.nodes[prefix_node].terminal
2914 && path.get(offset + 1).is_some_and(|byte| is_separator(*byte))
2915 {
2916 prefix_len = Some(offset + 1);
2917 break;
2918 }
2919 }
2920 let Some(prefix_len) = prefix_len else {
2921 return false;
2922 };
2923 let variable_start = prefix_len + 1;
2924
2925 let mut suffix_node = 0;
2926 for (offset, &byte) in path.iter().rev().enumerate() {
2927 let Some(next) = self
2928 .suffixes
2929 .next(suffix_node, fold_ascii(byte, options.case_insensitive))
2930 else {
2931 return false;
2932 };
2933 suffix_node = next;
2934 if !self.suffixes.nodes[suffix_node].terminal {
2935 continue;
2936 }
2937 let variable_end = path.len() - offset - 1;
2938 if variable_start > variable_end {
2939 continue;
2940 }
2941 if star_stops_before_hidden_component(path, variable_end, options) {
2942 continue;
2943 }
2944 if options.match_hidden
2945 || !contains_hidden_component_in(
2946 path,
2947 variable_start,
2948 variable_end,
2949 options.candidate_starts_component,
2950 )
2951 {
2952 return true;
2953 }
2954 }
2955 false
2956 }
2957}
2958
2959fn literal_index<'a>(
2960 literals: &mut Vec<&'a [u8]>,
2961 literal: &'a [u8],
2962 case_insensitive: bool,
2963) -> usize {
2964 if let Some(index) = literals.iter().position(|existing| {
2965 existing.len() == literal.len()
2966 && existing
2967 .iter()
2968 .zip(literal)
2969 .all(|(&left, &right)| bytes_equal(left, right, case_insensitive))
2970 }) {
2971 return index;
2972 }
2973 literals.push(literal);
2974 literals.len() - 1
2975}
2976
2977impl AffixTrie {
2978 fn new(
2979 words: &[&[u8]],
2980 reverse: bool,
2981 case_insensitive: bool,
2982 budget: &mut IrBudget,
2983 ) -> Result<Self, PatternError> {
2984 budget.charge(2, 0)?;
2987 let mut building = vec![BuildingAffixNode::default()];
2988 for word in words {
2989 if reverse {
2990 Self::insert(
2991 &mut building,
2992 word.iter()
2993 .rev()
2994 .map(|&byte| fold_ascii(byte, case_insensitive)),
2995 budget,
2996 )?;
2997 } else {
2998 Self::insert(
2999 &mut building,
3000 word.iter().map(|&byte| fold_ascii(byte, case_insensitive)),
3001 budget,
3002 )?;
3003 }
3004 }
3005
3006 let edge_count = building.iter().map(|node| node.edges.len()).sum();
3007 let mut nodes = Vec::with_capacity(building.len());
3008 let mut edges = Vec::with_capacity(edge_count);
3009 for node in building {
3010 let first_edge = edges.len();
3011 edges.extend(
3012 node.edges
3013 .into_iter()
3014 .map(|(byte, next)| AffixEdge { byte, next }),
3015 );
3016 nodes.push(AffixNode {
3017 first_edge,
3018 edge_count: edges.len() - first_edge,
3019 terminal: node.terminal,
3020 });
3021 }
3022 Ok(Self { nodes, edges })
3023 }
3024
3025 fn insert(
3026 building: &mut Vec<BuildingAffixNode>,
3027 bytes: impl Iterator<Item = u8>,
3028 budget: &mut IrBudget,
3029 ) -> Result<(), PatternError> {
3030 let mut node = 0;
3031 for byte in bytes {
3032 let existing = building[node]
3033 .edges
3034 .iter()
3035 .find_map(|&(edge, next)| (edge == byte).then_some(next));
3036 node = if let Some(existing) = existing {
3037 existing
3038 } else {
3039 budget.charge(3, 0)?;
3044 let next = building.len();
3045 building.push(BuildingAffixNode::default());
3046 building[node].edges.push((byte, next));
3047 next
3048 };
3049 }
3050 building[node].terminal = true;
3051 Ok(())
3052 }
3053
3054 fn next(&self, node: usize, byte: u8) -> Option<usize> {
3055 let node = self.nodes[node];
3056 self.edges[node.first_edge..node.first_edge + node.edge_count]
3057 .iter()
3058 .find_map(|edge| (edge.byte == byte).then_some(edge.next))
3059 }
3060}
3061
3062#[derive(Debug, Clone, PartialEq, Eq)]
3068enum FastPath {
3069 LiteralTokens(Vec<Token>),
3070 DeterministicTokens(Vec<Token>),
3071 Star,
3072 PrefixStar {
3073 prefix: Vec<u8>,
3074 },
3075 StarSuffix {
3076 suffix: LiteralSuffix,
3077 },
3078 InfixStar {
3079 prefix: Vec<u8>,
3080 suffix: Vec<u8>,
3081 },
3082 StaticStar {
3083 prefix: Vec<Token>,
3084 suffix: Vec<Token>,
3085 },
3086 RecursiveTerminalPrefix {
3087 prefix: Vec<u8>,
3088 },
3089 RecursiveSuffix {
3090 suffix: LiteralSuffix,
3091 suffix_last: u8,
3092 },
3093 RecursivePrefixSuffix {
3094 prefix: Vec<u8>,
3095 suffix: LiteralSuffix,
3096 suffix_last: u8,
3097 },
3098}
3099
3100impl FastPath {
3101 fn compile(tokens: &[Token], options: PatternOptions) -> Option<Self> {
3102 if tokens
3103 .iter()
3104 .all(|token| matches!(token, Token::Literal(_) | Token::Separator))
3105 {
3106 return Some(Self::LiteralTokens(tokens.to_vec()));
3107 }
3108 if tokens.iter().all(|token| {
3109 matches!(
3110 token,
3111 Token::Literal(_) | Token::Separator | Token::Any | Token::Class(_)
3112 )
3113 }) {
3114 return Some(Self::DeterministicTokens(tokens.to_vec()));
3115 }
3116 if options.component_wildcards {
3117 return None;
3118 }
3119 match tokens {
3120 [Token::Star] => return Some(Self::Star),
3121 [Token::Literal(prefix), Token::Star] => {
3122 return Some(Self::PrefixStar {
3123 prefix: prefix.clone(),
3124 });
3125 }
3126 [Token::Star, Token::Literal(suffix)] => {
3127 return Some(Self::StarSuffix {
3128 suffix: LiteralSuffix::new(suffix.clone(), options.case_insensitive),
3129 });
3130 }
3131 [Token::Literal(prefix), Token::Star, Token::Literal(suffix)] => {
3132 return Some(Self::InfixStar {
3133 prefix: prefix.clone(),
3134 suffix: suffix.clone(),
3135 });
3136 }
3137 _ => {}
3138 }
3139 if let Some(star_index) = tokens.iter().position(|token| matches!(token, Token::Star))
3140 && tokens.len() > 1
3141 && tokens.iter().enumerate().all(|(index, token)| {
3142 index == star_index || matches!(token, Token::Literal(_) | Token::Separator)
3143 })
3144 {
3145 return Some(Self::StaticStar {
3146 prefix: tokens[..star_index].to_vec(),
3147 suffix: tokens[star_index + 1..].to_vec(),
3148 });
3149 }
3150 if let [
3151 Token::Literal(prefix),
3152 Token::Separator,
3153 Token::RecursiveStar,
3154 ] = tokens
3155 {
3156 return Some(Self::RecursiveTerminalPrefix {
3157 prefix: prefix.clone(),
3158 });
3159 }
3160 if let [Token::RecursivePrefix, Token::Star, Token::Literal(suffix)] = tokens {
3161 return Some(Self::RecursiveSuffix {
3162 suffix: LiteralSuffix::new(suffix.clone(), options.case_insensitive),
3163 suffix_last: *suffix.last().expect("literal token is non-empty"),
3164 });
3165 }
3166 let [
3167 Token::Literal(prefix),
3168 Token::Separator,
3169 Token::RecursivePrefix,
3170 Token::Star,
3171 Token::Literal(suffix),
3172 ] = tokens
3173 else {
3174 return None;
3175 };
3176 Some(Self::RecursivePrefixSuffix {
3177 prefix: prefix.clone(),
3178 suffix: LiteralSuffix::new(suffix.clone(), options.case_insensitive),
3179 suffix_last: *suffix.last().expect("literal token is non-empty"),
3180 })
3181 }
3182
3183 fn supports_component_wildcards(&self) -> bool {
3184 matches!(
3185 self,
3186 Self::LiteralTokens(_)
3187 | Self::DeterministicTokens(_)
3188 | Self::StarSuffix { .. }
3189 | Self::RecursiveSuffix { .. }
3190 | Self::RecursivePrefixSuffix { .. }
3191 )
3192 }
3193
3194 fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
3195 match self {
3196 Self::LiteralTokens(tokens) => {
3197 let mut path_index = 0;
3198 for token in tokens {
3199 match token {
3200 Token::Literal(literal) => {
3201 let Some(candidate) = path.get(path_index..path_index + literal.len())
3202 else {
3203 return false;
3204 };
3205 if !literal.iter().zip(candidate).all(|(&expected, &actual)| {
3206 bytes_equal(expected, actual, options.case_insensitive)
3207 }) {
3208 return false;
3209 }
3210 path_index += literal.len();
3211 }
3212 Token::Separator => {
3213 if !path.get(path_index).is_some_and(|byte| is_separator(*byte)) {
3214 return false;
3215 }
3216 path_index += 1;
3217 }
3218 _ => unreachable!("literal fast path only stores literal tokens"),
3219 }
3220 }
3221 path_index == path.len()
3222 }
3223 Self::DeterministicTokens(tokens) => {
3224 let mut path_index = 0;
3225 for (token_index, token) in tokens.iter().enumerate() {
3226 match token {
3227 Token::Literal(literal) => {
3228 let Some(candidate) = path.get(path_index..path_index + literal.len())
3229 else {
3230 return false;
3231 };
3232 if !literal.iter().zip(candidate).all(|(&expected, &actual)| {
3233 bytes_equal(expected, actual, options.case_insensitive)
3234 }) {
3235 return false;
3236 }
3237 path_index += literal.len();
3238 }
3239 Token::Separator => {
3240 if !path.get(path_index).is_some_and(|byte| is_separator(*byte)) {
3241 return false;
3242 }
3243 path_index += 1;
3244 }
3245 Token::Any => {
3246 let Some(&byte) = path.get(path_index) else {
3247 return false;
3248 };
3249 if Pattern::component_wildcard(tokens, token_index, options)
3250 && is_separator(byte)
3251 {
3252 return false;
3253 }
3254 if !options.match_hidden
3255 && byte == b'.'
3256 && at_component_start(path, path_index, options)
3257 {
3258 return false;
3259 }
3260 path_index += 1;
3261 }
3262 Token::Class(class) => {
3263 let Some(&byte) = path.get(path_index) else {
3264 return false;
3265 };
3266 if (Pattern::component_wildcard(tokens, token_index, options)
3267 && is_separator(byte))
3268 || !class.matches(byte, options.case_insensitive)
3269 || (!options.match_hidden
3270 && byte == b'.'
3271 && at_component_start(path, path_index, options))
3272 {
3273 return false;
3274 }
3275 path_index += 1;
3276 }
3277 Token::Star | Token::RecursiveStar | Token::RecursivePrefix => {
3278 return false;
3279 }
3280 }
3281 }
3282 path_index == path.len()
3283 }
3284 Self::Star => {
3285 options.match_hidden
3286 || !contains_hidden_component_in(
3287 path,
3288 0,
3289 path.len(),
3290 options.candidate_starts_component,
3291 )
3292 }
3293 Self::PrefixStar { prefix } => {
3294 let Some(variable) = strip_literal_prefix(path, prefix, options.case_insensitive)
3295 else {
3296 return false;
3297 };
3298 options.match_hidden
3299 || !contains_hidden_component_in(
3300 path,
3301 path.len() - variable.len(),
3302 path.len(),
3303 options.candidate_starts_component,
3304 )
3305 }
3306 Self::StarSuffix { suffix } => {
3307 let Some(variable) = suffix.strip_from(path, options.case_insensitive) else {
3308 return false;
3309 };
3310 if options.component_wildcards
3314 && (options.root_component_wildcards
3315 || options.candidate_root_component_wildcard)
3316 && next_separator(variable).is_some()
3317 {
3318 return false;
3319 }
3320 if star_stops_before_hidden_component(path, variable.len(), options) {
3321 return false;
3322 }
3323 options.match_hidden
3324 || !contains_hidden_component_in(
3325 path,
3326 0,
3327 variable.len(),
3328 options.candidate_starts_component,
3329 )
3330 }
3331 Self::InfixStar { prefix, suffix } => {
3332 let Some(remainder) = strip_literal_prefix(path, prefix, options.case_insensitive)
3333 else {
3334 return false;
3335 };
3336 let Some(variable) =
3337 strip_literal_suffix(remainder, suffix, options.case_insensitive)
3338 else {
3339 return false;
3340 };
3341 let variable_start = path.len() - remainder.len();
3342 let variable_end = variable_start + variable.len();
3343 if star_stops_before_hidden_component(path, variable_end, options) {
3344 return false;
3345 }
3346 options.match_hidden
3347 || !contains_hidden_component_in(
3348 path,
3349 variable_start,
3350 variable_end,
3351 options.candidate_starts_component,
3352 )
3353 }
3354 Self::StaticStar { prefix, suffix } => {
3355 let Some(variable_start) = match_static_prefix(prefix, path, options) else {
3356 return false;
3357 };
3358 let Some(variable_end) = match_static_suffix(suffix, path, options) else {
3359 return false;
3360 };
3361 if variable_start > variable_end {
3362 return false;
3363 }
3364 if star_stops_before_hidden_component(path, variable_end, options) {
3365 return false;
3366 }
3367 options.match_hidden
3368 || !contains_hidden_component_in(
3369 path,
3370 variable_start,
3371 variable_end,
3372 options.candidate_starts_component,
3373 )
3374 }
3375 Self::RecursiveTerminalPrefix { prefix } => {
3376 let Some(remainder) = strip_literal_prefix(path, prefix, options.case_insensitive)
3377 else {
3378 return false;
3379 };
3380 remainder.is_empty()
3381 || remainder.first().is_some_and(|byte| is_separator(*byte))
3382 && (options.match_hidden
3383 || !contains_hidden_component_in(
3384 path,
3385 path.len() - remainder.len() + 1,
3386 path.len(),
3387 options.candidate_starts_component,
3388 ))
3389 }
3390 Self::RecursiveSuffix {
3391 suffix,
3392 suffix_last,
3393 } => {
3394 let Some(&path_last) = path.last() else {
3395 return false;
3396 };
3397 if !bytes_equal(*suffix_last, path_last, options.case_insensitive) {
3398 return false;
3399 }
3400 let Some(variable) = suffix.strip_from(path, options.case_insensitive) else {
3401 return false;
3402 };
3403 if star_stops_before_hidden_component(path, variable.len(), options) {
3404 return false;
3405 }
3406 options.match_hidden
3407 || !contains_hidden_component_in(
3408 path,
3409 0,
3410 variable.len(),
3411 options.candidate_starts_component,
3412 )
3413 }
3414 Self::RecursivePrefixSuffix {
3415 prefix,
3416 suffix,
3417 suffix_last,
3418 } => {
3419 let Some(&path_last) = path.last() else {
3420 return false;
3421 };
3422 if !bytes_equal(*suffix_last, path_last, options.case_insensitive) {
3423 return false;
3424 }
3425 let Some(prefix_and_variable) = suffix.strip_from(path, options.case_insensitive)
3426 else {
3427 return false;
3428 };
3429 let Some(remainder) =
3430 strip_literal_prefix(prefix_and_variable, prefix, options.case_insensitive)
3431 else {
3432 return false;
3433 };
3434 let Some((&separator, variable)) = remainder.split_first() else {
3435 return false;
3436 };
3437 if !is_separator(separator) {
3438 return false;
3439 }
3440 let variable_start = prefix.len() + 1;
3441 let variable_end = variable_start + variable.len();
3442 if star_stops_before_hidden_component(path, variable_end, options) {
3443 return false;
3444 }
3445 options.match_hidden
3446 || !contains_hidden_component_in(
3447 path,
3448 variable_start,
3449 variable_end,
3450 options.candidate_starts_component,
3451 )
3452 }
3453 }
3454 }
3455}
3456
3457fn match_static_prefix(tokens: &[Token], path: &[u8], options: PatternOptions) -> Option<usize> {
3458 let mut index = 0;
3459 for token in tokens {
3460 match token {
3461 Token::Literal(literal) => {
3462 let candidate = path.get(index..index + literal.len())?;
3463 if !literal.iter().zip(candidate).all(|(&expected, &actual)| {
3464 bytes_equal(expected, actual, options.case_insensitive)
3465 }) {
3466 return None;
3467 }
3468 index += literal.len();
3469 }
3470 Token::Separator => {
3471 if !path.get(index).is_some_and(|byte| is_separator(*byte)) {
3472 return None;
3473 }
3474 index += 1;
3475 }
3476 _ => return None,
3477 }
3478 }
3479 Some(index)
3480}
3481
3482fn match_static_suffix(tokens: &[Token], path: &[u8], options: PatternOptions) -> Option<usize> {
3483 let mut index = path.len();
3484 for token in tokens.iter().rev() {
3485 match token {
3486 Token::Literal(literal) => {
3487 let start = index.checked_sub(literal.len())?;
3488 if !literal
3489 .iter()
3490 .zip(&path[start..index])
3491 .all(|(&expected, &actual)| {
3492 bytes_equal(expected, actual, options.case_insensitive)
3493 })
3494 {
3495 return None;
3496 }
3497 index = start;
3498 }
3499 Token::Separator => {
3500 let start = index.checked_sub(1)?;
3501 if !is_separator(path[start]) {
3502 return None;
3503 }
3504 index = start;
3505 }
3506 _ => return None,
3507 }
3508 }
3509 Some(index)
3510}
3511
3512fn strip_literal_prefix<'a>(
3513 path: &'a [u8],
3514 literal: &[u8],
3515 case_insensitive: bool,
3516) -> Option<&'a [u8]> {
3517 let candidate = path.get(..literal.len())?;
3518 literal
3519 .iter()
3520 .zip(candidate)
3521 .all(|(&expected, &actual)| bytes_equal(expected, actual, case_insensitive))
3522 .then_some(&path[literal.len()..])
3523}
3524
3525fn strip_literal_suffix<'a>(
3526 path: &'a [u8],
3527 literal: &[u8],
3528 case_insensitive: bool,
3529) -> Option<&'a [u8]> {
3530 let start = path.len().checked_sub(literal.len())?;
3531 literal
3532 .iter()
3533 .zip(&path[start..])
3534 .all(|(&expected, &actual)| bytes_equal(expected, actual, case_insensitive))
3535 .then_some(&path[..start])
3536}
3537
3538fn contains_hidden_component_in(
3539 path: &[u8],
3540 start: usize,
3541 end: usize,
3542 candidate_starts_component: bool,
3543) -> bool {
3544 let Some(segment) = path.get(start..end) else {
3545 return false;
3546 };
3547 let mut offset = start;
3548 while let Some(found) = memchr(b'.', &segment[offset - start..]) {
3549 let index = offset + found;
3550 if (index == 0 && candidate_starts_component)
3551 || (index > 0 && is_separator(path[index - 1]))
3552 {
3553 return true;
3554 }
3555 offset = index + 1;
3556 }
3557 false
3558}
3559
3560fn star_stops_before_hidden_component(path: &[u8], index: usize, options: PatternOptions) -> bool {
3563 !options.match_hidden
3564 && path.get(index) == Some(&b'.')
3565 && at_component_start(path, index, options)
3566}
3567
3568fn without_leading_dot_slash(path: &[u8]) -> &[u8] {
3570 path.strip_prefix(b"./").unwrap_or(path)
3571}
3572
3573fn path_after_base<'a>(base_path: &[u8], path: &'a [u8]) -> Option<&'a [u8]> {
3574 if base_path.is_empty() {
3575 return Some(path);
3576 }
3577 let base_path = base_path
3578 .iter()
3579 .rposition(|byte| !is_separator(*byte))
3580 .map_or(&base_path[..1], |last| &base_path[..=last]);
3581 if base_path.len() == 1 && is_separator(base_path[0]) {
3582 return path.strip_prefix(base_path);
3583 }
3584 let suffix = path.strip_prefix(base_path)?;
3585 suffix
3586 .strip_prefix(b"/")
3587 .or_else(|| cfg!(windows).then(|| suffix.strip_prefix(b"\\")).flatten())
3588}
3589
3590#[derive(Debug, Clone, PartialEq, Eq)]
3591struct Class {
3592 negated: bool,
3593 members: Vec<ClassMember>,
3594}
3595
3596impl Class {
3597 fn matches(&self, byte: u8, case_insensitive: bool) -> bool {
3598 let included = self.members.iter().any(|member| match member {
3599 ClassMember::Byte(expected) => bytes_equal(*expected, byte, case_insensitive),
3600 ClassMember::Range(start, end) => {
3601 let byte = fold_ascii(byte, case_insensitive);
3602 let start = fold_ascii(*start, case_insensitive);
3603 let end = fold_ascii(*end, case_insensitive);
3604 start <= byte && byte <= end
3605 }
3606 ClassMember::Posix(class) => class.matches(byte, case_insensitive),
3607 });
3608 included != self.negated
3609 }
3610}
3611
3612#[derive(Debug, Clone, PartialEq, Eq)]
3613enum ClassMember {
3614 Byte(u8),
3615 Range(u8, u8),
3616 Posix(PosixClass),
3617}
3618
3619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3624struct ClassValue {
3625 byte: u8,
3626 escaped: bool,
3627}
3628
3629impl ClassValue {
3630 const fn literal(byte: u8) -> Self {
3631 Self {
3632 byte,
3633 escaped: false,
3634 }
3635 }
3636
3637 const fn escaped(byte: u8) -> Self {
3638 Self {
3639 byte,
3640 escaped: true,
3641 }
3642 }
3643
3644 const fn is_range_separator(self) -> bool {
3645 self.byte == b'-' && !self.escaped
3646 }
3647}
3648
3649#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3650enum PosixClass {
3651 Alnum,
3652 Alpha,
3653 Ascii,
3654 Blank,
3655 Cntrl,
3656 Digit,
3657 Graph,
3658 Lower,
3659 Print,
3660 Punct,
3661 Space,
3662 Upper,
3663 Word,
3664 Xdigit,
3665}
3666
3667const MAX_POSIX_CLASS_NAME_LEN: usize = 6;
3668
3669impl PosixClass {
3670 fn matches(self, byte: u8, case_insensitive: bool) -> bool {
3671 match self {
3672 Self::Alnum => byte.is_ascii_alphanumeric(),
3673 Self::Alpha => byte.is_ascii_alphabetic(),
3674 Self::Ascii => byte.is_ascii(),
3675 Self::Blank => matches!(byte, b' ' | b'\t'),
3676 Self::Cntrl => byte.is_ascii_control(),
3677 Self::Digit => byte.is_ascii_digit(),
3678 Self::Graph => byte.is_ascii_graphic(),
3679 Self::Lower | Self::Upper if case_insensitive => byte.is_ascii_alphabetic(),
3680 Self::Lower => byte.is_ascii_lowercase(),
3681 Self::Print => byte.is_ascii_graphic() || byte == b' ',
3682 Self::Punct => byte.is_ascii_punctuation(),
3683 Self::Space => byte.is_ascii_whitespace(),
3684 Self::Upper => byte.is_ascii_uppercase(),
3685 Self::Word => byte.is_ascii_alphanumeric() || byte == b'_',
3686 Self::Xdigit => byte.is_ascii_hexdigit(),
3687 }
3688 }
3689}
3690
3691fn parse_class(
3692 pattern: &[u8],
3693 start: usize,
3694 escapes: bool,
3695) -> Result<(Class, usize), PatternError> {
3696 let mut index = start + 1;
3697 let mut negated = false;
3698 if pattern
3699 .get(index)
3700 .is_some_and(|byte| matches!(byte, b'!' | b'^'))
3701 {
3702 negated = true;
3703 index += 1;
3704 }
3705
3706 let mut values: Vec<ClassValue> = Vec::new();
3710 let mut members = Vec::new();
3711 if pattern.get(index) == Some(&b']') {
3712 values.push(ClassValue::literal(b']'));
3713 index += 1;
3714 }
3715 while let Some(&byte) = pattern.get(index) {
3716 if byte == b']' {
3717 if values.is_empty() && members.is_empty() {
3718 return Err(PatternError {
3719 offset: start,
3720 message: "empty character class",
3721 });
3722 }
3723 return Ok((
3724 Class {
3725 negated,
3726 members: {
3727 members.extend(class_members(values));
3728 members
3729 },
3730 },
3731 index + 1,
3732 ));
3733 }
3734 if byte == b'[' && pattern.get(index + 1) == Some(&b':') {
3735 let name_start = index + 2;
3736 let search_end = pattern.len().min(name_start + MAX_POSIX_CLASS_NAME_LEN + 2);
3740 if let Some(posix_end) = memmem::find(&pattern[name_start..search_end], b":]")
3741 && let Some(class) = parse_posix_class(&pattern[name_start..name_start + posix_end])
3742 {
3743 let name_end = name_start + posix_end;
3744 members.extend(class_members(std::mem::take(&mut values)));
3745 members.push(ClassMember::Posix(class));
3746 index = name_end + 2;
3747 continue;
3748 }
3749 }
3750 if byte == b'\\' && escapes {
3751 let Some(&escaped) = pattern.get(index + 1) else {
3752 return Err(PatternError {
3753 offset: index,
3754 message: "trailing escape in character class",
3755 });
3756 };
3757 values.push(ClassValue::escaped(escaped));
3758 index += 2;
3759 } else {
3760 values.push(ClassValue::literal(byte));
3761 index += 1;
3762 }
3763 }
3764
3765 Err(PatternError {
3766 offset: start,
3767 message: "unclosed character class",
3768 })
3769}
3770
3771#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
3773enum WalkerComponentKind {
3774 #[default]
3775 Empty,
3776 Dot,
3777 Parent,
3778 Other,
3779}
3780
3781#[derive(Debug, Clone, Copy, Default)]
3789struct DiagnosticOffset(Option<usize>);
3790
3791impl PartialEq for DiagnosticOffset {
3792 fn eq(&self, _: &Self) -> bool {
3793 true
3794 }
3795}
3796
3797impl Eq for DiagnosticOffset {}
3798
3799impl Hash for DiagnosticOffset {
3800 fn hash<H: Hasher>(&self, _: &mut H) {}
3801}
3802
3803#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
3804struct WalkerComponent {
3805 kind: WalkerComponentKind,
3806 offset: Option<usize>,
3809 separator: DiagnosticOffset,
3814}
3815
3816impl WalkerComponent {
3817 const fn opened_at(offset: Option<usize>) -> Self {
3819 Self {
3820 kind: WalkerComponentKind::Empty,
3821 offset: None,
3822 separator: DiagnosticOffset(offset),
3823 }
3824 }
3825
3826 fn close_at(&mut self, offset: Option<usize>) {
3829 if self.kind == WalkerComponentKind::Empty && offset.is_some() {
3830 self.separator = DiagnosticOffset(offset);
3831 }
3832 }
3833
3834 const fn empty_location(self) -> Option<usize> {
3836 match self.kind {
3837 WalkerComponentKind::Empty => self.separator.0,
3838 _ => None,
3839 }
3840 }
3841
3842 fn push_literal(&mut self, byte: u8, offset: Option<usize>) {
3843 match (self.kind, byte) {
3844 (WalkerComponentKind::Empty, b'.') => {
3845 self.kind = WalkerComponentKind::Dot;
3846 self.offset = offset;
3847 }
3848 (WalkerComponentKind::Dot, b'.') => self.kind = WalkerComponentKind::Parent,
3849 (
3850 WalkerComponentKind::Empty | WalkerComponentKind::Dot | WalkerComponentKind::Parent,
3851 _,
3852 ) => {
3853 self.kind = WalkerComponentKind::Other;
3854 self.offset = None;
3855 }
3856 (WalkerComponentKind::Other, _) => {}
3857 }
3858 }
3859
3860 fn wildcard(&mut self) {
3861 self.kind = WalkerComponentKind::Other;
3862 self.offset = None;
3863 }
3864
3865 fn append(&mut self, other: Self) {
3866 match other.kind {
3867 WalkerComponentKind::Empty => {
3868 if self.kind == WalkerComponentKind::Empty && other.separator.0.is_some() {
3869 self.separator = other.separator;
3870 }
3871 }
3872 WalkerComponentKind::Dot => self.push_literal(b'.', other.offset),
3873 WalkerComponentKind::Parent => {
3874 self.push_literal(b'.', other.offset);
3875 self.push_literal(b'.', None);
3876 }
3877 WalkerComponentKind::Other => self.wildcard(),
3878 }
3879 }
3880}
3881
3882#[derive(Default)]
3888struct WalkerPathShapeBuilder {
3889 leading_dot_is_normalized: bool,
3890 components: Vec<WalkerComponent>,
3891 current: WalkerComponent,
3892}
3893
3894impl WalkerPathShapeBuilder {
3895 fn new(leading_dot_is_normalized: bool) -> Self {
3896 Self {
3897 leading_dot_is_normalized,
3898 components: Vec::new(),
3899 current: WalkerComponent::default(),
3900 }
3901 }
3902 fn literal(&mut self, byte: u8, offset: Option<usize>) {
3903 self.current.push_literal(byte, offset);
3904 }
3905
3906 fn escaped(&mut self) {
3907 self.current.wildcard();
3908 }
3909
3910 fn wildcard(&mut self) {
3911 self.current.wildcard();
3912 }
3913
3914 fn separator(&mut self, offset: Option<usize>) {
3915 let mut component =
3916 std::mem::replace(&mut self.current, WalkerComponent::opened_at(offset));
3917 component.close_at(offset);
3918 self.components.push(component);
3919 }
3920
3921 fn finish(mut self) -> WalkerPathShape {
3922 self.components.push(self.current);
3923 WalkerPathShape {
3924 leading_dot_is_normalized: self.leading_dot_is_normalized,
3925 components: self.components,
3926 }
3927 }
3928}
3929
3930#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3931struct WalkerPathState {
3932 leading_dot_is_normalized: bool,
3933 completed_components: u8,
3936 all_empty_or_dot: bool,
3937 first_parent: Option<WalkerComponent>,
3938 first_dot: Option<WalkerComponent>,
3939 last_nonempty: Option<WalkerComponent>,
3940 first_empty: Option<WalkerComponent>,
3944 current: WalkerComponent,
3945}
3946
3947impl WalkerPathState {
3948 fn new(leading_dot_is_normalized: bool) -> Self {
3949 Self {
3950 leading_dot_is_normalized,
3951 completed_components: 0,
3952 all_empty_or_dot: true,
3953 first_parent: None,
3954 first_dot: None,
3955 last_nonempty: None,
3956 first_empty: None,
3957 current: WalkerComponent::default(),
3958 }
3959 }
3960 fn literal(&mut self, byte: u8, offset: Option<usize>) {
3961 self.current.push_literal(byte, offset);
3962 }
3963
3964 fn escaped(&mut self) {
3965 self.current.wildcard();
3969 }
3970
3971 fn wildcard(&mut self) {
3972 self.current.wildcard();
3973 }
3974
3975 fn separator(&mut self, offset: Option<usize>) {
3976 let mut component =
3977 std::mem::replace(&mut self.current, WalkerComponent::opened_at(offset));
3978 component.close_at(offset);
3979 if component.kind == WalkerComponentKind::Empty && self.first_empty.is_none() {
3980 self.first_empty = Some(component);
3981 }
3982 self.record_component(component);
3983 }
3984
3985 fn append_shape(&mut self, shape: &WalkerPathShape) {
3986 for (index, component) in shape.components.iter().copied().enumerate() {
3987 self.current.append(component);
3988 if index + 1 != shape.components.len() {
3989 self.separator(None);
3992 }
3993 }
3994 }
3995
3996 fn record_component(&mut self, component: WalkerComponent) {
3997 let first = self.completed_components == 0;
3998 if !matches!(
3999 component.kind,
4000 WalkerComponentKind::Empty | WalkerComponentKind::Dot
4001 ) {
4002 self.all_empty_or_dot = false;
4003 }
4004 if component.kind == WalkerComponentKind::Parent && self.first_parent.is_none() {
4005 self.first_parent = Some(component);
4006 }
4007 if component.kind == WalkerComponentKind::Dot
4008 && (!first || !self.leading_dot_is_normalized)
4009 && self.first_dot.is_none()
4010 {
4011 self.first_dot = Some(component);
4012 }
4013 if component.kind != WalkerComponentKind::Empty {
4014 self.last_nonempty = Some(component);
4015 }
4016 self.completed_components = self.completed_components.saturating_add(1).min(2);
4017 }
4018
4019 fn finish(mut self) -> WalkerPathEvaluation {
4020 let component = std::mem::take(&mut self.current);
4021 let empty_component = self
4022 .first_empty
4023 .or((component.kind == WalkerComponentKind::Empty).then_some(component));
4024 self.record_component(component);
4025 WalkerPathEvaluation {
4026 selects_candidate: empty_component.is_none(),
4027 empty_component_offset: empty_component.and_then(WalkerComponent::empty_location),
4028 problem: WalkerPathSummary {
4029 all_empty_or_dot: self.all_empty_or_dot,
4030 first_parent: self.first_parent,
4031 first_dot: self.first_dot,
4032 last_nonempty: self.last_nonempty,
4033 }
4034 .problem(),
4035 }
4036 }
4037}
4038
4039#[derive(Clone, Copy)]
4040struct WalkerPathEvaluation {
4041 selects_candidate: bool,
4045 empty_component_offset: Option<usize>,
4047 problem: Option<WalkerPathProblem>,
4048}
4049
4050impl WalkerPathEvaluation {
4051 fn complete_problem(self) -> Option<WalkerPathProblem> {
4052 self.problem.or_else(|| {
4053 (!self.selects_candidate).then_some(WalkerPathProblem {
4054 viability: WalkerPathViability::EmptyComponent,
4055 offset: self.empty_component_offset,
4056 })
4057 })
4058 }
4059}
4060
4061#[derive(Debug, Clone, Copy)]
4062struct WalkerPathSummary {
4063 all_empty_or_dot: bool,
4064 first_parent: Option<WalkerComponent>,
4065 first_dot: Option<WalkerComponent>,
4066 last_nonempty: Option<WalkerComponent>,
4067}
4068
4069impl WalkerPathSummary {
4070 fn problem(self) -> Option<WalkerPathProblem> {
4071 if self.all_empty_or_dot {
4072 return Some(WalkerPathProblem {
4073 viability: WalkerPathViability::Root,
4074 offset: self.last_nonempty.and_then(|component| component.offset),
4075 });
4076 }
4077 if let Some(component) = self.first_parent {
4078 return Some(WalkerPathProblem {
4079 viability: WalkerPathViability::ParentComponent,
4080 offset: component.offset,
4081 });
4082 }
4083 if self
4084 .last_nonempty
4085 .is_some_and(|component| component.kind == WalkerComponentKind::Dot)
4086 {
4087 return Some(WalkerPathProblem {
4088 viability: WalkerPathViability::TrailingDot,
4089 offset: self.last_nonempty.and_then(|component| component.offset),
4090 });
4091 }
4092 self.first_dot.map(|component| WalkerPathProblem {
4093 viability: WalkerPathViability::DotComponent,
4094 offset: component.offset,
4095 })
4096 }
4097}
4098
4099#[derive(Debug, Clone, PartialEq, Eq, Default)]
4100struct WalkerPathShape {
4101 leading_dot_is_normalized: bool,
4102 components: Vec<WalkerComponent>,
4103}
4104
4105impl WalkerPathShape {
4106 fn has_separator(&self) -> bool {
4107 self.components.len() > 1
4108 }
4109
4110 fn is_empty(&self) -> bool {
4111 matches!(
4112 self.components.as_slice(),
4113 [WalkerComponent {
4114 kind: WalkerComponentKind::Empty,
4115 ..
4116 }]
4117 )
4118 }
4119
4120 fn problem(&self) -> Option<WalkerPathProblem> {
4121 let mut state = WalkerPathState::new(self.leading_dot_is_normalized);
4122 state.append_shape(self);
4123 state.finish().complete_problem()
4124 }
4125}
4126
4127#[derive(Debug, Clone, Copy)]
4128struct WalkerPathProblem {
4129 viability: WalkerPathViability,
4130 offset: Option<usize>,
4131}
4132
4133fn walker_path_analysis(
4139 alternatives: &[CompiledAlternative],
4140 budget: &mut IrBudget,
4141) -> Result<(WalkerPathViability, Option<usize>), PatternError> {
4142 let mut first_problem = None;
4143 let mut saw_viable = false;
4144 for alternative in alternatives {
4145 match alternative.walker_path_problem(budget)? {
4146 None => saw_viable = true,
4147 Some(problem) if problem.viability == WalkerPathViability::ParentComponent => {
4148 return Ok((problem.viability, problem.offset));
4149 }
4150 Some(problem) => {
4151 first_problem.get_or_insert(problem);
4152 }
4153 };
4154 }
4155 if saw_viable {
4156 return Ok((WalkerPathViability::Viable, None));
4157 }
4158 match first_problem {
4159 Some(problem) => Ok((problem.viability, problem.offset)),
4160 None => Ok((WalkerPathViability::Viable, None)),
4161 }
4162}
4163
4164impl CompiledAlternative {
4165 fn walker_path_problem(
4166 &self,
4167 budget: &mut IrBudget,
4168 ) -> Result<Option<WalkerPathProblem>, PatternError> {
4169 match &self.extglob {
4170 Some(program) => program.walker_path_problem(budget),
4171 None => Ok(self.walker_path_shape.problem()),
4172 }
4173 }
4174}
4175
4176impl CompiledExtglob {
4177 fn walker_path_problem(
4178 &self,
4179 budget: &mut IrBudget,
4180 ) -> Result<Option<WalkerPathProblem>, PatternError> {
4181 let mut states = vec![WalkerPathState::new(self.leading_dot_is_normalized)];
4182 let mut index = 0;
4183 while let Some(step) = self.steps.get(index) {
4184 match step {
4185 ExtglobStep::Byte(b'/') => {
4186 let offset = self
4187 .walker_source_provenance
4188 .as_ref()
4189 .and_then(|provenance| provenance.offset_at(index));
4190 for state in &mut states {
4191 state.separator(offset);
4192 }
4193 index += 1;
4194 }
4195 ExtglobStep::Byte(byte) => {
4196 for state in &mut states {
4197 state.literal(
4198 *byte,
4199 self.walker_source_provenance
4200 .as_ref()
4201 .and_then(|provenance| provenance.offset_at(index)),
4202 );
4203 }
4204 index += 1;
4205 }
4206 ExtglobStep::Escape { .. } => {
4207 for state in &mut states {
4208 state.escaped();
4209 }
4210 index += 2;
4211 }
4212 ExtglobStep::Group(group) => {
4213 states = self.groups[*group].apply_to(states, budget)?;
4214 index = self.groups[*group].rest;
4215 }
4216 ExtglobStep::Star { next, .. } | ExtglobStep::Class { next, .. } => {
4217 for state in &mut states {
4218 state.wildcard();
4219 }
4220 index = *next;
4221 }
4222 ExtglobStep::Any | ExtglobStep::UnclosedGroup { .. } => {
4223 for state in &mut states {
4224 state.wildcard();
4225 }
4226 index += 1;
4227 }
4228 ExtglobStep::NoMatch => {
4233 for state in &mut states {
4234 state.wildcard();
4235 }
4236 index += 1;
4237 }
4238 }
4239 }
4240 let mut first_problem = None;
4241 let mut first_unselectable = None;
4242 for state in states {
4243 let evaluation = state.finish();
4244 if !evaluation.selects_candidate {
4245 first_unselectable.get_or_insert(evaluation.empty_component_offset);
4246 continue;
4247 }
4248 let Some(problem) = evaluation.problem else {
4249 return Ok(None);
4250 };
4251 first_problem.get_or_insert(problem);
4252 }
4253 Ok(first_problem.or_else(|| {
4254 first_unselectable.map(|offset| WalkerPathProblem {
4255 viability: WalkerPathViability::EmptyComponent,
4256 offset,
4257 })
4258 }))
4259 }
4260}
4261
4262impl ExtglobGroup {
4263 fn apply_to(
4264 &self,
4265 states: Vec<WalkerPathState>,
4266 budget: &mut IrBudget,
4267 ) -> Result<Vec<WalkerPathState>, PatternError> {
4268 match self.kind {
4269 ExtglobKind::Negated => {
4270 let mut states = states;
4271 for state in &mut states {
4272 state.wildcard();
4276 }
4277 Ok(states)
4278 }
4279 ExtglobKind::Optional => {
4280 let mut output = states.clone();
4281 output.extend(self.exact_states(&states, budget)?);
4282 Ok(deduplicate_walker_states(output))
4283 }
4284 ExtglobKind::ZeroOrMore => self.fixed_point(states, false, budget),
4285 ExtglobKind::OneOrMore => self.fixed_point(states, true, budget),
4286 ExtglobKind::ExactlyOne => self.exact_states(&states, budget),
4287 }
4288 }
4289
4290 fn fixed_point(
4291 &self,
4292 states: Vec<WalkerPathState>,
4293 require_one: bool,
4294 budget: &mut IrBudget,
4295 ) -> Result<Vec<WalkerPathState>, PatternError> {
4296 let mut seen = HashSet::new();
4297 let mut all = Vec::new();
4298 let mut frontier = if require_one {
4299 self.exact_states(&states, budget)?
4300 } else {
4301 states
4302 };
4303 frontier.retain(|state| seen.insert(state.clone()));
4304 all.extend(frontier.iter().cloned());
4305 while !frontier.is_empty() {
4306 let next = self.exact_states(&frontier, budget)?;
4307 frontier = next
4308 .into_iter()
4309 .filter(|state| seen.insert(state.clone()))
4310 .collect();
4311 all.extend(frontier.iter().cloned());
4312 }
4313 Ok(all)
4314 }
4315
4316 fn exact_states(
4317 &self,
4318 states: &[WalkerPathState],
4319 budget: &mut IrBudget,
4320 ) -> Result<Vec<WalkerPathState>, PatternError> {
4321 let mut seen = HashSet::new();
4322 let mut output = Vec::new();
4323 for alternative in &self.alternatives {
4324 for arm in &alternative.compiled {
4325 for state in states {
4326 budget.charge(1, self.start)?;
4331 let mut state = state.clone();
4332 if arm.walker_path_shape.has_separator() {
4333 state.append_shape(&arm.walker_path_shape);
4334 } else if arm.walker_path_shape.is_empty() {
4335 } else {
4339 state.wildcard();
4342 }
4343 if seen.insert(state.clone()) {
4344 output.push(state);
4345 }
4346 }
4347 }
4348 }
4349 Ok(output)
4350 }
4351}
4352
4353fn deduplicate_walker_states(states: Vec<WalkerPathState>) -> Vec<WalkerPathState> {
4354 let mut seen = HashSet::new();
4355 states
4356 .into_iter()
4357 .filter(|state| seen.insert(state.clone()))
4358 .collect()
4359}
4360
4361fn parse_posix_class(name: &[u8]) -> Option<PosixClass> {
4362 match name {
4363 b"alnum" => Some(PosixClass::Alnum),
4364 b"alpha" => Some(PosixClass::Alpha),
4365 b"ascii" => Some(PosixClass::Ascii),
4366 b"blank" => Some(PosixClass::Blank),
4367 b"cntrl" => Some(PosixClass::Cntrl),
4368 b"digit" => Some(PosixClass::Digit),
4369 b"graph" => Some(PosixClass::Graph),
4370 b"lower" => Some(PosixClass::Lower),
4371 b"print" => Some(PosixClass::Print),
4372 b"punct" => Some(PosixClass::Punct),
4373 b"space" => Some(PosixClass::Space),
4374 b"upper" => Some(PosixClass::Upper),
4375 b"word" => Some(PosixClass::Word),
4376 b"xdigit" => Some(PosixClass::Xdigit),
4377 _ => None,
4378 }
4379}
4380
4381fn class_members(values: Vec<ClassValue>) -> Vec<ClassMember> {
4382 let mut members = Vec::new();
4383 let mut index = 0;
4384 while index < values.len() {
4385 if index + 2 < values.len() && values[index + 1].is_range_separator() {
4389 members.push(ClassMember::Range(
4390 values[index].byte,
4391 values[index + 2].byte,
4392 ));
4393 index += 3;
4394 } else {
4395 members.push(ClassMember::Byte(values[index].byte));
4396 index += 1;
4397 }
4398 }
4399 members
4400}
4401
4402fn flush_literals(tokens: &mut Vec<Token>, literals: &mut Vec<u8>) {
4405 if !literals.is_empty() {
4406 tokens.push(Token::Literal(literals.clone()));
4407 literals.clear();
4408 }
4409}
4410
4411const MAX_BRACE_ALTERNATIVES: usize = 1 << 12;
4424
4425const MAX_BRACE_EXPANSION_BYTES: usize = 1 << 26;
4441
4442const MAX_COMPILED_IR_UNITS: usize = 1 << 20;
4464
4465const TOO_MUCH_COMPILED_IR: &str = "pattern compiles to too much";
4471
4472struct IrBudget {
4473 remaining: usize,
4474}
4475
4476impl IrBudget {
4477 const fn new() -> Self {
4478 Self {
4479 remaining: MAX_COMPILED_IR_UNITS,
4480 }
4481 }
4482
4483 fn charge(&mut self, units: usize, offset: usize) -> Result<(), PatternError> {
4489 match self.remaining.checked_sub(units) {
4490 Some(remaining) => {
4491 self.remaining = remaining;
4492 Ok(())
4493 }
4494 None => Err(PatternError {
4495 offset,
4496 message: TOO_MUCH_COMPILED_IR,
4497 }),
4498 }
4499 }
4500
4501 fn ensure(&self, units: usize, offset: usize) -> Result<(), PatternError> {
4506 if units <= self.remaining {
4507 Ok(())
4508 } else {
4509 Err(PatternError {
4510 offset,
4511 message: TOO_MUCH_COMPILED_IR,
4512 })
4513 }
4514 }
4515}
4516
4517struct ProvenanceBudget {
4523 remaining: usize,
4524}
4525
4526impl ProvenanceBudget {
4527 const fn new() -> Self {
4528 Self {
4529 remaining: MAX_BRACE_EXPANSION_BYTES / std::mem::size_of::<SourceSpan>(),
4530 }
4531 }
4532
4533 fn charge(&mut self, spans: usize, offset: usize) -> Result<(), PatternError> {
4534 match self.remaining.checked_sub(spans) {
4535 Some(remaining) => {
4536 self.remaining = remaining;
4537 Ok(())
4538 }
4539 None => Err(PatternError {
4540 offset,
4541 message: "brace provenance is too large",
4542 }),
4543 }
4544 }
4545}
4546
4547#[must_use]
4590pub fn path_bytes(path: &(impl AsRef<OsStr> + ?Sized)) -> &[u8] {
4591 path.as_ref().as_encoded_bytes()
4592}
4593
4594const fn is_syntax_byte(byte: u8) -> bool {
4598 matches!(
4599 byte,
4600 b'\\'
4601 | b'*'
4602 | b'?'
4603 | b'['
4604 | b']'
4605 | b'{'
4606 | b'}'
4607 | b'('
4608 | b')'
4609 | b','
4610 | b'|'
4611 | b'!'
4612 | b'@'
4613 | b'+'
4614 )
4615}
4616
4617#[must_use]
4657pub fn escape(text: impl AsRef<[u8]>) -> Vec<u8> {
4658 let text = text.as_ref();
4659 let mut escaped = Vec::with_capacity(text.len());
4660 for &byte in text {
4661 if is_syntax_byte(byte) {
4662 escaped.push(b'\\');
4663 }
4664 escaped.push(byte);
4665 }
4666 escaped
4667}
4668
4669#[must_use]
4687pub fn escape_str(text: &str) -> String {
4688 let mut escaped = String::with_capacity(text.len());
4689 for character in text.chars() {
4690 if u8::try_from(character).is_ok_and(is_syntax_byte) {
4691 escaped.push('\\');
4692 }
4693 escaped.push(character);
4694 }
4695 escaped
4696}
4697
4698pub fn expand_braces(
4727 pattern: impl AsRef<[u8]>,
4728 options: PatternOptions,
4729) -> Result<Vec<Vec<u8>>, PatternError> {
4730 let pattern = pattern.as_ref();
4731 if !options.braces {
4732 return Ok(vec![pattern.to_vec()]);
4733 }
4734 let mut provenance_budget = ProvenanceBudget::new();
4735 Ok(expand_brace_alternatives_with_provenance(
4736 pattern,
4737 None,
4738 options.escape,
4739 &mut provenance_budget,
4740 )?
4741 .into_iter()
4742 .map(|alternative| alternative.bytes)
4743 .collect())
4744}
4745
4746type AlternativeWithFirstOffset = (Vec<u8>, Option<usize>);
4748
4749#[doc(hidden)]
4756pub fn expand_braces_with_first_offsets(
4757 pattern: impl AsRef<[u8]>,
4758 options: PatternOptions,
4759) -> Result<Vec<AlternativeWithFirstOffset>, PatternError> {
4760 let pattern = pattern.as_ref();
4761 let first = (!pattern.is_empty()).then_some(0);
4762 if !options.braces {
4763 return Ok(vec![(pattern.to_vec(), first)]);
4764 }
4765 let source = SourceProvenance::Contiguous { source_start: 0 };
4766 let mut provenance_budget = ProvenanceBudget::new();
4767 Ok(expand_brace_alternatives_with_provenance(
4768 pattern,
4769 Some(&source),
4770 options.escape,
4771 &mut provenance_budget,
4772 )?
4773 .into_iter()
4774 .map(|alternative| {
4775 let first = if alternative.bytes.is_empty() {
4776 None
4777 } else {
4778 alternative
4779 .source_provenance
4780 .as_ref()
4781 .and_then(|provenance| provenance.offset_at(0))
4782 };
4783 (alternative.bytes, first)
4784 })
4785 .collect())
4786}
4787
4788#[derive(Clone, Debug, PartialEq, Eq)]
4794struct SourceSpan {
4795 output_start: usize,
4796 source_start: usize,
4797 len: usize,
4798}
4799
4800#[derive(Clone, Debug, PartialEq, Eq)]
4802enum SourceProvenance {
4803 Contiguous { source_start: usize },
4804 Spans(Vec<SourceSpan>),
4805}
4806
4807impl SourceProvenance {
4808 fn offset_at(&self, index: usize) -> Option<usize> {
4809 match self {
4810 Self::Contiguous { source_start } => Some(source_start + index),
4811 Self::Spans(spans) => spans.iter().find_map(|span| {
4812 (index >= span.output_start && index - span.output_start < span.len)
4813 .then_some(span.source_start + index - span.output_start)
4814 }),
4815 }
4816 }
4817
4818 fn span_count_in(&self, start: usize, end: usize) -> usize {
4819 match self {
4820 Self::Contiguous { .. } => usize::from(start < end),
4821 Self::Spans(spans) => spans
4822 .iter()
4823 .filter(|span| span.output_start < end && span.output_start + span.len > start)
4824 .count(),
4825 }
4826 }
4827
4828 fn append_range(
4829 &self,
4830 start: usize,
4831 end: usize,
4832 output_start: usize,
4833 target: &mut Vec<SourceSpan>,
4834 ) {
4835 if start == end {
4836 return;
4837 }
4838 match self {
4839 Self::Contiguous { source_start } => target.push(SourceSpan {
4840 output_start,
4841 source_start: source_start + start,
4842 len: end - start,
4843 }),
4844 Self::Spans(spans) => {
4845 for span in spans {
4846 let span_end = span.output_start + span.len;
4847 let overlap_start = span.output_start.max(start);
4848 let overlap_end = span_end.min(end);
4849 if overlap_start < overlap_end {
4850 target.push(SourceSpan {
4851 output_start: output_start + overlap_start - start,
4852 source_start: span.source_start + overlap_start - span.output_start,
4853 len: overlap_end - overlap_start,
4854 });
4855 }
4856 }
4857 }
4858 }
4859 }
4860
4861 fn slice(
4862 &self,
4863 start: usize,
4864 end: usize,
4865 provenance_budget: &mut ProvenanceBudget,
4866 error_offset: usize,
4867 ) -> Result<Self, PatternError> {
4868 if let Self::Contiguous { source_start } = self {
4869 return Ok(Self::Contiguous {
4870 source_start: source_start + start,
4871 });
4872 }
4873 let span_count = self.span_count_in(start, end);
4874 provenance_budget.charge(span_count, error_offset)?;
4875 let mut spans = Vec::with_capacity(span_count);
4876 self.append_range(start, end, 0, &mut spans);
4877 Ok(Self::Spans(spans))
4878 }
4879}
4880
4881struct BraceExpansion {
4883 bytes: Vec<u8>,
4884 source_provenance: Option<SourceProvenance>,
4885}
4886
4887#[derive(Clone, Copy)]
4888struct BraceExpansionSummary {
4889 source_length: usize,
4890 alternatives: usize,
4891 minimum_length: usize,
4892 final_length_sum: usize,
4893 nodes: usize,
4894 written: usize,
4895}
4896
4897impl BraceExpansionSummary {
4898 const fn literal(length: usize) -> Self {
4899 Self {
4900 source_length: length,
4901 alternatives: 1,
4902 minimum_length: length,
4903 final_length_sum: length,
4904 nodes: 1,
4905 written: length,
4906 }
4907 }
4908
4909 fn concat(self, suffix: Self) -> Self {
4912 let suffix_descendants = suffix.nodes.saturating_sub(1);
4913 Self {
4914 source_length: self.source_length.saturating_add(suffix.source_length),
4915 alternatives: self.alternatives.saturating_mul(suffix.alternatives),
4916 minimum_length: self.minimum_length.saturating_add(suffix.minimum_length),
4917 final_length_sum: suffix
4918 .alternatives
4919 .saturating_mul(self.final_length_sum)
4920 .saturating_add(self.alternatives.saturating_mul(suffix.final_length_sum)),
4921 nodes: self
4922 .nodes
4923 .saturating_add(self.alternatives.saturating_mul(suffix_descendants)),
4924 written: self
4925 .written
4926 .saturating_add(self.nodes.saturating_mul(suffix.source_length))
4927 .saturating_add(suffix_descendants.saturating_mul(self.final_length_sum))
4928 .saturating_add(
4929 self.alternatives
4930 .saturating_mul(suffix.written.saturating_sub(suffix.source_length)),
4931 ),
4932 }
4933 }
4934}
4935
4936fn ensure_source_brace_compiled_ir_lower_bound(
4946 pattern: &[u8],
4947 options: PatternOptions,
4948 budget: &IrBudget,
4949 provenance_budget: &ProvenanceBudget,
4950) -> Result<(), PatternError> {
4951 if !source_classes_are_stable_and_valid(pattern, options.escape) {
4955 return Ok(());
4956 }
4957 let Some(summary) = brace_expansion_summary(pattern, options.escape, 0) else {
4958 return Ok(());
4959 };
4960 if summary.alternatives > MAX_BRACE_ALTERNATIVES {
4961 return Ok(());
4962 }
4963 if summary.written > MAX_BRACE_EXPANSION_BYTES {
4966 return Ok(());
4967 }
4968 let brace_openers = unescaped_brace_openers(pattern, options.escape);
4972 let spans_per_node = 1_usize.saturating_add(brace_openers.saturating_mul(2));
4973 let provenance_upper = summary
4974 .nodes
4975 .saturating_sub(1)
4976 .saturating_mul(spans_per_node);
4977 if provenance_upper > provenance_budget.remaining {
4978 return Ok(());
4979 }
4980 let (question_marks, has_extglob) = unconditional_compiled_units(pattern, options);
4981 let per_alternative = question_marks.saturating_add(if has_extglob {
4982 summary.minimum_length
4983 } else {
4984 0
4985 });
4986 budget.ensure(summary.alternatives.saturating_mul(per_alternative), 0)
4987}
4988
4989fn unescaped_brace_openers(pattern: &[u8], escapes: bool) -> usize {
4990 let mut openers = 0_usize;
4991 let mut index = 0;
4992 while index < pattern.len() {
4993 if escapes && pattern[index] == b'\\' {
4994 index += usize::from(index + 1 < pattern.len()) + 1;
4995 } else {
4996 openers += usize::from(pattern[index] == b'{');
4997 index += 1;
4998 }
4999 }
5000 openers
5001}
5002
5003fn source_classes_are_stable_and_valid(pattern: &[u8], escapes: bool) -> bool {
5004 let mut brace_depth = 0_usize;
5005 let mut index = 0;
5006 while index < pattern.len() {
5007 if escapes && pattern[index] == b'\\' {
5008 index += usize::from(index + 1 < pattern.len()) + 1;
5009 continue;
5010 }
5011 match pattern[index] {
5012 b'{' => brace_depth += 1,
5013 b'}' => brace_depth = brace_depth.saturating_sub(1),
5014 b'[' => {
5015 let Ok((_, next)) = parse_class(pattern, index, escapes) else {
5016 return false;
5017 };
5018 let mut class_index = index + 1;
5019 while class_index < next {
5020 if escapes && pattern[class_index] == b'\\' {
5021 class_index += usize::from(class_index + 1 < next) + 1;
5022 continue;
5023 }
5024 if matches!(pattern[class_index], b'{' | b'}')
5025 || (brace_depth > 0 && pattern[class_index] == b',')
5026 {
5027 return false;
5028 }
5029 class_index += 1;
5030 }
5031 index = next;
5032 continue;
5033 }
5034 _ => {}
5035 }
5036 index += 1;
5037 }
5038 true
5039}
5040
5041fn brace_expansion_summary(
5042 pattern: &[u8],
5043 escapes: bool,
5044 depth: usize,
5045) -> Option<BraceExpansionSummary> {
5046 const MAX_SUMMARY_DEPTH: usize = 128;
5047 if depth > MAX_SUMMARY_DEPTH {
5048 return None;
5049 }
5050 let Some(open) = first_unescaped_brace(pattern, escapes) else {
5051 return Some(BraceExpansionSummary::literal(pattern.len()));
5052 };
5053 let Some(close) = matching_brace(pattern, open, escapes) else {
5054 return Some(BraceExpansionSummary::literal(pattern.len()));
5055 };
5056
5057 let content = &pattern[open + 1..close];
5058 let mut group_alternatives = 0_usize;
5059 let mut group_minimum = usize::MAX;
5060 let mut group_final_length_sum = 0_usize;
5061 let mut group_nodes = 1_usize;
5062 let mut group_written = close - open + 1;
5063 for range in split_brace_alternatives(content, escapes) {
5064 let arm = brace_expansion_summary(&content[range], escapes, depth + 1)?;
5065 group_alternatives = group_alternatives.saturating_add(arm.alternatives);
5066 group_minimum = group_minimum.min(arm.minimum_length);
5067 group_final_length_sum = group_final_length_sum.saturating_add(arm.final_length_sum);
5068 group_nodes = group_nodes.saturating_add(arm.nodes);
5069 group_written = group_written.saturating_add(arm.written);
5070 }
5071 let prefix = BraceExpansionSummary::literal(open);
5072 let group = BraceExpansionSummary {
5073 source_length: close - open + 1,
5074 alternatives: group_alternatives,
5075 minimum_length: group_minimum,
5076 final_length_sum: group_final_length_sum,
5077 nodes: group_nodes,
5078 written: group_written,
5079 };
5080 let suffix = brace_expansion_summary(&pattern[close + 1..], escapes, depth + 1)?;
5081 Some(prefix.concat(group).concat(suffix))
5082}
5083
5084fn unconditional_compiled_units(pattern: &[u8], options: PatternOptions) -> (usize, bool) {
5086 let mut question_marks = 0_usize;
5087 let mut has_extglob = false;
5088 let mut index = 0;
5089 while index < pattern.len() {
5090 if options.escape && pattern[index] == b'\\' {
5091 index += usize::from(index + 1 < pattern.len()) + 1;
5092 continue;
5093 }
5094 if pattern[index] == b'{'
5095 && let Some(close) = matching_brace(pattern, index, options.escape)
5096 {
5097 index = close + 1;
5098 continue;
5099 }
5100 question_marks += usize::from(pattern[index] == b'?');
5101 has_extglob |= detect_extglob_at(pattern, index).is_some();
5102 index += 1;
5103 }
5104 (question_marks, options.extglob && has_extglob)
5105}
5106
5107fn ensure_brace_compiled_ir_lower_bound(
5118 alternatives: &[BraceExpansion],
5119 options: PatternOptions,
5120 budget: &IrBudget,
5121) -> Result<(), PatternError> {
5122 let mut total = 0_usize;
5123 for alternative in alternatives {
5124 let Some(units) = compiled_ir_lower_bound(&alternative.bytes, options) else {
5125 return Ok(());
5126 };
5127 total = total.saturating_add(units);
5128 if total > budget.remaining {
5129 return budget.ensure(total, 0);
5130 }
5131 }
5132 budget.ensure(total, 0)
5133}
5134
5135fn compiled_ir_lower_bound(pattern: &[u8], options: PatternOptions) -> Option<usize> {
5136 let mut units = if options.extglob && contains_extglob(pattern, options.escape) {
5137 pattern.len()
5138 } else {
5139 0
5140 };
5141 let mut index = 0;
5142 while index < pattern.len() {
5143 match pattern[index] {
5144 b'\\' if options.escape => index += usize::from(index + 1 < pattern.len()) + 1,
5145 b'[' => {
5146 let (class, next) = parse_class(pattern, index, options.escape).ok()?;
5147 units = units.saturating_add(1 + class.members.len());
5148 index = next;
5149 }
5150 b'?' => {
5151 units = units.saturating_add(1);
5152 index += 1;
5153 }
5154 _ => index += 1,
5155 }
5156 }
5157 Some(units)
5158}
5159
5160fn expand_brace_alternatives_with_provenance(
5180 pattern: &[u8],
5181 source_provenance: Option<&SourceProvenance>,
5182 escapes: bool,
5183 provenance_budget: &mut ProvenanceBudget,
5184) -> Result<Vec<BraceExpansion>, PatternError> {
5185 let Some(first_open) = first_unescaped_brace(pattern, escapes) else {
5186 return Ok(vec![BraceExpansion {
5187 bytes: pattern.to_vec(),
5188 source_provenance: source_provenance.cloned(),
5189 }]);
5190 };
5191
5192 let mut expanded = Vec::new();
5193 let mut pending = vec![BraceExpansion {
5194 bytes: pattern.to_vec(),
5195 source_provenance: source_provenance.cloned(),
5196 }];
5197 let mut written = pattern.len();
5198 while let Some(current) = pending.pop() {
5199 let Some(open) = first_unescaped_brace(¤t.bytes, escapes) else {
5200 expanded.push(current);
5201 continue;
5202 };
5203 let Some(close) = matching_brace(¤t.bytes, open, escapes) else {
5204 expanded.push(current);
5206 continue;
5207 };
5208
5209 let alternatives = split_brace_alternatives(¤t.bytes[open + 1..close], escapes);
5210 for range in alternatives.iter().rev() {
5211 let alternative = ¤t.bytes[open + 1 + range.start..open + 1 + range.end];
5212 if expanded.len() + pending.len() >= MAX_BRACE_ALTERNATIVES {
5213 return Err(PatternError {
5214 offset: first_open,
5217 message: "too many brace alternatives",
5218 });
5219 }
5220 let length = open + alternative.len() + current.bytes.len() - close - 1;
5221 written = written.saturating_add(length);
5222 if written > MAX_BRACE_EXPANSION_BYTES {
5223 return Err(PatternError {
5224 offset: first_open,
5225 message: "brace expansion is too large",
5226 });
5227 }
5228 let mut bytes = Vec::with_capacity(length);
5229 bytes.extend_from_slice(¤t.bytes[..open]);
5230 bytes.extend_from_slice(alternative);
5231 bytes.extend_from_slice(¤t.bytes[close + 1..]);
5232 let source_provenance = current
5233 .source_provenance
5234 .as_ref()
5235 .map(|provenance| {
5236 let alternative_start = open + 1 + range.start;
5237 let span_count = provenance.span_count_in(0, open)
5238 + provenance.span_count_in(
5239 alternative_start,
5240 alternative_start + alternative.len(),
5241 )
5242 + provenance.span_count_in(close + 1, current.bytes.len());
5243 provenance_budget.charge(span_count, first_open)?;
5247 let mut spans = Vec::with_capacity(span_count);
5248 provenance.append_range(0, open, 0, &mut spans);
5249 provenance.append_range(
5250 alternative_start,
5251 alternative_start + alternative.len(),
5252 open,
5253 &mut spans,
5254 );
5255 provenance.append_range(
5256 close + 1,
5257 current.bytes.len(),
5258 open + alternative.len(),
5259 &mut spans,
5260 );
5261 Ok::<_, PatternError>(SourceProvenance::Spans(spans))
5262 })
5263 .transpose()?;
5264 pending.push(BraceExpansion {
5265 bytes,
5266 source_provenance,
5267 });
5268 }
5269 }
5270 Ok(expanded)
5271}
5272
5273#[cfg(test)]
5274fn expand_brace_alternatives(pattern: &[u8], escapes: bool) -> Result<Vec<Vec<u8>>, PatternError> {
5275 let mut provenance_budget = ProvenanceBudget::new();
5276 Ok(
5277 expand_brace_alternatives_with_provenance(pattern, None, escapes, &mut provenance_budget)?
5278 .into_iter()
5279 .map(|alternative| alternative.bytes)
5280 .collect(),
5281 )
5282}
5283
5284fn first_unescaped_brace(pattern: &[u8], escapes: bool) -> Option<usize> {
5285 if !escapes {
5286 return memchr(b'{', pattern);
5287 }
5288 let mut index = 0;
5289 while index < pattern.len() {
5290 if escapes && pattern[index] == b'\\' {
5291 index += 2;
5292 } else if pattern[index] == b'{' {
5293 return Some(index);
5294 } else {
5295 index += 1;
5296 }
5297 }
5298 None
5299}
5300
5301fn matching_brace(pattern: &[u8], open: usize, escapes: bool) -> Option<usize> {
5302 let mut depth = 0_usize;
5303 let mut index = open;
5304 while index < pattern.len() {
5305 if escapes && pattern[index] == b'\\' {
5306 index += 2;
5307 continue;
5308 }
5309 match pattern[index] {
5310 b'{' => depth += 1,
5311 b'}' => {
5312 depth -= 1;
5313 if depth == 0 {
5314 return Some(index);
5315 }
5316 }
5317 _ => {}
5318 }
5319 index += 1;
5320 }
5321 None
5322}
5323
5324fn split_brace_alternatives(content: &[u8], escapes: bool) -> Vec<std::ops::Range<usize>> {
5325 let mut alternatives = Vec::new();
5326 let mut start = 0;
5327 let mut depth = 0_usize;
5328 let mut index = 0;
5329 while index < content.len() {
5330 if escapes && content[index] == b'\\' {
5331 index += 2;
5332 continue;
5333 }
5334 match content[index] {
5335 b'{' => depth += 1,
5336 b'}' => depth -= 1,
5337 b',' if depth == 0 => {
5338 alternatives.push(start..index);
5339 start = index + 1;
5340 }
5341 _ => {}
5342 }
5343 index += 1;
5344 }
5345 alternatives.push(start..content.len());
5346 alternatives
5347}
5348
5349fn is_separator(byte: u8) -> bool {
5350 byte == b'/' || (cfg!(windows) && byte == b'\\')
5351}
5352
5353fn at_component_start(path: &[u8], index: usize, options: PatternOptions) -> bool {
5354 (index == 0 && options.candidate_starts_component)
5355 || (index > 0 && path.get(index - 1).is_some_and(|byte| is_separator(*byte)))
5356}
5357
5358fn bytes_equal(expected: u8, actual: u8, case_insensitive: bool) -> bool {
5359 fold_ascii(expected, case_insensitive) == fold_ascii(actual, case_insensitive)
5360}
5361
5362fn fold_ascii(byte: u8, case_insensitive: bool) -> u8 {
5363 if case_insensitive {
5364 byte.to_ascii_lowercase()
5365 } else {
5366 byte
5367 }
5368}
5369
5370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5371enum ExtglobKind {
5372 Optional,
5373 ZeroOrMore,
5374 OneOrMore,
5375 ExactlyOne,
5376 Negated,
5377}
5378
5379#[derive(Debug, Clone, PartialEq, Eq)]
5391struct CompiledExtglob {
5392 literal_prefix: Vec<u8>,
5396 steps: Vec<ExtglobStep>,
5397 groups: Vec<ExtglobGroup>,
5398 positive_nfa: Option<PositiveExtglobNfa>,
5403 memo_state_indices: Vec<usize>,
5408 memo_state_count: usize,
5409 trailing_globstar_separator: Option<usize>,
5413 walker_source_provenance: Option<SourceProvenance>,
5416 leading_dot_is_normalized: bool,
5417}
5418
5419#[derive(Debug, Clone, PartialEq, Eq)]
5421enum ExtglobStep {
5422 NoMatch,
5425 Star {
5427 next: usize,
5428 blocks_leading_period: bool,
5431 },
5432 Any,
5434 Class { class: Class, next: usize },
5436 Escape { escaped: u8 },
5441 Byte(u8),
5443 Group(usize),
5445 UnclosedGroup { byte: u8 },
5448}
5449
5450#[derive(Debug, Clone, PartialEq, Eq)]
5451struct PositiveExtglobNfa {
5452 states: Vec<PositiveExtglobState>,
5453 start: usize,
5454}
5455
5456#[derive(Debug, Clone, PartialEq, Eq)]
5457enum PositiveExtglobState {
5458 Epsilon {
5459 targets: Vec<usize>,
5460 blocks_leading_period: bool,
5461 },
5462 Consume {
5463 matcher: PositiveExtglobMatcher,
5464 next: usize,
5465 },
5466 Star {
5467 matcher: PositiveExtglobMatcher,
5468 next: usize,
5469 blocks_leading_period: bool,
5470 stops_at_component_start: bool,
5472 },
5473 Match,
5474}
5475
5476#[derive(Debug, Clone, PartialEq, Eq)]
5477enum PositiveExtglobMatcher {
5478 Literal(u8),
5479 GroupLiteral(u8),
5480 GroupSeparator,
5481 Wildcard(WildcardScope),
5482 Class(Class, WildcardScope),
5483}
5484
5485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5486enum WildcardScope {
5487 Extglob(bool),
5490 Group(bool),
5493 Recursive,
5496}
5497
5498struct PositiveExtglobBuilder<'budget> {
5499 states: Vec<PositiveExtglobState>,
5500 budget: &'budget mut IrBudget,
5501}
5502
5503#[derive(Default)]
5504struct PositiveExtglobClosureScratch {
5505 seen: Vec<bool>,
5506 pending: Vec<usize>,
5507}
5508
5509#[derive(Default)]
5510struct PositiveExtglobMatchScratch {
5511 current: Vec<usize>,
5512 next: Vec<usize>,
5513 closure: PositiveExtglobClosureScratch,
5514}
5515
5516impl PositiveExtglobMatchScratch {
5517 fn prepare(&mut self, state_count: usize) {
5518 self.current.clear();
5519 self.next.clear();
5520 self.closure.seen.resize(state_count, false);
5521 self.closure.seen.fill(false);
5522 self.closure.pending.clear();
5523 }
5524
5525 fn release(&mut self) {
5526 self.current.clear();
5527 if self.current.capacity() > RETAINED_SCRATCH_WORDS {
5528 self.current.shrink_to(RETAINED_SCRATCH_WORDS);
5529 }
5530 self.next.clear();
5531 if self.next.capacity() > RETAINED_SCRATCH_WORDS {
5532 self.next.shrink_to(RETAINED_SCRATCH_WORDS);
5533 }
5534 self.closure.pending.clear();
5535 if self.closure.pending.capacity() > RETAINED_SCRATCH_WORDS {
5536 self.closure.pending.shrink_to(RETAINED_SCRATCH_WORDS);
5537 }
5538 let retained_seen_bits = RETAINED_SCRATCH_WORDS * u64::BITS as usize;
5539 if self.closure.seen.capacity() > retained_seen_bits {
5540 self.closure.seen.clear();
5541 self.closure.seen.shrink_to(retained_seen_bits);
5542 }
5543 }
5544}
5545
5546thread_local! {
5549 static POSITIVE_EXTGLOB_SCRATCH: RefCell<PositiveExtglobMatchScratch> =
5550 RefCell::new(PositiveExtglobMatchScratch::default());
5551}
5552
5553#[derive(Debug, Clone, PartialEq, Eq)]
5555struct ExtglobAlternative {
5556 compiled: Vec<CompiledAlternative>,
5557 width: Option<usize>,
5560 absorbed_by_separator: bool,
5565}
5566
5567#[derive(Debug, Clone, PartialEq, Eq)]
5569struct ExtglobGroup {
5570 kind: ExtglobKind,
5571 alternatives: Vec<ExtglobAlternative>,
5572 start: usize,
5574 rest: usize,
5576 component_local: bool,
5579 spans_components: bool,
5583 root_wildcard: bool,
5586 through_separator: Vec<ExtglobAlternative>,
5596}
5597
5598impl ExtglobGroup {
5599 fn first_iteration_differs(&self) -> bool {
5603 self.component_local && self.root_wildcard
5604 }
5605
5606 fn matched_alternatives(&self) -> impl Iterator<Item = &ExtglobAlternative> {
5609 self.alternatives
5610 .iter()
5611 .filter(|alternative| !alternative.absorbed_by_separator)
5612 }
5613
5614 fn first_iteration_apart(&self, options: PatternOptions) -> bool {
5618 self.first_iteration_differs()
5619 && options.component_wildcards
5620 && !options.root_component_wildcards
5621 }
5622
5623 fn later_iteration_root_local(&self, options: PatternOptions) -> bool {
5625 self.component_local && !self.first_iteration_apart(options)
5626 }
5627
5628 fn alternative_end_limit(
5639 &self,
5640 path: &[u8],
5641 path_index: usize,
5642 options: PatternOptions,
5643 ) -> usize {
5644 if self.kind == ExtglobKind::Negated {
5645 return extglob_component_end(path, path_index, options, self.component_local);
5646 }
5647 if options.component_wildcards && options.root_component_wildcards && !self.spans_components
5648 {
5649 return extglob_component_end(path, path_index, options, true);
5650 }
5651 path.len()
5652 }
5653}
5654
5655impl PositiveExtglobNfa {
5656 fn compile(
5657 steps: &[ExtglobStep],
5658 groups: &[ExtglobGroup],
5659 trailing_globstar_separator: Option<usize>,
5660 budget: &mut IrBudget,
5661 ) -> Result<Option<Self>, PatternError> {
5662 if groups
5663 .iter()
5664 .any(|group| group.kind == ExtglobKind::Negated)
5665 || steps.iter().any(|step| {
5666 matches!(
5667 step,
5668 ExtglobStep::Star { .. } | ExtglobStep::UnclosedGroup { byte: b'*' }
5669 )
5670 })
5671 {
5672 return Ok(None);
5673 }
5674 let mut builder = PositiveExtglobBuilder {
5675 states: Vec::new(),
5676 budget,
5677 };
5678 let accept = builder.push(PositiveExtglobState::Match, 0)?;
5679 let mut suffixes = vec![None; steps.len() + 1];
5680 suffixes[steps.len()] = Some(accept);
5681
5682 for index in (0..steps.len()).rev() {
5683 let start =
5684 match &steps[index] {
5685 ExtglobStep::NoMatch => None,
5686 ExtglobStep::Byte(byte) => builder
5687 .consume(PositiveExtglobMatcher::Literal(*byte), suffixes[index + 1])?,
5688 ExtglobStep::Any => builder.consume(
5689 PositiveExtglobMatcher::Wildcard(WildcardScope::Extglob(
5690 extglob_step_component_local(steps, index),
5691 )),
5692 suffixes[index + 1],
5693 )?,
5694 ExtglobStep::Class { class, next } => builder.consume(
5695 PositiveExtglobMatcher::Class(
5696 class.clone(),
5697 WildcardScope::Extglob(extglob_step_component_local(steps, index)),
5698 ),
5699 suffixes[*next],
5700 )?,
5701 ExtglobStep::Star { .. } => {
5702 unreachable!("outer stars keep the compatible interpreter")
5703 }
5704 ExtglobStep::Escape { escaped } => builder.consume(
5705 PositiveExtglobMatcher::Literal(*escaped),
5706 suffixes[index + 2],
5707 )?,
5708 ExtglobStep::Group(group) => {
5709 let group = &groups[*group];
5710 let behind_separator = if group.through_separator.is_empty() {
5711 None
5712 } else {
5713 suffixes[group.rest + 1]
5714 };
5715 builder.group(group, suffixes[group.rest], behind_separator)?
5716 }
5717 ExtglobStep::UnclosedGroup { byte: b'*' } => {
5718 unreachable!("unclosed outer stars keep the compatible interpreter")
5719 }
5720 ExtglobStep::UnclosedGroup { byte: b'?' } => builder.consume(
5721 PositiveExtglobMatcher::Wildcard(WildcardScope::Extglob(
5722 extglob_step_component_local(steps, index),
5723 )),
5724 suffixes[index + 1],
5725 )?,
5726 ExtglobStep::UnclosedGroup { byte } => builder
5727 .consume(PositiveExtglobMatcher::Literal(*byte), suffixes[index + 1])?,
5728 };
5729 let start = if trailing_globstar_separator == Some(index) {
5732 builder.epsilon(start.into_iter().chain([accept]).collect(), false)?
5733 } else {
5734 start
5735 };
5736 suffixes[index] = start;
5737 }
5738 Ok(suffixes[0].map(|start| Self {
5739 states: builder.states,
5740 start,
5741 }))
5742 }
5743
5744 fn is_match(&self, path: &[u8], options: PatternOptions) -> bool {
5745 POSITIVE_EXTGLOB_SCRATCH.with(|cell| match cell.try_borrow_mut() {
5746 Ok(mut scratch) => {
5747 scratch.prepare(self.states.len());
5748 let matched = self.is_match_with_scratch(path, options, &mut scratch);
5749 scratch.release();
5750 matched
5751 }
5752 Err(_) => {
5753 let mut scratch = PositiveExtglobMatchScratch::default();
5754 scratch.prepare(self.states.len());
5755 self.is_match_with_scratch(path, options, &mut scratch)
5756 }
5757 })
5758 }
5759
5760 fn is_match_with_scratch(
5761 &self,
5762 path: &[u8],
5763 options: PatternOptions,
5764 scratch: &mut PositiveExtglobMatchScratch,
5765 ) -> bool {
5766 let PositiveExtglobMatchScratch {
5767 current,
5768 next,
5769 closure,
5770 } = scratch;
5771 self.add_closure(
5772 self.start,
5773 path.first().copied(),
5774 options.candidate_starts_component,
5775 options,
5776 current,
5777 closure,
5778 );
5779
5780 let mut at_component_start = options.candidate_starts_component;
5781 for (path_index, &byte) in path.iter().enumerate() {
5782 next.clear();
5783 closure.seen.fill(false);
5784 let next_byte = path.get(path_index + 1).copied();
5785 let next_starts_component = is_separator(byte);
5786 for &state in current.iter() {
5787 match &self.states[state] {
5788 PositiveExtglobState::Consume {
5789 matcher,
5790 next: target,
5791 } if matcher.matches(byte, at_component_start, options) => {
5792 self.add_closure(
5793 *target,
5794 next_byte,
5795 next_starts_component,
5796 options,
5797 next,
5798 closure,
5799 );
5800 }
5801 PositiveExtglobState::Star { matcher, .. }
5802 if matcher.matches(byte, at_component_start, options) =>
5803 {
5804 self.add_closure(
5805 state,
5806 next_byte,
5807 next_starts_component,
5808 options,
5809 next,
5810 closure,
5811 );
5812 }
5813 _ => {}
5814 }
5815 }
5816 if next.is_empty() {
5817 return false;
5818 }
5819 std::mem::swap(current, next);
5820 at_component_start = next_starts_component;
5821 }
5822 current
5823 .iter()
5824 .any(|&state| matches!(self.states[state], PositiveExtglobState::Match))
5825 }
5826
5827 fn add_closure(
5828 &self,
5829 start: usize,
5830 byte: Option<u8>,
5831 at_component_start: bool,
5832 options: PatternOptions,
5833 active: &mut Vec<usize>,
5834 scratch: &mut PositiveExtglobClosureScratch,
5835 ) {
5836 scratch.pending.clear();
5837 scratch.pending.push(start);
5838 while let Some(state) = scratch.pending.pop() {
5839 if std::mem::replace(&mut scratch.seen[state], true) {
5840 continue;
5841 }
5842 match &self.states[state] {
5843 PositiveExtglobState::Epsilon {
5844 targets,
5845 blocks_leading_period,
5846 } => {
5847 if *blocks_leading_period
5848 && !options.match_hidden
5849 && at_component_start
5850 && byte == Some(b'.')
5851 {
5852 continue;
5853 }
5854 scratch.pending.extend(targets.iter().copied());
5855 }
5856 PositiveExtglobState::Star {
5857 next,
5858 blocks_leading_period,
5859 stops_at_component_start,
5860 ..
5861 } => {
5862 active.push(state);
5863 if !(*blocks_leading_period
5864 && !options.match_hidden
5865 && at_component_start
5866 && byte == Some(b'.'))
5867 && (!*stops_at_component_start || at_component_start)
5868 {
5869 scratch.pending.push(*next);
5870 }
5871 }
5872 PositiveExtglobState::Consume { .. } | PositiveExtglobState::Match => {
5873 active.push(state);
5874 }
5875 }
5876 }
5877 }
5878}
5879
5880#[cfg(test)]
5883fn positive_extglob_scratch_capacities() -> (usize, usize, usize, usize) {
5884 POSITIVE_EXTGLOB_SCRATCH.with(|cell| {
5885 let scratch = cell.borrow();
5886 (
5887 scratch.current.capacity(),
5888 scratch.next.capacity(),
5889 scratch.closure.seen.capacity(),
5890 scratch.closure.pending.capacity(),
5891 )
5892 })
5893}
5894
5895impl PositiveExtglobMatcher {
5896 fn matches(&self, byte: u8, at_component_start: bool, options: PatternOptions) -> bool {
5897 match self {
5898 Self::Literal(expected) => bytes_equal(*expected, byte, options.case_insensitive),
5899 Self::GroupLiteral(expected) => bytes_equal(*expected, byte, options.case_insensitive),
5900 Self::GroupSeparator => is_separator(byte),
5901 Self::Wildcard(scope) => scope.accepts(byte, at_component_start, options),
5902 Self::Class(class, scope) => {
5903 scope.accepts(byte, at_component_start, options)
5904 && class.matches(byte, options.case_insensitive)
5905 }
5906 }
5907 }
5908}
5909
5910impl WildcardScope {
5911 fn accepts(self, byte: u8, at_component_start: bool, options: PatternOptions) -> bool {
5912 let component_local = match self {
5913 Self::Extglob(position_local) | Self::Group(position_local) => {
5914 options.component_wildcards && (options.root_component_wildcards || position_local)
5915 }
5916 Self::Recursive => false,
5917 };
5918 (!is_separator(byte) || !component_local)
5919 && (options.match_hidden || byte != b'.' || !at_component_start)
5920 }
5921}
5922
5923fn token_position_component_local(
5924 tokens: &[Token],
5925 token_index: usize,
5926 root_component_local: bool,
5927) -> bool {
5928 token_index == 0 && root_component_local
5929 || token_index > 0 && matches!(tokens[token_index - 1], Token::Separator)
5930}
5931
5932fn extglob_step_component_local(steps: &[ExtglobStep], step_index: usize) -> bool {
5933 step_index > 0 && matches!(steps[step_index - 1], ExtglobStep::Byte(byte) if is_separator(byte))
5934}
5935
5936impl PositiveExtglobBuilder<'_> {
5937 fn push(
5938 &mut self,
5939 state: PositiveExtglobState,
5940 edge_count: usize,
5941 ) -> Result<usize, PatternError> {
5942 self.budget.charge(2 + edge_count, 0)?;
5946 let index = self.states.len();
5947 self.states.push(state);
5948 Ok(index)
5949 }
5950
5951 fn consume(
5952 &mut self,
5953 matcher: PositiveExtglobMatcher,
5954 next: Option<usize>,
5955 ) -> Result<Option<usize>, PatternError> {
5956 next.map(|next| self.push(PositiveExtglobState::Consume { matcher, next }, 1))
5957 .transpose()
5958 }
5959
5960 fn star(
5961 &mut self,
5962 matcher: PositiveExtglobMatcher,
5963 next: Option<usize>,
5964 semantics: StarSemantics,
5965 ) -> Result<Option<usize>, PatternError> {
5966 next.map(|next| {
5967 self.push(
5968 PositiveExtglobState::Star {
5969 matcher,
5970 next,
5971 blocks_leading_period: semantics.blocks_hidden_stop,
5972 stops_at_component_start: semantics.stops_at_component_start,
5973 },
5974 1,
5975 )
5976 })
5977 .transpose()
5978 }
5979
5980 fn epsilon(
5981 &mut self,
5982 mut targets: Vec<usize>,
5983 blocks_leading_period: bool,
5984 ) -> Result<Option<usize>, PatternError> {
5985 targets.sort_unstable();
5986 targets.dedup();
5987 if targets.is_empty() {
5988 return Ok(None);
5989 }
5990 if targets.len() == 1 && !blocks_leading_period {
5991 return Ok(targets.first().copied());
5992 }
5993 let edge_count = targets.len();
5994 self.push(
5995 PositiveExtglobState::Epsilon {
5996 targets,
5997 blocks_leading_period,
5998 },
5999 edge_count,
6000 )
6001 .map(Some)
6002 }
6003
6004 fn group(
6008 &mut self,
6009 group: &ExtglobGroup,
6010 rest: Option<usize>,
6011 behind_separator: Option<usize>,
6012 ) -> Result<Option<usize>, PatternError> {
6013 let Some(rest) = rest else {
6016 return Ok(None);
6017 };
6018 let blocks_leading_period = !extglob_group_allows_literal_leading_period(group);
6019 match group.kind {
6020 ExtglobKind::ExactlyOne => {
6021 let mut alternatives = self.alternatives(group, rest, group.component_local)?;
6022 if let Some(behind_separator) = behind_separator {
6023 alternatives.extend(self.through_separator(
6024 group,
6025 behind_separator,
6026 group.component_local,
6027 )?);
6028 }
6029 self.epsilon(alternatives, blocks_leading_period)
6030 }
6031 ExtglobKind::Optional => {
6032 let mut alternatives = self.alternatives(group, rest, group.component_local)?;
6033 alternatives.push(rest);
6034 if let Some(behind_separator) = behind_separator {
6035 alternatives.extend(self.through_separator(
6036 group,
6037 behind_separator,
6038 group.component_local,
6039 )?);
6040 }
6041 self.epsilon(alternatives, blocks_leading_period)
6042 }
6043 ExtglobKind::ZeroOrMore | ExtglobKind::OneOrMore => {
6044 let hub = self.push(
6045 PositiveExtglobState::Epsilon {
6046 targets: Vec::new(),
6047 blocks_leading_period: false,
6048 },
6049 0,
6050 )?;
6051 let later = self.alternatives(group, hub, false)?;
6055 let mut first = if group.first_iteration_differs() {
6056 self.alternatives(group, hub, true)?
6057 } else {
6058 later.clone()
6059 };
6060 let mut loop_targets = later;
6061 loop_targets.push(rest);
6062 if let Some(behind_separator) = behind_separator {
6065 first.extend(self.through_separator(
6066 group,
6067 behind_separator,
6068 group.first_iteration_differs(),
6069 )?);
6070 loop_targets.extend(self.through_separator(group, behind_separator, false)?);
6071 }
6072 let edge_count = loop_targets.len();
6073 self.budget.charge(edge_count, 0)?;
6074 self.states[hub] = PositiveExtglobState::Epsilon {
6075 targets: loop_targets,
6076 blocks_leading_period: false,
6077 };
6078 if group.kind == ExtglobKind::ZeroOrMore {
6079 let mut entry = first;
6080 entry.push(rest);
6081 self.epsilon(entry, blocks_leading_period)
6082 } else {
6083 self.epsilon(first, blocks_leading_period)
6084 }
6085 }
6086 ExtglobKind::Negated => unreachable!("negated groups do not compile to this NFA"),
6087 }
6088 }
6089
6090 fn alternatives(
6091 &mut self,
6092 group: &ExtglobGroup,
6093 next: usize,
6094 root_component_local: bool,
6095 ) -> Result<Vec<usize>, PatternError> {
6096 self.alternative_list(group.matched_alternatives(), next, root_component_local)
6097 }
6098
6099 fn through_separator(
6102 &mut self,
6103 group: &ExtglobGroup,
6104 behind_separator: usize,
6105 root_component_local: bool,
6106 ) -> Result<Vec<usize>, PatternError> {
6107 self.alternative_list(
6108 &group.through_separator,
6109 behind_separator,
6110 root_component_local,
6111 )
6112 }
6113
6114 fn alternative_list<'a>(
6115 &mut self,
6116 alternatives: impl IntoIterator<Item = &'a ExtglobAlternative>,
6117 next: usize,
6118 root_component_local: bool,
6119 ) -> Result<Vec<usize>, PatternError> {
6120 let mut starts = Vec::new();
6121 for alternative in alternatives {
6122 for alternative in &alternative.compiled {
6123 if let Some(start) = self.tokens(&alternative.tokens, next, root_component_local)? {
6124 starts.push(start);
6125 }
6126 }
6127 }
6128 Ok(starts)
6129 }
6130
6131 fn tokens(
6132 &mut self,
6133 tokens: &[Token],
6134 next: usize,
6135 root_component_local: bool,
6136 ) -> Result<Option<usize>, PatternError> {
6137 let mut start = Some(next);
6138 for (token_index, token) in tokens.iter().enumerate().rev() {
6139 start = match token {
6140 Token::Literal(literal) => {
6141 let mut literal_start = start;
6142 for &byte in literal.iter().rev() {
6143 literal_start = self
6144 .consume(PositiveExtglobMatcher::GroupLiteral(byte), literal_start)?;
6145 }
6146 literal_start
6147 }
6148 Token::Separator => self.consume(PositiveExtglobMatcher::GroupSeparator, start)?,
6149 Token::Any => self.consume(
6150 PositiveExtglobMatcher::Wildcard(WildcardScope::Group(
6151 token_position_component_local(tokens, token_index, root_component_local),
6152 )),
6153 start,
6154 )?,
6155 Token::Class(class) => self.consume(
6156 PositiveExtglobMatcher::Class(
6157 class.clone(),
6158 WildcardScope::Group(token_position_component_local(
6159 tokens,
6160 token_index,
6161 root_component_local,
6162 )),
6163 ),
6164 start,
6165 )?,
6166 Token::Star => self.star(
6167 PositiveExtglobMatcher::Wildcard(WildcardScope::Group(
6168 token_position_component_local(tokens, token_index, root_component_local),
6169 )),
6170 start,
6171 StarSemantics::ordinary(false),
6172 )?,
6173 Token::RecursiveStar => self.star(
6174 PositiveExtglobMatcher::Wildcard(WildcardScope::Recursive),
6175 start,
6176 StarSemantics::ordinary(true),
6177 )?,
6178 Token::RecursivePrefix => self.star(
6179 PositiveExtglobMatcher::Wildcard(WildcardScope::Recursive),
6180 start,
6181 StarSemantics::RECURSIVE_PREFIX,
6182 )?,
6183 };
6184 if token_index + 2 == tokens.len()
6185 && matches!(token, Token::Separator)
6186 && matches!(tokens.last(), Some(Token::RecursiveStar))
6187 && let Some(normal) = start
6188 {
6189 start = self.epsilon(vec![normal, next], false)?;
6190 }
6191 }
6192 Ok(start)
6193 }
6194}
6195
6196fn compile_extglob(
6200 pattern: &[u8],
6201 options: PatternOptions,
6202 budget: &mut IrBudget,
6203 provenance_budget: &mut ProvenanceBudget,
6204 walker_source_provenance: Option<&SourceProvenance>,
6205 leading_dot_is_normalized: bool,
6206) -> Result<Option<Box<CompiledExtglob>>, PatternError> {
6207 if !options.extglob || !contains_extglob(pattern, options.escape) {
6208 return Ok(None);
6209 }
6210 budget.charge(pattern.len(), 0)?;
6213 let mut steps = vec![ExtglobStep::NoMatch; pattern.len()];
6214 let mut compiled = vec![false; pattern.len()];
6215 let mut groups = Vec::new();
6216 let mut pending = vec![0_usize];
6217 while let Some(index) = pending.pop() {
6218 if index >= pattern.len() || compiled[index] {
6219 continue;
6220 }
6221 compiled[index] = true;
6222 let step = compile_extglob_step(
6223 pattern,
6224 index,
6225 options,
6226 &mut groups,
6227 budget,
6228 provenance_budget,
6229 walker_source_provenance,
6230 )?;
6231 match &step {
6232 ExtglobStep::Group(group) => pending.push(groups[*group].rest),
6233 ExtglobStep::Star { next, .. } | ExtglobStep::Class { next, .. } => pending.push(*next),
6234 ExtglobStep::Escape { .. } => pending.push(index + 2),
6237 ExtglobStep::Any | ExtglobStep::Byte(_) | ExtglobStep::UnclosedGroup { .. } => {
6238 pending.push(index + 1);
6239 }
6240 ExtglobStep::NoMatch => pending.push(index + 1),
6244 }
6245 steps[index] = step;
6246 }
6247 let mut memo_state_indices = vec![usize::MAX; pattern.len() + 1];
6248 let mut memo_state_count = 0;
6249 let mut add_memo_state = |offset| {
6250 if memo_state_indices[offset] == usize::MAX {
6251 memo_state_indices[offset] = memo_state_count;
6252 memo_state_count += 1;
6253 }
6254 };
6255 add_memo_state(0);
6256 for group in &groups {
6257 add_memo_state(group.start);
6258 add_memo_state(group.rest);
6259 if !group.through_separator.is_empty() {
6260 add_memo_state(group.rest + 1);
6262 }
6263 }
6264 for step in &steps {
6265 if let ExtglobStep::Star {
6266 next,
6267 blocks_leading_period: false,
6268 } = step
6269 && steps
6270 .get(*next)
6271 .is_some_and(|step| matches!(step, ExtglobStep::Byte(byte) if is_separator(*byte)))
6272 {
6273 add_memo_state(*next + 1);
6274 }
6275 }
6276 let trailing_globstar_separator =
6277 trailing_globstar_separator(pattern, &steps, &groups, options);
6278 let positive_nfa =
6279 PositiveExtglobNfa::compile(&steps, &groups, trailing_globstar_separator, budget)?;
6280 let literal_prefix = steps[..trailing_globstar_separator.unwrap_or(steps.len())]
6283 .iter()
6284 .map_while(|step| match step {
6285 ExtglobStep::Byte(byte) => Some(*byte),
6286 _ => None,
6287 })
6288 .collect();
6289 Ok(Some(Box::new(CompiledExtglob {
6290 literal_prefix,
6291 steps,
6292 groups,
6293 positive_nfa,
6294 memo_state_indices,
6295 memo_state_count,
6296 trailing_globstar_separator,
6297 walker_source_provenance: walker_source_provenance.cloned(),
6298 leading_dot_is_normalized,
6299 })))
6300}
6301
6302fn compile_extglob_step(
6304 pattern: &[u8],
6305 index: usize,
6306 options: PatternOptions,
6307 groups: &mut Vec<ExtglobGroup>,
6308 budget: &mut IrBudget,
6309 provenance_budget: &mut ProvenanceBudget,
6310 walker_source_provenance: Option<&SourceProvenance>,
6311) -> Result<ExtglobStep, PatternError> {
6312 if let Some(kind) = detect_extglob_at(pattern, index) {
6313 let open = index + 1;
6314 let Some(close) = closing_extglob_parenthesis(pattern, open, options.escape) else {
6315 return Ok(ExtglobStep::UnclosedGroup {
6316 byte: pattern[index],
6317 });
6318 };
6319 let bounds = ComponentBounds {
6322 starts: starts_component_at(pattern, index, options.escape, ComponentBounds::WHOLE),
6323 ends: ends_component_at(pattern, close + 1, ComponentBounds::WHOLE),
6324 };
6325 let separator_follows = pattern.get(close + 1) == Some(&b'/');
6328 let mut alternatives = Vec::new();
6329 let mut through_separator = Vec::new();
6330 for range in split_extglob_alternatives(&pattern[open + 1..close], options.escape) {
6331 let start = open + 1 + range.start;
6332 let end = open + 1 + range.end;
6333 let alternative_provenance = match walker_source_provenance {
6334 Some(provenance) => Some(provenance.slice(start, end, provenance_budget, index)?),
6335 None => None,
6336 };
6337 let mut alternative = compile_extglob_alternative(
6338 &pattern[start..end],
6339 options,
6340 budget,
6341 provenance_budget,
6342 alternative_provenance.as_ref(),
6343 false,
6344 bounds,
6345 )?;
6346 if kind != ExtglobKind::Negated
6347 && separator_follows
6348 && alternative_ends_in_recursive_star(&alternative)
6349 {
6350 let mut with_separator = pattern[start..end].to_vec();
6355 with_separator.push(b'/');
6356 let absorbing = compile_extglob_alternative(
6357 &with_separator,
6358 options,
6359 budget,
6360 provenance_budget,
6361 None,
6362 false,
6363 bounds,
6364 )?;
6365 debug_assert!(
6366 absorbing.compiled.iter().all(|compiled| matches!(
6367 compiled.tokens.last(),
6368 Some(Token::RecursivePrefix)
6369 )),
6370 "the separator turns the trailing `**` into a `**/`"
6371 );
6372 through_separator.push(absorbing);
6373 alternative.absorbed_by_separator = true;
6374 }
6375 alternatives.push(alternative);
6376 }
6377 let spans_components = alternatives.iter().any(|alternative| {
6378 alternative.compiled.iter().any(|compiled| {
6379 compiled.tokens.iter().any(|token| match token {
6380 Token::Separator | Token::RecursiveStar | Token::RecursivePrefix => true,
6381 Token::Literal(literal) => literal.iter().any(|byte| is_separator(*byte)),
6382 Token::Any | Token::Star | Token::Class(_) => false,
6383 })
6384 })
6385 });
6386 let root_wildcard = alternatives.iter().any(|alternative| {
6387 alternative.compiled.iter().any(|compiled| {
6388 matches!(
6389 compiled.tokens.first(),
6390 Some(Token::Any | Token::Class(_) | Token::Star)
6391 )
6392 })
6393 });
6394 groups.push(ExtglobGroup {
6395 kind,
6396 alternatives,
6397 start: index,
6398 rest: close + 1,
6399 component_local: index > 0 && is_separator(pattern[index - 1]),
6400 spans_components,
6401 root_wildcard,
6402 through_separator,
6403 });
6404 return Ok(ExtglobStep::Group(groups.len() - 1));
6405 }
6406 Ok(match pattern[index] {
6407 b'*' => {
6408 let mut next = index + 1;
6409 while pattern.get(next) == Some(&b'*') {
6416 let closed_group_starts_here = detect_extglob_at(pattern, next).is_some()
6417 && closing_extglob_parenthesis(pattern, next + 1, options.escape).is_some();
6418 if closed_group_starts_here {
6419 break;
6420 }
6421 next += 1;
6422 }
6423 ExtglobStep::Star {
6424 next,
6425 blocks_leading_period: !(options.recursive_double_star
6426 && next - index == 2
6427 && pattern.get(next) == Some(&b'/')
6428 && starts_component_at(pattern, index, options.escape, ComponentBounds::WHOLE)),
6429 }
6430 }
6431 b'?' => ExtglobStep::Any,
6432 b'[' => match parse_class(pattern, index, options.escape) {
6435 Ok((class, next)) => ExtglobStep::Class { class, next },
6436 Err(_) => ExtglobStep::NoMatch,
6437 },
6438 b'\\' if options.escape && index + 1 < pattern.len() => ExtglobStep::Escape {
6439 escaped: pattern[index + 1],
6440 },
6441 byte => ExtglobStep::Byte(byte),
6442 })
6443}
6444
6445fn compile_extglob_alternative(
6452 alternative: &[u8],
6453 options: PatternOptions,
6454 budget: &mut IrBudget,
6455 provenance_budget: &mut ProvenanceBudget,
6456 walker_source_provenance: Option<&SourceProvenance>,
6457 leading_dot_is_normalized: bool,
6458 bounds: ComponentBounds,
6459) -> Result<ExtglobAlternative, PatternError> {
6460 let options = PatternOptions {
6461 braces: false,
6462 extglob: false,
6463 component_wildcards: false,
6464 root_component_wildcards: false,
6465 ..options
6466 };
6467 let mut compiled = Pattern::compile_within(
6468 alternative,
6469 options,
6470 budget,
6471 provenance_budget,
6472 walker_source_provenance,
6473 leading_dot_is_normalized,
6474 bounds,
6475 )?
6476 .alternatives;
6477 for compiled in &mut compiled {
6481 if sweep_is_deferred(compiled) {
6482 compiled.sweep = Some(SweepEngine::build(&compiled.tokens, options)?);
6483 }
6484 }
6485 let width = fixed_token_width(&compiled);
6486 Ok(ExtglobAlternative {
6487 compiled,
6488 width,
6489 absorbed_by_separator: false,
6490 })
6491}
6492
6493fn alternative_ends_in_recursive_star(alternative: &ExtglobAlternative) -> bool {
6496 alternative
6497 .compiled
6498 .iter()
6499 .any(|compiled| matches!(compiled.tokens.last(), Some(Token::RecursiveStar)))
6500}
6501
6502fn alternative_is_globstar(alternative: &ExtglobAlternative) -> bool {
6506 alternative
6507 .compiled
6508 .iter()
6509 .any(|compiled| matches!(compiled.tokens.as_slice(), [Token::RecursiveStar]))
6510}
6511
6512fn trailing_globstar_separator(
6520 pattern: &[u8],
6521 steps: &[ExtglobStep],
6522 groups: &[ExtglobGroup],
6523 options: PatternOptions,
6524) -> Option<usize> {
6525 if !options.recursive_double_star {
6526 return None;
6527 }
6528 let outer_star = steps.len().checked_sub(2).filter(
6530 |&star| matches!(steps[star], ExtglobStep::Star { next, .. } if next == steps.len()),
6531 );
6532 let globstar = match outer_star {
6535 Some(star) => star,
6536 None => {
6537 groups
6538 .iter()
6539 .find(|group| {
6540 group.rest == steps.len()
6541 && group.kind != ExtglobKind::Negated
6542 && matches!(steps.get(group.start), Some(ExtglobStep::Group(_)))
6543 && group.alternatives.iter().any(alternative_is_globstar)
6544 })?
6545 .start
6546 }
6547 };
6548 let separator = globstar.checked_sub(1)?;
6550 (matches!(steps[separator], ExtglobStep::Byte(b'/'))
6551 && starts_component_at(
6552 pattern,
6553 separator + 1,
6554 options.escape,
6555 ComponentBounds::WHOLE,
6556 ))
6557 .then_some(separator)
6558}
6559
6560fn fixed_token_width(alternatives: &[CompiledAlternative]) -> Option<usize> {
6566 let [alternative] = alternatives else {
6567 return None;
6568 };
6569 let mut width = 0_usize;
6570 for token in &alternative.tokens {
6571 width += match token {
6572 Token::Literal(literal) => literal.len(),
6573 Token::Separator | Token::Any | Token::Class(_) => 1,
6574 Token::Star | Token::RecursiveStar | Token::RecursivePrefix => {
6575 return None;
6576 }
6577 };
6578 }
6579 Some(width)
6580}
6581
6582fn contains_extglob(pattern: &[u8], escapes: bool) -> bool {
6583 let mut index = 0;
6584 while index + 1 < pattern.len() {
6585 if escapes && pattern[index] == b'\\' {
6586 index += 2;
6587 continue;
6588 }
6589 if detect_extglob_at(pattern, index).is_some() {
6590 return true;
6591 }
6592 index += 1;
6593 }
6594 false
6595}
6596
6597fn detect_extglob_at(pattern: &[u8], index: usize) -> Option<ExtglobKind> {
6598 if pattern.get(index + 1) != Some(&b'(') {
6599 return None;
6600 }
6601 match pattern[index] {
6602 b'?' => Some(ExtglobKind::Optional),
6603 b'*' => Some(ExtglobKind::ZeroOrMore),
6604 b'+' => Some(ExtglobKind::OneOrMore),
6605 b'@' => Some(ExtglobKind::ExactlyOne),
6606 b'!' => Some(ExtglobKind::Negated),
6607 _ => None,
6608 }
6609}
6610
6611fn closing_extglob_parenthesis(pattern: &[u8], open: usize, escapes: bool) -> Option<usize> {
6612 if pattern.get(open) != Some(&b'(') {
6613 return None;
6614 }
6615 let mut depth = 1_usize;
6616 let mut index = open + 1;
6617 while index < pattern.len() {
6618 if escapes && pattern[index] == b'\\' {
6619 index += 2;
6620 continue;
6621 }
6622 match pattern[index] {
6623 b'(' => depth += 1,
6624 b')' => {
6625 depth -= 1;
6626 if depth == 0 {
6627 return Some(index);
6628 }
6629 }
6630 _ => {}
6631 }
6632 index += 1;
6633 }
6634 None
6635}
6636
6637fn split_extglob_alternatives(content: &[u8], escapes: bool) -> Vec<std::ops::Range<usize>> {
6638 let mut alternatives = Vec::new();
6639 let mut start = 0;
6640 let mut depth = 0_usize;
6641 let mut index = 0;
6642 while index < content.len() {
6643 if escapes && content[index] == b'\\' {
6644 index += 2;
6645 continue;
6646 }
6647 match content[index] {
6648 b'(' => depth += 1,
6649 b')' if depth > 0 => depth -= 1,
6650 b'|' if depth == 0 => {
6651 alternatives.push(start..index);
6652 start = index + 1;
6653 }
6654 _ => {}
6655 }
6656 index += 1;
6657 }
6658 alternatives.push(start..content.len());
6659 alternatives
6660}
6661
6662#[derive(Default)]
6664struct ExtglobScratch {
6665 visited: Vec<u64>,
6667 failed: Vec<ExtglobFailedRow>,
6670 #[cfg(test)]
6671 failed_page_count: usize,
6674 #[cfg(test)]
6675 failed_state_count: usize,
6678 #[cfg(test)]
6679 pending_peak: usize,
6682 pending: Vec<(usize, usize)>,
6685 ends: Vec<usize>,
6687 candidate_ends: Vec<usize>,
6690 excluded: Vec<bool>,
6692 sweep_states: Vec<SweepState>,
6694 prefix_sweep_state: Option<SweepState>,
6697}
6698
6699struct ExtglobMatchState<'scratch> {
6701 visited: &'scratch mut Vec<u64>,
6702 failed: ExtglobFailedStates<'scratch>,
6703 pending: &'scratch mut Vec<(usize, usize)>,
6704 #[cfg(test)]
6705 pending_peak: &'scratch mut usize,
6706 ends: &'scratch mut Vec<usize>,
6707 candidate_ends: &'scratch mut Vec<usize>,
6708 excluded: &'scratch mut Vec<bool>,
6709 sweep_states: &'scratch mut Vec<SweepState>,
6710 prefix_sweep_state: &'scratch mut Option<SweepState>,
6711}
6712
6713thread_local! {
6714 static EXTGLOB_SCRATCH: RefCell<ExtglobScratch> = RefCell::new(ExtglobScratch::default());
6715}
6716
6717fn match_extglob_program(program: &CompiledExtglob, path: &[u8], options: PatternOptions) -> bool {
6724 let prefix = &program.literal_prefix;
6725 if path.len() < prefix.len()
6726 || !prefix
6727 .iter()
6728 .zip(path)
6729 .all(|(&expected, &actual)| bytes_equal(expected, actual, options.case_insensitive))
6730 {
6731 return false;
6732 }
6733 if let Some(nfa) = &program.positive_nfa {
6734 return nfa.is_match(path, options);
6735 }
6736 EXTGLOB_SCRATCH.with(|cell| match cell.try_borrow_mut() {
6737 Ok(mut scratch) => {
6738 let matched = {
6739 let ExtglobScratch {
6740 visited,
6741 failed,
6742 #[cfg(test)]
6743 failed_page_count,
6744 #[cfg(test)]
6745 failed_state_count,
6746 #[cfg(test)]
6747 pending_peak,
6748 pending,
6749 ends,
6750 candidate_ends,
6751 excluded,
6752 sweep_states,
6753 prefix_sweep_state,
6754 } = &mut *scratch;
6755 visited.clear();
6756 failed.clear();
6757 pending.clear();
6758 ends.clear();
6759 candidate_ends.clear();
6760 excluded.clear();
6761 sweep_states.clear();
6762 #[cfg(test)]
6763 {
6764 *failed_page_count = 0;
6765 *failed_state_count = 0;
6766 *pending_peak = 0;
6767 }
6768 let mut state = ExtglobMatchState {
6769 visited,
6770 failed: ExtglobFailedStates::new(
6771 program,
6772 failed,
6773 #[cfg(test)]
6774 failed_page_count,
6775 #[cfg(test)]
6776 failed_state_count,
6777 ),
6778 pending,
6779 #[cfg(test)]
6780 pending_peak,
6781 ends,
6782 candidate_ends,
6783 excluded,
6784 sweep_states,
6785 prefix_sweep_state,
6786 };
6787 match_extglob_from(program, path, 0, 0, options, &mut state)
6788 };
6789 scratch.visited.clear();
6790 if scratch.visited.capacity() > RETAINED_SCRATCH_WORDS {
6791 scratch.visited.shrink_to(RETAINED_SCRATCH_WORDS);
6792 }
6793 scratch.failed.clear();
6794 let retained_rows = RETAINED_SCRATCH_WORDS * std::mem::size_of::<u64>()
6795 / std::mem::size_of::<ExtglobFailedRow>();
6796 if scratch.failed.capacity() > retained_rows {
6797 scratch.failed.shrink_to(retained_rows);
6798 }
6799 scratch.pending.clear();
6800 if scratch.pending.capacity() > RETAINED_SCRATCH_WORDS {
6801 scratch.pending.shrink_to(RETAINED_SCRATCH_WORDS);
6802 }
6803 scratch.ends.clear();
6804 if scratch.ends.capacity() > RETAINED_SCRATCH_WORDS {
6805 scratch.ends.shrink_to(RETAINED_SCRATCH_WORDS);
6806 }
6807 scratch.candidate_ends.clear();
6808 if scratch.candidate_ends.capacity() > RETAINED_SCRATCH_WORDS {
6809 scratch.candidate_ends.shrink_to(RETAINED_SCRATCH_WORDS);
6810 }
6811 scratch.excluded.clear();
6812 if scratch.excluded.capacity() > RETAINED_SCRATCH_WORDS {
6813 scratch.excluded.shrink_to(RETAINED_SCRATCH_WORDS);
6814 }
6815 scratch.sweep_states.clear();
6816 if scratch.sweep_states.capacity() > RETAINED_SCRATCH_WORDS {
6817 scratch.sweep_states.shrink_to(RETAINED_SCRATCH_WORDS);
6818 }
6819 if scratch.prefix_sweep_state.as_ref().is_some_and(|state| {
6820 SweepEngine::state_exceeds_retained_words(state, RETAINED_SCRATCH_WORDS)
6821 }) {
6822 scratch.prefix_sweep_state = None;
6823 }
6824 matched
6825 }
6826 Err(_) => {
6827 let mut scratch = ExtglobScratch::default();
6828 let ExtglobScratch {
6829 visited,
6830 failed,
6831 #[cfg(test)]
6832 failed_page_count,
6833 #[cfg(test)]
6834 failed_state_count,
6835 #[cfg(test)]
6836 pending_peak,
6837 pending,
6838 ends,
6839 candidate_ends,
6840 excluded,
6841 sweep_states,
6842 prefix_sweep_state,
6843 } = &mut scratch;
6844 let mut state = ExtglobMatchState {
6845 visited,
6846 failed: ExtglobFailedStates::new(
6847 program,
6848 failed,
6849 #[cfg(test)]
6850 failed_page_count,
6851 #[cfg(test)]
6852 failed_state_count,
6853 ),
6854 pending,
6855 #[cfg(test)]
6856 pending_peak,
6857 ends,
6858 candidate_ends,
6859 excluded,
6860 sweep_states,
6861 prefix_sweep_state,
6862 };
6863 match_extglob_from(program, path, 0, 0, options, &mut state)
6864 }
6865 })
6866}
6867
6868#[cfg(test)]
6871fn extglob_scratch_capacities() -> (usize, usize, usize, usize, usize, usize, usize, usize) {
6872 EXTGLOB_SCRATCH.with(|cell| {
6873 let scratch = cell.borrow();
6874 (
6875 scratch.visited.capacity(),
6876 scratch.failed.capacity() * std::mem::size_of::<ExtglobFailedRow>()
6877 / std::mem::size_of::<u64>(),
6878 scratch.pending.capacity(),
6879 scratch.ends.capacity(),
6880 scratch.candidate_ends.capacity(),
6881 scratch.excluded.capacity(),
6882 scratch.sweep_states.capacity(),
6883 scratch
6884 .prefix_sweep_state
6885 .as_ref()
6886 .map_or(0, SweepEngine::retained_state_capacity),
6887 )
6888 })
6889}
6890
6891#[cfg(test)]
6892fn extglob_failed_len() -> usize {
6893 EXTGLOB_SCRATCH.with(|cell| cell.borrow().failed.len())
6894}
6895
6896#[cfg(test)]
6897fn extglob_failed_stats() -> (usize, usize) {
6898 EXTGLOB_SCRATCH.with(|cell| {
6899 let scratch = cell.borrow();
6900 (scratch.failed_page_count, scratch.failed_state_count)
6901 })
6902}
6903
6904#[cfg(test)]
6905fn extglob_pending_peak() -> usize {
6906 EXTGLOB_SCRATCH.with(|cell| cell.borrow().pending_peak)
6907}
6908
6909#[derive(Default)]
6911struct ExtglobFailedRow {
6912 pages: BTreeMap<usize, Box<[u64; EXTGLOB_MEMO_PAGE_WORDS]>>,
6913}
6914
6915const EXTGLOB_MEMO_PAGE_WORDS: usize = 64;
6918const EXTGLOB_MEMO_PAGE_BITS: usize = EXTGLOB_MEMO_PAGE_WORDS * u64::BITS as usize;
6919
6920struct ExtglobFailedStates<'scratch> {
6924 rows: &'scratch mut [ExtglobFailedRow],
6925 #[cfg(test)]
6926 page_count: &'scratch mut usize,
6927 #[cfg(test)]
6928 state_count: &'scratch mut usize,
6929}
6930
6931impl<'scratch> ExtglobFailedStates<'scratch> {
6932 fn new(
6933 program: &CompiledExtglob,
6934 scratch: &'scratch mut Vec<ExtglobFailedRow>,
6935 #[cfg(test)] page_count: &'scratch mut usize,
6936 #[cfg(test)] state_count: &'scratch mut usize,
6937 ) -> Self {
6938 scratch.resize_with(program.memo_state_count, ExtglobFailedRow::default);
6939 Self {
6940 rows: scratch,
6941 #[cfg(test)]
6942 page_count,
6943 #[cfg(test)]
6944 state_count,
6945 }
6946 }
6947
6948 fn insert(&mut self, state_index: usize, path_index: usize) -> bool {
6949 let candidate_page = path_index / EXTGLOB_MEMO_PAGE_BITS;
6950 let page_bit = path_index % EXTGLOB_MEMO_PAGE_BITS;
6951 let row = &mut self.rows[state_index];
6952 let words = match row.pages.entry(candidate_page) {
6953 std::collections::btree_map::Entry::Occupied(entry) => entry.into_mut(),
6954 std::collections::btree_map::Entry::Vacant(entry) => {
6955 let words = entry.insert(Box::new([0; EXTGLOB_MEMO_PAGE_WORDS]));
6956 #[cfg(test)]
6957 {
6958 *self.page_count += 1;
6959 }
6960 words
6961 }
6962 };
6963 let word = &mut words[page_bit / u64::BITS as usize];
6964 let mask = 1_u64 << (page_bit % u64::BITS as usize);
6965 if *word & mask != 0 {
6966 false
6967 } else {
6968 *word |= mask;
6969 #[cfg(test)]
6970 {
6971 *self.state_count += 1;
6972 }
6973 true
6974 }
6975 }
6976}
6977
6978fn push_visited(visited: &mut Vec<u64>, positions: usize) -> usize {
6980 let base = visited.len();
6981 let words = positions.div_ceil(u64::BITS as usize);
6982 visited.resize(base + words, 0);
6983 base
6984}
6985
6986fn visit(visited: &mut [u64], base: usize, position: usize) -> bool {
6988 let word = &mut visited[base + position / u64::BITS as usize];
6989 let mask = 1_u64 << (position % u64::BITS as usize);
6990 if *word & mask != 0 {
6991 false
6992 } else {
6993 *word |= mask;
6994 true
6995 }
6996}
6997
6998fn visited(visited: &[u64], base: usize, position: usize) -> bool {
6999 visited[base + position / u64::BITS as usize] & (1_u64 << (position % u64::BITS as usize)) != 0
7000}
7001
7002fn match_extglob_from(
7003 program: &CompiledExtglob,
7004 path: &[u8],
7005 start: usize,
7006 start_path_index: usize,
7007 options: PatternOptions,
7008 state: &mut ExtglobMatchState<'_>,
7009) -> bool {
7010 let pending_base = state.pending.len();
7011 queue_extglob_continuation(program, state, start, start_path_index);
7012 let mut matched = false;
7013 while state.pending.len() > pending_base {
7014 let (start, start_path_index) = state
7015 .pending
7016 .pop()
7017 .expect("the extglob continuation slice is non-empty");
7018 if match_extglob_task(program, path, start, start_path_index, options, state) {
7019 matched = true;
7020 break;
7021 }
7022 }
7023 state.pending.truncate(pending_base);
7024 matched
7025}
7026
7027fn match_extglob_task(
7032 program: &CompiledExtglob,
7033 path: &[u8],
7034 start: usize,
7035 start_path_index: usize,
7036 options: PatternOptions,
7037 state: &mut ExtglobMatchState<'_>,
7038) -> bool {
7039 let steps = &program.steps;
7040 let mut pattern_index = start;
7041 let mut path_index = start_path_index;
7042 let mut star_pattern_index = 0_usize;
7043 let mut star_path_index = 0_usize;
7044 let mut has_star = false;
7045 let mut star_component_local = false;
7046 let mut crossing_star: Option<(usize, usize)> = None;
7054
7055 while path_index < path.len() || pattern_index < steps.len() {
7056 if pattern_index < steps.len() {
7057 let leading_period_is_forbidden = match &steps[pattern_index] {
7058 ExtglobStep::Group(group) => {
7059 !extglob_group_allows_literal_leading_period(&program.groups[*group])
7060 }
7061 ExtglobStep::UnclosedGroup { .. } => true,
7062 _ => false,
7063 };
7064 if leading_period_is_forbidden
7065 && !options.match_hidden
7066 && path.get(path_index) == Some(&b'.')
7067 && at_component_start(path, path_index, options)
7068 {
7069 return false;
7070 }
7071 match &steps[pattern_index] {
7072 ExtglobStep::Group(group) => {
7073 queue_extglob_group(
7074 program,
7075 &program.groups[*group],
7076 path,
7077 path_index,
7078 options,
7079 state,
7080 );
7081 }
7085 ExtglobStep::Star {
7086 next,
7087 blocks_leading_period,
7088 } => {
7089 let blocked = *blocks_leading_period
7090 && !options.match_hidden
7091 && path.get(path_index) == Some(&b'.')
7092 && at_component_start(path, path_index, options);
7093 if !blocked {
7094 if !*blocks_leading_period
7095 && steps
7096 .get(*next)
7097 .is_some_and(|step| matches!(step, ExtglobStep::Byte(byte) if is_separator(*byte)))
7098 {
7099 queue_extglob_continuation(
7100 program,
7101 state,
7102 *next + 1,
7103 path_index,
7104 );
7105 }
7106 let component_local =
7107 extglob_star_component_local(steps, pattern_index, options);
7108 remember_crossing_star(
7109 &mut crossing_star,
7110 has_star.then_some((
7111 star_pattern_index,
7112 star_path_index,
7113 star_component_local,
7114 )),
7115 component_local,
7116 );
7117 star_pattern_index = *next;
7118 star_path_index = path_index;
7119 star_component_local = component_local;
7120 has_star = true;
7121 pattern_index = *next;
7122 continue;
7123 }
7124 }
7125 ExtglobStep::UnclosedGroup { byte: b'*' } => {
7126 let component_local =
7127 extglob_wildcard_component_local(steps, pattern_index, options);
7128 remember_crossing_star(
7129 &mut crossing_star,
7130 has_star.then_some((
7131 star_pattern_index,
7132 star_path_index,
7133 star_component_local,
7134 )),
7135 component_local,
7136 );
7137 star_pattern_index = pattern_index + 1;
7138 star_path_index = path_index;
7139 star_component_local = component_local;
7140 has_star = true;
7141 pattern_index += 1;
7142 continue;
7143 }
7144 ExtglobStep::Any | ExtglobStep::UnclosedGroup { byte: b'?' } => {
7145 if path.get(path_index).is_some_and(|&byte| {
7146 (!extglob_wildcard_component_local(steps, pattern_index, options)
7147 || !is_separator(byte))
7148 && (options.match_hidden
7149 || byte != b'.'
7150 || !at_component_start(path, path_index, options))
7151 }) {
7152 pattern_index += 1;
7153 path_index += 1;
7154 continue;
7155 }
7156 if path
7159 .get(path_index)
7160 .is_some_and(|&actual| bytes_equal(b'?', actual, options.case_insensitive))
7161 {
7162 pattern_index += 1;
7163 path_index += 1;
7164 continue;
7165 }
7166 }
7167 ExtglobStep::Class { class, next } => {
7168 if path.get(path_index).is_some_and(|&byte| {
7169 (!extglob_wildcard_component_local(steps, pattern_index, options)
7170 || !is_separator(byte))
7171 && (options.match_hidden
7172 || byte != b'.'
7173 || !at_component_start(path, path_index, options))
7174 && class.matches(byte, options.case_insensitive)
7175 }) {
7176 pattern_index = *next;
7177 path_index += 1;
7178 continue;
7179 }
7180 }
7181 ExtglobStep::Escape { escaped } => {
7182 if path
7183 .get(path_index)
7184 .is_some_and(|&byte| bytes_equal(*escaped, byte, options.case_insensitive))
7185 {
7186 pattern_index += 2;
7187 path_index += 1;
7188 continue;
7189 }
7190 }
7191 ExtglobStep::Byte(expected) | ExtglobStep::UnclosedGroup { byte: expected } => {
7192 if path.get(path_index).is_some_and(|&actual| {
7193 bytes_equal(*expected, actual, options.case_insensitive)
7194 }) {
7195 pattern_index += 1;
7196 path_index += 1;
7197 continue;
7198 }
7199 if path_index == path.len()
7202 && program.trailing_globstar_separator == Some(pattern_index)
7203 {
7204 return true;
7205 }
7206 }
7207 ExtglobStep::NoMatch => {}
7208 }
7209 }
7210
7211 if has_star
7212 && star_path_index < path.len()
7213 && !extglob_star_can_consume(path, star_path_index, options, star_component_local)
7214 && let Some((resume, consumed)) = crossing_star.take()
7215 {
7216 star_pattern_index = resume;
7217 star_path_index = consumed;
7218 star_component_local = false;
7219 }
7220 if has_star && star_path_index < path.len() {
7221 if !extglob_star_can_consume(path, star_path_index, options, star_component_local) {
7222 pattern_index = star_pattern_index;
7223 path_index = star_path_index;
7224 has_star = false;
7225 continue;
7226 }
7227 pattern_index = star_pattern_index;
7228 star_path_index += 1;
7229 path_index = star_path_index;
7230 continue;
7231 }
7232 return false;
7233 }
7234 true
7235}
7236
7237fn remember_crossing_star(
7244 crossing_star: &mut Option<(usize, usize)>,
7245 previous: Option<(usize, usize, bool)>,
7246 component_local: bool,
7247) {
7248 if !component_local {
7249 *crossing_star = None;
7250 } else if let Some((resume, consumed, false)) = previous {
7251 *crossing_star = Some((resume, consumed));
7252 }
7253}
7254
7255fn extglob_wildcard_component_local(
7256 steps: &[ExtglobStep],
7257 step_index: usize,
7258 options: PatternOptions,
7259) -> bool {
7260 options.component_wildcards
7261 && (options.root_component_wildcards || extglob_step_component_local(steps, step_index))
7262}
7263
7264fn extglob_star_component_local(
7274 steps: &[ExtglobStep],
7275 step_index: usize,
7276 options: PatternOptions,
7277) -> bool {
7278 match steps.get(step_index) {
7279 Some(ExtglobStep::Star { next, .. }) if *next - step_index >= 2 => {
7280 !(options.recursive_double_star
7281 && extglob_star_run_is_whole_component(steps, step_index, *next))
7282 && options.component_wildcards
7283 && options.root_component_wildcards
7284 }
7285 _ => extglob_wildcard_component_local(steps, step_index, options),
7286 }
7287}
7288
7289fn extglob_star_run_is_whole_component(steps: &[ExtglobStep], start: usize, end: usize) -> bool {
7295 let is_separator_step =
7296 |index: usize| matches!(steps.get(index), Some(ExtglobStep::Byte(b'/')));
7297 (start == 0 || is_separator_step(start - 1)) && (end == steps.len() || is_separator_step(end))
7298}
7299
7300fn extglob_star_can_consume(
7301 path: &[u8],
7302 path_index: usize,
7303 options: PatternOptions,
7304 component_local: bool,
7305) -> bool {
7306 let byte = path[path_index];
7307 (!component_local || !is_separator(byte))
7308 && (options.match_hidden || byte != b'.' || !at_component_start(path, path_index, options))
7309}
7310
7311fn extglob_group_allows_literal_leading_period(group: &ExtglobGroup) -> bool {
7316 if group.kind == ExtglobKind::Negated {
7319 return false;
7320 }
7321 group.alternatives.iter().any(|alternative| {
7322 alternative.compiled.iter().any(|alternative| {
7323 matches!(
7324 alternative.tokens.first(),
7325 Some(Token::Literal(literal)) if literal.first() == Some(&b'.')
7326 )
7327 })
7328 }) || group.through_separator.iter().any(|alternative| {
7329 alternative
7334 .compiled
7335 .iter()
7336 .any(|alternative| matches!(alternative.tokens.first(), Some(Token::RecursivePrefix)))
7337 })
7338}
7339
7340fn queue_extglob_group(
7341 program: &CompiledExtglob,
7342 group: &ExtglobGroup,
7343 path: &[u8],
7344 path_index: usize,
7345 options: PatternOptions,
7346 state: &mut ExtglobMatchState<'_>,
7347) {
7348 match group.kind {
7349 ExtglobKind::ExactlyOne => {
7350 matching_extglob_group_ends(
7351 group,
7352 path,
7353 path_index,
7354 options,
7355 state.prefix_sweep_state,
7356 state.ends,
7357 );
7358 queue_extglob_continuations(program, state, group.rest);
7359 queue_extglob_through_separator(
7360 program, group, path, path_index, false, options, state,
7361 );
7362 }
7363 ExtglobKind::Optional => {
7364 queue_extglob_continuation(program, state, group.rest, path_index);
7365 matching_extglob_group_ends(
7366 group,
7367 path,
7368 path_index,
7369 options,
7370 state.prefix_sweep_state,
7371 state.ends,
7372 );
7373 queue_extglob_continuations(program, state, group.rest);
7374 queue_extglob_through_separator(
7375 program, group, path, path_index, false, options, state,
7376 );
7377 }
7378 ExtglobKind::ZeroOrMore => {
7379 queue_extglob_continuation(program, state, group.rest, path_index);
7380 matching_extglob_repetition_ends(group, path, path_index, options, state);
7381 queue_extglob_continuations(program, state, group.rest);
7382 queue_extglob_through_separator(program, group, path, path_index, true, options, state);
7383 }
7384 ExtglobKind::OneOrMore => {
7385 matching_extglob_repetition_ends(group, path, path_index, options, state);
7386 queue_extglob_continuations(program, state, group.rest);
7387 queue_extglob_through_separator(program, group, path, path_index, true, options, state);
7388 }
7389 ExtglobKind::Negated => {
7390 let stop_limit = negated_extglob_stop_limit(
7391 path,
7392 path_index,
7393 extglob_component_end(path, path_index, options, group.component_local),
7394 options,
7395 );
7396 state.excluded.resize(stop_limit - path_index, false);
7397 state.excluded.fill(false);
7398 matching_extglob_group_ends(
7399 group,
7400 path,
7401 path_index,
7402 options,
7403 state.prefix_sweep_state,
7404 state.ends,
7405 );
7406 for &end in state.ends.iter() {
7409 if let Some(excluded) = state.excluded.get_mut(end - path_index) {
7410 *excluded = true;
7411 }
7412 }
7413 for offset in 0..state.excluded.len() {
7414 if !state.excluded[offset] {
7415 queue_extglob_continuation(program, state, group.rest, path_index + offset);
7416 }
7417 }
7418 }
7419 }
7420}
7421
7422fn queue_extglob_continuations(
7423 program: &CompiledExtglob,
7424 state: &mut ExtglobMatchState<'_>,
7425 rest: usize,
7426) {
7427 for index in 0..state.ends.len() {
7428 let end = state.ends[index];
7429 queue_extglob_continuation(program, state, rest, end);
7430 }
7431}
7432
7433fn queue_extglob_through_separator(
7443 program: &CompiledExtglob,
7444 group: &ExtglobGroup,
7445 path: &[u8],
7446 path_index: usize,
7447 repeated: bool,
7448 options: PatternOptions,
7449 state: &mut ExtglobMatchState<'_>,
7450) {
7451 if group.through_separator.is_empty() {
7452 return;
7453 }
7454 let resume = group.rest + 1;
7455 let later_root_local = group.later_iteration_root_local(options);
7456 let starts = if repeated { 1 + state.ends.len() } else { 1 };
7457 for start_index in 0..starts {
7458 let (start, root_component_local) = match start_index.checked_sub(1) {
7459 None => (path_index, group.component_local),
7460 Some(end_index) => (state.ends[end_index], later_root_local),
7461 };
7462 let span = AlternativeSpan {
7463 path,
7464 start,
7465 end_limit: group.alternative_end_limit(path, start, options),
7466 root_component_local,
7467 };
7468 state.candidate_ends.clear();
7469 for alternative in &group.through_separator {
7470 matching_extglob_alternative_ends(
7471 alternative,
7472 span,
7473 options,
7474 state.prefix_sweep_state,
7475 state.candidate_ends,
7476 );
7477 }
7478 for index in 0..state.candidate_ends.len() {
7479 let end = state.candidate_ends[index];
7480 queue_extglob_continuation(program, state, resume, end);
7481 }
7482 }
7483}
7484
7485fn queue_extglob_continuation(
7489 program: &CompiledExtglob,
7490 state: &mut ExtglobMatchState<'_>,
7491 start: usize,
7492 path_index: usize,
7493) {
7494 if state
7495 .failed
7496 .insert(program.memo_state_index(start), path_index)
7497 {
7498 state.pending.push((start, path_index));
7499 #[cfg(test)]
7500 {
7501 *state.pending_peak = (*state.pending_peak).max(state.pending.len());
7502 }
7503 }
7504}
7505
7506fn matching_extglob_group_ends(
7507 group: &ExtglobGroup,
7508 path: &[u8],
7509 path_index: usize,
7510 options: PatternOptions,
7511 prefix_sweep_state: &mut Option<SweepState>,
7512 output: &mut Vec<usize>,
7513) {
7514 output.clear();
7515 let span = AlternativeSpan {
7516 path,
7517 start: path_index,
7518 end_limit: group.alternative_end_limit(path, path_index, options),
7519 root_component_local: group.component_local,
7520 };
7521 for alternative in group.matched_alternatives() {
7522 matching_extglob_alternative_ends(alternative, span, options, prefix_sweep_state, output);
7523 }
7524 output.sort_unstable();
7525 output.dedup();
7526}
7527
7528fn matching_extglob_repetition_ends(
7535 group: &ExtglobGroup,
7536 path: &[u8],
7537 path_index: usize,
7538 options: PatternOptions,
7539 state: &mut ExtglobMatchState<'_>,
7540) {
7541 let component_end = group.alternative_end_limit(path, path_index, options);
7542 let position_count = component_end - path_index + 1;
7543 let first_iteration_apart = group.first_iteration_apart(options);
7550 let later_root_local = group.later_iteration_root_local(options);
7551 state.visited.clear();
7552 let reachable_base = push_visited(state.visited, position_count);
7553 let matched_base = push_visited(state.visited, position_count);
7554 visit(state.visited, reachable_base, 0);
7555 prepare_extglob_sweeps(group, state.sweep_states);
7556 for offset in 0..position_count {
7557 let absolute = path_index + offset;
7558 let root_component_local = if offset == 0 {
7559 group.component_local
7560 } else {
7561 later_root_local
7562 };
7563 if visited(state.visited, reachable_base, offset) {
7564 let mut sweep_index = 0;
7565 for alternative in group.matched_alternatives() {
7566 if let Some(width) = alternative.width {
7567 let Some(end) = absolute.checked_add(width) else {
7568 continue;
7569 };
7570 if end <= component_end
7571 && match_extglob_alternative_exact(
7572 alternative,
7573 &path[absolute..end],
7574 at_component_start(path, absolute, options),
7575 options,
7576 root_component_local,
7577 )
7578 {
7579 let reached = end - path_index;
7580 visit(state.visited, reachable_base, reached);
7581 visit(state.visited, matched_base, reached);
7582 }
7583 continue;
7584 }
7585 let enumerate_apart = offset == 0 && first_iteration_apart;
7586 let mut has_fallback = enumerate_apart;
7587 for compiled in &alternative.compiled {
7588 if let Some(sweep) = &compiled.sweep {
7589 let sweep_state = &mut state.sweep_states[sweep_index];
7590 sweep_index += 1;
7591 if enumerate_apart {
7592 continue;
7593 }
7594 sweep
7595 .inject_start(sweep_state, at_component_start(path, absolute, options));
7596 if sweep.accepts(sweep_state) {
7597 visit(state.visited, matched_base, offset);
7598 }
7599 } else {
7600 has_fallback = true;
7601 }
7602 }
7603 if has_fallback {
7604 state.candidate_ends.clear();
7605 matching_extglob_alternative_ends(
7606 alternative,
7607 AlternativeSpan {
7608 path,
7609 start: absolute,
7610 end_limit: component_end,
7611 root_component_local,
7612 },
7613 options,
7614 state.prefix_sweep_state,
7615 state.candidate_ends,
7616 );
7617 for &end in state.candidate_ends.iter() {
7618 let reached = end - path_index;
7619 visit(state.visited, reachable_base, reached);
7620 visit(state.visited, matched_base, reached);
7621 }
7622 }
7623 }
7624 debug_assert_eq!(
7625 sweep_index,
7626 state.sweep_states.len(),
7627 "prepared repetition sweeps must align with injected variable-width alternatives"
7628 );
7629 }
7630
7631 if absolute == component_end {
7632 break;
7633 }
7634 let byte = path[absolute];
7635 let starts_component = at_component_start(path, absolute, options);
7636 let mut sweep_index = 0;
7637 for alternative in group.matched_alternatives() {
7638 for compiled in &alternative.compiled {
7639 let Some(sweep) = &compiled.sweep else {
7640 continue;
7641 };
7642 let sweep_state = &mut state.sweep_states[sweep_index];
7643 sweep_index += 1;
7644 let alternative_options =
7645 extglob_alternative_options(path, absolute, options, later_root_local);
7646 if sweep.advance(sweep_state, byte, starts_component, alternative_options)
7647 && sweep.accepts(sweep_state)
7648 {
7649 visit(state.visited, reachable_base, offset + 1);
7650 visit(state.visited, matched_base, offset + 1);
7651 }
7652 }
7653 }
7654 debug_assert_eq!(
7655 sweep_index,
7656 state.sweep_states.len(),
7657 "prepared repetition sweep count must match the advance loop"
7658 );
7659 }
7660 state.ends.clear();
7661 state.ends.extend(
7662 (0..position_count)
7663 .filter(|&offset| visited(state.visited, matched_base, offset))
7664 .map(|offset| path_index + offset),
7665 );
7666}
7667
7668fn prepare_extglob_sweeps(group: &ExtglobGroup, states: &mut Vec<SweepState>) {
7669 let mut count = 0;
7670 for alternative in group.matched_alternatives() {
7671 for compiled in &alternative.compiled {
7672 let Some(sweep) = &compiled.sweep else {
7673 continue;
7674 };
7675 if let Some(state) = states.get_mut(count) {
7676 sweep.reset_state(state);
7677 } else {
7678 states.push(sweep.empty_state());
7679 }
7680 count += 1;
7681 }
7682 }
7683 states.truncate(count);
7684}
7685
7686#[derive(Clone, Copy)]
7688struct AlternativeSpan<'a> {
7689 path: &'a [u8],
7690 start: usize,
7691 end_limit: usize,
7693 root_component_local: bool,
7696}
7697
7698fn matching_extglob_alternative_ends(
7699 alternative: &ExtglobAlternative,
7700 span: AlternativeSpan<'_>,
7701 options: PatternOptions,
7702 prefix_sweep_state: &mut Option<SweepState>,
7703 output: &mut Vec<usize>,
7704) {
7705 let AlternativeSpan {
7706 path,
7707 start: path_index,
7708 end_limit: component_end,
7709 root_component_local,
7710 } = span;
7711 if let Some(width) = alternative.width {
7712 let Some(end) = path_index.checked_add(width) else {
7713 return;
7714 };
7715 if end <= component_end
7716 && match_extglob_alternative_exact(
7717 alternative,
7718 &path[path_index..end],
7719 at_component_start(path, path_index, options),
7720 options,
7721 root_component_local,
7722 )
7723 {
7724 output.push(end);
7725 }
7726 return;
7727 }
7728
7729 let alternative_options =
7730 extglob_alternative_options(path, path_index, options, root_component_local);
7731 let suffix = &path[path_index..component_end];
7732 for compiled in &alternative.compiled {
7733 if let Some(sweep) = &compiled.sweep {
7734 sweep.matching_prefix_ends(
7735 suffix,
7736 alternative_options,
7737 path_index,
7738 prefix_sweep_state,
7739 output,
7740 );
7741 } else {
7742 for end in path_index..=component_end {
7743 if Pattern::match_alternatives(
7744 std::slice::from_ref(compiled),
7745 alternative_options,
7746 &path[path_index..end],
7747 ) {
7748 output.push(end);
7749 }
7750 }
7751 }
7752 }
7753}
7754
7755fn extglob_alternative_options(
7756 path: &[u8],
7757 path_index: usize,
7758 options: PatternOptions,
7759 root_component_local: bool,
7760) -> PatternOptions {
7761 PatternOptions {
7762 braces: false,
7763 extglob: false,
7764 candidate_starts_component: at_component_start(path, path_index, options),
7765 candidate_root_component_wildcard: root_component_local,
7766 ..options
7767 }
7768}
7769
7770fn match_extglob_alternative_exact(
7776 alternative: &ExtglobAlternative,
7777 path: &[u8],
7778 candidate_starts_component: bool,
7779 options: PatternOptions,
7780 root_component_local: bool,
7781) -> bool {
7782 if alternative.width.is_some_and(|width| width != path.len()) {
7783 return false;
7784 }
7785 let mut options = extglob_alternative_options(path, 0, options, root_component_local);
7786 options.candidate_starts_component = candidate_starts_component;
7787 Pattern::match_alternatives(&alternative.compiled, options, path)
7788}
7789
7790fn negated_extglob_stop_limit(
7801 path: &[u8],
7802 path_index: usize,
7803 component_end: usize,
7804 options: PatternOptions,
7805) -> usize {
7806 (path_index..component_end)
7807 .find(|&index| star_stops_before_hidden_component(path, index, options))
7808 .unwrap_or(component_end + 1)
7809}
7810
7811fn extglob_component_end(
7812 path: &[u8],
7813 path_index: usize,
7814 options: PatternOptions,
7815 position_component_local: bool,
7816) -> usize {
7817 if options.component_wildcards && (options.root_component_wildcards || position_component_local)
7818 {
7819 path_index
7820 + path[path_index..]
7821 .iter()
7822 .position(|byte| is_separator(*byte))
7823 .unwrap_or(path.len() - path_index)
7824 } else {
7825 path.len()
7826 }
7827}
7828
7829impl CompiledExtglob {
7830 fn memo_state_index(&self, offset: usize) -> usize {
7831 let state_index = self.memo_state_indices[offset];
7832 debug_assert_ne!(
7833 state_index,
7834 usize::MAX,
7835 "only the entry point and group transitions participate in extglob recursion"
7836 );
7837 state_index
7838 }
7839}
7840
7841#[cfg(test)]
7842mod tests {
7843 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
7844 use super::LiteralSuffix;
7845 use super::{
7846 AlternativeFastPath, ComponentBounds, ExtglobStep, FailedStates, FastPath, IrBudget,
7847 Pattern, PatternOptions, Prefilter, ProvenanceBudget, SweepEngine, Token,
7848 WalkerPathViability, extglob_failed_len, extglob_failed_stats, extglob_pending_peak,
7849 extglob_scratch_capacities, positive_extglob_scratch_capacities, scratch_capacities,
7850 sweep_is_deferred,
7851 };
7852
7853 fn compile(pattern: &str) -> Pattern {
7854 Pattern::compile(pattern, PatternOptions::default()).unwrap()
7855 }
7856
7857 #[test]
7858 fn compiled_patterns_stay_shareable_across_threads() {
7859 fn assert_send_sync<T: Send + Sync>() {}
7860 assert_send_sync::<Pattern>();
7861 assert_send_sync::<PatternOptions>();
7862 }
7863
7864 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
7865 #[test]
7866 fn apple_silicon_only_packs_short_case_sensitive_suffixes() {
7867 assert!(matches!(
7868 LiteralSuffix::new(b".ts".to_vec(), false),
7869 LiteralSuffix::Packed16(_)
7870 ));
7871 assert!(matches!(
7872 LiteralSuffix::new(b".ts".to_vec(), true),
7873 LiteralSuffix::Plain(_)
7874 ));
7875 assert!(matches!(
7876 LiteralSuffix::new(vec![b'x'; 17], false),
7877 LiteralSuffix::Plain(_)
7878 ));
7879 }
7880
7881 #[test]
7882 fn repeated_general_matches_reuse_the_thread_local_scratch() {
7883 let mut pattern = Pattern::compile(
7886 "**/*.ts",
7887 PatternOptions::default().recursive_double_star(true),
7888 )
7889 .unwrap();
7890 pattern.strip_engines(true, true, false);
7894 let matching = "src/deep/nested/module/component/widget/main.ts";
7895 let other = "src/deep/nested/module/component/widget/main.tsx";
7896
7897 for _ in 0..8 {
7898 assert!(pattern.is_match_glob_path(matching));
7899 assert!(!pattern.is_match_glob_path(other));
7900 }
7901 let warm = scratch_capacities();
7902 assert!(
7903 warm.0 > 0 && warm.1 > 0 && warm.2 > 0,
7904 "this shape must leave the inline budget and use every buffer: {warm:?}"
7905 );
7906
7907 for _ in 0..1_000 {
7908 assert!(pattern.is_match_glob_path(matching));
7909 assert!(!pattern.is_match_glob_path(other));
7910 }
7911 assert_eq!(
7912 scratch_capacities(),
7913 warm,
7914 "steady-state matching must not grow a scratch buffer"
7915 );
7916
7917 let long = "x".repeat(300_000);
7919 let mut oversized = Pattern::compile("*a*y", PatternOptions::default()).unwrap();
7920 oversized.strip_engines(false, true, true);
7921 assert!(!oversized.is_match(&long));
7922 assert!(
7923 scratch_capacities().0 <= super::RETAINED_SCRATCH_WORDS,
7924 "an oversized candidate must release the matrix it needed"
7925 );
7926 }
7927
7928 #[test]
7929 fn literal_skipping_honours_component_and_leading_dot_policies() {
7930 let component = Pattern::compile(
7932 "*b.ts",
7933 PatternOptions {
7934 component_wildcards: true,
7935 root_component_wildcards: true,
7936 ..PatternOptions::default()
7937 },
7938 )
7939 .unwrap();
7940 assert!(component.is_match("ab.ts"));
7941 assert!(!component.is_match("a/b.ts"));
7942
7943 assert!(!compile("*b.ts").is_match("a/.b.ts"));
7945 assert!(
7946 Pattern::compile("*b.ts", PatternOptions::default().match_hidden(true))
7947 .unwrap()
7948 .is_match("a/.b.ts")
7949 );
7950 assert!(!compile("*.ts").is_match(".hidden.ts"));
7951
7952 assert!(compile("*b.ts").is_match("bb.ts"));
7955 assert!(!compile("*x.ts").is_match(".a/x.ts"));
7956 }
7957
7958 #[test]
7962 fn walker_preset_enables_exactly_the_walker_syntax() {
7963 assert_eq!(
7964 PatternOptions::walker(),
7965 PatternOptions::default()
7966 .recursive_double_star(true)
7967 .braces(true)
7968 .extglob(true)
7969 );
7970 }
7971
7972 #[test]
7973 fn matches_literals_and_component_wildcards() {
7974 assert!(compile("src/*.rs").is_match("src/lib.rs"));
7975 assert!(compile("src/?.rs").is_match("src/a.rs"));
7976 assert!(compile("src/*.rs").is_match("src/bin/main.rs"));
7977 assert!(compile("*.rs").is_match("lib.rs"));
7978
7979 let star = compile("*");
7980 assert!(star.is_match("d/e/a.c"));
7981 assert!(star.is_match_path("d/e/a.c"));
7982 assert!(!star.is_match_glob_path("d/e/a.c"));
7983 assert!(compile("?").is_match("/"));
7984 }
7985
7986 #[test]
7987 fn leading_period_requires_an_explicit_option_or_literal() {
7988 assert!(!compile("*").is_match(".gitignore"));
7989 assert!(!compile("*.rs").is_match(".rs"));
7990 assert!(compile(".*").is_match(".gitignore"));
7991 assert!(
7992 Pattern::compile("*", PatternOptions::default().match_hidden(true))
7993 .unwrap()
7994 .is_match(".gitignore")
7995 );
7996 assert!(
7997 Pattern::compile("*.rs", PatternOptions::default().match_hidden(true))
7998 .unwrap()
7999 .is_match(".rs")
8000 );
8001 }
8002
8003 #[test]
8007 fn extglob_escapes_match_only_the_escaped_byte() {
8008 let options = PatternOptions::default().extglob(true);
8009 for (pattern, candidate, expected, positive_nfa) in [
8013 ("\\*@(a)", "*a", true, Some(true)),
8014 ("\\*@(a)", "\\xa", false, Some(true)),
8015 ("\\*@(a)", "xa", false, Some(true)),
8016 ("*(a)\\*", "aa*", true, Some(true)),
8017 ("*(a)\\*", "*", true, Some(true)),
8018 ("*(a)\\*", "\\x)\\", false, Some(true)),
8019 ("@(a)\\/b", "a/b", true, Some(true)),
8020 ("@(a)\\/b", "a\\/b", false, Some(true)),
8021 ("*\\*@(a)", "x*a", true, Some(false)),
8024 ("*\\*@(a)", "*a", true, Some(false)),
8025 ("*\\*@(a)", "x\\xa", false, Some(false)),
8026 ("!(b)\\*", "a*", true, Some(false)),
8027 ("!(b)\\*", "\\x", false, Some(false)),
8028 ("!(b)\\*", "a\\x", false, Some(false)),
8029 ("\\*(a)", "*(a)", true, None),
8030 ("\\*(a)", "\\a", false, None),
8031 ] {
8032 let compiled = Pattern::compile(pattern, options).expect("pattern compiles");
8033 assert_eq!(
8034 compiled.alternatives[0]
8035 .extglob
8036 .as_ref()
8037 .map(|program| program.positive_nfa.is_some()),
8038 positive_nfa,
8039 "{pattern}: engine selection"
8040 );
8041 assert_eq!(
8042 compiled.is_match(candidate),
8043 expected,
8044 "{pattern} against {candidate}"
8045 );
8046 assert!(
8047 compiled.engines_agree(candidate),
8048 "{pattern} against {candidate}: engines disagree"
8049 );
8050 }
8051 }
8052
8053 #[test]
8061 fn only_a_wildcard_directly_behind_a_separator_is_component_local() {
8062 let options = PatternOptions::default();
8063 let local = Pattern::compile("a/*/z", options).unwrap();
8064 assert!(local.is_match_path("a/b/z"));
8065 assert!(
8066 !local.is_match_path("a/b/c/z"),
8067 "a/*/z stays inside one component"
8068 );
8069 for pattern in ["a/**/z", "a/?*/z", "a/*[a-z]*/z", "a/b*/z"] {
8070 let compiled = Pattern::compile(pattern, options).unwrap();
8071 assert!(compiled.is_match("a/b/c/z"), "{pattern}: is_match crosses");
8072 assert!(compiled.is_match_path("a/b/z"), "{pattern}: one component");
8073 assert!(
8074 compiled.is_match_path("a/b/c/z"),
8075 "{pattern}: the wildcard behind the first one crosses under is_match_path, as in zlob's matchPaths"
8076 );
8077 assert!(
8078 !compiled.is_match_glob_path("a/b/c/z"),
8079 "{pattern}: the glob-path reading is component-local throughout"
8080 );
8081 for candidate in ["a/b/z", "a/b/c/z"] {
8082 assert!(
8083 compiled.engines_agree(candidate),
8084 "{pattern}: engines disagree on {candidate}"
8085 );
8086 }
8087 }
8088 }
8089
8090 #[test]
8093 fn zero_width_extglob_branches_keep_the_leading_period_rule() {
8094 let walker_options = PatternOptions::default()
8095 .braces(true)
8096 .recursive_double_star(true)
8097 .extglob(true);
8098 let zero_width = Pattern::compile("**/?(visible).hidden/keep.txt", walker_options)
8099 .expect("pattern compiles");
8100 assert!(!zero_width.is_match("target/.hidden/keep.txt"));
8101 let explicitly_hidden = Pattern::compile("**/?(.visible).hidden/keep.txt", walker_options)
8102 .expect("pattern compiles");
8103 assert!(explicitly_hidden.is_match("target/.hidden/keep.txt"));
8104 }
8105
8106 #[test]
8107 fn recursive_double_star_is_explicit_and_component_aware() {
8108 let options = PatternOptions::default().recursive_double_star(true);
8109 let pattern = Pattern::compile("**/*.rs", options).unwrap();
8110 assert!(pattern.is_match("lib.rs"));
8111 assert!(pattern.is_match("src/bin/main.rs"));
8112 assert!(!pattern.is_match("src/.private.rs"));
8113 assert!(Pattern::compile("src/**", options).unwrap().is_match("src"));
8114 assert!(compile("**/*.rs").is_match("src/main.rs"));
8115 assert!(compile("**/*.rs").is_match("src/bin/main.rs"));
8116 }
8117
8118 #[test]
8119 fn long_candidates_do_not_overflow_the_native_stack() {
8120 std::thread::Builder::new()
8125 .stack_size(256 * 1024)
8126 .spawn(|| {
8127 let filler = "x".repeat(300_000);
8128 let pattern = compile("*a*y");
8129 assert!(!pattern.is_match(&filler));
8130
8131 let mut matching = String::with_capacity(filler.len() + 2);
8132 matching.push_str(&filler[..150_000]);
8133 matching.push('a');
8134 matching.push_str(&filler[150_000..]);
8135 matching.push('y');
8136 assert!(pattern.is_match(&matching));
8137
8138 assert!(pattern.is_match_glob_path(&matching));
8141 assert!(!pattern.is_match_glob_path(&filler));
8142
8143 let recursive = Pattern::compile(
8144 "**/*a*.rs",
8145 PatternOptions::default().recursive_double_star(true),
8146 )
8147 .unwrap();
8148 let mut deep = "dir/".repeat(20_000);
8149 deep.push_str("zzz.rs");
8150 assert!(!recursive.is_match_glob_path(&deep));
8151 deep.truncate(deep.len() - "zzz.rs".len());
8152 deep.push_str("zaz.rs");
8153 assert!(recursive.is_match_glob_path(&deep));
8154 })
8155 .expect("spawn a small-stack matcher worker")
8156 .join()
8157 .expect("the matcher must not overflow a 256 KiB stack");
8158 }
8159
8160 #[test]
8161 fn character_classes_support_ranges_and_negation() {
8162 assert!(compile("file[0-9].rs").is_match("file7.rs"));
8163 assert!(compile("file[!0-9].rs").is_match("filex.rs"));
8164 assert!(compile("file[^0-9].rs").is_match("filex.rs"));
8165 assert!(!compile("file[!0-9].rs").is_match("file7.rs"));
8166 }
8167
8168 #[test]
8169 fn escaped_dash_in_a_class_is_a_literal_member() {
8170 let pattern = compile(r"[a\-z]");
8171 assert!(pattern.is_match("a"));
8172 assert!(pattern.is_match("-"));
8173 assert!(pattern.is_match("z"));
8174 assert!(!pattern.is_match("b"));
8175 assert!(!pattern.is_match("y"));
8176 assert!(!pattern.is_match("\\"));
8177 }
8178
8179 #[test]
8180 fn escaped_dash_stays_literal_without_escape_processing() {
8181 let options = PatternOptions::default().escape(false);
8182 let pattern = Pattern::compile(r"[a\-z]", options).unwrap();
8183 assert!(pattern.is_match("b"));
8184 assert!(pattern.is_match("\\"));
8185 assert!(!pattern.is_match("-"));
8186 }
8187
8188 #[test]
8189 fn escaped_class_members_still_bound_ranges() {
8190 let low = compile(r"[\--0]");
8191 assert!(low.is_match("-"));
8192 assert!(low.is_match("0"));
8193 assert!(!low.is_match("a"));
8194 assert!(!low.is_match("\\"));
8195
8196 let reversed = compile(r"[a-\-]");
8197 assert!(!reversed.is_match("a"));
8198 assert!(!reversed.is_match("-"));
8199 assert!(!reversed.is_match("\\"));
8200
8201 let escaped_start = compile(r"[\a-z]");
8202 assert!(escaped_start.is_match("a"));
8203 assert!(escaped_start.is_match("b"));
8204 assert!(escaped_start.is_match("z"));
8205 assert!(!escaped_start.is_match("-"));
8206 }
8207
8208 #[test]
8209 fn unescaped_edge_dashes_stay_literal() {
8210 let trailing = compile("[a-]");
8211 assert!(trailing.is_match("a"));
8212 assert!(trailing.is_match("-"));
8213 assert!(!trailing.is_match("b"));
8214
8215 let leading = compile("[-a]");
8216 assert!(leading.is_match("a"));
8217 assert!(leading.is_match("-"));
8218 assert!(!leading.is_match("b"));
8219
8220 let both = compile("[-a-c-]");
8221 assert!(both.is_match("-"));
8222 assert!(both.is_match("b"));
8223 assert!(!both.is_match("d"));
8224 }
8225
8226 #[test]
8227 fn negated_classes_honour_escaped_dashes() {
8228 let pattern = compile(r"[!a\-z]");
8229 assert!(!pattern.is_match("a"));
8230 assert!(!pattern.is_match("-"));
8231 assert!(!pattern.is_match("z"));
8232 assert!(pattern.is_match("b"));
8233 }
8234
8235 #[test]
8236 fn character_classes_support_posix_named_sets() {
8237 assert!(compile("[[:alpha:]]").is_match("a"));
8238 assert!(!compile("[[:alpha:]]").is_match("7"));
8239 assert!(compile("[[:digit:]]").is_match("7"));
8240 assert!(compile("[[:word:]]").is_match("_"));
8241 assert!(compile("[![:space:]]").is_match("x"));
8242 }
8243
8244 #[test]
8245 fn case_folding_makes_upper_and_lower_classes_ascii_alphabetic() {
8246 let folded = PatternOptions::default().case_insensitive(true);
8247 for class in ["lower", "upper"] {
8248 let pattern = Pattern::compile(format!("[[:{class}:]]"), folded).unwrap();
8249 assert!(pattern.is_match("a"));
8250 assert!(pattern.is_match("A"));
8251 assert!(!pattern.is_match("1"));
8252 }
8253
8254 let not_upper = Pattern::compile("[![:upper:]]", folded).unwrap();
8255 assert!(!not_upper.is_match("A"));
8256 assert!(not_upper.is_match("1"));
8257
8258 let path = Pattern::compile("src/[[:upper:]]*.rs", folded).unwrap();
8259 assert!(path.is_match("src/Main.rs"));
8260
8261 assert!(compile("[[:upper:]]").is_match("A"));
8262 assert!(!compile("[[:upper:]]").is_match("a"));
8263 assert!(compile("[[:lower:]]").is_match("a"));
8264 assert!(!compile("[[:lower:]]").is_match("A"));
8265 }
8266
8267 #[test]
8271 fn walker_empty_component_is_located_at_its_separator() {
8272 let options = PatternOptions::default()
8273 .braces(true)
8274 .recursive_double_star(true)
8275 .extglob(true);
8276 for (source, offset) in [
8277 ("/bar", 0),
8278 ("src//*.ts", 4),
8279 ("a///b", 2),
8280 ("**//x", 3),
8281 ("src/", 3),
8282 ("src/{}/bar", 6),
8283 ("{a,b}//x", 6),
8284 ("src/{,./a.rs}/bar", 13),
8285 ("src/?()/bar", 7),
8286 ("src/@(a/)/bar", 9),
8287 ("src/@(a/|b/)", 7),
8288 ] {
8289 let compiled = Pattern::compile(source, options).expect("pattern compiles");
8290 assert_eq!(
8291 compiled.walker_path_viability(),
8292 WalkerPathViability::EmptyComponent,
8293 "{source}"
8294 );
8295 assert_eq!(
8296 compiled.walker_path_problem_offset(),
8297 Some(offset),
8298 "{source}"
8299 );
8300 assert_eq!(
8301 source.as_bytes()[offset],
8302 b'/',
8303 "{source} points at a separator"
8304 );
8305 }
8306
8307 let arms = (0..1000)
8311 .map(|index| format!("a{index}/"))
8312 .collect::<Vec<_>>()
8313 .join("|");
8314 let empty_arms = (0..1000)
8315 .map(|index| format!("a{index}//"))
8316 .collect::<Vec<_>>()
8317 .join("|");
8318 for source in [
8319 format!("*({arms})x"),
8320 format!("@({arms})@({arms})x"),
8321 format!("*({empty_arms})x"),
8322 ] {
8323 assert_eq!(
8324 Pattern::compile(&source, options)
8325 .expect("a wide group stays within the IR budget")
8326 .walker_path_viability(),
8327 WalkerPathViability::Viable
8328 );
8329 }
8330 }
8331
8332 #[test]
8333 fn brace_alternatives_carry_the_source_offset_of_their_first_byte() {
8334 let braces = PatternOptions::default().braces(true);
8335 let expand = |source: &str, options| {
8336 super::expand_braces_with_first_offsets(source, options)
8337 .expect("review pattern expands")
8338 .into_iter()
8339 .map(|(bytes, first)| (String::from_utf8(bytes).unwrap(), first))
8340 .collect::<Vec<_>>()
8341 };
8342
8343 assert_eq!(expand("!src/**", braces), [("!src/**".into(), Some(0))]);
8344 assert_eq!(
8345 expand("{!a,b}/**", braces),
8346 [("!a/**".into(), Some(1)), ("b/**".into(), Some(4))]
8347 );
8348 assert_eq!(
8349 expand("{a,}!b", braces),
8350 [("a!b".into(), Some(1)), ("!b".into(), Some(4))]
8351 );
8352 assert_eq!(
8353 expand("x{{!a,b},c}", braces),
8354 [
8355 ("x!a".into(), Some(0)),
8356 ("xb".into(), Some(0)),
8357 ("xc".into(), Some(0))
8358 ]
8359 );
8360 assert_eq!(
8361 expand("{{!a,b},c}", braces),
8362 [
8363 ("!a".into(), Some(2)),
8364 ("b".into(), Some(5)),
8365 ("c".into(), Some(8))
8366 ]
8367 );
8368 assert_eq!(
8369 expand("{,}", braces),
8370 [(String::new(), None), (String::new(), None)]
8371 );
8372 assert_eq!(
8373 expand("{!a,b}", PatternOptions::default()),
8374 [("{!a,b}".into(), Some(0))]
8375 );
8376 assert_eq!(
8377 super::expand_braces_with_first_offsets("{!a,b}/**", braces)
8378 .unwrap()
8379 .into_iter()
8380 .map(|(bytes, _)| bytes)
8381 .collect::<Vec<_>>(),
8382 super::expand_braces("{!a,b}/**", braces).unwrap(),
8383 "the alternatives are the public expansion's"
8384 );
8385 }
8386
8387 #[test]
8388 fn walker_path_viability_follows_compiled_alternatives() {
8389 let options = PatternOptions::default().braces(true).extglob(true);
8390 fn viability(source: &str, options: PatternOptions) -> WalkerPathViability {
8391 Pattern::compile(source, options)
8392 .expect("review pattern compiles")
8393 .walker_path_viability()
8394 }
8395
8396 let posix = Pattern::compile("src/[[:alpha:]/../].rs", options).unwrap();
8397 assert!(posix.is_match("src/a.rs"));
8398 assert_eq!(
8399 viability("src/[[:alpha:]/../].rs", options),
8400 WalkerPathViability::Viable
8401 );
8402
8403 let leading_bracket = Pattern::compile("src/[]/../].rs", options).unwrap();
8404 assert!(leading_bracket.is_match("src/].rs"));
8405 assert_eq!(
8406 viability("src/[]/../].rs", options),
8407 WalkerPathViability::Viable
8408 );
8409
8410 assert_eq!(
8414 viability("{dead/../branch,src/main.rs}", options),
8415 WalkerPathViability::ParentComponent
8416 );
8417 assert_eq!(
8418 viability("src/{a,..}", options),
8419 WalkerPathViability::ParentComponent
8420 );
8421
8422 assert_eq!(
8425 viability("@(dead/../branch|src/main.rs)", options),
8426 WalkerPathViability::Viable
8427 );
8428 assert_eq!(
8429 viability("{dead/{nested/../branch},src/main.rs}", options),
8430 WalkerPathViability::ParentComponent
8431 );
8432 assert_eq!(
8433 viability("@(dead/@(nested/../branch)|src/main.rs)", options),
8434 WalkerPathViability::Viable
8435 );
8436 assert_eq!(
8437 viability(r"dead\/../branch", options),
8438 WalkerPathViability::Viable
8439 );
8440
8441 let brace_class = Pattern::compile("src/[{],a}/../].rs", options).unwrap();
8442 assert!(brace_class.is_match("src/a.rs"));
8443 assert!(brace_class.is_match("src/].rs"));
8444 assert_eq!(
8445 brace_class.walker_path_viability(),
8446 WalkerPathViability::Viable
8447 );
8448
8449 let brace_extglob =
8450 Pattern::compile("@(dead/{),x}/../branch|src/main.rs)", options).unwrap();
8451 assert!(brace_extglob.is_match("src/main.rs"));
8452 assert_eq!(
8453 brace_extglob.walker_path_viability(),
8454 WalkerPathViability::Viable
8455 );
8456
8457 for source in [
8460 "{dead/[}]]/../x,src/main.rs}",
8461 "{dead/../branch}",
8462 "@(dead/../branch)",
8463 "@(dead|src)/../main.rs",
8464 ] {
8465 assert_eq!(
8466 viability(source, options),
8467 WalkerPathViability::ParentComponent,
8468 "all compiled arms remain unwalkable: {source}"
8469 );
8470 }
8471 }
8472
8473 #[test]
8477 fn a_leading_dot_slash_is_read_per_brace_alternative() {
8478 let options = PatternOptions::default()
8479 .braces(true)
8480 .extglob(true)
8481 .recursive_double_star(true);
8482 let compile = |source: &str| Pattern::compile(source, options).expect("valid pattern");
8483
8484 for source in [
8485 "{./src/*.rs,./lib/*.rs}",
8486 "{./src/*.rs,lib/*.rs}",
8487 "{./a.rs}",
8488 "{x,}./src/*.rs",
8489 "{{./src,lib}/*.rs,x}",
8490 "./{src,lib}/*.rs",
8491 ] {
8492 assert_eq!(
8493 compile(source).walker_path_viability(),
8494 WalkerPathViability::Viable,
8495 "{source}"
8496 );
8497 }
8498 for (source, offset) in [
8500 ("{././a,././b}", 3),
8501 ("src/{./a,./b}", 5),
8502 ("{./src/./a,./lib/./b}", 7),
8503 ] {
8504 let pattern = compile(source);
8505 assert_eq!(
8506 pattern.walker_path_viability(),
8507 WalkerPathViability::DotComponent,
8508 "{source}"
8509 );
8510 assert_eq!(
8511 pattern.walker_path_problem_offset(),
8512 Some(offset),
8513 "{source}"
8514 );
8515 }
8516
8517 for (source, path, expected) in [
8518 ("{./src/*.rs,lib/*.rs}", "src/m.rs", true),
8519 ("{./src/*.rs,lib/*.rs}", "lib/n.rs", true),
8520 ("{./src/*.rs,./lib/*.rs}", "lib/n.rs", true),
8521 ("{./src/*.rs,lib/*.rs}", "src/a/m.rs", true),
8522 ("{./@(src|lib)/*.rs,x}", "lib/a/n.rs", true),
8523 ("{x,}./src/*", "src/m.rs", true),
8524 ("{./src,lib}", "src", true),
8525 ("./{./src/*.rs,lib/*.rs}", "src/m.rs", false),
8526 ("./{./src/*.rs,lib/*.rs}", "lib/n.rs", true),
8527 ("src/{./a,b}/*.rs", "src/a/m.rs", false),
8528 ("{./src/*.rs,lib/*.rs}", "docs/d.md", false),
8529 ] {
8530 let pattern = compile(source);
8531 assert_eq!(
8532 pattern.is_match_crossing_path(path),
8533 expected,
8534 "{source} vs {path}"
8535 );
8536 if !path.contains("/a/") {
8539 assert_eq!(
8540 pattern.is_match_glob_path(path),
8541 expected,
8542 "{source} vs {path}"
8543 );
8544 }
8545 }
8546 let extglob = compile("@(./a.rs)");
8549 assert!(!extglob.is_match_glob_path("a.rs"));
8550 assert!(!extglob.is_match_crossing_path("a.rs"));
8551 let plain = compile("src/*.rs");
8553 assert!(plain.is_match_crossing_path("src/a/m.rs"));
8554 assert!(!plain.is_match_glob_path("src/a/m.rs"));
8555 }
8556
8557 #[test]
8558 fn walker_path_viability_composes_extglob_quantifiers_without_state_products() {
8559 let options = PatternOptions::default().braces(true).extglob(true);
8560 let mandatory =
8561 Pattern::compile("+(dead/../branch)", options).expect("mandatory extglob compiles");
8562 assert!(mandatory.is_match("dead/../branch"));
8563 assert_eq!(
8564 mandatory.walker_path_viability(),
8565 WalkerPathViability::ParentComponent,
8566 "one required repetition preserves its real parent component"
8567 );
8568 assert_eq!(
8569 Pattern::compile("prefix+(../bar)", options)
8570 .expect("prefixed repetition compiles")
8571 .walker_path_viability(),
8572 WalkerPathViability::Viable,
8573 "the compiler composes an arm with its outer component"
8574 );
8575
8576 for source in [
8577 "?(dead/../branch)",
8578 "*(dead/../branch)",
8579 "+(dead/../branch)",
8580 "@(dead/../branch)",
8581 ] {
8582 assert_ne!(
8583 Pattern::compile(source, options)
8584 .expect("quantified extglob compiles")
8585 .walker_path_viability(),
8586 WalkerPathViability::Viable,
8587 "every matching repetition of {source} remains unwalkable"
8588 );
8589 }
8590 assert_eq!(
8591 Pattern::compile("!(dead/../branch)", options)
8592 .expect("negated extglob compiles")
8593 .walker_path_viability(),
8594 WalkerPathViability::Viable
8595 );
8596
8597 for source in [
8603 "@(./a.rs)",
8604 "src/?(./a.rs)",
8605 "src/*(./a.rs)",
8606 "src/?(./a.rs)/bar",
8607 "src/*(./a.rs)/bar",
8608 ] {
8609 assert_eq!(
8610 Pattern::compile(source, options)
8611 .expect("edge-normalization regression compiles")
8612 .walker_path_viability(),
8613 WalkerPathViability::DotComponent,
8614 "{source} has no selectable walker candidate"
8615 );
8616 }
8617 for source in ["src/?()/bar", "src/*()/bar", "?()/bar", "*()/bar"] {
8618 assert_eq!(
8619 Pattern::compile(source, options)
8620 .expect("empty extglob arm compiles")
8621 .walker_path_viability(),
8622 WalkerPathViability::EmptyComponent,
8623 "{source} has only an empty leading or interior component"
8624 );
8625 }
8626 for source in ["src//bar", "/bar", "src/{}/bar", "src/{,./a.rs}/bar"] {
8627 assert_eq!(
8628 Pattern::compile(source, options)
8629 .expect("empty plain or brace arm compiles")
8630 .walker_path_viability(),
8631 WalkerPathViability::EmptyComponent,
8632 "{source} has no selectable complete walker alternative"
8633 );
8634 }
8635 for source in ["", ".", "./", "//"] {
8638 assert_eq!(
8639 Pattern::compile(source, options)
8640 .expect("root spelling compiles")
8641 .walker_path_viability(),
8642 WalkerPathViability::Root,
8643 "{source} names only the walk root"
8644 );
8645 }
8646 for source in ["src/?(x)/bar", "src/?()bar", "?(x)/bar"] {
8647 assert_eq!(
8648 Pattern::compile(source, options)
8649 .expect("selectable nullable control compiles")
8650 .walker_path_viability(),
8651 WalkerPathViability::Viable,
8652 "{source} retains a selectable compiler arm"
8653 );
8654 }
8655 assert_eq!(
8656 Pattern::compile("./a.rs", options)
8657 .expect("raw normalized spelling compiles")
8658 .walker_path_viability(),
8659 WalkerPathViability::Viable
8660 );
8661
8662 let sequential = "@(a|b)".repeat(32);
8666 let compiled = Pattern::compile(&sequential, options)
8667 .expect("sequential equivalent extglobs stay within the IR budget");
8668 assert_eq!(
8669 compiled.walker_path_viability(),
8670 WalkerPathViability::Viable
8671 );
8672
8673 let nested = "@(@(a|b)|@(c|d))".repeat(16);
8676 Pattern::compile(&nested, options)
8677 .expect("nested equivalent extglobs stay within the IR budget");
8678 }
8679
8680 #[test]
8681 fn braces_expand_nested_and_empty_alternatives() {
8682 let options = PatternOptions::default().braces(true);
8683 let pattern = Pattern::compile("{src,{lib,bin}}/*.{rs,toml}", options).unwrap();
8684 assert!(pattern.is_match("src/main.rs"));
8685 assert!(pattern.is_match("lib/Cargo.toml"));
8686 assert!(pattern.is_match("bin/main.rs"));
8687 assert!(!pattern.is_match("tests/main.rs"));
8688 assert!(
8689 Pattern::compile("test{,_suffix}.txt", options)
8690 .unwrap()
8691 .is_match("test_suffix.txt")
8692 );
8693 }
8694
8695 #[test]
8696 fn brace_expansion_keeps_its_order() {
8697 assert_eq!(
8700 super::expand_brace_alternatives(b"{a,b}{c,d}", true).unwrap(),
8701 vec![
8702 b"ac".to_vec(),
8703 b"ad".to_vec(),
8704 b"bc".to_vec(),
8705 b"bd".to_vec()
8706 ]
8707 );
8708 assert_eq!(
8709 super::expand_brace_alternatives(b"{x,{y,z}}!", true).unwrap(),
8710 vec![b"x!".to_vec(), b"y!".to_vec(), b"z!".to_vec()]
8711 );
8712 }
8713
8714 #[test]
8715 fn brace_provenance_stays_span_bounded_for_large_and_branching_sources() {
8716 let mut provenance_budget = super::ProvenanceBudget::new();
8717 let literal = vec![b'x'; 1 << 20];
8718 let expanded = super::expand_brace_alternatives_with_provenance(
8719 &literal,
8720 Some(&super::SourceProvenance::Contiguous { source_start: 0 }),
8721 true,
8722 &mut provenance_budget,
8723 )
8724 .expect("large literal has no provenance allocation per byte");
8725 assert!(matches!(
8726 expanded[0].source_provenance,
8727 Some(super::SourceProvenance::Contiguous { source_start: 0 })
8728 ));
8729
8730 let branching = "{a,b}".repeat(12);
8731 let expanded = super::expand_brace_alternatives_with_provenance(
8732 branching.as_bytes(),
8733 Some(&super::SourceProvenance::Contiguous { source_start: 0 }),
8734 true,
8735 &mut provenance_budget,
8736 )
8737 .expect("bounded brace expansion compiles");
8738 assert_eq!(expanded.len(), 1 << 12);
8739 let span_count = expanded
8740 .iter()
8741 .map(|alternative| match &alternative.source_provenance {
8742 Some(super::SourceProvenance::Spans(spans)) => spans.len(),
8743 Some(super::SourceProvenance::Contiguous { .. }) | None => 0,
8744 })
8745 .sum::<usize>();
8746 assert!(
8747 span_count <= 12 * (1 << 12),
8748 "one compact source span per selected brace arm, never one usize per output byte"
8749 );
8750
8751 let mut capped = super::ProvenanceBudget::new();
8752 assert!(
8753 capped.charge(super::MAX_BRACE_EXPANSION_BYTES, 0).is_err(),
8754 "provenance rejects before a span vector can exceed its byte bound"
8755 );
8756 }
8757
8758 #[test]
8759 fn brace_expansion_stops_at_the_alternative_budget() {
8760 let options = PatternOptions::default().braces(true);
8761 let within = "{a,b}".repeat(12);
8763 assert_eq!(
8764 super::expand_brace_alternatives(within.as_bytes(), true)
8765 .unwrap()
8766 .len(),
8767 super::MAX_BRACE_ALTERNATIVES
8768 );
8769 assert!(Pattern::compile(&within, options).is_ok());
8770
8771 let beyond = "{a,b}".repeat(13);
8772 let error = Pattern::compile(&beyond, options).unwrap_err();
8773 assert_eq!(error.message(), "too many brace alternatives");
8774 assert_eq!(error.offset(), 0);
8775
8776 for groups in [5, 10] {
8778 let pattern = "{,,,,,,,,}".repeat(groups);
8779 let error = Pattern::compile(&pattern, options).unwrap_err();
8780 assert_eq!(error.message(), "too many brace alternatives");
8781 }
8782
8783 let error = Pattern::compile(format!("src/{beyond}"), options).unwrap_err();
8785 assert_eq!(error.offset(), 4);
8786
8787 assert!(Pattern::compile(&beyond, PatternOptions::default()).is_ok());
8789 }
8790
8791 #[test]
8792 fn brace_expansion_survives_many_small_groups() {
8793 let options = PatternOptions::default().braces(true);
8798 let pattern = "{a}".repeat(2_000);
8799 let compiled = Pattern::compile(&pattern, options).unwrap();
8800 assert!(compiled.is_match("a".repeat(2_000)));
8801 }
8802
8803 #[test]
8804 fn brace_expansion_summary_matches_the_materialization_tree() {
8805 for (pattern, expected) in [
8806 ("{a,b}", (5, 2, 1, 2, 3, 7)),
8807 ("{a,b}{c,d}", (10, 4, 2, 8, 7, 30)),
8808 ("x{a,b}y", (7, 2, 3, 6, 3, 13)),
8809 ("{a,{b,c}}", (9, 3, 1, 3, 5, 17)),
8810 ("x{a", (3, 1, 3, 3, 1, 3)),
8811 ] {
8812 let summary = super::brace_expansion_summary(pattern.as_bytes(), true, 0)
8813 .expect("small expansion has a summary");
8814 assert_eq!(
8815 (
8816 summary.source_length,
8817 summary.alternatives,
8818 summary.minimum_length,
8819 summary.final_length_sum,
8820 summary.nodes,
8821 summary.written,
8822 ),
8823 expected,
8824 "{pattern}"
8825 );
8826 }
8827 }
8828
8829 #[test]
8830 fn brace_expansion_stops_at_the_work_budget() {
8831 let options = PatternOptions::default().braces(true);
8832
8833 let degenerate = "{a}".repeat(200_000);
8838 let error = Pattern::compile(°enerate, options).unwrap_err();
8839 assert_eq!(error.message(), "brace expansion is too large");
8840 assert_eq!(error.offset(), 0);
8841
8842 let wide = format!("{}{}", "x".repeat(10_000), "{a,b}".repeat(12));
8845 let error = Pattern::compile(&wide, options).unwrap_err();
8846 assert_eq!(error.message(), "brace expansion is too large");
8847 assert_eq!(error.offset(), 10_000);
8848
8849 let overlapping = format!("{}{}", "?".repeat(10_000), "{a,b}".repeat(12));
8852 let error = Pattern::compile(&overlapping, options).unwrap_err();
8853 assert_eq!(error.message(), "brace expansion is too large");
8854 assert_eq!(error.offset(), 10_000);
8855
8856 let narrow = format!("{}{}", "x".repeat(1_000), "{a,b}".repeat(12));
8859 assert!(Pattern::compile(&narrow, options).is_ok());
8860 assert!(Pattern::compile("{a,b}".repeat(12), options).is_ok());
8861
8862 assert!(Pattern::compile(°enerate, PatternOptions::default()).is_ok());
8864 }
8865
8866 #[test]
8867 fn compiled_size_is_budgeted_across_every_alternative() {
8868 let options = PatternOptions::default().braces(true).extglob(true);
8869
8870 let extglob = format!("@(a|b){}{}", "x".repeat(300), "{a,b}".repeat(12));
8873 let error = Pattern::compile(&extglob, options).unwrap_err();
8874 assert_eq!(error.message(), "pattern compiles to too much");
8875
8876 let wildcards = format!("{}{}", "?".repeat(300), "{a,b}".repeat(12));
8878 let error =
8879 Pattern::compile(&wildcards, PatternOptions::default().braces(true)).unwrap_err();
8880 assert_eq!(error.message(), "pattern compiles to too much");
8881 assert!(
8882 extglob.len() < 384 && wildcards.len() < 384,
8883 "the budget rejection must remain reachable below the matcher fuzz ceiling"
8884 );
8885
8886 let literals = format!("{}{}", "x".repeat(1_000), "{a,b}".repeat(12));
8889 assert!(Pattern::compile(&literals, PatternOptions::default().braces(true)).is_ok());
8890
8891 let invalid_class = format!("{}[{}", "?".repeat(300), "{a,b}".repeat(12));
8894 assert_eq!(
8895 Pattern::compile(&invalid_class, PatternOptions::default().braces(true))
8896 .unwrap_err()
8897 .message(),
8898 "unclosed character class"
8899 );
8900 let stable_class = format!("{}[a-z]{}", "?".repeat(300), "{a,b}".repeat(12));
8901 assert_eq!(
8902 Pattern::compile(&stable_class, PatternOptions::default().braces(true))
8903 .unwrap_err()
8904 .message(),
8905 "pattern compiles to too much"
8906 );
8907
8908 assert!(Pattern::compile(&wildcards, PatternOptions::default()).is_ok());
8910
8911 let braces = PatternOptions::default().braces(true);
8915 let single = format!("{}{}", "{a,b}".repeat(9), "?".repeat(700));
8916 let doubled = format!("{}/{}", "{a,b}".repeat(9), "?".repeat(700));
8917 assert!(Pattern::compile(&single, braces).is_ok());
8918 assert_eq!(
8919 Pattern::compile(&doubled, braces).unwrap_err().message(),
8920 "pattern compiles to too much"
8921 );
8922 }
8923
8924 #[test]
8925 fn compiled_size_budget_leaves_realistic_patterns_alone() {
8926 let options = PatternOptions::default()
8927 .braces(true)
8928 .extglob(true)
8929 .recursive_double_star(true);
8930 for pattern in [
8931 "src/**/+(main|lib).{rs,toml}",
8932 "src/**/*.{js,jsx,ts,tsx,mjs,cjs}",
8933 "src/{a,b}/{c,d}/*.{e,f}",
8934 "**/@(foo|bar)/**/*.ts",
8935 ] {
8936 assert!(
8937 Pattern::compile(pattern, options).is_ok(),
8938 "{pattern} must stay inside the compiled-size ceiling"
8939 );
8940 }
8941 }
8942
8943 #[test]
8944 fn wide_sweep_patterns_at_one_mebibyte_are_bounded() {
8945 let pattern = format!("{}*b", "?".repeat(1 << 20));
8950 let error = Pattern::compile(&pattern, PatternOptions::default()).unwrap_err();
8951 assert_eq!(error.message(), "pattern compiles to too much");
8952 }
8953
8954 #[test]
8955 fn the_three_compile_budgets_are_reported_apart() {
8956 let options = PatternOptions::default().braces(true);
8960 assert_eq!(
8961 Pattern::compile("{a,b}".repeat(13), options)
8962 .unwrap_err()
8963 .message(),
8964 "too many brace alternatives"
8965 );
8966 assert_eq!(
8967 Pattern::compile("{a}".repeat(200_000), options)
8968 .unwrap_err()
8969 .message(),
8970 "brace expansion is too large"
8971 );
8972 assert_eq!(
8973 Pattern::compile(
8974 format!("{}{}", "?".repeat(1_000), "{a,b}".repeat(12)),
8975 options
8976 )
8977 .unwrap_err()
8978 .message(),
8979 "pattern compiles to too much"
8980 );
8981 }
8982
8983 #[test]
8984 fn compiled_unit_sizes_match_what_the_budget_documents() {
8985 assert_eq!(
8989 size_of::<Token>(),
8990 32,
8991 "Token grew; the budget doc is stale"
8992 );
8993 assert_eq!(
8994 size_of::<super::ExtglobStep>(),
8995 40,
8996 "ExtglobStep grew; the budget doc is stale"
8997 );
8998 assert!(
8999 size_of::<super::BuildingAffixNode>()
9000 + size_of::<(u8, usize)>()
9001 + size_of::<super::AffixNode>()
9002 + size_of::<super::AffixEdge>()
9003 <= 3 * size_of::<Token>(),
9004 "affix-trie construction outgrew its three-unit charge"
9005 );
9006 }
9007
9008 #[test]
9009 fn brace_expansion_budgets_are_reported_apart() {
9010 let options = PatternOptions::default().braces(true);
9013 let many = Pattern::compile("{a,b}".repeat(13), options).unwrap_err();
9014 assert_eq!(many.message(), "too many brace alternatives");
9015 let large = Pattern::compile("{a}".repeat(200_000), options).unwrap_err();
9016 assert_eq!(large.message(), "brace expansion is too large");
9017 }
9018
9019 #[test]
9020 fn escapes_and_case_folding_are_opt_in_and_byte_oriented() {
9021 assert!(compile("\\*.rs").is_match("*.rs"));
9022 assert!(!compile("\\*.rs").is_match("main.rs"));
9023 assert!(
9024 Pattern::compile(
9025 "README.md",
9026 PatternOptions::default().case_insensitive(true)
9027 )
9028 .unwrap()
9029 .is_match("readme.MD")
9030 );
9031 }
9032
9033 #[test]
9034 fn extglobs_support_alternation_repetition_and_negation() {
9035 let options = PatternOptions::default().extglob(true);
9036 assert!(
9037 Pattern::compile("@(foo|bar)", options)
9038 .unwrap()
9039 .is_match("foo")
9040 );
9041 assert!(
9042 Pattern::compile("file?(.bak).txt", options)
9043 .unwrap()
9044 .is_match("file.txt")
9045 );
9046 assert!(
9047 Pattern::compile("a*(X)b", options)
9048 .unwrap()
9049 .is_match("aXXXb")
9050 );
9051 assert!(
9052 Pattern::compile("+(a|b)", options)
9053 .unwrap()
9054 .is_match("abba")
9055 );
9056 assert!(
9057 Pattern::compile("*.!(js)", options)
9058 .unwrap()
9059 .is_match("file.txt")
9060 );
9061 assert!(
9062 !Pattern::compile("*.!(js)", options)
9063 .unwrap()
9064 .is_match("file.js")
9065 );
9066 assert!(
9067 !Pattern::compile("@(foo|bar)", PatternOptions::default())
9068 .unwrap()
9069 .is_match("foo")
9070 );
9071 }
9072
9073 #[test]
9074 fn sequential_extglob_repetitions_share_failed_suffixes() {
9075 let pattern = Pattern::compile("+(a)+(a)+(a)+(a)", PatternOptions::default().extglob(true))
9076 .expect("repeating extglobs compile");
9077
9078 let mut non_matching = "a".repeat(400);
9082 non_matching.push('x');
9083 assert!(!pattern.is_match(&non_matching));
9084
9085 assert!(pattern.is_match("aaaa"));
9087 assert!(!pattern.is_match("aaa"));
9088
9089 let single = Pattern::compile("+(a)", PatternOptions::default().extglob(true))
9091 .expect("single repeating extglob compiles");
9092 assert!(single.is_match("a".repeat(5_000)));
9093 }
9094
9095 #[test]
9096 fn positive_extglob_nfa_reuses_thread_local_scratch() {
9097 let pattern = Pattern::compile(
9098 "@(src|tests)/lib.rs",
9099 PatternOptions::default().extglob(true),
9100 )
9101 .expect("positive extglob compiles");
9102 assert!(pattern.is_match("src/lib.rs"));
9103 assert!(!pattern.is_match("vendor/lib.rs"));
9104
9105 let warmed = positive_extglob_scratch_capacities();
9106 assert!(
9107 warmed.0 > 0 && warmed.1 > 0 && warmed.2 > 0 && warmed.3 > 0,
9108 "the NFA populated every reusable buffer: {warmed:?}"
9109 );
9110 for _ in 0..1_000 {
9111 assert!(pattern.is_match("tests/lib.rs"));
9112 assert!(!pattern.is_match("vendor/lib.rs"));
9113 }
9114 assert_eq!(
9115 positive_extglob_scratch_capacities(),
9116 warmed,
9117 "steady-state matches must not grow the reusable buffers"
9118 );
9119 }
9120
9121 #[test]
9122 fn positive_extglob_nfa_releases_oversized_scratch() {
9123 let mut scratch = super::PositiveExtglobMatchScratch::default();
9124 scratch.current.reserve(super::RETAINED_SCRATCH_WORDS + 1);
9125 scratch.next.reserve(super::RETAINED_SCRATCH_WORDS + 1);
9126 scratch
9127 .closure
9128 .pending
9129 .reserve(super::RETAINED_SCRATCH_WORDS + 1);
9130 let retained_seen_bits = super::RETAINED_SCRATCH_WORDS * u64::BITS as usize;
9131 scratch.closure.seen.reserve(retained_seen_bits + 1);
9132
9133 scratch.release();
9134
9135 assert!(scratch.current.capacity() <= super::RETAINED_SCRATCH_WORDS);
9136 assert!(scratch.next.capacity() <= super::RETAINED_SCRATCH_WORDS);
9137 assert!(scratch.closure.pending.capacity() <= super::RETAINED_SCRATCH_WORDS);
9138 assert!(scratch.closure.seen.capacity() <= retained_seen_bits);
9139 }
9140
9141 #[test]
9142 fn positive_extglob_nfa_has_a_reentrant_scratch_fallback() {
9143 let pattern = Pattern::compile("@(src|tests)", PatternOptions::default().extglob(true))
9144 .expect("positive extglob compiles");
9145
9146 super::POSITIVE_EXTGLOB_SCRATCH.with(|cell| {
9147 let _borrow = cell.borrow_mut();
9148 assert!(pattern.is_match("src"));
9149 assert!(!pattern.is_match("vendor"));
9150 });
9151 }
9152
9153 #[test]
9154 fn negated_extglob_interpreter_keeps_sparse_memo_and_releases_it() {
9155 let pattern = Pattern::compile(
9156 "!(z)",
9157 PatternOptions::default().extglob(true).match_hidden(true),
9158 )
9159 .expect("negated extglob compiles");
9160 let program = pattern.alternatives[0]
9161 .extglob
9162 .as_ref()
9163 .expect("the pattern carries an extglob program");
9164 assert!(program.positive_nfa.is_none());
9165 assert!(pattern.is_match("x"));
9166 let (pages, states) = extglob_failed_stats();
9167 assert!(
9168 pages > 0 && states > 0,
9169 "the fallback memo records reached states"
9170 );
9171
9172 let capacities = extglob_scratch_capacities();
9173 assert!(
9174 capacities.1 <= super::RETAINED_SCRATCH_WORDS,
9175 "the failed-state bitset must stay within its retained cap: {capacities:?}"
9176 );
9177 assert_eq!(
9178 extglob_failed_len(),
9179 0,
9180 "the retained memo is logically empty"
9181 );
9182 }
9183
9184 #[test]
9185 fn outer_star_deduplicates_negated_group_continuations_before_queueing() {
9186 let pattern = Pattern::compile(
9187 "*!(a)b",
9188 PatternOptions::default().extglob(true).match_hidden(true),
9189 )
9190 .expect("extglob compiles");
9191 assert!(
9192 pattern.alternatives[0]
9193 .extglob
9194 .as_ref()
9195 .is_some_and(|program| program.positive_nfa.is_none()),
9196 "the regression exercises the retained interpreter"
9197 );
9198 let candidate = vec![b'a'; 4_096];
9199 assert!(!pattern.is_match(&candidate));
9200 assert!(
9201 extglob_pending_peak() <= candidate.len() + 1,
9202 "duplicate retries must not make the worklist quadratic"
9203 );
9204 }
9205
9206 #[test]
9207 fn retained_extglob_interpreter_reuses_group_scratch() {
9208 let options = PatternOptions::default().extglob(true).match_hidden(true);
9209 let outer_star = Pattern::compile("*@(a)b", options).expect("extglob compiles");
9210 let negated = Pattern::compile("!(z)y", options).expect("extglob compiles");
9211 let repeated = Pattern::compile("*+(a*|b)c", options).expect("extglob compiles");
9212 let wide_literal = "a".repeat(70);
9213 let wide_negated = Pattern::compile(format!("*!({wide_literal}*)y"), options)
9214 .expect("wide extglob compiles");
9215
9216 for pattern in [&outer_star, &negated, &repeated, &wide_negated] {
9217 assert!(
9218 pattern.alternatives[0]
9219 .extglob
9220 .as_ref()
9221 .is_some_and(|program| program.positive_nfa.is_none()),
9222 "the regression exercises the retained interpreter"
9223 );
9224 }
9225 assert!(outer_star.is_match("aab"));
9226 assert!(negated.is_match("xy"));
9227 assert!(repeated.is_match("aaabc"));
9228 let wide_candidate = format!("{}y", "x".repeat(128));
9229 assert!(wide_negated.is_match(&wide_candidate));
9230
9231 let warmed = extglob_scratch_capacities();
9232 assert!(
9233 warmed.3 > 0 && warmed.5 > 0 && warmed.6 > 0 && warmed.7 > 0,
9234 "group matching populated its retained scratch: {warmed:?}"
9235 );
9236 for _ in 0..1_000 {
9237 assert!(outer_star.is_match("aab"));
9238 assert!(negated.is_match("xy"));
9239 assert!(repeated.is_match("aaabc"));
9240 assert!(wide_negated.is_match(&wide_candidate));
9241 }
9242 assert_eq!(
9243 extglob_scratch_capacities(),
9244 warmed,
9245 "steady-state retained extglob matches must not grow their scratch"
9246 );
9247 }
9248
9249 #[test]
9250 fn extglob_alternatives_keep_the_enclosing_component_context() {
9251 let options = PatternOptions::default().extglob(true);
9252 let star = Pattern::compile("a@(*)", options).expect("extglob compiles");
9253 let question = Pattern::compile("a@(?b)", options).expect("extglob compiles");
9254 assert!(star.is_match("a.b"));
9255 assert!(question.is_match("a.b"));
9256
9257 let path_group = Pattern::compile("dir/a@(*)", options).expect("extglob compiles");
9258 assert!(path_group.is_match_path("dir/a.b"));
9259 assert!(path_group.is_match_glob_path("dir/a.b"));
9260
9261 let at_start = Pattern::compile("@(*)", options).expect("extglob compiles");
9262 assert!(!at_start.is_match(".b"));
9263 assert!(!at_start.is_match_path(".b"));
9264 assert!(!at_start.is_match_glob_path(".b"));
9265
9266 let after_separator = Pattern::compile("dir/@(*)", options).expect("extglob compiles");
9267 assert!(!after_separator.is_match_path("dir/.b"));
9268 assert!(!after_separator.is_match_glob_path("dir/.b"));
9269 }
9270
9271 #[test]
9272 fn extglob_zero_width_forms_honor_leading_period_policy() {
9273 let options = PatternOptions::default().extglob(true);
9274 let optional = Pattern::compile("?(a|b).c", options).expect("optional extglob compiles");
9275 assert!(!optional.is_match(".c"));
9276 assert!(optional.is_match("a.c"));
9277
9278 let repeating = Pattern::compile("*(ab).c", options).expect("repeating extglob compiles");
9279 assert!(!repeating.is_match(".c"));
9280 assert!(repeating.is_match("ab.c"));
9281
9282 let outer_star = Pattern::compile("*.c@(x)", options).expect("outer star compiles");
9283 assert!(!outer_star.is_match(".cx"));
9284
9285 let group_star = Pattern::compile("x@(*.c)", options).expect("group star compiles");
9286 assert!(!group_star.is_match("xvisible/.c"));
9287
9288 let hidden = Pattern::compile("?(a|b).c", options.match_hidden(true))
9289 .expect("period-enabled optional extglob compiles");
9290 assert!(hidden.is_match(".c"));
9291 assert!(
9292 Pattern::compile("*.c@(x)", options.match_hidden(true))
9293 .expect("period-enabled outer star compiles")
9294 .is_match(".cx")
9295 );
9296 assert!(
9297 Pattern::compile("x@(*.c)", options.match_hidden(true))
9298 .expect("period-enabled group star compiles")
9299 .is_match("xvisible/.c")
9300 );
9301 }
9302
9303 #[test]
9304 fn extglob_recursive_prefix_exemption_requires_exactly_two_stars() {
9305 let options = PatternOptions::default()
9306 .extglob(true)
9307 .recursive_double_star(true);
9308
9309 let recursive = Pattern::compile("**/@(foo)", options).expect("extglob compiles");
9310 assert!(matches!(
9311 recursive.alternatives[0]
9312 .extglob
9313 .as_ref()
9314 .expect("the pattern carries an extglob program")
9315 .steps[0],
9316 ExtglobStep::Star {
9317 next: 2,
9318 blocks_leading_period: false
9319 }
9320 ));
9321
9322 let ordinary = Pattern::compile("***/@(foo)", options).expect("extglob compiles");
9323 assert!(matches!(
9324 ordinary.alternatives[0]
9325 .extglob
9326 .as_ref()
9327 .expect("the pattern carries an extglob program")
9328 .steps[0],
9329 ExtglobStep::Star {
9330 next: 3,
9331 blocks_leading_period: true
9332 }
9333 ));
9334 assert!(!ordinary.is_match(".hidden/foo"));
9335 }
9336
9337 #[test]
9338 fn extglob_literal_dot_alternatives_honor_leading_period_policy() {
9339 let options = PatternOptions::default().extglob(true);
9340 let matcher =
9341 Pattern::compile("@(.gitignore|.npmignore)", options).expect("extglob compiles");
9342 assert!(matcher.is_match(".gitignore"));
9343 assert!(matcher.is_match(".npmignore"));
9344 assert!(!matcher.is_match(".env"));
9345
9346 let wildcard = Pattern::compile("@(*|visible)", options).expect("extglob compiles");
9349 assert!(!wildcard.is_match(".hidden"));
9350
9351 let mixed = Pattern::compile("@(.gitignore|*)", options).expect("extglob compiles");
9352 assert!(mixed.is_match(".gitignore"));
9353 assert!(!mixed.is_match(".hidden"));
9354
9355 let negated = Pattern::compile("!(.gitignore)", options).expect("extglob compiles");
9358 assert!(!negated.is_match(".hidden"));
9359 let hidden = Pattern::compile("!(.gitignore)", options.match_hidden(true))
9360 .expect("period-enabled extglob compiles");
9361 assert!(hidden.is_match(".hidden"));
9362 assert!(!hidden.is_match(".gitignore"));
9363 }
9364
9365 #[test]
9366 fn negated_extglob_never_consumes_a_later_leading_period() {
9367 let options = PatternOptions::default().extglob(true);
9372 let refused = [
9373 ("!(x)", "a/.env"),
9374 ("!(x)", ".env"),
9375 ("a!(x)", "a/.env"),
9376 ("src/!(x)", "src/a/.env"),
9377 ("!(.env)", "a/.git"),
9378 ("!(x)/b", "a/.c/b"),
9379 ("!(x).env", "a/.env"),
9383 ];
9384 for (pattern, path) in refused {
9385 let matcher = Pattern::compile(pattern, options).expect("extglob compiles");
9386 assert!(matcher.engines_agree(path), "{pattern} against {path}");
9387 assert!(!matcher.is_match(path), "{pattern} against {path}");
9388 assert!(!matcher.is_match_path(path), "{pattern} against {path}");
9389 assert!(
9390 !matcher.is_match_glob_path(path),
9391 "{pattern} against {path}"
9392 );
9393
9394 let hidden = Pattern::compile(pattern, options.match_hidden(true))
9395 .expect("period-enabled extglob compiles");
9396 assert!(hidden.engines_agree(path), "{pattern} against {path}");
9397 assert!(
9398 hidden.is_match(path),
9399 "{pattern} against {path} with match_hidden"
9400 );
9401 }
9402 let star = Pattern::compile("*.env", options).expect("star compiles");
9403 assert!(!star.is_match("a/.env"));
9404
9405 let explicit = Pattern::compile("!(x)/.env", options).expect("extglob compiles");
9408 assert!(explicit.is_match("a/.env"));
9409 assert!(explicit.is_match_path("a/.env"));
9410 assert!(explicit.engines_agree("a/.env"));
9411 let inner = Pattern::compile("!(x)", options).expect("extglob compiles");
9413 assert!(inner.is_match("a/b.env"));
9414 assert!(inner.is_match_path("a/b.env"));
9415 }
9416
9417 #[test]
9418 fn extglob_programs_are_compiled_only_where_the_syntax_is_present() {
9419 let options = PatternOptions::default().extglob(true);
9420 let with_group = Pattern::compile("@(a|b)c", options).expect("group compiles");
9421 assert!(with_group.alternatives[0].extglob.is_some());
9422
9423 let without_group = Pattern::compile("src/**/*.ts", options).expect("plain compiles");
9426 assert!(without_group.alternatives[0].extglob.is_none());
9427
9428 let disabled =
9430 Pattern::compile("@(a|b)c", PatternOptions::default()).expect("plain compiles");
9431 assert!(disabled.alternatives[0].extglob.is_none());
9432 assert!(disabled.is_match("@(a|b)c"));
9433 }
9434
9435 #[test]
9436 fn extglob_alternatives_carry_a_fixed_width_only_when_they_have_one() {
9437 let options = PatternOptions::default().extglob(true);
9438 let pattern = Pattern::compile("@(foo|b?|c*)", options).expect("group compiles");
9439 let program = pattern.alternatives[0]
9440 .extglob
9441 .as_ref()
9442 .expect("the pattern carries a group");
9443 let widths: Vec<Option<usize>> = program.groups[0]
9444 .alternatives
9445 .iter()
9446 .map(|alternative| alternative.width)
9447 .collect();
9448 assert_eq!(widths, vec![Some(3), Some(2), None]);
9450 }
9451
9452 #[test]
9453 fn extglobs_avoid_native_stack_recursion() {
9454 let options = PatternOptions::default().extglob(true);
9455 let pattern = Pattern::compile("+(a*)b", options).expect("repetition compiles");
9456 let program = pattern.alternatives[0]
9457 .extglob
9458 .as_ref()
9459 .expect("the pattern carries an extglob program");
9460 assert!(program.positive_nfa.is_some());
9461
9462 std::thread::Builder::new()
9463 .stack_size(256 * 1024)
9464 .spawn(move || {
9465 assert!(!pattern.is_match("a".repeat(50_000)));
9466 let sequential = Pattern::compile("@(a)".repeat(2_000), options)
9467 .expect("sequential groups compile");
9468 assert!(sequential.is_match("a".repeat(2_000)));
9469
9470 let compatible = Pattern::compile(["*", &"@(a)".repeat(800)].concat(), options)
9473 .expect("compatible extglob compiles");
9474 assert!(compatible.is_match("a".repeat(800)));
9475 })
9476 .expect("spawn a small-stack matcher worker")
9477 .join()
9478 .expect("positive extglobs must not recurse through the native stack");
9479 }
9480
9481 #[test]
9482 fn positive_extglob_nfa_agrees_with_the_fallback_interpreter() {
9483 let candidates = byte_words(b"ab./", 5);
9484 for source in [
9485 "@(a|ab)b",
9486 "?(a|b)c",
9487 "*(a|bc)d",
9488 "+(a*|b?)c",
9489 "@(|a)b",
9490 "+(|a)b",
9491 "a@(b|[a-c])/?(.x|y)",
9492 r"x\@(a|b)@(c)",
9493 "x@(**)a",
9496 ] {
9497 for match_hidden in [false, true] {
9498 for case_insensitive in [false, true] {
9499 let options = PatternOptions::default()
9500 .extglob(true)
9501 .recursive_double_star(true)
9502 .match_hidden(match_hidden)
9503 .case_insensitive(case_insensitive);
9504 let compiled = Pattern::compile(source, options)
9505 .expect("positive differential pattern compiles");
9506 let mut interpreted = compiled.clone();
9507 for alternative in interpreted.compiled_alternatives_mut() {
9508 alternative
9509 .extglob
9510 .as_mut()
9511 .expect("every differential pattern carries an extglob")
9512 .positive_nfa = None;
9513 }
9514 for candidate in &candidates {
9515 assert_eq!(
9516 compiled.is_match(candidate),
9517 interpreted.is_match(candidate),
9518 "is_match diverges for {source:?} against {candidate:?} under {options:?}"
9519 );
9520 assert_eq!(
9521 compiled.is_match_path(candidate),
9522 interpreted.is_match_path(candidate),
9523 "is_match_path diverges for {source:?} against {candidate:?} under {options:?}"
9524 );
9525 assert_eq!(
9526 compiled.is_match_glob_path(candidate),
9527 interpreted.is_match_glob_path(candidate),
9528 "is_match_glob_path diverges for {source:?} against {candidate:?} under {options:?}"
9529 );
9530 }
9531 }
9532 }
9533 }
9534 }
9535
9536 #[test]
9540 fn positive_extglob_nfa_agrees_on_recursive_group_alternatives() {
9541 let candidates = byte_words(b"ab./", 5);
9542 for source in [
9543 "@(**/a|b)",
9544 "a/@(**|b)/a",
9545 "@(**)/a",
9546 "+(**/a)",
9547 "@(a/**)",
9548 "x@(**/a)",
9549 "@(**)x",
9550 ] {
9551 for match_hidden in [false, true] {
9552 let options = PatternOptions::default()
9553 .extglob(true)
9554 .recursive_double_star(true)
9555 .match_hidden(match_hidden);
9556 let compiled = Pattern::compile(source, options).expect("pattern compiles");
9557 assert!(
9558 compiled.alternatives[0]
9559 .extglob
9560 .as_ref()
9561 .is_some_and(|program| program.positive_nfa.is_some()),
9562 "{source} compiles to the positive NFA"
9563 );
9564 let mut interpreted = compiled.clone();
9565 for alternative in interpreted.compiled_alternatives_mut() {
9566 if let Some(program) = alternative.extglob.as_mut() {
9567 program.positive_nfa = None;
9568 }
9569 }
9570 for candidate in &candidates {
9571 let answers = |pattern: &Pattern| {
9572 (
9573 pattern.is_match(candidate),
9574 pattern.is_match_path(candidate),
9575 pattern.is_match_glob_path(candidate),
9576 )
9577 };
9578 assert_eq!(
9579 answers(&compiled),
9580 answers(&interpreted),
9581 "the engines diverge for {source:?} against {:?} under {options:?}",
9582 String::from_utf8_lossy(candidate)
9583 );
9584 }
9585 }
9586 }
9587 }
9588
9589 #[test]
9590 fn outer_stars_honor_the_selected_path_component_policy() {
9591 let options = PatternOptions::default()
9592 .extglob(true)
9593 .recursive_double_star(true);
9594 let star_before_group = Pattern::compile("*@(a)b", options).expect("extglob compiles");
9595 assert!(
9596 star_before_group.alternatives[0]
9597 .extglob
9598 .as_ref()
9599 .is_some_and(|program| program.positive_nfa.is_none())
9600 );
9601 assert!(star_before_group.is_match("/ab"));
9602 assert!(star_before_group.is_match_path("/ab"));
9603 assert!(!star_before_group.is_match_glob_path("/ab"));
9604
9605 let retained = Pattern::compile("a@(b|[a-c])*/?(.x|y)", options)
9606 .expect("backtracking extglob compiles");
9607 assert!(retained.is_match_path("aa//"));
9608 assert!(!retained.is_match("aa/./"));
9609 }
9610
9611 #[test]
9612 fn last_star_in_a_run_opens_an_extglob_group() {
9613 let options = PatternOptions::default().extglob(true).match_hidden(true);
9614
9615 for pattern in ["**(a)", "***(a)", "a**(b)"] {
9616 let compiled = Pattern::compile(pattern, options).expect("extglob compiles");
9617 assert!(
9618 compiled.alternatives[0]
9619 .extglob
9620 .as_ref()
9621 .is_some_and(|program| !program.groups.is_empty()),
9622 "{pattern:?} must retain its final `*(` as an extglob opener"
9623 );
9624 }
9625
9626 let leading = Pattern::compile("**(a)", options).expect("extglob compiles");
9627 for candidate in ["", "ab", "x", "(a)", "x(a)"] {
9628 assert!(
9629 leading.is_match(candidate),
9630 "the outer star may consume {candidate:?} before the zero-width group"
9631 );
9632 }
9633
9634 let embedded = Pattern::compile("a**(b)", options).expect("extglob compiles");
9635 for candidate in ["a", "ab", "ax"] {
9636 assert!(
9637 embedded.is_match(candidate),
9638 "expected {candidate:?} to match"
9639 );
9640 }
9641
9642 let disabled = Pattern::compile("**(a)", PatternOptions::default())
9643 .expect("literal parenthesized suffix compiles");
9644 assert!(disabled.is_match("x(a)"));
9645 assert!(!disabled.is_match("x"));
9646 }
9647
9648 #[test]
9649 fn unclosed_extglob_after_a_star_run_keeps_its_literal_suffix() {
9650 let options = PatternOptions::default().extglob(true).match_hidden(true);
9651
9652 let leading = Pattern::compile("**(a", options).expect("fallback pattern compiles");
9653 let leading_program = leading.alternatives[0]
9654 .extglob
9655 .as_ref()
9656 .expect("extglob interpreter is retained");
9657 assert!(matches!(
9658 leading_program.steps[0],
9659 ExtglobStep::Star { next: 2, .. }
9660 ));
9661 assert!(leading.is_match("x(a"));
9662 assert!(!leading.is_match("x"));
9663
9664 let embedded = Pattern::compile("a**(b", options).expect("fallback pattern compiles");
9665 assert!(embedded.is_match("ax(b"));
9666 assert!(!embedded.is_match("ax"));
9667 }
9668
9669 #[test]
9670 fn extglob_alternation_agrees_with_braces_over_exhaustive_byte_words() {
9671 let extglob = Pattern::compile(
9675 "@(a|bc)d",
9676 PatternOptions::default().extglob(true).match_hidden(true),
9677 )
9678 .expect("extglob alternation compiles");
9679 let braces = Pattern::compile(
9680 "{a,bc}d",
9681 PatternOptions::default().braces(true).match_hidden(true),
9682 )
9683 .expect("brace alternation compiles");
9684 for candidate in byte_words(b"abcd", 4) {
9685 assert_eq!(
9686 extglob.is_match(&candidate),
9687 braces.is_match(&candidate),
9688 "spellings disagree on {candidate:?}"
9689 );
9690 }
9691 }
9692
9693 #[test]
9694 fn extglob_reads_an_escaped_group_opener_as_text() {
9695 let options = PatternOptions::default().extglob(true);
9698 let pattern = Pattern::compile("x\\@(a|b)@(c)", options).expect("pattern compiles");
9699 assert!(pattern.is_match("x@(a|b)c"));
9700 assert!(!pattern.is_match("xac"));
9701
9702 let no_escapes = Pattern::compile("x\\@(a|b)@(c)", options.escape(false))
9703 .expect("pattern compiles without escapes");
9704 assert!(no_escapes.is_match("x\\ac"));
9705 }
9706
9707 #[test]
9708 fn literal_only_patterns_match_exactly_over_exhaustive_byte_words() {
9709 let words = byte_words(b"abc", 3);
9710 for pattern in &words {
9711 let matcher = Pattern::compile(pattern, PatternOptions::default())
9712 .expect("literal byte patterns compile");
9713 for candidate in &words {
9714 assert_eq!(
9715 matcher.is_match(candidate),
9716 pattern == candidate,
9717 "literal pattern {pattern:?} against {candidate:?}"
9718 );
9719 }
9720 }
9721 }
9722
9723 #[test]
9724 fn wildcard_subset_invariants_hold_over_exhaustive_byte_words() {
9725 let options = PatternOptions::default().match_hidden(true);
9726 let any = Pattern::compile("?", options).expect("single wildcard compiles");
9727 let star = Pattern::compile("*", options).expect("star wildcard compiles");
9728 let prefixed = Pattern::compile("a*", options).expect("prefixed star compiles");
9729 let words = byte_words(b"ab.", 4);
9730 for candidate in &words {
9731 assert!(
9732 !any.is_match(candidate) || star.is_match(candidate),
9733 "question-mark matches a path the star rejects: {candidate:?}"
9734 );
9735 assert!(
9736 !prefixed.is_match(candidate) || star.is_match(candidate),
9737 "prefixed star matches a path the unrestricted star rejects: {candidate:?}"
9738 );
9739 }
9740 }
9741
9742 #[test]
9743 fn failed_state_memos_use_inline_and_heap_storage_without_changing_membership() {
9744 let mut words = Vec::new();
9745 let tokens = vec![Token::Star];
9746 let mut inline = FailedStates::new(&tokens, b"abc", &mut words);
9747 assert!(matches!(&inline, FailedStates::Inline { .. }));
9748 assert!(inline.insert(0, 1));
9749 assert!(!inline.insert(0, 1));
9750 assert!(inline.insert(0, 2));
9751 assert!(!inline.insert(0, 2));
9752 assert!(words.is_empty(), "an inline matrix must not touch the heap");
9753
9754 let tokens = vec![Token::Star; 2];
9755 let mut heap = FailedStates::new(&tokens, &[b'a'; 64], &mut words);
9756 assert!(matches!(&heap, FailedStates::Heap { .. }));
9757 assert!(heap.insert(1, 63));
9758 assert!(!heap.insert(1, 63));
9759 assert!(heap.insert(0, 63));
9760 assert!(!heap.insert(0, 63));
9761 assert_eq!(words.len(), 3);
9763
9764 let mut reused = FailedStates::new(&tokens, &[b'a'; 64], &mut words);
9767 assert!(reused.insert(1, 63));
9768 assert!(reused.insert(0, 63));
9769 }
9770
9771 #[test]
9772 fn recursive_prefix_suffix_fast_path_matches_the_general_matcher() {
9773 let options = PatternOptions::default()
9774 .recursive_double_star(true)
9775 .case_insensitive(true);
9776 let fast = Pattern::compile("Src/**/*.RS", options).expect("pattern compiles");
9777 assert!(matches!(
9778 fast.alternatives[0].fast_path,
9779 Some(FastPath::RecursivePrefixSuffix { .. })
9780 ));
9781 let mut general = fast.clone();
9782 general.alternatives[0].fast_path = None;
9783
9784 let mut candidates = vec![
9785 b"".to_vec(),
9786 b"src".to_vec(),
9787 b"src/".to_vec(),
9788 b"src/.r".to_vec(),
9789 b"src/rs".to_vec(),
9790 b"src/.rs".to_vec(),
9791 b"src/.hidden.rs".to_vec(),
9792 b"src/visible.rs".to_vec(),
9793 b"src/nested/.hidden.rs".to_vec(),
9794 b"src/nested/visible.rs".to_vec(),
9795 b"SRC/NESTED/VISIBLE.RS".to_vec(),
9796 b"SRC\\NESTED\\VISIBLE.RS".to_vec(),
9797 b"other/visible.rs".to_vec(),
9798 ];
9799 candidates.extend(
9800 byte_words(b"ab./rs", 4)
9801 .into_iter()
9802 .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
9803 );
9804 for candidate in candidates {
9805 assert_eq!(
9806 fast.is_match(&candidate),
9807 general.is_match(&candidate),
9808 "fast path differs for {candidate:?}"
9809 );
9810 assert_eq!(
9811 fast.is_match_glob_path(&candidate),
9812 general.is_match_glob_path(&candidate),
9813 "component fast path differs for {candidate:?}"
9814 );
9815 }
9816 #[cfg(windows)]
9817 assert!(fast.is_match_glob_path(br"SRC\NESTED\VISIBLE.RS"));
9818 }
9819
9820 #[test]
9821 fn recursive_suffix_fast_path_matches_the_general_matcher() {
9822 for options in [
9823 PatternOptions::default().recursive_double_star(true),
9824 PatternOptions::default()
9825 .recursive_double_star(true)
9826 .case_insensitive(true),
9827 PatternOptions::default()
9828 .recursive_double_star(true)
9829 .match_hidden(true),
9830 ] {
9831 let fast = Pattern::compile("**/*.ts", options).expect("pattern compiles");
9832 assert!(matches!(
9833 fast.alternatives[0].fast_path,
9834 Some(FastPath::RecursiveSuffix { .. })
9835 ));
9836 let mut general = fast.clone();
9837 general.alternatives[0].fast_path = None;
9838
9839 let mut candidates = vec![
9840 b"".to_vec(),
9841 b".ts".to_vec(),
9842 b"index.ts".to_vec(),
9843 b"INDEX.TS".to_vec(),
9844 b"index.tsx".to_vec(),
9845 b".hidden.ts".to_vec(),
9846 b"src/.ts".to_vec(),
9847 b"src/index.ts".to_vec(),
9848 b"src/.hidden.ts".to_vec(),
9849 b".hidden/index.ts".to_vec(),
9850 b"src/deep/index.ts".to_vec(),
9851 ];
9852 candidates.extend(byte_words(b"ab./tsTS", 4));
9853 for candidate in candidates {
9854 assert_eq!(
9855 fast.is_match(&candidate),
9856 general.is_match(&candidate),
9857 "fast path differs for {options:?} against {candidate:?}"
9858 );
9859 assert_eq!(
9860 fast.is_match_glob_path(&candidate),
9861 general.is_match_glob_path(&candidate),
9862 "component fast path differs for {options:?} against {candidate:?}"
9863 );
9864 }
9865 }
9866 }
9867
9868 #[test]
9869 fn suffix_set_fast_path_matches_linear_alternatives() {
9870 for options in [
9871 PatternOptions::default()
9872 .braces(true)
9873 .recursive_double_star(true),
9874 PatternOptions::default()
9875 .braces(true)
9876 .recursive_double_star(true)
9877 .case_insensitive(true),
9878 PatternOptions::default()
9879 .braces(true)
9880 .recursive_double_star(true)
9881 .match_hidden(true),
9882 ] {
9883 for source in ["*.{ts,tsx,js,jsx,mjs,cjs}", "**/*.{ts,tsx,js,jsx,mjs,cjs}"] {
9884 let fast = Pattern::compile(source, options).expect("suffix set compiles");
9885 assert!(matches!(
9886 fast.alternative_fast_path.as_deref(),
9887 Some(AlternativeFastPath::SuffixSet(_))
9888 ));
9889 let mut linear = fast.clone();
9890 linear.alternative_fast_path = None;
9891
9892 let mut candidates = vec![
9893 b"".to_vec(),
9894 b".ts".to_vec(),
9895 b"index.ts".to_vec(),
9896 b"INDEX.TSX".to_vec(),
9897 b"index.cjs".to_vec(),
9898 b"index.vue".to_vec(),
9899 b"src/index.ts".to_vec(),
9900 b"src/.hidden.ts".to_vec(),
9901 b".hidden/index.jsx".to_vec(),
9902 b"src/deep/index.mjs".to_vec(),
9903 ];
9904 candidates.extend(byte_words(b"ab./tTsSxjc", 4));
9905 for candidate in candidates {
9906 assert_eq!(
9907 fast.is_match(&candidate),
9908 linear.is_match(&candidate),
9909 "crossing match differs for {source:?}, {options:?}, {candidate:?}"
9910 );
9911 assert_eq!(
9912 fast.is_match_glob_path(&candidate),
9913 linear.is_match_glob_path(&candidate),
9914 "component match differs for {source:?}, {options:?}, {candidate:?}"
9915 );
9916 }
9917 }
9918 }
9919 }
9920
9921 #[test]
9922 fn scoped_suffix_set_fast_path_matches_only_complete_cross_products() {
9923 for options in [
9924 PatternOptions::default()
9925 .braces(true)
9926 .recursive_double_star(true),
9927 PatternOptions::default()
9928 .braces(true)
9929 .recursive_double_star(true)
9930 .case_insensitive(true),
9931 PatternOptions::default()
9932 .braces(true)
9933 .recursive_double_star(true)
9934 .match_hidden(true),
9935 ] {
9936 let fast = Pattern::compile("{src,packages}/**/*.{ts,tsx,js,jsx,mjs,cjs}", options)
9937 .expect("scoped suffix set compiles");
9938 assert!(matches!(
9939 fast.alternative_fast_path.as_deref(),
9940 Some(AlternativeFastPath::ScopedSuffixSet(_))
9941 ));
9942 let mut linear = fast.clone();
9943 linear.alternative_fast_path = None;
9944
9945 let mut candidates = vec![
9946 b"".to_vec(),
9947 b"src/.ts".to_vec(),
9948 b"src/index.ts".to_vec(),
9949 b"SRC/INDEX.TSX".to_vec(),
9950 b"packages/.js".to_vec(),
9951 b"packages/index.cjs".to_vec(),
9952 b"packages/deep/index.vue".to_vec(),
9953 b"vendor/index.ts".to_vec(),
9954 b"src/.hidden/index.ts".to_vec(),
9955 b"src/deep/.hidden.mjs".to_vec(),
9956 b"packages/deep/index.jsx".to_vec(),
9957 ];
9958 candidates.extend(
9959 byte_words(b"ab./tTsSxjc", 4)
9960 .into_iter()
9961 .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
9962 );
9963 assert_eq!(fast.is_match("packages/.js"), options.match_hidden);
9968 assert_eq!(
9969 fast.is_match_glob_path("packages/.js"),
9970 options.match_hidden
9971 );
9972 for candidate in candidates {
9973 assert_eq!(
9974 fast.is_match(&candidate),
9975 linear.is_match(&candidate),
9976 "crossing match differs for {options:?}, {candidate:?}"
9977 );
9978 assert_eq!(
9979 fast.is_match_glob_path(&candidate),
9980 linear.is_match_glob_path(&candidate),
9981 "component match differs for {options:?}, {candidate:?}"
9982 );
9983 }
9984 }
9985
9986 let coupled = Pattern::compile(
9987 "{src/**/*.ts,lib/**/*.js}",
9988 PatternOptions::default()
9989 .braces(true)
9990 .recursive_double_star(true),
9991 )
9992 .expect("coupled alternatives compile");
9993 assert!(coupled.alternative_fast_path.is_none());
9994 assert!(!coupled.is_match_glob_path("src/index.js"));
9995 assert!(!coupled.is_match_glob_path("lib/index.ts"));
9996 }
9997
9998 #[test]
9999 fn recursive_terminal_fast_path_matches_the_general_matcher() {
10000 let options = PatternOptions::default()
10001 .recursive_double_star(true)
10002 .case_insensitive(true);
10003 let fast = Pattern::compile("Src/**", options).expect("pattern compiles");
10004 assert!(matches!(
10005 fast.alternatives[0].fast_path,
10006 Some(FastPath::RecursiveTerminalPrefix { .. })
10007 ));
10008 let mut general = fast.clone();
10009 general.alternatives[0].fast_path = None;
10010
10011 let mut candidates = vec![
10012 b"src".to_vec(),
10013 b"SRC/".to_vec(),
10014 b"src/file.rs".to_vec(),
10015 b"src/nested/file.rs".to_vec(),
10016 b"src/.hidden".to_vec(),
10017 b"src/.hidden/file.rs".to_vec(),
10018 b"source/file.rs".to_vec(),
10019 ];
10020 candidates.extend(
10021 byte_words(b"src./", 5)
10022 .into_iter()
10023 .map(|suffix| [b"src".as_slice(), suffix.as_slice()].concat()),
10024 );
10025 for candidate in candidates {
10026 assert_eq!(
10027 fast.is_match(&candidate),
10028 general.is_match(&candidate),
10029 "fast path differs for {candidate:?}"
10030 );
10031 }
10032 }
10033
10034 #[test]
10035 fn literal_fast_path_matches_the_general_matcher() {
10036 let options = PatternOptions::default().case_insensitive(true);
10037 let fast = Pattern::compile("Src/Readme", options).expect("pattern compiles");
10038 assert!(matches!(
10039 fast.alternatives[0].fast_path,
10040 Some(FastPath::LiteralTokens(_))
10041 ));
10042 let mut general = fast.clone();
10043 general.alternatives[0].fast_path = None;
10044
10045 for candidate in [
10046 b"src/readme".as_slice(),
10047 b"SRC/README".as_slice(),
10048 b"src/readme.txt".as_slice(),
10049 b"src/readme/".as_slice(),
10050 b"other/readme".as_slice(),
10051 ] {
10052 assert_eq!(
10053 fast.is_match(candidate),
10054 general.is_match(candidate),
10055 "fast path differs for {candidate:?}"
10056 );
10057 }
10058 }
10059
10060 #[test]
10061 fn deterministic_fast_path_matches_the_general_matcher() {
10062 let options = PatternOptions::default().case_insensitive(true);
10063 let fast = Pattern::compile("src/[ab]?.[Rr][Ss]", options).expect("pattern compiles");
10064 assert!(matches!(
10065 fast.alternatives[0].fast_path,
10066 Some(FastPath::DeterministicTokens(_))
10067 ));
10068 let mut general = fast.clone();
10069 general.alternatives[0].fast_path = None;
10070
10071 let mut candidates = vec![
10072 b"src/ab.rs".to_vec(),
10073 b"src/aX.RS".to_vec(),
10074 b"src/.a.rs".to_vec(),
10075 b"src/ab.txt".to_vec(),
10076 b"lib/ab.rs".to_vec(),
10077 ];
10078 candidates.extend(
10079 byte_words(b"ab./rsRS", 4)
10080 .into_iter()
10081 .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
10082 );
10083 for candidate in candidates {
10084 assert_eq!(
10085 fast.is_match(&candidate),
10086 general.is_match(&candidate),
10087 "fast path differs for {candidate:?}"
10088 );
10089 }
10090 }
10091
10092 #[test]
10093 fn infix_star_fast_path_matches_the_general_matcher() {
10094 let options = PatternOptions::default().case_insensitive(true);
10095 let fast = Pattern::compile("Src*.rs", options).expect("pattern compiles");
10096 assert!(matches!(
10097 fast.alternatives[0].fast_path,
10098 Some(FastPath::InfixStar { .. })
10099 ));
10100 let mut general = fast.clone();
10101 general.alternatives[0].fast_path = None;
10102
10103 let mut candidates = vec![
10104 b"src.rs".to_vec(),
10105 b"srcMain.RS".to_vec(),
10106 b"src/nested/main.rs".to_vec(),
10107 b"src/.hidden.rs".to_vec(),
10108 b"other.rs".to_vec(),
10109 b"src.txt".to_vec(),
10110 ];
10111 candidates.extend(
10112 byte_words(b"src./RS", 5)
10113 .into_iter()
10114 .map(|middle| [b"src".as_slice(), middle.as_slice(), b".rs"].concat()),
10115 );
10116 for candidate in candidates {
10117 assert_eq!(
10118 fast.is_match(&candidate),
10119 general.is_match(&candidate),
10120 "fast path differs for {candidate:?}"
10121 );
10122 }
10123 }
10124
10125 #[test]
10126 fn static_star_fast_path_matches_the_general_matcher() {
10127 let options = PatternOptions::default().case_insensitive(true);
10128 let fast = Pattern::compile("Src/Lib/*.RS", options).expect("pattern compiles");
10129 assert!(matches!(
10130 fast.alternatives[0].fast_path,
10131 Some(FastPath::StaticStar { .. })
10132 ));
10133 let mut general = fast.clone();
10134 general.alternatives[0].fast_path = None;
10135
10136 let mut candidates = vec![
10137 b"src/lib/.rs".to_vec(),
10138 b"src/lib/main.rs".to_vec(),
10139 b"SRC/LIB/main.RS".to_vec(),
10140 b"src/lib/nested/main.rs".to_vec(),
10141 b"src/lib/.hidden.rs".to_vec(),
10142 b"src/other/main.rs".to_vec(),
10143 b"src/lib/main.txt".to_vec(),
10144 ];
10145 candidates.extend(
10146 byte_words(b"ab./rsRS", 4)
10147 .into_iter()
10148 .map(|middle| [b"src/lib/".as_slice(), middle.as_slice(), b".rs"].concat()),
10149 );
10150 for candidate in candidates {
10151 assert_eq!(
10152 fast.is_match(&candidate),
10153 general.is_match(&candidate),
10154 "fast path differs for {candidate:?}"
10155 );
10156 }
10157 }
10158
10159 #[test]
10160 fn static_star_edge_fast_paths_match_the_general_matcher() {
10161 let options = PatternOptions::default().case_insensitive(true);
10162 for (pattern, candidates) in [
10163 (
10164 "Src/Lib/*",
10165 vec![
10166 b"src/lib/".as_slice(),
10167 b"src/lib/main.rs".as_slice(),
10168 b"SRC/LIB/MAIN.RS".as_slice(),
10169 b"src/lib/.hidden".as_slice(),
10170 b"src/lib/nested/main.rs".as_slice(),
10171 b"src/other/main.rs".as_slice(),
10172 ],
10173 ),
10174 (
10175 "*/Main.RS",
10176 vec![
10177 b"main.rs".as_slice(),
10178 b"src/main.rs".as_slice(),
10179 b"SRC/DEEP/MAIN.RS".as_slice(),
10180 b".hidden/main.rs".as_slice(),
10181 b"src/main.txt".as_slice(),
10182 ],
10183 ),
10184 ] {
10185 let fast = Pattern::compile(pattern, options).expect("pattern compiles");
10186 assert!(matches!(
10187 fast.alternatives[0].fast_path,
10188 Some(FastPath::StaticStar { .. })
10189 ));
10190 let mut general = fast.clone();
10191 general.alternatives[0].fast_path = None;
10192 for candidate in candidates {
10193 assert_eq!(
10194 fast.is_match(candidate),
10195 general.is_match(candidate),
10196 "fast path differs for {pattern:?} against {candidate:?}"
10197 );
10198 }
10199 }
10200 }
10201
10202 #[test]
10203 fn single_star_fast_paths_match_the_general_matcher() {
10204 let mut candidates = byte_words(b"ab./rs", 4);
10205 candidates.extend([b"src..rs".to_vec(), b"src/.hidden".to_vec()]);
10206 for pattern in ["*", "src*", "*.rs"] {
10207 let fast =
10208 Pattern::compile(pattern, PatternOptions::default()).expect("pattern compiles");
10209 assert!(fast.alternatives[0].fast_path.is_some());
10210 let mut general = fast.clone();
10211 general.alternatives[0].fast_path = None;
10212 for candidate in &candidates {
10213 assert_eq!(
10214 fast.is_match(candidate),
10215 general.is_match(candidate),
10216 "fast path differs for {pattern:?} against {candidate:?}"
10217 );
10218 assert_eq!(
10219 fast.is_match_glob_path(candidate),
10220 general.is_match_glob_path(candidate),
10221 "component fast path differs for {pattern:?} against {candidate:?}"
10222 );
10223 }
10224 }
10225 }
10226
10227 #[test]
10228 fn component_suffix_fast_paths_preserve_separator_and_hidden_rules() {
10229 let star = Pattern::compile("*.ts", PatternOptions::default()).unwrap();
10230 assert!(star.is_match_glob_path("index.ts"));
10231 assert!(!star.is_match_glob_path(".ts"));
10232 assert!(!star.is_match_glob_path("src/index.ts"));
10233 assert!(!star.is_match_glob_path(".hidden.ts"));
10234
10235 let recursive = Pattern::compile(
10236 "**/*.ts",
10237 PatternOptions::default().recursive_double_star(true),
10238 )
10239 .unwrap();
10240 assert!(recursive.is_match_glob_path("index.ts"));
10241 assert!(recursive.is_match_glob_path("src/deep/index.ts"));
10242 assert!(!recursive.is_match_glob_path("src/.ts"));
10243 assert!(!recursive.is_match_glob_path("src/.hidden.ts"));
10244 assert!(!recursive.is_match_glob_path(".hidden/index.ts"));
10245
10246 let hidden = Pattern::compile(
10247 "**/*.ts",
10248 PatternOptions::default()
10249 .recursive_double_star(true)
10250 .match_hidden(true),
10251 )
10252 .unwrap();
10253 assert!(hidden.is_match_glob_path("src/.ts"));
10254 assert!(hidden.is_match_glob_path("src/.hidden.ts"));
10255 assert!(hidden.is_match_glob_path(".hidden/index.ts"));
10256 }
10257
10258 #[test]
10259 fn filter_paths_preserves_input_order_and_borrows_inputs() {
10260 let pattern = Pattern::compile("*.txt", PatternOptions::default()).unwrap();
10261 let paths = ["first.txt", "skip.rs", "second.txt"];
10262 assert_eq!(
10263 pattern.filter_paths(&paths),
10264 vec![&"first.txt", &"second.txt"]
10265 );
10266 }
10267
10268 #[test]
10269 fn filter_paths_keeps_wildcards_after_a_separator_in_one_component() {
10270 let pattern = Pattern::compile(
10271 "**/lua/*.lua",
10272 PatternOptions::default().recursive_double_star(true),
10273 )
10274 .unwrap();
10275 let paths = [
10276 "lua/init.lua",
10277 "nvim/lua/setup.lua",
10278 "nvim/lua/sub/nested.lua",
10279 ];
10280 assert_eq!(
10281 pattern.filter_paths(&paths),
10282 vec![&"lua/init.lua", &"nvim/lua/setup.lua"]
10283 );
10284 assert!(pattern.is_match("nvim/lua/sub/nested.lua"));
10285 assert!(pattern.is_match_path("nvim/lua/setup.lua"));
10286 assert!(!pattern.is_match_path("nvim/lua/sub/nested.lua"));
10287
10288 let root_pattern = Pattern::compile("*.rs", PatternOptions::default()).unwrap();
10289 assert!(root_pattern.is_match_path("src/nested.rs"));
10290 assert!(!root_pattern.is_match_glob_path("src/nested.rs"));
10291 assert!(root_pattern.is_match_glob_path("nested.rs"));
10292
10293 let extglob = Pattern::compile("@(a*)", PatternOptions::default().extglob(true)).unwrap();
10294 assert!(extglob.is_match_glob_path("aaa"));
10295 assert!(!extglob.is_match_glob_path("aaa/nested"));
10296 let nested_extglob =
10297 Pattern::compile("@(foo)/*/bar", PatternOptions::default().extglob(true)).unwrap();
10298 assert!(nested_extglob.is_match_glob_path("foo/a/bar"));
10299 assert!(!nested_extglob.is_match_glob_path("foo/a/deep/bar"));
10300 }
10301
10302 #[test]
10303 fn extglobs_share_each_entry_points_component_policy() {
10304 let options = PatternOptions::default().extglob(true);
10305 let mixed = Pattern::compile("*/x/b*@(y)", options).unwrap();
10306 assert!(mixed.is_match("a/b/x/bzy"));
10307 assert!(mixed.is_match_path("a/b/x/bzy"));
10308 assert!(!mixed.is_match_glob_path("a/b/x/bzy"));
10309
10310 let root = Pattern::compile("*@(y)", options).unwrap();
10311 assert!(root.is_match("a/b/zy"));
10312 assert!(root.is_match_path("a/b/zy"));
10313 assert!(!root.is_match_glob_path("a/b/zy"));
10314
10315 for pattern in [&mixed, &root] {
10316 assert!(pattern.engines_agree("a/b/x/bzy"));
10317 assert!(pattern.engines_agree("a/b/zy"));
10318 }
10319 }
10320
10321 #[test]
10322 fn recursive_prefix_before_extglob_matches_at_depth_zero() {
10323 let options = PatternOptions::default()
10324 .extglob(true)
10325 .recursive_double_star(true);
10326 let pattern = Pattern::compile("**/@(x)", options).unwrap();
10327 for candidate in ["x", "a/x"] {
10328 assert!(pattern.is_match(candidate), "is_match: {candidate}");
10329 assert!(
10330 pattern.is_match_path(candidate),
10331 "is_match_path: {candidate}"
10332 );
10333 assert!(
10334 pattern.is_match_glob_path(candidate),
10335 "is_match_glob_path: {candidate}"
10336 );
10337 assert!(pattern.engines_agree(candidate), "engines: {candidate}");
10338 }
10339
10340 let negated_suffix = Pattern::compile("**/*.!(js)", options).unwrap();
10341 assert!(negated_suffix.is_match_path("src/app.ts"));
10342 assert!(!negated_suffix.is_match_path("src/app.js"));
10343 }
10344
10345 #[test]
10346 fn filter_paths_precompiles_its_component_sensitive_ir() {
10347 let component_pattern = Pattern::compile("src/*.rs", PatternOptions::default())
10348 .expect("component pattern compiles");
10349 assert!(
10350 component_pattern
10351 .path_filter
10352 .as_ref()
10353 .is_some_and(|filter| {
10354 filter.compiled.len() == 1
10355 && filter.compiled[0].fast_path.is_none()
10356 && filter.programs.is_empty()
10357 && filter.whole.is_empty()
10358 })
10359 );
10360
10361 let root_pattern =
10362 Pattern::compile("*.rs", PatternOptions::default()).expect("root pattern compiles");
10363 assert!(root_pattern.path_filter.is_none());
10364 }
10365
10366 #[test]
10367 fn filter_paths_routes_each_brace_arm_on_its_own() {
10368 let options = PatternOptions::default().braces(true).extglob(true);
10369 let mixed = Pattern::compile("{zz,lib/*,src/@(*.ts)}", options).expect("pattern compiles");
10372 let filter = mixed
10373 .path_filter
10374 .as_ref()
10375 .expect("component arms need the list-filter routes");
10376 assert_eq!(filter.compiled.len(), 2);
10377 assert_eq!(filter.programs, [2]);
10378 assert!(filter.whole.is_empty());
10379
10380 let lean = Pattern::compile("{zz,src/@(*.ts)}", options).expect("pattern compiles");
10383 let filter = lean
10384 .path_filter
10385 .as_ref()
10386 .expect("the extglob arm is routed");
10387 assert!(filter.compiled.is_empty());
10388 assert_eq!(filter.programs, [1]);
10389 assert_eq!(filter.whole, [0]);
10390
10391 for pattern in [
10394 "src/@(*.ts)",
10395 "src/+(*.ts)",
10396 "src/?(*.ts)",
10397 "src/*(*.ts)",
10398 "src/!(x)",
10399 "src/@(x|*.ts)",
10400 "{src/@(*.ts),zz}",
10401 "{src/@(*.ts),lib/*}",
10402 "{zz,src/@(*.ts)}",
10403 "./src/@(*.ts)",
10404 ] {
10405 let pattern = Pattern::compile(pattern, options).expect("pattern compiles");
10406 assert!(pattern.is_match_path("src/b.ts"));
10407 assert!(!pattern.is_match_path("src/a/b.ts"));
10408 assert!(pattern.is_match("src/a/b.ts") || pattern.is_match("./src/a/b.ts"));
10409 assert!(!pattern.is_match_glob_path("src/a/b.ts"));
10410 }
10411
10412 let repeated_like = Pattern::compile("src/*.ts*.ts", options).expect("pattern compiles");
10416 for pattern in ["src/+(*.ts)", "src/*(*.ts)"] {
10417 let pattern = Pattern::compile(pattern, options).expect("pattern compiles");
10418 for candidate in ["src/a.tsb/c.ts", "src/a.ts/c.ts", "src/a/b.ts"] {
10419 assert_eq!(
10420 pattern.is_match_path(candidate),
10421 repeated_like.is_match_path(candidate),
10422 "{candidate}"
10423 );
10424 assert!(pattern.engines_agree(candidate), "{candidate}");
10425 }
10426 let mut interpreted = pattern.clone();
10427 for alternative in interpreted.compiled_alternatives_mut() {
10428 if let Some(program) = alternative.extglob.as_mut() {
10429 program.positive_nfa = None;
10430 }
10431 }
10432 for candidate in ["src/a.tsb/c.ts", "src/a/b.ts", "src/a.ts"] {
10433 assert_eq!(
10434 pattern.is_match_path(candidate),
10435 interpreted.is_match_path(candidate),
10436 "{candidate}"
10437 );
10438 }
10439 }
10440 }
10441
10442 #[test]
10448 fn extglob_groups_follow_the_plain_position_rule() {
10449 let pairs = [
10450 ("src/@(*.ts)", "src/*.ts"),
10452 ("a/@(*)/z", "a/*/z"),
10453 ("x/@(*[z])*", "x/*[z]*"),
10454 ("src/a@(*.ts)", "src/a*.ts"),
10456 ("@(*.ts)", "*.ts"),
10457 ("*/a@(*.ts)", "*/a*.ts"),
10458 ("a/@(b*)/z", "a/b*/z"),
10459 ("*/@(b*)/z", "*/b*/z"),
10460 ("a/@(?*)/z", "a/?*/z"),
10461 ("*/*.@(ts)", "*/*.ts"),
10464 ("a*/*.@(ts)", "a*/*.ts"),
10465 ("**/*.@(ts)", "**/*.ts"),
10466 ("**/lua/@(*.lua)", "**/lua/*.lua"),
10467 ("*/@(a/b)", "*/a/b"),
10469 ("x@(y)/***/z", "xy/***/z"),
10473 ("src/@(*.ts)/***", "src/*.ts/***"),
10474 ("src/**/*.@(ts)", "src/**/*.ts"),
10475 ("@(x)/**", "x/**"),
10479 ("x/@(**)", "x/**"),
10480 ("@(**)/z", "**/z"),
10481 ("a/@(**)/z", "a/**/z"),
10482 ("@(x/**)/z", "x/**/z"),
10483 ("@(**/**)/z", "**/**/z"),
10484 ("@(**)/*", "**/*"),
10485 ];
10486 let candidates = [
10487 "xy/b/c/z",
10488 "src/a.ts/b/c",
10489 "src/a/b/c.ts",
10490 "a/b/c/d.ts",
10491 "b.ts",
10492 "a/b.ts",
10493 "src/b.ts",
10494 "src/ab.ts",
10495 "src/a/b.ts",
10496 "src/ax/b.ts",
10497 "x/c.ts",
10498 "x/a/b/c.ts",
10499 "ax/b/c.ts",
10500 "q/a/b.ts",
10501 "q/ab.ts",
10502 "a/b/z",
10503 "a/bx/z",
10504 "a/b/c/z",
10505 "x/z",
10506 "x/zq",
10507 "x/a/z",
10508 "x/a/b",
10509 "lua/a.lua",
10510 "x/lua/a.lua",
10511 "x/lua/a/b.lua",
10512 "x/y/lua/a.lua",
10513 "x",
10514 "z",
10515 "a/z",
10516 ".z",
10517 "x/.z",
10518 "sz",
10519 ];
10520 for recursive in [false, true] {
10521 let options = PatternOptions::default()
10522 .extglob(true)
10523 .recursive_double_star(recursive);
10524 for (extglob, plain) in pairs {
10525 let extglob = Pattern::compile(extglob, options).expect("pattern compiles");
10526 let plain = Pattern::compile(plain, options).expect("pattern compiles");
10527 for candidate in candidates {
10528 assert_eq!(
10529 (
10530 extglob.is_match(candidate),
10531 extglob.is_match_path(candidate),
10532 extglob.is_match_glob_path(candidate)
10533 ),
10534 (
10535 plain.is_match(candidate),
10536 plain.is_match_path(candidate),
10537 plain.is_match_glob_path(candidate)
10538 ),
10539 "{extglob:?} against {candidate} (recursive {recursive})"
10540 );
10541 assert!(extglob.engines_agree(candidate), "{candidate}");
10542 }
10543 }
10544 }
10545
10546 let options = PatternOptions::default()
10549 .extglob(true)
10550 .recursive_double_star(true);
10551 for (pattern, candidate) in [("a/@(x|**)/*.ts", "a/b/c/d.ts"), ("*/@(**)/z", "a/b/c/z")] {
10552 let pattern = Pattern::compile(pattern, options).expect("pattern compiles");
10553 assert!(pattern.is_match_path(candidate), "{candidate}");
10554 assert!(pattern.is_match_glob_path(candidate), "{candidate}");
10555 assert!(pattern.engines_agree(candidate), "{candidate}");
10556 }
10557 }
10558
10559 #[test]
10565 fn extglob_groups_around_a_globstar_read_like_their_inlined_alternatives() {
10566 let inlined: &[(&str, &[&str])] = &[
10567 ("@(a|b)/**", &["a/**", "b/**"]),
10568 ("?(a)/**", &["a/**", "/**"]),
10569 ("a/@(**)", &["a/**"]),
10570 ("a/?(**|b)", &["a/**", "a/b", "a/"]),
10571 ("@(**)/a", &["**/a"]),
10572 ("@(**|b)/a", &["**/a", "b/a"]),
10573 ("?(**)/a", &["**/a", "/a"]),
10574 ("@(**)/.a", &["**/.a"]),
10575 ("@(**)/*", &["**/*"]),
10576 ("@(**)/?", &["**/?"]),
10577 ("a/@(**)/b", &["a/**/b"]),
10578 ("@(a/**)/b", &["a/**/b"]),
10579 ("@(**/**)/a", &["**/**/a"]),
10580 ("@(a/**/**)/b", &["a/**/**/b"]),
10581 ("@(**/a|**)/b", &["**/a/b", "**/b"]),
10582 ("@(**)//a", &["**//a"]),
10583 ("/@(**)/", &["/**/"]),
10584 ("@(****)/a", &["****/a"]),
10585 ("@(***)/a", &["***/a"]),
10586 ];
10587 let candidates = byte_words(b"ab./", 4);
10588 for match_hidden in [false, true] {
10589 let options = PatternOptions::default()
10590 .extglob(true)
10591 .recursive_double_star(true)
10592 .match_hidden(match_hidden);
10593 for &(source, alternatives) in inlined {
10594 let group = Pattern::compile(source, options).expect("pattern compiles");
10595 let mut interpreted = group.clone();
10596 for alternative in interpreted.compiled_alternatives_mut() {
10597 if let Some(program) = alternative.extglob.as_mut() {
10598 program.positive_nfa = None;
10599 }
10600 }
10601 let alternatives = alternatives
10602 .iter()
10603 .map(|alternative| Pattern::compile(alternative, options).unwrap())
10604 .collect::<Vec<_>>();
10605 let answers = |pattern: &Pattern, candidate: &[u8]| {
10606 [
10607 pattern.is_match(candidate),
10608 pattern.is_match_path(candidate),
10609 pattern.is_match_glob_path(candidate),
10610 ]
10611 };
10612 for candidate in &candidates {
10613 if candidate.starts_with(b"./") {
10617 continue;
10618 }
10619 let mut expected = alternatives.iter().fold([false; 3], |any, alternative| {
10620 let answers = answers(alternative, candidate);
10621 [0, 1, 2].map(|entry| any[entry] || answers[entry])
10622 });
10623 let mut actual = answers(&group, candidate);
10624 let mut actual_interpreted = answers(&interpreted, candidate);
10625 if candidate.first() == Some(&b'/')
10633 || candidate.windows(2).any(|pair| pair == b"//")
10634 {
10635 expected[1] = false;
10636 actual[1] = false;
10637 actual_interpreted[1] = false;
10638 }
10639 let lossy = String::from_utf8_lossy(candidate);
10640 assert_eq!(
10641 actual, expected,
10642 "{source:?} against {lossy:?} (is_match, is_match_path, \
10643 is_match_glob_path), match_hidden {match_hidden}"
10644 );
10645 assert_eq!(
10646 actual_interpreted, expected,
10647 "interpreter: {source:?} against {lossy:?}, match_hidden {match_hidden}"
10648 );
10649 assert!(
10650 group.engines_agree(candidate),
10651 "{source:?} against {lossy:?}"
10652 );
10653 }
10654 }
10655 }
10656
10657 let options = PatternOptions::default()
10661 .extglob(true)
10662 .recursive_double_star(true);
10663 for (source, candidate, expected) in [
10664 ("!(y)/**", "x", true),
10665 ("!(y)/**", "y", false),
10666 ("!(y)/**", "x/a", true),
10667 ("*(a)/**", "a", true),
10668 ("*(a)/**", "aa", true),
10669 ("*(a)/**", "b", false),
10670 ("+(a)/**", "a", true),
10671 ("+(**)/a", "a", true),
10672 ("*(**)/a", "a", true),
10673 ("*(**)/a", "b/a", true),
10674 ("+(b|**)/a", "b/a", true),
10675 ("x/*(**)", "x", true),
10676 ("x/!(**)", "x", false),
10677 ("!(**)/a", "a", false),
10678 ] {
10679 let pattern = Pattern::compile(source, options).expect("pattern compiles");
10680 let mut interpreted = pattern.clone();
10681 for alternative in interpreted.compiled_alternatives_mut() {
10682 if let Some(program) = alternative.extglob.as_mut() {
10683 program.positive_nfa = None;
10684 }
10685 }
10686 for matcher in [&pattern, &interpreted] {
10687 assert_eq!(
10688 [
10689 matcher.is_match(candidate),
10690 matcher.is_match_path(candidate),
10691 matcher.is_match_glob_path(candidate),
10692 ],
10693 [expected; 3],
10694 "{source} against {candidate}"
10695 );
10696 }
10697 assert!(
10698 pattern.engines_agree(candidate),
10699 "{source} against {candidate}"
10700 );
10701 }
10702 }
10703
10704 #[test]
10705 fn component_deterministic_fast_path_matches_the_general_matcher() {
10706 let options = PatternOptions::default().case_insensitive(true);
10707 let fast = Pattern::compile("src/[ab]?.[Rr][Ss]", options).expect("pattern compiles");
10708 assert!(fast.path_filter.as_ref().is_some_and(|filter| {
10709 matches!(
10710 filter.compiled[0].fast_path,
10711 Some(FastPath::DeterministicTokens(_))
10712 )
10713 }));
10714 let mut general = fast.clone();
10715 for alternative in general.compiled_alternatives_mut() {
10716 alternative.fast_path = None;
10717 }
10718
10719 let mut candidates = vec![
10720 b"src/a1.rs".as_slice(),
10721 b"src/Bx.RS".as_slice(),
10722 b"src/c1.rs".as_slice(),
10723 b"src/a/.rs".as_slice(),
10724 b"src/.1.rs".as_slice(),
10725 b"src/a1.rs/extra".as_slice(),
10726 ]
10727 .into_iter()
10728 .map(ToOwned::to_owned)
10729 .collect::<Vec<_>>();
10730 candidates.extend(
10731 byte_words(b"ab./rRsS", 4)
10732 .into_iter()
10733 .map(|suffix| [b"src/".as_slice(), suffix.as_slice()].concat()),
10734 );
10735 for candidate in candidates {
10736 assert_eq!(
10737 fast.is_match_path(&candidate),
10738 general.is_match_path(&candidate),
10739 "path-list candidate: {}",
10740 String::from_utf8_lossy(&candidate)
10741 );
10742 assert_eq!(
10743 fast.is_match_glob_path(&candidate),
10744 general.is_match_glob_path(&candidate),
10745 "glob-path candidate: {}",
10746 String::from_utf8_lossy(&candidate)
10747 );
10748 }
10749 }
10750
10751 #[test]
10752 fn filter_paths_at_matches_relative_to_a_component_boundary() {
10753 let recursive = Pattern::compile(
10754 "**/*.c",
10755 PatternOptions::default().recursive_double_star(true),
10756 )
10757 .expect("recursive pattern compiles");
10758 let paths = [
10759 "/home/user/project/src/main.c",
10760 "/home/user/project/src/test/unit.c",
10761 "/home/user/project/lib/utils.c",
10762 "/home/user/project/docs/readme.md",
10763 "/home/user/project-other/ignored.c",
10764 "/short",
10765 ];
10766 let expected = vec![
10767 &"/home/user/project/src/main.c",
10768 &"/home/user/project/src/test/unit.c",
10769 &"/home/user/project/lib/utils.c",
10770 ];
10771 assert_eq!(
10772 recursive.filter_paths_at("/home/user/project/", &paths),
10773 expected
10774 );
10775 assert_eq!(
10776 recursive.filter_paths_at("/home/user/project", &paths),
10777 expected
10778 );
10779
10780 let literal = Pattern::compile("config.json", PatternOptions::default())
10781 .expect("literal pattern compiles");
10782 assert_eq!(
10783 literal.filter_paths_at(
10784 "/srv/data",
10785 &["/srv/data/config.json", "/srv/data/readme.md"],
10786 ),
10787 vec![&"/srv/data/config.json"]
10788 );
10789
10790 let dotted = Pattern::compile(
10791 "./**/*.c",
10792 PatternOptions::default().recursive_double_star(true),
10793 )
10794 .expect("dot-slash pattern compiles");
10795 assert_eq!(
10796 dotted.filter_paths_at(
10797 "/home/user/project",
10798 &[
10799 "/home/user/project/src/main.c",
10800 "/home/user/project/lib/utils.c"
10801 ],
10802 ),
10803 vec![
10804 &"/home/user/project/src/main.c",
10805 &"/home/user/project/lib/utils.c"
10806 ]
10807 );
10808 }
10809
10810 #[test]
10811 fn filter_path_indices_preserve_input_order_and_support_a_base() {
10812 let suffix =
10813 Pattern::compile("*.rs", PatternOptions::default()).expect("suffix pattern compiles");
10814 assert_eq!(
10815 suffix.filter_path_indices(&["foo.rs", "bar.txt", "baz.rs", "qux.md"]),
10816 vec![0, 2]
10817 );
10818 assert_eq!(
10819 suffix.filter_path_indices(&["z.rs", "a.rs", "m.rs"]),
10820 vec![0, 1, 2]
10821 );
10822
10823 let recursive = Pattern::compile(
10824 "src/**/*.rs",
10825 PatternOptions::default().recursive_double_star(true),
10826 )
10827 .expect("recursive pattern compiles");
10828 assert_eq!(
10829 recursive.filter_path_indices_at(
10830 "/home/me/proj",
10831 &[
10832 "/home/me/proj/src/main.rs",
10833 "/home/me/proj/src/lib/util.rs",
10834 "/home/me/proj/tests/test.rs",
10835 "/home/me/proj/Cargo.toml",
10836 ],
10837 ),
10838 vec![0, 1]
10839 );
10840 }
10841
10842 #[test]
10843 fn non_recursive_double_star_matches_one_star_per_entry_point() {
10844 let single = Pattern::compile("*/*.c", PatternOptions::default()).unwrap();
10845 let doubled = Pattern::compile("**/*.c", PatternOptions::default()).unwrap();
10846 for candidate in ["a.c", "d/a.c", "d/e/a.c", "d/e/a.rs"] {
10847 assert_eq!(
10848 doubled.is_match(candidate),
10849 single.is_match(candidate),
10850 "is_match differs for {candidate:?}"
10851 );
10852 assert_eq!(
10853 doubled.is_match_path(candidate),
10854 single.is_match_path(candidate),
10855 "is_match_path differs for {candidate:?}"
10856 );
10857 assert_eq!(
10858 doubled.is_match_glob_path(candidate),
10859 single.is_match_glob_path(candidate),
10860 "is_match_glob_path differs for {candidate:?}"
10861 );
10862 }
10863 }
10864
10865 #[test]
10866 fn filter_paths_normalizes_a_leading_dot_slash() {
10867 let options = PatternOptions::default().recursive_double_star(true);
10868 let bare = Pattern::compile("**/*.lua", options).unwrap();
10869 let dotted = Pattern::compile("./**/*.lua", options).unwrap();
10870 let paths = [
10871 "init.lua",
10872 "lua/setup.lua",
10873 "nested/deep/plugin.lua",
10874 "src/main.zig",
10875 ];
10876 assert_eq!(dotted.filter_paths(&paths), bare.filter_paths(&paths));
10877 assert!(!dotted.is_match("nested/deep/plugin.lua"));
10878
10879 for pattern in [&bare, &dotted] {
10880 assert!(pattern.is_match_path("lua/setup.lua"));
10881 assert!(pattern.is_match_path("./lua/setup.lua"));
10882 assert!(pattern.is_match_glob_path("lua/setup.lua"));
10883 assert!(pattern.is_match_glob_path("./lua/setup.lua"));
10884 }
10885
10886 let current_directory = compile("./");
10887 assert!(current_directory.is_match_path(""));
10888 assert!(current_directory.is_match_path("./"));
10889 assert!(current_directory.is_match_glob_path(""));
10890 assert!(current_directory.is_match_glob_path("./"));
10891 assert!(!current_directory.is_match(""));
10892 }
10893
10894 #[test]
10895 fn invalid_syntax_has_a_location() {
10896 let error = Pattern::compile("[abc", PatternOptions::default()).unwrap_err();
10897 assert_eq!(error.offset(), 0);
10898 assert_eq!(error.message(), "unclosed character class");
10899 assert!(compile("foo\\").is_match("foo\\"));
10900 }
10901
10902 #[test]
10903 fn invalid_classes_in_extglob_alternatives_are_compile_errors() {
10904 let options = PatternOptions::default().extglob(true);
10905 for (source, offset) in [
10906 ("@([)]", 2),
10907 ("@(a|[)]", 4),
10908 ("@(dead/[)]]/../x|src/main.rs)", 7),
10909 ] {
10910 let error = Pattern::compile(source, options)
10911 .expect_err("an invalid extglob alternative is rejected");
10912 assert_eq!(error.message(), "unclosed character class");
10913 assert_eq!(error.offset(), offset);
10914 }
10915 }
10916
10917 #[test]
10918 fn brace_expansion_errors_keep_source_offsets() {
10919 let options = PatternOptions::default().braces(true);
10920 for (pattern, offset) in [("abc{d,[}", 6), ("{a,b}[", 5), ("x{a,b}{c,[d}", 9)] {
10921 let error = Pattern::compile(pattern, options)
10922 .expect_err("the expanded alternative contains an unclosed class");
10923 assert_eq!(error.message(), "unclosed character class");
10924 assert_eq!(error.offset(), offset);
10925 assert_eq!(
10926 pattern.as_bytes().get(error.offset()),
10927 Some(&b'['),
10928 "{pattern} points into the caller's source"
10929 );
10930 }
10931 }
10932
10933 #[test]
10934 fn deeply_nested_unclosed_posix_openers_are_rejected() {
10935 let mut pattern = String::from("[");
10936 pattern.push_str("[:".repeat(32_768).as_str());
10937 let error = Pattern::compile(&pattern, PatternOptions::default()).unwrap_err();
10938 assert_eq!(error.offset(), 0);
10939 assert_eq!(error.message(), "unclosed character class");
10940 }
10941
10942 #[test]
10943 fn deeply_nested_posix_openers_with_a_final_bracket_compile_linearly() {
10944 let mut pattern = String::from("[");
10945 pattern.push_str("[:".repeat(32_768).as_str());
10946 pattern.push(']');
10947 assert!(Pattern::compile(&pattern, PatternOptions::default()).is_ok());
10948 }
10949
10950 #[test]
10951 fn invalid_posix_opener_does_not_hide_a_later_valid_class() {
10952 let pattern = Pattern::compile("[[:[:alpha:]]", PatternOptions::default())
10953 .expect("character class compiles");
10954 assert!(pattern.is_match("x"));
10955 assert!(!pattern.is_match("1"));
10956 }
10957
10958 #[test]
10959 fn has_wildcards_respects_brace_and_extglob_options() {
10960 let plain = PatternOptions::default();
10961 assert!(!Pattern::has_wildcards("literal", plain));
10962 assert!(Pattern::has_wildcards("file?.rs", plain));
10963 assert!(Pattern::has_wildcards("[[:alpha:]]", plain));
10964 assert!(!Pattern::has_wildcards("{src,lib}", plain));
10965 assert!(Pattern::has_wildcards(
10966 "{src,lib}",
10967 PatternOptions::default().braces(true)
10968 ));
10969 assert!(!Pattern::has_wildcards("@(src|lib)", plain));
10970 assert!(Pattern::has_wildcards(
10971 "@(src|lib)",
10972 PatternOptions::default().extglob(true)
10973 ));
10974 assert!(!Pattern::has_wildcards(
10975 "literal(",
10976 PatternOptions::default().extglob(true)
10977 ));
10978 }
10979
10980 fn only_the_general_engine(pattern: &mut Pattern) {
10983 pattern.alternative_fast_path = None;
10984 for alternative in pattern.compiled_alternatives_mut() {
10985 alternative.fast_path = None;
10986 }
10987 }
10988
10989 fn without_the_prefilter(pattern: &Pattern) -> Pattern {
10992 let mut unfiltered = pattern.clone();
10993 for alternative in unfiltered.compiled_alternatives_mut() {
10994 alternative.prefilter = Prefilter::default();
10995 }
10996 unfiltered
10997 }
10998
10999 #[test]
11008 fn the_prefilter_rejects_only_what_the_general_engine_rejects() {
11009 let plain = PatternOptions::default();
11010 let recursive = PatternOptions::default().recursive_double_star(true);
11011 let folded = recursive.case_insensitive(true);
11012 let escaped = plain.escape(true);
11013 let cases: [(&str, PatternOptions); 24] = [
11014 ("a*a*a*a*b", plain),
11016 ("*a*a*", plain),
11017 ("a*", plain),
11018 ("*a", plain),
11019 ("*", plain),
11020 ("?a*b?", plain),
11021 ("[ab]*b", plain),
11022 ("a/*/b", plain),
11023 ("a/**", recursive),
11024 ("**/a", recursive),
11025 ("**/*.b", recursive),
11026 ("**", recursive),
11027 ("a/**/b*b", recursive),
11028 ("A/**/*.B", folded),
11029 ("A*A*B", folded),
11030 ("./a/**/b", recursive),
11031 ("**/ab/**", recursive),
11033 ("**/a/*.b", recursive),
11034 ("*/ab/*", plain),
11035 ("*ab*", plain),
11036 ("*a\\/b*", escaped),
11037 ("./**/b./**", recursive),
11038 ("**/AB/**", folded),
11039 ("*A.B*", folded),
11040 ];
11041 let mut candidates = byte_words(b"ab./", 4);
11042 candidates.push(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec());
11043 candidates.push(b"a/b/a/b/a/b.b".to_vec());
11044 candidates.push(b"A/B/A.B".to_vec());
11045 candidates.push(b"a/AB/b".to_vec());
11046 candidates.push(b"./a/b./b".to_vec());
11047
11048 for (source, options) in cases {
11049 let mut filtered = Pattern::compile(source, options).expect("case pattern compiles");
11050 only_the_general_engine(&mut filtered);
11051 let unfiltered = without_the_prefilter(&filtered);
11052 for candidate in &candidates {
11053 for (entry, filtered, unfiltered) in [
11054 (
11055 "is_match",
11056 filtered.is_match(candidate),
11057 unfiltered.is_match(candidate),
11058 ),
11059 (
11060 "is_match_path",
11061 filtered.is_match_path(candidate),
11062 unfiltered.is_match_path(candidate),
11063 ),
11064 (
11065 "is_match_glob_path",
11066 filtered.is_match_glob_path(candidate),
11067 unfiltered.is_match_glob_path(candidate),
11068 ),
11069 ] {
11070 assert_eq!(
11071 filtered, unfiltered,
11072 "{source} via {entry} disagrees with the unfiltered engine on {candidate:?}"
11073 );
11074 }
11075 }
11076 }
11077 }
11078
11079 #[test]
11085 fn the_prefilter_leaves_out_the_separator_a_terminal_double_star_may_elide() {
11086 let recursive = PatternOptions::default().recursive_double_star(true);
11087 let terminal = Pattern::compile("ab/**", recursive).expect("terminal pattern compiles");
11088 let prefilter = &terminal.alternatives[0].prefilter;
11089 assert_eq!(
11090 prefilter.prefix, b"ab",
11091 "the elidable separator is left off"
11092 );
11093 assert_eq!(prefilter.suffix, b"", "a trailing run cannot hold a `**`");
11094 assert_eq!(prefilter.min_length, 2, "the separator is not counted");
11095 assert!(!prefilter.rejects(b"ab", false), "`ab/**` accepts `ab`");
11096
11097 let inner = Pattern::compile("ab/**/cd", recursive).expect("inner pattern compiles");
11099 let prefilter = &inner.alternatives[0].prefilter;
11100 assert_eq!(prefilter.prefix, b"ab/");
11101 assert_eq!(prefilter.suffix, b"cd");
11102 assert_eq!(prefilter.min_length, 5);
11103
11104 let mixed = Pattern::compile("a?[bc]*d", PatternOptions::default())
11106 .expect("mixed pattern compiles");
11107 let prefilter = &mixed.alternatives[0].prefilter;
11108 assert_eq!(prefilter.prefix, b"a");
11109 assert_eq!(prefilter.suffix, b"d");
11110 assert_eq!(prefilter.min_length, 4);
11111 }
11112
11113 #[test]
11115 fn the_prefilter_searches_for_the_longest_interior_run() {
11116 let recursive = PatternOptions::default().recursive_double_star(true);
11117 let needle = |source: &str, options: PatternOptions| {
11118 let pattern = Pattern::compile(source, options).expect("needle pattern compiles");
11119 pattern.alternatives[0]
11120 .prefilter
11121 .interior
11122 .as_ref()
11123 .map(|interior| interior.0.needle().to_vec())
11124 };
11125 assert_eq!(
11128 needle("**/node_modules/**", recursive).as_deref(),
11129 Some(b"node_modules".as_slice())
11130 );
11131 let lib = if cfg!(windows) { "lib" } else { "lib/" };
11134 assert_eq!(
11135 needle("**/lib/*.js", recursive).as_deref(),
11136 Some(lib.as_bytes())
11137 );
11138 assert_eq!(
11139 needle("src/**/test/*.rs", recursive).as_deref(),
11140 Some(if cfg!(windows) {
11141 b"test".as_slice()
11142 } else {
11143 b"test/"
11144 })
11145 );
11146 assert_eq!(
11148 needle("*a*bcd*ef*", PatternOptions::default()).as_deref(),
11149 Some(b"bcd".as_slice())
11150 );
11151 assert_eq!(
11152 needle("*ab*cd*", PatternOptions::default()).as_deref(),
11153 Some(b"ab".as_slice())
11154 );
11155 assert_eq!(
11157 needle("*a\\/b*", PatternOptions::default().escape(true)).as_deref(),
11158 Some(b"a/b".as_slice())
11159 );
11160 assert_eq!(needle("ab/**", recursive), None);
11163 assert_eq!(needle("ab*cd", PatternOptions::default()), None);
11164 assert_eq!(needle("src/main.rs", PatternOptions::default()), None);
11165 assert_eq!(needle("*/*", PatternOptions::default()), None);
11166 assert_eq!(needle("**/*", recursive), None);
11167
11168 let excluded = Pattern::compile("**/node_modules/**", recursive).unwrap();
11169 let prefilter = &excluded.alternatives[0].prefilter;
11170 assert!(prefilter.rejects(b"src/lib/index.js", false));
11171 assert!(!prefilter.rejects(b"a/node_modules/b", false));
11172 assert!(!prefilter.rejects(b"node_modules", false));
11173 assert!(!prefilter.rejects(b"a/NODE_MODULES/b", true));
11175 }
11176
11177 #[test]
11188 fn the_prefilter_agrees_with_the_unfiltered_engines_over_randomized_patterns() {
11189 let fragments: &[&[u8]] = &[
11190 b"ab",
11191 b"nm",
11192 b"a",
11193 b"B",
11194 b".",
11195 b"/",
11196 b"./",
11197 b"*",
11198 b"?",
11199 b"**",
11200 b"**/",
11201 b"/**",
11202 b"[ab]",
11203 b"[!a]",
11204 b"{ab,B}",
11205 b"{a/b,nm}",
11206 b"\\/",
11207 b"\\*",
11208 b"@(ab|B)",
11209 b"!(nm)",
11210 b"+(a)",
11211 b"*(ab/)",
11212 ];
11213 let path_pieces: &[&[u8]] = &[
11214 b"a", b"b", b"B", b"ab", b"AB", b"nm", b"NM", b".", b"/", b"./", b"a/b",
11215 ];
11216 let mut seed = 0x9E37_79B9_7F4A_7C15_u64;
11218 let mut next = move |bound: usize| {
11219 seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
11220 (usize::try_from(seed >> 33).expect("31 bits fit a usize")) % bound
11221 };
11222 let mut searched = 0_usize;
11223 for _ in 0..3_000 {
11224 let mut pattern = Vec::new();
11225 for _ in 0..1 + next(7) {
11226 pattern.extend_from_slice(fragments[next(fragments.len())]);
11227 }
11228 let options = PatternOptions::default()
11229 .braces(next(2) == 0)
11230 .recursive_double_star(next(4) != 0)
11231 .extglob(next(3) == 0)
11232 .match_hidden(next(2) == 0)
11233 .case_insensitive(next(4) == 0)
11234 .escape(next(2) == 0);
11235 let Ok(mut filtered) = Pattern::compile(&pattern, options) else {
11236 continue;
11237 };
11238 filtered.strip_engines(true, false, false);
11239 let mut unfiltered = filtered.clone();
11240 unfiltered.strip_engines(false, false, true);
11241 searched += usize::from(
11242 filtered
11243 .alternatives
11244 .iter()
11245 .any(|alternative| alternative.prefilter.interior.is_some()),
11246 );
11247 for _ in 0..32 {
11248 let mut path = Vec::new();
11249 for _ in 0..next(9) {
11250 path.extend_from_slice(path_pieces[next(path_pieces.len())]);
11251 }
11252 let shown = || {
11253 format!(
11254 "{:?} against {:?} under {options:?}",
11255 String::from_utf8_lossy(&pattern),
11256 String::from_utf8_lossy(&path)
11257 )
11258 };
11259 assert_eq!(
11260 filtered.is_match(&path),
11261 unfiltered.is_match(&path),
11262 "is_match: {}",
11263 shown()
11264 );
11265 assert_eq!(
11266 filtered.is_match_path(&path),
11267 unfiltered.is_match_path(&path),
11268 "is_match_path: {}",
11269 shown()
11270 );
11271 assert_eq!(
11272 filtered.is_match_glob_path(&path),
11273 unfiltered.is_match_glob_path(&path),
11274 "is_match_glob_path: {}",
11275 shown()
11276 );
11277 assert_eq!(
11280 filtered.is_match_crossing_path(&path),
11281 unfiltered.is_match_crossing_path(&path),
11282 "is_match_crossing_path: {}",
11283 shown()
11284 );
11285 }
11286 }
11287 assert!(
11288 searched > 500,
11289 "only {searched} generated patterns carry an interior search"
11290 );
11291 }
11292
11293 fn assert_sweep_agrees(pattern: &[u8], options: PatternOptions, candidates: &[Vec<u8>]) {
11297 let compiled = Pattern::compile(pattern, options).expect("differential pattern compiles");
11298 let mut sweep_only = compiled.clone();
11299 sweep_only.strip_engines(true, false, false);
11300 let mut memoized = sweep_only.clone();
11301 memoized.strip_engines(false, true, true);
11302 for candidate in candidates {
11303 let name = String::from_utf8_lossy(pattern);
11304 let shown = String::from_utf8_lossy(candidate);
11305 assert_eq!(
11306 sweep_only.is_match(candidate),
11307 memoized.is_match(candidate),
11308 "is_match diverges for {name:?} against {shown:?} under {options:?}"
11309 );
11310 assert_eq!(
11311 sweep_only.is_match_path(candidate),
11312 memoized.is_match_path(candidate),
11313 "is_match_path diverges for {name:?} against {shown:?} under {options:?}"
11314 );
11315 assert_eq!(
11316 sweep_only.is_match_glob_path(candidate),
11317 memoized.is_match_glob_path(candidate),
11318 "is_match_glob_path diverges for {name:?} against {shown:?} under {options:?}"
11319 );
11320 assert!(
11321 compiled.engines_agree(candidate),
11322 "an engine diverges for {name:?} against {shown:?} under {options:?}"
11323 );
11324 }
11325 }
11326
11327 #[test]
11328 fn sweep_engine_agrees_with_the_memoized_matcher_over_exhaustive_byte_words() {
11329 let words = byte_words(b"aB./", 4);
11332 let patterns: &[&[u8]] = &[
11333 b"*",
11334 b"?",
11335 b"a",
11336 b"*a",
11337 b"a*",
11338 b"*a*",
11339 b"a*B",
11340 b"*.*",
11341 b"a*a*a*B",
11342 b"?a?",
11343 b"[aB]",
11344 b"[!a]*",
11345 b"[.]a",
11346 b"[a-b]*",
11347 b"[[:alpha:]]?",
11348 b"*/",
11349 b"/*",
11350 b"a/*",
11351 b"*/a",
11352 b"a/?",
11353 b"a/??",
11354 b"**",
11355 b"**/",
11356 b"a/**",
11357 b"**/a",
11358 b"a/**/B",
11359 b"**/*.a",
11360 b"a/?*B",
11361 b"./a/*",
11362 b"\\*a",
11363 b"a\\/B",
11364 b"\\.a",
11365 ];
11366 for &pattern in patterns {
11367 for recursive in [false, true] {
11368 for match_hidden in [false, true] {
11369 for case_insensitive in [false, true] {
11370 let options = PatternOptions::default()
11371 .recursive_double_star(recursive)
11372 .match_hidden(match_hidden)
11373 .case_insensitive(case_insensitive);
11374 assert_sweep_agrees(pattern, options, &words);
11375 }
11376 }
11377 }
11378 }
11379 }
11380
11381 #[test]
11382 fn sweep_engine_agrees_for_brace_alternatives_over_exhaustive_byte_words() {
11383 let words = byte_words(b"aB./", 4);
11384 for &pattern in [b"{a,B}*".as_slice(), b"{a/**,B?}", b"*.{a,B}"].iter() {
11385 for match_hidden in [false, true] {
11386 let options = PatternOptions::default()
11387 .braces(true)
11388 .recursive_double_star(true)
11389 .match_hidden(match_hidden);
11390 assert_sweep_agrees(pattern, options, &words);
11391 }
11392 }
11393 }
11394
11395 #[test]
11396 fn sweep_engine_agrees_over_randomized_patterns_and_paths() {
11397 let fragments: &[&[u8]] = &[
11401 b"a", b"B", b".", b"/", b"*", b"?", b"**", b"[aB]", b"[!a]", b"[.-b]", b"{a,B}", b"\\*",
11402 ];
11403 let path_bytes = b"aB./";
11404 let mut seed = 0x0123_4567_89AB_CDEF_u64;
11405 let mut next = move |bound: usize| {
11406 seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
11407 (usize::try_from(seed >> 33).expect("31 bits fit a usize")) % bound
11408 };
11409 for _ in 0..2_000 {
11410 let mut pattern = Vec::new();
11411 for _ in 0..next(8) {
11412 pattern.extend_from_slice(fragments[next(fragments.len())]);
11413 }
11414 let options = PatternOptions::default()
11415 .braces(next(2) == 0)
11416 .recursive_double_star(next(2) == 0)
11417 .match_hidden(next(2) == 0)
11418 .case_insensitive(next(2) == 0)
11419 .escape(next(2) == 0);
11420 let Ok(compiled) = Pattern::compile(&pattern, options) else {
11421 continue;
11422 };
11423 for _ in 0..24 {
11424 let mut path = Vec::new();
11425 for _ in 0..next(12) {
11426 path.push(path_bytes[next(path_bytes.len())]);
11427 }
11428 assert!(
11429 compiled.engines_agree(&path),
11430 "engines diverge for {:?} against {:?} under {options:?}",
11431 String::from_utf8_lossy(&pattern),
11432 String::from_utf8_lossy(&path)
11433 );
11434 }
11435 }
11436 }
11437
11438 #[test]
11439 fn sweep_engine_is_present_exactly_where_the_general_path_needs_it() {
11440 let starred = Pattern::compile("a*a*B", PatternOptions::default()).unwrap();
11441 assert!(starred.alternatives[0].sweep.is_some());
11442
11443 let literal = Pattern::compile("src/main.rs", PatternOptions::default()).unwrap();
11445 assert!(literal.alternatives[0].sweep.is_none());
11446 let deterministic = Pattern::compile("src/[ab]?.rs", PatternOptions::default()).unwrap();
11447 assert!(deterministic.alternatives[0].sweep.is_none());
11448
11449 let extglob = Pattern::compile("@(a|b)*", PatternOptions::default().extglob(true)).unwrap();
11451 assert!(extglob.alternatives[0].sweep.is_none());
11452
11453 let oversized_source = [b"a".repeat(70), b"*B".to_vec()].concat();
11456 let oversized = Pattern::compile(&oversized_source, PatternOptions::default()).unwrap();
11457 assert!(oversized.alternatives[0].sweep.is_some());
11458 let candidate = [b"a".repeat(70), b"xB".to_vec()].concat();
11459 assert!(oversized.is_match(&candidate));
11460 assert!(oversized.engines_agree(&candidate));
11461 }
11462
11463 #[test]
11464 fn sweeps_behind_a_complete_fast_path_are_built_only_where_they_run() {
11465 let options = PatternOptions::walker();
11468 for source in ["*.rs", "**/*.rs", "src/**/*.rs"] {
11469 let pattern = Pattern::compile(source, options).unwrap();
11470 let alternative = &pattern.alternatives[0];
11471 assert!(
11472 alternative
11473 .fast_path
11474 .as_ref()
11475 .is_some_and(FastPath::supports_component_wildcards),
11476 "{source} keeps its complete fast path"
11477 );
11478 assert!(alternative.sweep.is_none(), "{source} builds no engine");
11479 assert!(sweep_is_deferred(alternative));
11480 let mut sweep_only = pattern.clone();
11482 sweep_only.strip_engines(true, false, false);
11483 assert!(sweep_only.alternatives[0].sweep.is_some());
11484 for path in ["lib.rs", "src/lib.rs", "src/.x.rs", "src/lib.ts"] {
11485 assert!(pattern.engines_agree(path), "{source} against {path}");
11486 }
11487 }
11488
11489 let extglob = Pattern::compile("*(*.rs|x)", options).unwrap();
11492 let program = extglob.alternatives[0].extglob.as_ref().unwrap();
11493 let arm = &program.groups[0].alternatives[0].compiled[0];
11494 assert!(matches!(arm.fast_path, Some(FastPath::StarSuffix { .. })));
11495 assert!(arm.sweep.is_some());
11496
11497 let mut deferred = IrBudget::new();
11500 Pattern::compile_within(
11501 b"*.rs",
11502 options,
11503 &mut deferred,
11504 &mut ProvenanceBudget::new(),
11505 None,
11506 false,
11507 ComponentBounds::WHOLE,
11508 )
11509 .unwrap();
11510 let mut built = IrBudget::new();
11511 let tokens = [Token::Star, Token::Literal(b".rs".to_vec())];
11512 built.charge(tokens.len(), 0).unwrap();
11513 SweepEngine::compile(&tokens, options, &mut built).unwrap();
11514 assert_eq!(deferred.remaining, built.remaining);
11515 }
11516
11517 #[test]
11518 fn list_filter_copies_share_their_originals_engine() {
11519 let pattern = Pattern::compile("docs/*.md", PatternOptions::walker()).unwrap();
11523 let filter = pattern.path_filter.as_ref().expect("a component copy");
11524 let (Some(original), Some(copy)) =
11525 (&pattern.alternatives[0].sweep, &filter.compiled[0].sweep)
11526 else {
11527 panic!("both run the sweep");
11528 };
11529 assert!(original.shares_tables_with(copy));
11530 assert!(pattern.is_match_path("docs/a.md"));
11531 assert!(!pattern.is_match_path("docs/a/b.md"));
11532
11533 let dotted = Pattern::compile("./docs/*.md", PatternOptions::walker()).unwrap();
11535 let filter = dotted.path_filter.as_ref().expect("a stripped copy");
11536 assert!(filter.compiled[0].sweep.is_some());
11537 assert!(dotted.is_match_glob_path("docs/a.md"));
11538 }
11539
11540 #[test]
11541 fn wide_sweep_agrees_across_machine_word_boundaries() {
11542 let prefixes = [
11543 ["a".repeat(62), "***B".to_owned()].concat(),
11544 ["*a".repeat(40), "*B".to_owned()].concat(),
11545 ["a/".repeat(32), "**/B".to_owned()].concat(),
11546 ["[aB]?".repeat(33), "*".to_owned()].concat(),
11547 ];
11548 let mut candidates = Vec::new();
11549 for prefix in ["a".repeat(62), "a".repeat(80), "a/".repeat(32)] {
11550 for suffix in byte_words(b"aB./", 3) {
11551 candidates.push([prefix.as_bytes(), suffix.as_slice()].concat());
11552 }
11553 }
11554 for source in &prefixes {
11555 for match_hidden in [false, true] {
11556 let options = PatternOptions::default()
11557 .recursive_double_star(true)
11558 .match_hidden(match_hidden);
11559 assert_sweep_agrees(source.as_bytes(), options, &candidates);
11560 }
11561 }
11562 }
11563
11564 #[test]
11565 fn sweep_engine_keeps_the_terminal_recursive_star_end_of_path_case() {
11566 let options = PatternOptions::default().recursive_double_star(true);
11567 assert_sweep_agrees(
11570 b"src/**",
11571 options,
11572 &[
11573 b"src".to_vec(),
11574 b"src/".to_vec(),
11575 b"sr".to_vec(),
11576 b"src/a".to_vec(),
11577 b"src/.a".to_vec(),
11578 ],
11579 );
11580 assert_sweep_agrees(
11581 b"src/**/",
11582 options,
11583 &[b"src".to_vec(), b"src/".to_vec(), b"src/a/".to_vec()],
11584 );
11585 let terminal = Pattern::compile("src/**", options).unwrap();
11586 assert!(terminal.is_match("src"));
11587 let prefixed = Pattern::compile("src/**/", options).unwrap();
11588 assert!(!prefixed.is_match("src"));
11589 }
11590
11591 fn byte_words(alphabet: &[u8], max_length: usize) -> Vec<Vec<u8>> {
11592 let mut words = vec![Vec::new()];
11593 let mut current = vec![Vec::new()];
11594 for _ in 0..max_length {
11595 let mut next = Vec::new();
11596 for prefix in current {
11597 for &byte in alphabet {
11598 let mut word = prefix.clone();
11599 word.push(byte);
11600 next.push(word);
11601 }
11602 }
11603 words.extend(next.iter().cloned());
11604 current = next;
11605 }
11606 words
11607 }
11608
11609 #[derive(Clone, Copy, Debug)]
11611 enum ReferenceReach {
11612 Crossing,
11614 PathFilter,
11616 GlobPath,
11618 }
11619
11620 fn reference_glob_match(
11632 pattern: &[u8],
11633 path: &[u8],
11634 recursive: bool,
11635 reach: ReferenceReach,
11636 match_hidden: bool,
11637 ) -> bool {
11638 fn component_start(path: &[u8], index: usize) -> bool {
11639 index == 0 || path[index - 1] == b'/'
11640 }
11641 fn hidden_at(path: &[u8], index: usize, match_hidden: bool) -> bool {
11642 !match_hidden && path.get(index) == Some(&b'.') && component_start(path, index)
11643 }
11644 fn globstar_at(pattern: &[u8], index: usize) -> bool {
11645 pattern.get(index..index + 2) == Some(b"**")
11646 && pattern.get(index + 2) != Some(&b'*')
11647 && (index == 0 || pattern[index - 1] == b'/')
11648 && pattern.get(index + 2).is_none_or(|&byte| byte == b'/')
11649 }
11650 #[allow(clippy::too_many_arguments)]
11651 fn go(
11652 pattern: &[u8],
11653 path: &[u8],
11654 p: usize,
11655 s: usize,
11656 recursive: bool,
11657 reach: ReferenceReach,
11658 match_hidden: bool,
11659 memo: &mut std::collections::HashMap<(usize, usize), bool>,
11660 ) -> bool {
11661 if let Some(&known) = memo.get(&(p, s)) {
11662 return known;
11663 }
11664 let local = match reach {
11665 ReferenceReach::Crossing => false,
11666 ReferenceReach::GlobPath => true,
11667 ReferenceReach::PathFilter => {
11671 p > 0
11672 && pattern[p - 1] == b'/'
11673 && !(recursive && p >= 3 && globstar_at(pattern, p - 3))
11674 }
11675 };
11676 let mut recurse = |p, s| go(pattern, path, p, s, recursive, reach, match_hidden, memo);
11677 let result = if p == pattern.len() {
11678 s == path.len()
11679 } else if recursive && globstar_at(pattern, p) {
11680 if p + 2 == pattern.len() {
11681 (s..path.len()).all(|index| !hidden_at(path, index, match_hidden))
11683 } else {
11684 recurse(p + 3, s) || {
11686 let end = path[s..]
11687 .iter()
11688 .position(|&byte| byte == b'/')
11689 .map(|offset| s + offset);
11690 end.is_some_and(|end| {
11691 !hidden_at(path, s, match_hidden) && recurse(p, end + 1)
11692 })
11693 }
11694 }
11695 } else {
11696 match pattern[p] {
11697 b'*' => {
11698 let stop = !hidden_at(path, s, match_hidden) && recurse(p + 1, s);
11699 stop || (s < path.len()
11700 && !(local && path[s] == b'/')
11701 && !hidden_at(path, s, match_hidden)
11702 && recurse(p, s + 1))
11703 }
11704 b'?' => {
11705 s < path.len()
11706 && !(local && path[s] == b'/')
11707 && !hidden_at(path, s, match_hidden)
11708 && recurse(p + 1, s + 1)
11709 }
11710 b'/' if recursive
11711 && s == path.len()
11712 && p + 3 == pattern.len()
11713 && globstar_at(pattern, p + 1) =>
11714 {
11715 true
11716 }
11717 byte => path.get(s) == Some(&byte) && recurse(p + 1, s + 1),
11718 }
11719 };
11720 memo.insert((p, s), result);
11721 result
11722 }
11723 go(
11724 pattern,
11725 path,
11726 0,
11727 0,
11728 recursive,
11729 reach,
11730 match_hidden,
11731 &mut std::collections::HashMap::new(),
11732 )
11733 }
11734
11735 #[test]
11738 fn every_engine_reads_double_star_as_a_whole_component_over_exhaustive_patterns() {
11739 let paths = byte_words(b"a./", 4);
11743 let mut patterns = byte_words(b"a*/.", 5);
11744 patterns.extend(byte_words(b"a*?/.", 4));
11745 patterns.sort_unstable();
11746 patterns.dedup();
11747 let mut compared = 0_usize;
11748 for pattern in patterns {
11749 if pattern.windows(3).any(|window| window == b"***") {
11752 continue;
11753 }
11754 for recursive in [false, true] {
11755 for match_hidden in [false, true] {
11756 let options = PatternOptions::default()
11757 .recursive_double_star(recursive)
11758 .match_hidden(match_hidden);
11759 let compiled = Pattern::compile(&pattern, options).expect("pattern compiles");
11760 let mut sweep_only = compiled.clone();
11761 sweep_only.strip_engines(true, false, false);
11762 let mut memoized = compiled.clone();
11763 memoized.strip_engines(true, true, true);
11764 for path in &paths {
11765 let expected = [
11766 ReferenceReach::Crossing,
11767 ReferenceReach::PathFilter,
11768 ReferenceReach::GlobPath,
11769 ]
11770 .map(|reach| {
11771 reference_glob_match(&pattern, path, recursive, reach, match_hidden)
11772 });
11773 for (engine, matcher) in [
11774 ("compiled", &compiled),
11775 ("sweep", &sweep_only),
11776 ("memoized", &memoized),
11777 ] {
11778 let actual = [
11779 matcher.is_match(path),
11780 if path.starts_with(b"./") || pattern.starts_with(b"./") {
11783 expected[1]
11784 } else {
11785 matcher.is_match_path(path)
11786 },
11787 if path.starts_with(b"./") || pattern.starts_with(b"./") {
11788 expected[2]
11789 } else {
11790 matcher.is_match_glob_path(path)
11791 },
11792 ];
11793 assert_eq!(
11794 actual,
11795 expected,
11796 "{engine} engine: {:?} against {:?} (is_match, is_match_path, \
11797 is_match_glob_path) under {options:?}",
11798 String::from_utf8_lossy(&pattern),
11799 String::from_utf8_lossy(path),
11800 );
11801 }
11802 compared += 1;
11803 }
11804 }
11805 }
11806 }
11807 assert!(
11808 compared > 500_000,
11809 "the sweep compared only {compared} pairs"
11810 );
11811 }
11812
11813 #[test]
11814 fn only_a_whole_component_star_run_compiles_to_recursive_tokens() {
11815 let options = PatternOptions::default()
11816 .recursive_double_star(true)
11817 .braces(true);
11818 let tokens = |source: &str| {
11819 Pattern::compile(source, options)
11820 .expect("pattern compiles")
11821 .alternatives
11822 .into_iter()
11823 .map(|alternative| alternative.tokens)
11824 .collect::<Vec<_>>()
11825 };
11826 let literal = |bytes: &[u8]| Token::Literal(bytes.to_vec());
11827 assert_eq!(
11828 tokens("**/x"),
11829 [vec![Token::RecursivePrefix, literal(b"x")]]
11830 );
11831 assert_eq!(
11832 tokens("a/**"),
11833 [vec![literal(b"a"), Token::Separator, Token::RecursiveStar]]
11834 );
11835 assert_eq!(
11836 tokens("a**/x"),
11837 [vec![
11838 literal(b"a"),
11839 Token::Star,
11840 Token::Star,
11841 Token::Separator,
11842 literal(b"x"),
11843 ]]
11844 );
11845 assert_eq!(
11846 tokens("**x"),
11847 [vec![Token::Star, Token::Star, literal(b"x")]]
11848 );
11849 assert_eq!(
11850 tokens("x/**y"),
11851 [vec![
11852 literal(b"x"),
11853 Token::Separator,
11854 Token::Star,
11855 Token::Star,
11856 literal(b"y")
11857 ]]
11858 );
11859 assert_eq!(
11861 tokens(r"a\/**"),
11862 [vec![literal(b"a/"), Token::Star, Token::Star]]
11863 );
11864 assert_eq!(
11866 tokens("***/x"),
11867 [vec![
11868 Token::RecursiveStar,
11869 Token::Star,
11870 Token::Separator,
11871 literal(b"x"),
11872 ]]
11873 );
11874 assert_eq!(
11876 tokens("{**,a**}/x"),
11877 [
11878 vec![Token::RecursivePrefix, literal(b"x")],
11879 vec![
11880 literal(b"a"),
11881 Token::Star,
11882 Token::Star,
11883 Token::Separator,
11884 literal(b"x"),
11885 ],
11886 ]
11887 );
11888 let ordinary = Pattern::compile("**/x", PatternOptions::default()).unwrap();
11890 assert_eq!(
11891 ordinary.alternatives[0].tokens,
11892 [Token::Star, Token::Star, Token::Separator, literal(b"x")]
11893 );
11894 }
11895
11896 #[test]
11897 fn extglob_alternatives_inherit_the_component_position_of_their_group() {
11898 let options = PatternOptions::default()
11899 .recursive_double_star(true)
11900 .extglob(true);
11901 let alternative_tokens = |source: &str| {
11902 let compiled = Pattern::compile(source, options).expect("pattern compiles");
11903 let program = compiled.alternatives[0]
11904 .extglob
11905 .as_ref()
11906 .expect("an extglob program")
11907 .clone();
11908 program.groups[0]
11909 .alternatives
11910 .iter()
11911 .map(|alternative| alternative.compiled[0].tokens.clone())
11912 .collect::<Vec<_>>()
11913 };
11914 assert_eq!(alternative_tokens("@(**)/y")[0], [Token::RecursiveStar]);
11915 assert_eq!(alternative_tokens("a/@(**)")[0], [Token::RecursiveStar]);
11916 assert_eq!(
11917 alternative_tokens("x@(**)/y")[0],
11918 [Token::Star, Token::Star]
11919 );
11920 assert_eq!(alternative_tokens("@(**)y")[0], [Token::Star, Token::Star]);
11921 assert_eq!(
11922 alternative_tokens("x@(**/y|z)")[0],
11923 [
11924 Token::Star,
11925 Token::Star,
11926 Token::Separator,
11927 Token::Literal(b"y".to_vec())
11928 ]
11929 );
11930 assert_eq!(
11931 alternative_tokens("@(y/**|z)x")[0],
11932 [
11933 Token::Literal(b"y".to_vec()),
11934 Token::Separator,
11935 Token::Star,
11936 Token::Star
11937 ]
11938 );
11939
11940 for (source, candidate, crossing, expected) in [
11943 ("@(**)/y", "a/b/y", true, true),
11944 ("@(**)/y", "a/b/y", false, true),
11945 ("@(**/x|z)", "a/b/x", false, true),
11946 ("x@(**)/y", "xa/b/y", false, false),
11947 ("x@(**)/y", "xa/y", false, true),
11948 ("@(**/x|z)", "sx", true, false),
11949 ("@(**/x|z)", "sx", false, false),
11950 ("@(**/x|z)", "a/b/x", true, true),
11951 ("+(**/x)", "a/x/b/x", true, true),
11952 ("+(**/x)", "a/sx", true, false),
11953 ("**/@(x)", "sx", true, false),
11954 ("**/@(x)", "a/x", false, true),
11955 ("a**/@(y)", "ay", true, false),
11956 ("a**/@(y)", "ab/y", false, true),
11957 ("a**/@(y)", "a/b/y", false, false),
11958 ] {
11959 let compiled = Pattern::compile(source, options).expect("pattern compiles");
11960 let actual = if crossing {
11961 compiled.is_match(candidate)
11962 } else {
11963 compiled.is_match_glob_path(candidate)
11964 };
11965 assert_eq!(actual, expected, "{source} against {candidate}");
11966 assert!(
11967 compiled.engines_agree(candidate),
11968 "{source} against {candidate}: engines disagree"
11969 );
11970 }
11971 }
11972}
11973
11974#[cfg(test)]
11975mod escape_tests {
11976 use super::{Pattern, PatternOptions, escape, escape_str};
11977
11978 const ALPHABET: &[u8] = b"\\*?[]{}(),|!@+-^./aAbZ: \x00\xc3\xa9\xff";
11982
11983 fn generator(mut seed: u64) -> impl FnMut(usize) -> usize {
11985 move |bound: usize| {
11986 seed = seed.wrapping_mul(0x2545_F491_4F6C_DD1D).wrapping_add(1);
11987 (usize::try_from(seed >> 33).expect("31 bits fit a usize")) % bound
11988 }
11989 }
11990
11991 fn escaping_options() -> Vec<PatternOptions> {
11993 (0..32_u8)
11994 .map(|bits| {
11995 PatternOptions::default()
11996 .braces(bits & 1 != 0)
11997 .recursive_double_star(bits & 2 != 0)
11998 .extglob(bits & 4 != 0)
11999 .match_hidden(bits & 8 != 0)
12000 .case_insensitive(bits & 16 != 0)
12001 })
12002 .collect()
12003 }
12004
12005 fn random_text(next: &mut impl FnMut(usize) -> usize, max_len: usize) -> Vec<u8> {
12006 (0..next(max_len + 1))
12007 .map(|_| ALPHABET[next(ALPHABET.len())])
12008 .collect()
12009 }
12010
12011 fn candidates(text: &[u8], next: &mut impl FnMut(usize) -> usize) -> Vec<Vec<u8>> {
12014 let mut candidates = vec![text.to_vec(), [b"./", text].concat()];
12015 if let Some(rest) = text.strip_prefix(b"./") {
12016 candidates.push(rest.to_vec());
12017 }
12018 for _ in 0..12 {
12019 let mut candidate = text.to_vec();
12020 let at = next(candidate.len() + 1);
12021 match next(4) {
12022 0 => candidate.insert(at, ALPHABET[next(ALPHABET.len())]),
12023 1 if at < candidate.len() => {
12024 candidate.remove(at);
12025 }
12026 2 if at < candidate.len() => candidate[at] = ALPHABET[next(ALPHABET.len())],
12027 3 if at < candidate.len() => candidate[at] ^= 0x20,
12028 _ => candidate = random_text(next, 6),
12029 }
12030 candidates.push(candidate);
12031 }
12032 candidates
12033 }
12034
12035 fn folded(bytes: &[u8], options: PatternOptions) -> Vec<u8> {
12036 if options.case_insensitive {
12037 bytes.to_ascii_lowercase()
12038 } else {
12039 bytes.to_vec()
12040 }
12041 }
12042
12043 fn without_dot_slash(bytes: &[u8]) -> &[u8] {
12045 bytes.strip_prefix(b"./").unwrap_or(bytes)
12046 }
12047
12048 #[test]
12056 fn escaped_text_matches_exactly_itself_under_every_option() {
12057 let options = escaping_options();
12058 let mut next = generator(0x0E5C_A9ED_7E57_0001);
12059 for _ in 0..1_500 {
12060 let text = random_text(&mut next, 10);
12061 let escaped = escape(&text);
12062 if let Ok(utf8) = std::str::from_utf8(&text) {
12063 assert_eq!(escape_str(utf8).as_bytes(), escaped, "{utf8:?}");
12064 }
12065 let candidates = candidates(&text, &mut next);
12066 for &options in &options {
12067 let shown = || format!("{:?} under {options:?}", String::from_utf8_lossy(&text));
12068 let pattern = Pattern::compile(&escaped, options)
12069 .unwrap_or_else(|error| panic!("{error}: {}", shown()));
12070 for candidate in &candidates {
12071 if cfg!(windows) && (text.contains(&b'\\') || candidate.contains(&b'\\')) {
12072 continue;
12073 }
12074 let whole = folded(candidate, options) == folded(&text, options);
12075 let path = folded(without_dot_slash(candidate), options)
12076 == folded(without_dot_slash(&text), options);
12077 let candidate_shown = String::from_utf8_lossy(candidate);
12078 assert_eq!(
12079 pattern.is_match(candidate),
12080 whole,
12081 "is_match({candidate_shown:?}): {}",
12082 shown()
12083 );
12084 assert_eq!(
12085 pattern.is_match_path(candidate),
12086 path,
12087 "is_match_path({candidate_shown:?}): {}",
12088 shown()
12089 );
12090 assert_eq!(
12091 pattern.is_match_glob_path(candidate),
12092 path,
12093 "is_match_glob_path({candidate_shown:?}): {}",
12094 shown()
12095 );
12096 }
12097 }
12098 }
12099 }
12100
12101 #[test]
12104 fn escaped_text_stays_literal_inside_a_group() {
12105 let mut next = generator(0x0E5C_A9ED_7E57_0002);
12106 for _ in 0..1_500 {
12107 let left = random_text(&mut next, 6);
12109 let right = random_text(&mut next, 6);
12110 if [&left, &right]
12111 .iter()
12112 .any(|text| text.contains(&b'/') || (cfg!(windows) && text.contains(&b'\\')))
12113 {
12114 continue;
12115 }
12116 let options = PatternOptions::walker().match_hidden(next(2) == 0);
12117 let braces = [b"{", &escape(&left)[..], b",", &escape(&right), b"}"].concat();
12118 let extglob = [b"@(", &escape(&left)[..], b"|", &escape(&right), b")"].concat();
12119 for pattern in [braces, extglob] {
12120 let shown = || {
12121 format!(
12122 "{:?} for {:?} and {:?}",
12123 String::from_utf8_lossy(&pattern),
12124 String::from_utf8_lossy(&left),
12125 String::from_utf8_lossy(&right)
12126 )
12127 };
12128 let compiled = Pattern::compile(&pattern, options)
12129 .unwrap_or_else(|error| panic!("{error}: {}", shown()));
12130 for candidate in candidates(&left, &mut next).iter().chain([&right]) {
12131 let expected = *candidate == left || *candidate == right;
12132 assert_eq!(
12133 compiled.is_match(candidate),
12134 expected,
12135 "{:?}: {}",
12136 String::from_utf8_lossy(candidate),
12137 shown()
12138 );
12139 }
12140 }
12141 }
12142 }
12143
12144 #[test]
12147 fn escape_marks_every_syntax_byte_and_nothing_else() {
12148 assert_eq!(escape(br"\*?[]{}(),|!@+"), br"\\\*\?\[\]\{\}\(\)\,\|\!\@\+");
12149 assert_eq!(escape(b"./a-b^c/.d:e f\xff"), b"./a-b^c/.d:e f\xff");
12150 assert_eq!(escape(b""), b"");
12151 assert_eq!(escape(escape(b"*")), br"\\\*");
12152 assert_eq!(escape_str("caf\u{e9} [1]"), "caf\u{e9} \\[1\\]");
12153 }
12154
12155 #[test]
12158 fn escaped_text_needs_escaping_enabled() {
12159 let pattern = Pattern::compile(escape("a*"), PatternOptions::default().escape(false))
12160 .expect("valid pattern");
12161 assert!(!pattern.is_match("a*"));
12162 assert!(pattern.is_match(r"a\*"));
12163 }
12164}