blob: 3f6f450952d803e02e6fdb0e489599f6f67e1b3d [file] [edit]
// 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 <cstdint>
#include <utility>
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/cord.h"
#include "absl/strings/string_view.h"
#include "formats/id3/constants.h"
#include "formats/id3/id3.h"
#include "formats/id3/reader.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/writer.h"
namespace credentio {
namespace {
// The size of the ID3 header excluded from the ID3 tag size calculation.
constexpr uint32_t kExcludedId3TagHeaderSize = 10;
// The size of the ID3 extended header excluded from the ID3 extended header
// size calculation.
constexpr uint32_t kExcludedId3ExtendedHeaderSize = 4;
constexpr absl::string_view kGeobFrameFileName = "c2pa";
constexpr absl::string_view kGeobFrameContentDescription =
"c2pa manifest store";
// Encodes the C2PA manifest store as an ID3 GEOB frame, returns the encoded
// frame as an `absl::Cord`. ID3v2.4 frame sizes are encoded as synchsafe
// integers, whereas ID3v2.3 frame sizes are 32-bit big-endian integers.
absl::StatusOr<absl::Cord> EncodeManifestStoreAsId3Frame(
absl::string_view manifest_store, uint8_t major_version) {
absl::Cord geob_frame_data;
// Encode manifest store as the GEOB frame data.
geob_frame_data.Append(
absl::string_view("\x00", 1)); // Text encoding [ISO-8859-1]
geob_frame_data.Append(kGeobFrameMimeType); // MIME type
geob_frame_data.Append(absl::string_view("\x00", 1)); // NULL terminator
geob_frame_data.Append(kGeobFrameFileName); // File name
geob_frame_data.Append(absl::string_view("\x00", 1)); // NULL terminator
geob_frame_data.Append(kGeobFrameContentDescription); // Content description
geob_frame_data.Append(absl::string_view("\x00", 1)); // NULL terminator
geob_frame_data.Append(manifest_store); // Manifest store as encapsulated
// object
absl::Cord output;
// Write the GEOB frame header.
output.Append(absl::string_view("GEOB", 4));
uint32_t frame_size = geob_frame_data.size();
char size_bytes[4];
if (major_version == 4) {
if (frame_size > 0x0FFFFFFF) {
return absl::InvalidArgumentError(
"Frame size exceeds maximum allowable ID3v2.4 frame size");
}
// Encode the frame size as a synchsafe integer.
size_bytes[0] = (frame_size >> 21) & 0x7F;
size_bytes[1] = (frame_size >> 14) & 0x7F;
size_bytes[2] = (frame_size >> 7) & 0x7F;
size_bytes[3] = frame_size & 0x7F;
} else { // ID3v2.3
size_bytes[0] = (frame_size >> 24) & 0xFF;
size_bytes[1] = (frame_size >> 16) & 0xFF;
size_bytes[2] = (frame_size >> 8) & 0xFF;
size_bytes[3] = frame_size & 0xFF;
}
output.Append(absl::string_view(size_bytes, 4));
output.Append(absl::string_view("\x00\x00", 2)); // Flags
// Append the GEOB frame data.
output.Append(std::move(geob_frame_data));
return output;
}
} // namespace
absl::StatusOr<int64_t> Id3Embedder::CopyAndEmbed(
riegeli::Reader& input, absl::string_view manifest_store,
riegeli::Writer& output) const {
if (!input.SupportsSize() || !input.Size().has_value()) {
return absl::InvalidArgumentError("input does not support size");
}
uint64_t asset_size = input.Size().value();
int64_t beginning_offset = input.pos();
auto status = IterateOverId3Frames(
input, [&](const Id3Frame& frame) -> absl::StatusOr<bool> {
if (frame.id == "GEOB") {
ABSL_ASSIGN_OR_RETURN(auto geob_frame,
GeobFrame::Parse(input, frame.data_offset(),
frame.data_length()));
if (geob_frame.mime_type == kGeobFrameMimeType ||
geob_frame.mime_type == kGeobFrameMimeTypeDeprecated) {
return absl::AlreadyExistsError(
"C2PA manifest store already exists.");
}
}
return true;
});
if (absl::IsNotFound(status.status())) {
// No ID3v2 tag found in the input audio file, so we prepend a simplest ID3
// tag with only one GEOB frame for the manifest store.
ABSL_ASSIGN_OR_RETURN(
absl::Cord manifest_store_frame_encoded,
EncodeManifestStoreAsId3Frame(manifest_store, /*major_version=*/3));
Id3Header id3_header = {
.use_unsynchronisation = false,
.has_extended_header = false,
.is_experimental = false,
.tag_size = static_cast<uint32_t>(manifest_store_frame_encoded.size()),
};
if (!output.Write(id3_header.ToCord())) {
return output.StatusOrAnnotate(
absl::InternalError("failed to write ID3 header"));
}
if (!output.Write(manifest_store_frame_encoded)) {
return output.StatusOrAnnotate(
absl::InternalError("failed to write manifest store frame"));
}
int64_t manifest_store_offset = output.pos() - manifest_store.size();
if (!input.Seek(beginning_offset)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to seek to start of input"));
}
if (!input.Copy(asset_size - beginning_offset, output)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to copy input"));
}
return manifest_store_offset;
}
if (!status.ok()) {
// Propagate other errors during reader creation.
return status.status();
}
Id3Header id3_header = status.value();
uint32_t id3_header_size =
kExcludedId3TagHeaderSize +
(id3_header.has_extended_header
? kExcludedId3ExtendedHeaderSize + id3_header.extended_header_size
: 0);
ABSL_ASSIGN_OR_RETURN(
absl::Cord manifest_store_frame,
EncodeManifestStoreAsId3Frame(manifest_store, id3_header.major_version));
// Update the tag size to include the new manifest store frame.
if (static_cast<uint64_t>(id3_header.tag_size) + manifest_store_frame.size() >
0x0FFFFFFF) {
return absl::InvalidArgumentError(
"Tag size exceeds maximum allowable ID3 tag size");
}
id3_header.tag_size += static_cast<uint32_t>(manifest_store_frame.size());
if (!output.Write(id3_header.ToCord())) {
return output.StatusOrAnnotate(
absl::InternalError("failed to write ID3 header"));
}
if (!output.Write(manifest_store_frame)) {
return output.StatusOrAnnotate(
absl::InternalError("failed to write manifest store frame"));
}
int64_t manifest_store_offset = output.pos() - manifest_store.size();
// Copy the rest of the input audio file.
if (!input.Seek(beginning_offset + id3_header_size)) {
return input.StatusOrAnnotate(
absl::InternalError("failed to seek to start of input"));
}
if (!input.Copy(asset_size - id3_header_size, output)) {
return input.StatusOrAnnotate(absl::InternalError("failed to copy input"));
}
return manifest_store_offset;
}
} // namespace credentio