From c624971215708dfc942eeceb591a65accc615752 Mon Sep 17 00:00:00 2001 From: SC123667 <205642379+SC123667@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:39:51 +0800 Subject: [PATCH] feat(dom): parse RFC 6901 JSON pointers --- docs/usage.md | 35 ++++---- example/at_pointer.cpp | 16 +++- include/sonic/dom/genericnode.h | 27 +++--- include/sonic/dom/json_pointer.h | 90 ++++++++++++++++++- include/sonic/internal/arch/simd_skip.h | 21 +++-- tests/json_pointer_test.cpp | 115 ++++++++++++++++++++++++ tests/skip_test.cpp | 9 ++ 7 files changed, 270 insertions(+), 43 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 7b970b5b..1aad1137 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -231,26 +231,20 @@ The flag is sticky until cleared. This is a `MemoryPoolAllocator` feature, not part of the abstract allocator concept. ### JSON Pointer -Sonic provides a JsonPointer class but doesn't support resolving the JSON pointer -syntax of [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901). We will support -it in the future. +Sonic's owning `JsonPointer` type can parse the JSON Pointer string syntax from +[RFC 6901](https://www.rfc-editor.org/rfc/rfc6901). It decodes `~1` as `/` and +`~0` as `~`, and rejects malformed escapes. Empty input addresses the document +root. Numeric reference tokens remain object keys when the current value is an +object and are interpreted as canonical array indices when it is an array. ```c++ #include "sonic/sonic.h" -// Sonic JSON pointer need a template parameter that describes using which string -// type, such as std::string and std::string_view. string_view can avoid coping -// string data. But, the user should keep the memory is always valid when using -// string_view as template parameter. -using PointerType = sonic_json::GenericJsonPointer; +using PointerType = sonic_json::JsonPointer; -// Sonic also defines a typename JsonPointerView using string_view. -// example: -// using PointerType = sonic_json::JsonPointerView; -// The typename JsonPointer in sonic is defined as: -// using JsonPointer = -// GenericJsonPointer; -// The macro SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE is std::string. +// JsonPointerView is still available for pre-tokenized paths whose source +// strings outlive the pointer. RFC parsing uses JsonPointer because decoded +// tokens need owned storage. // query by JSON pointer @@ -268,9 +262,14 @@ using PointerType = sonic_json::GenericJsonPointer; std::cout << "/a doesn't exist!\n"; } - sonic_json::Node* node2 = doc.AtPointer(PointerType({"b", 1, "a"})); + PointerType pointer; + if (!pointer.Parse("/b/1/a")) { + std::cout << "Invalid RFC 6901 pointer!\n"; + return; + } + sonic_json::Node* node2 = doc.AtPointer(pointer); if (node2 != nullptr) { - std::cout << "/b/1/a Eixsts!\n"; + std::cout << "/b/1/a exists!\n"; } else { std::cout << "/b/1/a doesn't exist!\n"; } @@ -315,7 +314,7 @@ Example: ```c++ sonic_json::Node* node3 = doc.AtPointer("b", 1, "b"); if (node3 != nullptr) { - std::cout << "/b/1/b Eixsts!\n"; + std::cout << "/b/1/b exists!\n"; } else { std::cout << "/b/1/b doesn't exist!\n"; } diff --git a/example/at_pointer.cpp b/example/at_pointer.cpp index 674e8bd7..3f0ae6e8 100644 --- a/example/at_pointer.cpp +++ b/example/at_pointer.cpp @@ -31,18 +31,30 @@ int main() { sonic_json::Node* node2 = doc.AtPointer(PointerType({"b", 1, "a"})); if (node2 != nullptr) { - std::cout << "/b/1/a Eixsts!\n"; + std::cout << "/b/1/a exists!\n"; } else { std::cout << "/b/1/a doesn't exist!\n"; } sonic_json::Node* node3 = doc.AtPointer("b", 1, "b"); if (node3 != nullptr) { - std::cout << "/b/1/b Eixsts!\n"; + std::cout << "/b/1/b exists!\n"; } else { std::cout << "/b/1/b doesn't exist!\n"; } + sonic_json::JsonPointer rfc_pointer; + if (!rfc_pointer.Parse("/b/1/b")) { + std::cout << "Invalid RFC 6901 pointer!\n"; + return -1; + } + sonic_json::Node* node4 = doc.AtPointer(rfc_pointer); + if (node4 != nullptr) { + std::cout << "RFC pointer /b/1/b exists!\n"; + } else { + std::cout << "RFC pointer /b/1/b doesn't exist!\n"; + } + return 0; } diff --git a/include/sonic/dom/genericnode.h b/include/sonic/dom/genericnode.h index 7b3c4e9d..f126b439 100644 --- a/include/sonic/dom/genericnode.h +++ b/include/sonic/dom/genericnode.h @@ -1208,25 +1208,20 @@ class GenericNode { NodeType* atPointerImpl(const GenericJsonPointer& pointer) const { const NodeType* re = downCast(); for (auto& node : pointer) { - if (node.IsStr()) { - if (re->IsObject()) { - auto m = re->FindMember(node.GetStr()); - if (m != re->MemberEnd()) { - re = &(m->value); - continue; - } + if (re->IsObject() && node.IsStr()) { + auto m = re->FindMember(node.GetStr()); + if (m != re->MemberEnd()) { + re = &(m->value); + continue; } - return nullptr; - } else { // Json Pointer node is number - if (re->IsArray()) { - int idx = node.GetNum(); - if (idx >= 0 && idx < static_cast(re->Size())) { - re = &(re->operator[]((size_t)idx)); - continue; - } + } else if (re->IsArray()) { + size_t idx = 0; + if (node.TryGetArrayIndex(idx) && idx < re->Size()) { + re = &(re->operator[](idx)); + continue; } - return nullptr; } + return nullptr; } return const_cast(re); } diff --git a/include/sonic/dom/json_pointer.h b/include/sonic/dom/json_pointer.h index 8ee95b36..dd3020a3 100644 --- a/include/sonic/dom/json_pointer.h +++ b/include/sonic/dom/json_pointer.h @@ -16,6 +16,8 @@ #pragma once +#include +#include #include #include @@ -26,7 +28,6 @@ #define SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE \ SONIC_JSON_POINTER_NODE_STRING #else -#include #define SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE std::string #endif @@ -69,6 +70,43 @@ class GenericJsonPointerNode { size_t Size() const { return str_.size(); } const char* Data() const { return str_.data(); } + /** + * @brief Convert this token to an RFC 6901 array index when possible. + * @param index receives the parsed zero-based array index + * @return true for an integer node or a canonical decimal token + * @note String indices must be "0" or start with [1-9]. Leading zeroes, + * signs, overflow, and the RFC "-" token are rejected. + */ + bool TryGetArrayIndex(size_t& index) const noexcept { + if (IsNum()) { + if (num_ < 0) { + return false; + } + index = static_cast(num_); + return true; + } + + const size_t size = str_.size(); + const char* data = str_.data(); + if (size == 0 || (size > 1 && data[0] == '0')) { + return false; + } + + size_t value = 0; + for (size_t i = 0; i < size; ++i) { + if (data[i] < '0' || data[i] > '9') { + return false; + } + const size_t digit = static_cast(data[i] - '0'); + if (value > (std::numeric_limits::max() - digit) / 10) { + return false; + } + value = value * 10 + digit; + } + index = value; + return true; + } + private: StringType str_{}; int num_{}; @@ -107,6 +145,56 @@ class GenericJsonPointer } } + /** + * @brief Parse the JSON Pointer string representation from RFC 6901. + * @param pointer an empty root pointer or slash-prefixed reference tokens + * @return true on success; false for an invalid prefix or '~' escape + * @note Parsed tokens are copied into StringType. JsonPointerView is excluded + * because decoded '~0' and '~1' tokens require owned storage. + */ + template ::value, + int>::type = 0> + bool Parse(StringView pointer) { + this->clear(); + if (pointer.empty()) { + return true; + } + if (pointer[0] != '/') { + return false; + } + + size_t token_count = 1; + for (size_t i = 1; i < pointer.size(); ++i) { + token_count += pointer[i] == '/'; + } + this->reserve(token_count); + + std::string token; + size_t i = 1; + while (true) { + token.clear(); + while (i < pointer.size() && pointer[i] != '/') { + if (pointer[i] != '~') { + token.push_back(pointer[i++]); + continue; + } + if (i + 1 >= pointer.size() || + (pointer[i + 1] != '0' && pointer[i + 1] != '1')) { + this->clear(); + return false; + } + token.push_back(pointer[i + 1] == '0' ? '~' : '/'); + i += 2; + } + this->emplace_back(token); + if (i == pointer.size()) { + return true; + } + ++i; + } + } + GenericJsonPointer(const GenericJsonPointer& rhs) = default; GenericJsonPointer(GenericJsonPointer&& rhs) = default; GenericJsonPointer& operator=(const GenericJsonPointer& other) = default; diff --git a/include/sonic/internal/arch/simd_skip.h b/include/sonic/internal/arch/simd_skip.h index 1f4ec258..053de94a 100644 --- a/include/sonic/internal/arch/simd_skip.h +++ b/include/sonic/internal/arch/simd_skip.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -246,18 +247,26 @@ class SkipScanner { query: if (i++ != path.size()) { c = SkipSpaceSafe(data, pos, len); - if (path[i - 1].IsStr()) { - if (c != '{') goto err_mismatch_type; + const auto &node = path[i - 1]; + if (c == '{' && node.IsStr()) { c = GetNextToken(data, pos, len, "\"}"); if (c != '"') goto err_unknown_key; - key = StringView(path[i - 1].GetStr()); + key = StringView(node.GetStr()); goto obj_key; - } else { - if (c != '[') goto err_mismatch_type; - err = GetArrayElem(data, pos, len, path[i - 1].GetNum()); + } + if (c == '[') { + size_t index = 0; + if (!node.TryGetArrayIndex(index)) { + goto err_mismatch_type; + } + if (index > static_cast(std::numeric_limits::max())) { + return -kParseErrorArrIndexOutOfRange; + } + err = GetArrayElem(data, pos, len, static_cast(index)); if (err) return -err; goto query; } + goto err_mismatch_type; } return SkipOne(data, pos, len); diff --git a/tests/json_pointer_test.cpp b/tests/json_pointer_test.cpp index 5c983004..4ebcc992 100644 --- a/tests/json_pointer_test.cpp +++ b/tests/json_pointer_test.cpp @@ -19,6 +19,7 @@ #include #include "gtest/gtest.h" +#include "sonic/sonic.h" #include "sonic/string_view.h" namespace { @@ -113,6 +114,120 @@ TYPED_TEST(JsonPointerTest, Constructor) { { JsonPointerType p = std::move(path); } } +TEST(JsonPointerRfc6901, ParseReferenceTokens) { + JsonPointer pointer; + + EXPECT_TRUE(pointer.Parse("")); + EXPECT_TRUE(pointer.empty()); + + ASSERT_TRUE(pointer.Parse("/foo/0/a~1b/m~0n/~01/")); + ASSERT_EQ(pointer.size(), 6); + const std::vector expected = {"foo", "0", "a/b", + "m~n", "~1", ""}; + for (size_t i = 0; i < expected.size(); ++i) { + EXPECT_TRUE(pointer[i].IsStr()); + EXPECT_EQ(pointer[i].GetStr(), expected[i]); + } + + const char pointer_with_nul[] = {'/', 'a', '\0', 'b'}; + ASSERT_TRUE( + pointer.Parse(StringView(pointer_with_nul, sizeof(pointer_with_nul)))); + ASSERT_EQ(pointer.size(), 1); + EXPECT_EQ(pointer[0].GetStr(), std::string("a\0b", 3)); +} + +TEST(JsonPointerRfc6901, RejectInvalidSyntax) { + JsonPointer pointer; + for (StringView invalid : {StringView("foo"), StringView("/~"), + StringView("/~2"), StringView("/foo/~a")}) { + EXPECT_FALSE(pointer.Parse(invalid)); + EXPECT_TRUE(pointer.empty()); + } +} + +TEST(JsonPointerRfc6901, ResolveObjectsAndArrays) { + Document doc; + doc.Parse(R"({ + "foo":["bar","baz"], + "":0, + "a/b":1, + "m~n":2, + "c%d":3, + "e^f":4, + "g|h":5, + "i\\j":6, + "k\"l":7, + " ":8, + "0":"object zero", + "arr":["zero","one"], + "leading":{"01":"object leading zero"} + })"); + ASSERT_FALSE(doc.HasParseError()); + + auto resolve = [&doc](StringView source) -> Node* { + JsonPointer pointer; + return pointer.Parse(source) ? doc.AtPointer(pointer) : nullptr; + }; + + EXPECT_EQ(resolve(""), &doc); + + Node* node = resolve("/foo/0"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetString(), "bar"); + + node = resolve("/"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 0); + + node = resolve("/a~1b"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 1); + + node = resolve("/m~0n"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 2); + + node = resolve("/c%d"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 3); + + node = resolve("/e^f"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 4); + + node = resolve("/g|h"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 5); + + node = resolve("/i\\j"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 6); + + node = resolve("/k\"l"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 7); + + node = resolve("/ "); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetUint64(), 8); + + node = resolve("/0"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetString(), "object zero"); + + node = resolve("/arr/1"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetString(), "one"); + + node = resolve("/leading/01"); + ASSERT_NE(node, nullptr); + EXPECT_EQ(node->GetString(), "object leading zero"); + + EXPECT_EQ(resolve("/arr/01"), nullptr); + EXPECT_EQ(resolve("/arr/-"), nullptr); + EXPECT_EQ(resolve("/arr/184467440737095516160"), nullptr); +} + TYPED_TEST(JsonPointerTest, PushAndPop) { using JsonPointerType = TypeParam; using QueryNode = typename JsonPointerType::JsonPointerNodeType; diff --git a/tests/skip_test.cpp b/tests/skip_test.cpp index 1f59b60f..9a905aa3 100644 --- a/tests/skip_test.cpp +++ b/tests/skip_test.cpp @@ -60,6 +60,15 @@ TEST(GetOnDemand, SuccessNestedArray) { TestGetOnDemand(R"([[1], [2, 3], [4, 5, 6]])", {2, 2}, "6"); } +TEST(GetOnDemand, SuccessRfc6901Pointer) { + JsonPointer path; + ASSERT_TRUE(path.Parse("/a~1b/1/value")); + TestGetOnDemand(R"({"a/b":[{"value":1},{"value":2}]})", path, "2"); + + ASSERT_TRUE(path.Parse("/0")); + TestGetOnDemand(R"({"0":"object zero"})", path, R"("object zero")"); +} + TEST(GetOnDemand, SuccessUnicode) { TestGetOnDemand( R"("未来简史-从智人到智神hello: world, \\ {\" / \b \f \n \r \t } [景] 测试中文 😀")",