Skip to main content

ebustl_parser/
parser.rs

1use std::io;
2use std::str::FromStr;
3
4use codepage_strings::ConvertError;
5use thiserror::Error;
6use winnow::{
7    self, ModalParser, ModalResult, Parser,
8    binary::{le_u8, le_u16},
9    combinator::{cut_err, repeat, trace},
10    error::{ContextError, ErrMode, FromExternalError, ParserError, StrContext::Label},
11    seq,
12    stream::ToUsize,
13    token::take,
14};
15
16use super::*;
17use crate::gsi::CodePageCodec;
18
19/// Errors from reading, parsing or writing STL data
20#[derive(Debug, Error)]
21pub enum ParseError {
22    /// Reading or writing a file failed
23    #[error(transparent)]
24    IoError(#[from] io::Error),
25    /// The input ended too early. Not produced when parsing a complete slice, which gives
26    /// [`ParseError::WinnowParsingError`] instead.
27    #[error("Error parsing, file may be incomplete or corrupted")]
28    Incomplete,
29    /// The Code Page Number (CPN) is not one of the code pages allowed by the spec
30    #[error("Unknown Code Page Number: {0}")]
31    CodePageNumber(u16),
32    /// The Display Standard Code (DSC) is not a value defined by the spec
33    #[error("Error parsing Display Standard Code")]
34    DisplayStandardCode,
35    /// The Time Code Status (TCS) is not a value defined by the spec
36    #[error("Error parsing Time Code Status")]
37    TimeCodeStatus,
38    /// The Disk Format Code (DFC) is neither `STL25.01` nor `STL30.01`
39    #[error("Error parsing Disk Format Code: {0}")]
40    DiskFormatCode(String),
41    /// The Character Code Table (CCT) is not a value defined by the spec
42    #[error("Error parsing Character Code Table")]
43    CharacterCodeTable,
44    /// The Cumulative Status (CS) of a TTI block is not a value defined by the spec
45    #[error("Error parsing Cumulative Status")]
46    CumulativeStatus,
47    /// The data is not a valid STL file, e.g. an invalid TTI block or trailing data
48    #[error("Parse error: {message}")]
49    WinnowParsingError {
50        /// Description of what failed and where
51        message: String,
52    },
53    /// A text field of the GSI block cannot be encoded with its code page when writing
54    #[error("Failed to encode string '{value}' using codepage {codepage}: {source}")]
55    CodePageEncoding {
56        /// The Code Page Number of the GSI block
57        codepage: u16,
58        /// The text that could not be encoded
59        value: String,
60        /// The encoding error
61        source: ConvertError,
62    },
63    /// The text of a subtitle needs more TTI blocks than a subtitle can have
64    #[error("Subtitle text needs {blocks} TTI blocks, at most 241 are allowed")]
65    SubtitleTextTooLong {
66        /// The number of TTI blocks the text would need
67        blocks: usize,
68    },
69    /// Adding a subtitle would make the Total Number of TTI Blocks (TNB) exceed 99 999
70    #[error("Total Number of TTI Blocks would exceed 99 999")]
71    TooManyTtiBlocks,
72    /// Adding a subtitle would make the Subtitle Number (SN) exceed FFFFh
73    #[error("Subtitle Number would exceed FFFFh")]
74    SubtitleNumberOverflow,
75    /// A field value is invalid according to the spec, with the message of the
76    /// [`InvalidFieldValue`] it was converted from
77    #[error("{0}")]
78    InvalidFieldValue(String),
79}
80
81/// Keeps the message but not the value, which can be of different types
82impl<T> From<InvalidFieldValue<T>> for ParseError {
83    fn from(err: InvalidFieldValue<T>) -> Self {
84        ParseError::InvalidFieldValue(err.to_string())
85    }
86}
87
88impl<E> From<ErrMode<E>> for ParseError
89where
90    E: fmt::Display,
91{
92    fn from(err: ErrMode<E>) -> Self {
93        match err {
94            ErrMode::Incomplete(_) => ParseError::Incomplete,
95            ErrMode::Backtrack(e) | ErrMode::Cut(e) => Self::WinnowParsingError {
96                message: e.to_string(),
97            },
98        }
99    }
100}
101
102/// Parses STL data in memory.
103///
104/// The input must consist of exactly one GSI block followed by zero or more whole TTI blocks.
105///
106/// # Errors
107///
108/// Fails if the GSI block or a TTI block is invalid, or if there is trailing data that does not
109/// form a complete 128 byte TTI block. The error converts into [`ParseError`].
110///
111/// # Example
112///
113/// ```rust
114/// use ebustl_parser::parser::parse_stl_from_slice;
115///
116/// let data = std::fs::read("stls/test.stl").expect("Read file");
117/// let stl = parse_stl_from_slice(&mut data.as_slice()).expect("Parse stl from slice");
118/// assert_eq!(13, stl.ttis.len());
119/// ```
120pub fn parse_stl_from_slice(input: &mut &[u8]) -> ModalResult<Stl> {
121    let gsi = parse_gsi_block(input)?;
122    // TNB may be 0, so a file with only a GSI block is valid
123    let ttis = repeat(0.., parse_tti_block(gsi.cct)).parse_next(input)?;
124    Ok(Stl { gsi, ttis })
125}
126
127#[inline(always)]
128fn take_str<'a, C, Error: ParserError<&'a [u8]>>(
129    count: C,
130) -> impl ModalParser<&'a [u8], &'a str, Error>
131where
132    C: ToUsize,
133{
134    let c = count.to_usize();
135    move |i: &mut &'a [u8]| {
136        let first = take(c).parse_next(i)?;
137        str::from_utf8(first).map_err(|_err| ErrMode::Backtrack(Error::from_input(i)))
138    }
139}
140
141/// Code page used when the Code Page Number field is blank
142const DEFAULT_CODE_PAGE: u16 = 850;
143
144/// Parses a numeric GSI field, ignoring space padding. Unused bytes in the GSI block are set
145/// to 20h, so a blank field gives `default`.
146fn number_with_default_if_blank<T: FromStr>(input: &str, default: T) -> Result<T, T::Err> {
147    let trimmed = input.trim_matches(' ');
148    if trimmed.is_empty() {
149        Ok(default)
150    } else {
151        trimmed.parse()
152    }
153}
154
155/// Parses the Time Code Status. A blank (20h) status is treated as intended for use, since
156/// the time codes are needed to use the subtitles at all.
157fn time_code_status_with_default_if_blank(data: u8) -> Result<TimeCodeStatus, ParseError> {
158    if data == b' ' {
159        Ok(TimeCodeStatus::IntendedForUse)
160    } else {
161        TimeCodeStatus::parse(data)
162    }
163}
164
165fn parse_gsi_block(input: &mut &[u8]) -> ModalResult<GsiBlock> {
166    let codepage: u16 = trace(
167        "codepage",
168        take_str(3_u16)
169            .try_map(|data| number_with_default_if_blank(data, DEFAULT_CODE_PAGE))
170            .context(Label("codepage")),
171    )
172    .parse_next(input)?;
173
174    let cpn = CodePageNumber::from_u16(codepage)
175        .map_err(|err| ErrMode::from_external_error(&input, err))?;
176
177    let coding =
178        CodePageCodec::new(codepage).map_err(|err| ErrMode::from_external_error(&input, err))?;
179
180    let dfc = take_str(10 - 3 + 1_u16)
181        .try_map(DiskFormatCode::parse)
182        .context(Label("dfc"))
183        .parse_next(input)?;
184
185    let dsc = le_u8
186        .try_map(DisplayStandardCode::parse)
187        .context(Label("dsc"))
188        .parse_next(input)?;
189
190    let cct = take(13 - 12 + 1_u16)
191        .try_map(CharacterCodeTable::parse)
192        .context(Label("cct"))
193        .parse_next(input)?;
194
195    let lc = take(15 - 14 + 1_u16)
196        .try_map(|data| coding.decode(data))
197        .context(Label("lc"))
198        .parse_next(input)?;
199
200    let opt = take(47 - 16 + 1_u16)
201        .try_map(|data| coding.decode(data))
202        .context(Label("opt"))
203        .parse_next(input)?;
204
205    let oet = take(79 - 48 + 1_u16)
206        .try_map(|data| coding.decode(data))
207        .context(Label("oet"))
208        .parse_next(input)?;
209
210    let tpt = take(111 - 80 + 1_u16)
211        .try_map(|data| coding.decode(data))
212        .context(Label("tpt"))
213        .parse_next(input)?;
214
215    let tet = take(143 - 112 + 1_u16)
216        .try_map(|data| coding.decode(data))
217        .context(Label("tet"))
218        .parse_next(input)?;
219
220    let tn = take(175 - 144 + 1_u16)
221        .try_map(|data| coding.decode(data))
222        .context(Label("tn"))
223        .parse_next(input)?;
224
225    let tcd = take(207 - 176 + 1_u16)
226        .try_map(|data| coding.decode(data))
227        .context(Label("tcd"))
228        .parse_next(input)?;
229
230    let slr = take(223 - 208 + 1_u16)
231        .try_map(|data| coding.decode(data))
232        .context(Label("slr"))
233        .parse_next(input)?;
234
235    let cd = take(229 - 224 + 1_u16)
236        .try_map(|data| coding.decode(data))
237        .context(Label("cd"))
238        .parse_next(input)?;
239
240    let rd = take(235 - 230 + 1_u16)
241        .try_map(|data| coding.decode(data))
242        .context(Label("rd"))
243        .parse_next(input)?;
244
245    let rn = take(237 - 236 + 1_u16)
246        .try_map(|data| coding.decode(data))
247        .context(Label("rn"))
248        .parse_next(input)?;
249
250    // TNB and TNS range 0-99 999, which does not fit in a u16
251    let tnb = take_str(242 - 238 + 1_u16)
252        .try_map(|data| number_with_default_if_blank(data, 0_u32))
253        .context(Label("tnb"))
254        .parse_next(input)?;
255
256    let tns = take_str(247 - 243 + 1_u16)
257        .try_map(|data| number_with_default_if_blank(data, 0_u32))
258        .context(Label("tns"))
259        .parse_next(input)?;
260
261    let tng = take_str(250 - 248 + 1_u16)
262        .try_map(|data| number_with_default_if_blank(data, 0_u16))
263        .context(Label("tng"))
264        .parse_next(input)?;
265
266    let mnc = take_str(252 - 251 + 1_u16)
267        .try_map(|data| number_with_default_if_blank(data, 0_u16))
268        .context(Label("mnc"))
269        .parse_next(input)?;
270
271    let mnr = take_str(254 - 253 + 1_u16)
272        .try_map(|data| number_with_default_if_blank(data, 0_u16))
273        .context(Label("mnr"))
274        .parse_next(input)?;
275
276    let tcs = le_u8
277        .try_map(time_code_status_with_default_if_blank)
278        .context(Label("tcs"))
279        .parse_next(input)?;
280
281    let tcp = take(263 - 256 + 1_u16)
282        .try_map(|data| coding.decode(data))
283        .context(Label("tcp"))
284        .parse_next(input)?;
285
286    let tcf = take(271 - 264 + 1_u16)
287        .try_map(|data| coding.decode(data))
288        .context(Label("tcf"))
289        .parse_next(input)?;
290
291    let tnd = take_str(1_u16)
292        .try_map(|data| number_with_default_if_blank(data, 1_u8))
293        .context(Label("tnd"))
294        .parse_next(input)?;
295
296    let dsn = take_str(1_u16)
297        .try_map(|data| number_with_default_if_blank(data, 1_u8))
298        .context(Label("dns"))
299        .parse_next(input)?;
300
301    let co = take(276 - 274 + 1_u16)
302        .try_map(|data| coding.decode(data))
303        .context(Label("co"))
304        .parse_next(input)?;
305
306    let pub_ = take(308 - 277 + 1_u16)
307        .try_map(|data| coding.decode(data))
308        .context(Label("pub_"))
309        .parse_next(input)?;
310
311    let en = take(340 - 309 + 1_u16)
312        .try_map(|data| coding.decode(data))
313        .context(Label("en"))
314        .parse_next(input)?;
315
316    let ecd = take(372 - 341 + 1_u16)
317        .try_map(|data| coding.decode(data))
318        .context(Label("ecd"))
319        .parse_next(input)?;
320
321    let _spare = take(447 - 373 + 1_u16)
322        .try_map(|data| coding.decode(data))
323        .context(Label("_spare"))
324        .parse_next(input)?;
325
326    let uda = take(1023 - 448 + 1_u16)
327        .try_map(|data| coding.decode(data))
328        .context(Label("uda"))
329        .parse_next(input)?;
330
331    Ok(GsiBlock {
332        cpn,
333        dfc,
334        dsc,
335        cct,
336        lc,
337        opt,
338        oet,
339        tpt,
340        tet,
341        tn,
342        tcd,
343        slr,
344        cd,
345        rd,
346        rn,
347        tnb,
348        tns,
349        tng,
350        mnc,
351        mnr,
352        tcs,
353        tcp,
354        tcf,
355        tnd,
356        dsn,
357        co,
358        pub_,
359        en,
360        ecd,
361        _spare,
362        uda,
363    })
364}
365
366fn parse_time(input: &mut &[u8]) -> ModalResult<Time> {
367    seq!(Time {
368        hours: le_u8.context(Label("hours")),
369        minutes: le_u8.context(Label("minutes")),
370        seconds: le_u8.context(Label("seconds")),
371        frames: le_u8.context(Label("frames")),
372    })
373    .context(Label("Time"))
374    .parse_next(input)
375}
376
377#[inline(always)]
378fn parse_tti_block<'a>(
379    cct: CharacterCodeTable,
380) -> impl ModalParser<&'a [u8], TtiBlock, ContextError> {
381    move |input: &mut &'a [u8]| {
382        // End of input is the only valid way to end the list of TTI blocks, so it is the only
383        // case that backtracks (which makes `repeat` stop).
384        if input.is_empty() {
385            return Err(ErrMode::Backtrack(winnow::error::ParserError::from_input(
386                input,
387            )));
388        }
389
390        // Any failure once a block has started, e.g. an invalid field value or a block shorter
391        // than 128 bytes, is a hard error. A backtrack here would make `repeat` silently drop
392        // this and all following blocks.
393        cut_err(seq!(TtiBlock {
394            sgn: le_u8.context(Label("sgn")),
395            sn: le_u16.context(Label("sn")),
396            ebn: le_u8.context(Label("ebn")),
397            cs: le_u8.try_map(CumulativeStatus::parse).context(Label("cs")),
398            tci: parse_time.context(Label("tci")),
399            tco: parse_time.context(Label("tco")),
400            vp: le_u8.context(Label("vp")),
401            jc: le_u8.context(Label("jc")),
402            cf: le_u8.context(Label("cf")),
403            tf: take(112_u16)
404                .map(|a: &[u8]| a.to_vec())
405                .context(Label("tf")),
406            cct: ().map(|_i| cct).context(Label("cct")),
407        }))
408        .context(Label("TtiBlock"))
409        .parse_next(input)
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use walkdir::WalkDir;
416
417    use super::*;
418
419    #[test]
420    fn test_parse_time() {
421        let ok = [0x1, 0x2, 0x3, 0x4];
422
423        let time = parse_time(&mut ok.as_slice()).unwrap();
424        println!("time {time:?}");
425        assert_eq!(
426            parse_time(&mut ok.as_slice()),
427            Ok(Time {
428                hours: 1,
429                minutes: 2,
430                seconds: 3,
431                frames: 4,
432            })
433        );
434    }
435
436    #[test]
437    fn parse_basic_file() {
438        let mut f = File::open("stls/test.stl").expect("Open stls/test.stl");
439        let mut buffer = vec![];
440        f.read_to_end(&mut buffer).expect("Read to end");
441
442        let stl = parse_stl_from_slice(&mut buffer.as_slice())
443            .map_err(|err| {
444                eprintln!("Error: {}", err);
445                err.to_string()
446            })
447            .expect("parse_stl_from_slice");
448        let stl2 = parse_stl_from_file("stls/test.stl").expect("parse_stl_from_file");
449
450        println!("STL:\n{:?}", stl);
451        assert_eq!(CodePageNumber::CPN_850, stl.gsi.cpn);
452        assert_eq!(1_u8, stl.gsi.tnd);
453        assert_eq!(1_u8, stl.gsi.dsn);
454        assert_eq!("TESTSUB 1.0.1                   ", stl.gsi.en);
455        assert_eq!(13, stl.ttis.len());
456        assert_eq!(
457            "    dans la baie de New York.\r\n",
458            stl.ttis.get(11).unwrap().text()
459        );
460
461        assert_eq!(stl, stl2);
462    }
463
464    fn roundtrip_file<P>(filename: P) -> Result<Stl, ParseError>
465    where
466        P: AsRef<Path>,
467        P: fmt::Debug,
468    {
469        let filepath = filename.as_ref();
470        let mut f = File::open(filepath).unwrap_or_else(|_| panic!("Open file {filepath:?}"));
471        let mut buffer = vec![];
472        f.read_to_end(&mut buffer).expect("Read to end");
473
474        let stl = parse_stl_from_slice(&mut buffer.as_slice())
475            .map_err(|err| {
476                eprintln!("Error: {}", err);
477                err.to_string()
478            })
479            .expect("Parse stl");
480
481        let mut serialized = stl.gsi.serialize().expect("Serialize GSI");
482        stl.ttis
483            .iter()
484            .for_each(|tti| serialized.append(&mut tti.serialize()));
485        assert_eq!(buffer, serialized);
486        Ok(stl)
487    }
488    #[test]
489    fn roundtrip_basic_file() {
490        roundtrip_file("stls/test.stl").expect("roundtrip stls/test.stl");
491    }
492
493    const GSI_SIZE: usize = 1024;
494    const TTI_SIZE: usize = 128;
495
496    fn read_test_file() -> Vec<u8> {
497        std::fs::read("stls/test.stl").expect("Read stls/test.stl")
498    }
499
500    #[test]
501    fn invalid_tti_block_is_an_error() {
502        let mut buffer = read_test_file();
503        // Cumulative Status (byte 4) of the fifth TTI block, only 00h-03h are valid
504        buffer[GSI_SIZE + 4 * TTI_SIZE + 4] = 0x07;
505
506        let err = parse_stl_from_slice(&mut buffer.as_slice())
507            .expect_err("invalid CS must not be silently skipped");
508        assert!(matches!(err, ErrMode::Cut(_)), "unexpected error {err:?}");
509    }
510
511    #[test]
512    fn truncated_tti_block_is_an_error() {
513        let buffer = read_test_file();
514        let truncated = &buffer[..buffer.len() - 10];
515
516        let err = parse_stl_from_slice(&mut &truncated[..])
517            .expect_err("truncated TTI block must not be silently dropped");
518        assert!(matches!(err, ErrMode::Cut(_)), "unexpected error {err:?}");
519    }
520
521    #[test]
522    fn trailing_data_is_an_error() {
523        let mut buffer = read_test_file();
524        // E.g. a DOS end-of-file marker
525        buffer.push(0x1A);
526
527        let err = parse_stl_from_slice(&mut buffer.as_slice())
528            .expect_err("trailing data must not be silently ignored");
529        assert!(matches!(err, ErrMode::Cut(_)), "unexpected error {err:?}");
530    }
531
532    #[test]
533    fn file_without_tti_blocks_is_valid() {
534        let buffer = read_test_file();
535        let mut input = &buffer[..GSI_SIZE];
536
537        let stl = parse_stl_from_slice(&mut input).expect("GSI block only");
538        assert!(stl.ttis.is_empty());
539        assert!(input.is_empty());
540        assert_eq!(CodePageNumber::CPN_850, stl.gsi.cpn);
541    }
542
543    #[test]
544    fn tnb_and_tns_above_u16_range() {
545        let mut buffer = read_test_file();
546        // TNB and TNS range 0-99 999
547        buffer[238..243].copy_from_slice(b"99999");
548        buffer[243..248].copy_from_slice(b"70000");
549
550        let stl = parse_stl_from_slice(&mut buffer.as_slice()).expect("parse TNB/TNS");
551        assert_eq!(99_999, stl.gsi.total_number_of_text_and_timing_blocks());
552        assert_eq!(70_000, stl.gsi.total_number_of_subtitles());
553
554        let serialized = stl.gsi.serialize().expect("Serialize GSI");
555        assert_eq!(&buffer[..GSI_SIZE], serialized.as_slice());
556    }
557
558    #[test]
559    fn negative_tnb_is_an_error() {
560        let mut buffer = read_test_file();
561        buffer[238..243].copy_from_slice(b"-0001");
562
563        assert!(parse_stl_from_slice(&mut buffer.as_slice()).is_err());
564    }
565
566    /// Parses stls/test.stl with the given GSI bytes replaced
567    fn parse_with_gsi_field(offset: usize, value: &[u8]) -> ModalResult<Stl> {
568        let mut buffer = read_test_file();
569        buffer[offset..offset + value.len()].copy_from_slice(value);
570        parse_stl_from_slice(&mut buffer.as_slice())
571    }
572
573    #[test]
574    fn space_padded_numeric_fields() {
575        let gsi = |offset, value| parse_with_gsi_field(offset, value).expect("parse").gsi;
576
577        assert_eq!(13, gsi(238, b"   13").tnb);
578        assert_eq!(13, gsi(238, b"13   ").tnb);
579        assert_eq!(12, gsi(243, b" 12  ").tns);
580        assert_eq!(1, gsi(248, b"  1").tng);
581        assert_eq!(9, gsi(251, b" 9").mnc);
582        assert_eq!(9, gsi(253, b"9 ").mnr);
583    }
584
585    #[test]
586    fn blank_numeric_fields_are_zero() {
587        let gsi = |offset, value| parse_with_gsi_field(offset, value).expect("parse").gsi;
588
589        assert_eq!(0, gsi(238, b"     ").tnb);
590        assert_eq!(0, gsi(243, b"     ").tns);
591        assert_eq!(0, gsi(248, b"   ").tng);
592        assert_eq!(0, gsi(251, b"  ").mnc);
593        assert_eq!(0, gsi(253, b"  ").mnr);
594    }
595
596    #[test]
597    fn blank_disk_numbers_are_one() {
598        let stl = parse_with_gsi_field(272, b"  ").expect("parse");
599
600        assert_eq!(1, stl.gsi.tnd);
601        assert_eq!(1, stl.gsi.dsn);
602    }
603
604    #[test]
605    fn invalid_numeric_fields_are_errors() {
606        assert!(parse_with_gsi_field(238, b"1 3  ").is_err());
607        assert!(parse_with_gsi_field(238, b"abcde").is_err());
608        assert!(parse_with_gsi_field(248, b"\0\0\0").is_err());
609        assert!(parse_with_gsi_field(251, b"-1").is_err());
610    }
611
612    #[test]
613    fn padded_and_blank_numeric_fields_are_written_zero_padded() {
614        let tnb_as_written = |value| {
615            let stl = parse_with_gsi_field(238, value).expect("parse");
616            stl.gsi.serialize().expect("Serialize GSI")[238..243].to_vec()
617        };
618
619        assert_eq!(b"00013", tnb_as_written(b"   13").as_slice());
620        assert_eq!(b"00000", tnb_as_written(b"     ").as_slice());
621    }
622
623    #[test]
624    fn blank_code_page_number_is_850() {
625        let stl = parse_with_gsi_field(0, b"   ").expect("parse");
626
627        assert_eq!(CodePageNumber::CPN_850, stl.gsi.cpn);
628        assert_eq!("TESTSUB 1.0.1                   ", stl.gsi.en);
629    }
630
631    #[test]
632    fn unknown_code_page_number_is_an_error() {
633        assert!(parse_with_gsi_field(0, b"999").is_err());
634    }
635
636    #[test]
637    fn blank_time_code_status_is_intended_for_use() {
638        let stl = parse_with_gsi_field(255, b" ").expect("parse");
639        assert_eq!(TimeCodeStatus::IntendedForUse, stl.gsi.tcs);
640
641        let stl = parse_with_gsi_field(255, b"0").expect("parse");
642        assert_eq!(TimeCodeStatus::NotIntendedForUse, stl.gsi.tcs);
643
644        assert!(parse_with_gsi_field(255, b"2").is_err());
645    }
646
647    #[test]
648    fn incomplete_gsi_block_is_an_error() {
649        let buffer = read_test_file();
650
651        assert!(parse_stl_from_slice(&mut &buffer[..GSI_SIZE - 1]).is_err());
652        assert!(parse_stl_from_slice(&mut &buffer[..0]).is_err());
653    }
654
655    #[test]
656    fn all_tti_blocks_are_parsed_and_input_consumed() {
657        let buffer = read_test_file();
658        let mut input = buffer.as_slice();
659
660        let stl = parse_stl_from_slice(&mut input).expect("parse stls/test.stl");
661        assert_eq!((buffer.len() - GSI_SIZE) / TTI_SIZE, stl.ttis.len());
662        assert!(input.is_empty());
663    }
664
665    #[test]
666    fn parse_error_from_file_reports_failure() {
667        let mut buffer = read_test_file();
668        buffer[GSI_SIZE + 4] = 0x07;
669        let path = std::env::temp_dir().join("ebustl_parser_invalid_cs.stl");
670        std::fs::write(&path, &buffer).expect("Write temp file");
671
672        let result = parse_stl_from_file(&path);
673        std::fs::remove_file(&path).ok();
674        assert!(matches!(result, Err(ParseError::WinnowParsingError { .. })));
675    }
676
677    // Test to test basic parsing against a non-public subtitle test file library
678    #[test]
679    fn test_local_file_library() -> Result<(), Box<dyn std::error::Error>> {
680        let Ok(base_folder) = std::env::var("EBUSTL_PARSER_STL_TEST_FILES") else {
681            return Ok(());
682        };
683
684        println!("Will walk {base_folder} and try to parse all stl files found");
685        for entry in WalkDir::new(base_folder).into_iter().filter_map(|e| e.ok()) {
686            let Some(filename) = entry.file_name().to_str() else {
687                continue;
688            };
689            if filename.starts_with('.') || !filename.to_lowercase().ends_with(".stl") {
690                continue;
691            }
692            println!("Roundtrip file {:?}", entry.path());
693            let stl = roundtrip_file(entry.path())?;
694            println!(
695                "Roundtripped file {:?} of codepage {:?}",
696                entry.path(),
697                stl.gsi.code_page_number()
698            );
699            // Real files deviate from the spec, so the issues are only reported
700            let issues = stl.validate();
701            println!(
702                "Test library file {filename}: {} validation issues",
703                issues.len()
704            );
705            if let Some(issue) = issues.first() {
706                println!("  e.g. {issue}");
707            }
708            // Every block belongs to exactly one subtitle
709            let blocks_in_subtitles: usize = stl.subtitles().map(|s| s.blocks().len()).sum();
710            assert_eq!(stl.ttis.len(), blocks_in_subtitles, "{:?}", entry.path());
711            if let Some(subtitle) = stl.subtitles().find(|s| s.blocks().len() > 1) {
712                println!(
713                    "Test library file {filename}: {} blocks in subtitle {}: {:?}",
714                    subtitle.blocks().len(),
715                    subtitle.subtitle_number(),
716                    subtitle.text()
717                );
718            }
719            if !stl.ttis.is_empty() {
720                let text = stl
721                    .ttis
722                    .iter()
723                    .find(|a| !a.text().is_empty())
724                    .map(|tti| tti.text())
725                    .unwrap_or_else(|| {
726                        panic!("{:?} doesn't have any non-empty text blocks", entry.path())
727                    });
728                let first_line = text
729                    .lines()
730                    .next()
731                    .unwrap_or_else(|| panic!("{:?} doesn't have a first text line", entry.path()));
732                println!("Test library file {filename}: {}", first_line);
733            }
734        }
735        Ok(())
736    }
737}