1use crate::{
7 utils::{
8 errors::{encoding::validate_bom_handling, resource::check_input_size_limit},
9 CoreError,
10 },
11 Result, ScriptVersion,
12};
13use alloc::{format, string::String, string::ToString, vec::Vec};
14
15use super::{
16 ast::Section,
17 binary_data::{FontsParser, GraphicsParser},
18 errors::{IssueCategory, IssueSeverity, ParseError, ParseIssue},
19 script::Script,
20 sections::{EventsParser, ScriptInfoParser, StylesParser},
21};
22
23#[cfg(feature = "plugins")]
24use crate::plugin::{ExtensionRegistry, SectionResult};
25
26pub(super) struct Parser<'a> {
28 source: &'a str,
30 position: usize,
32 line: usize,
34 version: ScriptVersion,
36 sections: Vec<Section<'a>>,
38 issues: Vec<ParseIssue>,
40 styles_format: Option<Vec<&'a str>>,
42 events_format: Option<Vec<&'a str>>,
44 #[cfg(feature = "plugins")]
46 registry: Option<&'a ExtensionRegistry>,
47}
48
49impl<'a> Parser<'a> {
50 pub const fn new(source: &'a str) -> Self {
52 Self {
53 source,
54 position: 0,
55 line: 1,
56 version: ScriptVersion::AssV4, sections: Vec::new(),
58 issues: Vec::new(),
59 styles_format: None,
60 events_format: None,
61 #[cfg(feature = "plugins")]
62 registry: None,
63 }
64 }
65
66 #[cfg(feature = "plugins")]
68 pub const fn new_with_registry(
69 source: &'a str,
70 registry: Option<&'a ExtensionRegistry>,
71 ) -> Self {
72 Self {
73 source,
74 position: 0,
75 line: 1,
76 version: ScriptVersion::AssV4, sections: Vec::new(),
78 issues: Vec::new(),
79 styles_format: None,
80 events_format: None,
81 registry,
82 }
83 }
84
85 pub fn parse(mut self) -> Script<'a> {
87 const MAX_INPUT_SIZE: usize = 50 * 1024 * 1024; if let Err(e) = check_input_size_limit(self.source.len(), MAX_INPUT_SIZE) {
90 self.issues.push(ParseIssue::new(
91 IssueSeverity::Error,
92 IssueCategory::Security,
93 format!("Input size limit exceeded: {e}"),
94 self.line,
95 ));
96 return Script::from_parts(
98 self.source,
99 self.version,
100 Vec::new(),
101 self.issues,
102 self.styles_format,
103 self.events_format,
104 );
105 }
106
107 if let Err(e) = validate_bom_handling(self.source.as_bytes()) {
109 self.issues.push(ParseIssue::new(
110 IssueSeverity::Warning,
111 IssueCategory::Format,
112 format!("BOM validation warning: {e}"),
113 self.line,
114 ));
115 }
116
117 if self.source.starts_with('\u{FEFF}') {
119 self.position = 3;
120 }
121
122 while self.position < self.source.len() {
123 self.skip_whitespace_and_comments();
124
125 if self.position >= self.source.len() {
126 break;
127 }
128
129 match self.parse_section() {
130 Ok(section) => self.sections.push(section),
131 Err(e) => {
132 let (severity, message) = if e.to_string().contains("Unknown section") {
133 (IssueSeverity::Warning, e.to_string())
134 } else {
135 (
136 IssueSeverity::Error,
137 format!("Failed to parse section: {e}"),
138 )
139 };
140
141 self.issues.push(ParseIssue::new(
142 severity,
143 IssueCategory::Structure,
144 message,
145 self.line,
146 ));
147
148 self.skip_to_next_section();
149 }
150 }
151 }
152
153 Script::from_parts(
154 self.source,
155 self.version,
156 self.sections,
157 self.issues,
158 self.styles_format,
159 self.events_format,
160 )
161 }
162
163 fn parse_section(&mut self) -> Result<Section<'a>> {
165 if !self.source[self.position..].starts_with('[') {
166 return Err(CoreError::from(ParseError::ExpectedSectionHeader {
167 line: self.line,
168 }));
169 }
170
171 let header_end = self.source[self.position..].find(']').ok_or_else(|| {
172 CoreError::from(ParseError::UnclosedSectionHeader { line: self.line })
173 })? + self.position;
174
175 let section_name = &self.source[self.position + 1..header_end];
176 self.position = header_end + 1;
177 self.skip_line();
178
179 let start_line = self.line;
180
181 match section_name.trim() {
182 "Script Info" => {
183 let parser = ScriptInfoParser::new(self.source, self.position, start_line);
184 let (section, detected_version, issues, final_position, final_line) =
185 parser.parse().map_err(CoreError::from)?;
186
187 if let Some(version) = detected_version {
189 self.version = version;
190 }
191 self.issues.extend(issues);
192 self.position = final_position;
193 self.line = final_line;
194
195 Ok(section)
196 }
197 "V4+ Styles" | "V4 Styles" | "V4++ Styles" => {
198 let parser = StylesParser::new(self.source, self.position, start_line);
199 let (section, format, issues, final_position, final_line) =
200 parser.parse().map_err(CoreError::from)?;
201
202 self.styles_format = format;
204 self.issues.extend(issues);
205 self.position = final_position;
206 self.line = final_line;
207
208 Ok(section)
209 }
210 "Events" => {
211 let parser = EventsParser::new(self.source, self.position, start_line);
212 let (section, format, issues, final_position, final_line) =
213 parser.parse().map_err(CoreError::from)?;
214
215 self.events_format = format;
217 self.issues.extend(issues);
218 self.position = final_position;
219 self.line = final_line;
220
221 Ok(section)
222 }
223 "Fonts" => {
224 let (section, final_position, final_line) =
225 FontsParser::parse(self.source, self.position, start_line);
226
227 self.position = final_position;
229 self.line = final_line;
230
231 Ok(section)
232 }
233 "Graphics" => {
234 let (section, final_position, final_line) =
235 GraphicsParser::parse(self.source, self.position, start_line);
236
237 self.position = final_position;
239 self.line = final_line;
240
241 Ok(section)
242 }
243 _ => {
244 #[cfg(feature = "plugins")]
245 if self.registry.is_some() {
246 if let Some(result) = self.try_process_with_registry(section_name, start_line) {
248 return result;
249 }
250 }
251
252 let suggestion = self.skip_to_next_section();
253 let error = ParseError::UnknownSection {
254 section: section_name.to_string(),
255 line: self.line,
256 };
257
258 if let Some(suggestion_text) = suggestion {
260 self.issues.push(ParseIssue {
261 severity: IssueSeverity::Info,
262 category: IssueCategory::Structure,
263 message: suggestion_text,
264 line: self.line,
265 column: Some(0),
266 span: None,
267 suggestion: None,
268 });
269 }
270
271 Err(CoreError::from(error))
272 }
273 }
274 }
275
276 fn at_next_section(&self) -> bool {
278 let remaining = self.source[self.position..].trim_start();
279 if !remaining.starts_with('[') {
280 return false;
281 }
282
283 remaining.find('\n').map_or_else(
285 || remaining.contains(']'),
286 |line_end| remaining[..line_end].contains(']'),
287 )
288 }
289
290 fn skip_line(&mut self) {
292 if let Some(newline_pos) = self.source[self.position..].find('\n') {
293 self.position += newline_pos + 1;
294 self.line += 1;
295 } else {
296 self.position = self.source.len();
297 }
298 }
299
300 fn skip_whitespace_and_comments(&mut self) {
302 while self.position < self.source.len() {
303 let remaining = &self.source[self.position..];
304 let trimmed = remaining.trim_start();
305
306 if trimmed.starts_with(';') || trimmed.starts_with("!:") {
307 self.skip_line();
308 } else if trimmed != remaining {
309 self.position += remaining.len() - trimmed.len();
310 } else {
311 break;
312 }
313 }
314 }
315
316 #[cfg(feature = "plugins")]
318 fn try_process_with_registry(
319 &mut self,
320 section_name: &str,
321 start_line: usize,
322 ) -> Option<Result<Section<'a>>> {
323 let registry = self.registry?;
324
325 let mut lines = Vec::new();
327
328 while self.position < self.source.len() && !self.at_next_section() {
329 let line_start = self.position;
330 let line_end = self.source[self.position..]
331 .find('\n')
332 .map_or(self.source.len(), |i| self.position + i);
333
334 if line_end > line_start {
335 let line = &self.source[line_start..line_end];
336 lines.push(line);
337 }
338
339 self.skip_line();
340 }
341
342 match registry.process_section(section_name, section_name, &lines) {
344 Some(SectionResult::Processed) => {
345 self.issues.push(ParseIssue::new(
349 IssueSeverity::Info,
350 IssueCategory::Structure,
351 format!("Section '{section_name}' processed by plugin"),
352 start_line,
353 ));
354
355 None
358 }
359 Some(SectionResult::Failed(msg)) => {
360 self.issues.push(ParseIssue::new(
361 IssueSeverity::Warning,
362 IssueCategory::Structure,
363 format!("Plugin failed to process section '{section_name}': {msg}"),
364 start_line,
365 ));
366 None
367 }
368 Some(SectionResult::Ignored) | None => None,
369 }
370 }
371
372 fn skip_to_next_section(&mut self) -> Option<String> {
374 let mut suggestion = None;
375 let start_position = self.position;
376
377 while self.position < self.source.len() {
378 if self.at_next_section() {
379 break;
380 }
381
382 let line_start = self.position;
384 let line_end = self.source[self.position..]
385 .find('\n')
386 .map_or(self.source.len(), |i| self.position + i);
387
388 if line_end > line_start {
389 let line = &self.source[line_start..line_end];
390
391 if suggestion.is_none() {
393 if line.trim_start().starts_with("Style:") {
394 suggestion = Some("Did you mean '[V4+ Styles]'?".to_string());
395 } else if line.trim_start().starts_with("Dialogue:")
396 || line.trim_start().starts_with("Comment:")
397 {
398 suggestion = Some("Did you mean '[Events]'?".to_string());
399 } else if line.trim_start().starts_with("Title:")
400 || line.trim_start().starts_with("ScriptType:")
401 {
402 suggestion = Some("Did you mean '[Script Info]'?".to_string());
403 } else if line.trim_start().starts_with("Format:") {
404 let remaining = &self.source[self.position..];
406 if remaining.contains("Dialogue:") {
407 suggestion = Some("Did you mean '[Events]'?".to_string());
408 } else if remaining.contains("Style:") {
409 suggestion = Some("Did you mean '[V4+ Styles]'?".to_string());
410 }
411 }
412 }
413 }
414
415 self.skip_line();
416
417 if self.position == start_position {
419 self.position = (self.position + 1).min(self.source.len());
420 break;
421 }
422 }
423
424 suggestion
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 fn create_test_script(content: &str) -> String {
433 format!("[Script Info]\nTitle: Test\n\n{content}")
434 }
435
436 #[test]
437 fn parser_new() {
438 let source = "test content";
439 let parser = Parser::new(source);
440 assert_eq!(parser.source, source);
441 assert_eq!(parser.position, 0);
442 assert_eq!(parser.line, 1);
443 assert_eq!(parser.version, ScriptVersion::AssV4);
444 assert!(parser.sections.is_empty());
445 assert!(parser.issues.is_empty());
446 assert!(parser.styles_format.is_none());
447 assert!(parser.events_format.is_none());
448 }
449
450 #[test]
451 fn parser_parse_empty_script() {
452 let parser = Parser::new("");
453 let script = parser.parse();
454 assert_eq!(script.version(), ScriptVersion::AssV4);
455 assert!(script.sections().is_empty());
456 }
457
458 #[test]
459 fn parser_parse_with_bom() {
460 let content = "\u{FEFF}[Script Info]\nTitle: Test";
461 let parser = Parser::new(content);
462 let script = parser.parse();
463 assert!(!script.sections().is_empty());
464 }
465
466 #[test]
467 fn parser_parse_input_size_limit() {
468 let large_content = "a".repeat(51 * 1024 * 1024); let parser = Parser::new(&large_content);
470 let script = parser.parse();
471 assert!(!script.issues().is_empty());
472 let has_size_error = script
473 .issues()
474 .iter()
475 .any(|issue| issue.message.contains("Input size limit exceeded"));
476 assert!(has_size_error);
477 }
478
479 #[test]
480 fn parser_parse_unknown_section() {
481 let content = "[Unknown Section]\nSome content";
482 let parser = Parser::new(content);
483 let script = parser.parse();
484 let has_unknown_section_warning = script
485 .issues()
486 .iter()
487 .any(|issue| issue.message.contains("Unknown section"));
488 assert!(has_unknown_section_warning);
489 }
490
491 #[test]
492 fn parser_parse_unclosed_section_header() {
493 let content = "[Script Info\nTitle: Test";
494 let parser = Parser::new(content);
495 let script = parser.parse();
496 let has_unclosed_error = script.issues().iter().any(|issue| {
497 issue.message.contains("Unclosed section header")
498 || issue.message.contains("Failed to parse section")
499 });
500 assert!(has_unclosed_error);
501 }
502
503 #[test]
504 fn parser_parse_missing_section_header() {
505 let content = "Title: Test\nAuthor: Someone";
506 let parser = Parser::new(content);
507 let script = parser.parse();
508 let has_header_error = script.issues().iter().any(|issue| {
509 issue.message.contains("Expected section header")
510 || issue.message.contains("Failed to parse section")
511 });
512 assert!(has_header_error);
513 }
514
515 #[test]
516 fn parser_parse_script_info_section() {
517 let content = "[Script Info]\nTitle: Test Script\nScriptType: v4.00+";
518 let parser = Parser::new(content);
519 let script = parser.parse();
520 assert_eq!(script.sections().len(), 1);
521 assert!(
523 script.version() == ScriptVersion::AssV4Plus
524 || script.version() == ScriptVersion::AssV4
525 );
526 }
527
528 #[test]
529 fn parser_parse_styles_section() {
530 let content =
531 create_test_script("[V4+ Styles]\nFormat: Name, Fontname\nStyle: Default,Arial");
532 let parser = Parser::new(&content);
533 let script = parser.parse();
534 assert!(script.sections().len() >= 2);
535 }
536
537 #[test]
538 fn parser_parse_events_section() {
539 let content = create_test_script(
540 "[Events]\nFormat: Start, End, Text\nDialogue: 0:00:00.00,0:00:05.00,Test",
541 );
542 let parser = Parser::new(&content);
543 let script = parser.parse();
544 assert!(script.sections().len() >= 2);
545 }
546
547 #[test]
548 fn parser_parse_fonts_section() {
549 let content = create_test_script("[Fonts]\nfontname: Arial\nfontdata: ABCD1234");
550 let parser = Parser::new(&content);
551 let script = parser.parse();
552 assert!(script.sections().len() >= 2);
553 }
554
555 #[test]
556 fn parser_parse_graphics_section() {
557 let content = create_test_script("[Graphics]\nfilename: image.png\ndata: ABCD1234");
558 let parser = Parser::new(&content);
559 let script = parser.parse();
560 assert!(script.sections().len() >= 2);
561 }
562
563 #[test]
564 fn parser_skip_comments() {
565 let content = "; This is a comment\n!: Another comment\n[Script Info]\nTitle: Test";
566 let parser = Parser::new(content);
567 let script = parser.parse();
568 assert!(!script.sections().is_empty());
569 }
570
571 #[test]
572 fn parser_error_recovery_style_suggestion() {
573 let content = "[BadSection]\nStyle: Default,Arial\n[Script Info]\nTitle: Test";
574 let parser = Parser::new(content);
575 let script = parser.parse();
576 let has_suggestion = script
577 .issues()
578 .iter()
579 .any(|issue| issue.message.contains("[V4+ Styles]"));
580 assert!(has_suggestion);
581 }
582
583 #[test]
584 fn parser_error_recovery_events_suggestion() {
585 let content =
586 "[BadSection]\nDialogue: 0:00:00.00,0:00:05.00,Test\n[Script Info]\nTitle: Test";
587 let parser = Parser::new(content);
588 let script = parser.parse();
589 let has_suggestion = script
590 .issues()
591 .iter()
592 .any(|issue| issue.message.contains("[Events]"));
593 assert!(has_suggestion);
594 }
595
596 #[test]
597 fn parser_error_recovery_script_info_suggestion() {
598 let content = "[BadSection]\nTitle: Test Script\n[Script Info]\nTitle: Real";
599 let parser = Parser::new(content);
600 let script = parser.parse();
601 let has_suggestion = script
602 .issues()
603 .iter()
604 .any(|issue| issue.message.contains("[Script Info]"));
605 assert!(has_suggestion);
606 }
607
608 #[test]
609 fn parser_error_recovery_format_line_events() {
610 let content = "[BadSection]\nFormat: Start, End, Text\nDialogue: 0:00:00.00,0:00:05.00,Test\n[Script Info]\nTitle: Test";
611 let parser = Parser::new(content);
612 let script = parser.parse();
613 let has_suggestion = script
614 .issues()
615 .iter()
616 .any(|issue| issue.message.contains("[Events]"));
617 assert!(has_suggestion);
618 }
619
620 #[test]
621 fn parser_error_recovery_format_line_styles() {
622 let content = "[BadSection]\nFormat: Name, Fontname\nStyle: Default,Arial\n[Script Info]\nTitle: Test";
623 let parser = Parser::new(content);
624 let script = parser.parse();
625 let has_suggestion = script
626 .issues()
627 .iter()
628 .any(|issue| issue.message.contains("[V4+ Styles]"));
629 assert!(has_suggestion);
630 }
631
632 #[test]
633 fn parser_multiple_sections() {
634 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name\nStyle: Default\n\n[Events]\nFormat: Text\nDialogue: Test";
635 let parser = Parser::new(content);
636 let script = parser.parse();
637 assert_eq!(script.sections().len(), 3);
638 }
639
640 #[test]
641 fn parser_whitespace_handling() {
642 let content = " \n\n [Script Info] \n Title: Test \n\n ";
643 let parser = Parser::new(content);
644 let script = parser.parse();
645 assert!(!script.sections().is_empty());
646 }
647
648 #[test]
649 fn parser_invalid_bom_warning() {
650 let content = "[Script Info]\nTitle: Test";
652 let parser = Parser::new(content);
653 let script = parser.parse();
654 assert!(!script.sections().is_empty());
656 }
657
658 #[test]
659 fn parser_v4_styles_section() {
660 let content = "[V4 Styles]\nFormat: Name, Fontname\nStyle: Default,Arial";
661 let parser = Parser::new(content);
662 let script = parser.parse();
663 assert!(!script.sections().is_empty());
664 }
665
666 #[test]
667 fn parser_skip_to_next_section_with_format_line_events() {
668 let content = "[BadSection]\nFormat: Start, End, Text\nDialogue: 0:00:00.00,0:00:05.00,Test\n[Events]\nFormat: Start, End, Text\nDialogue: 0:00:00.00,0:00:05.00,Real";
669 let parser = Parser::new(content);
670 let script = parser.parse();
671 let has_events_suggestion = script
672 .issues()
673 .iter()
674 .any(|issue| issue.message.contains("Did you mean '[Events]'?"));
675 assert!(has_events_suggestion);
676 }
677
678 #[test]
679 fn parser_skip_to_next_section_with_format_line_styles() {
680 let content = "[BadSection]\nFormat: Name, Fontname\nStyle: Default,Arial\n[V4+ Styles]\nFormat: Name, Fontname\nStyle: Real,Arial";
681 let parser = Parser::new(content);
682 let script = parser.parse();
683 let has_styles_suggestion = script
684 .issues()
685 .iter()
686 .any(|issue| issue.message.contains("Did you mean '[V4+ Styles]'?"));
687 assert!(has_styles_suggestion);
688 }
689
690 #[test]
691 fn parser_at_next_section_edge_cases() {
692 let content = "[Incomplete";
694 let parser = Parser::new(content);
695 let script = parser.parse();
696 assert!(!script.issues().is_empty());
698 }
699
700 #[test]
701 fn parser_at_next_section_with_closing_bracket() {
702 let content = "[Script Info]\nTitle: Test\n[V4+ Styles]\nFormat: Name";
703 let parser = Parser::new(content);
704 let script = parser.parse();
705 assert!(!script.sections().is_empty());
706 }
707
708 #[test]
709 fn parser_skip_line_edge_cases() {
710 let content = "[Script Info]\n\n\n\nTitle: Test\n";
711 let parser = Parser::new(content);
712 let script = parser.parse();
713 assert!(!script.sections().is_empty());
714 }
715
716 #[test]
717 fn parser_mixed_comment_styles() {
718 let content =
719 "; Comment style 1\n!: Comment style 2\n; Another comment\n[Script Info]\nTitle: Test";
720 let parser = Parser::new(content);
721 let script = parser.parse();
722 assert!(!script.sections().is_empty());
723 }
724
725 #[test]
726 fn parser_section_header_with_extra_brackets() {
727 let content = "[Script Info]]\nTitle: Test";
728 let parser = Parser::new(content);
729 let script = parser.parse();
730 assert!(!script.sections().is_empty());
731 }
732
733 #[test]
734 fn parser_empty_section_header() {
735 let content = "[]\nSome content\n[Script Info]\nTitle: Test";
736 let parser = Parser::new(content);
737 let script = parser.parse();
738 let has_error = script.issues().iter().any(|issue| {
739 issue.message.contains("Unknown section")
740 || issue.message.contains("Failed to parse section")
741 });
742 assert!(has_error);
743 }
744
745 #[test]
746 fn parser_section_header_only_spaces() {
747 let content = "[ ]\nSome content\n[Script Info]\nTitle: Test";
748 let parser = Parser::new(content);
749 let script = parser.parse();
750 let has_error = script.issues().iter().any(|issue| {
751 issue.message.contains("Unknown section")
752 || issue.message.contains("Failed to parse section")
753 });
754 assert!(has_error);
755 }
756
757 #[test]
758 fn parser_malformed_bom_sequence() {
759 let content = "\u{00EF}\u{00BB}[Script Info]\nTitle: Test";
761 let parser = Parser::new(content);
762 let script = parser.parse();
763 assert!(script.sections().is_empty() || !script.sections().is_empty());
765 }
766
767 #[test]
768 fn parser_content_after_eof() {
769 let content = "[Script Info]\nTitle: Test";
770 let parser = Parser::new(content);
771 let script = parser.parse();
772 assert!(!script.sections().is_empty());
773 assert!(
774 script.issues().is_empty()
775 || script
776 .issues()
777 .iter()
778 .all(|i| i.severity != IssueSeverity::Error)
779 );
780 }
781
782 #[test]
783 fn parser_multiple_consecutive_section_headers() {
784 let content = "[Script Info]\n[V4+ Styles]\n[Events]\nFormat: Text\nDialogue: Test";
785 let parser = Parser::new(content);
786 let script = parser.parse();
787 assert!(!script.sections().is_empty());
788 }
789
790 #[test]
791 fn parser_section_header_with_special_chars() {
792 let content = "[Script Info & More!]\nTitle: Test\n[Script Info]\nTitle: Real";
793 let parser = Parser::new(content);
794 let script = parser.parse();
795 let has_unknown_section = script
796 .issues()
797 .iter()
798 .any(|issue| issue.message.contains("Unknown section"));
799 assert!(has_unknown_section);
800 }
801
802 #[test]
803 fn parser_skip_to_next_section_no_advance_protection() {
804 let content = "[BadSection\nContent without proper section end";
806 let parser = Parser::new(content);
807 let script = parser.parse();
808 assert!(!script.issues().is_empty());
810 }
811
812 #[test]
813 fn parser_whitespace_before_and_after_sections() {
814 let content = " \n\n ; Comment\n [Script Info] \n Title: Test \n\n [V4+ Styles] \n Format: Name\n Style: Default \n\n ";
815 let parser = Parser::new(content);
816 let script = parser.parse();
817 assert!(script.sections().len() >= 2);
818 }
819
820 #[test]
821 fn parser_comment_lines_between_sections() {
822 let content = "[Script Info]\nTitle: Test\n; This is a comment\n!: Another comment\n\n[V4+ Styles]\nFormat: Name\nStyle: Default";
823 let parser = Parser::new(content);
824 let script = parser.parse();
825 assert!(script.sections().len() >= 2);
826 }
827
828 #[test]
829 fn parser_find_section_end_no_newline() {
830 let content = "[Script Info]";
831 let parser = Parser::new(content);
832 let script = parser.parse();
833 assert!(!script.sections().is_empty() || !script.issues().is_empty());
834 }
835
836 #[test]
837 fn parser_unicode_in_section_names() {
838 let content = "[Script Info 中文]\nTitle: Test\n[Script Info]\nTitle: Real";
839 let parser = Parser::new(content);
840 let script = parser.parse();
841 let has_unknown_section = script
842 .issues()
843 .iter()
844 .any(|issue| issue.message.contains("Unknown section"));
845 assert!(has_unknown_section);
846 }
847
848 #[test]
849 fn parser_very_long_section_name() {
850 let long_name = "a".repeat(1000);
851 let content = format!("[{long_name}]\nTitle: Test\n[Script Info]\nTitle: Real");
852 let parser = Parser::new(&content);
853 let script = parser.parse();
854 let has_unknown_section = script
855 .issues()
856 .iter()
857 .any(|issue| issue.message.contains("Unknown section"));
858 assert!(has_unknown_section);
859 }
860
861 #[test]
862 fn parser_case_sensitive_section_names() {
863 let content = "[script info]\nTitle: Test\n[Script Info]\nTitle: Real";
864 let parser = Parser::new(content);
865 let script = parser.parse();
866 let has_unknown_section = script
867 .issues()
868 .iter()
869 .any(|issue| issue.message.contains("Unknown section"));
870 assert!(has_unknown_section);
871 }
872
873 #[test]
874 fn parser_parse_section_error_unknown_section_with_content() {
875 let content = "[BadSection]\nSome content here\nMore content\n[Script Info]\nTitle: Test";
876 let parser = Parser::new(content);
877 let script = parser.parse();
878 let has_unknown_error = script.issues().iter().any(|issue| {
879 issue.message.contains("Unknown section") || issue.message.contains("BadSection")
880 });
881 assert!(has_unknown_error);
882 }
883
884 #[test]
885 fn parser_parse_section_error_unclosed_bracket_at_eof() {
886 let content = "[Script Info";
887 let parser = Parser::new(content);
888 let script = parser.parse();
889 let has_unclosed_error = script.issues().iter().any(|issue| {
890 issue.message.contains("Unclosed section header")
891 || issue.message.contains("Failed to parse section")
892 });
893 assert!(has_unclosed_error);
894 }
895
896 #[test]
897 fn parser_parse_section_error_empty_section_name() {
898 let content = "[]\nTitle: Test";
899 let parser = Parser::new(content);
900 let script = parser.parse();
901 let has_empty_section_error = script.issues().iter().any(|issue| {
902 issue.message.contains("Unknown section")
903 || issue.message.contains("Failed to parse section")
904 });
905 assert!(has_empty_section_error);
906 }
907
908 #[test]
909 fn parser_parse_section_error_whitespace_only_section() {
910 let content = "[ ]\nTitle: Test";
911 let parser = Parser::new(content);
912 let script = parser.parse();
913 let has_whitespace_error = script.issues().iter().any(|issue| {
914 issue.message.contains("Unknown section")
915 || issue.message.contains("Failed to parse section")
916 });
917 assert!(has_whitespace_error);
918 }
919
920 #[test]
921 fn parser_error_recovery_multiple_unknown_sections() {
922 let content = "[BadSection1]\nStyle: Default,Arial\n[BadSection2]\nDialogue: 0:00:00.00,0:00:05.00,Test\n[Script Info]\nTitle: Test";
923 let parser = Parser::new(content);
924 let script = parser.parse();
925 let style_suggestion_count = script
926 .issues()
927 .iter()
928 .filter(|issue| issue.message.contains("[V4+ Styles]"))
929 .count();
930 let events_suggestion_count = script
931 .issues()
932 .iter()
933 .filter(|issue| issue.message.contains("[Events]"))
934 .count();
935 assert!(style_suggestion_count >= 1);
936 assert!(events_suggestion_count >= 1);
937 }
938
939 #[test]
940 fn parser_skip_to_next_section_no_protection_edge_case() {
941 let content = "[UnknownSection]\nLine without next section";
942 let parser = Parser::new(content);
943 let script = parser.parse();
944 let has_unknown_error = script
945 .issues()
946 .iter()
947 .any(|issue| issue.message.contains("Unknown section"));
948 assert!(has_unknown_error);
949 }
950
951 #[test]
952 fn parser_find_section_end_at_exact_boundary() {
953 let content = "[Script Info]\nTitle: Test\n[V4+ Styles]";
954 let parser = Parser::new(content);
955 let script = parser.parse();
956 assert!(!script.sections().is_empty());
957 }
958
959 #[test]
960 fn parser_section_header_without_content() {
961 let content = "[Script Info]\n[V4+ Styles]\nFormat: Name\nStyle: Default,Arial";
962 let parser = Parser::new(content);
963 let script = parser.parse();
964 assert!(script.sections().len() >= 2);
965 }
966
967 #[test]
968 fn parser_malformed_section_headers_mixed() {
969 let content = "[Script Info\nTitle: Test\n]NotASection[\n[V4+ Styles]\nFormat: Name\nStyle: Default,Arial";
970 let parser = Parser::new(content);
971 let script = parser.parse();
972 let has_errors = script.issues().iter().any(|issue| {
973 issue.message.contains("Unclosed section header")
974 || issue.message.contains("Unknown section")
975 || issue.message.contains("Failed to parse section")
976 });
977 assert!(has_errors);
978 }
979
980 #[test]
981 fn parser_nested_bracket_edge_cases() {
982 let content =
983 "[[Script Info]]\nTitle: Test\n[V4+ Styles]\nFormat: Name\nStyle: Default,Arial";
984 let parser = Parser::new(content);
985 let script = parser.parse();
986 let has_unknown_error = script.issues().iter().any(|issue| {
987 issue.message.contains("Unknown section") || issue.message.contains("[Script Info]")
988 });
989 assert!(has_unknown_error);
990 }
991
992 #[test]
993 fn parser_section_with_trailing_characters() {
994 let content = "[Script Info] Extra Text\nTitle: Test";
995 let parser = Parser::new(content);
996 let script = parser.parse();
997 assert!(!script.sections().is_empty());
999 let has_unknown_error = script
1001 .issues()
1002 .iter()
1003 .any(|issue| issue.message.contains("Unknown section"));
1004 assert!(!has_unknown_error);
1005 }
1006
1007 #[test]
1008 fn parser_complex_error_recovery_scenario() {
1009 let content = "[BadSection1]\nStyle: Test,Arial,20\nComment: 0,0:00:00.00,0:00:01.00,,Comment text\n[BadSection2]\nDialogue: 0,0:00:00.00,0:00:01.00,Default,,0,0,0,,Test\n[Script Info]\nTitle: Test";
1010 let parser = Parser::new(content);
1011 let script = parser.parse();
1012
1013 let has_style_suggestion = script
1014 .issues()
1015 .iter()
1016 .any(|issue| issue.message.contains("[V4+ Styles]"));
1017 let has_events_suggestion = script
1018 .issues()
1019 .iter()
1020 .any(|issue| issue.message.contains("[Events]"));
1021
1022 assert!(has_style_suggestion);
1023 assert!(has_events_suggestion);
1024 }
1025
1026 #[test]
1027 fn parser_input_size_limit_exactly_at_boundary() {
1028 let content = "a".repeat(50 * 1024 * 1024 - 1);
1029 let parser = Parser::new(&content);
1030 let script = parser.parse();
1031 let has_size_error = script
1033 .issues()
1034 .iter()
1035 .any(|issue| issue.message.contains("Input size limit exceeded"));
1036 assert!(!has_size_error);
1037 }
1038
1039 #[test]
1040 fn parser_bom_detection_partial_sequences() {
1041 let bytes = &[
1043 0xEF, 0xBB, b'[', b'S', b'c', b'r', b'i', b'p', b't', b' ', b'I', b'n', b'f', b'o',
1044 b']', b'\n', b'T', b'i', b't', b'l', b'e', b':', b' ', b'T', b'e', b's', b't',
1045 ];
1046 let content_partial_bom = String::from_utf8_lossy(bytes);
1047 let parser = Parser::new(&content_partial_bom);
1048 let script = parser.parse();
1049 let has_bom_warning = script.issues().iter().any(|issue| {
1050 issue.message.contains("BOM") || issue.message.contains("byte order mark")
1051 });
1052 assert!(has_bom_warning);
1053 }
1054
1055 #[test]
1056 fn parser_version_detection_edge_cases() {
1057 let content = "[Script Info]\nScriptType: v4.00++\nTitle: Test";
1058 let parser = Parser::new(content);
1059 let script = parser.parse();
1060 assert!(!script.sections().is_empty());
1062 }
1063}