blob: 1e98844bd4fcab9c870fae594c9d06fe4374dcd5 [file]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "formats/id3/reader.h"
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "formats/id3/id3.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/string_reader.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;
TEST(Id3ReaderTest, TagEndOffsetExceedsInputSize) {
constexpr char mp3_data[] = {'I', 'D', '3', '\x03', '\x00',
'\x00', '\x7f', '\x7f', '\x7f', '\x7f'};
auto mp3_file_contents = std::string(mp3_data, sizeof(mp3_data));
riegeli::StringReader<> input(mp3_file_contents);
EXPECT_THAT(IterateOverId3Frames(input, [](const Id3Frame&) { return true; }),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("tag end offset exceeds input size")));
}
class NonSizeSupportingStringReader : public riegeli::StringReader<> {
public:
using riegeli::StringReader<>::StringReader;
bool SupportsSize() override { return false; }
};
TEST(Id3ReaderTest, ReaderDoesNotSupportSize) {
NonSizeSupportingStringReader non_size_input("dummy data");
EXPECT_THAT(IterateOverId3Frames(non_size_input,
[](const Id3Frame&) { return true; }),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("reader does not support size")));
}
TEST(Id3ReaderTest, EarlyExit) {
constexpr char mp3_data[] = {
'I', 'D', '3', '\x03', '\x00', '\x40', '\x00', '\x00', '\x00',
'\x2c', // Tag size = 44
'\x00', '\x00', '\x00', '\x0a', // Extended header size = 10
'\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00',
'\x00',
// A 15-byte COMM frame
'C', 'O', 'M', 'M', '\x00', '\x00', '\x00', '\x05', '\x00', '\x00',
'\x01', '\x02', '\x03', '\x04', '\x05',
// A 15-byte GEOB frame
'G', 'E', 'O', 'B', '\x00', '\x00', '\x00', '\x05', '\x00', '\x00',
'\x05', '\x04', '\x03', '\x02', '\x01'};
auto mp3_file_contents = std::string(mp3_data, sizeof(mp3_data));
riegeli::StringReader<> input(mp3_file_contents);
std::vector<Id3Frame> frames;
EXPECT_THAT(IterateOverId3Frames(input,
[&](const Id3Frame& frame) {
frames.push_back(frame);
return false;
}),
IsOk());
EXPECT_THAT(frames, testing::ElementsAre(Id3Frame{
.offset = 24,
.length = 15,
.id = "COMM",
}));
}
TEST(Id3ReaderTest, ReadsAllFrames) {
constexpr char mp3_data[] = {
'I', 'D', '3', '\x03', '\x00', '\x40', '\x00', '\x00', '\x00',
'\x2c', // Tag size = 44
'\x00', '\x00', '\x00', '\x0a', // Extended header size = 10
'\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00',
'\x00',
// A 15-byte COMM frame
'C', 'O', 'M', 'M', '\x00', '\x00', '\x00', '\x05', '\x00', '\x00',
'\x01', '\x02', '\x03', '\x04', '\x05',
// A 15-byte GEOB frame
'G', 'E', 'O', 'B', '\x00', '\x00', '\x00', '\x05', '\x00', '\x00',
'\x05', '\x04', '\x03', '\x02', '\x01'};
auto mp3_file_contents = std::string(mp3_data, sizeof(mp3_data));
riegeli::StringReader<> input(mp3_file_contents);
std::vector<Id3Frame> frames;
auto status = IterateOverId3Frames(input, [&](const Id3Frame& frame) {
frames.push_back(frame);
return true;
});
ASSERT_THAT(status, IsOk());
ASSERT_TRUE(status.value().has_extended_header);
EXPECT_THAT(frames, testing::UnorderedElementsAre(
Id3Frame{
.offset = 24,
.length = 15,
.id = "COMM",
},
Id3Frame{
.offset = 39,
.length = 15,
.id = "GEOB",
}));
}
TEST(Id3ReaderTest, FrameSizeExceedsRemainingTagSize) {
constexpr char mp3_data[] = {
'I', 'D', '3', '\x03', '\x00', '\x40', '\x00', '\x00', '\x00',
'\x22', // Tag size = 34 (syncsafe 0x22)
'\x00', '\x00', '\x00', '\x0a', // Extended header size = 10
'\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00',
'\x00',
// Frame: size = 0xFFFFFFF6
'F', 'I', 'D', 'S', '\xFF', '\xFF', '\xFF', '\xF6', '\x00', '\x00',
// Padding to satisfy the tag size (10 bytes)
'\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00',
'\x00'};
auto mp3_file_contents = std::string(mp3_data, sizeof(mp3_data));
riegeli::StringReader<> input(mp3_file_contents);
int call_count = 0;
auto status = IterateOverId3Frames(input, [&](const Id3Frame& frame) {
call_count++;
return true;
});
EXPECT_THAT(status,
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("ID3 frame size exceeds remaining tag size")));
EXPECT_EQ(call_count, 0);
}
TEST(Id3ReaderTest, FrameLengthOneTerminates) {
constexpr char mp3_data[] = {
'I', 'D', '3', '\x03', '\x00', '\x40', '\x00', '\x00', '\x00',
'\x22', // Tag size = 34
'\x00', '\x00', '\x00', '\x0a', // Extended header size = 10
'\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00',
'\x00',
// Frame: size = 0xFFFFFFF7
'F', 'I', 'D', 'S', '\xFF', '\xFF', '\xFF', '\xF7', '\x00', '\x00',
// Padding
'\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00',
'\x00'};
auto mp3_file_contents = std::string(mp3_data, sizeof(mp3_data));
riegeli::StringReader<> input(mp3_file_contents);
auto status =
IterateOverId3Frames(input, [](const Id3Frame& frame) { return true; });
EXPECT_THAT(status,
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("ID3 frame size exceeds remaining tag size")));
}
} // namespace
} // namespace credentio