1use std::{
2 collections::{VecDeque, hash_map::Entry},
3 mem::take,
4 sync::LazyLock,
5};
6
7use anyhow::{Result, bail};
8use bincode::{Decode, Encode};
9use regex::Regex;
10use rustc_hash::{FxHashMap, FxHashSet};
11use tracing::Instrument;
12use turbo_rcstr::{RcStr, rcstr};
13use turbo_tasks::{NonLocalValue, ReadRef, TaskInput, ValueToString, Vc, debug::ValueDebugFormat};
14use turbo_tasks_fs::{
15 FileSystemEntryType, FileSystemPath, LinkContent, RawDirectoryContent, RawDirectoryEntry,
16};
17use turbo_unix_path::normalize_path;
18
19#[turbo_tasks::value]
20#[derive(Hash, Clone, Debug, Default, ValueToString)]
21#[value_to_string(self.describe_as_string())]
22pub enum Pattern {
23 Constant(RcStr),
24 #[default]
25 Dynamic,
26 DynamicNoSlash,
27 Alternatives(Vec<Pattern>),
28 Concatenation(Vec<Pattern>),
29}
30
31impl TaskInput for Pattern {
34 fn is_transient(&self) -> bool {
35 false
37 }
38}
39
40fn concatenation_push_or_merge_item(list: &mut Vec<Pattern>, pat: Pattern) {
41 if let Pattern::Constant(ref s) = pat
42 && let Some(Pattern::Constant(last)) = list.last_mut()
43 {
44 let mut buf = last.to_string();
45 buf.push_str(s);
46 *last = buf.into();
47 return;
48 }
49 list.push(pat);
50}
51
52fn concatenation_push_front_or_merge_item(list: &mut Vec<Pattern>, pat: Pattern) {
53 if let Pattern::Constant(s) = pat {
54 if let Some(Pattern::Constant(first)) = list.iter_mut().next() {
55 let mut buf = s.into_owned();
56 buf.push_str(first);
57
58 *first = buf.into();
59 return;
60 }
61 list.insert(0, Pattern::Constant(s));
62 } else {
63 list.insert(0, pat);
64 }
65}
66
67fn concatenation_extend_or_merge_items(
68 list: &mut Vec<Pattern>,
69 mut iter: impl Iterator<Item = Pattern>,
70) {
71 if let Some(first) = iter.next() {
72 concatenation_push_or_merge_item(list, first);
73 list.extend(iter);
74 }
75}
76
77fn longest_common_prefix<'a>(strings: &[&'a str]) -> &'a str {
78 if strings.is_empty() {
79 return "";
80 }
81 if let [single] = strings {
82 return single;
83 }
84 let first = strings[0];
85 let mut len = first.len();
86 for str in &strings[1..] {
87 len = std::cmp::min(
88 len,
89 str.chars()
91 .zip(first.chars())
92 .take_while(|&(a, b)| a == b)
93 .count(),
94 );
95 }
96 &first[..len]
97}
98
99fn longest_common_suffix<'a>(strings: &[&'a str]) -> &'a str {
100 if strings.is_empty() {
101 return "";
102 }
103 let first = strings[0];
104 let mut len = first.len();
105 for str in &strings[1..] {
106 len = std::cmp::min(
107 len,
108 str.chars()
110 .rev()
111 .zip(first.chars().rev())
112 .take_while(|&(a, b)| a == b)
113 .count(),
114 );
115 }
116 &first[(first.len() - len)..]
117}
118
119impl Pattern {
120 pub fn as_constant_string(&self) -> Option<&RcStr> {
122 match self {
123 Pattern::Constant(str) => Some(str),
124 _ => None,
125 }
126 }
127
128 pub fn has_constant_parts(&self) -> bool {
131 match self {
132 Pattern::Constant(str) => str != "/",
133 Pattern::Dynamic | Pattern::DynamicNoSlash => false,
134 Pattern::Alternatives(list) | Pattern::Concatenation(list) => {
135 list.iter().any(|p| p.has_constant_parts())
136 }
137 }
138 }
139
140 pub fn has_dynamic_parts(&self) -> bool {
141 match self {
142 Pattern::Constant(_) => false,
143 Pattern::Dynamic | Pattern::DynamicNoSlash => true,
144 Pattern::Alternatives(list) | Pattern::Concatenation(list) => {
145 list.iter().any(|p| p.has_dynamic_parts())
146 }
147 }
148 }
149
150 pub fn filter_static(&self) -> Option<Pattern> {
154 if let Pattern::Alternatives(list) = self {
155 let mut static_alternatives = list
156 .iter()
157 .filter(|alternative| !alternative.has_dynamic_parts())
158 .cloned()
159 .collect::<Vec<_>>();
160 match static_alternatives.len() {
161 0 => None,
162 1 => static_alternatives.pop(),
163 _ => Some(Pattern::Alternatives(static_alternatives)),
164 }
165 } else if self.has_dynamic_parts() {
166 None
167 } else {
168 Some(self.clone())
169 }
170 }
171
172 pub fn constant_prefix(&self) -> &str {
173 if let Pattern::Constant(c) = self {
178 return c;
179 }
180
181 fn collect_constant_prefix<'a: 'b, 'b>(pattern: &'a Pattern, result: &mut Vec<&'b str>) {
182 match pattern {
183 Pattern::Constant(c) => {
184 result.push(c.as_str());
185 }
186 Pattern::Concatenation(list) => {
187 if let Some(Pattern::Constant(first)) = list.first() {
188 result.push(first.as_str());
189 }
190 }
191 Pattern::Alternatives(_) => {
192 panic!("for constant_prefix a Pattern must be normalized");
193 }
194 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
195 }
196 }
197
198 let mut strings: Vec<&str> = vec![];
199 match self {
200 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
201 collect_constant_prefix(c, &mut strings);
202 }
203 Pattern::Alternatives(list) => {
204 for c in list {
205 collect_constant_prefix(c, &mut strings);
206 }
207 }
208 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
209 }
210 longest_common_prefix(&strings)
211 }
212
213 pub fn constant_suffix(&self) -> &str {
214 fn collect_constant_suffix<'a: 'b, 'b>(pattern: &'a Pattern, result: &mut Vec<&'b str>) {
219 match pattern {
220 Pattern::Constant(c) => {
221 result.push(c.as_str());
222 }
223 Pattern::Concatenation(list) => {
224 if let Some(Pattern::Constant(first)) = list.last() {
225 result.push(first.as_str());
226 }
227 }
228 Pattern::Alternatives(_) => {
229 panic!("for constant_suffix a Pattern must be normalized");
230 }
231 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
232 }
233 }
234
235 let mut strings: Vec<&str> = vec![];
236 match self {
237 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
238 collect_constant_suffix(c, &mut strings);
239 }
240 Pattern::Alternatives(list) => {
241 for c in list {
242 collect_constant_suffix(c, &mut strings);
243 }
244 }
245 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
246 }
247 longest_common_suffix(&strings)
248 }
249
250 pub fn strip_prefix(&self, prefix: &str) -> Result<Option<Self>> {
251 if self.must_match(prefix) {
252 let mut pat = self.clone();
253 pat.strip_prefix_len(prefix.len())?;
254 Ok(Some(pat))
255 } else {
256 Ok(None)
257 }
258 }
259
260 pub fn strip_prefix_len(&mut self, len: usize) -> Result<()> {
261 fn strip_prefix_internal(pattern: &mut Pattern, chars_to_strip: &mut usize) -> Result<()> {
262 match pattern {
263 Pattern::Constant(c) => {
264 let c_len = c.len();
265 if *chars_to_strip >= c_len {
266 *c = rcstr!("");
267 } else {
268 *c = (&c[*chars_to_strip..]).into();
269 }
270 *chars_to_strip = (*chars_to_strip).saturating_sub(c_len);
271 }
272 Pattern::Concatenation(list) => {
273 for c in list {
274 if *chars_to_strip > 0 {
275 strip_prefix_internal(c, chars_to_strip)?;
276 }
277 }
278 }
279 Pattern::Alternatives(_) => {
280 bail!("strip_prefix pattern must be normalized");
281 }
282 Pattern::Dynamic | Pattern::DynamicNoSlash => {
283 bail!("strip_prefix prefix is too long");
284 }
285 }
286 Ok(())
287 }
288
289 match &mut *self {
290 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
291 let mut len_local = len;
292 strip_prefix_internal(c, &mut len_local)?;
293 }
294 Pattern::Alternatives(list) => {
295 for c in list {
296 let mut len_local = len;
297 strip_prefix_internal(c, &mut len_local)?;
298 }
299 }
300 Pattern::Dynamic | Pattern::DynamicNoSlash => {
301 if len > 0 {
302 bail!(
303 "strip_prefix prefix ({}) is too long: {}",
304 len,
305 self.describe_as_string()
306 );
307 }
308 }
309 };
310
311 self.normalize();
312
313 Ok(())
314 }
315
316 pub fn strip_suffix_len(&mut self, len: usize) {
317 fn strip_suffix_internal(pattern: &mut Pattern, chars_to_strip: &mut usize) {
318 match pattern {
319 Pattern::Constant(c) => {
320 let c_len = c.len();
321 if *chars_to_strip >= c_len {
322 *c = rcstr!("");
323 } else {
324 *c = (&c[..(c_len - *chars_to_strip)]).into();
325 }
326 *chars_to_strip = (*chars_to_strip).saturating_sub(c_len);
327 }
328 Pattern::Concatenation(list) => {
329 for c in list.iter_mut().rev() {
330 if *chars_to_strip > 0 {
331 strip_suffix_internal(c, chars_to_strip);
332 }
333 }
334 }
335 Pattern::Alternatives(_) => {
336 panic!("for strip_suffix a Pattern must be normalized");
337 }
338 Pattern::Dynamic | Pattern::DynamicNoSlash => {
339 panic!("strip_suffix suffix is too long");
340 }
341 }
342 }
343
344 match &mut *self {
345 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
346 let mut len_local = len;
347 strip_suffix_internal(c, &mut len_local);
348 }
349 Pattern::Alternatives(list) => {
350 for c in list {
351 let mut len_local = len;
352 strip_suffix_internal(c, &mut len_local);
353 }
354 }
355 Pattern::Dynamic | Pattern::DynamicNoSlash => {
356 if len > 0 {
357 panic!("strip_suffix suffix is too long");
358 }
359 }
360 };
361
362 self.normalize()
363 }
364
365 pub fn spread_into_star(&self, template: &str) -> Pattern {
370 if template.contains("*") {
371 let alternatives: Box<dyn Iterator<Item = &Pattern>> = match self {
372 Pattern::Alternatives(list) => Box::new(list.iter()),
373 c => Box::new(std::iter::once(c)),
374 };
375
376 let mut result = Pattern::alternatives(alternatives.map(|pat| {
377 let mut split = template.split("*");
378 let mut concatenation: Vec<Pattern> = Vec::with_capacity(3);
379
380 concatenation.push(Pattern::Constant(split.next().unwrap().into()));
382
383 for part in split {
384 concatenation.push(pat.clone());
385 if !part.is_empty() {
386 concatenation.push(Pattern::Constant(part.into()));
387 }
388 }
389 Pattern::Concatenation(concatenation)
390 }));
391
392 result.normalize();
393 result
394 } else {
395 Pattern::Constant(template.into())
396 }
397 }
398
399 pub fn extend(&mut self, concatenated: impl Iterator<Item = Self>) {
401 if let Pattern::Concatenation(list) = self {
402 concatenation_extend_or_merge_items(list, concatenated);
403 } else {
404 let mut vec = vec![take(self)];
405 for item in concatenated {
406 if let Pattern::Concatenation(more) = item {
407 concatenation_extend_or_merge_items(&mut vec, more.into_iter());
408 } else {
409 concatenation_push_or_merge_item(&mut vec, item);
410 }
411 }
412 *self = Pattern::Concatenation(vec);
413 }
414 }
415
416 pub fn push(&mut self, pat: Pattern) {
418 if let Pattern::Constant(this) = &*self
419 && this.is_empty()
420 {
421 *self = pat;
423 return;
424 }
425 if let Pattern::Constant(pat) = &pat
426 && pat.is_empty()
427 {
428 return;
430 }
431
432 match (self, pat) {
433 (Pattern::Concatenation(list), Pattern::Concatenation(more)) => {
434 concatenation_extend_or_merge_items(list, more.into_iter());
435 }
436 (Pattern::Concatenation(list), pat) => {
437 concatenation_push_or_merge_item(list, pat);
438 }
439 (this, Pattern::Concatenation(mut list)) => {
440 concatenation_push_front_or_merge_item(&mut list, take(this));
441 *this = Pattern::Concatenation(list);
442 }
443 (Pattern::Constant(str), Pattern::Constant(other)) => {
444 let mut buf = str.to_string();
445 buf.push_str(&other);
446 *str = buf.into();
447 }
448 (this, pat) => {
449 *this = Pattern::Concatenation(vec![take(this), pat]);
450 }
451 }
452 }
453
454 pub fn push_front(&mut self, pat: Pattern) {
456 match (self, pat) {
457 (Pattern::Concatenation(list), Pattern::Concatenation(mut more)) => {
458 concatenation_extend_or_merge_items(&mut more, take(list).into_iter());
459 *list = more;
460 }
461 (Pattern::Concatenation(list), pat) => {
462 concatenation_push_front_or_merge_item(list, pat);
463 }
464 (this, Pattern::Concatenation(mut list)) => {
465 concatenation_push_or_merge_item(&mut list, take(this));
466 *this = Pattern::Concatenation(list);
467 }
468 (Pattern::Constant(str), Pattern::Constant(other)) => {
469 let mut buf = other.into_owned();
470
471 buf.push_str(str);
472 *str = buf.into();
473 }
474 (this, pat) => {
475 *this = Pattern::Concatenation(vec![pat, take(this)]);
476 }
477 }
478 }
479
480 pub fn alternatives(alts: impl IntoIterator<Item = Pattern>) -> Self {
481 let mut list = Vec::new();
482 for alt in alts {
483 if let Pattern::Alternatives(inner) = alt {
484 list.extend(inner);
485 } else {
486 list.push(alt)
487 }
488 }
489 Self::Alternatives(list)
490 }
491
492 pub fn concat(items: impl IntoIterator<Item = Pattern>) -> Self {
493 let mut items = items.into_iter();
494 let mut current = items.next().unwrap_or_default();
495 for item in items {
496 current.push(item);
497 }
498 current
499 }
500
501 pub fn with_normalized_path(&self) -> Option<Pattern> {
508 let mut new = self.clone();
509
510 #[derive(Debug)]
511 enum PathElement {
512 Segment(Pattern),
513 Separator,
514 }
515
516 fn normalize_path_internal(pattern: &mut Pattern) -> Option<()> {
517 match pattern {
518 Pattern::Constant(c) => {
519 let normalized = c.replace('\\', "/");
520 *c = RcStr::from(normalize_path(normalized.as_str())?);
521 Some(())
522 }
523 Pattern::Dynamic | Pattern::DynamicNoSlash => Some(()),
524 Pattern::Concatenation(list) => {
525 let mut segments = Vec::new();
526 for segment in list.iter() {
527 match segment {
528 Pattern::Constant(str) => {
529 let mut iter = str.split('/').peekable();
530 while let Some(segment) = iter.next() {
531 match segment {
532 "." | "" => {
533 continue;
535 }
536 ".." => {
537 if segments.is_empty() {
538 return None;
540 }
541
542 if let Some(PathElement::Separator) = segments.last()
543 && let Some(PathElement::Segment(
544 Pattern::Constant(_),
545 )) = segments.get(segments.len() - 2)
546 {
547 segments.truncate(segments.len() - 2);
549 continue;
550 }
551
552 segments.push(PathElement::Segment(Pattern::Constant(
554 rcstr!(".."),
555 )));
556 }
557 segment => {
558 segments.push(PathElement::Segment(Pattern::Constant(
559 segment.into(),
560 )));
561 }
562 }
563
564 if iter.peek().is_some() {
565 segments.push(PathElement::Separator);
567 }
568 }
569 }
570 Pattern::Dynamic | Pattern::DynamicNoSlash => {
571 segments.push(PathElement::Segment(segment.clone()));
572 }
573 Pattern::Alternatives(_) | Pattern::Concatenation(_) => {
574 panic!("for with_normalized_path the Pattern must be normalized");
575 }
576 }
577 }
578 let separator = rcstr!("/");
579 *list = segments
580 .into_iter()
581 .map(|c| match c {
582 PathElement::Segment(p) => p,
583 PathElement::Separator => Pattern::Constant(separator.clone()),
584 })
585 .collect();
586 Some(())
587 }
588 Pattern::Alternatives(_) => {
589 panic!("for with_normalized_path the Pattern must be normalized");
590 }
591 }
592 }
593
594 match &mut new {
595 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
596 normalize_path_internal(c)?;
597 }
598 Pattern::Alternatives(list) => {
599 for c in list {
600 normalize_path_internal(c)?;
601 }
602 }
603 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
604 }
605
606 new.normalize();
607 Some(new)
608 }
609
610 pub fn normalize(&mut self) {
613 match self {
614 Pattern::Dynamic | Pattern::DynamicNoSlash | Pattern::Constant(_) => {
615 }
617 Pattern::Alternatives(list) => {
618 for alt in list.iter_mut() {
619 alt.normalize();
620 }
621 let mut new_alternatives = Vec::new();
622 let mut has_dynamic = false;
623 for alt in list.drain(..) {
624 if let Pattern::Alternatives(inner) = alt {
625 for alt in inner {
626 if alt == Pattern::Dynamic {
627 if !has_dynamic {
628 has_dynamic = true;
629 new_alternatives.push(alt);
630 }
631 } else {
632 new_alternatives.push(alt);
633 }
634 }
635 } else if alt == Pattern::Dynamic {
636 if !has_dynamic {
637 has_dynamic = true;
638 new_alternatives.push(alt);
639 }
640 } else {
641 new_alternatives.push(alt);
642 }
643 }
644 if new_alternatives.len() == 1 {
645 *self = new_alternatives.into_iter().next().unwrap();
646 } else {
647 *list = new_alternatives;
648 }
649 }
650 Pattern::Concatenation(list) => {
651 let mut has_alternatives = false;
652 for part in list.iter_mut() {
653 part.normalize();
654 if let Pattern::Alternatives(_) = part {
655 has_alternatives = true;
656 }
657 }
658 if has_alternatives {
659 let mut new_alternatives: Vec<Vec<Pattern>> = vec![Vec::new()];
663 for part in list.drain(..) {
664 if let Pattern::Alternatives(list) = part {
665 let mut combined = Vec::new();
667 for alt2 in list.iter() {
668 for mut alt in new_alternatives.clone() {
669 if let Pattern::Concatenation(parts) = alt2 {
670 alt.extend(parts.clone());
671 } else {
672 alt.push(alt2.clone());
673 }
674 combined.push(alt)
675 }
676 }
677 new_alternatives = combined;
678 } else {
679 for alt in new_alternatives.iter_mut() {
681 if let Pattern::Concatenation(ref parts) = part {
682 alt.extend(parts.clone());
683 } else {
684 alt.push(part.clone());
685 }
686 }
687 }
688 }
689 *self = Pattern::Alternatives(
692 new_alternatives
693 .into_iter()
694 .map(|parts| {
695 if parts.len() == 1 {
696 parts.into_iter().next().unwrap()
697 } else {
698 Pattern::Concatenation(parts)
699 }
700 })
701 .collect(),
702 );
703 self.normalize();
705 } else {
706 let mut new_parts = Vec::new();
707 for part in list.drain(..) {
708 fn add_part(part: Pattern, new_parts: &mut Vec<Pattern>) {
709 match part {
710 Pattern::Constant(c) => {
711 if !c.is_empty() {
712 if let Some(Pattern::Constant(last)) = new_parts.last_mut()
713 {
714 let mut buf = last.to_string();
715 buf.push_str(&c);
716 *last = buf.into();
717 } else {
718 new_parts.push(Pattern::Constant(c));
719 }
720 }
721 }
722 Pattern::Dynamic => {
723 if let Some(Pattern::Dynamic | Pattern::DynamicNoSlash) =
724 new_parts.last()
725 {
726 } else {
728 new_parts.push(Pattern::Dynamic);
729 }
730 }
731 Pattern::DynamicNoSlash => {
732 if let Some(Pattern::DynamicNoSlash) = new_parts.last() {
733 } else {
735 new_parts.push(Pattern::DynamicNoSlash);
736 }
737 }
738 Pattern::Concatenation(parts) => {
739 for part in parts {
740 add_part(part, new_parts);
741 }
742 }
743 Pattern::Alternatives(_) => unreachable!(),
744 }
745 }
746
747 add_part(part, &mut new_parts);
748 }
749 if new_parts.len() == 1 {
750 *self = new_parts.into_iter().next().unwrap();
751 } else {
752 *list = new_parts;
753 }
754 }
755 }
756 }
757 }
758
759 pub fn is_empty(&self) -> bool {
760 match self {
761 Pattern::Constant(s) => s.is_empty(),
762 Pattern::Dynamic | Pattern::DynamicNoSlash => false,
763 Pattern::Concatenation(parts) => parts.iter().all(|p| p.is_empty()),
764 Pattern::Alternatives(parts) => parts.iter().all(|p| p.is_empty()),
765 }
766 }
767
768 pub fn filter_could_match(&self, value: &str) -> Option<Pattern> {
769 if let Pattern::Alternatives(list) = self {
770 let new_list = list
771 .iter()
772 .filter(|alt| alt.could_match(value))
773 .cloned()
774 .collect::<Vec<_>>();
775 if new_list.is_empty() {
776 None
777 } else {
778 Some(Pattern::Alternatives(new_list))
779 }
780 } else if self.could_match(value) {
781 Some(self.clone())
782 } else {
783 None
784 }
785 }
786
787 pub fn filter_could_not_match(&self, value: &str) -> Option<Pattern> {
788 if let Pattern::Alternatives(list) = self {
789 let new_list = list
790 .iter()
791 .filter(|alt| !alt.could_match(value))
792 .cloned()
793 .collect::<Vec<_>>();
794 if new_list.is_empty() {
795 None
796 } else {
797 Some(Pattern::Alternatives(new_list))
798 }
799 } else if self.could_match(value) {
800 None
801 } else {
802 Some(self.clone())
803 }
804 }
805
806 pub fn split_could_match(&self, value: &str) -> (Option<Pattern>, Option<Pattern>) {
807 if let Pattern::Alternatives(list) = self {
808 let mut could_match_list = Vec::new();
809 let mut could_not_match_list = Vec::new();
810 for alt in list.iter() {
811 if alt.could_match(value) {
812 could_match_list.push(alt.clone());
813 } else {
814 could_not_match_list.push(alt.clone());
815 }
816 }
817 (
818 if could_match_list.is_empty() {
819 None
820 } else if could_match_list.len() == 1 {
821 Some(could_match_list.into_iter().next().unwrap())
822 } else {
823 Some(Pattern::Alternatives(could_match_list))
824 },
825 if could_not_match_list.is_empty() {
826 None
827 } else if could_not_match_list.len() == 1 {
828 Some(could_not_match_list.into_iter().next().unwrap())
829 } else {
830 Some(Pattern::Alternatives(could_not_match_list))
831 },
832 )
833 } else if self.could_match(value) {
834 (Some(self.clone()), None)
835 } else {
836 (None, Some(self.clone()))
837 }
838 }
839
840 pub fn is_match(&self, value: &str) -> bool {
841 if let Pattern::Alternatives(list) = self {
842 list.iter().any(|alt| {
843 alt.match_internal(value, None, InNodeModules::False, false)
844 .is_match()
845 })
846 } else {
847 self.match_internal(value, None, InNodeModules::False, false)
848 .is_match()
849 }
850 }
851
852 pub fn is_match_ignore_dynamic(&self, value: &str) -> bool {
855 if let Pattern::Alternatives(list) = self {
856 list.iter().any(|alt| {
857 alt.match_internal(value, None, InNodeModules::False, true)
858 .is_match()
859 })
860 } else {
861 self.match_internal(value, None, InNodeModules::False, true)
862 .is_match()
863 }
864 }
865
866 pub fn match_position(&self, value: &str) -> Option<usize> {
867 if let Pattern::Alternatives(list) = self {
868 list.iter().position(|alt| {
869 alt.match_internal(value, None, InNodeModules::False, false)
870 .is_match()
871 })
872 } else {
873 self.match_internal(value, None, InNodeModules::False, false)
874 .is_match()
875 .then_some(0)
876 }
877 }
878
879 pub fn could_match_others(&self, value: &str) -> bool {
880 if let Pattern::Alternatives(list) = self {
881 list.iter().any(|alt| {
882 alt.match_internal(value, None, InNodeModules::False, false)
883 .could_match_others()
884 })
885 } else {
886 self.match_internal(value, None, InNodeModules::False, false)
887 .could_match_others()
888 }
889 }
890
891 pub fn must_match(&self, value: &str) -> bool {
893 if let Pattern::Alternatives(list) = self {
894 list.iter().all(|alt| {
895 alt.match_internal(value, None, InNodeModules::False, false)
896 .could_match()
897 })
898 } else {
899 self.match_internal(value, None, InNodeModules::False, false)
900 .could_match()
901 }
902 }
903
904 pub fn could_match(&self, value: &str) -> bool {
906 if let Pattern::Alternatives(list) = self {
907 list.iter().any(|alt| {
908 alt.match_internal(value, None, InNodeModules::False, false)
909 .could_match()
910 })
911 } else {
912 self.match_internal(value, None, InNodeModules::False, false)
913 .could_match()
914 }
915 }
916
917 pub fn could_match_position(&self, value: &str) -> Option<usize> {
918 if let Pattern::Alternatives(list) = self {
919 list.iter().position(|alt| {
920 alt.match_internal(value, None, InNodeModules::False, false)
921 .could_match()
922 })
923 } else {
924 self.match_internal(value, None, InNodeModules::False, false)
925 .could_match()
926 .then_some(0)
927 }
928 }
929 fn match_internal<'a>(
930 &self,
931 mut value: &'a str,
932 mut any_offset: Option<usize>,
933 mut in_node_modules: InNodeModules,
934 ignore_dynamic: bool,
935 ) -> MatchResult<'a> {
936 match self {
937 Pattern::Constant(c) => {
938 if let Some(offset) = any_offset {
939 if let Some(index) = value.find(&**c) {
940 if index <= offset {
941 MatchResult::Consumed {
942 remaining: &value[index + c.len()..],
943 any_offset: None,
944 in_node_modules: InNodeModules::check(c),
945 }
946 } else {
947 MatchResult::None
948 }
949 } else if offset >= value.len() {
950 MatchResult::Partial
951 } else {
952 MatchResult::None
953 }
954 } else if value.starts_with(&**c) {
955 MatchResult::Consumed {
956 remaining: &value[c.len()..],
957 any_offset: None,
958 in_node_modules: InNodeModules::check(c),
959 }
960 } else if c.starts_with(value) {
961 MatchResult::Partial
962 } else {
963 MatchResult::None
964 }
965 }
966 Pattern::Dynamic | Pattern::DynamicNoSlash => {
967 static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
968 Regex::new(r"(/|^)(ROOT|\.|/|(node_modules|__tests?__)(/|$))").unwrap()
969 });
970 static FORBIDDEN_MATCH: LazyLock<Regex> =
971 LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
972 if in_node_modules == InNodeModules::FolderSlashMatched
973 || (in_node_modules == InNodeModules::FolderMatched && value.starts_with('/'))
974 {
975 MatchResult::None
976 } else if let Some(m) = FORBIDDEN.find(value) {
977 MatchResult::Consumed {
978 remaining: value,
979 any_offset: Some(m.start()),
980 in_node_modules: InNodeModules::False,
981 }
982 } else if FORBIDDEN_MATCH.find(value).is_some() {
983 MatchResult::Partial
984 } else if ignore_dynamic {
985 MatchResult::None
986 } else {
987 let match_length = matches!(self, Pattern::DynamicNoSlash)
988 .then(|| value.find("/"))
989 .flatten()
990 .unwrap_or(value.len());
991 MatchResult::Consumed {
992 remaining: value,
993 any_offset: Some(match_length),
994 in_node_modules: InNodeModules::False,
995 }
996 }
997 }
998 Pattern::Alternatives(_) => {
999 panic!("for matching a Pattern must be normalized {self:?}")
1000 }
1001 Pattern::Concatenation(list) => {
1002 for part in list {
1003 match part.match_internal(value, any_offset, in_node_modules, ignore_dynamic) {
1004 MatchResult::None => return MatchResult::None,
1005 MatchResult::Partial => return MatchResult::Partial,
1006 MatchResult::Consumed {
1007 remaining: new_value,
1008 any_offset: new_any_offset,
1009 in_node_modules: new_in_node_modules,
1010 } => {
1011 value = new_value;
1012 any_offset = new_any_offset;
1013 in_node_modules = new_in_node_modules
1014 }
1015 }
1016 }
1017 MatchResult::Consumed {
1018 remaining: value,
1019 any_offset,
1020 in_node_modules,
1021 }
1022 }
1023 }
1024 }
1025
1026 fn match_collect_internal<'a>(
1029 &self,
1030 mut value: &'a str,
1031 mut any_offset: Option<usize>,
1032 mut in_node_modules: InNodeModules,
1033 dynamics: &mut VecDeque<&'a str>,
1034 ) -> MatchResult<'a> {
1035 match self {
1036 Pattern::Constant(c) => {
1037 if let Some(offset) = any_offset {
1038 if let Some(index) = value.find(&**c) {
1039 if index <= offset {
1040 if index > 0 {
1041 dynamics.push_back(&value[..index]);
1042 }
1043 MatchResult::Consumed {
1044 remaining: &value[index + c.len()..],
1045 any_offset: None,
1046 in_node_modules: InNodeModules::check(c),
1047 }
1048 } else {
1049 MatchResult::None
1050 }
1051 } else if offset >= value.len() {
1052 MatchResult::Partial
1053 } else {
1054 MatchResult::None
1055 }
1056 } else if value.starts_with(&**c) {
1057 MatchResult::Consumed {
1058 remaining: &value[c.len()..],
1059 any_offset: None,
1060 in_node_modules: InNodeModules::check(c),
1061 }
1062 } else if c.starts_with(value) {
1063 MatchResult::Partial
1064 } else {
1065 MatchResult::None
1066 }
1067 }
1068 Pattern::Dynamic | Pattern::DynamicNoSlash => {
1069 static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
1070 Regex::new(r"(/|^)(ROOT|\.|/|(node_modules|__tests?__)(/|$))").unwrap()
1071 });
1072 static FORBIDDEN_MATCH: LazyLock<Regex> =
1073 LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
1074 if in_node_modules == InNodeModules::FolderSlashMatched
1075 || (in_node_modules == InNodeModules::FolderMatched && value.starts_with('/'))
1076 {
1077 MatchResult::None
1078 } else if let Some(m) = FORBIDDEN.find(value) {
1079 MatchResult::Consumed {
1080 remaining: value,
1081 any_offset: Some(m.start()),
1082 in_node_modules: InNodeModules::False,
1083 }
1084 } else if FORBIDDEN_MATCH.find(value).is_some() {
1085 MatchResult::Partial
1086 } else {
1087 let match_length = matches!(self, Pattern::DynamicNoSlash)
1088 .then(|| value.find("/"))
1089 .flatten()
1090 .unwrap_or(value.len());
1091 MatchResult::Consumed {
1092 remaining: value,
1093 any_offset: Some(match_length),
1094 in_node_modules: InNodeModules::False,
1095 }
1096 }
1097 }
1098 Pattern::Alternatives(_) => {
1099 panic!("for matching a Pattern must be normalized {self:?}")
1100 }
1101 Pattern::Concatenation(list) => {
1102 for part in list {
1103 match part.match_collect_internal(value, any_offset, in_node_modules, dynamics)
1104 {
1105 MatchResult::None => return MatchResult::None,
1106 MatchResult::Partial => return MatchResult::Partial,
1107 MatchResult::Consumed {
1108 remaining: new_value,
1109 any_offset: new_any_offset,
1110 in_node_modules: new_in_node_modules,
1111 } => {
1112 value = new_value;
1113 any_offset = new_any_offset;
1114 in_node_modules = new_in_node_modules
1115 }
1116 }
1117 }
1118 if let Some(offset) = any_offset
1119 && offset == value.len()
1120 {
1121 dynamics.push_back(value);
1122 }
1123 MatchResult::Consumed {
1124 remaining: value,
1125 any_offset,
1126 in_node_modules,
1127 }
1128 }
1129 }
1130 }
1131
1132 pub fn next_constants<'a>(&'a self, value: &str) -> Option<Vec<(&'a str, bool)>> {
1133 if let Pattern::Alternatives(list) = self {
1134 let mut results = Vec::new();
1135 for alt in list.iter() {
1136 match alt.next_constants_internal(value, None) {
1137 NextConstantUntilResult::NoMatch => {}
1138 NextConstantUntilResult::PartialDynamic => {
1139 return None;
1140 }
1141 NextConstantUntilResult::Partial(s, end) => {
1142 results.push((s, end));
1143 }
1144 NextConstantUntilResult::Consumed(rem, None) => {
1145 if rem.is_empty() {
1146 results.push(("", true));
1147 }
1148 }
1149 NextConstantUntilResult::Consumed(rem, Some(any)) => {
1150 if any == rem.len() {
1151 return None;
1154 }
1155 }
1156 }
1157 }
1158 Some(results)
1159 } else {
1160 match self.next_constants_internal(value, None) {
1161 NextConstantUntilResult::NoMatch => None,
1162 NextConstantUntilResult::PartialDynamic => None,
1163 NextConstantUntilResult::Partial(s, e) => Some(vec![(s, e)]),
1164 NextConstantUntilResult::Consumed(_, _) => None,
1165 }
1166 }
1167 }
1168
1169 fn next_constants_internal<'a, 'b>(
1170 &'a self,
1171 mut value: &'b str,
1172 mut any_offset: Option<usize>,
1173 ) -> NextConstantUntilResult<'a, 'b> {
1174 match self {
1175 Pattern::Constant(c) => {
1176 if let Some(offset) = any_offset {
1177 if let Some(index) = value.find(&**c) {
1178 if index <= offset {
1179 NextConstantUntilResult::Consumed(&value[index + c.len()..], None)
1180 } else {
1181 NextConstantUntilResult::NoMatch
1182 }
1183 } else if offset >= value.len() {
1184 NextConstantUntilResult::PartialDynamic
1185 } else {
1186 NextConstantUntilResult::NoMatch
1187 }
1188 } else if let Some(stripped) = value.strip_prefix(&**c) {
1189 NextConstantUntilResult::Consumed(stripped, None)
1190 } else if let Some(stripped) = c.strip_prefix(value) {
1191 NextConstantUntilResult::Partial(stripped, true)
1192 } else {
1193 NextConstantUntilResult::NoMatch
1194 }
1195 }
1196 Pattern::Dynamic | Pattern::DynamicNoSlash => {
1197 static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
1198 Regex::new(r"(/|^)(\.|(node_modules|__tests?__)(/|$))").unwrap()
1199 });
1200 static FORBIDDEN_MATCH: LazyLock<Regex> =
1201 LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
1202 if let Some(m) = FORBIDDEN.find(value) {
1203 NextConstantUntilResult::Consumed(value, Some(m.start()))
1204 } else if FORBIDDEN_MATCH.find(value).is_some() {
1205 NextConstantUntilResult::PartialDynamic
1206 } else {
1207 NextConstantUntilResult::Consumed(value, Some(value.len()))
1208 }
1209 }
1210 Pattern::Alternatives(_) => {
1211 panic!("for next_constants() the Pattern must be normalized");
1212 }
1213 Pattern::Concatenation(list) => {
1214 let mut iter = list.iter();
1215 while let Some(part) = iter.next() {
1216 match part.next_constants_internal(value, any_offset) {
1217 NextConstantUntilResult::NoMatch => {
1218 return NextConstantUntilResult::NoMatch;
1219 }
1220 NextConstantUntilResult::PartialDynamic => {
1221 return NextConstantUntilResult::PartialDynamic;
1222 }
1223 NextConstantUntilResult::Partial(r, end) => {
1224 return NextConstantUntilResult::Partial(
1225 r,
1226 end && iter.next().is_none(),
1227 );
1228 }
1229 NextConstantUntilResult::Consumed(new_value, new_any_offset) => {
1230 value = new_value;
1231 any_offset = new_any_offset;
1232 }
1233 }
1234 }
1235 NextConstantUntilResult::Consumed(value, any_offset)
1236 }
1237 }
1238 }
1239
1240 pub fn or_any_nested_file(&self) -> Self {
1241 let mut new = self.clone();
1242 new.push(Pattern::Constant(rcstr!("/")));
1243 new.push(Pattern::Dynamic);
1244 new.normalize();
1245 Pattern::alternatives([self.clone(), new])
1246 }
1247
1248 pub fn replace_final_constants(
1252 &mut self,
1253 cb: &mut impl FnMut(&RcStr) -> Option<Pattern>,
1254 ) -> bool {
1255 let mut replaced = false;
1256 match self {
1257 Pattern::Constant(c) => {
1258 if let Some(replacement) = cb(c) {
1259 *self = replacement;
1260 replaced = true;
1261 }
1262 }
1263 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
1264 Pattern::Alternatives(list) => {
1265 for i in list {
1266 replaced = i.replace_final_constants(cb) || replaced;
1267 }
1268 }
1269 Pattern::Concatenation(list) => {
1270 if let Some(i) = list.last_mut() {
1271 replaced = i.replace_final_constants(cb) || replaced;
1272 }
1273 }
1274 }
1275 replaced
1276 }
1277
1278 pub fn replace_constants(&mut self, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> bool {
1281 let mut replaced = false;
1282 match self {
1283 Pattern::Constant(c) => {
1284 if let Some(replacement) = cb(c) {
1285 *self = replacement;
1286 replaced = true;
1287 }
1288 }
1289 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
1290 Pattern::Concatenation(list) | Pattern::Alternatives(list) => {
1291 for i in list {
1292 replaced = i.replace_constants(cb) || replaced;
1293 }
1294 }
1295 }
1296 replaced
1297 }
1298
1299 pub fn match_apply_template(&self, value: &str, target: &Pattern) -> Option<String> {
1304 let match_idx = self.match_position(value)?;
1305 let source = match self {
1306 Pattern::Alternatives(list) => list.get(match_idx),
1307 Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
1308 if match_idx == 0 =>
1309 {
1310 Some(self)
1311 }
1312 _ => None,
1313 }?;
1314 let target = match target {
1315 Pattern::Alternatives(list) => list.get(match_idx),
1316 Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
1317 if match_idx == 0 =>
1318 {
1319 Some(target)
1320 }
1321 _ => None,
1322 }?;
1323
1324 let mut dynamics = VecDeque::new();
1325 source.match_collect_internal(value, None, InNodeModules::False, &mut dynamics);
1327
1328 let mut result = "".to_string();
1329 match target {
1330 Pattern::Constant(c) => result.push_str(c),
1331 Pattern::Dynamic | Pattern::DynamicNoSlash => result.push_str(dynamics.pop_front()?),
1332 Pattern::Concatenation(list) => {
1333 for c in list {
1334 match c {
1335 Pattern::Constant(c) => result.push_str(c),
1336 Pattern::Dynamic | Pattern::DynamicNoSlash => {
1337 result.push_str(dynamics.pop_front()?)
1338 }
1339 Pattern::Alternatives(_) | Pattern::Concatenation(_) => {
1340 panic!("Pattern must be normalized")
1341 }
1342 }
1343 }
1344 }
1345 Pattern::Alternatives(_) => panic!("Pattern must be normalized"),
1346 }
1347 if !dynamics.is_empty() {
1348 return None;
1349 }
1350
1351 Some(result)
1352 }
1353}
1354
1355impl Pattern {
1356 pub fn new(mut pattern: Pattern) -> Vc<Self> {
1357 pattern.normalize();
1358 Pattern::new_internal(pattern)
1359 }
1360}
1361
1362#[turbo_tasks::value_impl]
1363impl Pattern {
1364 #[turbo_tasks::function]
1365 fn new_internal(pattern: Pattern) -> Vc<Self> {
1366 Self::cell(pattern)
1367 }
1368}
1369
1370#[derive(PartialEq, Debug)]
1371enum InNodeModules {
1372 False,
1373 FolderMatched,
1375 FolderSlashMatched,
1377}
1378impl InNodeModules {
1379 fn check(value: &str) -> Self {
1380 if value.ends_with("node_modules/") {
1381 InNodeModules::FolderSlashMatched
1382 } else if value.ends_with("node_modules") {
1383 InNodeModules::FolderMatched
1384 } else {
1385 InNodeModules::False
1386 }
1387 }
1388}
1389
1390#[derive(PartialEq, Debug)]
1391enum MatchResult<'a> {
1392 None,
1394 Partial,
1396 Consumed {
1398 remaining: &'a str,
1400 any_offset: Option<usize>,
1403 in_node_modules: InNodeModules,
1406 },
1407}
1408
1409impl MatchResult<'_> {
1410 fn is_match(&self) -> bool {
1412 match self {
1413 MatchResult::None => false,
1414 MatchResult::Partial => false,
1415 MatchResult::Consumed {
1416 remaining: rem,
1417 any_offset,
1418 in_node_modules: _,
1419 } => {
1420 if let Some(offset) = any_offset {
1421 *offset == rem.len()
1422 } else {
1423 rem.is_empty()
1424 }
1425 }
1426 }
1427 }
1428
1429 fn could_match_others(&self) -> bool {
1432 match self {
1433 MatchResult::None => false,
1434 MatchResult::Partial => true,
1435 MatchResult::Consumed {
1436 remaining: rem,
1437 any_offset,
1438 in_node_modules: _,
1439 } => {
1440 if let Some(offset) = any_offset {
1441 *offset == rem.len()
1442 } else {
1443 false
1444 }
1445 }
1446 }
1447 }
1448
1449 fn could_match(&self) -> bool {
1452 match self {
1453 MatchResult::None => false,
1454 MatchResult::Partial => true,
1455 MatchResult::Consumed {
1456 remaining: rem,
1457 any_offset,
1458 in_node_modules: _,
1459 } => {
1460 if let Some(offset) = any_offset {
1461 *offset == rem.len()
1462 } else {
1463 rem.is_empty()
1464 }
1465 }
1466 }
1467 }
1468}
1469
1470#[derive(PartialEq, Debug)]
1471enum NextConstantUntilResult<'a, 'b> {
1472 NoMatch,
1473 PartialDynamic,
1474 Partial(&'a str, bool),
1475 Consumed(&'b str, Option<usize>),
1476}
1477
1478impl From<RcStr> for Pattern {
1479 fn from(s: RcStr) -> Self {
1480 Pattern::Constant(s)
1481 }
1482}
1483
1484impl Pattern {
1485 pub fn describe_as_string(&self) -> String {
1486 match self {
1487 Pattern::Constant(c) => format!("'{c}'"),
1488 Pattern::Dynamic => "<dynamic>".to_string(),
1489 Pattern::DynamicNoSlash => "<dynamic no slash>".to_string(),
1490 Pattern::Alternatives(list) => format!(
1491 "({})",
1492 list.iter()
1493 .map(|i| i.describe_as_string())
1494 .collect::<Vec<_>>()
1495 .join(" | ")
1496 ),
1497 Pattern::Concatenation(list) => list
1498 .iter()
1499 .map(|i| i.describe_as_string())
1500 .collect::<Vec<_>>()
1501 .join(" "),
1502 }
1503 }
1504}
1505
1506#[derive(Debug, PartialEq, Eq, Clone, ValueDebugFormat, NonLocalValue, Encode, Decode)]
1507pub enum PatternMatch {
1508 File(RcStr, FileSystemPath),
1509 Directory(RcStr, FileSystemPath),
1510}
1511
1512impl PatternMatch {
1513 pub fn path(&self) -> Vc<FileSystemPath> {
1514 match self {
1515 PatternMatch::File(_, path) | PatternMatch::Directory(_, path) => path.clone().cell(),
1516 }
1517 }
1518
1519 pub fn name(&self) -> &str {
1520 match self {
1521 PatternMatch::File(name, _) | PatternMatch::Directory(name, _) => name.as_str(),
1522 }
1523 }
1524}
1525
1526#[turbo_tasks::value(transparent)]
1529#[derive(Debug)]
1530pub struct PatternMatches(Vec<PatternMatch>);
1531
1532async fn raw_read_dir_resolved(path: &FileSystemPath) -> Result<ReadRef<RawDirectoryContent>> {
1542 let resolved = path.realpath().await?.unwrap_or_else(|_| path.clone());
1543 resolved.raw_read_dir().await
1544}
1545
1546#[turbo_tasks::function]
1556pub async fn read_matches(
1557 lookup_dir: FileSystemPath,
1558 prefix: RcStr,
1559 force_in_lookup_dir: bool,
1560 pattern: Vc<Pattern>,
1561) -> Result<Vc<PatternMatches>> {
1562 let mut prefix = prefix.to_string();
1563 let pat = pattern.await?;
1564 let mut results = Vec::new();
1565 let mut nested = Vec::new();
1566 let slow_path = if let Some(constants) = pat.next_constants(&prefix) {
1567 if constants
1568 .iter()
1569 .all(|(str, until_end)| *until_end || str.contains('/'))
1570 {
1571 let mut handled = FxHashSet::default();
1575 let mut read_dir_results = FxHashMap::default();
1576 for (index, (str, until_end)) in constants.into_iter().enumerate() {
1577 if until_end {
1578 if !handled.insert(str) {
1579 continue;
1580 }
1581 let (parent_path, last_segment) = split_last_segment(str);
1582 if last_segment.is_empty() {
1583 let joined = if force_in_lookup_dir {
1585 lookup_dir.try_join_inside(parent_path)
1586 } else {
1587 lookup_dir.try_join(parent_path)
1588 };
1589 let Some(fs_path) = joined else {
1590 continue;
1591 };
1592 results.push((
1593 index,
1594 PatternMatch::Directory(concat(&prefix, str).into(), fs_path),
1595 ));
1596 continue;
1597 }
1598 let entry = read_dir_results.entry(parent_path);
1599 let read_dir = match entry {
1600 Entry::Occupied(e) => Some(e.into_mut()),
1601 Entry::Vacant(e) => {
1602 let path_option = if force_in_lookup_dir {
1603 lookup_dir.try_join_inside(parent_path)
1604 } else {
1605 lookup_dir.try_join(parent_path)
1606 };
1607 if let Some(path) = path_option {
1608 Some(e.insert((raw_read_dir_resolved(&path).await?, path)))
1609 } else {
1610 None
1611 }
1612 }
1613 };
1614 let Some((read_dir, parent_fs_path)) = read_dir else {
1615 continue;
1616 };
1617 let RawDirectoryContent::Entries(entries) = &**read_dir else {
1618 continue;
1619 };
1620 let Some(entry) = entries.get(last_segment) else {
1621 continue;
1622 };
1623 match *entry {
1624 RawDirectoryEntry::File => {
1625 results.push((
1626 index,
1627 PatternMatch::File(
1628 concat(&prefix, str).into(),
1629 parent_fs_path.join(last_segment)?,
1630 ),
1631 ));
1632 }
1633 RawDirectoryEntry::Directory => results.push((
1634 index,
1635 PatternMatch::Directory(
1636 concat(&prefix, str).into(),
1637 parent_fs_path.join(last_segment)?,
1638 ),
1639 )),
1640 RawDirectoryEntry::Symlink => {
1641 let fs_path = parent_fs_path.join(last_segment)?;
1642 let LinkContent::Link { target } = &*fs_path.read_link().await? else {
1643 continue;
1644 };
1645 let path = concat(&prefix, str).into();
1646 if matches!(
1647 target.resolved_type().await?,
1648 FileSystemEntryType::Directory
1649 ) {
1650 results.push((index, PatternMatch::Directory(path, fs_path)));
1651 } else {
1652 results.push((index, PatternMatch::File(path, fs_path)))
1653 }
1654 }
1655 _ => {}
1656 }
1657 } else {
1658 let subpath = &str[..=str.rfind('/').unwrap()];
1659 if handled.insert(subpath) {
1660 let joined = if force_in_lookup_dir {
1661 lookup_dir.try_join_inside(subpath)
1662 } else {
1663 lookup_dir.try_join(subpath)
1664 };
1665 let Some(fs_path) = joined else {
1666 continue;
1667 };
1668 nested.push((
1669 index,
1670 read_matches(
1671 fs_path.clone(),
1672 concat(&prefix, subpath).into(),
1673 force_in_lookup_dir,
1674 pattern,
1675 ),
1676 ));
1677 }
1678 }
1679 }
1680 false
1681 } else {
1682 true
1683 }
1684 } else {
1685 true
1686 };
1687
1688 if slow_path {
1689 async {
1690 if !force_in_lookup_dir {
1693 prefix.push_str("..");
1695 if let Some(pos) = pat.match_position(&prefix) {
1696 results.push((
1697 pos,
1698 PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1699 ));
1700 }
1701
1702 prefix.push('/');
1704 if let Some(pos) = pat.match_position(&prefix) {
1705 results.push((
1706 pos,
1707 PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1708 ));
1709 }
1710 if let Some(pos) = pat.could_match_position(&prefix) {
1711 nested.push((
1712 pos,
1713 read_matches(lookup_dir.parent(), prefix.clone().into(), false, pattern),
1714 ));
1715 }
1716 prefix.pop();
1717 prefix.pop();
1718 prefix.pop();
1719 }
1720 {
1721 prefix.push('.');
1722 if let Some(pos) = pat.match_position(&prefix) {
1724 results.push((
1725 pos,
1726 PatternMatch::Directory(prefix.clone().into(), lookup_dir.clone()),
1727 ));
1728 }
1729 prefix.pop();
1730 }
1731 if prefix.is_empty() {
1732 if let Some(pos) = pat.match_position("./") {
1733 results.push((
1734 pos,
1735 PatternMatch::Directory(rcstr!("./"), lookup_dir.clone()),
1736 ));
1737 }
1738 if let Some(pos) = pat.could_match_position("./") {
1739 nested.push((
1740 pos,
1741 read_matches(lookup_dir.clone(), rcstr!("./"), false, pattern),
1742 ));
1743 }
1744 } else {
1745 prefix.push('/');
1746 if let Some(pos) = pat.could_match_position(&prefix) {
1748 nested.push((
1749 pos,
1750 read_matches(
1751 lookup_dir.clone(),
1752 prefix.to_string().into(),
1753 false,
1754 pattern,
1755 ),
1756 ));
1757 }
1758 prefix.pop();
1759 prefix.push_str("./");
1760 if let Some(pos) = pat.could_match_position(&prefix) {
1762 nested.push((
1763 pos,
1764 read_matches(
1765 lookup_dir.clone(),
1766 prefix.to_string().into(),
1767 false,
1768 pattern,
1769 ),
1770 ));
1771 }
1772 prefix.pop();
1773 prefix.pop();
1774 }
1775 match &*raw_read_dir_resolved(&lookup_dir).await? {
1776 RawDirectoryContent::Entries(map) => {
1777 for (key, entry) in map.iter() {
1778 match entry {
1779 RawDirectoryEntry::File => {
1780 let len = prefix.len();
1781 prefix.push_str(key);
1782 if let Some(pos) = pat.match_position(&prefix) {
1784 let path = lookup_dir.join(key)?;
1785 results.push((
1786 pos,
1787 PatternMatch::File(prefix.clone().into(), path),
1788 ));
1789 }
1790 prefix.truncate(len)
1791 }
1792 RawDirectoryEntry::Directory => {
1793 let len = prefix.len();
1794 prefix.push_str(key);
1795 if prefix.ends_with('/') {
1797 prefix.pop();
1798 }
1799 if let Some(pos) = pat.match_position(&prefix) {
1800 let path = lookup_dir.join(key)?;
1801 results.push((
1802 pos,
1803 PatternMatch::Directory(prefix.clone().into(), path),
1804 ));
1805 }
1806 prefix.push('/');
1807 if let Some(pos) = pat.match_position(&prefix) {
1809 let path = lookup_dir.join(key)?;
1810 results.push((
1811 pos,
1812 PatternMatch::Directory(prefix.clone().into(), path),
1813 ));
1814 }
1815 if let Some(pos) = pat.could_match_position(&prefix) {
1816 let path = lookup_dir.join(key)?;
1817 nested.push((
1818 pos,
1819 read_matches(path, prefix.clone().into(), true, pattern),
1820 ));
1821 }
1822 prefix.truncate(len)
1823 }
1824 RawDirectoryEntry::Symlink => {
1825 let len = prefix.len();
1826 prefix.push_str(key);
1827 if prefix.ends_with('/') {
1829 prefix.pop();
1830 }
1831 if let Some(pos) = pat.match_position(&prefix) {
1832 let fs_path = lookup_dir.join(key)?;
1833 if let LinkContent::Link { target } =
1834 &*fs_path.read_link().await?
1835 {
1836 if matches!(
1837 target.resolved_type().await?,
1838 FileSystemEntryType::Directory
1839 ) {
1840 results.push((
1841 pos,
1842 PatternMatch::Directory(
1843 prefix.clone().into(),
1844 fs_path,
1845 ),
1846 ));
1847 } else {
1848 results.push((
1849 pos,
1850 PatternMatch::File(prefix.clone().into(), fs_path),
1851 ));
1852 }
1853 }
1854 }
1855 prefix.push('/');
1856 if let Some(pos) = pat.match_position(&prefix) {
1857 let fs_path = lookup_dir.join(key)?;
1858 if let LinkContent::Link { target } =
1859 &*fs_path.read_link().await?
1860 && matches!(
1861 target.resolved_type().await?,
1862 FileSystemEntryType::Directory
1863 )
1864 {
1865 results.push((
1866 pos,
1867 PatternMatch::Directory(prefix.clone().into(), fs_path),
1868 ));
1869 }
1870 }
1871 if let Some(pos) = pat.could_match_position(&prefix) {
1872 let fs_path = lookup_dir.join(key)?;
1873 if let LinkContent::Link { target } =
1874 &*fs_path.read_link().await?
1875 && matches!(
1876 target.resolved_type().await?,
1877 FileSystemEntryType::Directory
1878 )
1879 {
1880 nested.push((
1881 pos,
1882 read_matches(
1883 target.file_system_path().clone(),
1884 prefix.clone().into(),
1885 true,
1886 pattern,
1887 ),
1888 ));
1889 }
1890 }
1891 prefix.truncate(len)
1892 }
1893 RawDirectoryEntry::Other => {}
1894 }
1895 }
1896 }
1897 RawDirectoryContent::NotFound => {}
1898 };
1899 anyhow::Ok(())
1900 }
1901 .instrument(tracing::trace_span!("read_matches slow_path"))
1902 .await?;
1903 }
1904 if results.is_empty() && nested.len() == 1 {
1905 Ok(nested.into_iter().next().unwrap().1)
1906 } else {
1907 for (pos, nested) in nested.into_iter() {
1908 results.extend(nested.await?.iter().cloned().map(|p| (pos, p)));
1909 }
1910 results.sort_by(|(a, am), (b, bm)| (*a).cmp(b).then_with(|| am.name().cmp(bm.name())));
1911 Ok(Vc::cell(
1912 results.into_iter().map(|(_, p)| p).collect::<Vec<_>>(),
1913 ))
1914 }
1915}
1916
1917fn concat(a: &str, b: &str) -> String {
1918 let mut result = String::with_capacity(a.len() + b.len());
1919 result.push_str(a);
1920 result.push_str(b);
1921 result
1922}
1923
1924fn split_last_segment(path: &str) -> (&str, &str) {
1927 if let Some((remaining_path, last_segment)) = path.rsplit_once('/') {
1928 match last_segment {
1929 "" => split_last_segment(remaining_path),
1930 "." => split_last_segment(remaining_path),
1931 ".." => match split_last_segment(remaining_path) {
1932 (_, "") => (path, ""),
1933 (parent_path, _) => split_last_segment(parent_path),
1934 },
1935 _ => (remaining_path, last_segment),
1936 }
1937 } else {
1938 match path {
1939 "" => ("", ""),
1940 "." => ("", ""),
1941 ".." => ("..", ""),
1942 _ => ("", path),
1943 }
1944 }
1945}
1946
1947#[cfg(test)]
1948mod tests {
1949 use std::path::Path;
1950
1951 use rstest::*;
1952 use turbo_rcstr::{RcStr, rcstr};
1953 use turbo_tasks::Vc;
1954 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1955 use turbo_tasks_fs::{DiskFileSystem, FileSystem};
1956
1957 use super::{
1958 Pattern, PatternMatch, longest_common_prefix, longest_common_suffix, read_matches,
1959 split_last_segment,
1960 };
1961
1962 #[test]
1963 fn longest_common_prefix_test() {
1964 assert_eq!(longest_common_prefix(&["ab"]), "ab");
1965 assert_eq!(longest_common_prefix(&["ab", "cd", "ef"]), "");
1966 assert_eq!(longest_common_prefix(&["ab1", "ab23", "ab456"]), "ab");
1967 assert_eq!(longest_common_prefix(&["abc", "abc", "abc"]), "abc");
1968 assert_eq!(longest_common_prefix(&["abc", "a", "abc"]), "a");
1969 }
1970
1971 #[test]
1972 fn longest_common_suffix_test() {
1973 assert_eq!(longest_common_suffix(&["ab"]), "ab");
1974 assert_eq!(longest_common_suffix(&["ab", "cd", "ef"]), "");
1975 assert_eq!(longest_common_suffix(&["1ab", "23ab", "456ab"]), "ab");
1976 assert_eq!(longest_common_suffix(&["abc", "abc", "abc"]), "abc");
1977 assert_eq!(longest_common_suffix(&["abc", "c", "abc"]), "c");
1978 }
1979
1980 #[test]
1981 fn normalize() {
1982 let a = Pattern::Constant(rcstr!("a"));
1983 let b = Pattern::Constant(rcstr!("b"));
1984 let c = Pattern::Constant(rcstr!("c"));
1985 let s = Pattern::Constant(rcstr!("/"));
1986 let d = Pattern::Dynamic;
1987 {
1988 let mut p = Pattern::Concatenation(vec![
1989 Pattern::Alternatives(vec![a.clone(), b.clone()]),
1990 s.clone(),
1991 c.clone(),
1992 ]);
1993 p.normalize();
1994 assert_eq!(
1995 p,
1996 Pattern::Alternatives(vec![
1997 Pattern::Constant(rcstr!("a/c")),
1998 Pattern::Constant(rcstr!("b/c")),
1999 ])
2000 );
2001 }
2002
2003 #[allow(clippy::redundant_clone)] {
2005 let mut p = Pattern::Concatenation(vec![
2006 Pattern::Alternatives(vec![a.clone(), b.clone(), d.clone()]),
2007 s.clone(),
2008 Pattern::Alternatives(vec![b.clone(), c.clone(), d.clone()]),
2009 ]);
2010 p.normalize();
2011
2012 assert_eq!(
2013 p,
2014 Pattern::Alternatives(vec![
2015 Pattern::Constant(rcstr!("a/b")),
2016 Pattern::Constant(rcstr!("b/b")),
2017 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/b"))]),
2018 Pattern::Constant(rcstr!("a/c")),
2019 Pattern::Constant(rcstr!("b/c")),
2020 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/c"))]),
2021 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
2022 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
2023 Pattern::Concatenation(vec![
2024 Pattern::Dynamic,
2025 Pattern::Constant(rcstr!("/")),
2026 Pattern::Dynamic
2027 ]),
2028 ])
2029 );
2030 }
2031
2032 #[allow(clippy::redundant_clone)] {
2034 let mut p = Pattern::Alternatives(vec![a.clone()]);
2035 p.normalize();
2036
2037 assert_eq!(p, a);
2038 }
2039
2040 #[allow(clippy::redundant_clone)] {
2042 let mut p = Pattern::Alternatives(vec![Pattern::Dynamic, Pattern::Dynamic]);
2043 p.normalize();
2044
2045 assert_eq!(p, Pattern::Dynamic);
2046 }
2047 }
2048
2049 #[test]
2050 fn filter_static() {
2051 let static_a = Pattern::Constant(rcstr!("./next-i18next.config.js"));
2052 let static_b = Pattern::Constant(rcstr!("./i18next.config.js"));
2053
2054 assert_eq!(static_a.filter_static(), Some(static_a.clone()));
2055 assert_eq!(Pattern::Dynamic.filter_static(), None);
2056 assert_eq!(Pattern::DynamicNoSlash.filter_static(), None);
2057
2058 let pattern = Pattern::Alternatives(vec![
2059 static_a.clone(),
2060 static_b.clone(),
2061 Pattern::Dynamic,
2062 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/suffix"))]),
2063 Pattern::Concatenation(vec![
2064 Pattern::Constant(rcstr!("/prefix/")),
2065 Pattern::Dynamic,
2066 ]),
2067 ]);
2068 assert_eq!(
2069 pattern.filter_static(),
2070 Some(Pattern::Alternatives(vec![static_a, static_b]))
2071 );
2072
2073 assert_eq!(
2074 Pattern::Alternatives(vec![
2075 Pattern::Dynamic,
2076 Pattern::Concatenation(vec![
2077 Pattern::Constant(rcstr!("/prefix/")),
2078 Pattern::Dynamic,
2079 ]),
2080 ])
2081 .filter_static(),
2082 None
2083 );
2084 }
2085
2086 #[test]
2087 fn with_normalized_path() {
2088 assert!(
2089 Pattern::Constant(rcstr!("a/../.."))
2090 .with_normalized_path()
2091 .is_none()
2092 );
2093 assert_eq!(
2094 Pattern::Constant(rcstr!("a/b/../c"))
2095 .with_normalized_path()
2096 .unwrap(),
2097 Pattern::Constant(rcstr!("a/c"))
2098 );
2099 assert_eq!(
2100 Pattern::Alternatives(vec![
2101 Pattern::Constant(rcstr!("a/b/../c")),
2102 Pattern::Constant(rcstr!("a/b/../c/d"))
2103 ])
2104 .with_normalized_path()
2105 .unwrap(),
2106 Pattern::Alternatives(vec![
2107 Pattern::Constant(rcstr!("a/c")),
2108 Pattern::Constant(rcstr!("a/c/d"))
2109 ])
2110 );
2111 assert_eq!(
2112 Pattern::Constant(rcstr!("a/b/"))
2113 .with_normalized_path()
2114 .unwrap(),
2115 Pattern::Constant(rcstr!("a/b"))
2116 );
2117
2118 assert_eq!(
2120 Pattern::Concatenation(vec![
2121 Pattern::Constant(rcstr!("a/b/")),
2122 Pattern::Dynamic,
2123 Pattern::Constant(rcstr!("../c"))
2124 ])
2125 .with_normalized_path()
2126 .unwrap(),
2127 Pattern::Concatenation(vec![
2128 Pattern::Constant(rcstr!("a/b/")),
2129 Pattern::Dynamic,
2130 Pattern::Constant(rcstr!("../c"))
2131 ])
2132 );
2133
2134 assert_eq!(
2136 Pattern::Concatenation(vec![
2137 Pattern::Constant(rcstr!("a/b")),
2138 Pattern::Dynamic,
2139 Pattern::Constant(rcstr!("../c"))
2140 ])
2141 .with_normalized_path()
2142 .unwrap(),
2143 Pattern::Concatenation(vec![
2144 Pattern::Constant(rcstr!("a/b")),
2145 Pattern::Dynamic,
2146 Pattern::Constant(rcstr!("../c"))
2147 ])
2148 );
2149 assert_eq!(
2150 Pattern::Concatenation(vec![
2151 Pattern::Constant(rcstr!("src/")),
2152 Pattern::Dynamic,
2153 Pattern::Constant(rcstr!(".js"))
2154 ])
2155 .with_normalized_path()
2156 .unwrap(),
2157 Pattern::Concatenation(vec![
2158 Pattern::Constant(rcstr!("src/")),
2159 Pattern::Dynamic,
2160 Pattern::Constant(rcstr!(".js"))
2161 ])
2162 );
2163 }
2164
2165 #[test]
2166 fn is_match() {
2167 let pat = Pattern::Concatenation(vec![
2168 Pattern::Constant(rcstr!(".")),
2169 Pattern::Constant(rcstr!("/")),
2170 Pattern::Dynamic,
2171 Pattern::Constant(rcstr!(".js")),
2172 ]);
2173 assert!(pat.could_match(""));
2174 assert!(pat.could_match("./"));
2175 assert!(!pat.is_match("./"));
2176 assert!(pat.is_match("./index.js"));
2177 assert!(!pat.is_match("./index"));
2178 assert!(pat.is_match("./foo/index.js"));
2179 assert!(pat.is_match("./foo/bar/index.js"));
2180
2181 assert!(!pat.is_match("./../index.js"));
2183 assert!(!pat.is_match("././index.js"));
2184 assert!(!pat.is_match("./.git/index.js"));
2185 assert!(!pat.is_match("./inner/../index.js"));
2186 assert!(!pat.is_match("./inner/./index.js"));
2187 assert!(!pat.is_match("./inner/.git/index.js"));
2188 assert!(!pat.could_match("./../"));
2189 assert!(!pat.could_match("././"));
2190 assert!(!pat.could_match("./.git/"));
2191 assert!(!pat.could_match("./inner/../"));
2192 assert!(!pat.could_match("./inner/./"));
2193 assert!(!pat.could_match("./inner/.git/"));
2194 }
2195
2196 #[test]
2197 fn is_match_dynamic_no_slash() {
2198 let pat = Pattern::Concatenation(vec![
2199 Pattern::Constant(rcstr!(".")),
2200 Pattern::Constant(rcstr!("/")),
2201 Pattern::DynamicNoSlash,
2202 Pattern::Constant(rcstr!(".js")),
2203 ]);
2204 assert!(pat.could_match(""));
2205 assert!(pat.could_match("./"));
2206 assert!(!pat.is_match("./"));
2207 assert!(pat.is_match("./index.js"));
2208 assert!(!pat.is_match("./index"));
2209 assert!(!pat.is_match("./foo/index.js"));
2210 assert!(!pat.is_match("./foo/bar/index.js"));
2211 }
2212
2213 #[test]
2214 fn constant_prefix() {
2215 assert_eq!(
2216 Pattern::Constant(rcstr!("a/b/c.js")).constant_prefix(),
2217 "a/b/c.js",
2218 );
2219
2220 let pat = Pattern::Alternatives(vec![
2221 Pattern::Constant(rcstr!("a/b/x")),
2222 Pattern::Constant(rcstr!("a/b/y")),
2223 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]),
2224 ]);
2225 assert_eq!(pat.constant_prefix(), "a/b/");
2226 }
2227
2228 #[test]
2229 fn constant_suffix() {
2230 assert_eq!(
2231 Pattern::Constant(rcstr!("a/b/c.js")).constant_suffix(),
2232 "a/b/c.js",
2233 );
2234
2235 let pat = Pattern::Alternatives(vec![
2236 Pattern::Constant(rcstr!("a/b/x.js")),
2237 Pattern::Constant(rcstr!("a/b/y.js")),
2238 Pattern::Concatenation(vec![
2239 Pattern::Constant(rcstr!("a/b/c/")),
2240 Pattern::Dynamic,
2241 Pattern::Constant(rcstr!(".js")),
2242 ]),
2243 ]);
2244 assert_eq!(pat.constant_suffix(), ".js");
2245 }
2246
2247 #[test]
2248 fn strip_prefix() {
2249 fn strip(mut pat: Pattern, n: usize) -> Pattern {
2250 pat.strip_prefix_len(n).unwrap();
2251 pat
2252 }
2253
2254 assert_eq!(
2255 strip(Pattern::Constant(rcstr!("a/b")), 0),
2256 Pattern::Constant(rcstr!("a/b"))
2257 );
2258
2259 assert_eq!(
2260 strip(
2261 Pattern::Alternatives(vec![
2262 Pattern::Constant(rcstr!("a/b/x")),
2263 Pattern::Constant(rcstr!("a/b/y")),
2264 ]),
2265 2
2266 ),
2267 Pattern::Alternatives(vec![
2268 Pattern::Constant(rcstr!("b/x")),
2269 Pattern::Constant(rcstr!("b/y")),
2270 ])
2271 );
2272
2273 assert_eq!(
2274 strip(
2275 Pattern::Concatenation(vec![
2276 Pattern::Constant(rcstr!("a/")),
2277 Pattern::Constant(rcstr!("b")),
2278 Pattern::Constant(rcstr!("/")),
2279 Pattern::Constant(rcstr!("y/")),
2280 Pattern::Dynamic
2281 ]),
2282 4
2283 ),
2284 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("y/")), Pattern::Dynamic]),
2285 );
2286 }
2287
2288 #[test]
2289 fn strip_suffix() {
2290 fn strip(mut pat: Pattern, n: usize) -> Pattern {
2291 pat.strip_suffix_len(n);
2292 pat
2293 }
2294
2295 assert_eq!(
2296 strip(Pattern::Constant(rcstr!("a/b")), 0),
2297 Pattern::Constant(rcstr!("a/b"))
2298 );
2299
2300 assert_eq!(
2301 strip(
2302 Pattern::Alternatives(vec![
2303 Pattern::Constant(rcstr!("x/b/a")),
2304 Pattern::Constant(rcstr!("y/b/a")),
2305 ]),
2306 2
2307 ),
2308 Pattern::Alternatives(vec![
2309 Pattern::Constant(rcstr!("x/b")),
2310 Pattern::Constant(rcstr!("y/b")),
2311 ])
2312 );
2313
2314 assert_eq!(
2315 strip(
2316 Pattern::Concatenation(vec![
2317 Pattern::Dynamic,
2318 Pattern::Constant(rcstr!("/a/")),
2319 Pattern::Constant(rcstr!("b")),
2320 Pattern::Constant(rcstr!("/")),
2321 Pattern::Constant(rcstr!("y/")),
2322 ]),
2323 4
2324 ),
2325 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/a/")),]),
2326 );
2327 }
2328
2329 #[test]
2330 fn spread_into_star() {
2331 let pat = Pattern::Constant(rcstr!("xyz"));
2332 assert_eq!(
2333 pat.spread_into_star("before/after"),
2334 Pattern::Constant(rcstr!("before/after")),
2335 );
2336
2337 let pat =
2338 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]);
2339 assert_eq!(
2340 pat.spread_into_star("before/*/after"),
2341 Pattern::Concatenation(vec![
2342 Pattern::Constant(rcstr!("before/a/b/c/")),
2343 Pattern::Dynamic,
2344 Pattern::Constant(rcstr!("/after"))
2345 ])
2346 );
2347
2348 let pat = Pattern::Alternatives(vec![
2349 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
2350 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
2351 ]);
2352 assert_eq!(
2353 pat.spread_into_star("before/*/after"),
2354 Pattern::Alternatives(vec![
2355 Pattern::Concatenation(vec![
2356 Pattern::Constant(rcstr!("before/a/")),
2357 Pattern::Dynamic,
2358 Pattern::Constant(rcstr!("/after"))
2359 ]),
2360 Pattern::Concatenation(vec![
2361 Pattern::Constant(rcstr!("before/b/")),
2362 Pattern::Dynamic,
2363 Pattern::Constant(rcstr!("/after"))
2364 ]),
2365 ])
2366 );
2367
2368 let pat = Pattern::Alternatives(vec![
2369 Pattern::Constant(rcstr!("a")),
2370 Pattern::Constant(rcstr!("b")),
2371 ]);
2372 assert_eq!(
2373 pat.spread_into_star("before/*/*"),
2374 Pattern::Alternatives(vec![
2375 Pattern::Constant(rcstr!("before/a/a")),
2376 Pattern::Constant(rcstr!("before/b/b")),
2377 ])
2378 );
2379
2380 let pat = Pattern::Dynamic;
2381 assert_eq!(
2382 pat.spread_into_star("before/*/*"),
2383 Pattern::Concatenation(vec![
2384 Pattern::Constant(rcstr!("before/")),
2386 Pattern::Dynamic,
2387 Pattern::Constant(rcstr!("/")),
2388 Pattern::Dynamic
2389 ])
2390 );
2391 }
2392
2393 #[rstest]
2394 #[case::dynamic(Pattern::Dynamic)]
2395 #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2396 fn dynamic_match(#[case] pat: Pattern) {
2397 assert!(pat.could_match(""));
2398 assert!(pat.is_match("index.js"));
2399
2400 assert!(!pat.could_match("./"));
2402 assert!(!pat.is_match("./"));
2403 assert!(!pat.could_match("."));
2404 assert!(!pat.is_match("."));
2405 assert!(!pat.could_match("../"));
2406 assert!(!pat.is_match("../"));
2407 assert!(!pat.could_match(".."));
2408 assert!(!pat.is_match(".."));
2409 assert!(!pat.is_match("./../index.js"));
2410 assert!(!pat.is_match("././index.js"));
2411 assert!(!pat.is_match("./.git/index.js"));
2412 assert!(!pat.is_match("./inner/../index.js"));
2413 assert!(!pat.is_match("./inner/./index.js"));
2414 assert!(!pat.is_match("./inner/.git/index.js"));
2415 assert!(!pat.could_match("./../"));
2416 assert!(!pat.could_match("././"));
2417 assert!(!pat.could_match("./.git/"));
2418 assert!(!pat.could_match("./inner/../"));
2419 assert!(!pat.could_match("./inner/./"));
2420 assert!(!pat.could_match("./inner/.git/"));
2421 assert!(!pat.could_match("dir//"));
2422 assert!(!pat.could_match("dir//dir"));
2423 assert!(!pat.could_match("dir///dir"));
2424 assert!(!pat.could_match("/"));
2425 assert!(!pat.could_match("//"));
2426 assert!(!pat.could_match("/ROOT/"));
2427
2428 assert!(!pat.could_match("node_modules"));
2429 assert!(!pat.could_match("node_modules/package"));
2430 assert!(!pat.could_match("nested/node_modules"));
2431 assert!(!pat.could_match("nested/node_modules/package"));
2432
2433 assert!(pat.could_match("file.map"));
2435 assert!(!pat.is_match("file.map"));
2436 assert!(pat.is_match("file.map/file.js"));
2437 assert!(!pat.is_match("file.d.ts"));
2438 assert!(!pat.is_match("file.d.ts.map"));
2439 assert!(!pat.is_match("file.d.ts.map"));
2440 assert!(!pat.is_match("dir/file.d.ts.map"));
2441 assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2442 assert!(pat.could_match("dir/inner/file.d.ts.map"));
2443 }
2444
2445 #[rstest]
2446 #[case::slash(Pattern::Concatenation(vec![Pattern::Constant(rcstr!("node_modules/")),Pattern::Dynamic]))]
2447 #[case::nested(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file())]
2448 fn dynamic_match_node_modules(#[case] pat: Pattern) {
2449 assert!(!pat.is_match("node_modules/package"));
2450 assert!(!pat.could_match("node_modules/package"));
2451 assert!(!pat.is_match("node_modules/package/index.js"));
2452 assert!(!pat.could_match("node_modules/package/index.js"));
2453 }
2454
2455 #[rstest]
2456 fn dynamic_match2() {
2457 let pat = Pattern::Concatenation(vec![
2458 Pattern::Dynamic,
2459 Pattern::Constant(rcstr!("/")),
2460 Pattern::Dynamic,
2461 ]);
2462 assert!(pat.could_match("dir"));
2463 assert!(pat.could_match("dir/"));
2464 assert!(pat.is_match("dir/index.js"));
2465
2466 assert!(!pat.could_match("./"));
2468 assert!(!pat.is_match("./"));
2469 assert!(!pat.could_match("."));
2470 assert!(!pat.is_match("."));
2471 assert!(!pat.could_match("../"));
2472 assert!(!pat.is_match("../"));
2473 assert!(!pat.could_match(".."));
2474 assert!(!pat.is_match(".."));
2475 assert!(!pat.is_match("./../index.js"));
2476 assert!(!pat.is_match("././index.js"));
2477 assert!(!pat.is_match("./.git/index.js"));
2478 assert!(!pat.is_match("./inner/../index.js"));
2479 assert!(!pat.is_match("./inner/./index.js"));
2480 assert!(!pat.is_match("./inner/.git/index.js"));
2481 assert!(!pat.could_match("./../"));
2482 assert!(!pat.could_match("././"));
2483 assert!(!pat.could_match("./.git/"));
2484 assert!(!pat.could_match("./inner/../"));
2485 assert!(!pat.could_match("./inner/./"));
2486 assert!(!pat.could_match("./inner/.git/"));
2487 assert!(!pat.could_match("dir//"));
2488 assert!(!pat.could_match("dir//dir"));
2489 assert!(!pat.could_match("dir///dir"));
2490 assert!(!pat.could_match("/ROOT/"));
2491
2492 assert!(!pat.could_match("node_modules"));
2493 assert!(!pat.could_match("node_modules/package"));
2494 assert!(!pat.could_match("nested/node_modules"));
2495 assert!(!pat.could_match("nested/node_modules/package"));
2496
2497 assert!(pat.could_match("dir/file.map"));
2499 assert!(!pat.is_match("dir/file.map"));
2500 assert!(pat.is_match("file.map/file.js"));
2501 assert!(!pat.is_match("dir/file.d.ts"));
2502 assert!(!pat.is_match("dir/file.d.ts.map"));
2503 assert!(!pat.is_match("dir/file.d.ts.map"));
2504 assert!(!pat.is_match("dir/file.d.ts.map"));
2505 assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2506 assert!(pat.could_match("dir/inner/file.d.ts.map"));
2507 }
2508
2509 #[rstest]
2510 #[case::dynamic(Pattern::Dynamic)]
2511 #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2512 #[case::dynamic_concat2(Pattern::Concatenation(vec![
2513 Pattern::Dynamic,
2514 Pattern::Constant(rcstr!("/")),
2515 Pattern::Dynamic,
2516 ]))]
2517 #[case::dynamic_alt_concat(Pattern::alternatives(vec![
2518 Pattern::Concatenation(vec![
2519 Pattern::Dynamic,
2520 Pattern::Constant(rcstr!("/")),
2521 Pattern::Dynamic,
2522 ]),
2523 Pattern::Dynamic,
2524 ]))]
2525 fn split_could_match(#[case] pat: Pattern) {
2526 let (abs, rel) = pat.split_could_match("/ROOT/");
2527 assert!(abs.is_none());
2528 assert!(rel.is_some());
2529 }
2530
2531 #[rstest]
2532 #[case::dynamic(Pattern::Dynamic, "feijf", None)]
2533 #[case::dynamic_concat(
2534 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]),
2535 "hello.", None
2536 )]
2537 #[case::constant(Pattern::Constant(rcstr!("Hello World")), "Hello ", Some(vec![("World", true)]))]
2538 #[case::alternatives(
2539 Pattern::Alternatives(vec![
2540 Pattern::Constant(rcstr!("Hello World")),
2541 Pattern::Constant(rcstr!("Hello All"))
2542 ]), "Hello ", Some(vec![("World", true), ("All", true)])
2543 )]
2544 #[case::alternatives_non_end(
2545 Pattern::Alternatives(vec![
2546 Pattern::Constant(rcstr!("Hello World")),
2547 Pattern::Constant(rcstr!("Hello All")),
2548 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("Hello more")), Pattern::Dynamic])
2549 ]), "Hello ", Some(vec![("World", true), ("All", true), ("more", false)])
2550 )]
2551 #[case::request_with_extensions(
2552 Pattern::Alternatives(vec![
2553 Pattern::Constant(rcstr!("./file.js")),
2554 Pattern::Constant(rcstr!("./file.ts")),
2555 Pattern::Constant(rcstr!("./file.cjs")),
2556 ]), "./", Some(vec![("file.js", true), ("file.ts", true), ("file.cjs", true)])
2557 )]
2558 fn next_constants(
2559 #[case] pat: Pattern,
2560 #[case] value: &str,
2561 #[case] expected: Option<Vec<(&str, bool)>>,
2562 ) {
2563 assert_eq!(pat.next_constants(value), expected);
2564 }
2565
2566 #[test]
2567 fn replace_final_constants() {
2568 fn f(mut p: Pattern, cb: &mut impl FnMut(&RcStr) -> Option<Pattern>) -> Pattern {
2569 p.replace_final_constants(cb);
2570 p
2571 }
2572
2573 let mut js_to_ts_tsx = |c: &RcStr| -> Option<Pattern> {
2574 c.strip_suffix(".js").map(|rest| {
2575 let new_ending = Pattern::Alternatives(vec![
2576 Pattern::Constant(rcstr!(".ts")),
2577 Pattern::Constant(rcstr!(".tsx")),
2578 Pattern::Constant(rcstr!(".js")),
2579 ]);
2580 if !rest.is_empty() {
2581 Pattern::Concatenation(vec![Pattern::Constant(rest.into()), new_ending])
2582 } else {
2583 new_ending
2584 }
2585 })
2586 };
2587
2588 assert_eq!(
2589 f(
2590 Pattern::Concatenation(vec![
2591 Pattern::Constant(rcstr!(".")),
2592 Pattern::Constant(rcstr!("/")),
2593 Pattern::Dynamic,
2594 Pattern::Alternatives(vec![
2595 Pattern::Constant(rcstr!(".js")),
2596 Pattern::Constant(rcstr!(".node")),
2597 ])
2598 ]),
2599 &mut js_to_ts_tsx
2600 ),
2601 Pattern::Concatenation(vec![
2602 Pattern::Constant(rcstr!(".")),
2603 Pattern::Constant(rcstr!("/")),
2604 Pattern::Dynamic,
2605 Pattern::Alternatives(vec![
2606 Pattern::Alternatives(vec![
2607 Pattern::Constant(rcstr!(".ts")),
2608 Pattern::Constant(rcstr!(".tsx")),
2609 Pattern::Constant(rcstr!(".js")),
2610 ]),
2611 Pattern::Constant(rcstr!(".node")),
2612 ])
2613 ]),
2614 );
2615 assert_eq!(
2616 f(
2617 Pattern::Concatenation(vec![
2618 Pattern::Constant(rcstr!(".")),
2619 Pattern::Constant(rcstr!("/")),
2620 Pattern::Constant(rcstr!("abc.js")),
2621 ]),
2622 &mut js_to_ts_tsx
2623 ),
2624 Pattern::Concatenation(vec![
2625 Pattern::Constant(rcstr!(".")),
2626 Pattern::Constant(rcstr!("/")),
2627 Pattern::Concatenation(vec![
2628 Pattern::Constant(rcstr!("abc")),
2629 Pattern::Alternatives(vec![
2630 Pattern::Constant(rcstr!(".ts")),
2631 Pattern::Constant(rcstr!(".tsx")),
2632 Pattern::Constant(rcstr!(".js")),
2633 ])
2634 ]),
2635 ])
2636 );
2637 }
2638
2639 #[test]
2640 fn match_apply_template() {
2641 assert_eq!(
2642 Pattern::Concatenation(vec![
2643 Pattern::Constant(rcstr!("a/b/")),
2644 Pattern::Dynamic,
2645 Pattern::Constant(rcstr!(".ts")),
2646 ])
2647 .match_apply_template(
2648 "a/b/foo.ts",
2649 &Pattern::Concatenation(vec![
2650 Pattern::Constant(rcstr!("@/a/b/")),
2651 Pattern::Dynamic,
2652 Pattern::Constant(rcstr!(".js")),
2653 ])
2654 )
2655 .as_deref(),
2656 Some("@/a/b/foo.js")
2657 );
2658 assert_eq!(
2659 Pattern::Concatenation(vec![
2660 Pattern::Constant(rcstr!("b/")),
2661 Pattern::Dynamic,
2662 Pattern::Constant(rcstr!(".ts")),
2663 ])
2664 .match_apply_template(
2665 "a/b/foo.ts",
2666 &Pattern::Concatenation(vec![
2667 Pattern::Constant(rcstr!("@/a/b/")),
2668 Pattern::Dynamic,
2669 Pattern::Constant(rcstr!(".js")),
2670 ])
2671 )
2672 .as_deref(),
2673 None,
2674 );
2675 assert_eq!(
2676 Pattern::Concatenation(vec![
2677 Pattern::Constant(rcstr!("a/b/")),
2678 Pattern::Dynamic,
2679 Pattern::Constant(rcstr!(".ts")),
2680 ])
2681 .match_apply_template(
2682 "a/b/foo.ts",
2683 &Pattern::Concatenation(vec![
2684 Pattern::Constant(rcstr!("@/a/b/x")),
2685 Pattern::Constant(rcstr!(".js")),
2686 ])
2687 )
2688 .as_deref(),
2689 None,
2690 );
2691 assert_eq!(
2692 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("./sub/")), Pattern::Dynamic])
2693 .match_apply_template(
2694 "./sub/file1",
2695 &Pattern::Concatenation(vec![
2696 Pattern::Constant(rcstr!("@/sub/")),
2697 Pattern::Dynamic
2698 ])
2699 )
2700 .as_deref(),
2701 Some("@/sub/file1"),
2702 );
2703 }
2704
2705 #[test]
2706 fn test_split_last_segment() {
2707 assert_eq!(split_last_segment(""), ("", ""));
2708 assert_eq!(split_last_segment("a"), ("", "a"));
2709 assert_eq!(split_last_segment("a/"), ("", "a"));
2710 assert_eq!(split_last_segment("a/b"), ("a", "b"));
2711 assert_eq!(split_last_segment("a/b/"), ("a", "b"));
2712 assert_eq!(split_last_segment("a/b/c"), ("a/b", "c"));
2713 assert_eq!(split_last_segment("a/b/."), ("a", "b"));
2714 assert_eq!(split_last_segment("a/b/.."), ("", "a"));
2715 assert_eq!(split_last_segment("a/b/c/.."), ("a", "b"));
2716 assert_eq!(split_last_segment("a/b/c/../.."), ("", "a"));
2717 assert_eq!(split_last_segment("a/b/c/d/../.."), ("a", "b"));
2718 assert_eq!(split_last_segment("a/b/c/../d/.."), ("a", "b"));
2719 assert_eq!(split_last_segment("a/b/../c/d/.."), ("a/b/..", "c"));
2720 assert_eq!(split_last_segment("."), ("", ""));
2721 assert_eq!(split_last_segment("./"), ("", ""));
2722 assert_eq!(split_last_segment(".."), ("..", ""));
2723 assert_eq!(split_last_segment("../"), ("..", ""));
2724 assert_eq!(split_last_segment("./../"), ("./..", ""));
2725 assert_eq!(split_last_segment("../../"), ("../..", ""));
2726 assert_eq!(split_last_segment("../../."), ("../..", ""));
2727 assert_eq!(split_last_segment("../.././"), ("../..", ""));
2728 assert_eq!(split_last_segment("a/.."), ("", ""));
2729 assert_eq!(split_last_segment("a/../"), ("", ""));
2730 assert_eq!(split_last_segment("a/../.."), ("a/../..", ""));
2731 assert_eq!(split_last_segment("a/../../"), ("a/../..", ""));
2732 assert_eq!(split_last_segment("a/././../"), ("", ""));
2733 assert_eq!(split_last_segment("../a"), ("..", "a"));
2734 assert_eq!(split_last_segment("../a/"), ("..", "a"));
2735 assert_eq!(split_last_segment("../../a"), ("../..", "a"));
2736 assert_eq!(split_last_segment("../../a/"), ("../..", "a"));
2737 }
2738
2739 #[cfg(all(unix, debug_assertions))]
2740 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2741 async fn test_read_matches_resolves_lookup_dir_for_enumeration() {
2742 use std::{fs::create_dir_all, os::unix::fs::symlink};
2743
2744 let scratch = tempfile::tempdir().unwrap();
2745 let root = scratch.path();
2746 create_dir_all(root.join("real")).unwrap();
2747 std::fs::write(root.join("real/file.js"), "content").unwrap();
2748 symlink("real", root.join("alias")).unwrap();
2749 symlink("real/file.js", root.join("file-alias")).unwrap();
2750
2751 #[turbo_tasks::function(operation, root)]
2752 async fn operation(disk_root: RcStr) -> anyhow::Result<()> {
2753 let root = DiskFileSystem::new(rcstr!("test"), Vc::cell(disk_root))
2754 .root()
2755 .owned()
2756 .await?;
2757 let logical_dir = root.join("alias")?;
2758 let matches = read_matches(
2759 logical_dir.clone(),
2760 rcstr!(""),
2761 true,
2762 Pattern::new(Pattern::Dynamic),
2763 )
2764 .await?;
2765 assert!(matches.iter().any(|m| m
2766 == &PatternMatch::File(rcstr!("file.js"), logical_dir.join("file.js").unwrap(),)));
2767
2768 let file_probe = read_matches(
2769 root.join("file-alias")?,
2770 rcstr!(""),
2771 true,
2772 Pattern::new(Pattern::Dynamic),
2773 )
2774 .await?;
2775 assert!(file_probe.is_empty());
2776
2777 Ok(())
2778 }
2779
2780 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2781 BackendOptions::default(),
2782 noop_backing_storage(),
2783 ));
2784 let disk_root: RcStr = root.to_str().unwrap().into();
2785 tt.run_once(async move {
2786 operation(disk_root).read_strongly_consistent().await?;
2787 anyhow::Ok(())
2788 })
2789 .await
2790 .unwrap();
2791 }
2792
2793 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2794 async fn test_read_matches() {
2795 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2796 BackendOptions::default(),
2797 noop_backing_storage(),
2798 ));
2799 tt.run_once(async {
2800 #[turbo_tasks::value]
2801 struct ReadMatchesOutput {
2802 dynamic: Vec<String>,
2803 dynamic_file_suffix: Vec<String>,
2804 node_modules_dynamic: Vec<String>,
2805 extension_ordering: Vec<String>,
2806 subpath_ordering: Vec<String>,
2807 }
2808
2809 #[turbo_tasks::function(operation, root)]
2810 async fn read_matches_operation() -> anyhow::Result<Vc<ReadMatchesOutput>> {
2811 let root = DiskFileSystem::new(
2812 rcstr!("test"),
2813 Vc::cell(
2814 Path::new(env!("CARGO_MANIFEST_DIR"))
2815 .join("tests/pattern/read_matches")
2816 .to_str()
2817 .unwrap()
2818 .into(),
2819 ),
2820 )
2821 .root()
2822 .owned()
2823 .await?;
2824
2825 let dynamic = read_matches(
2826 root.clone(),
2827 rcstr!(""),
2828 false,
2829 Pattern::new(Pattern::Dynamic),
2830 )
2831 .await?
2832 .into_iter()
2833 .map(|m| m.name().to_string())
2834 .collect::<Vec<_>>();
2835
2836 let dynamic_file_suffix = read_matches(
2837 root.clone(),
2838 rcstr!(""),
2839 false,
2840 Pattern::new(Pattern::concat([
2841 Pattern::Constant(rcstr!("sub/foo")),
2842 Pattern::Dynamic,
2843 ])),
2844 )
2845 .await?
2846 .into_iter()
2847 .map(|m| m.name().to_string())
2848 .collect::<Vec<_>>();
2849
2850 let node_modules_dynamic = read_matches(
2851 root.clone(),
2852 rcstr!(""),
2853 false,
2854 Pattern::new(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file()),
2855 )
2856 .await?
2857 .into_iter()
2858 .map(|m| m.name().to_string())
2859 .collect::<Vec<_>>();
2860
2861 let extension_ordering = read_matches(
2865 root.clone(),
2866 rcstr!(""),
2867 false,
2868 Pattern::new(Pattern::Alternatives(vec![
2869 Pattern::Constant(rcstr!("extensions/Component")),
2870 Pattern::Constant(rcstr!("extensions/Component.web.tsx")),
2871 Pattern::Constant(rcstr!("extensions/Component.tsx")),
2872 ])),
2873 )
2874 .await?
2875 .into_iter()
2876 .map(|m| m.name().to_string())
2877 .collect::<Vec<_>>();
2878
2879 let subpath_ordering = read_matches(
2883 root.clone(),
2884 rcstr!(""),
2885 false,
2886 Pattern::new({
2887 let mut p = Pattern::Alternatives(vec![
2888 Pattern::Concatenation(vec![
2889 Pattern::Constant(rcstr!("prio/a/")),
2890 Pattern::Dynamic,
2891 ]),
2892 Pattern::Concatenation(vec![
2893 Pattern::Constant(rcstr!("prio/b/")),
2894 Pattern::Dynamic,
2895 ]),
2896 ]);
2897 p.normalize();
2898 p
2899 }),
2900 )
2901 .await?
2902 .into_iter()
2903 .map(|m| m.name().to_string())
2904 .collect::<Vec<_>>();
2905
2906 Ok(ReadMatchesOutput {
2907 dynamic,
2908 dynamic_file_suffix,
2909 node_modules_dynamic,
2910 extension_ordering,
2911 subpath_ordering,
2912 }
2913 .cell())
2914 }
2915
2916 let matches = read_matches_operation().read_strongly_consistent().await?;
2917
2918 assert_eq!(
2920 matches.dynamic,
2921 &[
2922 "extensions",
2923 "extensions/",
2924 "extensions/Component.tsx",
2925 "extensions/Component.web.tsx",
2926 "index.js",
2927 "prio",
2928 "prio/",
2929 "prio/a",
2930 "prio/a/",
2931 "prio/a/Component.tsx",
2932 "prio/b",
2933 "prio/b/",
2934 "prio/b/Component.tsx",
2935 "sub",
2936 "sub/",
2937 "sub/foo-a.js",
2938 "sub/foo-b.js",
2939 ]
2940 );
2941
2942 assert_eq!(
2944 matches.dynamic_file_suffix,
2945 &["sub/foo-a.js", "sub/foo-b.js"]
2946 );
2947
2948 assert_eq!(matches.node_modules_dynamic, &["node_modules"]);
2951
2952 assert_eq!(
2954 matches.extension_ordering,
2955 &["extensions/Component.web.tsx", "extensions/Component.tsx",]
2956 );
2957
2958 assert!(
2962 matches
2963 .subpath_ordering
2964 .iter()
2965 .position(|s| s.starts_with("prio/a/"))
2966 .unwrap()
2967 < matches
2968 .subpath_ordering
2969 .iter()
2970 .position(|s| s.starts_with("prio/b/"))
2971 .unwrap(),
2972 "Expected prio/a/ results before prio/b/ results, got: {:?}",
2973 matches.subpath_ordering
2974 );
2975
2976 Ok(())
2977 })
2978 .await
2979 .unwrap();
2980 }
2981}