blob: ca5bef9f7fa5a7a9c9ecc64902d399d3e6ab8ba8 [file]
// Protocol Buffers - Google's data interchange format
// Copyright 2026 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "google/protobuf/parse_context.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <vector>
#include <gtest/gtest.h>
#include "absl/log/absl_check.h"
#include "absl/strings/cord.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "google/protobuf/io/test_zero_copy_stream.h"
#include "google/protobuf/io/zero_copy_stream_impl_lite.h"
#include "google/protobuf/unittest.pb.h"
#include "utf8_validity.h"
namespace google {
namespace protobuf {
namespace internal {
namespace {
constexpr int kTooLargeSize =
static_cast<int>(std::numeric_limits<uint32_t>::max());
constexpr int kMaxLimit = std::numeric_limits<int>::max();
TEST(ParseContextTest, NoOverflowMaxDepth) {
constexpr char kPayload[] =
"\x81\x81\x81\x81\x08\x01\x00"; // Malformed varint size.
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, kPayload);
proto2_unittest::TestAllTypes proto;
const char* result = ctx.ParseMessage(&proto, kPayload);
ASSERT_EQ(result, nullptr);
}
TEST(ParseContextTest, VerifyUTF8) {
constexpr absl::string_view kPayload = "hello world, a valid UTF8 payload";
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, kPayload);
ptr = ctx.VerifyUTF8(ptr, static_cast<int>(kPayload.size()));
EXPECT_NE(ptr, nullptr);
EXPECT_EQ(ctx.DataAvailable(ptr), 0);
}
TEST(ParseContextTest, VerifyFragmentedUTF8) {
constexpr absl::string_view kPayload =
"hello world, a valid UTF8 payload #1"
"hello world, a valid UTF8 payload #2"
"hello world, a valid UTF8 payload #3"
"hello world, a valid UTF8 payload #4"
"hello world, a valid UTF8 payload #5";
const char* ptr;
io::ArrayInputStream ais(kPayload.data(), kPayload.size(), /*block_size*/ 13);
ParseContext ctx(kMaxLimit, false, &ptr, &ais);
ptr = ctx.VerifyUTF8(ptr, static_cast<int>(kPayload.size()));
EXPECT_NE(ptr, nullptr);
EXPECT_EQ(ctx.DataAvailable(ptr), 0);
}
TEST(ParseContextTest, FailsOnInvalidUTF8) {
constexpr absl::string_view kInvalidUtf8 = "\xe2\x82\x28";
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, kInvalidUtf8);
ptr = ctx.VerifyUTF8(ptr, static_cast<int>(kInvalidUtf8.size()));
EXPECT_EQ(ptr, nullptr);
}
TEST(ParseContextTest, FailsOnFragmentedInvalidUTF8) {
constexpr absl::string_view kInvalidUtf8 =
"hello world, a valid UTF8 payload #1"
"hello world, a valid UTF8 payload #2"
"hello world, a valid UTF8 payload #3"
"hello world, a valid UTF8 payload #4"
"hello world, a valid UTF8 payload #5\xe2\x82\x28";
const char* ptr;
io::ArrayInputStream ais(kInvalidUtf8.data(), kInvalidUtf8.size(),
/*block_size*/ 13);
ParseContext ctx(kMaxLimit, false, &ptr, &ais);
ptr = ctx.VerifyUTF8(ptr, static_cast<int>(kInvalidUtf8.size()));
EXPECT_EQ(ptr, nullptr);
EXPECT_GT(ctx.DataAvailable(ptr), 0);
}
TEST(ParseContextTest, SkipTooLarge) {
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, "hello world");
ptr = ctx.Skip(ptr, kTooLargeSize);
EXPECT_EQ(ptr, nullptr);
}
TEST(ParseContextTest, ReadStringTooLarge) {
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, "hello world");
std::string out;
ptr = ctx.ReadString(ptr, kTooLargeSize, &out);
EXPECT_EQ(ptr, nullptr);
}
TEST(ParseContextTest, AppendStringTooLarge) {
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, "hello world");
std::string out;
ptr = ctx.AppendString(ptr, kTooLargeSize, &out);
EXPECT_EQ(ptr, nullptr);
}
TEST(ParseContextTest, ReadCordTooLarge) {
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, "hello world");
absl::Cord out;
ptr = ctx.ReadCord(ptr, kTooLargeSize, &out);
EXPECT_EQ(ptr, nullptr);
}
TEST(ParseContextTest, VerifyUTF8TooLarge) {
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, "hello world");
ptr = ctx.VerifyUTF8(ptr, static_cast<uint32_t>(kTooLargeSize));
EXPECT_EQ(ptr, nullptr);
}
TEST(ParseContextTest, ReadChunkAndCallbackTooLarge) {
const char* ptr;
ParseContext ctx(kMaxLimit, false, &ptr, "hello world");
ptr = ctx.ReadChunkAndCallback(ptr, kTooLargeSize,
[&](const char* ptr, int s) {});
EXPECT_EQ(ptr, nullptr);
}
TEST(ParseContextTest, ReadCordMustWorkAcrossFragments) {
const char* ptr;
io::internal::TestZeroCopyInputStream stream({"hello ", "world"});
ParseContext ctx(kMaxLimit, false, &ptr, &stream);
absl::Cord out;
ptr = ctx.ReadCord(ptr, 7, &out);
EXPECT_EQ(out, "hello w");
}
TEST(ParseContextTest, ReadCordFromMultipleFragments) {
const char* ptr;
io::internal::TestZeroCopyInputStream stream({"a", "bc", "def"});
ParseContext ctx(kMaxLimit, false, &ptr, &stream);
absl::Cord out;
ptr = ctx.ReadCord(ptr, 6, &out);
EXPECT_EQ(out, "abcdef");
}
TEST(ParseContextTest, ReadCordFromMultipleFragmentsWithEmptyFragment) {
const char* ptr;
io::internal::TestZeroCopyInputStream stream({"a", "bc", "", "", "def"});
ParseContext ctx(kMaxLimit, false, &ptr, &stream);
absl::Cord out;
ptr = ctx.ReadCord(ptr, 6, &out);
EXPECT_EQ(out, "abcdef");
}
template <typename DataT, typename PeekFunc>
void TestAdvancePtrSinkingAndPeeking(
const std::vector<std::string>& input, std::vector<int> advance_by,
const PeekFunc& peek_func = [](absl::string_view) { return true; }) {
std::string flattened = absl::StrJoin(input, "");
// To fully traverse the input.
int available = static_cast<int>(flattened.size());
// To ensure that we can cover the entire data.
advance_by.push_back(available);
io::internal::TestZeroCopyInputStream stream(input);
const char* ptr;
ParseContext ctx(io::CodedInputStream::GetDefaultRecursionLimit(), false,
&ptr, &stream);
if (ctx.Done(&ptr)) return;
WireFormatStringSink sink(ptr);
sink.data.reserve(flattened.size());
for (auto advance : advance_by) {
int adv = std::min(available, advance);
available -= adv;
ABSL_CHECK_GE(adv, 0);
ptr = ctx.AdvancePtrMaybeFlush<DataT>(ptr, adv, sink, peek_func);
if (!ptr) break;
if (available == 0) {
sink.Flush(ptr);
break;
}
}
// Output must match the flattened input.
EXPECT_EQ(sink.data, flattened);
}
void TestAdvancePtrPeeking(const std::vector<std::string>& input,
std::vector<int> advance_by) {
// Peeked data must match the flattened input.
std::string peeked;
TestAdvancePtrSinkingAndPeeking<char>(input, advance_by,
[&peeked](absl::string_view view) {
absl::StrAppend(&peeked, view);
return true;
});
EXPECT_EQ(peeked, absl::StrJoin(input, ""));
}
TEST(ParseAndFlushTest, SmallViewInput) {
TestAdvancePtrPeeking({"hello world"}, {});
}
TEST(ParseAndFlushTest, SmallViewInputIncremental) {
TestAdvancePtrPeeking({"hello world"}, {1, 2, 3, 4, 5});
}
TEST(ParseAndFlushTest, LargeViewInput) {
constexpr absl::string_view kPayload =
"hello world, a valid UTF8 payload #1"
"hello world, a valid UTF8 payload #2"
"hello world, a valid UTF8 payload #3"
"hello world, a valid UTF8 payload #4"
"hello world, a valid UTF8 payload #5";
TestAdvancePtrPeeking({std::string{kPayload}}, {});
}
TEST(ParseAndFlushTest, LargeViewInputIncremental) {
constexpr absl::string_view kPayload =
"hello world, a valid UTF8 payload #1"
"hello world, a valid UTF8 payload #2"
"hello world, a valid UTF8 payload #3"
"hello world, a valid UTF8 payload #4"
"hello world, a valid UTF8 payload #5";
TestAdvancePtrPeeking({std::string{kPayload}}, {1, 3, 5, 7, 9, 11});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression1) {
// "ptr" from ParseContext may not be flush-ready if the fragmented input has
// empty fragments. This is not plausible in practice.
TestAdvancePtrPeeking({"", "", "", "", "0", "pppppp", ""}, {50});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression2) {
// From fragmented input, advancing ptr by greater than kSlopBytes is not
// safe.
TestAdvancePtrPeeking(
{"", "\364\364", "###", "\022", "\223\036", "\366", "\366", "\366",
"\366", "\366", "\366", "\366", "\366", "\366", "\024", "\223"},
{1, 50});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression3) {
TestAdvancePtrPeeking({"\315\315", ""}, {3});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression4) {
TestAdvancePtrPeeking({"\323", "\323", "", "\033", "", ""}, {});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression5) {
TestAdvancePtrPeeking(
{"9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9",
"9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9", "9"},
{2});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression6) {
TestAdvancePtrPeeking(
{"\314", "\314", "\314", "\314", "\314", "\314", "\314", "\327", "\327",
"\327", "\327", "\327", "\327", "\314", "\314", "\314", "\271", "\017"},
{16, 16, 16, 6, 16, 12, 16, 16, 16, 16, 16,
16, 16, 16, 2, 6, 16, 16, 16, 16, 16, 16});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression7) {
TestAdvancePtrPeeking(
{"", "\244\030",
"\361\361\361\361\361\361\361\361\361\361\361\361\361\361\361\361\361"
"\361\361\361\361\361\361\361",
"\301", "9\300", "\332\332\332\332\332\330\330\300", "\250", "\235", "",
"\026"},
{13, 1, 1, 9, 1, 12, 16, 14, 8, 7, 6, 8, 1, 14, 16, 1, 16, 13, 16,
1, 1, 1, 14, 16, 4, 1, 1, 9, 12, 1, 1, 16, 1, 1, 1, 16, 7});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression8) {
TestAdvancePtrPeeking(
{"", "\343\343\343\343\343\343\343",
"||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||", "&",
"\360", "\207\207\207\207\207\207\207\207\207\207", "\352", "\340", ",",
"((((((", "\275", ""},
{1, 1, 8, 4, 1, 1, 4, 4, 4, 4, 4, 10, 10, 14,
13, 7, 3, 16, 11, 12, 16, 15, 16, 10, 15, 15, 14, 14,
11, 14, 10, 2, 16, 14, 14, 14, 8, 14, 14, 14});
}
TEST(ParseAndFlush, TestAdvancePtrPeekingRegression9) {
TestAdvancePtrPeeking(
{"D", "", "NNN", "J", "\270", "\200\200", "\021\021", "\320\320\320",
"\227",
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"ff",
"\331", "&", "L", "", "\237", "", "\311", "\353"},
{96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96,
96, 30, 30, 30, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 1,
26, 9, 31, 57, 38, 34, 97, 96, 96, 100, 96, 96, 96, 96, 96, 100,
96, 96, 7, 99, 96, 96, 96, 96, 96, 96, 96, 96, 1, 96, 96, 96,
96, 96, 96, 96, 96, 96, 96, 44, 96, 61, 66, 14});
}
void VerifyUTF8AndCopy(const std::vector<std::string>& input) {
std::string flattened = absl::StrJoin(input, "");
// To fully traverse the input.
io::internal::TestZeroCopyInputStream stream(input);
const char* ptr;
ParseContext ctx(io::CodedInputStream::GetDefaultRecursionLimit(), false,
&ptr, &stream);
WireFormatStringSink sink(ptr);
sink.data.reserve(flattened.size());
ptr = ctx.VerifyUTF8MaybeFlush(ptr, static_cast<int64_t>(flattened.size()),
sink);
if (ptr == nullptr) {
EXPECT_FALSE(utf8_range::IsStructurallyValid(flattened));
return;
}
sink.Flush(ptr);
// UTF8 validation must match the flattened input.
EXPECT_TRUE(utf8_range::IsStructurallyValid(flattened));
// If successful, the output must match the flattened input.
EXPECT_EQ(sink.data, flattened);
}
TEST(VerifyUTF8AndFlush, ValidInput) { VerifyUTF8AndCopy({"hello world!"}); }
TEST(VerifyUTF8AndFlush, InvalidInput) {
constexpr absl::string_view kInvalidUtf8 = "\xe2\x82\x28";
VerifyUTF8AndCopy({std::string(kInvalidUtf8)});
}
TEST(VerifyUTF8AndFlush, InvalidSplit) {
VerifyUTF8AndCopy({std::string("\xe2\x82"), std::string("\x28")});
}
TEST(VerifyUTF8AndFlush, IncompleteUtf8) {
ASSERT_FALSE(utf8_range::IsStructurallyValid("\xf0"));
VerifyUTF8AndCopy({std::string("\xf0")});
}
TEST(VerifyUTF8AndFlush, InvalidSeparatelyButValidTogether) {
ASSERT_TRUE(utf8_range::IsStructurallyValid("\xf0\x9f\x98\x80"));
VerifyUTF8AndCopy({std::string("\xf0"), std::string("\x9f\x98\x80")});
}
} // namespace
} // namespace internal
} // namespace protobuf
} // namespace google