blob: dacab974155c74f8d028d70ef06b3f6940630165 [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 <string>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/string_view.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/string_reader.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;
TEST(GeobFrameTest, ParseIso8859_1) {
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
'f', 'i', 'l', 'e', 0x00, // Filename
'd', 'e', 's', 'c', 0x00, // Content description
'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0,
/*data_length=*/geob_frame_contents.size());
ASSERT_THAT(geob_or, IsOk());
GeobFrame geob = *geob_or;
EXPECT_EQ(geob.mime_type, "text/plain");
EXPECT_EQ(geob.encapsulated_object, "object");
}
TEST(GeobFrameTest, ParseUtf16LeBom) {
const char geob_frame[] = {
0x01, // Text encoding [UTF-16]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
'\xff', '\xfe', // BOM
'f', 0x00, 0x00, 0x00, // Filename
'\xff', '\xfe', // BOM
'd', 0x00, 0x00, 0x00, // Content description
'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0,
/*data_length=*/geob_frame_contents.size());
ASSERT_THAT(geob_or, IsOk());
GeobFrame geob = *geob_or;
EXPECT_EQ(geob.mime_type, "text/plain");
EXPECT_EQ(geob.encapsulated_object, "object");
}
TEST(GeobFrameTest, ParseUtf16BeBom) {
const char geob_frame[] = {
0x01, // Text encoding [UTF-16]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
'\xfe', '\xff', // BOM
0x00, 'f', 0x00, 0x00, // Filename
'\xfe', '\xff', // BOM
0x00, 'd', 0x00, 0x00, // Content description
'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0,
/*data_length=*/geob_frame_contents.size());
ASSERT_THAT(geob_or, IsOk());
GeobFrame geob = *geob_or;
EXPECT_EQ(geob.mime_type, "text/plain");
EXPECT_EQ(geob.encapsulated_object, "object");
}
TEST(GeobFrameTest, ParseUtf16BeNoBom) {
const char geob_frame[] = {
0x02, // Text encoding [UTF-16BE]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
0x00, 'f', 0x00, 0x00, // Filename
0x00, 'd', 0x00, 0x00, // Content description
'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0,
/*data_length=*/geob_frame_contents.size());
ASSERT_THAT(geob_or, IsOk());
GeobFrame geob = *geob_or;
EXPECT_EQ(geob.mime_type, "text/plain");
EXPECT_EQ(geob.encapsulated_object, "object");
}
TEST(GeobFrameTest, ParseUtf8) {
const char geob_frame[] = {
0x03, // Text encoding [UTF-8]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
'f', 'i', 'l', 'e', 0x00, // Filename
'd', 'e', 's', 'c', 0x00, // Content description
'o', 'b', 'j', 'e', 'c', 't' // Encapsulated object
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0,
/*data_length=*/geob_frame_contents.size());
ASSERT_THAT(geob_or, IsOk());
GeobFrame geob = *geob_or;
EXPECT_EQ(geob.mime_type, "text/plain");
EXPECT_EQ(geob.encapsulated_object, "object");
}
TEST(GeobFrameTest, ParseInvalidTextEncodingFails) {
const char geob_frame[] = {
0x04, // Invalid text encoding
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0,
/*data_length=*/geob_frame_contents.size()),
StatusIs(absl::StatusCode::kFailedPrecondition,
HasSubstr("Invalid text encoding value")));
}
TEST(GeobFrameTest, ParseZeroLengthExceedingsBoundsFails) {
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
EXPECT_THAT(
GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/0),
StatusIs(
absl::StatusCode::kDataLoss,
HasSubstr(
"Data structure exceeded specified offset boundary limit.")));
}
TEST(GeobFrameTest, ParseMimeTypeNotTerminatedFails) {
// MIME type is not null-terminated within the frame data, but there is a
// null byte immediately after the frame data.
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
't', 'e', 'x', 't', '/',
'p', 'l', 'a', 'i', 'n', // MIME type (10 bytes, no null)
0x00 // Null byte outside the frame
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
// data_length is 11 (1 byte encoding + 10 bytes MIME type).
// The trailing null byte is at index 11, which is outside the frame.
EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/11),
StatusIs(absl::StatusCode::kDataLoss,
"Failed to read utf-8 text; at byte 1"));
}
TEST(GeobFrameTest, ParseFilenameNotTerminatedFails) {
// Filename is not null-terminated within the frame data, but there is a
// null byte immediately after the frame data.
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
'f', 'i', 'l', 'e', // Filename (4 bytes, no null)
0x00 // Null byte outside the frame
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
// data_length is 1 + 11 + 4 = 16.
// The trailing null byte is at index 16, which is outside the frame.
EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/16),
StatusIs(absl::StatusCode::kDataLoss,
"Failed to read utf-8 text; at byte 12"));
}
TEST(GeobFrameTest, ParseDescriptionNotTerminatedFails) {
// Content description is not null-terminated within the frame data, but there
// is a null byte immediately after the frame data.
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
'f', 'i', 'l', 'e', 0x00, // Filename
'd', 'e', 's', 'c', // Content description (4 bytes, no null)
0x00 // Null byte outside the frame
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
// data_length is 1 + 11 + 5 + 4 = 21.
// The trailing null byte is at index 21, which is outside the frame.
EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/21),
StatusIs(absl::StatusCode::kDataLoss,
"Failed to read utf-8 text; at byte 17"));
}
TEST(GeobFrameTest, ParseDescriptionMissingFails) {
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
'a', 0x00, // MIME type
'f', 'i', 'l', 'e', 0x00, // Filename
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
// data_length is 8 (1 + 2 + 5).
// Description is missing.
EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/8),
StatusIs(absl::StatusCode::kDataLoss,
"Failed to read utf-8 text; at byte 8"));
}
TEST(GeobFrameTest, ParseFilenameMissingFails) {
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
'a',
0x00, // MIME type
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
// data_length is 3 (1 + 2).
// Filename is missing.
EXPECT_THAT(GeobFrame::Parse(input, /*data_offset=*/0, /*data_length=*/3),
StatusIs(absl::StatusCode::kDataLoss,
"Failed to read utf-8 text; at byte 3"));
}
TEST(GeobFrameTest, ParseDescriptionEmptySucceeds) {
const char geob_frame[] = {
0x00, // Text encoding [ISO-8859-1]
't', 'e', 'x', 't', '/', 'p', 'l', 'a', 'i', 'n',
0x00, // MIME type
'f', 'i', 'l', 'e', 0x00, // Filename
0x00, // Content description (empty)
};
auto geob_frame_contents = std::string(geob_frame, sizeof(geob_frame));
riegeli::StringReader<> input(geob_frame_contents);
auto geob_or = GeobFrame::Parse(input, /*data_offset=*/0,
/*data_length=*/geob_frame_contents.size());
ASSERT_THAT(geob_or, IsOk());
GeobFrame geob = *geob_or;
EXPECT_EQ(geob.mime_type, "text/plain");
EXPECT_EQ(geob.encapsulated_object, "");
}
TEST(Id3HeaderTest, ParseId3HeaderNoExtendedHeader) {
const char id3_header[] = {
'I', 'D', '3', // ID3 identifier
0x03, 0x00, // Version 2.3
0x00, // Flags
0x00, 0x00, 0x00, 127, // Tag size
};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
auto header_or = Id3Header::Parse(input);
ASSERT_THAT(header_or, IsOk());
Id3Header header = *header_or;
EXPECT_FALSE(header.use_unsynchronisation);
EXPECT_FALSE(header.has_extended_header);
EXPECT_FALSE(header.is_experimental);
EXPECT_EQ(header.tag_size, 127);
}
TEST(Id3HeaderTest, ParseId3HeaderWithValidExtendedHeader) {
const char id3_header[] = {
'I', 'D', '3', // ID3 identifier
0x03, 0x00, // Version 2.3
0x40, // Flags (extended header)
0x00, 0x00, 0x00, 127, // Tag size
0x00, 0x00, 0x00, 6, // Extended header size
0x00, 0x00, // Extended flags
0x00, 0x00, 0x00, 0x00, // Padding size
};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
auto header_or = Id3Header::Parse(input);
ASSERT_THAT(header_or, IsOk());
Id3Header header = *header_or;
EXPECT_FALSE(header.use_unsynchronisation);
EXPECT_TRUE(header.has_extended_header);
EXPECT_FALSE(header.is_experimental);
EXPECT_EQ(header.tag_size, 127);
EXPECT_EQ(header.extended_header_size, 6);
EXPECT_EQ(header.extended_flags, std::string("\0\0", 2));
EXPECT_EQ(header.padding_size, 0);
}
TEST(Id3HeaderTest, ParseId3HeaderInvalidIdentifierFails) {
const char id3_header[] = {
'I', 'D', '4', // Invalid ID3 identifier
0x03, 0x00, // Version 2.3
0x00, // Flags
0x00, 0x00, 0x00, 127, // Tag size
};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
EXPECT_THAT(Id3Header::Parse(input),
StatusIs(absl::StatusCode::kNotFound, "ID3v2 tag not found"));
}
TEST(Id3HeaderTest, ParseId3HeaderInvalidVersionFails) {
const char id3_header[] = {
'I', 'D', '3', // ID3 identifier
0x03, 0x01, // Invalid version
0x00, // Flags
0x00, 0x00, 0x00, 127, // Tag size
};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
EXPECT_THAT(
Id3Header::Parse(input),
StatusIs(absl::StatusCode::kNotFound, HasSubstr("ID3v2 tag not found")));
}
TEST(Id3HeaderTest, ParseId3HeaderInvalidFlagFails) {
const char id3_header[] = {
'I', 'D', '3', // ID3 identifier
0x03, 0x00, // Version 2.3
0x01, // Invalid flags
0x00, 0x00, 0x00, 127, // Tag size
};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
EXPECT_THAT(
Id3Header::Parse(input),
StatusIs(absl::StatusCode::kInvalidArgument, "invalid ID3v2 flag byte"));
}
TEST(Id3HeaderTest, ParseId3HeaderZeroSizeFails) {
const char id3_header[] = {
'I', 'D', '3', // ID3 identifier
0x03, 0x00, // Version 2.3
0x00, // Flags
0x00, 0x00, 0x00, 0x00, // Tag size = 0
};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
EXPECT_THAT(Id3Header::Parse(input),
StatusIs(absl::StatusCode::kInvalidArgument,
"no frame inside the ID3v2 tag"));
}
TEST(Id3HeaderTest, ParseId3HeaderInvalidExtendedHeaderSizeFails) {
const char id3_header[] = {
'I', 'D', '3', // ID3 identifier
0x03, 0x00, // Version 2.3
0x40, // Flags (extended header)
0x00, 0x00, 0x00, 127, // Tag size
0x00, 0x00, 0x00, 7, // Invalid extended header size
0x00, 0x00, // Extended flags
0x00, 0x00, 0x00, 0x00, // Padding size
};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
EXPECT_THAT(Id3Header::Parse(input),
StatusIs(absl::StatusCode::kInvalidArgument,
"invalid extended header size value"));
}
TEST(Id3HeaderTest, ParseId3HeaderNoFrameWithExtendedHeader) {
constexpr char id3_header[] = {
'I', 'D', '3', // ID3 identifier
0x03, 0x00, // Version 2.3
0x40, // Flags (extended header)
0x00, 0x00, 0x00, 0x0e, // Tag size
0x00, 0x00, 0x00, 0x0a, // Extended header size = 10
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
auto id3_header_contents = std::string(id3_header, sizeof(id3_header));
riegeli::StringReader<> input(id3_header_contents);
EXPECT_THAT(Id3Header::Parse(input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("no frame inside the ID3v2 tag")));
}
TEST(Id3HeaderTest, EncodeId3HeaderNoExtendedHeader) {
Id3Header header = {
.use_unsynchronisation = false,
.has_extended_header = false,
.is_experimental = false,
.tag_size = 127,
};
EXPECT_EQ(header.ToCord(),
absl::string_view("ID3\x03\x00\x00\x00\x00\x00\x7F", 10));
}
TEST(Id3HeaderTest, EncodeId3HeaderWithExtendedHeader6Bytes) {
Id3Header header = {
.use_unsynchronisation = false,
.has_extended_header = true,
.is_experimental = false,
.tag_size = 127,
.extended_header_size = 6,
.extended_flags = std::string("\0\0", 2),
.padding_size = 0,
};
EXPECT_EQ(
header.ToCord(),
absl::string_view(
"ID3\x03\x00\x40\x00\x00\x00\x7F\x00\x00\x00\x06\x00\x00\x00\x00"
"\x00\x00",
20));
}
TEST(Id3HeaderTest, EncodeId3HeaderWithExtendedHeader10Bytes) {
Id3Header header = {
.use_unsynchronisation = false,
.has_extended_header = true,
.is_experimental = false,
.tag_size = 127,
.extended_header_size = 10,
.extended_flags = std::string("\0\0", 2),
.padding_size = 0,
.total_frame_crc = "\x01\x02\x03\x04",
};
EXPECT_EQ(
header.ToCord(),
absl::string_view(
"ID3\x03\x00\x40\x00\x00\x00\x7F\x00\x00\x00\x0A\x00\x00\x00\x00"
"\x00\x00\x01\x02\x03\x04",
24));
}
TEST(Id3HeaderTest, EncodeId3HeaderWithAllFlags) {
Id3Header header = {
.use_unsynchronisation = true,
.has_extended_header = true,
.is_experimental = true,
.tag_size = 127,
.extended_header_size = 6,
.extended_flags = std::string("\0\0", 2),
.padding_size = 0,
};
EXPECT_EQ(
header.ToCord(),
absl::string_view(
"ID3\x03\x00\xE0\x00\x00\x00\x7F\x00\x00\x00\x06\x00\x00\x00\x00"
"\x00\x00",
20));
}
} // namespace
} // namespace credentio