| // 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 |