ass_core/parser/ast/
media.rs

1//! Media AST nodes for embedded fonts and graphics
2//!
3//! Contains Font and Graphic structs representing embedded media from the
4//! [Fonts] and [Graphics] sections with zero-copy design and UU-decoding.
5
6#[cfg(not(feature = "std"))]
7extern crate alloc;
8
9#[cfg(not(feature = "std"))]
10use alloc::format;
11use alloc::vec::Vec;
12
13use super::Span;
14#[cfg(debug_assertions)]
15use core::ops::Range;
16
17/// Embedded font from `[Fonts\]` section
18///
19/// Represents a font file embedded in the ASS script using UU-encoding.
20/// Provides lazy decoding to avoid processing overhead unless the font
21/// data is actually needed.
22///
23/// # Examples
24///
25/// ```rust
26/// use ass_core::parser::ast::{Font, Span};
27///
28/// let font = Font {
29///     filename: "custom.ttf",
30///     data_lines: vec!["begin 644 custom.ttf", "M'XL..."],
31///     span: Span::new(0, 0, 0, 0),
32/// };
33///
34/// // Decode when needed
35/// let decoded = font.decode_data()?;
36/// # Ok::<(), Box<dyn std::error::Error>>(())
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Font<'a> {
40    /// Font filename as it appears in the `[Fonts\]` section
41    pub filename: &'a str,
42
43    /// UU-encoded font data lines as zero-copy spans
44    pub data_lines: Vec<&'a str>,
45
46    /// Span in source text where this font is defined
47    pub span: Span,
48}
49
50impl Font<'_> {
51    /// Decode UU-encoded font data with lazy evaluation
52    ///
53    /// Converts the UU-encoded data lines to raw binary font data.
54    /// This is expensive so it's only done when explicitly requested.
55    ///
56    /// # Returns
57    ///
58    /// Decoded binary font data on success, error if UU-decoding fails
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the UU-encoded data is malformed or cannot be decoded.
63    ///
64    /// # Examples
65    ///
66    /// ```rust
67    /// # use ass_core::parser::ast::{Font, Span};
68    /// # let font = Font { filename: "test.ttf", data_lines: vec![], span: Span::new(0, 0, 0, 0) };
69    /// match font.decode_data() {
70    ///     Ok(data) => println!("Font size: {} bytes", data.len()),
71    ///     Err(e) => eprintln!("Decode error: {}", e),
72    /// }
73    /// ```
74    pub fn decode_data(&self) -> Result<Vec<u8>, crate::utils::CoreError> {
75        crate::utils::decode_uu_data(self.data_lines.iter().copied())
76    }
77
78    /// Convert font to ASS string representation
79    ///
80    /// Generates the font entry as it appears in the `[Fonts\]` section.
81    ///
82    /// # Examples
83    ///
84    /// ```rust
85    /// # use ass_core::parser::ast::{Font, Span};
86    /// let font = Font {
87    ///     filename: "custom.ttf",
88    ///     data_lines: vec!["begin 644 custom.ttf", "M'XL...", "end"],
89    ///     span: Span::new(0, 0, 0, 0),
90    /// };
91    /// let ass_string = font.to_ass_string();
92    /// assert!(ass_string.starts_with("fontname: custom.ttf\n"));
93    /// assert!(ass_string.contains("M'XL..."));
94    /// ```
95    #[must_use]
96    pub fn to_ass_string(&self) -> alloc::string::String {
97        let mut result = format!("fontname: {}\n", self.filename);
98        for line in &self.data_lines {
99            result.push_str(line);
100            result.push('\n');
101        }
102        result
103    }
104
105    /// Validate all spans in this Font reference valid source
106    ///
107    /// Debug helper to ensure zero-copy invariants are maintained.
108    /// Validates that filename and all data line references point to
109    /// memory within the specified source range.
110    ///
111    /// Only available in debug builds to avoid performance overhead.
112    #[cfg(debug_assertions)]
113    #[must_use]
114    pub fn validate_spans(&self, source_range: &Range<usize>) -> bool {
115        let filename_ptr = self.filename.as_ptr() as usize;
116        let filename_valid = source_range.contains(&filename_ptr);
117
118        let data_valid = self.data_lines.iter().all(|line| {
119            let ptr = line.as_ptr() as usize;
120            source_range.contains(&ptr)
121        });
122
123        filename_valid && data_valid
124    }
125}
126
127/// Embedded graphic from `[Graphics\]` section
128///
129/// Represents an image file embedded in the ASS script using UU-encoding.
130/// Commonly used for logos, textures, and other graphical elements.
131/// Provides lazy decoding for performance.
132///
133/// # Examples
134///
135/// ```rust
136/// use ass_core::parser::ast::{Graphic, Span};
137///
138/// let graphic = Graphic {
139///     filename: "logo.png",
140///     data_lines: vec!["begin 644 logo.png", "M89PNG..."],
141///     span: Span::new(0, 0, 0, 0),
142/// };
143///
144/// let decoded = graphic.decode_data()?;
145/// # Ok::<(), Box<dyn std::error::Error>>(())
146/// ```
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct Graphic<'a> {
149    /// Graphic filename as it appears in the `[Graphics\]` section
150    pub filename: &'a str,
151
152    /// UU-encoded graphic data lines as zero-copy spans
153    pub data_lines: Vec<&'a str>,
154
155    /// Span in source text where this graphic is defined
156    pub span: Span,
157}
158
159impl Graphic<'_> {
160    /// Decode UU-encoded graphic data with lazy evaluation
161    ///
162    /// Converts the UU-encoded data lines to raw binary image data.
163    /// This is expensive so it's only done when explicitly requested.
164    ///
165    /// # Returns
166    ///
167    /// Decoded binary image data on success, error if UU-decoding fails
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if the UU-encoded data is malformed or cannot be decoded.
172    pub fn decode_data(&self) -> Result<Vec<u8>, crate::utils::CoreError> {
173        crate::utils::decode_uu_data(self.data_lines.iter().copied())
174    }
175
176    /// Convert graphic to ASS string representation
177    ///
178    /// Generates the graphic entry as it appears in the `[Graphics\]` section.
179    ///
180    /// # Examples
181    ///
182    /// ```rust
183    /// # use ass_core::parser::ast::{Graphic, Span};
184    /// let graphic = Graphic {
185    ///     filename: "logo.png",
186    ///     data_lines: vec!["begin 644 logo.png", "M'XL...", "end"],
187    ///     span: Span::new(0, 0, 0, 0),
188    /// };
189    /// let ass_string = graphic.to_ass_string();
190    /// assert!(ass_string.starts_with("filename: logo.png\n"));
191    /// assert!(ass_string.contains("M'XL..."));
192    /// ```
193    #[must_use]
194    pub fn to_ass_string(&self) -> alloc::string::String {
195        let mut result = format!("filename: {}\n", self.filename);
196        for line in &self.data_lines {
197            result.push_str(line);
198            result.push('\n');
199        }
200        result
201    }
202
203    /// Validate all spans in this Graphic reference valid source
204    ///
205    /// Debug helper to ensure zero-copy invariants are maintained.
206    /// Validates that filename and all data line references point to
207    /// memory within the specified source range.
208    ///
209    /// Only available in debug builds to avoid performance overhead.
210    #[cfg(debug_assertions)]
211    #[must_use]
212    pub fn validate_spans(&self, source_range: &Range<usize>) -> bool {
213        let filename_ptr = self.filename.as_ptr() as usize;
214        let filename_valid = source_range.contains(&filename_ptr);
215
216        let data_valid = self.data_lines.iter().all(|line| {
217            let ptr = line.as_ptr() as usize;
218            source_range.contains(&ptr)
219        });
220
221        filename_valid && data_valid
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    #[cfg(not(feature = "std"))]
229    use alloc::{format, vec};
230
231    #[test]
232    fn font_creation() {
233        let font = Font {
234            filename: "test.ttf",
235            data_lines: vec!["line1", "line2"],
236            span: Span::new(0, 0, 0, 0),
237        };
238
239        assert_eq!(font.filename, "test.ttf");
240        assert_eq!(font.data_lines.len(), 2);
241        assert_eq!(font.data_lines[0], "line1");
242        assert_eq!(font.data_lines[1], "line2");
243    }
244
245    #[test]
246    fn graphic_creation() {
247        let graphic = Graphic {
248            filename: "logo.png",
249            data_lines: vec!["data1", "data2", "data3"],
250            span: Span::new(0, 0, 0, 0),
251        };
252
253        assert_eq!(graphic.filename, "logo.png");
254        assert_eq!(graphic.data_lines.len(), 3);
255        assert_eq!(graphic.data_lines[0], "data1");
256    }
257
258    #[test]
259    fn font_clone_eq() {
260        let font = Font {
261            filename: "test.ttf",
262            data_lines: vec!["data"],
263            span: Span::new(0, 0, 0, 0),
264        };
265
266        let cloned = font.clone();
267        assert_eq!(font, cloned);
268    }
269
270    #[test]
271    fn graphic_clone_eq() {
272        let graphic = Graphic {
273            filename: "test.png",
274            data_lines: vec!["data"],
275            span: Span::new(0, 0, 0, 0),
276        };
277
278        let cloned = graphic.clone();
279        assert_eq!(graphic, cloned);
280    }
281
282    #[test]
283    fn font_debug() {
284        let font = Font {
285            filename: "debug.ttf",
286            data_lines: vec!["test"],
287            span: Span::new(0, 0, 0, 0),
288        };
289
290        let debug_str = format!("{font:?}");
291        assert!(debug_str.contains("Font"));
292        assert!(debug_str.contains("debug.ttf"));
293    }
294
295    #[test]
296    fn graphic_debug() {
297        let graphic = Graphic {
298            filename: "debug.png",
299            data_lines: vec!["test"],
300            span: Span::new(0, 0, 0, 0),
301        };
302
303        let debug_str = format!("{graphic:?}");
304        assert!(debug_str.contains("Graphic"));
305        assert!(debug_str.contains("debug.png"));
306    }
307
308    #[test]
309    fn empty_data_lines() {
310        let font = Font {
311            filename: "empty.ttf",
312            data_lines: Vec::new(),
313            span: Span::new(0, 0, 0, 0),
314        };
315
316        let graphic = Graphic {
317            filename: "empty.png",
318            data_lines: Vec::new(),
319            span: Span::new(0, 0, 0, 0),
320        };
321
322        assert!(font.data_lines.is_empty());
323        assert!(graphic.data_lines.is_empty());
324    }
325
326    #[test]
327    fn media_inequality() {
328        let font1 = Font {
329            filename: "font1.ttf",
330            data_lines: vec!["data"],
331            span: Span::new(0, 0, 0, 0),
332        };
333
334        let font2 = Font {
335            filename: "font2.ttf",
336            data_lines: vec!["data"],
337            span: Span::new(0, 0, 0, 0),
338        };
339
340        assert_ne!(font1, font2);
341    }
342
343    #[test]
344    fn font_decode_data_valid() {
345        // Valid UU-encoded data for "Cat" (test with known encoding)
346        let font = Font {
347            filename: "test.ttf",
348            data_lines: vec!["#0V%T", "`"],
349            span: Span::new(0, 0, 0, 0),
350        };
351        let decoded = font.decode_data().unwrap();
352        assert_eq!(decoded, b"Cat");
353    }
354
355    #[test]
356    fn font_decode_data_empty_lines() {
357        let font = Font {
358            filename: "test.ttf",
359            data_lines: vec![],
360            span: Span::new(0, 0, 0, 0),
361        };
362        let decoded = font.decode_data().unwrap();
363        assert!(decoded.is_empty());
364    }
365
366    #[test]
367    fn font_decode_data_whitespace_lines() {
368        let font = Font {
369            filename: "test.ttf",
370            data_lines: vec!["   ", "\t\n", ""],
371            span: Span::new(0, 0, 0, 0),
372        };
373        let decoded = font.decode_data().unwrap();
374        assert!(decoded.is_empty());
375    }
376
377    #[test]
378    fn font_decode_data_with_end_marker() {
379        let font = Font {
380            filename: "test.ttf",
381            data_lines: vec!["#0V%T", "end"],
382            span: Span::new(0, 0, 0, 0),
383        };
384        let decoded = font.decode_data().unwrap();
385        assert_eq!(decoded, b"Cat");
386    }
387
388    #[test]
389    fn font_decode_data_zero_length_line() {
390        let font = Font {
391            filename: "test.ttf",
392            data_lines: vec!["#0V%T", " "],
393            span: Span::new(0, 0, 0, 0),
394        };
395        let decoded = font.decode_data().unwrap();
396        assert_eq!(decoded, b"Cat");
397    }
398
399    #[test]
400    fn font_decode_data_multiline() {
401        // Multi-line UU-encoded data
402        let font = Font {
403            filename: "test.ttf",
404            data_lines: vec!["$4F3\"", "$4F3\""],
405            span: Span::new(0, 0, 0, 0),
406        };
407        let decoded = font.decode_data().unwrap();
408        // Should decode both lines and concatenate results
409        assert_eq!(decoded.len(), 6); // 3 bytes per line
410    }
411
412    #[test]
413    fn graphic_decode_data_valid() {
414        // Valid UU-encoded data for "PNG" (test with known encoding)
415        let graphic = Graphic {
416            filename: "test.png",
417            data_lines: vec!["#4$Y'"],
418            span: Span::new(0, 0, 0, 0),
419        };
420        let decoded = graphic.decode_data().unwrap();
421        assert_eq!(decoded, b"PNG");
422    }
423
424    #[test]
425    fn graphic_decode_data_empty_lines() {
426        let graphic = Graphic {
427            filename: "test.png",
428            data_lines: vec![],
429            span: Span::new(0, 0, 0, 0),
430        };
431        let decoded = graphic.decode_data().unwrap();
432        assert!(decoded.is_empty());
433    }
434
435    #[test]
436    fn graphic_decode_data_with_end_marker() {
437        let graphic = Graphic {
438            filename: "test.png",
439            data_lines: vec!["#4$Y'", "end"],
440            span: Span::new(0, 0, 0, 0),
441        };
442        let decoded = graphic.decode_data().unwrap();
443        assert_eq!(decoded, b"PNG");
444    }
445
446    #[test]
447    fn graphic_decode_data_whitespace_handling() {
448        let graphic = Graphic {
449            filename: "test.png",
450            data_lines: vec!["#4$Y'  ", "\t\n", ""],
451            span: Span::new(0, 0, 0, 0),
452        };
453        let decoded = graphic.decode_data().unwrap();
454        assert_eq!(decoded, b"PNG");
455    }
456
457    #[test]
458    fn font_decode_data_handles_malformed_gracefully() {
459        // UU decoding should not panic on malformed data but may return unexpected results
460        let font = Font {
461            filename: "test.ttf",
462            data_lines: vec!["invalid-characters-here"],
463            span: Span::new(0, 0, 0, 0),
464        };
465        // Should not panic, result depends on UU decoder implementation
466        let _result = font.decode_data();
467    }
468
469    #[test]
470    fn graphic_decode_data_handles_malformed_gracefully() {
471        // UU decoding should not panic on malformed data but may return unexpected results
472        let graphic = Graphic {
473            filename: "test.png",
474            data_lines: vec!["!@#$%^&*()"],
475            span: Span::new(0, 0, 0, 0),
476        };
477        // Should not panic, result depends on UU decoder implementation
478        let _result = graphic.decode_data();
479    }
480
481    #[test]
482    fn font_decode_data_length_validation() {
483        // Test that length encoding in first character is respected
484        let font = Font {
485            filename: "test.ttf",
486            data_lines: vec!["!    "], // '!' encodes length 1, but provides more data
487            span: Span::new(0, 0, 0, 0),
488        };
489        let decoded = font.decode_data().unwrap();
490        assert_eq!(decoded.len(), 1); // Should be truncated to declared length
491    }
492
493    #[test]
494    fn graphic_decode_data_length_validation() {
495        // Test that length encoding in first character is respected
496        let graphic = Graphic {
497            filename: "test.png",
498            data_lines: vec!["\"````"], // '"' encodes length 2, provides padding
499            span: Span::new(0, 0, 0, 0),
500        };
501        let decoded = graphic.decode_data().unwrap();
502        assert_eq!(decoded.len(), 2); // Should be truncated to declared length
503    }
504
505    #[cfg(debug_assertions)]
506    #[test]
507    fn font_validate_spans() {
508        let source = "fontname: test.ttf\ndata1\ndata2";
509        let font = Font {
510            filename: &source[10..18],                          // "test.ttf"
511            data_lines: vec![&source[19..24], &source[25..30]], // "data1", "data2"
512            span: Span::new(0, 0, 0, 0),
513        };
514
515        let source_range = (source.as_ptr() as usize)..(source.as_ptr() as usize + source.len());
516        assert!(font.validate_spans(&source_range));
517    }
518
519    #[cfg(debug_assertions)]
520    #[test]
521    fn graphic_validate_spans() {
522        let source = "filename: logo.png\nimage1\nimage2";
523        let graphic = Graphic {
524            filename: &source[10..18],                          // "logo.png"
525            data_lines: vec![&source[19..25], &source[26..32]], // "image1", "image2"
526            span: Span::new(0, 0, 0, 0),
527        };
528
529        let source_range = (source.as_ptr() as usize)..(source.as_ptr() as usize + source.len());
530        assert!(graphic.validate_spans(&source_range));
531    }
532
533    #[cfg(debug_assertions)]
534    #[test]
535    fn font_validate_spans_invalid() {
536        let source1 = "fontname: test.ttf";
537        let source2 = "different source";
538
539        let font = Font {
540            filename: &source1[10..18],       // "test.ttf" from source1
541            data_lines: vec![&source2[0..9]], // "different" from source2
542            span: Span::new(0, 0, 0, 0),
543        };
544
545        let source1_range =
546            (source1.as_ptr() as usize)..(source1.as_ptr() as usize + source1.len());
547        assert!(!font.validate_spans(&source1_range)); // Should fail because data_lines reference different source
548    }
549}