| // 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/riff/reader.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "formats/riff/create_riff.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| #include "riegeli/bytes/string_writer.h" | |
| #include "riegeli/endian/endian_writing.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOk; | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::testing::ElementsAre; | |
| using ::testing::HasSubstr; | |
| using ::testing::IsEmpty; | |
| constexpr uint64_t kMaxPayloadSize = 1024 * 1024 * 10; // 10 MiB | |
| std::string WriteRiffChunkMissingInvalidForm() { | |
| riegeli::StringWriter writer; | |
| writer.Write("RIFF"); | |
| riegeli::WriteLittleEndian<uint32_t>(2, writer); | |
| writer.Write("AB"); | |
| writer.Close(); | |
| return writer.dest(); | |
| } | |
| absl::StatusOr<std::vector<RiffChunk>> GetChunks(riegeli::Reader& reader) { | |
| std::vector<RiffChunk> chunks; | |
| ABSL_RETURN_IF_ERROR( | |
| IterateOverRiffChunks(reader, [&chunks](const RiffChunk& chunk) { | |
| chunks.push_back(chunk); | |
| return true; | |
| })); | |
| return chunks; | |
| } | |
| absl::StatusOr<std::vector<std::string>> ReadChunkPayloads( | |
| riegeli::Reader& reader, const std::vector<RiffChunk>& chunks) { | |
| std::vector<std::string> payloads; | |
| for (const auto& chunk : chunks) { | |
| ABSL_ASSIGN_OR_RETURN(auto payload, ReadRiffChunkData(reader, chunk)); | |
| payloads.push_back(payload); | |
| } | |
| return payloads; | |
| } | |
| TEST(RiffReaderUnitTest, ReadsChunks) { | |
| std::string contents = credentio_riff::RiffChunk( | |
| "XYZW", absl::StrCat(credentio_riff::Chunk("ABCD", "first chunk"), | |
| credentio_riff::Chunk("EFGH", "second chunk"), | |
| credentio_riff::Chunk("IJKL", "third chunk"), | |
| credentio_riff::Chunk("MNOP", "fourth chunk"))); | |
| riegeli::StringReader<> input(contents); | |
| auto chunks_or = GetChunks(input); | |
| ABSL_ASSERT_OK(chunks_or.status()); | |
| auto chunks = *chunks_or; | |
| ASSERT_THAT(chunks, ElementsAre( | |
| RiffChunk{ | |
| .offset = 0, | |
| .length = 92, | |
| .id = "RIFF", | |
| .type = "XYZW", | |
| .data_offset = 12, | |
| .data_length = 80, | |
| }, | |
| RiffChunk{ | |
| .offset = 12, | |
| .length = 20, | |
| .id = "ABCD", | |
| .type = "", | |
| .data_offset = 20, | |
| .data_length = 11, | |
| }, | |
| RiffChunk{ | |
| .offset = 32, | |
| .length = 20, | |
| .id = "EFGH", | |
| .type = "", | |
| .data_offset = 40, | |
| .data_length = 12, | |
| }, | |
| RiffChunk{ | |
| .offset = 52, | |
| .length = 20, | |
| .id = "IJKL", | |
| .type = "", | |
| .data_offset = 60, | |
| .data_length = 11, | |
| }, | |
| RiffChunk{ | |
| .offset = 72, | |
| .length = 20, | |
| .id = "MNOP", | |
| .type = "", | |
| .data_offset = 80, | |
| .data_length = 12, | |
| })); | |
| // Re-reading payloads requires seeking, ReadRiffChunkData handles it. | |
| EXPECT_THAT(ReadChunkPayloads(input, {chunks.begin() + 1, chunks.end()}), | |
| IsOkAndHolds(ElementsAre("first chunk", "second chunk", | |
| "third chunk", "fourth chunk"))); | |
| } | |
| TEST(RiffReaderUnitTest, MissingChunkTypeForChunkWithSubchunks) { | |
| std::string contents = WriteRiffChunkMissingInvalidForm(); | |
| riegeli::StringReader<> input(contents); | |
| EXPECT_THAT(GetChunks(input), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("RIFF/LIST chunk too short"))); | |
| } | |
| TEST(ReadChunkDataTest, SuccessfulRead) { | |
| std::string contents = "RIFF....WEBPVP8 ...."; | |
| // 01234567890123456789 | |
| riegeli::StringReader<> input(contents); | |
| RiffChunk chunk = { | |
| .offset = 12, | |
| .length = 8, | |
| .id = "VP8 ", | |
| .type = "", | |
| .data_offset = 12, | |
| .data_length = 8, | |
| }; | |
| EXPECT_THAT(ReadRiffChunkData(input, chunk), IsOkAndHolds("VP8 ....")); | |
| } | |
| TEST(ReadChunkDataTest, ReadBeyondEOF) { | |
| std::string contents = "RIFF....WEBPVP8 ...."; | |
| // 01234567890123456789 | |
| riegeli::StringReader<> input(contents); | |
| RiffChunk chunk = { | |
| .offset = 12, | |
| .length = 8, | |
| .id = "VP8 ", | |
| .type = "", | |
| .data_offset = 12, | |
| .data_length = 10, // Reads past end of contents | |
| }; | |
| EXPECT_THAT( | |
| ReadRiffChunkData(input, chunk), | |
| StatusIs(absl::StatusCode::kDataLoss, HasSubstr("kUnexpectedEof"))); | |
| } | |
| TEST(ReadChunkDataTest, EmptyChunk) { | |
| std::string contents = "RIFF....WEBPVP8 ...."; | |
| riegeli::StringReader<> input(contents); | |
| RiffChunk chunk = { | |
| .offset = 12, | |
| .length = 0, | |
| .id = "VP8 ", | |
| .type = "", | |
| .data_offset = 12, | |
| .data_length = 0, | |
| }; | |
| EXPECT_THAT(ReadRiffChunkData(input, chunk), IsOkAndHolds(IsEmpty())); | |
| } | |
| TEST(ReadChunkDataTest, ReadAtEOF) { | |
| std::string contents = "RIFF....WEBPVP8 ...."; | |
| riegeli::StringReader<> input(contents); | |
| RiffChunk chunk = { | |
| .offset = 20, | |
| .length = 0, | |
| .id = "....", | |
| .type = "", | |
| .data_offset = 20, | |
| .data_length = 0, | |
| }; | |
| EXPECT_THAT(ReadRiffChunkData(input, chunk), IsOkAndHolds(IsEmpty())); | |
| } | |
| TEST(ReadChunkDataTest, InvalidDataOffset) { | |
| std::string contents = "RIFF....WEBPVP8 ...."; | |
| riegeli::StringReader<> input(contents); | |
| RiffChunk chunk = { | |
| .offset = 12, | |
| .length = 8, | |
| .id = "VP8 ", | |
| .type = "", | |
| .data_offset = 100, // Past EOF | |
| .data_length = 1, | |
| }; | |
| EXPECT_THAT(ReadRiffChunkData(input, chunk), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Failed to seek to chunk data offset"))); | |
| } | |
| TEST(ReadChunkDataTest, ChunkTooLarge) { | |
| std::string contents = ""; // Not actually read in this test | |
| riegeli::StringReader<> input(contents); | |
| RiffChunk chunk = { | |
| .offset = 12, | |
| .length = kMaxPayloadSize + 9, | |
| .id = "C2PA", | |
| .type = "", | |
| .data_offset = 20, | |
| .data_length = kMaxPayloadSize + 1, | |
| }; | |
| EXPECT_THAT(ReadRiffChunkData(input, chunk), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("RIFF C2PA chunk is too large to extract"))); | |
| } | |
| TEST(RiffReaderUnitTest, ProcessorConsumesDataOfContainer) { | |
| std::string contents = | |
| credentio_riff::RiffChunk("WAVE", credentio_riff::ListChunk("INFO", "x")); | |
| riegeli::StringReader<> input(contents); | |
| auto processor = [&input](const RiffChunk& chunk) -> absl::StatusOr<bool> { | |
| if (chunk.id == "LIST") { | |
| std::string data; | |
| if (!input.Read(1, data)) { | |
| return absl::DataLossError("failed to read"); | |
| } | |
| EXPECT_EQ(data, "x"); | |
| } | |
| return true; | |
| }; | |
| EXPECT_THAT(IterateOverRiffChunks(input, processor), IsOk()); | |
| } | |
| TEST(RiffReaderUnitTest, OddContainerWithOddSubchunkAtEnd) { | |
| riegeli::StringWriter writer; | |
| writer.Write("RIFF"); | |
| riegeli::WriteLittleEndian<uint32_t>(26, writer); | |
| writer.Write("WAVE"); | |
| writer.Write("LIST"); | |
| riegeli::WriteLittleEndian<uint32_t>(13, writer); | |
| writer.Write("INFO"); | |
| writer.Write("ISFT"); | |
| riegeli::WriteLittleEndian<uint32_t>(1, writer); | |
| writer.Write("a"); | |
| writer.Write('\0'); // LIST pad | |
| writer.Close(); | |
| std::string contents = writer.dest(); | |
| riegeli::StringReader<> input(contents); | |
| auto chunks_or = GetChunks(input); | |
| ABSL_ASSERT_OK(chunks_or.status()); | |
| auto chunks = *chunks_or; | |
| ASSERT_THAT(chunks, ElementsAre( | |
| RiffChunk{ | |
| .offset = 0, | |
| .length = 34, | |
| .id = "RIFF", | |
| .type = "WAVE", | |
| .data_offset = 12, | |
| .data_length = 22, | |
| }, | |
| RiffChunk{ | |
| .offset = 12, | |
| .length = 22, | |
| .id = "LIST", | |
| .type = "INFO", | |
| .data_offset = 24, | |
| .data_length = 9, | |
| }, | |
| RiffChunk{ | |
| .offset = 24, | |
| .length = 10, | |
| .id = "ISFT", | |
| .type = "", | |
| .data_offset = 32, | |
| .data_length = 1, | |
| })); | |
| } | |
| class NoSizeReader : public riegeli::Reader { | |
| public: | |
| bool SupportsSize() override { return false; } | |
| bool PullSlow(size_t min_length, size_t recommended_length) override { | |
| return false; | |
| } | |
| }; | |
| TEST(RiffReaderTest, IterateOverRiffChunksRequiresSize) { | |
| NoSizeReader reader; | |
| EXPECT_THAT( | |
| IterateOverRiffChunks(reader, [](const RiffChunk&) { return true; }), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("reader size cannot be determined"))); | |
| } | |
| TEST(RiffReaderTest, OverflowChunkSizeNoInfiniteLoop) { | |
| riegeli::StringWriter writer; | |
| writer.Write("RIFF"); | |
| riegeli::WriteLittleEndian<uint32_t>(20, writer); | |
| writer.Write("WAVE"); | |
| writer.Write("JUNK"); | |
| riegeli::WriteLittleEndian<uint32_t>(0xFFFFFFF8, writer); | |
| writer.Close(); | |
| std::string contents = writer.dest(); | |
| riegeli::StringReader<> input(contents); | |
| EXPECT_THAT( | |
| IterateOverRiffChunks(input, [](const RiffChunk&) { return true; }), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("RIFF chunk extends beyond the end of the file"))); | |
| } | |
| } // namespace | |
| } // namespace credentio |