blob: e4ce30d64f49caa225798d5ef269845723f660ae [file] [edit]
// 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/gif/reader.h"
#include <optional>
#include <string>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "formats/gif/constants.h"
#include "formats/gif/test_builder.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/base/types.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_reader.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;
class NoSizeStringReader : public riegeli::StringReader<> {
public:
using riegeli::StringReader<>::StringReader;
bool SupportsSize() override { return false; }
};
class SizeNotValuedStringReader : public riegeli::StringReader<> {
public:
using riegeli::StringReader<>::StringReader;
bool SupportsSize() override { return true; }
protected:
std::optional<riegeli::Position> SizeImpl() override { return std::nullopt; }
};
absl::StatusOr<GifBlock> GetLastBlock(riegeli::Reader& input) {
GifBlock last_block;
if (auto status = IterateOverGifBlocks(input,
[&last_block](const GifBlock& block) {
last_block = block;
return true;
});
!status.ok()) {
return status;
}
return last_block;
}
TEST(GifReaderTest, ReadsHeader) {
TestGifBuilder builder;
builder.AddGifHeader();
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 0,
.length = 6,
.type = "GIF89a",
}));
}
TEST(GifReaderTest, FailsOnInvalidHeader) {
std::string contents = "GIF87a";
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input),
StatusIs(absl::StatusCode::kInvalidArgument));
}
TEST(GifReaderTest, ReadsLogicalDescriptorWithoutColorTable) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 6,
.length = 7,
.type = "LSD",
}));
}
TEST(GifReaderTest, ReadsLogicalDescriptorWithColorTable) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/true);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 6,
.length = 19,
.type = "LSD",
}));
}
TEST(GifReaderTest, ReadsPlainTextExtension) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionPlainText, "ext_info",
"plain_text_data");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 28,
.type = "2101",
}));
}
TEST(GifReaderTest, ReadsInvalidExtensionIntroducer) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, 0x0d);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input),
StatusIs(absl::StatusCode::kUnimplemented,
HasSubstr("Extension Type not supported: 0x0d")));
}
TEST(GifReaderTest, ReadsGraphicsControlExtension) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionGraphicsControl,
"ext_info", "gce");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 16,
.type = "21F9",
}));
}
TEST(GifReaderTest, ReadsComment) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionComment, "ext_info",
"comment");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 20,
.type = "21FE",
}));
}
TEST(GifReaderTest, ReadsTrailer) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifTrailerIntroducer);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 1,
.type = "3B",
}));
}
TEST(GifReaderTest, ReadsTableData) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(0x02, /*type=*/std::nullopt, /*info=*/std::nullopt,
"this_is_some_image_data");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 26,
.type = "TBID",
}));
}
TEST(GifReaderTest, ReadsImageDescriptorWithoutColorTable) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddImageDescriptor(/*has_color_table=*/false);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 10,
.type = "2C",
}));
}
TEST(GifReaderTest, ReadsImageDescriptorWithColorTable) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddImageDescriptor(/*has_color_table=*/true);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 22,
.type = "2C",
}));
}
TEST(GifReaderTest, ReadsApplicationExtension) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionApplication,
"ext_info", "data");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 17,
.type = "21FF",
}));
}
TEST(GifReaderTest, ReadsC2paExtension) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionApplication,
std::string(kGifC2paIdentifier) + "___", "manifest_store");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 13,
.length = 30,
.type = "C2PA",
}));
}
TEST(GifReaderTest, ReadsPastTrailer) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifTrailerIntroducer);
std::string contents = builder.GetAsset() + "random_data_past_the_trailer";
riegeli::StringReader<> input(contents);
EXPECT_THAT(GetLastBlock(input), IsOkAndHolds(GifBlock{
.offset = 14,
.length = 28,
.type = "c2pa.after",
}));
}
TEST(GifReaderTest, FailsWhenSizeNotSupported) {
std::string contents = "test";
NoSizeStringReader input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input, [](const GifBlock& block) { return true; }),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Input does not support size")));
}
TEST(GifReaderTest, FailsWhenSizeHasNoValue) {
std::string contents = "test";
SizeNotValuedStringReader input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input, [](const GifBlock& block) { return true; }),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Input does not support size")));
}
TEST(GifReaderTest, StopsAtHeaderIfProcessorReturnsFalse) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
int call_count = 0;
EXPECT_THAT(IterateOverGifBlocks(input,
[&](const GifBlock& block) {
call_count++;
return false;
}),
IsOk());
EXPECT_EQ(call_count, 1);
}
TEST(GifReaderTest, StopsAtLogicalDescriptorIfProcessorReturnsFalse) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionPlainText, "ext_info",
"plain_text_data");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
int call_count = 0;
EXPECT_THAT(IterateOverGifBlocks(input,
[&](const GifBlock& block) {
call_count++;
if (block.type == "LSD") {
return false;
}
return true;
}),
IsOk());
EXPECT_EQ(call_count, 2);
}
TEST(GifReaderTest, StopsInsideLoopIfProcessorReturnsFalse) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionPlainText, "ext_info",
"plain_text_data");
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionComment, "ext_info",
"comment");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
int call_count = 0;
EXPECT_THAT(
IterateOverGifBlocks(input,
[&](const GifBlock& block) {
call_count++;
if (block.type == "2101") { // PlainText extension
return false;
}
return true;
}),
IsOk());
EXPECT_EQ(call_count, 3); // Header, LSD, PlainText
}
TEST(GifReaderTest, FailsWhenReaderFailsDuringLogicalDescriptor) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input,
[&](const GifBlock& block) {
if (block.type == "GIF89a") {
input.Fail(
absl::InternalError("forced read error"));
}
return true;
}),
StatusIs(absl::StatusCode::kInternal, HasSubstr("forced read error")));
}
TEST(GifReaderTest, FailsWhenSeekFailsAfterLogicalDescriptor) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input,
[&](const GifBlock& block) {
if (block.type == "LSD") {
input.Fail(
absl::InternalError("forced seek error"));
}
return true;
}),
StatusIs(absl::StatusCode::kInternal, HasSubstr("forced seek error")));
}
TEST(GifReaderTest, FailsWhenSeekFailsInsideLoop) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionPlainText, "ext_info",
"plain_text_data");
std::string contents = builder.GetAsset();
riegeli::StringReader<> input(contents);
EXPECT_THAT(IterateOverGifBlocks(input,
[&](const GifBlock& block) {
if (block.type == "2101") {
input.Fail(absl::InternalError(
"forced seek error inside loop"));
}
return true;
}),
StatusIs(absl::StatusCode::kInternal,
HasSubstr("forced seek error inside loop")));
}
TEST(GifReaderTest, FailsWhenSeekToEndOfDataFailsOnReadByte) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionPlainText, "ext_info",
"plain_text_data");
std::string contents = builder.GetAsset();
// Truncate the last byte of the block, which should be the 0x00 termination
// byte.
contents.pop_back();
riegeli::StringReader<> input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input, [](const GifBlock& block) { return true; }),
StatusIs(absl::StatusCode::kDataLoss, HasSubstr("Failed to read byte")));
}
TEST(GifReaderTest, FailsWhenSeekToEndOfDataFailsOnSkip) {
TestGifBuilder builder;
builder.AddGifHeader();
builder.AddLogicalDescriptor(/*has_color_table=*/false);
builder.AddBlock(kGifExtensionIntroducer, kGifExtensionPlainText, "ext_info",
"plain_text_data");
std::string contents = builder.GetAsset();
// Truncate the contents by 5 bytes (so skip will fail because we claim size
// is 15 but only 10 left).
for (int i = 0; i < 5; i++) {
contents.pop_back();
}
riegeli::StringReader<> input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input, [](const GifBlock& block) { return true; }),
StatusIs(absl::StatusCode::kInternal,
HasSubstr("Failed to seek to offset")));
}
TEST(GifReaderTest, FailsOnHeaderTooShort) {
std::string contents = "GIF";
riegeli::StringReader<> input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input, [](const GifBlock& block) { return true; }),
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("Failed to read GIF header block")));
}
TEST(GifReaderTest, FailsOnDescriptorTooShort) {
TestGifBuilder builder;
builder.AddGifHeader();
std::string contents = builder.GetAsset() + "123"; // Only 3 bytes for LSD
riegeli::StringReader<> input(contents);
EXPECT_THAT(
IterateOverGifBlocks(input, [](const GifBlock& block) { return true; }),
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("Failed to read descriptor")));
}
} // namespace
} // namespace credentio