ass_core/parser/
binary_data.rs

1//! Binary data parsing for `[Fonts]` and `[Graphics]` sections
2//!
3//! Handles UU-encoded font and graphic data embedded in ASS scripts.
4//! Both sections use similar structure: filename declaration followed by
5//! base64/UU-encoded data lines.
6
7use alloc::vec::Vec;
8
9use super::{
10    ast::{Font, Graphic, Section, Span},
11    position_tracker::PositionTracker,
12};
13
14/// Generic parser for binary data sections (`[Fonts\]` and `[Graphics]`)
15pub(super) struct BinaryDataParser<'a, T> {
16    /// Position tracker for accurate span generation
17    tracker: PositionTracker<'a>,
18    /// Expected key for entries (e.g., "fontname" or "filename")
19    entry_key: &'static str,
20    /// Function to construct AST node from filename, data lines, and span
21    constructor: fn(&'a str, Vec<&'a str>, Span) -> T,
22}
23
24impl<'a, T> BinaryDataParser<'a, T> {
25    /// Create new binary data parser
26    pub fn new(
27        source: &'a str,
28        position: usize,
29        line: usize,
30        entry_key: &'static str,
31        constructor: fn(&'a str, Vec<&'a str>, Span) -> T,
32    ) -> Self {
33        Self {
34            tracker: PositionTracker::new_at(
35                source,
36                position,
37                u32::try_from(line).unwrap_or(u32::MAX),
38                1,
39            ),
40            entry_key,
41            constructor,
42        }
43    }
44
45    /// Parse complete binary data section
46    ///
47    /// Returns (entries, `final_position`, `final_line`)
48    pub fn parse(mut self) -> (Vec<T>, usize, usize) {
49        let mut entries = Vec::new();
50
51        while !self.tracker.is_at_end() && !self.at_next_section() {
52            self.skip_whitespace_and_comments();
53
54            if self.tracker.is_at_end() || self.at_next_section() {
55                break;
56            }
57
58            if let Some(entry) = self.parse_entry() {
59                entries.push(entry);
60            }
61        }
62
63        let final_position = self.tracker.offset();
64        let final_line = self.tracker.line() as usize;
65        (entries, final_position, final_line)
66    }
67
68    /// Parse single entry (key: + data lines)
69    fn parse_entry(&mut self) -> Option<T> {
70        let entry_start = self.tracker.checkpoint();
71        let line = self.current_line();
72
73        if let Some(colon_pos) = line.find(':') {
74            let key = line[..colon_pos].trim();
75            if key == self.entry_key {
76                let filename = line[colon_pos + 1..].trim();
77                self.tracker.skip_line();
78
79                let data_lines = self.collect_data_lines();
80
81                // Calculate span for this entry (from filename line to end of data)
82                let entry_end = self.tracker.checkpoint();
83                let span = entry_end.span_from(&entry_start);
84
85                return Some((self.constructor)(filename, data_lines, span));
86            }
87        }
88
89        self.tracker.skip_line();
90        None
91    }
92
93    /// Collect UU-encoded data lines until next section or empty line
94    fn collect_data_lines(&mut self) -> Vec<&'a str> {
95        let mut data_lines = Vec::new();
96
97        while !self.tracker.is_at_end() && !self.at_next_section() {
98            let data_line = self.current_line();
99            let trimmed = data_line.trim();
100
101            if trimmed.is_empty() || trimmed.starts_with('[') {
102                break;
103            }
104
105            // Skip comment lines
106            if trimmed.starts_with(';') || trimmed.starts_with('!') {
107                self.tracker.skip_line();
108                continue;
109            }
110
111            // Stop at hash comments (# followed by space or at end of line)
112            // But not UU-encoded data (# followed immediately by encoded chars)
113            if trimmed.starts_with("# ") || trimmed == "#" {
114                break;
115            }
116
117            data_lines.push(data_line);
118            self.tracker.skip_line();
119        }
120
121        data_lines
122    }
123
124    /// Check if at start of next section
125    fn at_next_section(&self) -> bool {
126        self.tracker.remaining().trim_start().starts_with('[')
127    }
128
129    /// Get current line from source
130    fn current_line(&self) -> &'a str {
131        let remaining = self.tracker.remaining();
132        let end = remaining.find('\n').unwrap_or(remaining.len());
133        &remaining[..end]
134    }
135
136    /// Skip whitespace and comment lines
137    fn skip_whitespace_and_comments(&mut self) {
138        loop {
139            self.tracker.skip_whitespace();
140
141            let remaining = self.tracker.remaining();
142            if remaining.is_empty() {
143                break;
144            }
145
146            if remaining.starts_with(';') || remaining.starts_with("!:") {
147                self.tracker.skip_line();
148                continue;
149            }
150
151            // Check for newlines in whitespace
152            if remaining.starts_with('\n') {
153                self.tracker.advance(1);
154                continue;
155            }
156
157            break;
158        }
159    }
160}
161
162/// Parser for `[Fonts\]` section - wrapper around `BinaryDataParser`
163pub(super) struct FontsParser;
164
165impl FontsParser {
166    /// Parse `[Fonts\]` section
167    ///
168    /// Returns tuple of (Section, `final_position`, `final_line`)
169    pub fn parse(source: &str, position: usize, line: usize) -> (Section<'_>, usize, usize) {
170        let parser = BinaryDataParser::new(
171            source,
172            position,
173            line,
174            "fontname",
175            |filename, data_lines, span| Font {
176                filename,
177                data_lines,
178                span,
179            },
180        );
181        let (fonts, final_position, final_line) = parser.parse();
182        (Section::Fonts(fonts), final_position, final_line)
183    }
184}
185
186/// Parser for `[Graphics\]` section - wrapper around `BinaryDataParser`
187pub(super) struct GraphicsParser;
188
189impl GraphicsParser {
190    /// Parse `[Graphics\]` section
191    ///
192    /// Returns tuple of (Section, `final_position`, `final_line`)
193    pub fn parse(source: &str, position: usize, line: usize) -> (Section<'_>, usize, usize) {
194        let parser = BinaryDataParser::new(
195            source,
196            position,
197            line,
198            "filename",
199            |filename, data_lines, span| Graphic {
200                filename,
201                data_lines,
202                span,
203            },
204        );
205        let (graphics, final_position, final_line) = parser.parse();
206        (Section::Graphics(graphics), final_position, final_line)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn fonts_parser_empty_section() {
216        let source = "";
217        let (section, _, _) = FontsParser::parse(source, 0, 1);
218
219        if let Section::Fonts(fonts) = section {
220            assert!(fonts.is_empty());
221        } else {
222            panic!("Expected Fonts section");
223        }
224    }
225
226    #[test]
227    fn fonts_parser_single_font() {
228        let source = "fontname: arial.ttf\ndata1\ndata2\n";
229        let (section, _, _) = FontsParser::parse(source, 0, 1);
230
231        if let Section::Fonts(fonts) = section {
232            assert_eq!(fonts.len(), 1);
233            assert_eq!(fonts[0].filename, "arial.ttf");
234            assert_eq!(fonts[0].data_lines.len(), 2);
235            assert_eq!(fonts[0].data_lines[0], "data1");
236            assert_eq!(fonts[0].data_lines[1], "data2");
237        } else {
238            panic!("Expected Fonts section");
239        }
240    }
241
242    #[test]
243    fn fonts_parser_multiple_fonts() {
244        let source = "fontname: font1.ttf\ndata1\ndata2\n\nfontname: font2.ttf\ndata3\ndata4\n";
245        let (section, _, _) = FontsParser::parse(source, 0, 1);
246
247        if let Section::Fonts(fonts) = section {
248            assert_eq!(fonts.len(), 2);
249
250            assert_eq!(fonts[0].filename, "font1.ttf");
251            assert_eq!(fonts[0].data_lines.len(), 2);
252            assert_eq!(fonts[0].data_lines[0], "data1");
253            assert_eq!(fonts[0].data_lines[1], "data2");
254
255            assert_eq!(fonts[1].filename, "font2.ttf");
256            assert_eq!(fonts[1].data_lines.len(), 2);
257            assert_eq!(fonts[1].data_lines[0], "data3");
258            assert_eq!(fonts[1].data_lines[1], "data4");
259        } else {
260            panic!("Expected Fonts section");
261        }
262    }
263
264    #[test]
265    fn fonts_parser_with_comments() {
266        let source = "; This is a comment\nfontname: test.ttf\n!: Another comment\ndata1\ndata2\n";
267        let (section, _, _) = FontsParser::parse(source, 0, 1);
268
269        if let Section::Fonts(fonts) = section {
270            assert_eq!(fonts.len(), 1);
271            assert_eq!(fonts[0].filename, "test.ttf");
272            assert_eq!(fonts[0].data_lines.len(), 2);
273        } else {
274            panic!("Expected Fonts section");
275        }
276    }
277
278    #[test]
279    fn fonts_parser_with_whitespace() {
280        let source = "  fontname:  arial.ttf  \n  data1  \n  data2  \n";
281        let (section, _, _) = FontsParser::parse(source, 0, 1);
282
283        if let Section::Fonts(fonts) = section {
284            assert_eq!(fonts.len(), 1);
285            assert_eq!(fonts[0].filename, "arial.ttf");
286            assert_eq!(fonts[0].data_lines.len(), 2);
287            assert_eq!(fonts[0].data_lines[0], "  data1  ");
288            assert_eq!(fonts[0].data_lines[1], "  data2  ");
289        } else {
290            panic!("Expected Fonts section");
291        }
292    }
293
294    #[test]
295    fn fonts_parser_stops_at_next_section() {
296        let source = "fontname: test.ttf\ndata1\ndata2\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n";
297        let (section, _, _) = FontsParser::parse(source, 0, 1);
298
299        if let Section::Fonts(fonts) = section {
300            assert_eq!(fonts.len(), 1);
301            assert_eq!(fonts[0].filename, "test.ttf");
302            assert_eq!(fonts[0].data_lines.len(), 2);
303        } else {
304            panic!("Expected Fonts section");
305        }
306    }
307
308    #[test]
309    fn fonts_parser_malformed_entry() {
310        let source = "invalid_line\nfontname: valid.ttf\ndata1\n";
311        let (section, _, _) = FontsParser::parse(source, 0, 1);
312
313        if let Section::Fonts(fonts) = section {
314            assert_eq!(fonts.len(), 1);
315            assert_eq!(fonts[0].filename, "valid.ttf");
316            assert_eq!(fonts[0].data_lines.len(), 1);
317        } else {
318            panic!("Expected Fonts section");
319        }
320    }
321
322    #[test]
323    fn fonts_parser_no_data_lines() {
324        let source = "fontname: empty.ttf\n[Events]\n";
325        let (section, _, _) = FontsParser::parse(source, 0, 1);
326
327        if let Section::Fonts(fonts) = section {
328            assert_eq!(fonts.len(), 1);
329            assert_eq!(fonts[0].filename, "empty.ttf");
330            assert!(fonts[0].data_lines.is_empty());
331        } else {
332            panic!("Expected Fonts section");
333        }
334    }
335
336    #[test]
337    fn graphics_parser_empty_section() {
338        let source = "";
339        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
340
341        if let Section::Graphics(graphics) = section {
342            assert!(graphics.is_empty());
343        } else {
344            panic!("Expected Graphics section");
345        }
346    }
347
348    #[test]
349    fn graphics_parser_single_graphic() {
350        let source = "filename: logo.png\nimage_data1\nimage_data2\n";
351        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
352
353        if let Section::Graphics(graphics) = section {
354            assert_eq!(graphics.len(), 1);
355            assert_eq!(graphics[0].filename, "logo.png");
356            assert_eq!(graphics[0].data_lines.len(), 2);
357            assert_eq!(graphics[0].data_lines[0], "image_data1");
358            assert_eq!(graphics[0].data_lines[1], "image_data2");
359        } else {
360            panic!("Expected Graphics section");
361        }
362    }
363
364    #[test]
365    fn graphics_parser_multiple_graphics() {
366        let source = "filename: img1.png\ndata1\ndata2\n\nfilename: img2.jpg\ndata3\ndata4\n";
367        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
368
369        if let Section::Graphics(graphics) = section {
370            assert_eq!(graphics.len(), 2);
371
372            assert_eq!(graphics[0].filename, "img1.png");
373            assert_eq!(graphics[0].data_lines.len(), 2);
374            assert_eq!(graphics[0].data_lines[0], "data1");
375            assert_eq!(graphics[0].data_lines[1], "data2");
376
377            assert_eq!(graphics[1].filename, "img2.jpg");
378            assert_eq!(graphics[1].data_lines.len(), 2);
379            assert_eq!(graphics[1].data_lines[0], "data3");
380            assert_eq!(graphics[1].data_lines[1], "data4");
381        } else {
382            panic!("Expected Graphics section");
383        }
384    }
385
386    #[test]
387    fn graphics_parser_with_comments() {
388        let source = "; Image section comment\nfilename: test.png\n!: Another comment\nimg_data1\nimg_data2\n";
389        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
390
391        if let Section::Graphics(graphics) = section {
392            assert_eq!(graphics.len(), 1);
393            assert_eq!(graphics[0].filename, "test.png");
394            assert_eq!(graphics[0].data_lines.len(), 2);
395        } else {
396            panic!("Expected Graphics section");
397        }
398    }
399
400    #[test]
401    fn graphics_parser_with_whitespace() {
402        let source = "  filename:  logo.png  \n  img_data1  \n  img_data2  \n";
403        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
404
405        if let Section::Graphics(graphics) = section {
406            assert_eq!(graphics.len(), 1);
407            assert_eq!(graphics[0].filename, "logo.png");
408            assert_eq!(graphics[0].data_lines.len(), 2);
409            assert_eq!(graphics[0].data_lines[0], "  img_data1  ");
410            assert_eq!(graphics[0].data_lines[1], "  img_data2  ");
411        } else {
412            panic!("Expected Graphics section");
413        }
414    }
415
416    #[test]
417    fn graphics_parser_stops_at_next_section() {
418        let source = "filename: test.png\nimg_data1\nimg_data2\n[Styles]\nFormat: Name, Fontname, Fontsize\n";
419        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
420
421        if let Section::Graphics(graphics) = section {
422            assert_eq!(graphics.len(), 1);
423            assert_eq!(graphics[0].filename, "test.png");
424            assert_eq!(graphics[0].data_lines.len(), 2);
425        } else {
426            panic!("Expected Graphics section");
427        }
428    }
429
430    #[test]
431    fn graphics_parser_malformed_entry() {
432        let source = "invalid_line_without_colon\nfilename: valid.png\nimg_data1\n";
433        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
434
435        if let Section::Graphics(graphics) = section {
436            assert_eq!(graphics.len(), 1);
437            assert_eq!(graphics[0].filename, "valid.png");
438            assert_eq!(graphics[0].data_lines.len(), 1);
439        } else {
440            panic!("Expected Graphics section");
441        }
442    }
443
444    #[test]
445    fn graphics_parser_no_data_lines() {
446        let source = "filename: empty.png\n[Fonts]\n";
447        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
448
449        if let Section::Graphics(graphics) = section {
450            assert_eq!(graphics.len(), 1);
451            assert_eq!(graphics[0].filename, "empty.png");
452            assert!(graphics[0].data_lines.is_empty());
453        } else {
454            panic!("Expected Graphics section");
455        }
456    }
457
458    #[test]
459    fn fonts_parser_colon_in_filename() {
460        let source = "fontname: C:\\Fonts\\arial.ttf\ndata1\n";
461        let (section, _, _) = FontsParser::parse(source, 0, 1);
462
463        if let Section::Fonts(fonts) = section {
464            assert_eq!(fonts.len(), 1);
465            assert_eq!(fonts[0].filename, "C:\\Fonts\\arial.ttf");
466        } else {
467            panic!("Expected Fonts section");
468        }
469    }
470
471    #[test]
472    fn graphics_parser_colon_in_filename() {
473        let source = "filename: D:\\Images\\logo.png\nimg_data1\n";
474        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
475
476        if let Section::Graphics(graphics) = section {
477            assert_eq!(graphics.len(), 1);
478            assert_eq!(graphics[0].filename, "D:\\Images\\logo.png");
479        } else {
480            panic!("Expected Graphics section");
481        }
482    }
483
484    #[test]
485    fn fonts_parser_malformed_entry_no_colon() {
486        let source = "invalid_font_entry\ndata1\ndata2\n";
487        let (section, _, _) = FontsParser::parse(source, 0, 1);
488
489        if let Section::Fonts(fonts) = section {
490            // Should skip malformed entries without colon
491            assert!(fonts.is_empty());
492        } else {
493            panic!("Expected Fonts section");
494        }
495    }
496
497    #[test]
498    fn fonts_parser_empty_filename() {
499        let source = "fontname: \ndata1\ndata2\n";
500        let (section, _, _) = FontsParser::parse(source, 0, 1);
501
502        if let Section::Fonts(fonts) = section {
503            assert_eq!(fonts.len(), 1);
504            assert_eq!(fonts[0].filename, "");
505            assert_eq!(fonts[0].data_lines.len(), 2);
506        } else {
507            panic!("Expected Fonts section");
508        }
509    }
510
511    #[test]
512    fn fonts_parser_whitespace_only_filename() {
513        let source = "fontname:   \ndata1\ndata2\n";
514        let (section, _, _) = FontsParser::parse(source, 0, 1);
515
516        if let Section::Fonts(fonts) = section {
517            assert_eq!(fonts.len(), 1);
518            assert_eq!(fonts[0].filename, "");
519            assert_eq!(fonts[0].data_lines.len(), 2);
520        } else {
521            panic!("Expected Fonts section");
522        }
523    }
524
525    #[test]
526    fn fonts_parser_comments_between_data_lines() {
527        let source =
528            "fontname: arial.ttf\ndata1\n; Comment line\ndata2\n! Another comment\ndata3\n";
529        let (section, _, _) = FontsParser::parse(source, 0, 1);
530
531        if let Section::Fonts(fonts) = section {
532            assert_eq!(fonts.len(), 1);
533            assert_eq!(fonts[0].filename, "arial.ttf");
534            // Comments should be skipped, only data lines included
535            assert_eq!(fonts[0].data_lines.len(), 3);
536            assert_eq!(fonts[0].data_lines[0], "data1");
537            assert_eq!(fonts[0].data_lines[1], "data2");
538            assert_eq!(fonts[0].data_lines[2], "data3");
539        } else {
540            panic!("Expected Fonts section");
541        }
542    }
543
544    #[test]
545    fn fonts_parser_empty_lines_between_data() {
546        let source = "fontname: arial.ttf\ndata1\n\n\ndata2\n   \ndata3\n";
547        let (section, _, _) = FontsParser::parse(source, 0, 1);
548
549        if let Section::Fonts(fonts) = section {
550            assert_eq!(fonts.len(), 1);
551            assert_eq!(fonts[0].filename, "arial.ttf");
552            // Parser stops at first empty line
553            assert_eq!(fonts[0].data_lines.len(), 1);
554            assert_eq!(fonts[0].data_lines[0], "data1");
555        } else {
556            panic!("Expected Fonts section");
557        }
558    }
559
560    #[test]
561    fn fonts_parser_entry_at_end_of_file() {
562        let source = "fontname: arial.ttf\ndata1\ndata2";
563        let (section, _, _) = FontsParser::parse(source, 0, 1);
564
565        if let Section::Fonts(fonts) = section {
566            assert_eq!(fonts.len(), 1);
567            assert_eq!(fonts[0].filename, "arial.ttf");
568            assert_eq!(fonts[0].data_lines.len(), 2);
569        } else {
570            panic!("Expected Fonts section");
571        }
572    }
573
574    #[test]
575    fn fonts_parser_mixed_comment_styles() {
576        let source = "fontname: arial.ttf\ndata1\n; Semicolon comment\ndata2\n! Exclamation comment\ndata3\n# Hash comment\ndata4\n";
577        let (section, _, _) = FontsParser::parse(source, 0, 1);
578
579        if let Section::Fonts(fonts) = section {
580            assert_eq!(fonts.len(), 1);
581            // Hash comments are not skipped, so parsing stops at # Hash comment
582            assert_eq!(fonts[0].data_lines.len(), 3);
583        } else {
584            panic!("Expected Fonts section");
585        }
586    }
587
588    #[test]
589    fn graphics_parser_malformed_entry_no_colon() {
590        let source = "invalid_graphic_entry\nimg_data1\nimg_data2\n";
591        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
592
593        if let Section::Graphics(graphics) = section {
594            // Should skip malformed entries without colon
595            assert!(graphics.is_empty());
596        } else {
597            panic!("Expected Graphics section");
598        }
599    }
600
601    #[test]
602    fn graphics_parser_empty_filename() {
603        let source = "filename: \nimg_data1\nimg_data2\n";
604        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
605
606        if let Section::Graphics(graphics) = section {
607            assert_eq!(graphics.len(), 1);
608            assert_eq!(graphics[0].filename, "");
609            assert_eq!(graphics[0].data_lines.len(), 2);
610        } else {
611            panic!("Expected Graphics section");
612        }
613    }
614
615    #[test]
616    fn graphics_parser_whitespace_only_filename() {
617        let source = "filename:   \nimg_data1\nimg_data2\n";
618        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
619
620        if let Section::Graphics(graphics) = section {
621            assert_eq!(graphics.len(), 1);
622            assert_eq!(graphics[0].filename, "");
623            assert_eq!(graphics[0].data_lines.len(), 2);
624        } else {
625            panic!("Expected Graphics section");
626        }
627    }
628
629    #[test]
630    fn graphics_parser_comments_between_data_lines() {
631        let source = "filename: logo.png\nimg_data1\n; Comment line\nimg_data2\n! Another comment\nimg_data3\n";
632        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
633
634        if let Section::Graphics(graphics) = section {
635            assert_eq!(graphics.len(), 1);
636            assert_eq!(graphics[0].filename, "logo.png");
637            // Comments should be skipped, only data lines included
638            assert_eq!(graphics[0].data_lines.len(), 3);
639            assert_eq!(graphics[0].data_lines[0], "img_data1");
640            assert_eq!(graphics[0].data_lines[1], "img_data2");
641            assert_eq!(graphics[0].data_lines[2], "img_data3");
642        } else {
643            panic!("Expected Graphics section");
644        }
645    }
646
647    #[test]
648    fn graphics_parser_empty_lines_between_data() {
649        let source = "filename: logo.png\nimg_data1\n\n\nimg_data2\n   \nimg_data3\n";
650        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
651
652        if let Section::Graphics(graphics) = section {
653            assert_eq!(graphics.len(), 1);
654            assert_eq!(graphics[0].filename, "logo.png");
655            // Parser stops at first empty line
656            assert_eq!(graphics[0].data_lines.len(), 1);
657            assert_eq!(graphics[0].data_lines[0], "img_data1");
658        } else {
659            panic!("Expected Graphics section");
660        }
661    }
662
663    #[test]
664    fn graphics_parser_entry_at_end_of_file() {
665        let source = "filename: logo.png\nimg_data1\nimg_data2";
666        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
667
668        if let Section::Graphics(graphics) = section {
669            assert_eq!(graphics.len(), 1);
670            assert_eq!(graphics[0].filename, "logo.png");
671            assert_eq!(graphics[0].data_lines.len(), 2);
672        } else {
673            panic!("Expected Graphics section");
674        }
675    }
676
677    #[test]
678    fn graphics_parser_mixed_comment_styles() {
679        let source = "filename: logo.png\nimg_data1\n; Semicolon comment\nimg_data2\n! Exclamation comment\nimg_data3\n# Hash comment\nimg_data4\n";
680        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
681
682        if let Section::Graphics(graphics) = section {
683            assert_eq!(graphics.len(), 1);
684            // Hash comments are not skipped, so parsing stops at # Hash comment
685            assert_eq!(graphics[0].data_lines.len(), 3);
686        } else {
687            panic!("Expected Graphics section");
688        }
689    }
690
691    #[test]
692    fn fonts_parser_multiple_entries_with_edge_cases() {
693        let source = "fontname: font1.ttf\ndata1_1\ndata1_2\n\ninvalid_entry_no_colon\n\nfontname: font2.ttf\n; Comment\ndata2_1\n\nfontname: \ndata3_1\n";
694        let (section, _, _) = FontsParser::parse(source, 0, 1);
695
696        if let Section::Fonts(fonts) = section {
697            assert_eq!(fonts.len(), 3); // All valid font entries should be parsed
698
699            assert_eq!(fonts[0].filename, "font1.ttf");
700            assert_eq!(fonts[0].data_lines.len(), 2);
701
702            assert_eq!(fonts[1].filename, "font2.ttf");
703            assert_eq!(fonts[1].data_lines.len(), 1);
704
705            assert_eq!(fonts[2].filename, "");
706            assert_eq!(fonts[2].data_lines.len(), 1);
707        } else {
708            panic!("Expected Fonts section");
709        }
710    }
711
712    #[test]
713    fn graphics_parser_multiple_entries_with_edge_cases() {
714        let source = "filename: image1.png\nimg1_1\nimg1_2\n\ninvalid_entry_no_colon\n\nfilename: image2.png\n; Comment\nimg2_1\n\nfilename: \nimg3_1\n";
715        let (section, _, _) = GraphicsParser::parse(source, 0, 1);
716
717        if let Section::Graphics(graphics) = section {
718            assert_eq!(graphics.len(), 3); // All valid graphic entries should be parsed
719
720            assert_eq!(graphics[0].filename, "image1.png");
721            assert_eq!(graphics[0].data_lines.len(), 2);
722
723            assert_eq!(graphics[1].filename, "image2.png");
724            assert_eq!(graphics[1].data_lines.len(), 1);
725
726            assert_eq!(graphics[2].filename, "");
727            assert_eq!(graphics[2].data_lines.len(), 1);
728        } else {
729            panic!("Expected Graphics section");
730        }
731    }
732}