ladybird/Tests/LibMedia/TestParseMatroska.cpp
Zaggy1024 ee95de40d6 LibMedia: Validate fixed-size Matroska frames
We were allowing Matroska blocks with fixed-size lacing to contain
frames with non-divisible sizes. This should not be possible, as it
inherently means that trailing bytes will be discarded.

We now have a valid and invalid testcase for fixed-size lacing to
ensure our handling remains correct.
2026-01-28 16:12:22 -06:00

141 lines
6.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2023-2026, Gregory Bertilson <gregory@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/File.h>
#include <LibMedia/Containers/Matroska/MatroskaDemuxer.h>
#include <LibTest/TestCase.h>
#include <LibMedia/Containers/Matroska/Reader.h>
TEST_CASE(master_elements_containing_crc32)
{
auto file = MUST(Core::File::open("./master_elements_containing_crc32.mkv"sv, Core::File::OpenMode::Read));
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(MUST(file->read_until_eof()));
auto matroska_reader = MUST(Media::Matroska::Reader::from_stream(stream->create_cursor()));
u64 video_track = 0;
MUST(matroska_reader.for_each_track_of_type(Media::Matroska::TrackEntry::TrackType::Video, [&](Media::Matroska::TrackEntry const& track_entry) -> Media::DecoderErrorOr<IterationDecision> {
video_track = track_entry.track_number();
return IterationDecision::Break;
}));
EXPECT_EQ(video_track, 1u);
auto iterator = MUST(matroska_reader.create_sample_iterator(stream->create_cursor(), video_track));
MUST(iterator.next_block());
MUST(matroska_reader.seek_to_random_access_point(iterator, AK::Duration::from_seconds(7)));
MUST(iterator.next_block());
}
TEST_CASE(seek_in_multi_frame_blocks)
{
auto file = MUST(Core::File::open("./test-webm-xiph-lacing.mka"sv, Core::File::OpenMode::Read));
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(MUST(file->read_until_eof()));
auto demuxer = MUST(Media::Matroska::MatroskaDemuxer::from_stream(stream->create_cursor()));
auto optional_track = MUST(demuxer->get_preferred_track_for_type(Media::TrackType::Audio));
EXPECT(optional_track.has_value());
auto track = optional_track.release_value();
MUST(demuxer->create_context_for_track(track, stream->create_cursor()));
auto initial_coded_frame = MUST(demuxer->get_next_sample_for_track(track));
EXPECT(initial_coded_frame.timestamp() <= AK::Duration::zero());
auto forward_seek_time = AK::Duration::from_seconds(5);
MUST(demuxer->seek_to_most_recent_keyframe(track, forward_seek_time, Media::DemuxerSeekOptions::None));
auto coded_frame_after_forward_seek = MUST(demuxer->get_next_sample_for_track(track));
EXPECT(coded_frame_after_forward_seek.timestamp() > AK::Duration::zero());
EXPECT(coded_frame_after_forward_seek.timestamp() <= forward_seek_time);
auto backward_seek_time = AK::Duration::from_seconds(2);
MUST(demuxer->seek_to_most_recent_keyframe(track, backward_seek_time, Media::DemuxerSeekOptions::None));
auto coded_frame_after_backward_seek = MUST(demuxer->get_next_sample_for_track(track));
EXPECT(coded_frame_after_backward_seek.timestamp() > AK::Duration::zero());
EXPECT(coded_frame_after_backward_seek.timestamp() <= backward_seek_time);
}
TEST_CASE(block_group)
{
auto file = MUST(Core::File::open("./test-matroska-block-group.mkv"sv, Core::File::OpenMode::Read));
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(MUST(file->read_until_eof()));
auto matroska_reader = MUST(Media::Matroska::Reader::from_stream(stream->create_cursor()));
u64 video_track = 0;
MUST(matroska_reader.for_each_track_of_type(Media::Matroska::TrackEntry::TrackType::Video, [&](Media::Matroska::TrackEntry const& track_entry) -> Media::DecoderErrorOr<IterationDecision> {
video_track = track_entry.track_number();
return IterationDecision::Break;
}));
EXPECT_EQ(video_track, 1u);
auto iterator = MUST(matroska_reader.create_sample_iterator(stream->create_cursor(), video_track));
auto first_block = MUST(iterator.next_block());
EXPECT(first_block.duration().has_value());
EXPECT_EQ(first_block.duration()->to_milliseconds(), 33);
auto second_block = MUST(iterator.next_block());
EXPECT_EQ(second_block.timestamp().to_milliseconds(), 33);
EXPECT(second_block.only_keyframes());
}
TEST_CASE(fixed_size_lacing)
{
auto file = MUST(Core::File::open("./test-matroska-fixed-size-lacing.mkv"sv, Core::File::OpenMode::Read));
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(MUST(file->read_until_eof()));
auto matroska_reader = MUST(Media::Matroska::Reader::from_stream(stream->create_cursor()));
u64 video_track = 0;
MUST(matroska_reader.for_each_track_of_type(Media::Matroska::TrackEntry::TrackType::Video, [&](Media::Matroska::TrackEntry const& track_entry) -> Media::DecoderErrorOr<IterationDecision> {
video_track = track_entry.track_number();
return IterationDecision::Break;
}));
EXPECT_EQ(video_track, 1u);
auto iterator = MUST(matroska_reader.create_sample_iterator(stream->create_cursor(), video_track));
// Block 1: 4 frames × 4 bytes
auto block1 = MUST(iterator.next_block());
EXPECT_EQ(block1.timestamp().to_milliseconds(), 0);
EXPECT(block1.only_keyframes());
EXPECT_EQ(block1.lacing(), Media::Matroska::Block::Lacing::FixedSize);
auto frames1 = MUST(iterator.get_frames(block1));
EXPECT_EQ(frames1.size(), 4u);
for (auto const& frame : frames1)
EXPECT_EQ(frame.size(), 4u);
// Block 2: 2 frames × 8 bytes
auto block2 = MUST(iterator.next_block());
EXPECT_EQ(block2.timestamp().to_milliseconds(), 33);
EXPECT_EQ(block2.lacing(), Media::Matroska::Block::Lacing::FixedSize);
auto frames2 = MUST(iterator.get_frames(block2));
EXPECT_EQ(frames2.size(), 2u);
for (auto const& frame : frames2)
EXPECT_EQ(frame.size(), 8u);
// Block 3: 3 frames × 1 byte
auto block3 = MUST(iterator.next_block());
EXPECT_EQ(block3.timestamp().to_milliseconds(), 66);
EXPECT_EQ(block3.lacing(), Media::Matroska::Block::Lacing::FixedSize);
auto frames3 = MUST(iterator.get_frames(block3));
EXPECT_EQ(frames3.size(), 3u);
for (auto const& frame : frames3)
EXPECT_EQ(frame.size(), 1u);
}
TEST_CASE(fixed_size_lacing_invalid)
{
auto file = MUST(Core::File::open("./test-matroska-fixed-size-lacing-invalid.mkv"sv, Core::File::OpenMode::Read));
auto stream = Media::IncrementallyPopulatedStream::create_from_buffer(MUST(file->read_until_eof()));
auto matroska_reader = MUST(Media::Matroska::Reader::from_stream(stream->create_cursor()));
u64 video_track = 0;
MUST(matroska_reader.for_each_track_of_type(Media::Matroska::TrackEntry::TrackType::Video, [&](Media::Matroska::TrackEntry const& track_entry) -> Media::DecoderErrorOr<IterationDecision> {
video_track = track_entry.track_number();
return IterationDecision::Break;
}));
EXPECT_EQ(video_track, 1u);
auto iterator = MUST(matroska_reader.create_sample_iterator(stream->create_cursor(), video_track));
auto block = MUST(iterator.next_block());
EXPECT_EQ(block.lacing(), Media::Matroska::Block::Lacing::FixedSize);
auto frames_or_error = iterator.get_frames(block);
EXPECT(frames_or_error.is_error());
}