ass_core/parser/
main.rs

1//! Main parser coordination and dispatch logic
2//!
3//! Contains the core `Parser` struct that orchestrates parsing of different
4//! ASS script sections and handles error recovery.
5
6use 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
26/// Internal parser state for coordinating section parsing
27pub(super) struct Parser<'a> {
28    /// Source text being parsed
29    source: &'a str,
30    /// Current byte position in source
31    position: usize,
32    /// Current line number for error reporting
33    line: usize,
34    /// Detected script version
35    version: ScriptVersion,
36    /// Parsed sections accumulated so far
37    sections: Vec<Section<'a>>,
38    /// Parse issues and warnings
39    issues: Vec<ParseIssue>,
40    /// Format fields for [V4+ Styles] section
41    styles_format: Option<Vec<&'a str>>,
42    /// Format fields for `[Events\]` section
43    events_format: Option<Vec<&'a str>>,
44    /// Extension registry for custom tag handlers and section processors
45    #[cfg(feature = "plugins")]
46    registry: Option<&'a ExtensionRegistry>,
47}
48
49impl<'a> Parser<'a> {
50    /// Create new parser for source text
51    pub const fn new(source: &'a str) -> Self {
52        Self {
53            source,
54            position: 0,
55            line: 1,
56            version: ScriptVersion::AssV4, // Default, updated when ScriptType found
57            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    /// Create new parser with extension registry
67    #[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, // Default, updated when ScriptType found
77            sections: Vec::new(),
78            issues: Vec::new(),
79            styles_format: None,
80            events_format: None,
81            registry,
82        }
83    }
84
85    /// Parse complete script
86    pub fn parse(mut self) -> Script<'a> {
87        // Check input size limit to prevent DoS attacks (50MB limit)
88        const MAX_INPUT_SIZE: usize = 50 * 1024 * 1024; // 50MB
89        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 early with empty script for security
97            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        // Validate and handle BOM if present
108        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        // Skip UTF-8 BOM if present
118        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    /// Parse a single section (e.g., [Script Info])
164    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                // Update parser state
188                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                // Update parser state
203                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                // Update parser state
216                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                // Update parser state
228                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                // Update parser state
238                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                    // Try to process unknown section with registered processors
247                    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                // Add suggestion to issues if we found one
259                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    /// Check if at start of next section
277    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        // Check if this looks like a complete section header (has closing ])
284        remaining.find('\n').map_or_else(
285            || remaining.contains(']'),
286            |line_end| remaining[..line_end].contains(']'),
287        )
288    }
289
290    /// Skip to next line
291    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    /// Skip whitespace and comment lines
301    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    /// Try to process unknown section using registered processors
317    #[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        // Collect section lines
326        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        // Try to process with registry
343        match registry.process_section(section_name, section_name, &lines) {
344            Some(SectionResult::Processed) => {
345                // Create a custom section for processed content
346                // For now, we'll create a generic unknown section
347                // In a full implementation, this would create a proper custom section type
348                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                // Return success but skip the section content since we don't have
356                // a proper custom section AST type yet
357                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    /// Skip to next section for error recovery
373    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            // Look for patterns that suggest what section this might be
383            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                // Check for common section entry patterns
392                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                        // Format lines could be in styles or events
405                        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            // Prevent infinite loop: if we haven't advanced, force advance by one character
418            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); // 51MB > 50MB limit
469        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        // Version should be updated based on ScriptType parsing
522        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        // Test with content that may have BOM-related issues
651        let content = "[Script Info]\nTitle: Test";
652        let parser = Parser::new(content);
653        let script = parser.parse();
654        // Should parse successfully
655        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        // Test incomplete section header
693        let content = "[Incomplete";
694        let parser = Parser::new(content);
695        let script = parser.parse();
696        // Should handle gracefully
697        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        // Test with partial BOM-like sequence
760        let content = "\u{00EF}\u{00BB}[Script Info]\nTitle: Test";
761        let parser = Parser::new(content);
762        let script = parser.parse();
763        // Should parse, potentially with warnings - but may not have valid sections
764        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        // Test case that would trigger the infinite loop protection
805        let content = "[BadSection\nContent without proper section end";
806        let parser = Parser::new(content);
807        let script = parser.parse();
808        // Should not hang and should produce some result
809        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        // Should parse successfully - trailing text after ] is ignored
998        assert!(!script.sections().is_empty());
999        // Should not generate unknown section errors
1000        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        // Should not have size limit error since we're just under the limit
1032        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        // Create content with partial UTF-8 BOM (0xEF, 0xBB without 0xBF)
1042        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        // Should handle malformed script type gracefully
1061        assert!(!script.sections().is_empty());
1062    }
1063}