1use serde::{Deserialize, Serialize};
2
3use crate::utils::is_path_separator;
4
5fn default_true() -> bool {
6 true
7}
8
9#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
10#[serde(default, rename_all = "camelCase")]
11pub struct CompileOptions {
12 pub bash: bool,
13 #[serde(default)]
14 pub basename: bool,
15 pub contains: bool,
16 pub dot: bool,
17 #[serde(default)]
18 pub flags: String,
19 #[serde(skip)]
20 pub literal_plus_quantifier: bool,
21 #[serde(default)]
22 pub match_base: bool,
23 pub nobrace: bool,
24 pub nobracket: bool,
25 pub noextglob: bool,
26 pub noglobstar: bool,
27 pub nocase: bool,
28 pub nonegate: bool,
29 #[serde(default = "default_true")]
30 pub posix: bool,
31 pub strict_brackets: bool,
32 pub strict_slashes: bool,
33 pub unescape: bool,
34 pub windows: bool,
35 pub regex: bool,
36 #[serde(default)]
37 pub keep_quotes: bool,
38 #[serde(default)]
39 pub max_length: Option<usize>,
40}
41
42impl Default for CompileOptions {
43 fn default() -> Self {
44 Self {
45 bash: false,
46 basename: false,
47 contains: false,
48 dot: false,
49 flags: String::new(),
50 literal_plus_quantifier: false,
51 match_base: false,
52 nobrace: false,
53 nobracket: false,
54 noextglob: false,
55 noglobstar: false,
56 nocase: false,
57 nonegate: false,
58 posix: true,
59 strict_brackets: false,
60 strict_slashes: false,
61 unescape: false,
62 windows: false,
63 regex: false,
64 keep_quotes: false,
65 max_length: None,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
71#[serde(rename_all = "camelCase")]
72pub struct ParseState {
73 pub input: String,
74 pub output: String,
75 pub negated: bool,
76 pub fastpaths: bool,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub tokens: Option<Vec<ParseToken>>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "camelCase")]
83pub struct RegexDescriptor {
84 pub source: String,
85 pub flags: String,
86 pub output: String,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub state: Option<ParseState>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92#[serde(rename_all = "camelCase")]
93pub struct ParseToken {
94 #[serde(rename = "type")]
95 pub kind: String,
96 pub value: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub output: Option<String>,
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102enum TokenKind {
103 None,
104 Literal,
105 Bracket,
106 Group,
107 RegexEscape,
108 Wildcard,
109}
110
111fn escape_regex_char(ch: char) -> &'static str {
112 match ch {
113 '$' => "\\$",
114 '(' => "\\(",
115 ')' => "\\)",
116 '*' => "\\*",
117 '+' => "\\+",
118 '.' => "\\.",
119 '?' => "\\?",
120 '[' => "\\[",
121 ']' => "\\]",
122 '^' => "\\^",
123 '{' => "\\{",
124 '|' => "\\|",
125 '}' => "\\}",
126 _ => "",
127 }
128}
129
130fn slash_literal(options: &CompileOptions) -> &'static str {
131 if options.windows {
132 r"[\\/]"
133 } else {
134 "/"
135 }
136}
137
138fn qmark(options: &CompileOptions) -> &'static str {
139 if options.windows {
140 r"[^\\/]"
141 } else {
142 r"[^/]"
143 }
144}
145
146fn qmark_no_dot(options: &CompileOptions) -> &'static str {
147 if options.windows {
148 r"[^.\\/]"
149 } else {
150 r"[^./]"
151 }
152}
153
154fn one_char() -> &'static str {
155 r"(?=.)"
156}
157
158fn star(options: &CompileOptions) -> String {
159 if options.bash {
160 ".*?".to_string()
161 } else {
162 format!("{}*?", qmark(options))
163 }
164}
165
166fn segment_leader(options: &CompileOptions) -> &'static str {
167 if options.dot {
168 ""
169 } else {
170 r"(?!\.)"
171 }
172}
173
174fn globstar_segment(options: &CompileOptions) -> String {
175 if options.dot {
176 format!(
177 r"(?!\.{{1,2}}(?:{}|$)){}+",
178 slash_literal(options),
179 qmark(options)
180 )
181 } else {
182 format!(r"(?!\.){}+", qmark(options))
183 }
184}
185
186fn extglob_slashy_body(options: &CompileOptions) -> String {
187 if options.windows {
188 r"(?:(?:(?!(?:^|[\\/])\.).)*?)".to_string()
189 } else {
190 r"(?:(?:(?!(?:^|/)\.).)*?)".to_string()
191 }
192}
193
194fn is_separator(ch: char, options: &CompileOptions) -> bool {
195 if options.windows {
196 is_path_separator(ch)
197 } else {
198 ch == '/'
199 }
200}
201
202fn collect_enclosed(
203 chars: &[char],
204 start: usize,
205 open: char,
206 close: char,
207) -> Option<(String, usize)> {
208 let mut depth = 0usize;
209 let mut index = start;
210
211 while index < chars.len() {
212 let ch = chars[index];
213
214 if ch == '\\' {
215 index += 2;
216 continue;
217 }
218
219 if ch == open {
220 depth += 1;
221 } else if ch == close {
222 depth = depth.saturating_sub(1);
223 if depth == 0 {
224 let inner = chars[start + 1..index].iter().collect::<String>();
225 return Some((inner, index + 1));
226 }
227 }
228
229 index += 1;
230 }
231
232 None
233}
234
235fn collect_bracket(chars: &[char], start: usize) -> Option<(String, usize)> {
236 let mut inner = String::new();
237 let mut index = start + 1;
238
239 while index < chars.len() {
240 let ch = chars[index];
241
242 if ch == '\\' {
243 inner.push(ch);
244 if let Some(next) = chars.get(index + 1) {
245 inner.push(*next);
246 index += 2;
247 } else {
248 index += 1;
249 }
250 continue;
251 }
252
253 if ch == '[' && chars.get(index + 1) == Some(&':') {
254 inner.push('[');
255 inner.push(':');
256 index += 2;
257
258 while index < chars.len() {
259 let current = chars[index];
260 inner.push(current);
261
262 if current == ':' && chars.get(index + 1) == Some(&']') {
263 inner.push(']');
264 index += 2;
265 break;
266 }
267
268 index += 1;
269 }
270
271 continue;
272 }
273
274 if ch == ']' && !inner.is_empty() && inner != "^" && inner != "!" {
275 return Some((inner, index + 1));
276 }
277
278 inner.push(ch);
279 index += 1;
280 }
281
282 None
283}
284
285fn split_top_level(input: &str, delimiter: char) -> Vec<String> {
286 let chars: Vec<char> = input.chars().collect();
287 let mut parts = Vec::new();
288 let mut current = String::new();
289 let mut parens = 0usize;
290 let mut braces = 0usize;
291 let mut brackets = 0usize;
292 let mut index = 0usize;
293
294 while index < chars.len() {
295 let ch = chars[index];
296
297 if ch == '\\' {
298 current.push(ch);
299 if let Some(next) = chars.get(index + 1) {
300 current.push(*next);
301 index += 2;
302 continue;
303 }
304 }
305
306 match ch {
307 '(' => parens += 1,
308 ')' => parens = parens.saturating_sub(1),
309 '{' => braces += 1,
310 '}' => braces = braces.saturating_sub(1),
311 '[' => brackets += 1,
312 ']' => brackets = brackets.saturating_sub(1),
313 _ => {}
314 }
315
316 if ch == delimiter && parens == 0 && braces == 0 && brackets == 0 {
317 parts.push(current);
318 current = String::new();
319 index += 1;
320 continue;
321 }
322
323 current.push(ch);
324 index += 1;
325 }
326
327 parts.push(current);
328 parts
329}
330
331fn split_top_level_range(input: &str) -> Option<(String, String)> {
332 let chars: Vec<char> = input.chars().collect();
333 let mut parens = 0usize;
334 let mut braces = 0usize;
335 let mut brackets = 0usize;
336 let mut index = 0usize;
337 let mut separator = None;
338
339 while index < chars.len() {
340 let ch = chars[index];
341
342 if ch == '\\' {
343 index += if index + 1 < chars.len() { 2 } else { 1 };
344 continue;
345 }
346
347 match ch {
348 '(' => parens += 1,
349 ')' => parens = parens.saturating_sub(1),
350 '{' => braces += 1,
351 '}' => braces = braces.saturating_sub(1),
352 '[' => brackets += 1,
353 ']' => brackets = brackets.saturating_sub(1),
354 '.' if chars.get(index + 1) == Some(&'.')
355 && parens == 0
356 && braces == 0
357 && brackets == 0 =>
358 {
359 if separator.is_some() {
360 return None;
361 }
362 separator = Some(index);
363 index += 2;
364 continue;
365 }
366 _ => {}
367 }
368
369 index += 1;
370 }
371
372 let position = separator?;
373 let start = chars[..position].iter().collect::<String>();
374 let end = chars[position + 2..].iter().collect::<String>();
375
376 if start.is_empty() || end.is_empty() {
377 return None;
378 }
379
380 Some((start, end))
381}
382
383fn expand_numeric_range(start: i64, end: i64) -> Option<String> {
384 let step = if start <= end { 1 } else { -1 };
385 let count = start.abs_diff(end) + 1;
386 if count > 1024 {
387 return None;
388 }
389
390 let mut values = Vec::with_capacity(count as usize);
391 let mut current = start;
392 loop {
393 values.push(escape_literal(¤t.to_string()));
394 if current == end {
395 break;
396 }
397 current += step;
398 }
399
400 Some(format!("(?:{})", values.join("|")))
401}
402
403fn expand_alpha_range(start: char, end: char) -> Option<String> {
404 if !start.is_ascii_alphanumeric() || !end.is_ascii_alphanumeric() {
405 return None;
406 }
407
408 let start_code = start as u32;
409 let end_code = end as u32;
410 if start_code <= end_code {
411 return Some(format!(
412 "[{}-{}]",
413 escape_literal(&start.to_string()),
414 escape_literal(&end.to_string())
415 ));
416 }
417
418 let count = start_code - end_code + 1;
419 if count > 128 {
420 return None;
421 }
422
423 let mut values = Vec::with_capacity(count as usize);
424 for code in (end_code..=start_code).rev() {
425 let ch = char::from_u32(code)?;
426 values.push(escape_literal(&ch.to_string()));
427 }
428 Some(format!("(?:{})", values.join("|")))
429}
430
431fn compile_range(start: &str, end: &str) -> Option<String> {
432 if let (Ok(left), Ok(right)) = (start.parse::<i64>(), end.parse::<i64>()) {
433 return expand_numeric_range(left, right);
434 }
435
436 let mut start_chars = start.chars();
437 let mut end_chars = end.chars();
438 let left = start_chars.next()?;
439 let right = end_chars.next()?;
440 if start_chars.next().is_none() && end_chars.next().is_none() {
441 return expand_alpha_range(left, right);
442 }
443
444 None
445}
446
447const POSIX_PUNCT_OUTPUT: &str = r##"\-!"#$%&'()\*+,./:;<=>?@[\]^_`{|}~"##;
448const POSIX_PUNCT_ENGINE: &str = r##"\-!"#$%&'()\*+,./:;<=>?@\[\]^_`{|}~"##;
449
450fn posix_class_source(name: &str) -> Option<&'static str> {
451 match name {
452 "alnum" => Some("a-zA-Z0-9"),
453 "alpha" => Some("a-zA-Z"),
454 "ascii" => Some(r"\x00-\x7F"),
455 "blank" => Some(r" \t"),
456 "cntrl" => Some(r"\x00-\x1F\x7F"),
457 "digit" => Some("0-9"),
458 "graph" => Some(r"\x21-\x7E"),
459 "lower" => Some("a-z"),
460 "print" => Some(r"\x20-\x7E "),
461 "punct" => Some(POSIX_PUNCT_OUTPUT),
462 "space" => Some(r" \t\r\n\v\f"),
463 "upper" => Some("A-Z"),
464 "word" => Some("A-Za-z0-9_"),
465 "xdigit" => Some("A-Fa-f0-9"),
466 _ => None,
467 }
468}
469
470pub fn regex_output_for_engine(output: &str) -> String {
471 output.replace(POSIX_PUNCT_OUTPUT, POSIX_PUNCT_ENGINE)
472}
473
474fn has_regex_chars(input: &str) -> bool {
475 input.chars().any(|ch| {
476 matches!(
477 ch,
478 '-' | '*' | '+' | '?' | '.' | '^' | '$' | '{' | '}' | '(' | ')' | '|' | '[' | ']'
479 )
480 })
481}
482
483fn escape_literal(input: &str) -> String {
484 let mut output = String::with_capacity(input.len() * 2);
485 for ch in input.chars() {
486 let escaped = escape_regex_char(ch);
487 if escaped.is_empty() {
488 if ch == '\\' {
489 output.push_str("\\\\");
490 } else {
491 output.push(ch);
492 }
493 } else {
494 output.push_str(escaped);
495 }
496 }
497 output
498}
499
500fn push_literal_char(output: &mut String, ch: char) {
501 if ch == '\\' {
502 output.push_str("\\\\");
503 return;
504 }
505
506 let escaped = escape_regex_char(ch);
507 if escaped.is_empty() {
508 output.push(ch);
509 } else {
510 output.push_str(escaped);
511 }
512}
513
514fn sanitize_nested_negation(input: &str, strip_terminal_anchor: bool) -> String {
515 let mut output = input.to_string();
516
517 if let Some(stripped) = output.strip_prefix("(?=.)") {
518 output = stripped.to_string();
519 }
520
521 if let Some(stripped) = output.strip_suffix("/?") {
522 output = stripped.to_string();
523 }
524
525 if strip_terminal_anchor {
526 output = output.replace("$))", "))");
527 output = output.replace("$)", ")");
528 }
529
530 output
531}
532
533fn contains_magic(input: &str) -> bool {
534 let mut escaped = false;
535
536 for ch in input.chars() {
537 if escaped {
538 escaped = false;
539 continue;
540 }
541
542 if ch == '\\' {
543 escaped = true;
544 continue;
545 }
546
547 if matches!(
548 ch,
549 '*' | '?' | '[' | ']' | '{' | '}' | '(' | ')' | '@' | '!'
550 ) {
551 return true;
552 }
553 }
554
555 false
556}
557
558fn dot_segment_guard(options: &CompileOptions) -> &'static str {
559 if options.windows {
560 r"(?!.*(?:^|[\\/])\.{1,2}(?:[\\/]|$))"
561 } else {
562 r"(?!.*(?:^|/)\.{1,2}(?:/|$))"
563 }
564}
565
566fn has_explicit_dot_segment(input: &str, options: &CompileOptions) -> bool {
567 let chars: Vec<char> = input.chars().collect();
568 let mut current = String::new();
569 let mut index = 0usize;
570
571 while index <= chars.len() {
572 let ch = chars.get(index).copied();
573
574 if let Some(value) = ch {
575 if value == '\\' && index + 1 < chars.len() && !options.windows {
576 current.push(value);
577 current.push(chars[index + 1]);
578 index += 2;
579 continue;
580 }
581
582 if is_separator(value, options) {
583 if current == "." || current == ".." {
584 return true;
585 }
586 current.clear();
587 index += 1;
588 continue;
589 }
590
591 current.push(value);
592 index += 1;
593 continue;
594 }
595
596 return current == "." || current == "..";
597 }
598
599 false
600}
601
602fn compile_bracket(inner: &str, options: &CompileOptions, segment_start: bool) -> String {
603 if options.windows && inner == "/" {
604 return slash_literal(options).to_string();
605 }
606
607 let chars: Vec<char> = inner.chars().collect();
608 let mut body = String::new();
609 let mut index = 0usize;
610 let mut posix_converted = false;
611
612 while index < chars.len() {
613 let ch = chars[index];
614
615 if options.posix && index == 0 && ch == '!' {
616 body.push('^');
617 index += 1;
618 continue;
619 }
620
621 if ch == '\\' {
622 body.push('\\');
623 if let Some(next) = chars.get(index + 1) {
624 body.push(*next);
625 index += 2;
626 } else {
627 index += 1;
628 }
629 continue;
630 }
631
632 if options.posix && ch == '[' && chars.get(index + 1) == Some(&':') {
633 let mut end = index + 2;
634 let mut converted = false;
635 while end + 1 < chars.len() {
636 if chars[end] == ':' && chars[end + 1] == ']' {
637 let name = chars[index + 2..end].iter().collect::<String>();
638 if let Some(source) = posix_class_source(&name) {
639 body.push_str(source);
640 posix_converted = true;
641 index = end + 2;
642 converted = true;
643 }
644 break;
645 }
646 end += 1;
647 }
648
649 if converted {
650 continue;
651 }
652 }
653
654 if ch == '[' && chars.get(index + 1) != Some(&':') {
655 body.push_str("\\[");
656 index += 1;
657 continue;
658 }
659
660 if ch == '-' && index + 1 == chars.len() {
661 body.push_str("\\-");
662 index += 1;
663 continue;
664 }
665
666 if ch == ']' && body.is_empty() {
667 body.push_str("\\]");
668 index += 1;
669 continue;
670 }
671
672 body.push(ch);
673 index += 1;
674 }
675
676 let class_body = if body.starts_with('^') && !body.contains('/') && !posix_converted {
677 format!("{body}/")
678 } else {
679 body
680 };
681 let class_output = format!("[{class_body}]");
682 let prefix = if posix_converted && segment_start {
683 one_char()
684 } else {
685 ""
686 };
687
688 if posix_converted || has_regex_chars(inner) {
689 return format!("{prefix}{class_output}");
690 }
691
692 let literal = escape_literal(&format!("[{inner}]"));
693 format!("{prefix}(?:{literal}|{class_output})")
694}
695
696fn compile_extglob(operator: char, inner: &str, options: &CompileOptions) -> Option<String> {
697 let mut inner_options = options.clone();
698 inner_options.literal_plus_quantifier = true;
699 let alternatives = split_top_level(inner, '|')
700 .into_iter()
701 .map(|part| compile_body(&part, &inner_options))
702 .collect::<Option<Vec<_>>>()?;
703 let body = alternatives.join("|");
704
705 if body.is_empty() {
706 return Some(String::new());
707 }
708
709 let output = match operator {
710 '@' => format!("(?:{body})"),
711 '*' => format!("(?:{body})*"),
712 '+' => format!("(?:{body})+"),
713 '?' => format!("(?:{body})?"),
714 _ => return None,
715 };
716
717 Some(output)
718}
719
720fn compile_group(inner: &str, options: &CompileOptions) -> Option<String> {
721 if inner.starts_with('?') {
722 return Some(format!("({inner})"));
723 }
724
725 let mut inner_options = options.clone();
726 inner_options.literal_plus_quantifier = true;
727 let alternatives = split_top_level(inner, '|')
728 .into_iter()
729 .map(|part| compile_body(&part, &inner_options))
730 .collect::<Option<Vec<_>>>()?;
731
732 Some(format!("({})", alternatives.join("|")))
733}
734
735fn is_regex_escape(next: char) -> bool {
736 next.is_ascii_digit()
737 || matches!(
738 next,
739 'b' | 'B'
740 | 'd'
741 | 'D'
742 | 'f'
743 | 'n'
744 | 'p'
745 | 'P'
746 | 'r'
747 | 's'
748 | 'S'
749 | 't'
750 | 'u'
751 | 'v'
752 | 'w'
753 | 'W'
754 | 'x'
755 | 'k'
756 | '0'
757 )
758}
759
760fn is_escaped_literal_in_windows(next: char) -> bool {
761 matches!(
762 next,
763 '*' | '?'
764 | '+'
765 | '@'
766 | '!'
767 | '('
768 | ')'
769 | '['
770 | ']'
771 | '{'
772 | '}'
773 | '|'
774 | '.'
775 | ';'
776 | '"'
777 | '\''
778 )
779}
780
781fn compile_body_with_context(
782 input: &str,
783 options: &CompileOptions,
784 initial_segment_start: bool,
785) -> Option<String> {
786 let mut pattern = if initial_segment_start {
787 input.strip_prefix("./").unwrap_or(input)
788 } else {
789 input
790 };
791 let mut optional_trailing_slash = false;
792 let mut optional_descendants = false;
793
794 if let Some(stripped) = pattern.strip_suffix("{,/}") {
795 optional_trailing_slash = true;
796 pattern = stripped;
797 } else if let Some(stripped) = pattern.strip_suffix("{,/**}") {
798 optional_descendants = true;
799 pattern = stripped;
800 }
801
802 let mut output = String::with_capacity(pattern.len() * 2);
803 let chars: Vec<char> = pattern.chars().collect();
804 let mut index = 0;
805 let mut segment_start = initial_segment_start;
806 let mut last_was_wildcard = false;
807 let mut last_segment_token_kind = TokenKind::None;
808 let mut last_token_kind = TokenKind::None;
809
810 while index < chars.len() {
811 let ch = chars[index];
812
813 if ch == '\\' {
814 if let Some(next) = chars.get(index + 1).copied() {
815 let mut slash_run = 1usize;
816 while chars.get(index + slash_run) == Some(&'\\') {
817 slash_run += 1;
818 }
819
820 if slash_run > 3 {
821 let next_index = index + slash_run;
822 if let Some(collapsed_next) = chars.get(next_index).copied() {
823 if options.unescape {
824 if options.windows && collapsed_next.is_ascii_alphanumeric() {
825 output.push_str(&format!("(?:{}+)?", slash_literal(options)));
826 }
827 push_literal_char(&mut output, collapsed_next);
828 } else {
829 push_literal_char(&mut output, '\\');
830 push_literal_char(&mut output, collapsed_next);
831 }
832
833 segment_start = false;
834 last_was_wildcard = false;
835 last_token_kind = TokenKind::Literal;
836 index = next_index + 1;
837 continue;
838 }
839 }
840
841 if options.unescape && matches!(next, '{' | '}') {
842 output.push(next);
843 segment_start = false;
844 last_was_wildcard = false;
845 last_token_kind = TokenKind::Literal;
846 index += 2;
847 continue;
848 }
849
850 if options.windows && next == '/' {
851 output.push_str(slash_literal(options));
852 segment_start = true;
853 last_was_wildcard = false;
854 last_token_kind = TokenKind::None;
855 index += 2;
856 continue;
857 }
858
859 if options.unescape && next.is_ascii_alphanumeric() {
860 if options.windows {
861 output.push_str(&format!("(?:{}+)?", slash_literal(options)));
862 }
863 push_literal_char(&mut output, next);
864 segment_start = false;
865 last_was_wildcard = false;
866 last_token_kind = TokenKind::Literal;
867 index += 2;
868 continue;
869 }
870
871 if is_regex_escape(next) {
872 output.push('\\');
873 output.push(next);
874 segment_start = false;
875 last_was_wildcard = false;
876 last_token_kind = TokenKind::RegexEscape;
877 index += 2;
878 continue;
879 }
880
881 if options.windows && is_escaped_literal_in_windows(next) {
882 push_literal_char(&mut output, next);
883 segment_start = false;
884 last_was_wildcard = false;
885 last_token_kind = TokenKind::Literal;
886 index += 2;
887 continue;
888 }
889
890 if options.windows {
891 output.push_str("\\\\");
892 last_segment_token_kind = last_token_kind;
893 segment_start = false;
894 last_was_wildcard = false;
895 last_token_kind = TokenKind::Literal;
896 index += 1;
897 continue;
898 }
899
900 if next == '\\' && !options.windows {
901 output.push_str("\\\\");
902 segment_start = false;
903 last_was_wildcard = false;
904 last_token_kind = TokenKind::Literal;
905 index += 2;
906 continue;
907 }
908
909 let escaped = escape_regex_char(next);
910 if escaped.is_empty() {
911 output.push(next);
912 } else {
913 output.push_str(escaped);
914 }
915 segment_start = false;
916 last_was_wildcard = false;
917 last_token_kind = TokenKind::Literal;
918 index += 2;
919 continue;
920 }
921
922 output.push_str("\\\\");
923 segment_start = false;
924 last_was_wildcard = false;
925 last_token_kind = TokenKind::Literal;
926 index += 1;
927 continue;
928 }
929
930 if is_separator(ch, options) {
931 output.push_str(slash_literal(options));
932 last_segment_token_kind = last_token_kind;
933 segment_start = true;
934 last_was_wildcard = false;
935 last_token_kind = TokenKind::None;
936 index += 1;
937 continue;
938 }
939
940 if ch == '"' {
941 let mut next_index = index + 1;
942 let mut literal = String::new();
943
944 while next_index < chars.len() && chars[next_index] != '"' {
945 literal.push(chars[next_index]);
946 next_index += 1;
947 }
948
949 if next_index < chars.len() {
950 if options.keep_quotes {
951 output.push_str(r#"\""#);
952 output.push_str(&escape_literal(&literal));
953 output.push_str(r#"\""#);
954 } else {
955 output.push_str(&escape_literal(&literal));
956 }
957 segment_start = false;
958 last_was_wildcard = false;
959 last_token_kind = TokenKind::Literal;
960 index = next_index + 1;
961 continue;
962 }
963 }
964
965 if ch == '!' && index == 0 && !options.nonegate && chars.get(1) != Some(&'(') {
966 output.push('!');
967 last_was_wildcard = false;
968 segment_start = false;
969 last_token_kind = TokenKind::Literal;
970 index += 1;
971 continue;
972 }
973
974 if ch == '[' {
975 if options.nobracket {
976 output.push_str("\\[");
977 segment_start = false;
978 last_was_wildcard = false;
979 last_token_kind = TokenKind::Literal;
980 index += 1;
981 continue;
982 }
983
984 let Some((inner, next_index)) = collect_bracket(&chars, index) else {
985 let remaining = chars[index..].iter().collect::<String>();
986 if remaining.starts_with("[[:") {
987 output.push_str(&remaining);
988 segment_start = false;
989 last_was_wildcard = false;
990 last_token_kind = TokenKind::Bracket;
991 index = chars.len();
992 continue;
993 }
994 output.push_str("\\[");
995 segment_start = false;
996 last_was_wildcard = false;
997 last_token_kind = TokenKind::Literal;
998 index += 1;
999 continue;
1000 };
1001 output.push_str(&compile_bracket(&inner, options, segment_start));
1002 segment_start = false;
1003 last_was_wildcard = true;
1004 last_token_kind = TokenKind::Bracket;
1005 index = next_index;
1006 continue;
1007 }
1008
1009 if ch == '!'
1010 && chars.get(index + 1) == Some(&'(')
1011 && !options.noextglob
1012 && (chars.get(index + 2) != Some(&'?')
1013 || !matches!(chars.get(index + 3), Some('!' | '=' | '<' | ':')))
1014 {
1015 let (inner, next_index) = collect_enclosed(&chars, index + 1, '(', ')')?;
1016 let rest = chars[next_index..].iter().collect::<String>();
1017
1018 if rest == "/**" && !inner.contains('/') && !inner.contains('\\') {
1019 let compiled_inner = compile_body(&inner, options)?;
1020 output.push_str(&format!(
1021 "(?:(?!(?:{})){})(?:{}{}|$)",
1022 compiled_inner,
1023 star(options),
1024 slash_literal(options),
1025 extglob_slashy_body(options)
1026 ));
1027 segment_start = false;
1028 last_was_wildcard = false;
1029 last_token_kind = TokenKind::Group;
1030 index = chars.len();
1031 continue;
1032 }
1033
1034 let compiled_inner = if inner.ends_with("/**") {
1035 let prefix = &inner[..inner.len().saturating_sub(3)];
1036 let compiled_prefix = compile_body(prefix, options)?;
1037 format!(
1038 "{compiled_prefix}{}{body}",
1039 slash_literal(options),
1040 body = extglob_slashy_body(options)
1041 )
1042 } else if inner.len() == 1
1043 && matches!(inner.chars().next(), Some('.' | '+' | '?' | '@'))
1044 {
1045 escape_literal(&inner)
1046 } else {
1047 let mut inner_options = options.clone();
1048 inner_options.literal_plus_quantifier = true;
1049 split_top_level(&inner, '|')
1050 .into_iter()
1051 .map(|part| {
1052 compile_body(&part, &inner_options)
1053 .map(|value| sanitize_nested_negation(&value, !rest.is_empty()))
1054 })
1055 .collect::<Option<Vec<_>>>()?
1056 .join("|")
1057 };
1058 let slashy = (inner.contains('/') || inner.contains('\\')) && inner != "/";
1059 let body_output = if slashy {
1060 extglob_slashy_body(options)
1061 } else {
1062 star(options)
1063 };
1064 let lookahead = if rest.starts_with('.')
1065 && !matches!(rest.chars().nth(1), Some('!'))
1066 && inner.contains('*')
1067 && !slashy
1068 {
1069 format!("{}{}", compiled_inner, compile_body(&rest, options)?)
1070 } else if slashy || next_index == chars.len() {
1071 format!("(?:{compiled_inner})$")
1072 } else {
1073 compiled_inner.clone()
1074 };
1075
1076 if index == 0 {
1077 output.push_str("(?=.)");
1078 }
1079
1080 if slashy || next_index == chars.len() {
1081 output.push_str(&format!("(?:(?!(?:{}))){}", lookahead, body_output));
1082 } else {
1083 output.push_str(&format!("(?:(?!(?:{})){})", lookahead, body_output));
1084 }
1085 segment_start = false;
1086 last_was_wildcard = false;
1087 last_token_kind = TokenKind::Group;
1088 index = next_index;
1089 continue;
1090 }
1091
1092 if ch == '|' {
1093 output.push('|');
1094 segment_start = false;
1095 last_was_wildcard = false;
1096 last_token_kind = TokenKind::None;
1097 index += 1;
1098 continue;
1099 }
1100
1101 if ch == '{' {
1102 if options.nobrace {
1103 output.push_str(r"\{");
1104 segment_start = false;
1105 last_was_wildcard = false;
1106 last_token_kind = TokenKind::Literal;
1107 index += 1;
1108 continue;
1109 }
1110
1111 let (inner, next_index) = collect_enclosed(&chars, index, '{', '}')?;
1112 let comma_parts = split_top_level(&inner, ',');
1113 let range = split_top_level_range(&inner);
1114
1115 if comma_parts.len() == 1 && range.is_none() {
1116 output.push_str(r"\{");
1117 output.push_str(&escape_literal(&inner));
1118 output.push_str(r"\}");
1119 segment_start = false;
1120 last_was_wildcard = false;
1121 last_token_kind = TokenKind::Literal;
1122 index = next_index;
1123 continue;
1124 }
1125
1126 if let Some((start, end)) = range {
1127 output.push_str(&compile_range(&start, &end)?);
1128 segment_start = false;
1129 last_was_wildcard = false;
1130 last_token_kind = TokenKind::Group;
1131 index = next_index;
1132 continue;
1133 }
1134
1135 let alternatives = comma_parts
1136 .into_iter()
1137 .map(|part| compile_body_with_context(&part, options, segment_start))
1138 .collect::<Option<Vec<_>>>()?;
1139 output.push_str(&format!("(?:{})", alternatives.join("|")));
1140 segment_start = false;
1141 last_was_wildcard = false;
1142 last_token_kind = TokenKind::Group;
1143 index = next_index;
1144 continue;
1145 }
1146
1147 if ch == '@' && chars.get(index + 1) == Some(&'(') && !options.noextglob {
1148 let (inner, next_index) = collect_enclosed(&chars, index + 1, '(', ')')?;
1149 output.push_str(&compile_extglob('@', &inner, options)?);
1150 segment_start = false;
1151 last_was_wildcard = false;
1152 last_token_kind = TokenKind::Group;
1153 index = next_index;
1154 continue;
1155 }
1156
1157 if ch == '(' {
1158 let Some((inner, next_index)) = collect_enclosed(&chars, index, '(', ')') else {
1159 if options.strict_brackets {
1160 return None;
1161 }
1162
1163 output.push_str(r"\(");
1164 segment_start = false;
1165 last_was_wildcard = false;
1166 last_token_kind = TokenKind::Literal;
1167 index += 1;
1168 continue;
1169 };
1170 output.push_str(&compile_group(&inner, options)?);
1171 segment_start = false;
1172 last_was_wildcard = false;
1173 last_token_kind = TokenKind::Group;
1174 index = next_index;
1175 continue;
1176 }
1177
1178 if matches!(ch, ')' | '}') {
1179 if options.strict_brackets {
1180 return None;
1181 }
1182
1183 let escaped = escape_regex_char(ch);
1184 output.push_str(escaped);
1185 segment_start = false;
1186 last_was_wildcard = false;
1187 last_token_kind = TokenKind::Literal;
1188 index += 1;
1189 continue;
1190 }
1191
1192 if ch == '?' {
1193 if chars.get(index + 1) == Some(&'(')
1194 && chars.get(index + 2) != Some(&'?')
1195 && !options.noextglob
1196 {
1197 let (inner, next_index) = collect_enclosed(&chars, index + 1, '(', ')')?;
1198 output.push_str(&compile_extglob('?', &inner, options)?);
1199 segment_start = false;
1200 last_was_wildcard = false;
1201 last_token_kind = TokenKind::Group;
1202 index = next_index;
1203 continue;
1204 }
1205
1206 if matches!(
1207 last_token_kind,
1208 TokenKind::Bracket | TokenKind::Group | TokenKind::RegexEscape
1209 ) {
1210 output.push('?');
1211 segment_start = false;
1212 last_was_wildcard = false;
1213 last_token_kind = TokenKind::None;
1214 index += 1;
1215 continue;
1216 }
1217
1218 if segment_start && !options.dot {
1219 output.push_str(segment_leader(options));
1220 output.push_str(qmark_no_dot(options));
1221 } else {
1222 output.push_str(qmark(options));
1223 }
1224 segment_start = false;
1225 last_was_wildcard = false;
1226 last_token_kind = TokenKind::Wildcard;
1227 index += 1;
1228 continue;
1229 }
1230
1231 if ch == '+' {
1232 if chars.get(index + 1) == Some(&'(')
1233 && chars.get(index + 2) != Some(&'?')
1234 && !options.noextglob
1235 {
1236 let (inner, next_index) = collect_enclosed(&chars, index + 1, '(', ')')?;
1237 output.push_str(&compile_extglob('+', &inner, options)?);
1238 segment_start = false;
1239 last_was_wildcard = false;
1240 last_token_kind = TokenKind::Group;
1241 index = next_index;
1242 continue;
1243 }
1244
1245 if matches!(
1246 last_token_kind,
1247 TokenKind::Bracket | TokenKind::Group | TokenKind::RegexEscape
1248 ) || ((options.regex || options.literal_plus_quantifier)
1249 && matches!(last_token_kind, TokenKind::Literal))
1250 {
1251 output.push('+');
1252 segment_start = false;
1253 last_was_wildcard = false;
1254 last_token_kind = TokenKind::None;
1255 } else {
1256 output.push_str(r"\+");
1257 segment_start = false;
1258 last_was_wildcard = false;
1259 last_token_kind = TokenKind::Literal;
1260 }
1261 index += 1;
1262 continue;
1263 }
1264
1265 if ch == '*' {
1266 if chars.get(index + 1) == Some(&'(')
1267 && chars.get(index + 2) != Some(&'?')
1268 && !options.noextglob
1269 {
1270 let (inner, next_index) = collect_enclosed(&chars, index + 1, '(', ')')?;
1271 output.push_str(&compile_extglob('*', &inner, options)?);
1272 segment_start = false;
1273 last_was_wildcard = false;
1274 last_token_kind = TokenKind::Group;
1275 index = next_index;
1276 continue;
1277 }
1278
1279 if options.regex
1280 && matches!(
1281 last_token_kind,
1282 TokenKind::Bracket | TokenKind::Group | TokenKind::RegexEscape
1283 )
1284 {
1285 output.push('*');
1286 segment_start = false;
1287 last_was_wildcard = false;
1288 last_token_kind = TokenKind::None;
1289 index += 1;
1290 continue;
1291 }
1292
1293 let mut stars = 1;
1294 while index + stars < chars.len() && chars[index + stars] == '*' {
1295 stars += 1;
1296 }
1297
1298 if stars >= 2
1299 && chars.get(index + 1) == Some(&'*')
1300 && chars.get(index + 2) == Some(&'(')
1301 {
1302 stars = 1;
1303 }
1304
1305 let prev_is_sep = index == 0 || is_separator(chars[index - 1], options);
1306 let next = chars.get(index + stars).copied();
1307 let next_is_sep = match next {
1308 None => true,
1309 Some(value) => is_separator(value, options),
1310 };
1311 let next_is_groupish = match next {
1312 Some('{') | Some('(') => true,
1313 Some('@') => chars.get(index + stars + 1) == Some(&'(') && !options.noextglob,
1314 _ => false,
1315 };
1316
1317 if stars >= 2 && prev_is_sep && next_is_sep && !options.noglobstar {
1318 let globstar = globstar_segment(options);
1319 if next.is_some() {
1320 let prefix = if index == 0 {
1321 format!("(?:{}|)", slash_literal(options))
1322 } else {
1323 String::new()
1324 };
1325 output.push_str(&format!(
1326 "{}(?:{}{})*",
1327 prefix,
1328 globstar,
1329 slash_literal(options)
1330 ));
1331 last_segment_token_kind = TokenKind::Wildcard;
1332 segment_start = true;
1333 last_was_wildcard = true;
1334 last_token_kind = TokenKind::Wildcard;
1335 index += stars + 1;
1336 } else {
1337 if prev_is_sep
1338 && !matches!(last_segment_token_kind, TokenKind::Wildcard)
1339 && !output.is_empty()
1340 {
1341 let slash = slash_literal(options);
1342 if options.bash {
1343 let new_len = output.len().saturating_sub(slash.len());
1344 output.truncate(new_len);
1345 if options.strict_slashes {
1346 output.push_str(&format!(
1347 "{}{}",
1348 slash,
1349 extglob_slashy_body(options)
1350 ));
1351 } else {
1352 output.push_str(&format!(
1353 "(?:|(?:{}{}))",
1354 slash,
1355 extglob_slashy_body(options)
1356 ));
1357 }
1358 } else if options.strict_slashes {
1359 output
1360 .push_str(&format!("(?:{}(?:{}{})*)?", globstar, slash, globstar));
1361 } else {
1362 let new_len = output.len().saturating_sub(slash.len());
1363 output.truncate(new_len);
1364 output.push_str(&format!(
1365 "(?:{slash}+(?:{globstar}(?:{slash}+{globstar})*)?)?",
1366 ));
1367 }
1368 } else {
1369 let slash = slash_literal(options);
1370 let leading = if index == 0 {
1371 format!("(?:{slash}+)?")
1372 } else {
1373 String::new()
1374 };
1375 output.push_str(&format!(
1376 "{leading}(?:{globstar}(?:{slash}+{globstar})*)?",
1377 ));
1378 }
1379 segment_start = false;
1380 last_was_wildcard = true;
1381 last_token_kind = TokenKind::Wildcard;
1382 index += stars;
1383 }
1384 continue;
1385 }
1386
1387 if stars >= 2 && prev_is_sep && next_is_groupish && !options.noglobstar {
1388 let slash = slash_literal(options);
1389 let globstar = globstar_segment(options);
1390 if index == 0 {
1391 output.push_str(&format!(
1392 "(?:|{}|{}(?:{}{})*)",
1393 slash, globstar, slash, globstar
1394 ));
1395 } else {
1396 output.push_str(&format!("(?:{}(?:{}{})*)?", globstar, slash, globstar));
1397 }
1398 last_segment_token_kind = TokenKind::Wildcard;
1399 segment_start = true;
1400 last_was_wildcard = true;
1401 last_token_kind = TokenKind::Wildcard;
1402 index += stars;
1403 continue;
1404 }
1405
1406 if segment_start {
1407 output.push_str(segment_leader(options));
1408 if !options.bash {
1409 output.push_str(one_char());
1410 }
1411 }
1412 output.push_str(&star(options));
1413 segment_start = false;
1414 last_was_wildcard = true;
1415 last_token_kind = TokenKind::Wildcard;
1416 index += stars;
1417 continue;
1418 }
1419
1420 let escaped = escape_regex_char(ch);
1421 if escaped.is_empty() {
1422 output.push(ch);
1423 } else {
1424 output.push_str(escaped);
1425 }
1426
1427 segment_start = false;
1428 last_was_wildcard = false;
1429 last_token_kind = TokenKind::Literal;
1430 index += 1;
1431 }
1432
1433 if last_was_wildcard && !options.strict_slashes && !pattern.is_empty() {
1434 output.push_str(&format!("(?:{}+)?", slash_literal(options)));
1435 }
1436
1437 if optional_trailing_slash {
1438 output.push_str(&format!("(?:|{})", slash_literal(options)));
1439 }
1440
1441 if optional_descendants {
1442 if options.bash {
1443 output.push_str(&format!(
1444 "(?:|(?:{}{}))",
1445 slash_literal(options),
1446 extglob_slashy_body(options)
1447 ));
1448 } else {
1449 let globstar = globstar_segment(options);
1450 output.push_str(&format!(
1451 "(?:|(?:{}(?:{}(?:{}{})*)?)?)",
1452 slash_literal(options),
1453 globstar,
1454 slash_literal(options),
1455 globstar
1456 ));
1457 }
1458 }
1459
1460 Some(output)
1461}
1462
1463fn compile_body(input: &str, options: &CompileOptions) -> Option<String> {
1464 compile_body_with_context(input, options, true)
1465}
1466
1467fn push_parse_token(tokens: &mut Vec<ParseToken>, token: ParseToken) {
1468 if let Some(prev) = tokens.last_mut() {
1469 if prev.kind == "text" && token.kind == "text" {
1470 let merged_output = format!(
1471 "{}{}",
1472 prev.output.clone().unwrap_or_else(|| prev.value.clone()),
1473 token.value
1474 );
1475 prev.output = Some(merged_output);
1476 prev.value.push_str(&token.value);
1477 return;
1478 }
1479 }
1480
1481 tokens.push(token);
1482}
1483
1484fn parse_tokens(input: &str, options: &CompileOptions) -> Vec<ParseToken> {
1485 let chars = input.chars().collect::<Vec<_>>();
1486 let mut tokens = vec![ParseToken {
1487 kind: "bos".to_string(),
1488 value: String::new(),
1489 output: Some(String::new()),
1490 }];
1491 let mut braces = 0usize;
1492 let mut parens = 0usize;
1493 let mut index = 0usize;
1494
1495 while index < chars.len() {
1496 let ch = chars[index];
1497 let next = chars.get(index + 1).copied();
1498 let prev_kind = tokens
1499 .last()
1500 .map(|token| token.kind.as_str())
1501 .unwrap_or("bos");
1502
1503 match ch {
1504 '{' if !options.nobrace => {
1505 braces += 1;
1506 push_parse_token(
1507 &mut tokens,
1508 ParseToken {
1509 kind: "brace".to_string(),
1510 value: "{".to_string(),
1511 output: Some("(".to_string()),
1512 },
1513 );
1514 }
1515 '}' if !options.nobrace && braces > 0 => {
1516 braces -= 1;
1517 push_parse_token(
1518 &mut tokens,
1519 ParseToken {
1520 kind: "brace".to_string(),
1521 value: "}".to_string(),
1522 output: Some(")".to_string()),
1523 },
1524 );
1525 }
1526 ',' => {
1527 let output = if !options.nobrace && braces > 0 {
1528 "|".to_string()
1529 } else {
1530 ",".to_string()
1531 };
1532 push_parse_token(
1533 &mut tokens,
1534 ParseToken {
1535 kind: "comma".to_string(),
1536 value: ",".to_string(),
1537 output: Some(output),
1538 },
1539 );
1540 }
1541 '(' => {
1542 parens += 1;
1543 push_parse_token(
1544 &mut tokens,
1545 ParseToken {
1546 kind: "paren".to_string(),
1547 value: "(".to_string(),
1548 output: None,
1549 },
1550 );
1551 }
1552 ')' => {
1553 let output = if parens > 0 { ")" } else { r"\)" }.to_string();
1554 parens = parens.saturating_sub(1);
1555 push_parse_token(
1556 &mut tokens,
1557 ParseToken {
1558 kind: "paren".to_string(),
1559 value: ")".to_string(),
1560 output: Some(output),
1561 },
1562 );
1563 }
1564 '.' => {
1565 let token = if braces + parens == 0 && prev_kind != "bos" && prev_kind != "slash" {
1566 ParseToken {
1567 kind: "text".to_string(),
1568 value: ".".to_string(),
1569 output: Some(r"\.".to_string()),
1570 }
1571 } else {
1572 ParseToken {
1573 kind: "dot".to_string(),
1574 value: ".".to_string(),
1575 output: Some(r"\.".to_string()),
1576 }
1577 };
1578 push_parse_token(&mut tokens, token);
1579 }
1580 '*' => {
1581 push_parse_token(
1582 &mut tokens,
1583 ParseToken {
1584 kind: "star".to_string(),
1585 value: "*".to_string(),
1586 output: Some(star(options)),
1587 },
1588 );
1589 }
1590 '?' => {
1591 let output = if !options.dot && matches!(prev_kind, "slash" | "bos") {
1592 qmark_no_dot(options).to_string()
1593 } else {
1594 qmark(options).to_string()
1595 };
1596 push_parse_token(
1597 &mut tokens,
1598 ParseToken {
1599 kind: "qmark".to_string(),
1600 value: "?".to_string(),
1601 output: Some(output),
1602 },
1603 );
1604 }
1605 '/' => {
1606 push_parse_token(
1607 &mut tokens,
1608 ParseToken {
1609 kind: "slash".to_string(),
1610 value: "/".to_string(),
1611 output: Some(slash_literal(options).to_string()),
1612 },
1613 );
1614 }
1615 '+' => {
1616 push_parse_token(
1617 &mut tokens,
1618 ParseToken {
1619 kind: "plus".to_string(),
1620 value: "+".to_string(),
1621 output: Some(r"\+".to_string()),
1622 },
1623 );
1624 }
1625 '|' => {
1626 push_parse_token(
1627 &mut tokens,
1628 ParseToken {
1629 kind: "text".to_string(),
1630 value: "|".to_string(),
1631 output: None,
1632 },
1633 );
1634 }
1635 '\\' => {
1636 let mut value = String::from("\\");
1637 if let Some(next_char) = next {
1638 value.push(next_char);
1639 index += 1;
1640 }
1641
1642 push_parse_token(
1643 &mut tokens,
1644 ParseToken {
1645 kind: "text".to_string(),
1646 value,
1647 output: None,
1648 },
1649 );
1650 }
1651 _ => {
1652 let mut value = ch.to_string();
1653 while index + 1 < chars.len()
1654 && !matches!(
1655 chars[index + 1],
1656 '{' | '}'
1657 | ','
1658 | '('
1659 | ')'
1660 | '.'
1661 | '*'
1662 | '?'
1663 | '/'
1664 | '+'
1665 | '|'
1666 | '\\'
1667 | '!'
1668 | '@'
1669 | '['
1670 | ']'
1671 )
1672 {
1673 index += 1;
1674 value.push(chars[index]);
1675 }
1676
1677 push_parse_token(
1678 &mut tokens,
1679 ParseToken {
1680 kind: "text".to_string(),
1681 value,
1682 output: None,
1683 },
1684 );
1685 }
1686 }
1687
1688 index += 1;
1689 }
1690
1691 if !options.strict_slashes
1692 && tokens
1693 .last()
1694 .is_some_and(|token| matches!(token.kind.as_str(), "star" | "bracket"))
1695 {
1696 push_parse_token(
1697 &mut tokens,
1698 ParseToken {
1699 kind: "maybe_slash".to_string(),
1700 value: String::new(),
1701 output: Some(format!("{}?", slash_literal(options))),
1702 },
1703 );
1704 }
1705
1706 tokens
1707}
1708
1709pub fn parse(input: &str, options: &CompileOptions) -> Option<ParseState> {
1710 let mut pattern = input;
1711 let mut negated = false;
1712
1713 if !options.nonegate {
1714 let chars: Vec<char> = input.chars().collect();
1715 let mut count = 0usize;
1716 while count < chars.len() && chars[count] == '!' {
1717 if chars.get(count + 1) == Some(&'(') && chars.get(count + 2) != Some(&'?') {
1718 break;
1719 }
1720 count += 1;
1721 }
1722
1723 if count > 0 {
1724 negated = count % 2 == 1;
1725 pattern = &input[count..];
1726 }
1727 }
1728
1729 let output = compile_body(pattern, options)?;
1730 Some(ParseState {
1731 input: input.to_string(),
1732 output,
1733 negated,
1734 fastpaths: false,
1735 tokens: Some(parse_tokens(pattern, options)),
1736 })
1737}
1738
1739pub fn make_re(
1740 input: &str,
1741 options: &CompileOptions,
1742 return_state: bool,
1743) -> Option<RegexDescriptor> {
1744 let state = parse(input, options)?;
1745 let prepend = if options.contains { "" } else { "^" };
1746 let append = if options.contains { "" } else { "$" };
1747 let flags = if options.flags.is_empty() {
1748 if options.nocase {
1749 "i".to_string()
1750 } else {
1751 String::new()
1752 }
1753 } else {
1754 options.flags.clone()
1755 };
1756 let output = state.output.clone();
1757 let guard = if contains_magic(input)
1758 && (input.contains('/') || input.contains('\\'))
1759 && !has_explicit_dot_segment(input, options)
1760 {
1761 dot_segment_guard(options)
1762 } else {
1763 ""
1764 };
1765 let mut source = format!(
1766 "{prepend}{guard}(?:{}){append}",
1767 regex_output_for_engine(&output)
1768 );
1769 if state.negated {
1770 source = format!("^(?!{source}).*$");
1771 }
1772
1773 Some(RegexDescriptor {
1774 source,
1775 flags,
1776 output,
1777 state: return_state.then_some(state),
1778 })
1779}