| // 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; | |
| }); | |
| ABSL_ASSERT_OK(status); | |
| 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 |