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