| // 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::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()); | |
| ABSL_ASSERT_OK(geob_or); | |
| 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()); | |
| ABSL_ASSERT_OK(geob_or); | |
| 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()); | |
| ABSL_ASSERT_OK(geob_or); | |
| 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()); | |
| ABSL_ASSERT_OK(geob_or); | |
| 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()); | |
| ABSL_ASSERT_OK(geob_or); | |
| 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()); | |
| ABSL_ASSERT_OK(geob_or); | |
| 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); | |
| ABSL_ASSERT_OK(header_or); | |
| 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); | |
| ABSL_ASSERT_OK(header_or); | |
| 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 |