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