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