blob: f445017ce4033cb155f3dbc91aec3542d0ab583b [file]
// 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