fix(subtitles): cap ASS \t nesting depth to prevent stack overflow

- guard override-block recursion in parseOverrideBlock at 8 levels; pathologically nested \t(...) tags no longer blow the call stack
- extract duplicate/animation-burst cue collapsing out of subtitle-cue-parser.ts into subtitle-cue-dedup.ts
- add regression tests for deep \t nesting, SRT/VTT brace stripping, and ASS content sniffed behind a lying .srt extension
This commit is contained in:
2026-08-05 00:30:05 -07:00
parent 64534299ed
commit c305bf34c2
6 changed files with 253 additions and 182 deletions
+13 -3
View File
@@ -175,7 +175,17 @@ function readCommandArgs(block: string, start: number): { args: string; next: nu
return { args: block.slice(start, end), next: end };
}
function parseOverrideBlock(block: string, animated: boolean, into: AssOverrideCommand[]): void {
// `\t(...)` can wrap another `\t(...)`, and nothing in the format stops an author (or a
// malformed file) from nesting them thousands deep. Real typesetting never goes past one
// or two levels, so stop recursing well before the call stack is at risk.
const MAX_ANIMATION_NESTING_DEPTH = 8;
function parseOverrideBlock(
block: string,
animated: boolean,
into: AssOverrideCommand[],
depth = 0,
): void {
let cursor = 0;
while (cursor < block.length) {
@@ -195,8 +205,8 @@ function parseOverrideBlock(block: string, animated: boolean, into: AssOverrideC
const { args, next } = readCommandArgs(block, cursor + 1 + name.length);
into.push({ name, args: args.trim(), animated });
// `\t(0,500,\frz30)` animates whatever it wraps, so record the inner tags too.
if (name === 't' && args.includes('\\')) {
parseOverrideBlock(args, true, into);
if (name === 't' && args.includes('\\') && depth < MAX_ANIMATION_NESTING_DEPTH) {
parseOverrideBlock(args, true, into, depth + 1);
}
cursor = next;
}