| // 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 |