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