| // 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/embedder.h" | |
| #include <string> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/strings/string_view.h" | |
| #include "formats/id3/extractor.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| #include "riegeli/bytes/string_writer.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::testing::Eq; | |
| using ::testing::HasSubstr; | |
| // Simple MPEG-2 Layer 3 frame header followed by 4 bytes of dummy data. | |
| constexpr char kMp3FileWithNoId3Tag[] = {'\xFF', '\xF3', '\x40', '\xC0', | |
| 0x01, 0x02, 0x03, 0x04}; | |
| constexpr char kMp3FileWithNoC2paFrame[] = { | |
| 'I', 'D', '3', '\x03', '\x00', '\x00', '\x00', '\x00', '\x00', | |
| '\x0F', // Tag size = 15 | |
| // A 15-byte COMM frame | |
| 'C', 'O', 'M', 'M', '\x00', '\x00', '\x00', '\x05', '\x00', '\x00', '\x00', | |
| '\x00', '\x00', '\x00', '\x00'}; | |
| constexpr char kMp3FileWithId3v24NoC2paFrame[] = { | |
| 'I', 'D', '3', '\x04', '\x00', '\x00', '\x00', '\x00', '\x00', | |
| '\x0F', // Tag size = 15 | |
| // A 15-byte COMM frame | |
| 'C', 'O', 'M', 'M', '\x00', '\x00', '\x00', '\x05', '\x00', '\x00', '\x00', | |
| '\x00', '\x00', '\x00', '\x00'}; | |
| constexpr char kMp3FileWithNoC2paFrameWithExtendedHeader[] = { | |
| 'I', 'D', '3', // ID3 file identifier | |
| 0x03, 0x00, // Version 2.3 | |
| 0x40, // Flags (bit 6 indicates extended header) | |
| 0x00, 0x00, 0x00, 0x19, // Tag size = 25 | |
| // Extended header | |
| 0x00, 0x00, 0x00, 0x06, // Extended header size = 6 | |
| 0x00, 0x00, // Extended flags | |
| 0x00, 0x00, 0x00, 0x00, // Size of padding | |
| // A 15-byte COMM frame | |
| 'C', 'O', 'M', 'M', 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, | |
| 0x00, 0x00}; | |
| constexpr char kMp3FileWithC2paManifestStore[] = { | |
| '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] | |
| '\xFF', '\xFE', 'f', 0x00, 'o', 0x00, 'o', 0x00, 0x00, 0x00, | |
| // Content description [UTF-16LE] | |
| '\xFF', '\xFE', 'b', 0x00, 'a', 0x00, 'r', 0x00, 0x00, 0x00, | |
| // Encapsulated object | |
| 'm', 'a', 'n', 'i', 'f', 'e', 's', 't', ' ', 's', 't', 'o', 'r', 'e'}; | |
| TEST(Id3EmbedderTest, CopyAndEmbedNoId3Tag) { | |
| std::string mp3_file_contents = | |
| std::string(kMp3FileWithNoId3Tag, sizeof(kMp3FileWithNoId3Tag)); | |
| absl::string_view kManifestStore = "C2PA manifest store bytes"; | |
| riegeli::StringReader<> input(mp3_file_contents); | |
| std::string output_s; | |
| riegeli::StringWriter<> output(&output_s); | |
| Id3Embedder embedder; | |
| auto manifest_store_offset = | |
| embedder.CopyAndEmbed(input, kManifestStore, output); | |
| ABSL_ASSERT_OK(manifest_store_offset.status()); | |
| ASSERT_TRUE(output.Close()); | |
| EXPECT_THAT(output_s.substr(*manifest_store_offset, kManifestStore.size()), | |
| Eq(kManifestStore)); | |
| Id3Extractor extractor; | |
| riegeli::StringReader<> output_reader(output_s); | |
| EXPECT_THAT(extractor.ExtractManifestStore(output_reader), | |
| IsOkAndHolds(kManifestStore)); | |
| } | |
| TEST(Id3EmbedderTest, CopyAndEmbedNoC2paFrame) { | |
| std::string mp3_file_contents = | |
| std::string(kMp3FileWithNoC2paFrame, sizeof(kMp3FileWithNoC2paFrame)); | |
| absl::string_view kManifestStore = "C2PA manifest store bytes"; | |
| riegeli::StringReader input(mp3_file_contents); | |
| std::string output_s; | |
| riegeli::StringWriter<> output(&output_s); | |
| Id3Embedder embedder; | |
| auto manifest_store_offset = | |
| embedder.CopyAndEmbed(input, kManifestStore, output); | |
| ABSL_ASSERT_OK(manifest_store_offset.status()); | |
| ASSERT_TRUE(output.Close()); | |
| EXPECT_THAT(output_s.substr(*manifest_store_offset, kManifestStore.size()), | |
| Eq(kManifestStore)); | |
| Id3Extractor extractor; | |
| riegeli::StringReader<> output_reader(output_s); | |
| EXPECT_THAT(extractor.ExtractManifestStore(output_reader), | |
| IsOkAndHolds(kManifestStore)); | |
| } | |
| TEST(Id3EmbedderTest, CopyAndEmbedNoC2paFrameWithExtendedHeader) { | |
| std::string mp3_file_contents = | |
| std::string(kMp3FileWithNoC2paFrameWithExtendedHeader, | |
| sizeof(kMp3FileWithNoC2paFrameWithExtendedHeader)); | |
| absl::string_view kManifestStore = "C2PA manifest store bytes"; | |
| riegeli::StringReader input(mp3_file_contents); | |
| std::string output_s; | |
| riegeli::StringWriter<> output(&output_s); | |
| Id3Embedder embedder; | |
| auto manifest_store_offset = | |
| embedder.CopyAndEmbed(input, kManifestStore, output); | |
| ABSL_ASSERT_OK(manifest_store_offset.status()); | |
| ASSERT_TRUE(output.Close()); | |
| EXPECT_THAT(output_s.substr(*manifest_store_offset, kManifestStore.size()), | |
| Eq(kManifestStore)); | |
| Id3Extractor extractor; | |
| riegeli::StringReader<> output_reader(output_s); | |
| EXPECT_THAT(extractor.ExtractManifestStore(output_reader), | |
| IsOkAndHolds(kManifestStore)); | |
| } | |
| TEST(Id3EmbedderTest, CopyAndEmbedId3v24SynchsafeFrameSize) { | |
| std::string mp3_file_contents = std::string( | |
| kMp3FileWithId3v24NoC2paFrame, sizeof(kMp3FileWithId3v24NoC2paFrame)); | |
| std::string manifest_store(300, 'a'); | |
| riegeli::StringReader input(mp3_file_contents); | |
| std::string output_s; | |
| riegeli::StringWriter<> output(&output_s); | |
| Id3Embedder embedder; | |
| auto manifest_store_offset = | |
| embedder.CopyAndEmbed(input, manifest_store, output); | |
| ABSL_ASSERT_OK(manifest_store_offset.status()); | |
| ASSERT_TRUE(output.Close()); | |
| EXPECT_THAT(output_s.substr(*manifest_store_offset, manifest_store.size()), | |
| Eq(manifest_store)); | |
| Id3Extractor extractor; | |
| riegeli::StringReader<> output_reader(output_s); | |
| EXPECT_THAT(extractor.ExtractManifestStore(output_reader), | |
| IsOkAndHolds(manifest_store)); | |
| } | |
| TEST(Id3EmbedderTest, CopyAndEmbedC2paAlreadyExists) { | |
| std::string mp3_file_contents = std::string( | |
| kMp3FileWithC2paManifestStore, sizeof(kMp3FileWithC2paManifestStore)); | |
| absl::string_view kManifestStore = "C2PA manifest store bytes"; | |
| riegeli::StringReader input(mp3_file_contents); | |
| std::string output_s; | |
| riegeli::StringWriter<> output(&output_s); | |
| Id3Embedder embedder; | |
| EXPECT_THAT(embedder.CopyAndEmbed(input, kManifestStore, output), | |
| StatusIs(absl::StatusCode::kAlreadyExists, | |
| HasSubstr("C2PA manifest store already exists"))); | |
| } | |
| } // namespace | |
| } // namespace credentio |