PR #25287 opened by Forgejo_Fairy
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25287
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25287.patch

A WAV file with a zero-length data chunk and trailing metadata currently 
returns that metadata as audio. The attachments in #24934 produce 40 and 2,160 
erroneous PCM bytes.

Apply the RIFF-size distinction suggested by kvog in #24934: honor zero-length 
data when the enclosing RIFF size is known, while preserving the compatibility 
fallback for zero or UINT32_MAX RIFF sizes and UINT32_MAX data sizes. Continue 
parsing footer metadata when the data boundary is known, and record zero 
duration for known-empty streams to prevent bitrate-based duration estimation 
from counting metadata. The explicit ignore_length override remains effective.

Adds a portable, in-memory FATE test with 28 cases covering 
seekable/nonseekable RIFF, RIFX, RF64 and BW64 input, empty/nonempty data, 
unknown lengths, metadata, duration and ignore_length. Expected output is 
stored in tests/ref/fate/wav-demux; no external samples are needed.

Validation on Linux AArch64 with GCC 13.3.0:

- Both issue attachments produce zero PCM bytes after the fix, using file and 
pipe input; ignore_length=1 retains the previous byte totals.
- The reported AIFF conversion changes from 10 sample frames to zero.
- The new fate-wav-demux test fails with the original demuxer and passes with 
this change.
- make -j8 fate-wav fate-wav-demux fate-w64 fate-wav-ac3 
SAMPLES=/opt/fate-suite passes.

The original regression was verified by building b7e0f88f2b3b and its parent: 
the first attachment changes from zero PCM bytes to 40.

Fixes #24934.


>From 7f37c5b2e250238fe45f73a088944c699fe4b395 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Wed, 21 Oct 2026 05:19:54 +0000
Subject: [PATCH] avformat/wavdec: respect empty data chunks with a known RIFF
 size

Keep treating a zero data size as unknown when the RIFF size is also
unknown, preserving compatibility with old FFmpeg pipe output. With a
known RIFF size, honor an empty data chunk and continue parsing footer
metadata instead of returning it as audio packets.

Record a zero duration for a known-empty stream so that trailing metadata
is not used to estimate a positive duration.

Add an in-memory FATE test covering seekable and nonseekable RIFF, RIFX,
RF64 and BW64 input, legacy unknown sizes and ignore_length. Expected
packet sizes, metadata and durations live in the FATE reference file.

Fixes #24934.

Assisted-by: Fairy
---
 libavformat/Makefile         |   1 +
 libavformat/tests/.gitignore |   1 +
 libavformat/tests/wav.c      | 213 +++++++++++++++++++++++++++++++++++
 libavformat/wavdec.c         |  13 ++-
 tests/fate/libavformat.mak   |   4 +
 tests/ref/fate/wav-demux     |  28 +++++
 6 files changed, 255 insertions(+), 5 deletions(-)
 create mode 100644 libavformat/tests/wav.c
 create mode 100644 tests/ref/fate/wav-demux

diff --git a/libavformat/Makefile b/libavformat/Makefile
index 7c17892c22f..49d7505e384 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -803,6 +803,7 @@ TESTPROGS-$(CONFIG_SRTP)                 += srtp
 TESTPROGS-$(CONFIG_IMF_DEMUXER)          += imf
 TESTPROGS-$(CONFIG_MP3_MUXER)            += id3v2enc
 TESTPROGS-$(CONFIG_OGG_MUXER)            += ogg_pages
+TESTPROGS-$(CONFIG_WAV_DEMUXER)          += wav
 
 TOOLS     = aviocat                                                     \
             ismindex                                                    \
diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore
index 40603e70a51..b624988e360 100644
--- a/libavformat/tests/.gitignore
+++ b/libavformat/tests/.gitignore
@@ -13,3 +13,4 @@
 /seek_utils
 /http
 /ogg_pages
