1use std::{borrow::Cow, path::MAIN_SEPARATOR};
194
195use bon::{builder, Builder};
196use logos::Logos;
197use regex_automata::{nfa::thompson, util::look::LookMatcher};
198use regex_syntax::{
199 hir::{
200 Class, ClassBytes, ClassBytesRange, ClassUnicode, ClassUnicodeRange, Dot, Hir, Repetition,
201 },
202 ParserBuilder,
203};
204
205use util::SurroundingWildcardHandler;
206
207mod util;
208
209#[derive(Logos, Clone, Copy, Debug, PartialEq)]
211pub enum WildcardToken {
212 #[token("?")]
214 Any,
215
216 #[token("*")]
218 Star,
219
220 #[regex("[^*?]+")]
222 Text,
223}
224
225#[builder]
227pub fn parse_wildcard(
228 #[builder(finish_fn)] pattern: &str,
229 #[builder(default = true)]
231 surrounding_wildcard_as_anchor: bool,
232) -> Hir {
233 let mut lex = WildcardToken::lexer(&pattern);
234 let mut hirs = Vec::new();
235 let mut surrounding_handler =
236 surrounding_wildcard_as_anchor.then(|| SurroundingWildcardHandler::new(PathSeparator::Any));
237 while let Some(Ok(token)) = lex.next() {
238 if let Some(h) = &mut surrounding_handler {
239 if h.skip(token, &mut hirs, &lex) {
240 continue;
241 }
242 }
243
244 hirs.push(match token {
245 WildcardToken::Any => Hir::dot(Dot::AnyChar),
246 WildcardToken::Star => Hir::repetition(Repetition {
247 min: 0,
248 max: None,
249 greedy: true,
250 sub: Hir::dot(Dot::AnyByte).into(),
251 }),
252 WildcardToken::Text => Hir::literal(lex.slice().as_bytes()),
253 });
254 }
255
256 if let Some(h) = surrounding_handler {
257 h.insert_anchors(&mut hirs);
258 }
259
260 Hir::concat(hirs)
261}
262
263#[derive(Default, Clone, Copy)]
265pub enum PathSeparator {
266 #[default]
268 Os,
269 Unix,
271 Windows,
273 Any,
275}
276
277impl PathSeparator {
278 fn os_desugar() -> Self {
279 if MAIN_SEPARATOR == '\\' {
280 PathSeparator::Windows
281 } else {
282 PathSeparator::Unix
283 }
284 }
285
286 fn desugar(self) -> Self {
287 match self {
288 PathSeparator::Os => Self::os_desugar(),
289 sep => sep,
290 }
291 }
292
293 pub fn is_unix_or_any(self) -> bool {
294 matches!(self.desugar(), PathSeparator::Unix | PathSeparator::Any)
295 }
296
297 pub fn is_windows_or_any(self) -> bool {
298 matches!(self.desugar(), PathSeparator::Windows | PathSeparator::Any)
299 }
300
301 fn literal(&self) -> Hir {
302 match self.desugar() {
303 PathSeparator::Os => unreachable!(),
304 PathSeparator::Unix => Hir::literal(*b"/"),
305 PathSeparator::Windows => Hir::literal(*b"\\"),
306 PathSeparator::Any => Hir::class(Class::Bytes(ClassBytes::new([
307 ClassBytesRange::new(b'/', b'/'),
308 ClassBytesRange::new(b'\\', b'\\'),
309 ]))),
310 }
311 }
312
313 pub fn any_byte_except(&self) -> Hir {
314 match self {
315 PathSeparator::Os => Hir::dot(Dot::AnyByteExcept(MAIN_SEPARATOR as u8)),
320 PathSeparator::Unix => Hir::dot(Dot::AnyByteExcept(b'/')),
321 PathSeparator::Windows => Hir::dot(Dot::AnyByteExcept(b'\\')),
322 PathSeparator::Any => Hir::class(Class::Bytes(ClassBytes::new([
323 ClassBytesRange::new(0, b'/' - 1),
324 ClassBytesRange::new(b'/' + 1, b'\\' - 1),
325 ClassBytesRange::new(b'\\' + 1, u8::MAX),
326 ]))),
327 }
328 }
329
330 pub fn any_char_except(&self) -> Hir {
331 match self {
332 PathSeparator::Os => Hir::dot(Dot::AnyCharExcept(MAIN_SEPARATOR)),
333 PathSeparator::Unix => Hir::dot(Dot::AnyCharExcept('/')),
334 PathSeparator::Windows => Hir::dot(Dot::AnyCharExcept('\\')),
335 PathSeparator::Any => Hir::class(Class::Unicode(ClassUnicode::new([
336 ClassUnicodeRange::new('\0', '.'),
337 ClassUnicodeRange::new('0', '['),
338 ClassUnicodeRange::new(']', char::MAX),
339 ]))),
340 }
341 }
342
343 pub fn look_matcher(&self) -> LookMatcher {
345 debug_assert!(!matches!(self, PathSeparator::Any));
346
347 let mut lookm = LookMatcher::new();
348 lookm.set_line_terminator(if self.is_unix_or_any() { b'/' } else { b'\\' });
349 lookm
350 }
351
352 pub fn look_matcher_config(&self) -> thompson::Config {
354 thompson::Config::new().look_matcher(self.look_matcher())
355 }
356
357 pub fn os_complement() -> PathSeparator {
368 if MAIN_SEPARATOR == '/' {
369 PathSeparator::Windows
370 } else {
371 PathSeparator::Unix
372 }
373 }
374}
375
376#[derive(Clone, Copy)]
377#[non_exhaustive]
378pub enum GlobStar {
379 Current,
381 Any,
383 ToChild,
385 ToChildStart,
387}
388
389impl GlobStar {
390 pub fn to_pattern(&self, separator: PathSeparator) -> &'static str {
391 match self {
392 GlobStar::Current => "*",
393 GlobStar::Any => "**",
394 GlobStar::ToChild => {
395 if separator.is_unix_or_any() {
396 "*/**"
397 } else {
398 r"*\**"
399 }
400 }
401 GlobStar::ToChildStart => {
402 if separator.is_unix_or_any() {
403 "**/"
404 } else {
405 r"**\"
406 }
407 }
408 }
409 }
410}
411
412#[derive(Logos, Debug, PartialEq)]
414enum GlobExtToken {
415 #[token("/")]
416 SepUnix,
417
418 #[token(r"\")]
419 SepWin,
420
421 #[token("//")]
422 TwoSepUnix,
423
424 #[token(r"\\")]
425 TwoSepWin,
426
427 #[regex(r"[^/\\]+")]
429 Text,
430}
431
432#[derive(Builder, Default, Clone, Copy)]
438pub struct GlobExtConfig {
439 #[builder(with = |sep: PathSeparator, star: GlobStar| (sep, star))]
445 two_separator_as_star: Option<(PathSeparator, GlobStar)>,
446 #[builder(with = |sep: PathSeparator, star: GlobStar| (sep, star))]
459 separator_as_star: Option<(PathSeparator, GlobStar)>,
460}
461
462impl GlobExtConfig {
463 pub fn new_ev() -> Self {
465 GlobExtConfig {
466 two_separator_as_star: Some((PathSeparator::Any, GlobStar::ToChild)),
467 separator_as_star: Some((PathSeparator::os_complement(), GlobStar::ToChildStart)),
468 }
469 }
470
471 #[cfg(test)]
472 fn desugar_single<'p>(&self, pattern: &'p str, to_separator: PathSeparator) -> Cow<'p, str> {
473 let mut pattern = Cow::Borrowed(pattern);
474 if let Some((sep, star)) = self.two_separator_as_star {
475 let star_pattern = star.to_pattern(to_separator);
476 pattern = match sep.desugar() {
477 PathSeparator::Os => unreachable!(),
478 PathSeparator::Unix => pattern.replace("//", star_pattern),
479 PathSeparator::Windows => pattern.replace(r"\\", star_pattern),
480 PathSeparator::Any => pattern
481 .replace("//", star_pattern)
482 .replace(r"\\", star_pattern),
483 }
484 .into();
485 }
486 if let Some((sep, star)) = self.separator_as_star {
487 let star_pattern = star.to_pattern(to_separator);
488 pattern = match sep.desugar() {
489 PathSeparator::Os => unreachable!(),
490 PathSeparator::Unix => pattern.replace('/', star_pattern),
491 PathSeparator::Windows => pattern.replace('\\', star_pattern),
492 PathSeparator::Any => {
493 if to_separator.is_unix_or_any() {
494 pattern
495 .replace('/', star_pattern)
496 .replace('\\', star_pattern)
497 } else {
498 pattern
499 .replace('\\', star_pattern)
500 .replace('/', star_pattern)
501 }
502 }
503 }
504 .into();
505 }
506 #[cfg(test)]
507 dbg!(&pattern);
508 pattern
509 }
510
511 pub fn desugar<'p>(&self, pattern: &'p str, to_separator: PathSeparator) -> Cow<'p, str> {
513 if self.two_separator_as_star.is_none() && self.separator_as_star.is_none() {
514 return Cow::Borrowed(pattern);
515 }
516 let mut lex = GlobExtToken::lexer(&pattern);
519 let mut pattern = String::with_capacity(pattern.len());
520 let sep_unix = self
521 .separator_as_star
522 .filter(|(sep, _)| sep.is_unix_or_any())
523 .map(|(_, star)| star.to_pattern(to_separator))
524 .unwrap_or("/");
525 let sep_win = self
526 .separator_as_star
527 .filter(|(sep, _)| sep.is_windows_or_any())
528 .map(|(_, star)| star.to_pattern(to_separator))
529 .unwrap_or(r"\");
530 let two_sep_unix = self
531 .two_separator_as_star
532 .filter(|(sep, _)| sep.is_unix_or_any())
533 .map(|(_, star)| star.to_pattern(to_separator))
534 .unwrap_or("//");
535 let two_sep_win = self
536 .two_separator_as_star
537 .filter(|(sep, _)| sep.is_windows_or_any())
538 .map(|(_, star)| star.to_pattern(to_separator))
539 .unwrap_or(r"\\");
540 while let Some(Ok(token)) = lex.next() {
541 pattern.push_str(match token {
542 GlobExtToken::SepUnix => sep_unix,
543 GlobExtToken::SepWin => sep_win,
544 GlobExtToken::TwoSepUnix => two_sep_unix,
545 GlobExtToken::TwoSepWin => two_sep_win,
546 GlobExtToken::Text => lex.slice(),
547 });
548 }
549 #[cfg(test)]
550 dbg!(&pattern);
551 Cow::Owned(pattern)
552 }
553}
554
555#[derive(Logos, Clone, Copy, Debug, PartialEq)]
557pub enum WildcardPathToken {
558 #[token("?")]
560 Any,
561
562 #[token("*")]
564 Star,
565
566 #[token("**")]
568 GlobStar,
569
570 #[token("/")]
571 SepUnix,
572
573 #[token(r"\")]
574 SepWin,
575
576 #[regex(r"[^*?/\\]+")]
578 Text,
579}
580
581#[builder]
585pub fn parse_wildcard_path(
586 #[builder(finish_fn)] pattern: &str,
587 pattern_separator: Option<PathSeparator>,
591 separator: PathSeparator,
595 #[builder(default = true)]
597 surrounding_wildcard_as_anchor: bool,
598 #[builder(default)] ext: GlobExtConfig,
599) -> Hir {
600 let pattern_separator = pattern_separator.unwrap_or(separator);
601
602 let pattern = ext.desugar(pattern, pattern_separator);
604
605 let mut lex = WildcardPathToken::lexer(&pattern);
606 let mut hirs = Vec::new();
607 let mut surrounding_handler =
608 surrounding_wildcard_as_anchor.then(|| SurroundingWildcardHandler::new(pattern_separator));
609 while let Some(Ok(token)) = lex.next() {
610 if let Some(h) = &mut surrounding_handler {
611 if h.skip(token, &mut hirs, &lex) {
612 continue;
613 }
614 }
615
616 hirs.push(match token {
617 WildcardPathToken::Any => separator.any_char_except(),
618 WildcardPathToken::Star => Hir::repetition(Repetition {
619 min: 0,
620 max: None,
621 greedy: true,
622 sub: separator.any_byte_except().into(),
623 }),
624 WildcardPathToken::GlobStar => Hir::repetition(Repetition {
625 min: 0,
626 max: None,
627 greedy: true,
628 sub: Hir::dot(Dot::AnyByte).into(),
629 }),
630 WildcardPathToken::SepUnix if pattern_separator.is_unix_or_any() => separator.literal(),
631 WildcardPathToken::SepWin if pattern_separator.is_windows_or_any() => {
632 separator.literal()
633 }
634 WildcardPathToken::Text | WildcardPathToken::SepUnix | WildcardPathToken::SepWin => {
635 Hir::literal(lex.slice().as_bytes())
636 }
637 });
638 }
639
640 if let Some(h) = surrounding_handler {
641 h.insert_anchors(&mut hirs);
642 }
643
644 Hir::concat(hirs)
645}
646
647#[derive(Logos, Clone, Copy, Debug, PartialEq)]
649pub enum GlobPathToken {
650 #[token("?")]
652 Any,
653
654 #[token("*")]
656 Star,
657
658 #[regex(r"\[[^\]]+\]\]?")]
660 Class,
661
662 #[token("**")]
664 GlobStar,
665
666 #[token("/")]
667 SepUnix,
668
669 #[token(r"\")]
670 SepWin,
671
672 #[regex(r"[^*?\[\]/\\]+")]
674 Text,
675}
676
677#[builder]
679pub fn parse_glob_path(
680 #[builder(finish_fn)] pattern: &str,
681 pattern_separator: Option<PathSeparator>,
685 separator: PathSeparator,
689 #[builder(default = true)]
691 surrounding_wildcard_as_anchor: bool,
692 #[builder(default)] ext: GlobExtConfig,
693) -> Hir {
694 let pattern_separator = pattern_separator.unwrap_or(separator);
695
696 let pattern = ext.desugar(pattern, pattern_separator);
698
699 let mut lex = GlobPathToken::lexer(&pattern);
700 let mut hirs = Vec::new();
701 let mut surrounding_handler =
702 surrounding_wildcard_as_anchor.then(|| SurroundingWildcardHandler::new(pattern_separator));
703 let mut parser = ParserBuilder::new().unicode(false).utf8(false).build();
704 while let Some(Ok(token)) = lex.next() {
705 if let Some(h) = &mut surrounding_handler {
706 if h.skip(token, &mut hirs, &lex) {
707 continue;
708 }
709 }
710
711 hirs.push(match token {
712 GlobPathToken::Any => separator.any_char_except(),
713 GlobPathToken::Star => Hir::repetition(Repetition {
714 min: 0,
715 max: None,
716 greedy: true,
717 sub: separator.any_byte_except().into(),
718 }),
719 GlobPathToken::GlobStar => Hir::repetition(Repetition {
720 min: 0,
721 max: None,
722 greedy: true,
723 sub: Hir::dot(Dot::AnyByte).into(),
724 }),
725 GlobPathToken::Class => {
726 let s = lex.slice();
727 match s {
728 "[[]" => Hir::literal("[".as_bytes()),
729 _ => {
731 match parser.parse(&s.replace("[!", "[^").replace(r"\", r"\\")) {
733 Ok(hir) => hir,
734 Err(_e) => {
735 #[cfg(test)]
736 println!("{_e}");
737 Hir::literal(s.as_bytes())
738 }
739 }
740 }
741 }
742 }
743 GlobPathToken::SepUnix if pattern_separator.is_unix_or_any() => separator.literal(),
744 GlobPathToken::SepWin if pattern_separator.is_windows_or_any() => separator.literal(),
745 GlobPathToken::Text | GlobPathToken::SepUnix | GlobPathToken::SepWin => {
746 Hir::literal(lex.slice().as_bytes())
747 }
748 });
749 }
750
751 if let Some(h) = surrounding_handler {
752 h.insert_anchors(&mut hirs);
753 }
754
755 Hir::concat(hirs)
756}
757
758#[cfg(test)]
759mod tests {
760 use regex_automata::Match;
761 use regex_syntax::ParserBuilder;
762
763 use crate::{matcher::MatchConfig, regex::lita::Regex};
764
765 use super::*;
766
767 #[test]
768 fn wildcard_path_token() {
769 let input = "*text?more*?text**end";
770 let mut lexer = WildcardPathToken::lexer(input);
771 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Star)));
772 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Text)));
773 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Any)));
774 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Text)));
775 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Star)));
776 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Any)));
777 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Text)));
778 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::GlobStar)));
779 assert_eq!(lexer.next(), Some(Ok(WildcardPathToken::Text)));
780 assert_eq!(lexer.next(), None);
781 }
782
783 #[test]
784 fn wildcard() {
785 let re = Regex::builder()
786 .build_from_hir(parse_wildcard().call("?a*b**c"))
787 .unwrap();
788 assert!(re.is_match(r"1a2b33c"));
789 assert!(re.is_match(r"1a\b33c"));
790 assert!(re.is_match(r"b1a\b33c") == false);
791
792 let re = Regex::builder()
793 .build_from_hir(parse_wildcard().call(r"Win*\*\*.exe"))
794 .unwrap();
795 assert!(re.is_match(r"C:\Windows\System32\notepad.exe"));
796 }
797
798 #[test]
799 fn wildcard_path() {
800 let hir1 = ParserBuilder::new()
801 .utf8(false)
802 .build()
803 .parse(r"[^\\](?s-u)a[^\\]*b.*c")
804 .unwrap();
805 println!("{:?}", hir1);
806
807 let hir2 = parse_wildcard_path()
808 .separator(PathSeparator::Windows)
809 .surrounding_wildcard_as_anchor(false)
810 .call("?a*b**c");
811 println!("{:?}", hir2);
812
813 assert_eq!(hir1, hir2);
814
815 let re = Regex::builder().build_from_hir(hir2).unwrap();
816 assert!(re.is_match(r"1a2b33c"));
817 assert!(re.is_match(r"1a\b33c") == false);
818
819 let re = Regex::builder()
820 .build_from_hir(
821 parse_wildcard_path()
822 .separator(PathSeparator::Windows)
823 .call(r"Win*\*\*.exe"),
824 )
825 .unwrap();
826 assert!(re.is_match(r"C:\Windows\System32\notepad.exe"));
827
828 let re = Regex::builder()
829 .build_from_hir(
830 parse_wildcard_path()
831 .separator(PathSeparator::Windows)
832 .call(r"Win**.exe"),
833 )
834 .unwrap();
835 assert!(re.is_match(r"C:\Windows\System32\notepad.exe"));
836
837 let re = Regex::builder()
838 .ib(MatchConfig::builder().pinyin(Default::default()).build())
839 .build_from_hir(
840 parse_wildcard_path()
841 .separator(PathSeparator::Windows)
842 .call(r"win**pyss.exe"),
843 )
844 .unwrap();
845 assert!(re.is_match(r"C:\Windows\System32\拼音搜索.exe"));
846
847 let re = Regex::builder()
848 .ib(MatchConfig::builder().romaji(Default::default()).build())
849 .build_from_hir(
850 parse_wildcard_path()
851 .separator(PathSeparator::Windows)
852 .call("wifi**miku"),
853 )
854 .unwrap();
855 assert!(re.is_match(r"C:\Windows\System32\ja-jp\WiFiTask\ミク.exe"));
856 }
857
858 #[test]
859 fn glob_path() {
860 let is_match = |p, h| {
861 Regex::builder()
862 .build_from_hir(parse_glob_path().separator(PathSeparator::Windows).call(p))
863 .unwrap()
864 .is_match(h)
865 };
866
867 assert!(is_match("a[b]z", "abz"));
869 assert!(is_match("a[b]z", "aBz") == false);
870 assert!(is_match("a[bcd]z", "acz"));
871
872 assert!(is_match("a[b-z]z", "ayz"));
874
875 assert!(is_match("a[!b]z", "abz") == false);
877 assert!(is_match("a[!b]z", "acz"));
878
879 assert!(is_match("a[[:space:]]z", "a z"));
881
882 assert!(is_match("a[?]z", "a?z"));
884 assert!(is_match("a[*]z", "a*z"));
885 assert!(is_match("a[[]z", "a[z"));
886 assert!(is_match("a[-]z", "a-z"));
887 assert!(is_match("a[]]z", "a]z"));
888 assert!(is_match(r"a[\d]z", r"a\z"));
889
890 assert!(is_match("a[b", "a[bz"));
892 assert!(is_match("a[[b]z", "a[[b]z"));
893 assert!(is_match("a[!]z", "a[!]z"));
894 }
895
896 #[test]
897 fn complement_separator_as_glob_star() {
898 let ext = GlobExtConfig::builder()
899 .separator_as_star(PathSeparator::Any, GlobStar::ToChild)
900 .build();
901
902 assert_eq!(
903 ext.desugar_single(r"xx/hj", PathSeparator::Windows),
904 r"xx*\**hj"
905 );
906 assert_eq!(ext.desugar(r"xx/hj", PathSeparator::Windows), r"xx*\**hj");
907 let re = Regex::builder()
908 .build_from_hir(
909 parse_wildcard_path()
910 .separator(PathSeparator::Windows)
911 .ext(ext)
912 .call(r"xx/hj"),
913 )
914 .unwrap();
915 assert!(re.is_match(r"xxzl\sj\8yhj"));
916
917 let re = Regex::builder()
918 .build_from_hir(
919 parse_wildcard_path()
920 .separator(PathSeparator::Unix)
921 .ext(ext)
922 .call(r"xx\hj"),
923 )
924 .unwrap();
925 assert!(re.is_match(r"xxzl/sj/8yhj"));
926
927 let re = Regex::builder()
928 .ib(MatchConfig::builder().pinyin(Default::default()).build())
929 .build_from_hir(
930 parse_wildcard_path()
931 .separator(PathSeparator::Windows)
932 .ext(ext)
933 .call(r"xx/hj"),
934 )
935 .unwrap();
936 assert!(re.is_match(r"学习资料\时间\7月合集"));
937
938 let ext = GlobExtConfig::builder()
940 .separator_as_star(PathSeparator::Any, GlobStar::ToChildStart)
941 .build();
942 let re = Regex::builder()
943 .ib(MatchConfig::default())
944 .build_from_hir(
945 parse_wildcard_path()
946 .separator(PathSeparator::Windows)
947 .ext(ext)
948 .call(r"xx/"),
949 )
950 .unwrap();
951 assert!(re.is_match(r"C:\Xxzl\sj\8yhj"));
952 assert!(re.is_match(r"C:\学习\Xxzl\sj\8yhj"));
953 }
954
955 #[test]
956 fn surrounding_wildcard_as_anchor() {
957 let re = Regex::builder()
959 .build_from_hir(
960 parse_wildcard_path()
961 .separator(PathSeparator::Windows)
962 .call(r"*.mp4"),
963 )
964 .unwrap();
965 assert!(re.is_match(r"瑠璃の宝石.mp4"));
966 assert!(re.is_match(r"瑠璃の宝石.mp4_001947.296.webp") == false);
967
968 let re = Regex::builder()
970 .ib(MatchConfig::builder().pinyin(Default::default()).build())
971 .build_from_hir(
972 parse_wildcard_path()
973 .separator(PathSeparator::Windows)
974 .call(r"ll*"),
975 )
976 .unwrap();
977 assert!(re.is_match(r"瑠璃の宝石.mp4"));
978 assert_eq!(re.find(r"瑠璃の宝石.mp4"), Some(Match::must(0, 0..6)));
979 assert!(re.is_match(r"ruri 瑠璃の宝石.mp4") == false);
980
981 let re = Regex::builder()
982 .ib(MatchConfig::builder().pinyin(Default::default()).build())
983 .build_from_hir(
984 parse_wildcard_path()
985 .separator(PathSeparator::Windows)
986 .call(r"ll***"),
987 )
988 .unwrap();
989 assert_eq!(re.find(r"瑠璃の宝石.mp4"), Some(Match::must(0, 0..6)));
990 assert!(re.is_match(r"ruri 瑠璃の宝石.mp4") == false);
991
992 let re = Regex::builder()
994 .ib(MatchConfig::builder().pinyin(Default::default()).build())
995 .build_from_hir(
996 parse_wildcard_path()
997 .separator(PathSeparator::Windows)
998 .call(r"ll*.mp4"),
999 )
1000 .unwrap();
1001 assert!(re.is_match(r"瑠璃の宝石.mp4"));
1002 assert!(re.is_match(r"ruri 瑠璃の宝石.mp4"));
1003 assert!(re.is_match(r"ruri 瑠璃の宝石.mp4_001133.937.webp"));
1004
1005 let re = Regex::builder()
1007 .ib(MatchConfig::builder().pinyin(Default::default()).build())
1008 .build_from_hir(
1009 parse_wildcard_path()
1010 .separator(PathSeparator::Windows)
1011 .call(r"??.mp4"),
1012 )
1013 .unwrap();
1014 assert_eq!(re.find(r"宝石.mp4"), Some(Match::must(0, 0..10)));
1015 assert_eq!(re.find(r"瑠璃の宝石.mp4"), None);
1016
1017 let re = Regex::builder()
1019 .ib(MatchConfig::builder().pinyin(Default::default()).build())
1020 .build_from_hir(
1021 parse_wildcard_path()
1022 .separator(PathSeparator::Windows)
1023 .call(r"ll???"),
1024 )
1025 .unwrap();
1026 assert_eq!(re.find(r"瑠璃の宝石"), Some(Match::must(0, 0..15)));
1027 assert!(re.is_match(r"ruri 瑠璃の宝石") == false);
1028 }
1029
1030 #[test]
1031 fn surrounding_wildcard_as_anchor_path() {
1032 let re = Regex::builder()
1034 .ib(MatchConfig::builder().pinyin(Default::default()).build())
1035 .thompson(PathSeparator::Windows.look_matcher_config())
1036 .build_from_hir(
1037 parse_wildcard_path()
1038 .separator(PathSeparator::Windows)
1039 .call(r"?:\$RECYCLE*\"),
1040 )
1041 .unwrap();
1042 assert!(re.is_match(r"C:\$RECYCLE.BIN\⑨"));
1043 assert!(re.is_match(r"C:\$RECYCLE.BIN\9"));
1044 assert!(re.is_match(r"C:\$RECYCLE.BIN\99"));
1045 assert!(re.is_match(r"D:\C:\$RECYCLE.BIN\9"));
1046 assert!(re.is_match(r"DC:\$RECYCLE.BIN\9") == false);
1047 assert!(re.is_match(r"D:\DC:\$RECYCLE.BIN\9") == false);
1048
1049 let re = Regex::builder()
1051 .ib(MatchConfig::builder().pinyin(Default::default()).build())
1052 .thompson(PathSeparator::Windows.look_matcher_config())
1053 .build_from_hir(
1054 parse_wildcard_path()
1055 .separator(PathSeparator::Windows)
1056 .call(r"?:\$RECYCLE*\?"),
1057 )
1058 .unwrap();
1059 assert!(re.is_match(r"C:\$RECYCLE.BIN\⑨"));
1060 assert!(re.is_match(r"C:\$RECYCLE.BIN\9"));
1061 assert!(re.is_match(r"C:\$RECYCLE.BIN\99") == false);
1062 assert!(re.is_match(r"D:\C:\$RECYCLE.BIN\9"));
1063 assert!(re.is_match(r"D:\C:\$RECYCLE.BIN\99") == false);
1064 assert!(re.is_match(r"DC:\$RECYCLE.BIN\9") == false);
1065 assert!(re.is_match(r"D:\DC:\$RECYCLE.BIN\9") == false);
1066 }
1067}