| // 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, 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 = 23, | |
| .length = 13, | |
| .type = "TBID", | |
| })); | |
| } | |
| 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 = 35, | |
| .length = 13, | |
| .type = "TBID", | |
| })); | |
| } | |
| 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 |