| // 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/id3.h" | |
| #include <cstdint> | |
| #include <string> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/cord.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "formats/id3/constants.h" | |
| #include "riegeli/bytes/reader.h" | |
| #include "riegeli/endian/endian_reading.h" | |
| #include "utils/riegeli.h" | |
| namespace credentio { | |
| namespace { | |
| constexpr uint64_t kMaxPayloadSize = 1024 * 1024 * 10; // 10 MiB | |
| constexpr int kUtf16BomSize = 2; | |
| enum class TextEncoding : uint8_t { | |
| // Text terminated with 0x00. | |
| kIso8859_1 = 0x00, | |
| // UTF-16 encoded Unicode with BOM. | |
| // Text terminated with 0x00 0x00. | |
| kUnicode = 0x01, | |
| // UTF-16BE encoded Unicode without BOM. | |
| // Text terminated with 0x00 0x00. | |
| kUtf16Be = 0x02, | |
| // UTF-8 encoded Unicode. | |
| // Text terminated with 0x00. | |
| kUtf8 = 0x03, | |
| }; | |
| absl::StatusOr<TextEncoding> ReadTextEncoding(riegeli::Reader& input) { | |
| uint8_t text_encoding_byte; | |
| if (!input.ReadByte(text_encoding_byte)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read text encoding byte")); | |
| } | |
| if (text_encoding_byte <= 0x03) { | |
| return static_cast<TextEncoding>(text_encoding_byte); | |
| } | |
| return absl::FailedPreconditionError( | |
| absl::StrCat("Invalid text encoding value: ", text_encoding_byte)); | |
| } | |
| absl::StatusOr<std::string> ReadUtf8Text(riegeli::Reader& input, | |
| uint64_t max_length) { | |
| std::string output; | |
| if (!ReadNullTerminatedString(input, max_length, output)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read utf-8 text")); | |
| } | |
| return output; | |
| } | |
| absl::Status SkipTerminatedUnicodeText(riegeli::Reader& input, | |
| uint64_t max_length, bool read_bom) { | |
| uint64_t read_size = 0; | |
| if (read_bom) { | |
| read_size = kUtf16BomSize; | |
| if (!input.Skip(kUtf16BomSize)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to skip UTF-16 BOM")); | |
| } | |
| } | |
| uint16_t wide_char; | |
| while (read_size < max_length) { | |
| read_size += 2; | |
| if (!riegeli::ReadBigEndian<uint16_t>(input, wide_char)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read UTF-16 Character")); | |
| } | |
| if (wide_char == 0) { | |
| break; | |
| } | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status SkipTerminatedText(riegeli::Reader& input, uint64_t max_length, | |
| TextEncoding text_encoding) { | |
| switch (text_encoding) { | |
| case TextEncoding::kIso8859_1: | |
| // ISO-8859-1 uses the same encoding as UTF-8 for the ASCII subset. | |
| return ReadUtf8Text(input, max_length).status(); | |
| case TextEncoding::kUnicode: | |
| return SkipTerminatedUnicodeText(input, max_length, /*read_bom=*/true); | |
| case TextEncoding::kUtf16Be: | |
| return SkipTerminatedUnicodeText(input, max_length, /*read_bom=*/false); | |
| case TextEncoding::kUtf8: | |
| return ReadUtf8Text(input, max_length).status(); | |
| default: | |
| return absl::UnimplementedError( | |
| absl::StrCat("Unsupported text encoding: ", text_encoding)); | |
| } | |
| } | |
| // Computes the ID3v2.3.0 tag size from a 4-byte syncsafe integer string. | |
| // IDv2.3.0 uses syncsafe integer strings for the sizes. | |
| absl::StatusOr<uint32_t> GetSizeFromSyncsafeBytes( | |
| absl::string_view syncsafe_bytes) { | |
| if (syncsafe_bytes.length() != 4) { | |
| return absl::InvalidArgumentError("Size should be 4 bytes"); | |
| } | |
| // Cast the characters to unsigned 8-bit integers (bytes) | |
| uint8_t byte1 = static_cast<uint8_t>(syncsafe_bytes[0]) & 0b01111111; | |
| uint8_t byte2 = static_cast<uint8_t>(syncsafe_bytes[1]) & 0b01111111; | |
| uint8_t byte3 = static_cast<uint8_t>(syncsafe_bytes[2]) & 0b01111111; | |
| uint8_t byte4 = static_cast<uint8_t>(syncsafe_bytes[3]) & 0b01111111; | |
| // Combine the 7-bit chunks from each byte | |
| uint32_t size = (byte1 << 21) | (byte2 << 14) | (byte3 << 7) | byte4; | |
| return size; | |
| } | |
| } // namespace | |
| absl::StatusOr<GeobFrame> GeobFrame::Parse(riegeli::Reader& input, | |
| uint64_t data_offset, | |
| uint64_t data_length) { | |
| if (!input.Seek(data_offset)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to seek to start of data")); | |
| } | |
| GeobFrame frame; | |
| const uint64_t end_offset = data_offset + data_length; | |
| ABSL_ASSIGN_OR_RETURN(auto text_encoding, ReadTextEncoding(input)); | |
| if (input.pos() > end_offset) { | |
| return absl::DataLossError( | |
| "Data structure exceeded specified offset boundary limit."); | |
| } | |
| // MIME type is always ISO-8859-1 encoded. | |
| ABSL_ASSIGN_OR_RETURN( | |
| frame.mime_type, | |
| ReadUtf8Text(input, /*max_length=*/end_offset - input.pos())); | |
| if (input.pos() > end_offset) { | |
| return absl::DataLossError( | |
| "Data structure exceeded specified offset boundary limit."); | |
| } | |
| // Skip filename. | |
| ABSL_RETURN_IF_ERROR(SkipTerminatedText( | |
| input, /*max_length=*/end_offset - input.pos(), text_encoding)); | |
| if (input.pos() > end_offset) { | |
| return absl::DataLossError( | |
| "Data structure exceeded specified offset boundary limit."); | |
| } | |
| // Skip content description. | |
| ABSL_RETURN_IF_ERROR(SkipTerminatedText( | |
| input, /*max_length=*/end_offset - input.pos(), text_encoding)); | |
| if (input.pos() > end_offset) { | |
| return absl::DataLossError( | |
| "Data structure exceeded specified offset boundary limit."); | |
| } | |
| // Read the encapsulated object as raw bytes. | |
| uint64_t payload_size = end_offset - input.pos(); | |
| if (payload_size > kMaxPayloadSize) { | |
| return absl::InvalidArgumentError( | |
| "Encapsulated object size exceeds maximum payload size"); | |
| } | |
| if (!input.Read(payload_size, frame.encapsulated_object)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read encapsulated object")); | |
| } | |
| return frame; | |
| } | |
| absl::StatusOr<Id3Header> Id3Header::Parse(riegeli::Reader& input) { | |
| Id3Header id3_header; | |
| std::string identifier_and_version; | |
| if (!input.Read(5, identifier_and_version)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read id and version")); | |
| } | |
| if (identifier_and_version == | |
| std::string(kId3FileIdentifierAndVersionV23, 5)) { | |
| id3_header.major_version = 3; | |
| } else if (identifier_and_version == | |
| std::string(kId3FileIdentifierAndVersionV24, 5)) { | |
| id3_header.major_version = 4; | |
| } else { | |
| return absl::NotFoundError("ID3v2 tag not found"); | |
| } | |
| // Read the flag byte | |
| uint8_t flag_byte; | |
| if (!input.ReadByte(flag_byte)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read flag byte")); | |
| } | |
| // Check that the flag is valid | |
| // Only bits 5-7 should be set. | |
| if ((flag_byte & 0b00011111) != 0) { | |
| return absl::InvalidArgumentError("invalid ID3v2 flag byte"); | |
| } | |
| // Check bit 7 for the presence of the unsynchronisation bit | |
| id3_header.use_unsynchronisation = (flag_byte & 0b10000000) != 0; | |
| // Check bit 6 for the presence of the extended header bit | |
| id3_header.has_extended_header = (flag_byte & 0b01000000) != 0; | |
| // Check bit 5 for the presence of the experimental indicator bit | |
| id3_header.is_experimental = (flag_byte & 0b00100000) != 0; | |
| std::string syncsafe_bytes; | |
| if (!input.Read(4, syncsafe_bytes)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read syncsafe bytes")); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(uint32_t size, | |
| GetSizeFromSyncsafeBytes(syncsafe_bytes)); | |
| if (size == 0) { | |
| return absl::InvalidArgumentError("no frame inside the ID3v2 tag"); | |
| } | |
| // `size` is the size of the tag, not including the header but including the | |
| // extended header if present. | |
| id3_header.tag_size = size; | |
| // Process the extended header if present. | |
| if (id3_header.has_extended_header) { | |
| // Read the size of the extended header, excluding the size field itself. | |
| // Header size field is 4 bytes. | |
| // The first 3 bytes should be zero. | |
| // The last byte should be 0x0A (ten) or 0x06 (six). | |
| if (!riegeli::ReadBigEndian<uint32_t>(input, | |
| id3_header.extended_header_size)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed extracting the extended header size")); | |
| } | |
| if (id3_header.extended_header_size != 10 && | |
| id3_header.extended_header_size != 6) { | |
| return absl::InvalidArgumentError("invalid extended header size value"); | |
| } | |
| if (size == id3_header.extended_header_size + 4) { | |
| // If the size of the extended header is the same as the size of the tag, | |
| // it means that there are no frames inside the tag. | |
| return absl::InvalidArgumentError("no frame inside the ID3v2 tag"); | |
| } | |
| if (!input.Read(2, id3_header.extended_flags)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed extracting the extended flags")); | |
| } | |
| if (!riegeli::ReadBigEndian<uint32_t>(input, id3_header.padding_size)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed extracting the padding size")); | |
| } | |
| if (id3_header.extended_header_size == 10) { | |
| if (!input.Read(4, id3_header.total_frame_crc)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed extracting the total frame crc")); | |
| } | |
| } | |
| } | |
| return id3_header; | |
| } | |
| absl::Cord Id3Header::ToCord() const { | |
| absl::Cord output; | |
| output.Append(absl::string_view("ID3", 3)); | |
| if (major_version == 4) { | |
| output.Append(absl::string_view("\x04\x00", 2)); // Version 2.4 | |
| } else { | |
| output.Append(absl::string_view("\x03\x00", 2)); // Version 2.3 | |
| } | |
| // Flags | |
| char flags = 0; | |
| if (use_unsynchronisation) { | |
| flags |= 1 << 7; | |
| } | |
| if (has_extended_header) { | |
| flags |= 1 << 6; | |
| } | |
| if (is_experimental) { | |
| flags |= 1 << 5; | |
| } | |
| output.Append(absl::string_view(&flags, 1)); | |
| char tag_size_bytes[4]; | |
| tag_size_bytes[0] = (tag_size >> 21) & 0x7F; | |
| tag_size_bytes[1] = (tag_size >> 14) & 0x7F; | |
| tag_size_bytes[2] = (tag_size >> 7) & 0x7F; | |
| tag_size_bytes[3] = tag_size & 0x7F; | |
| output.Append(absl::string_view(tag_size_bytes, 4)); | |
| if (has_extended_header) { | |
| // Append the size of the extended header (4 bytes, big-endian). | |
| char extended_header_size_bytes[4]; | |
| extended_header_size_bytes[0] = (extended_header_size >> 24) & 0xFF; | |
| extended_header_size_bytes[1] = (extended_header_size >> 16) & 0xFF; | |
| extended_header_size_bytes[2] = (extended_header_size >> 8) & 0xFF; | |
| extended_header_size_bytes[3] = extended_header_size & 0xFF; | |
| output.Append(absl::string_view(extended_header_size_bytes, 4)); | |
| // Append the extended flags (2 bytes). | |
| output.Append(extended_flags); | |
| // Append the padding size (4 bytes, big-endian). | |
| char padding_size_bytes[4]; | |
| padding_size_bytes[0] = (padding_size >> 24) & 0xFF; | |
| padding_size_bytes[1] = (padding_size >> 16) & 0xFF; | |
| padding_size_bytes[2] = (padding_size >> 8) & 0xFF; | |
| padding_size_bytes[3] = padding_size & 0xFF; | |
| output.Append(absl::string_view(padding_size_bytes, 4)); | |
| if (extended_header_size == 10) { | |
| // Append the total frame CRC (4 bytes). | |
| output.Append(total_frame_crc); | |
| } | |
| } | |
| return output; | |
| } | |
| } // namespace credentio |