Revision control
++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "qaudio_parsing_support_p.h"
++
++ #include "qaudio_qspan_support_p.h"
++ #include "qmultimedia_ranges_p.h"
++
++ #include <QtCore/qbytearrayview.h>
++
++ #include <array>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QtMultimediaPrivate::ParsingSupport {
++
++ namespace {
++
++ constexpr std::byte operator""_byte(unsigned long long value)
++ {
++ return std::byte(value);
++ }
++
++ // QSpan::operator[] asserts on out-of-bounds access, so every read below is
++ // bounds-checked in debug builds.
++ constexpr quint8 byteAt(QSpan<const std::byte> data, qsizetype index)
++ {
++ return std::to_integer<quint8>(data[index]);
++ }
++
++ // Returns true if data begins with marker.
++ bool hasMarker(QSpan<const std::byte> data, QByteArrayView marker)
++ {
++ return QByteArrayView(data).startsWith(marker);
++ }
++
++ // Scans data[from..] for a frame sync word: a 0xFF byte whose successor matches
++ // secondByteValue under secondByteMask, and whose header frameSize() accepts.
++ // headerSize is the number of bytes frameSize() needs in order to decide.
++ std::optional<qsizetype> findSyncWord(QSpan<const std::byte> data, qsizetype from,
++ std::byte secondByteMask, std::byte secondByteValue,
++ qsizetype headerSize,
++ qsizetype (*frameSize)(QSpan<const std::byte>, qsizetype))
++ {
++ const QByteArrayView view(data);
++ const qsizetype last = view.size() - headerSize;
++
++ for (qsizetype i = qMax<qsizetype>(from, 0); i <= last;) {
++ const qsizetype sync = view.indexOf(char(0xFF), i);
++ if (sync < 0 || sync > last)
++ return std::nullopt;
++ if ((data[sync + 1] & secondByteMask) == secondByteValue && frameSize(data, sync) > 0) {
++ return sync;
++ }
++ i = sync + 1;
++ }
++ return std::nullopt;
++ }
++
++ // Reads 1-4 bytes as an unsigned big-endian integer.
++ quint32 fromBigEndian(QSpan<const std::byte> data)
++ {
++ Q_ASSERT(data.size() <= 4);
++ quint32 value = 0;
++ for (std::byte b : data)
++ value = (value << 8) | std::to_integer<quint32>(b);
++ return value;
++ }
++
++ // Reads 1-4 bytes as an unsigned little-endian integer.
++ quint32 fromLittleEndian(QSpan<const std::byte> data)
++ {
++ Q_ASSERT(data.size() <= 4);
++ quint32 value = 0;
++ for (qsizetype i = data.size() - 1; i >= 0; --i)
++ value = (value << 8) | std::to_integer<quint32>(data[i]);
++ return value;
++ }
++
++ // CRC-8 with polynomial x^8 + x^2 + x + 1 (0x07) and initial value 0, as used
++ // by the FLAC frame header.
++ quint8 crc8(QSpan<const std::byte> data)
++ {
++ quint8 crc = 0;
++ for (std::byte b : data) {
++ crc ^= std::to_integer<quint8>(b);
++ for (int bit = 0; bit < 8; ++bit)
++ crc = (crc & 0x80) ? quint8((crc << 1) ^ 0x07) : quint8(crc << 1);
++ }
++ return crc;
++ }
++
++ // Returns the length in bytes of the FLAC frame header starting at
++ // data[offset], including its trailing CRC-8 byte, or 0 if the header is
++ // invalid or runs past the end of data. A FLAC frame header is between 6 and
++ // 16 bytes long, so a truncated tail is reported as invalid rather than
++ // guessed at.
++ qsizetype flacFrameHeaderSize(QSpan<const std::byte> data, qsizetype offset)
++ {
++ if (offset < 0)
++ return 0;
++
++ const QSpan<const std::byte> frame = drop(data, offset);
++ if (frame.size() < 6) // 4 header bytes + 1 coded-number byte + CRC-8
++ return 0;
++
++ // 14-bit sync code 0b11111111111110, then a reserved bit that must be 0,
++ // then the blocking strategy bit.
++ if (frame[0] != 0xFF_byte || (frame[1] & 0xFE_byte) != 0xF8_byte)
++ return 0;
++
++ const quint8 blockSizeCode = byteAt(frame, 2) >> 4;
++ const quint8 sampleRateCode = byteAt(frame, 2) & 0x0F;
++ const quint8 channelCode = byteAt(frame, 3) >> 4;
++ const quint8 sampleSizeCode = (byteAt(frame, 3) >> 1) & 0x07;
++
++ // Block size code 0, sample rate code 15, channel assignments above 10 and
++ // sample size code 3 are all reserved, as is the last bit of byte 3.
++ if (blockSizeCode == 0 || sampleRateCode == 0x0F || channelCode > 10
++ || sampleSizeCode == 3 || (byteAt(frame, 3) & 0x01) != 0) {
++ return 0;
++ }
++
++ // The coded frame or sample number uses a UTF-8 style variable length
++ // encoding of one to seven bytes.
++ const std::byte codedNumberLead = frame[4];
++ qsizetype codedNumberSize = 0;
++ if ((codedNumberLead & 0x80_byte) == 0x00_byte)
++ codedNumberSize = 1;
++ else if ((codedNumberLead & 0xE0_byte) == 0xC0_byte)
++ codedNumberSize = 2;
++ else if ((codedNumberLead & 0xF0_byte) == 0xE0_byte)
++ codedNumberSize = 3;
++ else if ((codedNumberLead & 0xF8_byte) == 0xF0_byte)
++ codedNumberSize = 4;
++ else if ((codedNumberLead & 0xFC_byte) == 0xF8_byte)
++ codedNumberSize = 5;
++ else if ((codedNumberLead & 0xFE_byte) == 0xFC_byte)
++ codedNumberSize = 6;
++ else if (codedNumberLead == 0xFE_byte)
++ codedNumberSize = 7;
++ else
++ return 0;
++
++ if (frame.size() < 4 + codedNumberSize)
++ return 0;
++ const bool continuationBytesValid =
++ ranges::all_of(frame.subspan(5, codedNumberSize - 1), [](std::byte b) {
++ return (b & std::byte{ 0xC0 }) == std::byte{ 0x80 };
++ });
++ if (!continuationBytesValid)
++ return 0;
++
++ qsizetype headerSize = 4 + codedNumberSize;
++
++ // Block size codes 6 and 7, and sample rate codes 12 to 14, carry their
++ // value in extra bytes placed after the coded number.
++ if (blockSizeCode == 6)
++ headerSize += 1;
++ else if (blockSizeCode == 7)
++ headerSize += 2;
++
++ if (sampleRateCode == 12)
++ headerSize += 1;
++ else if (sampleRateCode == 13 || sampleRateCode == 14)
++ headerSize += 2;
++
++ // The CRC-8 covers every header byte before it. Checking it is what makes
++ // a sync scan reliable, since the sync code alone occurs often in the
++ // compressed subframe data that follows.
++ if (frame.size() < headerSize + 1)
++ return 0;
++ if (crc8(take(frame, headerSize)) != byteAt(frame, headerSize))
++ return 0;
++
++ return headerSize + 1;
++ }
++
++ } // namespace
++
++ AudioCodec sniffCodec(QSpan<const std::byte> header)
++ {
++ if (header.size() < 4)
++ return AudioCodec::Unknown;
++
++ // ID3 tag -> MP3
++ if (hasMarker(header, "ID3"))
++ return AudioCodec::Mp3;
++
++ // AAC ADTS sync word. Checked before the MPEG audio sync word below, whose
++ // 0xFFE0 mask also matches an ADTS header. The two are told apart by the
++ // layer bits: MPEG Layer III uses 01, ADTS always leaves them 00.
++ if (header[0] == 0xFF_byte && (header[1] & 0xF6_byte) == 0xF0_byte)
++ return AudioCodec::Aac;
++
++ // MPEG audio sync word (0xFFE0 mask) -> MP3
++ if (header[0] == 0xFF_byte && (header[1] & 0xE0_byte) == 0xE0_byte)
++ return AudioCodec::Mp3;
++
++ // FLAC stream marker
++ if (hasMarker(header, "fLaC"))
++ return AudioCodec::Flac;
++
++ // Ogg container. The identification packet on the first page says whether
++ // the stream is Vorbis or Opus. If it has not arrived yet, assume Opus,
++ // which is the more common of the two on the web.
++ if (hasMarker(header, "OggS")) {
++ const std::optional<OggStreamInfo> info = oggStreamInfo(header);
++ return info ? info->codec : AudioCodec::Opus;
++ }
++
++ // WAV / RIFF - uncompressed
++ if (hasMarker(header, "RIFF"))
++ return AudioCodec::Wav;
++
++ return AudioCodec::Unknown;
++ }
++
++ bool hasFrameParser(AudioCodec codec)
++ {
++ return codec == AudioCodec::Mp3 || codec == AudioCodec::Aac || codec == AudioCodec::Flac;
++ }
++
++ std::optional<OggStreamInfo> oggStreamInfo(QSpan<const std::byte> data)
++ {
++ // Ogg page header: "OggS", then 22 more bytes, then the segment table
++ // whose length is given by the segment count at offset 26.
++ if (data.size() < 27 || !hasMarker(data, "OggS"))
++ return std::nullopt;
++
++ const qsizetype packetOffset = 27 + byteAt(data, 26);
++ const QSpan<const std::byte> packet = drop(data, packetOffset);
++ // Both identification packets are longer than this, but 16 bytes covers
++ // the magic, the channel count and the sample rate, which is all we read.
++ if (packet.size() < 16)
++ return std::nullopt;
++
++ // Both Vorbis and Opus store the sample rate as a little-endian 32-bit
++ // value at offset 12 of the identification packet.
++ const auto rateAt12 = [&packet] {
++ return int(fromLittleEndian(packet.subspan(12, 4)));
++ };
++
++ // Vorbis identification packet: 0x01 followed by "vorbis".
++ if (hasMarker(packet, QByteArrayView("\x01vorbis", 7))) {
++ OggStreamInfo info;
++ info.codec = AudioCodec::Vorbis;
++ info.numChannels = byteAt(packet, 11);
++ info.sampleRate = rateAt12();
++ if (info.sampleRate <= 0 || info.numChannels <= 0)
++ return std::nullopt;
++ return info;
++ }
++
++ // Opus identification header.
++ if (hasMarker(packet, "OpusHead")) {
++ OggStreamInfo info;
++ info.codec = AudioCodec::Opus;
++ info.numChannels = byteAt(packet, 9);
++ info.sampleRate = rateAt12();
++ if (info.sampleRate <= 0 || info.numChannels <= 0)
++ return std::nullopt;
++ return info;
++ }
++
++ return std::nullopt;
++ }
++
++ std::optional<FlacStreamInfo> flacStreamInfo(QSpan<const std::byte> data)
++ {
++ // "fLaC" marker + 4-byte block header + 34-byte STREAMINFO = 42 bytes minimum
++ if (data.size() < 42 || !hasMarker(data, "fLaC"))
++ return std::nullopt;
++
++ // Block header byte: bit 7 = last-metadata-block flag, bits [6:0] = block type.
++ // Block type 0 is STREAMINFO, which must be the first metadata block.
++ if ((data[4] & 0x7F_byte) != 0x00_byte)
++ return std::nullopt;
++
++ const qsizetype blockLength = fromBigEndian(data.subspan(5, 3));
++ if (blockLength < 34 || data.size() < 8 + blockLength)
++ return std::nullopt;
++
++ FlacStreamInfo info;
++ // STREAMINFO data starts at byte 8.
++ // Sample rate: top 20 bits spanning bytes [18..20]
++ info.sampleRate = fromBigEndian(data.subspan(18, 3)) >> 4;
++ // Number of channels: next 3 bits (bits [3:1] of byte 20)
++ info.numChannels = ((byteAt(data, 20) >> 1) & 0x7) + 1;
++ // Total samples: the 36 bits after the 5-bit sample size, which is the low
++ // 4 bits of byte 21 followed by bytes [22..25].
++ info.totalSamples = (qint64(byteAt(data, 21) & 0x0F) << 32)
++ | (qint64(byteAt(data, 22)) << 24) | (byteAt(data, 23) << 16)
++ | (byteAt(data, 24) << 8) | byteAt(data, 25);
++ return info;
++ }
++
++ std::optional<qsizetype> flacAudioOffset(QSpan<const std::byte> data)
++ {
++ if (data.size() < 8 || !hasMarker(data, "fLaC"))
++ return std::nullopt;
++
++ qsizetype offset = 4; // skip "fLaC"
++ while (offset + 4 <= data.size()) {
++ const bool isLast = (data[offset] & 0x80_byte) != 0x00_byte;
++ const qsizetype blockLength = fromBigEndian(data.subspan(offset + 1, 3));
++ offset += 4 + blockLength;
++ if (isLast)
++ return offset <= data.size() ? std::optional(offset) : std::nullopt;
++ }
++ return std::nullopt; // metadata blocks not fully buffered yet
++ }
++
++ std::optional<qsizetype> findFlacSync(QSpan<const std::byte> data, qsizetype from)
++ {
++ const QByteArrayView view(data);
++ for (qsizetype i = qMax<qsizetype>(from, 0); i < view.size();) {
++ const qsizetype sync = view.indexOf(char(0xFF), i);
++ if (sync < 0)
++ return std::nullopt;
++ if (flacFrameHeaderSize(data, sync) > 0)
++ return sync;
++ i = sync + 1;
++ }
++ return std::nullopt;
++ }
++
++ qsizetype mpegFrameSize(QSpan<const std::byte> data, qsizetype offset)
++ {
++ if (offset < 0)
++ return 0;
++
++ const QSpan<const std::byte> header = take(drop(data, offset), 4);
++ if (header.size() < 4)
++ return 0;
++
++ if (header[0] != 0xFF_byte || (header[1] & 0xE0_byte) != 0xE0_byte)
++ return 0;
++
++ const int versionBits = (byteAt(header, 1) >> 3) & 0x3; // 3=MPEG1, 2=MPEG2, 0=MPEG2.5
++ const int layerBits = (byteAt(header, 1) >> 1) & 0x3; // 1=LayerIII
++ const int bitrateIndex = (byteAt(header, 2) >> 4) & 0xF;
++ const int sampleRateIndex = (byteAt(header, 2) >> 2) & 0x3;
++ const int padding = (byteAt(header, 2) >> 1) & 0x1;
++
++ if (layerBits != 1)
++ return 0; // only Layer III
++ if (sampleRateIndex == 3)
++ return 0; // reserved sample rate
++ if (bitrateIndex == 0 || bitrateIndex == 15)
++ return 0; // free-format / bad index
++
++ static constexpr std::array<int, 16> bitratesV1 = // MPEG1 Layer III (kbps)
++ { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 };
++ static constexpr std::array<int, 16> bitratesV2 = // MPEG2/2.5 Layer III (kbps)
++ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 };
++
++ static constexpr std::array<std::array<int, 3>, 4> sampleRates = {{
++ { 11025, 12000, 8000 }, // MPEG 2.5 (versionBits == 0)
++ { 0, 0, 0 }, // reserved (versionBits == 1)
++ { 22050, 24000, 16000 }, // MPEG 2 (versionBits == 2)
++ { 44100, 48000, 32000 }, // MPEG 1 (versionBits == 3)
++ }};
++
++ const int sampleRate = sampleRates[versionBits][sampleRateIndex];
++ if (sampleRate == 0)
++ return 0;
++
++ const bool isMpeg1 = (versionBits == 3);
++ const int bitrateBps = (isMpeg1 ? bitratesV1 : bitratesV2)[bitrateIndex] * 1000;
++ if (bitrateBps == 0)
++ return 0;
++
++ // MPEG1 Layer III: frameSize = floor(144 * bitrate / sampleRate) + padding
++ // MPEG2/2.5 Layer III: frameSize = floor( 72 * bitrate / sampleRate) + padding
++ const int coefficient = isMpeg1 ? 144 : 72;
++ const qsizetype frameSize = (coefficient * bitrateBps / sampleRate) + padding;
++ return frameSize > 4 ? frameSize : 0; // must be larger than the header itself
++ }
++
++ std::optional<qsizetype> findMpegSync(QSpan<const std::byte> data, qsizetype from)
++ {
++ return findSyncWord(data, from, 0xE0_byte, 0xE0_byte, 4, mpegFrameSize);
++ }
++
++ qsizetype adtsFrameSize(QSpan<const std::byte> data, qsizetype offset)
++ {
++ if (offset < 0)
++ return 0;
++
++ const QSpan<const std::byte> header = take(drop(data, offset), 7);
++ if (header.size() < 7)
++ return 0;
++
++ // 12-bit sync word: 0xFFF
++ if (header[0] != 0xFF_byte || (header[1] & 0xF0_byte) != 0xF0_byte)
++ return 0;
++
++ // aac_frame_length is a 13-bit field at bits [30:18] of the ADTS header
++ // that gives the total byte length including the header itself.
++ const qsizetype frameLength = ((byteAt(header, 3) & 0x03) << 11) | (byteAt(header, 4) << 3)
++ | (byteAt(header, 5) >> 5);
++ return frameLength >= 7 ? frameLength : 0;
++ }
++
++ std::optional<qsizetype> findAdtsSync(QSpan<const std::byte> data, qsizetype from)
++ {
++ return findSyncWord(data, from, 0xF0_byte, 0xF0_byte, 7, adtsFrameSize);
++ }
++
++ } // namespace QtMultimediaPrivate::ParsingSupport
++
++ QT_END_NAMESPACE