blob: 333187499d5352d2f5833d96693c7b7d9d65d4b7 [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/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