+/wav
diff --git a/libavformat/tests/wav.c b/libavformat/tests/wav.c
new file mode 100644
index 00000000000..9aebbb6620b
--- /dev/null
+++ b/libavformat/tests/wav.c
@@ -0,0 +1,213 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "libavutil/common.h"
+#include "libavutil/dict.h"
+#include "libavutil/error.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/log.h"
+#include "libavutil/mem.h"
+#include "libavformat/avformat.h"
+#include "libavformat/avio.h"
+
+struct Buffer {
+    uint8_t *data;
+    int size;
+    int pos;
+};
+
+static int read_packet(void *opaque, uint8_t *buf, int size)
+{
+    struct Buffer *b = opaque;
+    size = FFMIN(size, b->size - b->pos);
+    if (!size)
+        return AVERROR_EOF;
+    memcpy(buf, b->data + b->pos, size);
+    b->pos += size;
+    return size;
+}
+
+static int64_t seek(void *opaque, int64_t offset, int whence)
+{
+    struct Buffer *b = opaque;
+    if (whence == AVSEEK_SIZE)
+        return b->size;
+    if (whence == SEEK_CUR)
+        offset += b->pos;
+    else if (whence == SEEK_END)
+        offset += b->size;
+    if (offset < 0 || offset > b->size)
+        return AVERROR(EINVAL);
+    b->pos = offset;
+    return offset;
+}
+
+/* riff_size == 1 requests the actual RIFF size. */
+static int test(const char *magic, uint32_t riff_size, uint32_t data_size,
+                int ignore_length, int seekable)
+{
+    AVFormatContext *fmt = NULL;
+    AVIOContext *pb = NULL, *writer;
+    AVPacket *pkt = NULL;
+    AVDictionary *opts = NULL;
+    const AVDictionaryEntry *title;
+    struct Buffer b = { 0 };
+    int big_endian = !strcmp(magic, "RIFX");
+    int rf64 = !strcmp(magic, "RF64") || !strcmp(magic, "BW64");
+    void (*w16)(AVIOContext *, unsigned) = big_endian ? avio_wb16 : avio_wl16;
+    void (*w32)(AVIOContext *, unsigned) = big_endian ? avio_wb32 : avio_wl32;
+    int ret, bytes = 0, packets = 0;
+    uint8_t *io_buffer;
+
+    ret = avio_open_dyn_buf(&writer);
+    if (ret < 0)
+        return ret;
+    avio_write(writer, magic, 4);
+    w32(writer, riff_size);
+    avio_write(writer, "WAVE", 4);
+    if (rf64) {
+        avio_write(writer, "ds64", 4);
+        avio_wl32(writer, 28);
+        avio_wl64(writer, 0); // filled in below
+        avio_wl64(writer, data_size);
+        avio_wl64(writer, data_size / 2);
+        avio_wl32(writer, 0);
+    }
+    avio_write(writer, "fmt ", 4);
+    w32(writer, 16);
+    w16(writer, 1); // PCM
+    w16(writer, 1); // mono
+    w32(writer, 8000);
+    w32(writer, 16000);
+    w16(writer, 2);
+    w16(writer, 16);
+    avio_write(writer, "data", 4);
+    w32(writer, rf64 ? UINT32_MAX : data_size);
+    if (data_size == 4)
+        avio_write(writer, "PCM!", 4);
+    avio_write(writer, "LQBE", 4);
+    w32(writer, 4);
+    avio_write(writer, "junk", 4);
+    /* LIST/INFO subchunks are read as little-endian even in RIFX. */
+    avio_write(writer, "LIST", 4);
+    w32(writer, 16);
+    avio_write(writer, "INFOINAM", 8);
+    avio_wl32(writer, 4);
+    avio_write(writer, "test", 4);
+    b.size = avio_close_dyn_buf(writer, &b.data);
+    if (riff_size == 1) {
+        if (big_endian)
+            AV_WB32(b.data + 4, b.size - 8);
+        else
+            AV_WL32(b.data + 4, b.size - 8);
+    }
+    if (rf64)
+        AV_WL64(b.data + 20, b.size - 8);
+
+    io_buffer = av_malloc(4096);
+    if (!io_buffer) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    pb = avio_alloc_context(io_buffer, 4096, 0, &b, read_packet,
+                            NULL, seekable ? seek : NULL);
+    if (!pb) {
+        av_free(io_buffer);
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    fmt = avformat_alloc_context();
+    pkt = av_packet_alloc();
+    if (!fmt || !pkt) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+    fmt->pb = pb;
+    ret = av_dict_set_int(&opts, "ignore_length", ignore_length, 0);
+    if (ret < 0)
+        goto end;
+    ret = avformat_open_input(&fmt, NULL, av_find_input_format("wav"), &opts);
+    if (ret < 0)
+        goto end;
+    title = av_dict_get(fmt->metadata, "title", NULL, 0);
+    while ((ret = av_read_frame(fmt, pkt)) >= 0) {
+        bytes += pkt->size;
+        packets++;
+        av_packet_unref(pkt);
+    }
+    printf("%s riff=%"PRIu32" data=%"PRIu32" ignore=%d seek=%d: "
+           "packets=%d bytes=%d title=%s duration=%"PRId64" end=%s\n",
+           magic, riff_size, data_size, ignore_length, seekable,
+           packets, bytes, title ? title->value : "-",
+           fmt->streams[0]->duration == AV_NOPTS_VALUE ? -1 : 
fmt->streams[0]->duration,
+           av_err2str(ret));
+    if (ret == AVERROR_EOF)
+        ret = 0;
+end:
+    av_dict_free(&opts);
+    av_packet_free(&pkt);
+    avformat_close_input(&fmt);
+    if (pb)
+        av_freep(&pb->buffer);
+    avio_context_free(&pb);
+    av_free(b.data);
+    return ret;
+}
+
+int main(void)
+{
+    static const struct {
+        const char *magic;
+        uint32_t riff_size, data_size;
+        int ignore_length;
+    } cases[] = {
+        { "RIFF", 1,          0,          0 },
+        { "RIFF", 1,          0,          1 },
+        { "RIFF", 0,          0,          0 },
+        { "RIFF", UINT32_MAX, 0,          0 },
+        { "RIFF", 1,          UINT32_MAX, 0 },
+        { "RIFF", UINT32_MAX, UINT32_MAX, 0 },
+        { "RIFF", 1,          4,          0 },
+        { "RIFX", 1,          0,          0 },
+        { "RIFX", 0,          0,          0 },
+        { "RIFX", 1,          4,          0 },
+        { "RF64", UINT32_MAX, 0,          0 },
+        { "RF64", UINT32_MAX, 4,          0 },
+        { "BW64", UINT32_MAX, 0,          0 },
+        { "BW64", UINT32_MAX, 4,          0 },
+    };
+    int ret;
+
+    av_log_set_level(AV_LOG_ERROR);
+    for (int i = 0; i < FF_ARRAY_ELEMS(cases); i++) {
+        for (int seekable = 0; seekable <= 1; seekable++) {
+            ret = test(cases[i].magic, cases[i].riff_size, cases[i].data_size,
+                       cases[i].ignore_length, seekable);
+            if (ret < 0) {
+                fprintf(stderr, "%s\n", av_err2str(ret));
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
diff --git a/libavformat/wavdec.c b/libavformat/wavdec.c
index d1f061d65b8..d41cfb787b4 100644
--- a/libavformat/wavdec.c
+++ b/libavformat/wavdec.c
@@ -364,7 +364,7 @@ static int wav_read_header(AVFormatContext *s)
     int64_t size, av_uninit(data_size);
     int64_t sample_count = 0;
     int rf64 = 0, bw64 = 0;
-    uint32_t tag;
+    uint32_t tag, riff_size;
     AVIOContext *pb      = s->pb;
     AVStream *st         = NULL;
     WAVDemuxContext *wav = s->priv_data;
@@ -398,7 +398,7 @@ static int wav_read_header(AVFormatContext *s)
     }
 
     /* read chunk size */
-    avio_rl32(pb);
+    riff_size = wav->rifx ? avio_rb32(pb) : avio_rl32(pb);
 
     /* read format */
     if (avio_rl32(pb) != MKTAG('W', 'A', 'V', 'E')) {
@@ -469,11 +469,13 @@ static int wav_read_header(AVFormatContext *s)
             if (rf64 || bw64) {
                 wav->data_end = av_sat_add64(avio_tell(pb), data_size);
                 next_tag_ofs = av_sat_add64(wav->data_end, data_size & 1);
-            } else if (size > 0 && size != 0xFFFFFFFF) {
+            } else if (size != UINT32_MAX &&
+                       (size || (riff_size && riff_size != UINT32_MAX))) {
                 data_size    = size;
                 wav->data_end = avio_tell(pb) + size;
                 next_tag_ofs = wav->data_end + (size & 1);
             } else {
+                /* Old FFmpeg versions used zero for both sizes when 
streaming. */
                 av_log(s, AV_LOG_WARNING, "Ignoring maximum wav data size, "
                        "file may be invalid\n");
                 data_size    = 0;
@@ -485,7 +487,7 @@ static int wav_read_header(AVFormatContext *s)
             /* don't look for footer metadata if we can't seek or if we don't
              * know where the data tag ends
              */
-            if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) || (!(rf64 && !bw64) && 
!size))
+            if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) || wav->data_end == 
INT64_MAX)
                 goto break_loop;
             break;
         case MKTAG('f', 'a', 'c', 't'):
@@ -668,7 +670,8 @@ break_loop:
                                   /
                 (st->codecpar->ch_layout.nb_channels * 
(uint64_t)av_get_bits_per_sample(st->codecpar->codec_id));
 
-    if (sample_count)
+    /* Do not estimate an empty stream's duration from trailing metadata. */
+    if (sample_count || (!wav->ignore_length && wav->data_end == data_ofs))
         st->duration = sample_count;
 
     if (st->codecpar->codec_id == AV_CODEC_ID_PCM_S32LE &&
diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak
index 45878891fd9..cd37db86393 100644
--- a/tests/fate/libavformat.mak
+++ b/tests/fate/libavformat.mak
@@ -73,6 +73,10 @@ fate-seek_utils: libavformat/tests/seek_utils$(EXESUF)
 fate-seek_utils: CMD = run libavformat/tests/seek_utils$(EXESUF)
 fate-seek_utils: CMP = null
 
+FATE_LIBAVFORMAT-$(CONFIG_WAV_DEMUXER) += fate-wav-demux
+fate-wav-demux: libavformat/tests/wav$(EXESUF)
+fate-wav-demux: CMD = run libavformat/tests/wav$(EXESUF)
+
 FATE_LIBAVFORMAT += $(FATE_LIBAVFORMAT-yes)
 FATE-$(CONFIG_AVFORMAT) += $(FATE_LIBAVFORMAT)
 fate-libavformat: $(FATE_LIBAVFORMAT)
diff --git a/tests/ref/fate/wav-demux b/tests/ref/fate/wav-demux
new file mode 100644
index 00000000000..9a0589bfd0e
--- /dev/null
+++ b/tests/ref/fate/wav-demux
@@ -0,0 +1,28 @@
+RIFF riff=1 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- duration=0 
end=End of file
+RIFF riff=1 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test duration=0 
end=End of file
+RIFF riff=1 data=0 ignore=1 seek=0: packets=1 bytes=36 title=- duration=-1 
end=End of file
+RIFF riff=1 data=0 ignore=1 seek=1: packets=1 bytes=36 title=test duration=-1 
end=End of file
+RIFF riff=0 data=0 ignore=0 seek=0: packets=1 bytes=36 title=- duration=-1 
end=End of file
+RIFF riff=0 data=0 ignore=0 seek=1: packets=1 bytes=36 title=- duration=-1 
end=End of file
+RIFF riff=4294967295 data=0 ignore=0 seek=0: packets=1 bytes=36 title=- 
duration=-1 end=End of file
+RIFF riff=4294967295 data=0 ignore=0 seek=1: packets=1 bytes=36 title=- 
duration=-1 end=End of file
+RIFF riff=1 data=4294967295 ignore=0 seek=0: packets=1 bytes=36 title=- 
duration=-1 end=End of file
+RIFF riff=1 data=4294967295 ignore=0 seek=1: packets=1 bytes=36 title=- 
duration=-1 end=End of file
+RIFF riff=4294967295 data=4294967295 ignore=0 seek=0: packets=1 bytes=36 
title=- duration=-1 end=End of file
+RIFF riff=4294967295 data=4294967295 ignore=0 seek=1: packets=1 bytes=36 
title=- duration=-1 end=End of file
+RIFF riff=1 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- duration=-1 
end=End of file
+RIFF riff=1 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test duration=2 
end=End of file
+RIFX riff=1 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- duration=0 
end=End of file
+RIFX riff=1 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test duration=0 
end=End of file
+RIFX riff=0 data=0 ignore=0 seek=0: packets=1 bytes=36 title=- duration=-1 
end=End of file
+RIFX riff=0 data=0 ignore=0 seek=1: packets=1 bytes=36 title=- duration=-1 
end=End of file
+RIFX riff=1 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- duration=-1 
end=End of file
+RIFX riff=1 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test duration=2 
end=End of file
+RF64 riff=4294967295 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- 
duration=0 end=End of file
+RF64 riff=4294967295 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test 
duration=0 end=End of file
+RF64 riff=4294967295 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- 
duration=2 end=End of file
+RF64 riff=4294967295 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test 
duration=2 end=End of file
+BW64 riff=4294967295 data=0 ignore=0 seek=0: packets=0 bytes=0 title=- 
duration=0 end=End of file
+BW64 riff=4294967295 data=0 ignore=0 seek=1: packets=0 bytes=0 title=test 
duration=0 end=End of file
+BW64 riff=4294967295 data=4 ignore=0 seek=0: packets=1 bytes=4 title=- 
duration=2 end=End of file
+BW64 riff=4294967295 data=4 ignore=0 seek=1: packets=1 bytes=4 title=test 
duration=2 end=End of file
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to