PR #25296 opened by Marti2203
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25296
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25296.patch
When an ASS event has an override block without a closing brace, e.g. `{\b1
hello world`, `ff_ass_split_override_codes()` calls the style callback for
`\b1` and then returns `AVERROR_INVALIDDATA` without passing on the rest of the
text or calling the end callback. srtenc, webvttenc and movtextenc ignore the
return value, so converting such a script loses the text without any message,
and in srt/webvtt the closing tags for the half-open style end up in the next
cue. ttmlenc warns and writes an empty cue.
libass treats `{` as the start of an override block only if a `}` follows it
(`if ((*p == '{') && (q = strchr(p, '}')))` in `ass_render.c`) and otherwise
renders it as text. This patch makes the splitter do the same, so the fix lives
in one place and all four encoders behave the same way.
Reproducer (`repro.sh` attached in the drafts, it writes this script itself):
```
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,{\b1 hello world
Dialogue: 0,0:00:03.00,0:00:04.00,Default,,0,0,0,,{\i1}second cue
```
```
ffmpeg -i in.ass -f srt -
```
Before:
```
1
00:00:01,000 --> 00:00:02,000
<font size="20"><b>
2
00:00:03,000 --> 00:00:04,000
<font size="20"><i>second cue</i></font></b></font>
```
After:
```
1
00:00:01,000 --> 00:00:02,000
<font size="20">{\b1 hello world</font>
2
00:00:03,000 --> 00:00:04,000
<font size="20"><i>second cue</i></font>
```
WebVTT changes the same way (`<b>` followed by `<i>second cue</i></b>` before,
`{\b1 hello world` followed by `<i>second cue</i>` after). With `-c:s mov_text`
the first cue was missing from the output entirely and is now kept. ttmlenc
printed "Splitting received ASS dialog text {\b1 hello world failed: Invalid
data found when processing input" and wrote an empty `<span>`; now the span
holds the text.
Testing: the second commit adds `tests/sub-unterminated-override.ass`, a small
inline script like the existing `tests/sub-karaoke.ass`, with
`fate-sub-unterminated-override-srtenc` and `-webvttenc`. Both fail without the
fix and pass with it. `make fate-subtitles` (53 tests, run with the `sub/` part
of fate-suite) passes, and so do `fate-sub-karaoke-webvttenc` and
`fate-sub-karaoke-webvttenc-ss`.
Caveats: an override block that does have a closing brace but contains text the
parser can't handle, e.g. `{\b1 x}text`, still makes the function return an
error, and the encoders still drop that text without a message. libass would
apply `\b1` and skip the junk. I kept this patch to the unterminated case.
Another option would be to make the encoders check the return value, but then
one malformed line would abort the whole conversion. I can extend the patch
either way if preferred.
This issue was found by an LLM-Enhanced Static Analysis Engine and verified
manually.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
From 05bdcb955495f9e301e0eb9b40dd88c8e0a4edf6 Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Wed, 21 Oct 2026 18:16:24 +0800
Subject: [PATCH 1/2] avcodec/ass_split: treat an unterminated override block
as text
An event such as "{\b1 hello world" has no closing brace.
ff_ass_split_override_codes() called the style callback for \b1, then
returned AVERROR_INVALIDDATA without emitting the rest of the line or
calling the end callback. The srt, webvtt and mov_text encoders ignore
that return value, so the cue was written as an opening "<b>" without
its text and the closing tags were appended to the next cue
(srt/webvtt), or the cue was lost entirely (mov_text). ttmlenc warns
and writes an empty cue (or fails with -err_detect explode).
libass only parses '{' as the start of an override block if a '}'
follows it and renders it as text otherwise; do the same.
Signed-off-by: Martin Mirchev <[email protected]>
---
libavcodec/ass_split.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/libavcodec/ass_split.c b/libavcodec/ass_split.c
index 5eb96072ebe..5d19351eccc 100644
--- a/libavcodec/ass_split.c
+++ b/libavcodec/ass_split.c
@@ -488,9 +488,10 @@ int ff_ass_split_override_codes(const ASSCodesCallbacks
*callbacks, void *priv,
int text_len = 0;
while (buf && *buf) {
+ /* like libass, treat an override block without a closing brace as
text */
+ int override = !strncmp(buf, "{\\", 2) && strchr(buf, '}');
if (text && callbacks->text &&
- (sscanf(buf, "\\%1[nN]", new_line) == 1 ||
- !strncmp(buf, "{\\", 2))) {
+ (sscanf(buf, "\\%1[nN]", new_line) == 1 || override)) {
callbacks->text(priv, text, text_len);
text = NULL;
}
@@ -498,7 +499,7 @@ int ff_ass_split_override_codes(const ASSCodesCallbacks
*callbacks, void *priv,
if (callbacks->new_line)
callbacks->new_line(priv, new_line[0] == 'N');
buf += 2;
- } else if (!strncmp(buf, "{\\", 2)) {
+ } else if (override) {
buf++;
while (*buf == '\\') {
char style[2], c[2], sep[2], c_num[2] = "0", tmp[128] = {0};
--
2.54.0
From 12d75b4be674fae97da91fc5f515b61d744c1f5b Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Wed, 21 Oct 2026 18:16:48 +0800
Subject: [PATCH 2/2] tests/fate/subtitles: add tests for unterminated ASS
override blocks
Check that the srt and webvtt encoders keep the text of an event whose
override block has no closing brace, and that no tags leak into the
following cues.
Signed-off-by: Martin Mirchev <[email protected]>
---
tests/fate/subtitles.mak | 10 ++++++++--
tests/ref/fate/sub-unterminated-override-srtenc | 12 ++++++++++++
tests/ref/fate/sub-unterminated-override-webvttenc | 10 ++++++++++
tests/sub-unterminated-override.ass | 14 ++++++++++++++
4 files changed, 44 insertions(+), 2 deletions(-)
create mode 100644 tests/ref/fate/sub-unterminated-override-srtenc
create mode 100644 tests/ref/fate/sub-unterminated-override-webvttenc
create mode 100644 tests/sub-unterminated-override.ass
diff --git a/tests/fate/subtitles.mak b/tests/fate/subtitles.mak
index 3ded96e044f..77376af5fb5 100644
--- a/tests/fate/subtitles.mak
+++ b/tests/fate/subtitles.mak
@@ -118,6 +118,12 @@ fate-sub-karaoke-webvttenc: CMD = fmtstdout webvtt -i
$(SRC_PATH)/tests/sub-kara
FATE_SUB_KARAOKE-$(call ALLYES, FILE_PROTOCOL PIPE_PROTOCOL ASS_DEMUXER
ASS_DECODER WEBVTT_ENCODER WEBVTT_MUXER) += fate-sub-karaoke-webvttenc-ss
fate-sub-karaoke-webvttenc-ss: CMD = fmtstdout webvtt -i
$(SRC_PATH)/tests/sub-karaoke.ass -ss 12
+FATE_SUB_UNTERMINATED-$(call ALLYES, FILE_PROTOCOL PIPE_PROTOCOL ASS_DEMUXER
ASS_DECODER SRT_ENCODER SRT_MUXER) += fate-sub-unterminated-override-srtenc
+fate-sub-unterminated-override-srtenc: CMD = fmtstdout srt -i
$(SRC_PATH)/tests/sub-unterminated-override.ass
+
+FATE_SUB_UNTERMINATED-$(call ALLYES, FILE_PROTOCOL PIPE_PROTOCOL ASS_DEMUXER
ASS_DECODER WEBVTT_ENCODER WEBVTT_MUXER) +=
fate-sub-unterminated-override-webvttenc
+fate-sub-unterminated-override-webvttenc: CMD = fmtstdout webvtt -i
$(SRC_PATH)/tests/sub-unterminated-override.ass
+
FATE_SUBTITLES-$(call ALLYES, SRT_DEMUXER SUBRIP_DECODER TEXT_ENCODER
SRT_MUXER) += fate-sub-textenc
fate-sub-textenc: CMD = fmtstdout srt -i
$(TARGET_SAMPLES)/sub/SubRip_capability_tester.srt -c:s text
@@ -177,5 +183,5 @@ fate-sub-%: CMP = rawdiff
FATE_SAMPLES_FFMPEG += $(FATE_SUBTITLES)
FATE_SAMPLES_FFPROBE += $(FATE_SUBTITLES_PROBE-yes)
-fate-subtitles: $(FATE_SUBTITLES) $(FATE_SUB_KARAOKE-yes)
$(FATE_SUBTITLES_PROBE-yes)
-FATE_FFMPEG-yes += $(FATE_SUB_KARAOKE-yes)
+fate-subtitles: $(FATE_SUBTITLES) $(FATE_SUB_KARAOKE-yes)
$(FATE_SUB_UNTERMINATED-yes) $(FATE_SUBTITLES_PROBE-yes)
+FATE_FFMPEG-yes += $(FATE_SUB_KARAOKE-yes) $(FATE_SUB_UNTERMINATED-yes)
diff --git a/tests/ref/fate/sub-unterminated-override-srtenc
b/tests/ref/fate/sub-unterminated-override-srtenc
new file mode 100644
index 00000000000..a3ac0d3b81a
--- /dev/null
+++ b/tests/ref/fate/sub-unterminated-override-srtenc
@@ -0,0 +1,12 @@
+1
+00:00:01,000 --> 00:00:02,000
+<font size="20">{\b1 unterminated override</font>
+
+2
+00:00:03,000 --> 00:00:04,000
+<font size="20"><i>Italic</i> text {\u1 unterminated</font>
+
+3
+00:00:05,000 --> 00:00:06,000
+<font size="20">Next cue</font>
+
diff --git a/tests/ref/fate/sub-unterminated-override-webvttenc
b/tests/ref/fate/sub-unterminated-override-webvttenc
new file mode 100644
index 00000000000..dc09bb8effb
--- /dev/null
+++ b/tests/ref/fate/sub-unterminated-override-webvttenc
@@ -0,0 +1,10 @@
+WEBVTT
+
+00:01.000 --> 00:02.000
+{\b1 unterminated override
+
+00:03.000 --> 00:04.000
+<i>Italic</i> text {\u1 unterminated
+
+00:05.000 --> 00:06.000
+Next cue
diff --git a/tests/sub-unterminated-override.ass
b/tests/sub-unterminated-override.ass
new file mode 100644
index 00000000000..7a86e7234ef
--- /dev/null
+++ b/tests/sub-unterminated-override.ass
@@ -0,0 +1,14 @@
+[Script Info]
+ScriptType: v4.00+
+PlayResX: 640
+PlayResY: 360
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour,
OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY,
Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR,
MarginV, Encoding
+Style:
Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1
+
+[Events]
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,{\b1 unterminated override
+Dialogue: 0,0:00:03.00,0:00:04.00,Default,,0,0,0,,{\i1}Italic{\i0} text {\u1
unterminated
+Dialogue: 0,0:00:05.00,0:00:06.00,Default,,0,0,0,,Next cue
--
2.54.0
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]