| // 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/extractor.h" |
| |
| #include <string> |
| |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/strings/string_view.h" |
| #include "formats/byte_range.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::AllOf; |
| using ::testing::Eq; |
| using ::testing::Field; |
| using ::testing::HasSubstr; |
| using ::testing::Optional; |
| |
| #ifndef ASSERT_OK_AND_ASSIGN |
| #define ASSERT_OK_AND_ASSIGN_CONCAT2(x, y) x##y |
| #define ASSERT_OK_AND_ASSIGN_CONCAT(x, y) ASSERT_OK_AND_ASSIGN_CONCAT2(x, y) |
| |
| #define ASSERT_OK_AND_ASSIGN(lhs, rexpr) \ |
| ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, __COUNTER__) |
| |
| #define ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, id) \ |
| auto ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id) = (rexpr); \ |
| ASSERT_THAT(ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id), \ |
| ::absl_testing::IsOk()); \ |
| lhs = std::move(*ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id)) |
| #endif |
| |
| constexpr unsigned char kId3TagWithC2paManifestStore[] = { |
| 'I', 'D', '3', // ID3 file identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x00, // Flags |
| 0x00, 0x00, 0x00, 0x3E, // Tag size = 62 (52 + 10) |
| 'G', 'E', 'O', 'B', // Frame ID |
| 0x00, 0x00, 0x00, 0x34, // Frame Size = 52 |
| 0x00, 0x00, // Frame flags |
| 0x01, // Text encoding [UTF-16] |
| // MIME type |
| 'a', 'p', 'p', 'l', 'i', 'c', 'a', 't', 'i', 'o', 'n', '/', 'c', '2', 'p', |
| 'a', 0x00, |
| // File name [UTF-16LE] |
| 0xFF, 0xFE, 'f', 0x00, 'o', 0x00, 'o', 0x00, 0x00, 0x00, |
| // Content description [UTF-16LE] |
| 0xFF, 0xFE, 'b', 0x00, 'a', 0x00, 'r', 0x00, 0x00, 0x00, |
| // Encapsulated object |
| 'm', 'a', 'n', 'i', 'f', 'e', 's', 't', ' ', 's', 't', 'o', 'r', 'e'}; |
| |
| constexpr unsigned char kId3TagWithMultipleC2paManifestStores[] = { |
| 'I', 'D', '3', // ID3 file identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x00, // Flags |
| 0x00, 0x00, 0x00, 0x7C, // Tag size = 124 (52 + 10 + 52 + 10) |
| // Frame 1 |
| 'G', 'E', 'O', 'B', // Frame ID |
| 0x00, 0x00, 0x00, 0x34, // Frame Size = 52 |
| 0x00, 0x00, // Frame flags |
| 0x01, // Text encoding [UTF-16] |
| // MIME type |
| 'a', 'p', 'p', 'l', 'i', 'c', 'a', 't', 'i', 'o', 'n', '/', 'c', '2', 'p', |
| 'a', 0x00, |
| // File name [UTF-16LE] |
| 0xFF, 0xFE, 'f', 0x00, 'o', 0x00, 'o', 0x00, 0x00, 0x00, |
| // Content description [UTF-16LE] |
| 0xFF, 0xFE, 'b', 0x00, 'a', 0x00, 'r', 0x00, 0x00, 0x00, |
| // Encapsulated object |
| 'm', 'a', 'n', 'i', 'f', 'e', 's', 't', ' ', 's', 't', 'o', 'r', 'e', |
| // Frame 2 |
| 'G', 'E', 'O', 'B', // Frame ID |
| 0x00, 0x00, 0x00, 0x34, // Frame Size = 52 |
| 0x00, 0x00, // Frame flags |
| 0x01, // Text encoding [UTF-16] |
| // MIME type |
| 'a', 'p', 'p', 'l', 'i', 'c', 'a', 't', 'i', 'o', 'n', '/', 'c', '2', 'p', |
| 'a', 0x00, |
| // File name [UTF-16LE] |
| 0xFF, 0xFE, 'f', 0x00, 'o', 0x00, 'o', 0x00, 0x00, 0x00, |
| // Content description [UTF-16LE] |
| 0xFF, 0xFE, 'b', 0x00, 'a', 0x00, 'r', 0x00, 0x00, 0x00, |
| // Encapsulated object |
| 'm', 'a', 'n', 'i', 'f', 'e', 's', 't', ' ', 's', 't', 'o', 'r', 'e'}; |
| |
| constexpr unsigned char kId3TagWithC2paManifestStoreDeprecatedMimeType[] = { |
| 'I', 'D', '3', // ID3 file identifier |
| 0x03, 0x00, // Version 2.3 |
| 0x00, // Flags |
| 0x00, 0x00, 0x00, 0x4F, // Tag size = 79 (69 + 10) |
| 'G', 'E', 'O', 'B', // Frame ID |
| 0x00, 0x00, 0x00, 0x45, // Frame Size = 69 |
| 0x00, 0x00, // Frame flags |
| 0x01, // Text encoding [UTF-16] |
| // MIME type |
| 'a', 'p', 'p', 'l', 'i', 'c', 'a', 't', 'i', 'o', 'n', '/', 'x', '-', 'c', |
| '2', 'p', 'a', '-', 'm', 'a', 'n', 'i', 'f', 'e', 's', 't', '-', 's', 't', |
| 'o', 'r', 'e', 0x00, |
| // File name [UTF-16LE] |
| 0xFF, 0xFE, 'f', 0x00, 'o', 0x00, 'o', 0x00, 0x00, 0x00, |
| // Content description [UTF-16LE] |
| 0xFF, 0xFE, 'b', 0x00, 'a', 0x00, 'r', 0x00, 0x00, 0x00, |
| // Encapsulated object |
| 'm', 'a', 'n', 'i', 'f', 'e', 's', 't', ' ', 's', 't', 'o', 'r', 'e'}; |
| |
| TEST(Id3ExtractorTest, |
| ExtractManifestStoreFromGeobFrameWithDeprecatedMimeType) { |
| std::string mp3_file_contents = |
| std::string(reinterpret_cast<const char*>( |
| kId3TagWithC2paManifestStoreDeprecatedMimeType), |
| sizeof(kId3TagWithC2paManifestStoreDeprecatedMimeType)); |
| riegeli::StringReader<> input(mp3_file_contents); |
| Id3Extractor extractor; |
| auto result = extractor.ExtractManifestStore(input); |
| ASSERT_THAT(result, IsOk()); |
| EXPECT_THAT(*result, HasSubstr("manifest store")); |
| } |
| |
| TEST(Id3ExtractorTest, ExtractManifestStore) { |
| std::string mp3_file_contents = |
| std::string(reinterpret_cast<const char*>(kId3TagWithC2paManifestStore), |
| sizeof(kId3TagWithC2paManifestStore)); |
| riegeli::StringReader<> input(mp3_file_contents); |
| Id3Extractor extractor; |
| auto result = extractor.ExtractManifestStore(input); |
| ASSERT_THAT(result, IsOk()); |
| EXPECT_THAT(*result, HasSubstr("manifest store")); |
| } |
| |
| TEST(Id3ExtractorTest, |
| ExtractManifestStoreThrowsErrorIfMultipleC2paManifestStoresFound) { |
| std::string mp3_file_contents = std::string( |
| reinterpret_cast<const char*>(kId3TagWithMultipleC2paManifestStores), |
| sizeof(kId3TagWithMultipleC2paManifestStores)); |
| riegeli::StringReader<> input(mp3_file_contents); |
| Id3Extractor extractor; |
| |
| EXPECT_THAT(extractor.ExtractManifestStore(input), |
| StatusIs(absl::StatusCode::kNotFound, |
| HasSubstr("Multiple manifest stores found"))); |
| } |
| |
| TEST(Id3ExtractorTest, ExtractManifestStoreLocation) { |
| std::string mp3_file_contents = |
| std::string(reinterpret_cast<const char*>(kId3TagWithC2paManifestStore), |
| sizeof(kId3TagWithC2paManifestStore)); |
| riegeli::StringReader<> input(mp3_file_contents); |
| Id3Extractor extractor; |
| ASSERT_OK_AND_ASSIGN( |
| auto manifest_store_location, |
| extractor.ExtractManifestStoreLocation(input, {.requires_c2pa = true})); |
| |
| EXPECT_THAT(manifest_store_location, |
| Optional(AllOf(Field(&ByteRange::offset, Eq(58)), |
| Field(&ByteRange::length, Eq(14))))); |
| } |
| |
| TEST(Id3ExtractorTest, ExtractBoxesUnimplemented) { |
| riegeli::StringReader<> input("test_content"); |
| Id3Extractor extractor; |
| EXPECT_THAT(extractor.ExtractBoxes(input, {.requires_c2pa = true}), |
| StatusIs(absl::StatusCode::kUnimplemented)); |
| } |
| |
| } // namespace |
| } // namespace credentio |