1use crate::error::FormatParseError;
4use crate::types::definitions::{FieldSpec, FieldType};
5use regex;
6use std::collections::HashMap;
7
8pub const MAX_NESTED_FORMAT_DEPTH: usize = 10;
10
11const MAX_BRACE_DEPTH_IN_FORMAT_SPEC: i32 = 10;
13
14pub type ParsedPatternParts = (
18 String,
19 String,
20 Vec<FieldSpec>,
21 Vec<Option<String>>,
22 Vec<Option<String>>,
23 HashMap<String, String>,
24 bool,
25);
26
27fn literal_delimits_empty_field(s: &str) -> bool {
29 !s.trim().is_empty()
30}
31
32fn collect_balanced_format_spec(
35 chars: &mut std::iter::Peekable<std::str::Chars>,
36) -> Result<String, FormatParseError> {
37 let mut out = String::new();
38 let mut depth = 0i32;
39 loop {
40 let Some(&ch) = chars.peek() else {
41 return Err(FormatParseError::PatternError(
42 "Unclosed '{' in pattern: expected '}' to close the field".to_string(),
43 ));
44 };
45 if ch == '}' && depth == 0 {
46 break;
47 }
48 let c = chars
49 .next()
50 .expect("peek matched a char so next() must succeed");
51 match c {
52 '{' => {
53 if chars.peek() == Some(&'{') {
54 chars.next();
55 out.push('{');
56 out.push('{');
57 } else {
58 depth += 1;
59 if depth > MAX_BRACE_DEPTH_IN_FORMAT_SPEC {
60 return Err(FormatParseError::PatternError(
61 "Format specification has too many nested '{' (max 10)".to_string(),
62 ));
63 }
64 out.push('{');
65 }
66 }
67 '}' => {
71 depth -= 1;
72 if depth < 0 {
73 return Err(FormatParseError::PatternError(
74 "Unexpected '}' in format specification".to_string(),
75 ));
76 }
77 out.push('}');
78 }
79 _ => out.push(c),
80 }
81 }
82 Ok(out)
83}
84
85fn brace_balance_valid_for_nested_candidate(s: &str) -> bool {
86 let mut depth = 0i32;
87 let mut it = s.chars().peekable();
88 while let Some(c) = it.next() {
89 match c {
90 '{' => {
91 if it.peek() == Some(&'{') {
92 it.next();
93 continue;
94 }
95 depth += 1;
96 }
97 '}' => {
98 depth -= 1;
99 if depth < 0 {
100 return false;
101 }
102 }
103 _ => {}
104 }
105 }
106 depth == 0
107}
108
109fn is_nested_format_spec_candidate(trimmed: &str) -> bool {
112 if trimmed.len() < 2 {
113 return false;
114 }
115 if !trimmed.starts_with('{') || trimmed.starts_with("{{") {
116 return false;
117 }
118 if !trimmed.ends_with('}') {
119 return false;
120 }
121 brace_balance_valid_for_nested_candidate(trimmed)
122}
123
124fn strip_regex_anchors(anchored: &str) -> String {
126 let s = anchored.strip_prefix('^').unwrap_or(anchored);
127 let s = s.strip_suffix('$').unwrap_or(s);
128 s.to_string()
129}
130
131fn has_trailing_literal_before_next_field(mut chars: std::iter::Peekable<std::str::Chars>) -> bool {
136 while chars.peek().is_some_and(|c| c.is_whitespace()) {
137 chars.next();
138 }
139 if chars.next() != Some('}') {
140 return false;
141 }
142 while chars.peek().is_some_and(|c| c.is_whitespace()) {
143 chars.next();
144 }
145 let mut literal = String::new();
146 loop {
147 match chars.next() {
148 None => return literal_delimits_empty_field(&literal),
149 Some('{') => {
150 if chars.peek() == Some(&'{') {
151 chars.next();
152 literal.push('{');
153 } else {
154 return literal_delimits_empty_field(&literal);
155 }
156 }
157 Some('}') => {
158 if chars.peek() == Some(&'}') {
159 chars.next();
160 literal.push('}');
161 } else {
162 literal.push('}');
163 }
164 }
165 Some(c) => literal.push(c),
166 }
167 }
168}
169
170pub fn parse_pattern(
174 pattern: &str,
175 custom_patterns: &HashMap<String, String>,
176 allow_empty_delimited_default_string: bool,
177 nesting_depth: usize,
178) -> Result<ParsedPatternParts, FormatParseError> {
179 let estimated_fields = pattern.matches('{').count();
181 let mut regex_parts = Vec::with_capacity(estimated_fields * 2);
182 let mut field_specs = Vec::with_capacity(estimated_fields);
183 let mut field_names = Vec::with_capacity(estimated_fields); let mut normalized_names = Vec::with_capacity(estimated_fields); let mut name_mapping = HashMap::with_capacity(estimated_fields); let mut field_name_specs = HashMap::with_capacity(estimated_fields); let mut field_count: usize = 0;
188 let mut chars: std::iter::Peekable<std::str::Chars> = pattern.chars().peekable();
189 let mut literal = String::new();
190 let mut allows_empty_default_string_match = true;
191
192 while let Some(ch) = chars.next() {
193 match ch {
194 '{' => {
195 if chars.peek() == Some(&'{') {
197 chars.next();
198 literal.push('{');
199 continue;
200 }
201
202 let had_leading_literal = !literal.trim().is_empty();
203
204 if !literal.is_empty() {
206 allows_empty_default_string_match = false;
207 let escaped = if literal.trim_end() != literal {
210 let trimmed = literal.trim_end();
213 let mut escaped_str = String::with_capacity(trimmed.len() + 4);
214 escaped_str.push_str(®ex::escape(trimmed));
215 escaped_str.push_str("\\s+");
216 escaped_str
217 } else {
218 regex::escape(&literal)
219 };
220 regex_parts.push(escaped);
221 literal.clear();
222 }
223
224 let (mut spec, name) = parse_field(&mut chars, nesting_depth)?;
226
227 if matches!(spec.field_type, FieldType::Nested) {
228 if nesting_depth >= MAX_NESTED_FORMAT_DEPTH {
229 return Err(FormatParseError::PatternError(
230 "Nested format patterns exceed max depth (10)".to_string(),
231 ));
232 }
233 let inner = spec.nested_subpattern.as_ref().ok_or_else(|| {
234 FormatParseError::PatternError(
235 "Internal error: nested field missing subpattern".to_string(),
236 )
237 })?;
238 let (inner_anchored, _, _, _, _, _, _) = parse_pattern(
239 inner,
240 custom_patterns,
241 allow_empty_delimited_default_string,
242 nesting_depth + 1,
243 )?;
244 spec.nested_regex_body = Some(strip_regex_anchors(&inner_anchored));
245 }
246
247 if !spec.is_default_unconstrained_string() {
248 allows_empty_default_string_match = false;
249 }
250
251 let has_trailing_literal = has_trailing_literal_before_next_field(chars.clone());
252
253 let mut peek_chars = chars.clone();
256 let next_field_is_greedy = loop {
257 let mut found_closing = false;
259 while let Some(&ch) = peek_chars.peek() {
260 if ch.is_whitespace() {
261 peek_chars.next();
262 } else if ch == '}' {
263 peek_chars.next(); found_closing = true;
265 break;
266 } else {
267 break;
268 }
269 }
270 if !found_closing {
271 break None; }
273 while let Some(&ch) = peek_chars.peek() {
275 if ch.is_whitespace() {
276 peek_chars.next();
277 } else {
278 break;
279 }
280 }
281 if peek_chars.peek() == Some(&'{') {
283 peek_chars.next();
284 if peek_chars.peek() == Some(&'{') {
286 peek_chars.next();
287 continue; }
289 if peek_chars.peek() == Some(&'}') {
291 break Some(false);
293 } else {
294 let mut field_chars = peek_chars.clone();
296 let mut has_precision = false;
297 while let Some(&ch) = field_chars.peek() {
298 if ch == '}' {
299 break;
300 }
301 if ch == ':' {
302 field_chars.next();
303 while let Some(&next_ch) = field_chars.peek() {
305 if next_ch == '}' {
306 break;
307 }
308 if next_ch == '.' {
309 has_precision = true;
310 break;
311 }
312 field_chars.next();
313 }
314 break;
315 }
316 field_chars.next();
317 }
318 break Some(has_precision);
321 }
322 } else {
323 break None;
325 }
326 };
327
328 let allow_empty_delimited = allow_empty_delimited_default_string
329 && spec.is_default_unconstrained_string()
330 && (had_leading_literal || has_trailing_literal);
331 let pattern = spec.to_regex_pattern(
332 custom_patterns,
333 next_field_is_greedy,
334 allow_empty_delimited,
335 );
336 let la_raw = spec.regex_lookahead.as_deref().unwrap_or("");
337 let (lb_prefix, body, la_emit) =
338 crate::rewrite_field_fragments_for_engine_anchor(&pattern, la_raw);
339
340 field_count += 1;
341 if field_count > crate::parser::MAX_FIELDS {
342 return Err(FormatParseError::PatternError(format!(
343 "Pattern has more than {} fields",
344 crate::parser::MAX_FIELDS
345 )));
346 }
347
348 if let Some(ref original_name) = name {
350 if let Some(existing_spec) = field_name_specs.get(original_name) {
351 if !field_specs_match_for_repeat(existing_spec, &spec) {
352 return Err(FormatParseError::RepeatedNameError(original_name.clone()));
353 }
354 } else {
355 field_name_specs.insert(original_name.clone(), spec.clone());
356 }
357 }
358
359 let group_pattern = if matches!(spec.field_type, FieldType::BracedContent) {
363 let Some(ref original_name) = name else {
364 return Err(FormatParseError::PatternError(
365 "The :brace format requires a named field (e.g. {content:brace})"
366 .to_string(),
367 ));
368 };
369 if original_name.chars().all(|c| c.is_ascii_digit()) {
370 return Err(FormatParseError::PatternError(
371 "The :brace format cannot be used with numbered fields".to_string(),
372 ));
373 }
374 let normalized =
375 normalize_field_name(original_name, &mut name_mapping, &normalized_names);
376 format!("\\{{(?P<{}>.*?)\\}}", normalized)
377 } else if let Some(ref original_name) = name {
378 let is_numeric = original_name.chars().all(|c| c.is_ascii_digit());
380
381 if is_numeric {
382 format!("{}{}({}){}", lb_prefix, "", body, la_emit)
384 } else {
385 let normalized = normalize_field_name(
387 original_name,
388 &mut name_mapping,
389 &normalized_names,
390 );
391 format!("{}{}(?P<{}>{}){}", lb_prefix, "", normalized, body, la_emit)
392 }
393 } else {
394 format!("{}{}({}){}", lb_prefix, "", body, la_emit)
395 };
396
397 regex_parts.push(group_pattern);
398
399 if let Some(ref original_name) = name {
401 let is_numeric = original_name.chars().all(|c| c.is_ascii_digit());
403
404 if is_numeric {
405 field_names.push(None); normalized_names.push(None);
407 } else {
408 let normalized = normalize_field_name(
409 original_name,
410 &mut name_mapping,
411 &normalized_names,
412 );
413 field_names.push(Some(original_name.clone())); normalized_names.push(Some(normalized.clone())); name_mapping.insert(normalized, original_name.clone()); }
417 } else {
418 field_names.push(None);
419 normalized_names.push(None);
420 }
421 field_specs.push(spec);
422
423 if chars.next() != Some('}') {
425 return Err(FormatParseError::PatternError(
426 "Expected '}' after field specification".to_string(),
427 ));
428 }
429 }
430 '}' => {
431 if chars.peek() == Some(&'}') {
433 chars.next();
434 literal.push('}');
435 continue;
436 }
437 literal.push('}');
438 }
439 _ => {
440 literal.push(ch);
441 }
442 }
443 }
444
445 if !literal.is_empty() {
447 allows_empty_default_string_match = false;
448 let escaped = if literal.trim_end() != literal {
450 let trimmed = literal.trim_end();
453 format!("{}\\s*", regex::escape(trimmed))
454 } else {
455 regex::escape(&literal)
456 };
457 regex_parts.push(escaped);
458 }
459
460 let regex_str = regex_parts.join("");
461 let regex_str_with_anchors = format!("^{}$", regex_str);
462 Ok((
463 regex_str_with_anchors,
464 regex_str,
465 field_specs,
466 field_names,
467 normalized_names,
468 name_mapping,
469 allows_empty_default_string_match,
470 ))
471}
472
473pub fn normalize_field_name(
480 name: &str,
481 _name_mapping: &mut HashMap<String, String>,
482 existing_normalized: &[Option<String>],
483) -> String {
484 let mut base_normalized = String::with_capacity(name.len());
485 for c in name.chars() {
486 match c {
487 '-' | '.' | '[' => base_normalized.push('_'),
488 ']' => {}
489 _ => base_normalized.push(c),
490 }
491 }
492
493 let mut normalized = base_normalized.clone();
495
496 let underscore_pos = normalized.find('_');
498
499 let mut collision_count = 0;
501 while existing_normalized
502 .iter()
503 .any(|n| n.as_ref().map(|s| s == &normalized).unwrap_or(false))
504 {
505 collision_count += 1;
506 if let Some(pos) = underscore_pos {
509 let before = &base_normalized[..pos];
510 let after = &base_normalized[pos + 1..];
511 normalized = format!("{}{}{}", before, "_".repeat(1 + collision_count), after);
513 } else {
514 normalized = format!("{}{}", base_normalized, "_".repeat(collision_count));
516 }
517 }
518
519 normalized
520}
521
522pub fn validate_multiline_mvp(spec: &FieldSpec) -> Result<(), FormatParseError> {
527 if !matches!(
528 spec.field_type,
529 FieldType::Multiline | FieldType::IndentBlock
530 ) {
531 return Ok(());
532 }
533 if spec.sign.is_some() || spec.zero_pad {
534 return Err(FormatParseError::PatternError(
535 "Multiline types :ml and :blk do not support sign or zero-padding".to_string(),
536 ));
537 }
538 if spec.alignment == Some('=') {
539 return Err(FormatParseError::PatternError(
540 "Multiline types :ml and :blk do not support '=' alignment".to_string(),
541 ));
542 }
543 Ok(())
544}
545
546pub fn field_types_match(t1: &FieldType, t2: &FieldType) -> bool {
548 match (t1, t2) {
549 (FieldType::Custom(a), FieldType::Custom(b)) => a == b,
550 (a, b) => std::mem::discriminant(a) == std::mem::discriminant(b),
551 }
552}
553
554pub fn field_specs_match_for_repeat(a: &FieldSpec, b: &FieldSpec) -> bool {
556 if !field_types_match(&a.field_type, &b.field_type) {
557 return false;
558 }
559 match (&a.field_type, &b.field_type) {
560 (FieldType::DateTimeStrftime, FieldType::DateTimeStrftime) => {
561 true
563 }
564 (FieldType::Integer, FieldType::Integer) => a.original_type_char == b.original_type_char,
565 (FieldType::Nested, FieldType::Nested) => a.nested_subpattern == b.nested_subpattern,
566 _ => true,
567 }
568}
569
570pub fn parse_field_path(field_name: &str) -> Vec<String> {
572 let mut path = Vec::new();
573 let mut current = String::new();
574 let mut in_brackets = false;
575
576 for ch in field_name.chars() {
577 match ch {
578 '[' => {
579 if !current.is_empty() {
580 path.push(current.clone());
581 current.clear();
582 }
583 in_brackets = true;
584 }
585 ']' => {
586 if in_brackets {
587 if !current.is_empty() {
588 path.push(current.clone());
589 current.clear();
590 }
591 in_brackets = false;
592 } else {
593 current.push(ch);
594 }
595 }
596 _ => {
597 current.push(ch);
598 }
599 }
600 }
601
602 if !current.is_empty() {
603 path.push(current);
604 }
605
606 path
607}
608
609pub fn parse_field(
611 chars: &mut std::iter::Peekable<std::str::Chars>,
612 nesting_depth: usize,
613) -> Result<(FieldSpec, Option<String>), FormatParseError> {
614 let mut spec = FieldSpec::new();
615 let mut field_name = String::new();
616 let mut in_name = true;
617
618 let mut in_brackets = false;
620 while let Some(&ch) = chars.peek() {
621 match ch {
622 ':' => {
623 chars.next();
624 in_name = false;
625 break;
626 }
627 '!' => {
628 chars.next();
629 if chars.peek().is_some() {
631 chars.next();
632 }
633 in_name = false;
634 }
635 '}' => {
636 break;
637 }
638 '[' => {
639 in_brackets = true;
640 field_name.push(ch);
641 chars.next();
642 }
643 ']' => {
644 in_brackets = false;
645 field_name.push(ch);
646 chars.next();
647 }
648 '\'' | '"' => {
649 if in_brackets {
651 return Err(FormatParseError::NotImplementedError(
652 "Quoted keys in field names".to_string(),
653 ));
654 }
655 in_name = false;
657 break;
658 }
659 _ => {
660 if ch.is_alphanumeric() || ch == '_' || ch == '-' || ch == '.' {
662 field_name.push(ch);
663 chars.next();
664 } else {
665 in_name = false;
667 break;
668 }
669 }
670 }
671 }
672
673 if !in_name {
675 let format_spec = collect_balanced_format_spec(chars)?;
676 let trimmed = format_spec.trim();
677 if is_nested_format_spec_candidate(trimmed) {
678 if nesting_depth >= MAX_NESTED_FORMAT_DEPTH {
679 return Err(FormatParseError::PatternError(
680 "Nested format patterns exceed max depth (10)".to_string(),
681 ));
682 }
683 spec.field_type = FieldType::Nested;
684 spec.nested_subpattern = Some(trimmed.to_string());
685 } else {
686 parse_format_spec(&format_spec, &mut spec)?;
687 }
688 validate_multiline_mvp(&spec)?;
689 }
690
691 let name = if field_name.is_empty() {
692 None
693 } else {
694 crate::parser::validate_field_name(&field_name).map_err(FormatParseError::PatternError)?;
695 Some(field_name)
696 };
697
698 Ok((spec, name))
699}
700
701pub fn parse_format_spec(format_spec: &str, spec: &mut FieldSpec) -> Result<(), FormatParseError> {
703 let mut chars = format_spec.chars().peekable();
707
708 if let Some(&ch) = chars.peek() {
711 if ch == '<' || ch == '>' || ch == '^' || ch == '=' {
712 spec.alignment = Some(ch);
713 chars.next();
714 } else {
715 let mut peek_iter = chars.clone();
717 peek_iter.next(); if let Some(next_ch) = peek_iter.next() {
719 if next_ch == '<' || next_ch == '>' || next_ch == '^' || next_ch == '=' {
720 spec.fill = Some(ch);
721 chars.next(); spec.alignment = Some(next_ch);
723 chars.next(); }
725 }
726 }
727 }
728
729 if let Some(&ch) = chars.peek() {
731 if ch == '+' || ch == '-' || ch == ' ' {
732 spec.sign = Some(ch);
733 chars.next();
734 }
735 }
736
737 if chars.peek() == Some(&'#') {
739 chars.next();
740 }
741
742 if chars.peek() == Some(&'0') {
744 spec.zero_pad = true;
745 chars.next();
746 }
747
748 let mut width_str = String::new();
750 while let Some(&ch) = chars.peek() {
751 if ch.is_ascii_digit() {
752 width_str.push(ch);
753 chars.next();
754 } else {
755 break;
756 }
757 }
758 if !width_str.is_empty() {
759 if let Ok(w) = width_str.parse::<usize>() {
760 if w > crate::parser::MAX_WIDTH_PRECISION {
761 return Err(FormatParseError::PatternError(format!(
762 "Width {} exceeds maximum allowed {}",
763 w,
764 crate::parser::MAX_WIDTH_PRECISION
765 )));
766 }
767 spec.width = Some(w);
768 }
769 }
770
771 if chars.peek() == Some(&',') {
773 chars.next();
774 }
775
776 if chars.peek() == Some(&'.') {
778 chars.next();
779 let mut precision_str = String::new();
780 while let Some(&ch) = chars.peek() {
781 if ch.is_ascii_digit() {
782 precision_str.push(ch);
783 chars.next();
784 } else {
785 break;
786 }
787 }
788 if !precision_str.is_empty() {
789 if let Ok(p) = precision_str.parse::<usize>() {
790 if p > crate::parser::MAX_WIDTH_PRECISION {
791 return Err(FormatParseError::PatternError(format!(
792 "Precision {} exceeds maximum allowed {}",
793 p,
794 crate::parser::MAX_WIDTH_PRECISION
795 )));
796 }
797 spec.precision = Some(p);
798 }
799 }
800 }
801
802 let mut type_str = String::new();
804 for ch in chars {
805 type_str.push(ch);
806 }
807
808 if type_str == "%" {
809 spec.field_type = FieldType::Percentage;
810 return Ok(());
811 }
812 if type_str.starts_with('%') {
813 crate::reject_lookaround_in_strftime(&type_str).map_err(FormatParseError::PatternError)?;
814 spec.field_type = FieldType::DateTimeStrftime;
815 spec.strftime_format = Some(type_str.clone());
816 return Ok(());
817 }
818
819 let (type_base, lookaround_tail) = crate::split_type_base_and_lookaround_tail(&type_str);
820 if type_base.is_empty() && !lookaround_tail.is_empty() {
821 return Err(FormatParseError::PatternError(
822 "Type specification must precede lookaround assertions".to_string(),
823 ));
824 }
825
826 let type_name: String = type_base
828 .chars()
829 .filter(|c| c.is_alphanumeric() || *c == '_')
830 .collect();
831
832 spec.field_type = if type_name.is_empty() {
833 FieldType::String
834 } else if type_name == "ti" {
835 FieldType::DateTimeISO
836 } else if type_name == "te" {
837 FieldType::DateTimeRFC2822
838 } else if type_name == "tg" {
839 FieldType::DateTimeGlobal
840 } else if type_name == "ta" {
841 FieldType::DateTimeUS
842 } else if type_name == "tc" {
843 FieldType::DateTimeCtime
844 } else if type_name == "th" {
845 FieldType::DateTimeHTTP
846 } else if type_name == "tt" {
847 FieldType::DateTimeTime
848 } else if type_name == "ts" {
849 FieldType::DateTimeSystem
850 } else if type_name == "brace" {
851 FieldType::BracedContent
852 } else if type_name == "ml" {
853 FieldType::Multiline
854 } else if type_name == "blk" {
855 FieldType::IndentBlock
856 } else if type_name.len() > 1 {
857 FieldType::Custom(type_name)
858 } else {
859 let type_char = type_name.chars().next().unwrap();
860 spec.original_type_char = Some(type_char);
861 match type_char {
862 's' => FieldType::String,
863 'd' | 'i' => FieldType::Integer,
864 'b' | 'o' | 'x' | 'X' => FieldType::Integer,
865 'n' => FieldType::NumberWithThousands,
866 'f' | 'F' => FieldType::Float,
867 'e' | 'E' => FieldType::Scientific,
868 'g' | 'G' => FieldType::GeneralNumber,
869 'l' => FieldType::Letters,
870 'w' => FieldType::Word,
871 'W' => FieldType::NonLetters,
872 'S' => FieldType::NonWhitespace,
873 'D' => FieldType::NonDigits,
874 c => FieldType::Custom(c.to_string()),
875 }
876 };
877
878 if !lookaround_tail.is_empty() {
879 let (lb, la) = crate::parse_lookaround_tail(lookaround_tail)
880 .map_err(FormatParseError::PatternError)?;
881 match &spec.field_type {
882 FieldType::Integer | FieldType::Float => {
883 spec.regex_lookbehind = if lb.is_empty() { None } else { Some(lb) };
884 spec.regex_lookahead = if la.is_empty() { None } else { Some(la) };
885 }
886 _ => {
887 return Err(FormatParseError::PatternError("Lookaround assertions are only supported for integer and float format types (d, i, b, o, x, X, f, F)".to_string()));
888 }
889 }
890 }
891
892 Ok(())
893}
894
895#[cfg(test)]
896mod normalize_field_name_tests {
897 use super::normalize_field_name;
898 use std::collections::HashMap;
899
900 #[test]
901 fn dict_style_brackets_map_to_underscores() {
902 let mut m = HashMap::new();
903 let existing: Vec<Option<String>> = vec![];
904 assert_eq!(
905 normalize_field_name("hello[world]", &mut m, &existing),
906 "hello_world"
907 );
908 assert_eq!(
909 normalize_field_name("hello[foo][baz]", &mut m, &existing),
910 "hello_foo_baz"
911 );
912 }
913
914 #[test]
915 fn deep_nested_brackets_normalize() {
916 let mut m = HashMap::new();
917 assert_eq!(normalize_field_name("a[b[c[d]]]", &mut m, &[]), "a_b_c_d");
918 }
919}