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#[derive(Debug, Error)]
21pub enum ParseError {
22 #[error(transparent)]
24 IoError(#[from] io::Error),
25 #[error("Error parsing, file may be incomplete or corrupted")]
28 Incomplete,
29 #[error("Unknown Code Page Number: {0}")]
31 CodePageNumber(u16),
32 #[error("Error parsing Display Standard Code")]
34 DisplayStandardCode,
35 #[error("Error parsing Time Code Status")]
37 TimeCodeStatus,
38 #[error("Error parsing Disk Format Code: {0}")]
40 DiskFormatCode(String),
41 #[error("Error parsing Character Code Table")]
43 CharacterCodeTable,
44 #[error("Error parsing Cumulative Status")]
46 CumulativeStatus,
47 #[error("Parse error: {message}")]
49 WinnowParsingError {
50 message: String,
52 },
53 #[error("Failed to encode string '{value}' using codepage {codepage}: {source}")]
55 CodePageEncoding {
56 codepage: u16,
58 value: String,
60 source: ConvertError,
62 },
63 #[error("Subtitle text needs {blocks} TTI blocks, at most 241 are allowed")]
65 SubtitleTextTooLong {
66 blocks: usize,
68 },
69 #[error("Total Number of TTI Blocks would exceed 99 999")]
71 TooManyTtiBlocks,
72 #[error("Subtitle Number would exceed FFFFh")]
74 SubtitleNumberOverflow,
75 #[error("{0}")]
78 InvalidFieldValue(String),
79}
80
81impl<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
102pub fn parse_stl_from_slice(input: &mut &[u8]) -> ModalResult<Stl> {
121 let gsi = parse_gsi_block(input)?;
122 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
141const DEFAULT_CODE_PAGE: u16 = 850;
143
144fn 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
155fn 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 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 if input.is_empty() {
385 return Err(ErrMode::Backtrack(winnow::error::ParserError::from_input(
386 input,
387 )));
388 }
389
390 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 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 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 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 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]
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 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 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}