fix(captions): reject overflowing TTML times and round extreme clocks safely
This commit is contained in:
parent
98ba9acbce
commit
5763460a62
|
|
@ -307,7 +307,8 @@ pub fn parse_clock(s: &str) -> Option<Tick> {
|
|||
|
||||
/// Format ticks as `HH:MM:SS<sep>mmm` (rounded to the nearest millisecond).
|
||||
pub fn format_clock(t: Tick, sep: char) -> String {
|
||||
let ms = (t.0.max(0) + TICKS_PER_MS / 2) / TICKS_PER_MS;
|
||||
// The rounding addition must not overflow for an input-derived Tick near i64::MAX.
|
||||
let ms = (i128::from(t.0.max(0)) + i128::from(TICKS_PER_MS) / 2) / i128::from(TICKS_PER_MS);
|
||||
let (h, m, s, f) = (ms / 3_600_000, (ms / 60_000) % 60, (ms / 1000) % 60, ms % 1000);
|
||||
format!("{h:02}:{m:02}:{s:02}{sep}{f:03}")
|
||||
}
|
||||
|
|
@ -368,6 +369,8 @@ mod tests {
|
|||
assert_eq!(parse_clock("00:61:00,000"), None);
|
||||
assert_eq!(parse_clock("aa:00:00,000"), None);
|
||||
assert_eq!(format_clock(Tick(TICKS_PER_SECOND * 3723 + TICKS_PER_MS * 45), ','), "01:02:03,045");
|
||||
assert!(format_clock(Tick(i64::MAX), ',').ends_with(",077"));
|
||||
assert_eq!(format_clock(Tick(i64::MIN), '.'), "00:00:00.000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -252,6 +252,10 @@ fn parse_time(s: &str, tm: Timing) -> Option<Tick> {
|
|||
let (h, m) = (num(parts.first()?)?, num(parts.get(1)?)?);
|
||||
let sec_part = parts.get(2)?;
|
||||
let (si, sf) = sec_part.split_once('.').unwrap_or((sec_part, ""));
|
||||
let seconds = num(si)?;
|
||||
if m >= 60 || seconds >= 60 {
|
||||
return None;
|
||||
}
|
||||
let mut t = (h * 3600 + m * 60) * tps + decimal_ticks(si, sf, tps)?;
|
||||
if let Some(fp) = parts.get(3) {
|
||||
if parts.len() > 4 {
|
||||
|
|
@ -259,13 +263,15 @@ fn parse_time(s: &str, tm: Timing) -> Option<Tick> {
|
|||
}
|
||||
let (fi, fs) = fp.split_once('.').unwrap_or((fp, ""));
|
||||
let frames = num(fi)?;
|
||||
let _ = tm.base;
|
||||
let sub: i128 = if fs.is_empty() { 0 } else { num(fs)? };
|
||||
if frames >= i128::from(tm.base) || sub >= i128::from(tm.sub_frame_rate) {
|
||||
return None;
|
||||
}
|
||||
// frames at the effective rate; sub-frames divide a frame
|
||||
let ft = tm.rate.tick_of(1).0 as i128;
|
||||
t += frames * ft + sub * ft / tm.sub_frame_rate.max(1) as i128;
|
||||
}
|
||||
return Some(Tick(t as i64));
|
||||
return i64::try_from(t).ok().map(Tick);
|
||||
}
|
||||
let unit_start = s.find(|c: char| c.is_ascii_alphabetic())?;
|
||||
let (v, unit) = s.split_at(unit_start);
|
||||
|
|
@ -290,7 +296,7 @@ fn parse_time(s: &str, tm: Timing) -> Option<Tick> {
|
|||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(Tick(t as i64))
|
||||
i64::try_from(t).ok().map(Tick)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, PartialEq)]
|
||||
|
|
@ -552,6 +558,33 @@ mod tests {
|
|||
assert_eq!(parse_time("1:2", t), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_and_overflowed_time_expressions_are_ignored() {
|
||||
let normal = tm(FrameRate::FPS_25);
|
||||
for expr in [
|
||||
"999999999999999h",
|
||||
"999999999999999m",
|
||||
"999999999999999s",
|
||||
"999999999999999ms",
|
||||
"999999999999999f",
|
||||
"999999999999999t",
|
||||
"00:60:00",
|
||||
"00:00:60",
|
||||
"00:00:01:25",
|
||||
] {
|
||||
assert_eq!(parse_time(expr, normal), None, "{expr}");
|
||||
}
|
||||
let subframes = Timing { sub_frame_rate: 2, ..normal };
|
||||
assert!(parse_time("00:00:01:12.1", subframes).is_some());
|
||||
assert_eq!(parse_time("00:00:01:12.2", subframes), None);
|
||||
// Bad cues must not wrap to a plausible but unrelated timeline position.
|
||||
let xml = r#"<tt xmlns="http://www.w3.org/ns/ttml"><body><div><p begin="999999999999999s" end="2s">Bad</p><p begin="1s" end="2s">Good</p></div></body></tt>"#;
|
||||
let doc = parse(xml).expect("well-formed TTML");
|
||||
assert_eq!(doc.cues.len(), 1);
|
||||
assert_eq!(doc.cues[0].text, "Good");
|
||||
assert!(!doc.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_styles_spans_and_nesting() {
|
||||
let x = r#"<?xml version="1.0"?>
|
||||
|
|
|
|||
Loading…
Reference in New Issue