LibWeb/MimeSniff: Add MP3 without ID3 sniffing

Removes the associated FIXME in match_an_audio_or_video_type_pattern().
Sniffing process is a simplified version of the full spec, as it only
checks one frame of the mp3. To fully align with the spec, it would
also have to check a second frame by calculating frame size as
described in the spec.
This commit is contained in:
Ben Eidson 2025-05-18 17:00:03 -04:00 committed by Andrew Kaster
commit bd68a99f14
Notes: github-actions[bot] 2025-06-09 13:51:34 +00:00
2 changed files with 169 additions and 2 deletions

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2023-2024, Kemal Zebari <kemalzebra@gmail.com>.
* Copyright (c) 2023-2024, Kemal Zebari <kemalzebra@gmail.com>
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2025, Ben Eidson <b.e.eidson@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -323,6 +324,96 @@ TEST_CASE(determine_computed_mime_type_when_trying_to_match_webm_signature)
}
}
// http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
struct MP3FrameOptions {
bool validLength = true;
// include the 0xFFF sync word?
bool sync = true;
// 3=MPEG-1, 2=MPEG-2, 0=MPEG-2.5
u8 version = 3;
// 1=III, 2=II, 3=I
u8 layer = 1;
// true=no CRC, false=CRC follows
bool protect = true;
// 114 valid
u8 bitrate_index = 9;
// 0=44.1k,1=48k,2=32k
u8 samplerate_index = 0;
// padding bit
bool padded = false;
// filler bytes
size_t payload_bytes = 100;
};
static ByteBuffer make_mp3_frame(MP3FrameOptions opts)
{
if (!opts.validLength)
return MUST(ByteBuffer::create_zeroed(2));
size_t total_size = 4 + opts.payload_bytes;
auto buffer = MUST(ByteBuffer::create_zeroed(total_size));
auto* data = buffer.data();
// first 8 bits of sync (0xFFF)
if (opts.sync)
data[0] = 0xFF;
// 1110 0000 = last three sync bits
data[1] = 0xE0
// bits 43: version
| ((opts.version & 0x3) << 3)
// bits 21: layer
| ((opts.layer & 0x3) << 1)
// bit 0: protection
| (opts.protect & 0x1);
// bits 74: bitrate index
data[2] = ((opts.bitrate_index & 0xF) << 4)
// bits 32: samplerate index
| ((opts.samplerate_index & 0x3) << 2)
// bit 1: pad
| ((opts.padded & 0x1) << 1);
// bit 0: private (keep zero)
// 3) Rest of header (channel flags, etc.) not needed for sniff
data[3] = 0x00;
// Payload bytes are already zeroed
return buffer;
}
TEST_CASE(determine_computed_mime_type_when_trying_to_match_mp3_no_id3_signature)
{
HashMap<StringView, Vector<ByteBuffer>> mime_type_to_headers_map;
mime_type_to_headers_map.set("application/octet-stream"sv, {
// Payload length < 4.
make_mp3_frame({ .validLength = false }),
// invalid sync
make_mp3_frame({ .sync = false }),
// invalid layer (reserved)
make_mp3_frame({ .layer = 0 }),
// invalid bitrate
make_mp3_frame({ .bitrate_index = 15 }),
// invalid sample rate
make_mp3_frame({ .samplerate_index = 3 }),
});
mime_type_to_headers_map.set("audio/mpeg"sv, {
make_mp3_frame({ .padded = true }),
make_mp3_frame({ .padded = false }),
});
for (auto const& mime_type_to_headers : mime_type_to_headers_map) {
auto mime_type = mime_type_to_headers.key;
for (auto const& header : mime_type_to_headers.value) {
auto computed_mime_type = Web::MimeSniff::Resource::sniff(header.bytes(), Web::MimeSniff::SniffingConfiguration { .sniffing_context = Web::MimeSniff::SniffingContext::AudioOrVideo });
EXPECT_EQ(mime_type, computed_mime_type.serialized());
}
}
}
TEST_CASE(determine_computed_mime_type_in_a_font_context)
{
// Cover case where supplied type is an XML MIME type.