Skip to main content

picomatch_rs/
compile.rs

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(&current.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}