Commit 4345a7b451 for ffmpeg
commit 4345a7b451052025cd5e9632e2725f345baaabfd
Author: liuyun <hfliuyun@gmail.com>
Date: Fri Sep 11 02:19:13 2026 +0800
avformat/movenc: truncate chpl titles at UTF-8 boundaries
Nero chpl chapter titles have a one-byte length field and are limited
to 255 bytes. Cutting a longer UTF-8 title at that byte offset can
split a code point and write an incomplete UTF-8 sequence.
When truncation is needed, move the cut point back to the start of the
incomplete code point. Leave titles of at most 255 bytes unchanged.
Add FATE coverage for a title that exactly fits the limit and for
truncation within three-byte and four-byte UTF-8 sequences.
Fixes: https://code.ffmpeg.org/FFmpeg/FFmpeg/issues/24423
Signed-off-by: liuyun <hfliuyun@gmail.com>
diff --git a/libavformat/movenc.c b/libavformat/movenc.c
index f36072470f..3030458ab9 100644
--- a/libavformat/movenc.c
+++ b/libavformat/movenc.c
@@ -5191,6 +5191,8 @@ static int mov_write_chpl_tag(AVIOContext *pb, AVFormatContext *s)
if ((t = av_dict_get(c->metadata, "title", NULL, 0))) {
int len = FFMIN(strlen(t->value), 255);
+ while (len > 252 && (t->value[len] & 0xC0) == 0x80)
+ len--;
avio_w8(pb, len);
avio_write(pb, t->value, len);
} else
diff --git a/tests/chpl-utf8.ffmeta b/tests/chpl-utf8.ffmeta
new file mode 100644
index 0000000000..b434d77910
--- /dev/null
+++ b/tests/chpl-utf8.ffmeta
@@ -0,0 +1,19 @@
+;FFMETADATA1
+; 252 A + U+2026 = 255 bytes: must be kept
+[CHAPTER]
+TIMEBASE=1/1000
+START=0
+END=333
+title=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…
+; 253 A + U+2026 = 256 bytes: truncate to 253
+[CHAPTER]
+TIMEBASE=1/1000
+START=333
+END=666
+title=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…
+; 252 A + U+1F600 = 256 bytes: truncate to 252
+[CHAPTER]
+TIMEBASE=1/1000
+START=666
+END=1000
+title=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA😀
diff --git a/tests/fate/mov.mak b/tests/fate/mov.mak
index 01b1346cec..bb94f66f67 100644
--- a/tests/fate/mov.mak
+++ b/tests/fate/mov.mak
@@ -626,6 +626,15 @@ fate-mov-stereo3d-sbs-mp4: CMD = transcode rawvideo $(TARGET_PATH)/tests/data/vs
"-c:v mpeg4 -qscale:v 10 -stereo3d:v sbsl -strict unofficial -frames:v 2" \
"-c:v copy" "-show_entries stream_side_data_list" "" "" "-s 352x288 -pix_fmt yuv420p"
+# chpl titles are limited to 255 bytes; truncation must not split a UTF-8
+# character. 3gp is used because MP4/MOV read the chapter track, not chpl.
+tests/data/chpl-utf8.ffmeta: TAG = COPY
+tests/data/chpl-utf8.ffmeta: $(SRC_PATH)/tests/chpl-utf8.ffmeta | tests/data
+ $(M)cp $< $@
+fate-mov-chpl-utf8: tests/data/chpl-utf8.ffmeta
+FATE_MOV_FFMPEG_FFPROBE-$(call ALLYES, COLOR_FILTER MPEG4_ENCODER FFMETADATA_DEMUXER TGP_MUXER MOV_DEMUXER FILE_PROTOCOL) += fate-mov-chpl-utf8
+fate-mov-chpl-utf8: CMD = run_with_temp "$(FFMPEG) -nostdin -v error -filter_complex color=c=black:s=2x2:r=1 -f ffmetadata -i $(TARGET_PATH)/tests/data/chpl-utf8.ffmeta -map_chapters 0 -frames:v 1 -c:v mpeg4 -threads 1 -flags +bitexact -fflags +bitexact -f 3gp -y" "ffprobe$(PROGSSUF)$(EXESUF) -loglevel error -bitexact -show_chapters" 3gp
+
FATE_FFMPEG += $(FATE_MOV_FFMPEG-yes)
FATE_FFMPEG_FFPROBE += $(FATE_MOV_FFMPEG_FFPROBE-yes)
diff --git a/tests/ref/fate/mov-chpl-utf8 b/tests/ref/fate/mov-chpl-utf8
new file mode 100644
index 0000000000..d2b903691f
--- /dev/null
+++ b/tests/ref/fate/mov-chpl-utf8
@@ -0,0 +1,27 @@
+[CHAPTER]
+id=0
+time_base=1/10000000
+start=0
+start_time=0.000000
+end=3330000
+end_time=0.333000
+TAG:title=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…
+[/CHAPTER]
+[CHAPTER]
+id=1
+time_base=1/10000000
+start=3330000
+start_time=0.333000
+end=6660000
+end_time=0.666000
+TAG:title=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+[/CHAPTER]
+[CHAPTER]
+id=2
+time_base=1/10000000
+start=6660000
+start_time=0.666000
+end=10000000
+end_time=1.000000
+TAG:title=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+[/CHAPTER]