use std::io;
use std::str::FromStr;
use codepage_strings::ConvertError;
use thiserror::Error;
use winnow::{
self, ModalParser, ModalResult, Parser,
binary::{le_u8, le_u16},
combinator::{cut_err, repeat, trace},
error::{ContextError, ErrMode, FromExternalError, ParserError, StrContext::Label},
seq,
stream::ToUsize,
token::take,
};
use super::*;
use crate::gsi::CodePageCodec;
#[derive(Debug, Error)]
pub enum ParseError {
#[error(transparent)]
IoError(#[from] io::Error),
#[error("Error parsing, file may be incomplete or corrupted")]
Incomplete,
#[error("Unknown Code Page Number: {0}")]
CodePageNumber(u16),
#[error("Error parsing Display Standard Code")]
DisplayStandardCode,
#[error("Error parsing Time Code Status")]
TimeCodeStatus,
#[error("Error parsing Disk Format Code: {0}")]
DiskFormatCode(String),
#[error("Error parsing Character Code Table")]
CharacterCodeTable,
#[error("Error parsing Cumulative Status")]
CumulativeStatus,
#[error("Parse error: {message}")]
WinnowParsingError {
message: String,
},
#[error("Failed to encode string '{value}' using codepage {codepage}: {source}")]
CodePageEncoding {
codepage: u16,
value: String,
source: ConvertError,
},
#[error("Subtitle text needs {blocks} TTI blocks, at most 241 are allowed")]
SubtitleTextTooLong {
blocks: usize,
},
#[error("Total Number of TTI Blocks would exceed 99 999")]
TooManyTtiBlocks,
#[error("Subtitle Number would exceed FFFFh")]
SubtitleNumberOverflow,
#[error("{0}")]
InvalidFieldValue(String),
}
impl<T> From<InvalidFieldValue<T>> for ParseError {
fn from(err: InvalidFieldValue<T>) -> Self {
ParseError::InvalidFieldValue(err.to_string())
}
}
impl<E> From<ErrMode<E>> for ParseError
where
E: fmt::Display,
{
fn from(err: ErrMode<E>) -> Self {
match err {
ErrMode::Incomplete(_) => ParseError::Incomplete,
ErrMode::Backtrack(e) | ErrMode::Cut(e) => Self::WinnowParsingError {
message: e.to_string(),
},
}
}
}
pub fn parse_stl_from_slice(input: &mut &[u8]) -> ModalResult<Stl> {
let gsi = parse_gsi_block(input)?;
let ttis = repeat(0.., parse_tti_block(gsi.cct)).parse_next(input)?;
Ok(Stl { gsi, ttis })
}
#[inline(always)]
fn take_str<'a, C, Error: ParserError<&'a [u8]>>(
count: C,
) -> impl ModalParser<&'a [u8], &'a str, Error>
where
C: ToUsize,
{
let c = count.to_usize();
move |i: &mut &'a [u8]| {
let first = take(c).parse_next(i)?;
str::from_utf8(first).map_err(|_err| ErrMode::Backtrack(Error::from_input(i)))
}
}
const DEFAULT_CODE_PAGE: u16 = 850;
fn number_with_default_if_blank<T: FromStr>(input: &str, default: T) -> Result<T, T::Err> {
let trimmed = input.trim_matches(' ');
if trimmed.is_empty() {
Ok(default)
} else {
trimmed.parse()
}
}
fn time_code_status_with_default_if_blank(data: u8) -> Result<TimeCodeStatus, ParseError> {
if data == b' ' {
Ok(TimeCodeStatus::IntendedForUse)
} else {
TimeCodeStatus::parse(data)
}
}
fn parse_gsi_block(input: &mut &[u8]) -> ModalResult<GsiBlock> {
let codepage: u16 = trace(
"codepage",
take_str(3_u16)
.try_map(|data| number_with_default_if_blank(data, DEFAULT_CODE_PAGE))
.context(Label("codepage")),
)
.parse_next(input)?;
let cpn = CodePageNumber::from_u16(codepage)
.map_err(|err| ErrMode::from_external_error(&input, err))?;
let coding =
CodePageCodec::new(codepage).map_err(|err| ErrMode::from_external_error(&input, err))?;
let dfc = take_str(10 - 3 + 1_u16)
.try_map(DiskFormatCode::parse)
.context(Label("dfc"))
.parse_next(input)?;
let dsc = le_u8
.try_map(DisplayStandardCode::parse)
.context(Label("dsc"))
.parse_next(input)?;
let cct = take(13 - 12 + 1_u16)
.try_map(CharacterCodeTable::parse)
.context(Label("cct"))
.parse_next(input)?;
let lc = take(15 - 14 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("lc"))
.parse_next(input)?;
let opt = take(47 - 16 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("opt"))
.parse_next(input)?;
let oet = take(79 - 48 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("oet"))
.parse_next(input)?;
let tpt = take(111 - 80 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("tpt"))
.parse_next(input)?;
let tet = take(143 - 112 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("tet"))
.parse_next(input)?;
let tn = take(175 - 144 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("tn"))
.parse_next(input)?;
let tcd = take(207 - 176 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("tcd"))
.parse_next(input)?;
let slr = take(223 - 208 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("slr"))
.parse_next(input)?;
let cd = take(229 - 224 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("cd"))
.parse_next(input)?;
let rd = take(235 - 230 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("rd"))
.parse_next(input)?;
let rn = take(237 - 236 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("rn"))
.parse_next(input)?;
let tnb = take_str(242 - 238 + 1_u16)
.try_map(|data| number_with_default_if_blank(data, 0_u32))
.context(Label("tnb"))
.parse_next(input)?;
let tns = take_str(247 - 243 + 1_u16)
.try_map(|data| number_with_default_if_blank(data, 0_u32))
.context(Label("tns"))
.parse_next(input)?;
let tng = take_str(250 - 248 + 1_u16)
.try_map(|data| number_with_default_if_blank(data, 0_u16))
.context(Label("tng"))
.parse_next(input)?;
let mnc = take_str(252 - 251 + 1_u16)
.try_map(|data| number_with_default_if_blank(data, 0_u16))
.context(Label("mnc"))
.parse_next(input)?;
let mnr = take_str(254 - 253 + 1_u16)
.try_map(|data| number_with_default_if_blank(data, 0_u16))
.context(Label("mnr"))
.parse_next(input)?;
let tcs = le_u8
.try_map(time_code_status_with_default_if_blank)
.context(Label("tcs"))
.parse_next(input)?;
let tcp = take(263 - 256 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("tcp"))
.parse_next(input)?;
let tcf = take(271 - 264 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("tcf"))
.parse_next(input)?;
let tnd = take_str(1_u16)
.try_map(|data| number_with_default_if_blank(data, 1_u8))
.context(Label("tnd"))
.parse_next(input)?;
let dsn = take_str(1_u16)
.try_map(|data| number_with_default_if_blank(data, 1_u8))
.context(Label("dns"))
.parse_next(input)?;
let co = take(276 - 274 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("co"))
.parse_next(input)?;
let pub_ = take(308 - 277 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("pub_"))
.parse_next(input)?;
let en = take(340 - 309 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("en"))
.parse_next(input)?;
let ecd = take(372 - 341 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("ecd"))
.parse_next(input)?;
let _spare = take(447 - 373 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("_spare"))
.parse_next(input)?;
let uda = take(1023 - 448 + 1_u16)
.try_map(|data| coding.decode(data))
.context(Label("uda"))
.parse_next(input)?;
Ok(GsiBlock {
cpn,
dfc,
dsc,
cct,
lc,
opt,
oet,
tpt,
tet,
tn,
tcd,
slr,
cd,
rd,
rn,
tnb,
tns,
tng,
mnc,
mnr,
tcs,
tcp,
tcf,
tnd,
dsn,
co,
pub_,
en,
ecd,
_spare,
uda,
})
}
fn parse_time(input: &mut &[u8]) -> ModalResult<Time> {
seq!(Time {
hours: le_u8.context(Label("hours")),
minutes: le_u8.context(Label("minutes")),
seconds: le_u8.context(Label("seconds")),
frames: le_u8.context(Label("frames")),
})
.context(Label("Time"))
.parse_next(input)
}
#[inline(always)]
fn parse_tti_block<'a>(
cct: CharacterCodeTable,
) -> impl ModalParser<&'a [u8], TtiBlock, ContextError> {
move |input: &mut &'a [u8]| {
if input.is_empty() {
return Err(ErrMode::Backtrack(winnow::error::ParserError::from_input(
input,
)));
}
cut_err(seq!(TtiBlock {
sgn: le_u8.context(Label("sgn")),
sn: le_u16.context(Label("sn")),
ebn: le_u8.context(Label("ebn")),
cs: le_u8.try_map(CumulativeStatus::parse).context(Label("cs")),
tci: parse_time.context(Label("tci")),
tco: parse_time.context(Label("tco")),
vp: le_u8.context(Label("vp")),
jc: le_u8.context(Label("jc")),
cf: le_u8.context(Label("cf")),
tf: take(112_u16)
.map(|a: &[u8]| a.to_vec())
.context(Label("tf")),
cct: ().map(|_i| cct).context(Label("cct")),
}))
.context(Label("TtiBlock"))
.parse_next(input)
}
}
#[cfg(test)]
mod tests {
use walkdir::WalkDir;
use super::*;
#[test]
fn test_parse_time() {
let ok = [0x1, 0x2, 0x3, 0x4];
let time = parse_time(&mut ok.as_slice()).unwrap();
println!("time {time:?}");
assert_eq!(
parse_time(&mut ok.as_slice()),
Ok(Time {
hours: 1,
minutes: 2,
seconds: 3,
frames: 4,
})
);
}
#[test]
fn parse_basic_file() {
let mut f = File::open("stls/test.stl").expect("Open stls/test.stl");
let mut buffer = vec![];
f.read_to_end(&mut buffer).expect("Read to end");
let stl = parse_stl_from_slice(&mut buffer.as_slice())
.map_err(|err| {
eprintln!("Error: {}", err);
err.to_string()
})
.expect("parse_stl_from_slice");
let stl2 = parse_stl_from_file("stls/test.stl").expect("parse_stl_from_file");
println!("STL:\n{:?}", stl);
assert_eq!(CodePageNumber::CPN_850, stl.gsi.cpn);
assert_eq!(1_u8, stl.gsi.tnd);
assert_eq!(1_u8, stl.gsi.dsn);
assert_eq!("TESTSUB 1.0.1 ", stl.gsi.en);
assert_eq!(13, stl.ttis.len());
assert_eq!(
" dans la baie de New York.\r\n",
stl.ttis.get(11).unwrap().text()
);
assert_eq!(stl, stl2);
}
fn roundtrip_file<P>(filename: P) -> Result<Stl, ParseError>
where
P: AsRef<Path>,
P: fmt::Debug,
{
let filepath = filename.as_ref();
let mut f = File::open(filepath).unwrap_or_else(|_| panic!("Open file {filepath:?}"));
let mut buffer = vec![];
f.read_to_end(&mut buffer).expect("Read to end");
let stl = parse_stl_from_slice(&mut buffer.as_slice())
.map_err(|err| {
eprintln!("Error: {}", err);
err.to_string()
})
.expect("Parse stl");
let mut serialized = stl.gsi.serialize().expect("Serialize GSI");
stl.ttis
.iter()
.for_each(|tti| serialized.append(&mut tti.serialize()));
assert_eq!(buffer, serialized);
Ok(stl)
}
#[test]
fn roundtrip_basic_file() {
roundtrip_file("stls/test.stl").expect("roundtrip stls/test.stl");
}
const GSI_SIZE: usize = 1024;
const TTI_SIZE: usize = 128;
fn read_test_file() -> Vec<u8> {
std::fs::read("stls/test.stl").expect("Read stls/test.stl")
}
#[test]
fn invalid_tti_block_is_an_error() {
let mut buffer = read_test_file();
buffer[GSI_SIZE + 4 * TTI_SIZE + 4] = 0x07;
let err = parse_stl_from_slice(&mut buffer.as_slice())
.expect_err("invalid CS must not be silently skipped");
assert!(matches!(err, ErrMode::Cut(_)), "unexpected error {err:?}");
}
#[test]
fn truncated_tti_block_is_an_error() {
let buffer = read_test_file();
let truncated = &buffer[..buffer.len() - 10];
let err = parse_stl_from_slice(&mut &truncated[..])
.expect_err("truncated TTI block must not be silently dropped");
assert!(matches!(err, ErrMode::Cut(_)), "unexpected error {err:?}");
}
#[test]
fn trailing_data_is_an_error() {
let mut buffer = read_test_file();
buffer.push(0x1A);
let err = parse_stl_from_slice(&mut buffer.as_slice())
.expect_err("trailing data must not be silently ignored");
assert!(matches!(err, ErrMode::Cut(_)), "unexpected error {err:?}");
}
#[test]
fn file_without_tti_blocks_is_valid() {
let buffer = read_test_file();
let mut input = &buffer[..GSI_SIZE];
let stl = parse_stl_from_slice(&mut input).expect("GSI block only");
assert!(stl.ttis.is_empty());
assert!(input.is_empty());
assert_eq!(CodePageNumber::CPN_850, stl.gsi.cpn);
}
#[test]
fn tnb_and_tns_above_u16_range() {
let mut buffer = read_test_file();
buffer[238..243].copy_from_slice(b"99999");
buffer[243..248].copy_from_slice(b"70000");
let stl = parse_stl_from_slice(&mut buffer.as_slice()).expect("parse TNB/TNS");
assert_eq!(99_999, stl.gsi.total_number_of_text_and_timing_blocks());
assert_eq!(70_000, stl.gsi.total_number_of_subtitles());
let serialized = stl.gsi.serialize().expect("Serialize GSI");
assert_eq!(&buffer[..GSI_SIZE], serialized.as_slice());
}
#[test]
fn negative_tnb_is_an_error() {
let mut buffer = read_test_file();
buffer[238..243].copy_from_slice(b"-0001");
assert!(parse_stl_from_slice(&mut buffer.as_slice()).is_err());
}
fn parse_with_gsi_field(offset: usize, value: &[u8]) -> ModalResult<Stl> {
let mut buffer = read_test_file();
buffer[offset..offset + value.len()].copy_from_slice(value);
parse_stl_from_slice(&mut buffer.as_slice())
}
#[test]
fn space_padded_numeric_fields() {
let gsi = |offset, value| parse_with_gsi_field(offset, value).expect("parse").gsi;
assert_eq!(13, gsi(238, b" 13").tnb);
assert_eq!(13, gsi(238, b"13 ").tnb);
assert_eq!(12, gsi(243, b" 12 ").tns);
assert_eq!(1, gsi(248, b" 1").tng);
assert_eq!(9, gsi(251, b" 9").mnc);
assert_eq!(9, gsi(253, b"9 ").mnr);
}
#[test]
fn blank_numeric_fields_are_zero() {
let gsi = |offset, value| parse_with_gsi_field(offset, value).expect("parse").gsi;
assert_eq!(0, gsi(238, b" ").tnb);
assert_eq!(0, gsi(243, b" ").tns);
assert_eq!(0, gsi(248, b" ").tng);
assert_eq!(0, gsi(251, b" ").mnc);
assert_eq!(0, gsi(253, b" ").mnr);
}
#[test]
fn blank_disk_numbers_are_one() {
let stl = parse_with_gsi_field(272, b" ").expect("parse");
assert_eq!(1, stl.gsi.tnd);
assert_eq!(1, stl.gsi.dsn);
}
#[test]
fn invalid_numeric_fields_are_errors() {
assert!(parse_with_gsi_field(238, b"1 3 ").is_err());
assert!(parse_with_gsi_field(238, b"abcde").is_err());
assert!(parse_with_gsi_field(248, b"\0\0\0").is_err());
assert!(parse_with_gsi_field(251, b"-1").is_err());
}
#[test]
fn padded_and_blank_numeric_fields_are_written_zero_padded() {
let tnb_as_written = |value| {
let stl = parse_with_gsi_field(238, value).expect("parse");
stl.gsi.serialize().expect("Serialize GSI")[238..243].to_vec()
};
assert_eq!(b"00013", tnb_as_written(b" 13").as_slice());
assert_eq!(b"00000", tnb_as_written(b" ").as_slice());
}
#[test]
fn blank_code_page_number_is_850() {
let stl = parse_with_gsi_field(0, b" ").expect("parse");
assert_eq!(CodePageNumber::CPN_850, stl.gsi.cpn);
assert_eq!("TESTSUB 1.0.1 ", stl.gsi.en);
}
#[test]
fn unknown_code_page_number_is_an_error() {
assert!(parse_with_gsi_field(0, b"999").is_err());
}
#[test]
fn blank_time_code_status_is_intended_for_use() {
let stl = parse_with_gsi_field(255, b" ").expect("parse");
assert_eq!(TimeCodeStatus::IntendedForUse, stl.gsi.tcs);
let stl = parse_with_gsi_field(255, b"0").expect("parse");
assert_eq!(TimeCodeStatus::NotIntendedForUse, stl.gsi.tcs);
assert!(parse_with_gsi_field(255, b"2").is_err());
}
#[test]
fn incomplete_gsi_block_is_an_error() {
let buffer = read_test_file();
assert!(parse_stl_from_slice(&mut &buffer[..GSI_SIZE - 1]).is_err());
assert!(parse_stl_from_slice(&mut &buffer[..0]).is_err());
}
#[test]
fn all_tti_blocks_are_parsed_and_input_consumed() {
let buffer = read_test_file();
let mut input = buffer.as_slice();
let stl = parse_stl_from_slice(&mut input).expect("parse stls/test.stl");
assert_eq!((buffer.len() - GSI_SIZE) / TTI_SIZE, stl.ttis.len());
assert!(input.is_empty());
}
#[test]
fn parse_error_from_file_reports_failure() {
let mut buffer = read_test_file();
buffer[GSI_SIZE + 4] = 0x07;
let path = std::env::temp_dir().join("ebustl_parser_invalid_cs.stl");
std::fs::write(&path, &buffer).expect("Write temp file");
let result = parse_stl_from_file(&path);
std::fs::remove_file(&path).ok();
assert!(matches!(result, Err(ParseError::WinnowParsingError { .. })));
}
#[test]
fn test_local_file_library() -> Result<(), Box<dyn std::error::Error>> {
let Ok(base_folder) = std::env::var("EBUSTL_PARSER_STL_TEST_FILES") else {
return Ok(());
};
println!("Will walk {base_folder} and try to parse all stl files found");
for entry in WalkDir::new(base_folder).into_iter().filter_map(|e| e.ok()) {
let Some(filename) = entry.file_name().to_str() else {
continue;
};
if filename.starts_with('.') || !filename.to_lowercase().ends_with(".stl") {
continue;
}
println!("Roundtrip file {:?}", entry.path());
let stl = roundtrip_file(entry.path())?;
println!(
"Roundtripped file {:?} of codepage {:?}",
entry.path(),
stl.gsi.code_page_number()
);
let issues = stl.validate();
println!(
"Test library file {filename}: {} validation issues",
issues.len()
);
if let Some(issue) = issues.first() {
println!(" e.g. {issue}");
}
let blocks_in_subtitles: usize = stl.subtitles().map(|s| s.blocks().len()).sum();
assert_eq!(stl.ttis.len(), blocks_in_subtitles, "{:?}", entry.path());
if let Some(subtitle) = stl.subtitles().find(|s| s.blocks().len() > 1) {
println!(
"Test library file {filename}: {} blocks in subtitle {}: {:?}",
subtitle.blocks().len(),
subtitle.subtitle_number(),
subtitle.text()
);
}
if !stl.ttis.is_empty() {
let text = stl
.ttis
.iter()
.find(|a| !a.text().is_empty())
.map(|tti| tti.text())
.unwrap_or_else(|| {
panic!("{:?} doesn't have any non-empty text blocks", entry.path())
});
let first_line = text
.lines()
.next()
.unwrap_or_else(|| panic!("{:?} doesn't have a first text line", entry.path()));
println!("Test library file {filename}: {}", first_line);
}
}
Ok(())
}
}