| // 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/id3.h" |
| |
| #include <string> |
| |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/strings/string_view.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(GeobFrameTest, ParseIso8859_1) { |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| 'f', 'i', 'l', 'e', 0x00, // Filename |
| 'd', 'e', 's', 'c', 0x00, // Content description |
| 'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0, |
| /*data_length=*/geob_frame_contents.size()); |
| ASSERT_THAT(geob_or, IsOk()); |
| GeobFrame geob = *geob_or; |
| |
| EXPECT_EQ(geob.mime_type, "text/plain"); |
| EXPECT_EQ(geob.encapsulated_object, "object"); |
| } |
| |
| TEST(GeobFrameTest, ParseUtf16LeBom) { |
| const char geob_frame[] = { |
| 0x01, // Text encoding [UTF-16] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| '\xff', '\xfe', // BOM |
| 'f', 0x00, 0x00, 0x00, // Filename |
| '\xff', '\xfe', // BOM |
| 'd', 0x00, 0x00, 0x00, // Content description |
| 'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0, |
| /*data_length=*/geob_frame_contents.size()); |
| ASSERT_THAT(geob_or, IsOk()); |
| GeobFrame geob = *geob_or; |
| |
| EXPECT_EQ(geob.mime_type, "text/plain"); |
| EXPECT_EQ(geob.encapsulated_object, "object"); |
| } |
| |
| TEST(GeobFrameTest, ParseUtf16BeBom) { |
| const char geob_frame[] = { |
| 0x01, // Text encoding [UTF-16] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| '\xfe', '\xff', // BOM |
| 0x00, 'f', 0x00, 0x00, // Filename |
| '\xfe', '\xff', // BOM |
| 0x00, 'd', 0x00, 0x00, // Content description |
| 'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0, |
| /*data_length=*/geob_frame_contents.size()); |
| ASSERT_THAT(geob_or, IsOk()); |
| GeobFrame geob = *geob_or; |
| |
| EXPECT_EQ(geob.mime_type, "text/plain"); |
| EXPECT_EQ(geob.encapsulated_object, "object"); |
| } |
| |
| TEST(GeobFrameTest, ParseUtf16BeNoBom) { |
| const char geob_frame[] = { |
| 0x02, // Text encoding [UTF-16BE] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| 0x00, 'f', 0x00, 0x00, // Filename |
| 0x00, 'd', 0x00, 0x00, // Content description |
| 'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0, |
| /*data_length=*/geob_frame_contents.size()); |
| ASSERT_THAT(geob_or, IsOk()); |
| GeobFrame geob = *geob_or; |
| |
| EXPECT_EQ(geob.mime_type, "text/plain"); |
| EXPECT_EQ(geob.encapsulated_object, "object"); |
| } |
| |
| TEST(GeobFrameTest, ParseUtf8) { |
| const char geob_frame[] = { |
| 0x03, // Text encoding [UTF-8] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| 'f', 'i', 'l', 'e', 0x00, // Filename |
| 'd', 'e', 's', 'c', 0x00, // Content description |
| 'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0, |
| /*data_length=*/geob_frame_contents.size()); |
| ASSERT_THAT(geob_or, IsOk()); |
| GeobFrame geob = *geob_or; |
| |
| EXPECT_EQ(geob.mime_type, "text/plain"); |
| EXPECT_EQ(geob.encapsulated_object, "object"); |
| } |
| |
| TEST(GeobFrameTest, ParseInvalidTextEncodingFails) { |
| const char geob_frame[] = { |
| 0x04, // Invalid text encoding |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| |
| EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, |
| /*data_length=*/geob_frame_contents.size()), |
| StatusIs(absl::StatusCode::kFailedPrecondition, |
| HasSubstr("Invalid text encoding value"))); |
| } |
| |
| TEST(GeobFrameTest, ParseZeroLengthExceedingsBoundsFails) { |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| |
| EXPECT_THAT( |
| GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/0), |
| StatusIs( |
| absl::StatusCode::kDataLoss, |
| HasSubstr( |
| "Data structure exceeded specified offset boundary limit."))); |
| } |
| |
| TEST(GeobFrameTest, ParseMimeTypeNotTerminatedFails) { |
| // MIME type is not null-terminated within the frame data, but there is a |
| // null byte immediately after the frame data. |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 't', 'e', 'x', 't', '/', |
| 'p', 'l', 'a', 'i', 'n', // MIME type (10 bytes, no null) |
| 0x00 // Null byte outside the frame |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| // data_length is 11 (1 byte encoding + 10 bytes MIME type). |
| // The trailing null byte is at index 11, which is outside the frame. |
| EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/11), |
| StatusIs(absl::StatusCode::kDataLoss, |
| "Failed to read utf-8 text; at byte 1")); |
| } |
| |
| TEST(GeobFrameTest, ParseFilenameNotTerminatedFails) { |
| // Filename is not null-terminated within the frame data, but there is a |
| // null byte immediately after the frame data. |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| 'f', 'i', 'l', 'e', // Filename (4 bytes, no null) |
| 0x00 // Null byte outside the frame |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| // data_length is 1 + 11 + 4 = 16. |
| // The trailing null byte is at index 16, which is outside the frame. |
| EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/16), |
| StatusIs(absl::StatusCode::kDataLoss, |
| "Failed to read utf-8 text; at byte 12")); |
| } |
| |
| TEST(GeobFrameTest, ParseDescriptionNotTerminatedFails) { |
| // Content description is not null-terminated within the frame data, but there |
| // is a null byte immediately after the frame data. |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| 'f', 'i', 'l', 'e', 0x00, // Filename |
| 'd', 'e', 's', 'c', // Content description (4 bytes, no null) |
| 0x00 // Null byte outside the frame |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| // data_length is 1 + 11 + 5 + 4 = 21. |
| // The trailing null byte is at index 21, which is outside the frame. |
| EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/21), |
| StatusIs(absl::StatusCode::kDataLoss, |
| "Failed to read utf-8 text; at byte 17")); |
| } |
| |
| TEST(GeobFrameTest, ParseDescriptionMissingFails) { |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 'a', 0x00, // MIME type |
| 'f', 'i', 'l', 'e', 0x00, // Filename |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| // data_length is 8 (1 + 2 + 5). |
| // Description is missing. |
| EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/8), |
| StatusIs(absl::StatusCode::kDataLoss, |
| "Failed to read utf-8 text; at byte 8")); |
| } |
| |
| TEST(GeobFrameTest, ParseFilenameMissingFails) { |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 'a', |
| 0x00, // MIME type |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| // data_length is 3 (1 + 2). |
| // Filename is missing. |
| EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/3), |
| StatusIs(absl::StatusCode::kDataLoss, |
| "Failed to read utf-8 text; at byte 3")); |
| } |
| |
| TEST(GeobFrameTest, ParseDescriptionEmptySucceeds) { |
| const char geob_frame[] = { |
| 0x00, // Text encoding [ISO-8859-1] |
| 't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n', |
| 0x00, // MIME type |
| 'f', 'i', 'l', 'e', 0x00, // Filename |
| 0x00, // Content description (empty) |
| }; |
| auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame)); |
| riegeli::StringReader<> input(geob_frame_contents); |
| auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0, |
| /*data_length=*/geob_frame_contents.size()); |
| ASSERT_THAT(geob_or, IsOk()); |
| GeobFrame geob = *geob_or; |
| |
| EXPECT_EQ(geob.mime_type, "text/plain"); |
| EXPECT_EQ(geob.encapsulated_object, ""); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderNoExtendedHeader) { |
| const char id3_header[] = { |
| 'I', 'D', '3', // ID3 identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x00, // Flags |
| 0x00, 0x00, 0x00, 127, // Tag size |
| }; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| auto header_or = Id3Header::Parse(input); |
| ASSERT_THAT(header_or, IsOk()); |
| Id3Header header = *header_or; |
| |
| EXPECT_FALSE(header.use_unsynchronisation); |
| EXPECT_FALSE(header.has_extended_header); |
| EXPECT_FALSE(header.is_experimental); |
| EXPECT_EQ(header.tag_size, 127); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderWithValidExtendedHeader) { |
| const char id3_header[] = { |
| 'I', 'D', '3', // ID3 identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x40, // Flags (extended header) |
| 0x00, 0x00, 0x00, 127, // Tag size |
| 0x00, 0x00, 0x00, 6, // Extended header size |
| 0x00, 0x00, // Extended flags |
| 0x00, 0x00, 0x00, 0x00, // Padding size |
| }; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| auto header_or = Id3Header::Parse(input); |
| ASSERT_THAT(header_or, IsOk()); |
| Id3Header header = *header_or; |
| |
| EXPECT_FALSE(header.use_unsynchronisation); |
| EXPECT_TRUE(header.has_extended_header); |
| EXPECT_FALSE(header.is_experimental); |
| EXPECT_EQ(header.tag_size, 127); |
| EXPECT_EQ(header.extended_header_size, 6); |
| EXPECT_EQ(header.extended_flags, std::string("\0\0", 2)); |
| EXPECT_EQ(header.padding_size, 0); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderInvalidIdentifierFails) { |
| const char id3_header[] = { |
| 'I', 'D', '4', // Invalid ID3 identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x00, // Flags |
| 0x00, 0x00, 0x00, 127, // Tag size |
| }; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| EXPECT_THAT(Id3Header::Parse(input), |
| StatusIs(absl::StatusCode::kNotFound, "ID3v2 tag not found")); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderInvalidVersionFails) { |
| const char id3_header[] = { |
| 'I', 'D', '3', // ID3 identifier |
| 0x03, 0x01, // Invalid version |
| 0x00, // Flags |
| 0x00, 0x00, 0x00, 127, // Tag size |
| }; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| EXPECT_THAT( |
| Id3Header::Parse(input), |
| StatusIs(absl::StatusCode::kNotFound, HasSubstr("ID3v2 tag not found"))); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderInvalidFlagFails) { |
| const char id3_header[] = { |
| 'I', 'D', '3', // ID3 identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x01, // Invalid flags |
| 0x00, 0x00, 0x00, 127, // Tag size |
| }; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| EXPECT_THAT( |
| Id3Header::Parse(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, "invalid ID3v2 flag byte")); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderZeroSizeFails) { |
| const char id3_header[] = { |
| 'I', 'D', '3', // ID3 identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x00, // Flags |
| 0x00, 0x00, 0x00, 0x00, // Tag size = 0 |
| }; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| EXPECT_THAT(Id3Header::Parse(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| "no frame inside the ID3v2 tag")); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderInvalidExtendedHeaderSizeFails) { |
| const char id3_header[] = { |
| 'I', 'D', '3', // ID3 identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x40, // Flags (extended header) |
| 0x00, 0x00, 0x00, 127, // Tag size |
| 0x00, 0x00, 0x00, 7, // Invalid extended header size |
| 0x00, 0x00, // Extended flags |
| 0x00, 0x00, 0x00, 0x00, // Padding size |
| }; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| EXPECT_THAT(Id3Header::Parse(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| "invalid extended header size value")); |
| } |
| |
| TEST(Id3HeaderTest, ParseId3HeaderNoFrameWithExtendedHeader) { |
| constexpr char id3_header[] = { |
| 'I', 'D', '3', // ID3 identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x40, // Flags (extended header) |
| 0x00, 0x00, 0x00, 0x0e, // Tag size |
| 0x00, 0x00, 0x00, 0x0a, // Extended header size = 10 |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; |
| auto id3_header_contents = std::string(id3_header, sizeof(id3_header)); |
| riegeli::StringReader<> input(id3_header_contents); |
| EXPECT_THAT(Id3Header::Parse(input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("no frame inside the ID3v2 tag"))); |
| } |
| |
| TEST(Id3HeaderTest, EncodeId3HeaderNoExtendedHeader) { |
| Id3Header header = { |
| .use_unsynchronisation = false, |
| .has_extended_header = false, |
| .is_experimental = false, |
| .tag_size = 127, |
| }; |
| |
| EXPECT_EQ(header.ToCord(), |
| absl::string_view("ID3\x03\x00\x00\x00\x00\x00\x7F", 10)); |
| } |
| |
| TEST(Id3HeaderTest, EncodeId3HeaderWithExtendedHeader6Bytes) { |
| Id3Header header = { |
| .use_unsynchronisation = false, |
| .has_extended_header = true, |
| .is_experimental = false, |
| .tag_size = 127, |
| .extended_header_size = 6, |
| .extended_flags = std::string("\0\0", 2), |
| .padding_size = 0, |
| }; |
| |
| EXPECT_EQ( |
| header.ToCord(), |
| absl::string_view( |
| "ID3\x03\x00\x40\x00\x00\x00\x7F\x00\x00\x00\x06\x00\x00\x00\x00" |
| "\x00\x00", |
| 20)); |
| } |
| |
| TEST(Id3HeaderTest, EncodeId3HeaderWithExtendedHeader10Bytes) { |
| Id3Header header = { |
| .use_unsynchronisation = false, |
| .has_extended_header = true, |
| .is_experimental = false, |
| .tag_size = 127, |
| .extended_header_size = 10, |
| .extended_flags = std::string("\0\0", 2), |
| .padding_size = 0, |
| .total_frame_crc = "\x01\x02\x03\x04", |
| }; |
| |
| EXPECT_EQ( |
| header.ToCord(), |
| absl::string_view( |
| "ID3\x03\x00\x40\x00\x00\x00\x7F\x00\x00\x00\x0A\x00\x00\x00\x00" |
| "\x00\x00\x01\x02\x03\x04", |
| 24)); |
| } |
| |
| TEST(Id3HeaderTest, EncodeId3HeaderWithAllFlags) { |
| Id3Header header = { |
| .use_unsynchronisation = true, |
| .has_extended_header = true, |
| .is_experimental = true, |
| .tag_size = 127, |
| .extended_header_size = 6, |
| .extended_flags = std::string("\0\0", 2), |
| .padding_size = 0, |
| }; |
| |
| EXPECT_EQ( |
| header.ToCord(), |
| absl::string_view( |
| "ID3\x03\x00\xE0\x00\x00\x00\x7F\x00\x00\x00\x06\x00\x00\x00\x00" |
| "\x00\x00", |
| 20)); |
| } |
| } // namespace |
| } // namespace credentio |