blob: 28bc9339a63e41a9984ef4bf1769166803ca225e [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/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);
ASSERT_THAT(chunks_or.status(), IsOk());
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);
ASSERT_THAT(chunks_or.status(), IsOk());
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