Skip to content

Commit

Permalink
fix formats: destroy rapidjson::GenericValue manually
Browse files Browse the repository at this point in the history
  • Loading branch information
lfaktorovich committed Feb 21, 2023
1 parent b9415c3 commit 1410c86
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 0 deletions.
72 changes: 72 additions & 0 deletions core/src/formats/json/stack_overflow_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include <string>

#include <gtest/gtest.h>

#include <userver/engine/run_standalone.hpp>
#include <userver/formats/json/serialize.hpp>

USERVER_NAMESPACE_BEGIN

namespace {

std::string MakeStringOfDeepObject(std::size_t depth) {
std::string str;
str.reserve(depth * 6 + 1);
for (std::size_t i = 0; i < depth; ++i) {
str += R"({"a":)";
}
str += "1";
for (std::size_t i = 0; i < depth; ++i) {
str += "}";
}
return str;
}

std::string MakeStringOfDeepArray(std::size_t depth) {
std::string str;
str.reserve(2 * depth + 1);
for (std::size_t i = 0; i < depth; ++i) {
str += "[";
}
str += "1";
for (std::size_t i = 0; i < depth; ++i) {
str += "]";
}
return str;
}

} // namespace

TEST(FormatsJson, DeepObjectFromString) {
constexpr std::size_t kWorkerThreads = 1;
engine::TaskProcessorPoolsConfig config;
config.coro_stack_size = 32 * 1024ULL;

engine::RunStandalone(kWorkerThreads, config, [] {
constexpr std::size_t kDepth = 16000;
auto value = formats::json::FromString(MakeStringOfDeepObject(kDepth));

for (std::size_t i = 0; i < kDepth; ++i) {
value = value["a"];
}
EXPECT_EQ(value.As<int>(), 1);
});
}

TEST(FormatsJson, DeepArrayFromString) {
constexpr std::size_t kWorkerThreads = 1;
engine::TaskProcessorPoolsConfig config;
config.coro_stack_size = 32 * 1024ULL;

engine::RunStandalone(kWorkerThreads, config, [] {
constexpr std::size_t kDepth = 16000;
auto value = formats::json::FromString(MakeStringOfDeepArray(kDepth));

for (std::size_t i = 0; i < kDepth; ++i) {
value = value[0];
}
EXPECT_EQ(value.As<int>(), 1);
});
}

USERVER_NAMESPACE_END
107 changes: 107 additions & 0 deletions shared/src/formats/json/impl/types_impl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include <formats/json/impl/types_impl.hpp>

#include <utility>

#include <userver/utils/assert.hpp>
#include <userver/utils/not_null.hpp>

USERVER_NAMESPACE_BEGIN

namespace formats::json::impl {

namespace {

bool IsComplex(const Value& value) {
if (value.IsObject()) {
return value.MemberBegin() != value.MemberEnd();
}
if (value.IsArray()) {
return value.Begin() != value.End();
}
return false;
}

class JsonTreeDestroyer final {
public:
explicit JsonTreeDestroyer(Value&& root)
: root_(std::move(root)), terminal_(&root_) {
UpdateTerminal();
}

void Destroy() && {
if (&root_ == terminal_) {
return;
}

do {
MoveComplexChildrenToTerminal();
} while (NextDepth());
}

private:
void MoveComplexChildrenToTerminal() {
UASSERT(IsComplex(root_));

if (root_.IsObject()) {
for (auto it = root_.MemberBegin() + 1; it != root_.MemberEnd(); ++it) {
if (IsComplex(it->value)) {
MoveValueToTerminal(std::move(it->value));
}
}
} else {
for (auto* it = root_.Begin() + 1; it != root_.End(); ++it) {
if (IsComplex(*it)) {
MoveValueToTerminal(std::move(*it));
}
}
}
}

bool NextDepth() {
UASSERT(IsComplex(root_));

Value& next_depth =
root_.IsObject() ? root_.MemberBegin()->value : *root_.Begin();

const bool is_last_level = &next_depth == terminal_;

Value field_to_destroy(std::move(next_depth));
root_.Swap(field_to_destroy);

return !is_last_level;
}

void MoveValueToTerminal(Value&& value) {
*terminal_ = std::move(value);
UpdateTerminal();
}

void UpdateTerminal() {
while (IsComplex(*terminal_)) {
if (terminal_->IsObject()) {
terminal_ = terminal_->MemberBegin()->value;
} else {
terminal_ = *terminal_->Begin();
}
}
}

Value root_;
utils::NotNull<Value*> terminal_;
};

// ~GenericValue destroys members with recursion, which causes
// stackoverflow error with deep objects
void DestroyMembersIteratively(Value&& native) noexcept {
JsonTreeDestroyer(std::move(native)).Destroy();
}

} // namespace

VersionedValuePtr::Data::~Data() {
DestroyMembersIteratively(std::move(native));
}

} // namespace formats::json::impl

USERVER_NAMESPACE_END
2 changes: 2 additions & 0 deletions shared/src/formats/json/impl/types_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ struct VersionedValuePtr::Data {
// https://github.com/Tencent/rapidjson/issues/387
explicit Data(Document&&);

~Data();

// native rapidjson value
Value native;

Expand Down
99 changes: 99 additions & 0 deletions shared/src/formats/json/serialize_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,105 @@ TEST(JsonToSortedString, Null) {
ASSERT_EQ(formats::json::ToStableString(example), "null");
}

namespace {

struct NotSortedTestData {
std::string source;
std::string result;
};

struct CycleTestData {
std::string source;
};

class JsonToStringCycle : public ::testing::TestWithParam<CycleTestData> {};

class NonSortedJsonToSortedString
: public ::testing::TestWithParam<NotSortedTestData> {};

const auto global_json1 =
formats::json::FromString(R"({"b":{"b":{"b":1}},"c":{"c":{"c":1}},"a":1})");
const auto global_json2 =
formats::json::FromString(R"({"c":1,"b":{"b":{"b":1,"a":1}},"a":1})");
const auto global_json3 =
formats::json::FromString(R"({"b":{"b":{"b":1,"a":1}},"a":1,"c":1})");
thread_local const auto thread_local_json1 = global_json1;
thread_local const auto thread_local_json2 = global_json2;
thread_local const auto thread_local_json3 = global_json3;

} // namespace

TEST_P(NonSortedJsonToSortedString, NonDepthTree) {
const NotSortedTestData pair_data_res = GetParam();
const auto json = formats::json::FromString(pair_data_res.source);
EXPECT_EQ(formats::json::ToStableString(json), pair_data_res.result);
}

TEST_P(JsonToStringCycle, NonDepthTree) {
const CycleTestData data = GetParam();
const auto json = formats::json::FromString(data.source);
const auto json_str = formats::json::ToString(json);
const auto json_copy = formats::json::FromString(json_str);
EXPECT_EQ(formats::json::ToStableString(json_copy),
formats::json::ToStableString(json));
}

INSTANTIATE_TEST_SUITE_P(
JsonToSortedString, NonSortedJsonToSortedString,
::testing::Values(
NotSortedTestData{"null", "null"}, NotSortedTestData{"false", "false"},
NotSortedTestData{"true", "true"}, NotSortedTestData{"{}", "{}"},
NotSortedTestData{"[]", "[]"}, NotSortedTestData{"1", "1"},
NotSortedTestData{R"({"b":1,"a":1})", R"({"a":1,"b":1})"},
NotSortedTestData{R"({"b":1,"a":1,"c":1})", R"({"a":1,"b":1,"c":1})"},
NotSortedTestData{R"({"b":{"b":1,"a":1}, "a":1,"c":1})",
R"({"a":1,"b":{"a":1,"b":1},"c":1})"},
NotSortedTestData{R"({"b":1,"a":1,"c":{"b":1,"a":1}})",
R"({"a":1,"b":1,"c":{"a":1,"b":1}})"},
NotSortedTestData{R"({"b":{"b":{"b":1}},"a":1,"c":1})",
R"({"a":1,"b":{"b":{"b":1}},"c":1})"},
NotSortedTestData{R"({"a":1,"c":1,"b":{"b":{"b":1}}})",
R"({"a":1,"b":{"b":{"b":1}},"c":1})"},
NotSortedTestData{R"({"b":{"b":1},"a":{"a":1}})",
R"({"a":{"a":1},"b":{"b":1}})"},
NotSortedTestData{R"({"c":{"b":1,"a":1},"b":{"b":1,"a":1},"a":1})",
R"({"a":1,"b":{"a":1,"b":1},"c":{"a":1,"b":1}})"},
NotSortedTestData{R"({"b":1,"c":{"c":{"c":1}},"a":1})",
R"({"a":1,"b":1,"c":{"c":{"c":1}}})"},
NotSortedTestData{R"({"b":{"b":{"b":1}},"a":{"a":1},"c":1})",
R"({"a":{"a":1},"b":{"b":{"b":1}},"c":1})"},
NotSortedTestData{R"({"c":1,"b":{"b":{"b":1}},"a":{"a":1}})",
R"({"a":{"a":1},"b":{"b":{"b":1}},"c":1})"},
NotSortedTestData{R"({"c":{"c":1},"a":1,"b":{"b":{"b":1}}})",
R"({"a":1,"b":{"b":{"b":1}},"c":{"c":1}})"},
NotSortedTestData{R"({"b":{"b":{"b":1}},"c":{"c":{"c":1}},"a":1})",
R"({"a":1,"b":{"b":{"b":1}},"c":{"c":{"c":1}}})"},
NotSortedTestData{R"({"b":{"b":{"b":1,"a":1}},"a":1,"c":1})",
R"({"a":1,"b":{"b":{"a":1,"b":1}},"c":1})"},
NotSortedTestData{R"({"c":1,"b":{"b":{"b":1,"a":1}},"a":1})",
R"({"a":1,"b":{"b":{"a":1,"b":1}},"c":1})"}));

INSTANTIATE_TEST_SUITE_P(
JsonToString, JsonToStringCycle,
::testing::Values(
CycleTestData{"null"}, CycleTestData{"false"}, CycleTestData{"true"},
CycleTestData{"{}"}, CycleTestData{"[]"}, CycleTestData{"1"},
CycleTestData{R"({"b":1,"a":1})"},
CycleTestData{R"({"b":1,"a":1,"c":1})"},
CycleTestData{R"({"b":{"b":1,"a":1}, "a":1,"c":1})"},
CycleTestData{R"({"b":1,"a":1,"c":{"b":1,"a":1}})"},
CycleTestData{R"({"b":{"b":{"b":1}},"a":1,"c":1})"},
CycleTestData{R"({"a":1,"c":1,"b":{"b":{"b":1}}})"},
CycleTestData{R"({"b":{"b":1},"a":{"a":1}})"},
CycleTestData{R"({"c":{"b":1,"a":1},"b":{"b":1,"a":1},"a":1})"},
CycleTestData{R"({"b":1,"c":{"c":{"c":1}},"a":1})"},
CycleTestData{R"({"b":{"b":{"b":1}},"a":{"a":1},"c":1})"},
CycleTestData{R"({"c":1,"b":{"b":{"b":1}},"a":{"a":1}})"},
CycleTestData{R"({"c":{"c":1},"a":1,"b":{"b":{"b":1}}})"},
CycleTestData{R"({"b":{"b":{"b":1}},"c":{"c":{"c":1}},"a":1})"},
CycleTestData{R"({"b":{"b":{"b":1,"a":1}},"a":1,"c":1})"},
CycleTestData{R"({"c":1,"b":{"b":{"b":1,"a":1}},"a":1})"}));

TEST(JsonToSortedString, Object) {
const formats::json::Value example =
formats::json::FromString(R"({"D":{"C":2},"A":1,"B":"sample"})");
Expand Down

0 comments on commit 1410c86

Please sign in to comment.