Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 17 additions & 18 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<sonic_json::StringView>;
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<SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE>;
// 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

Expand All @@ -268,9 +262,14 @@ using PointerType = sonic_json::GenericJsonPointer<sonic_json::StringView>;
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";
}
Expand Down Expand Up @@ -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";
}
Expand Down
16 changes: 14 additions & 2 deletions example/at_pointer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
27 changes: 11 additions & 16 deletions include/sonic/dom/genericnode.h
Original file line number Diff line number Diff line change
Expand Up @@ -1208,25 +1208,20 @@ class GenericNode {
NodeType* atPointerImpl(const GenericJsonPointer<StringType>& 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<int>(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<NodeType*>(re);
}
Expand Down
90 changes: 89 additions & 1 deletion include/sonic/dom/json_pointer.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

#pragma once

#include <limits>
#include <string>
#include <type_traits>
#include <vector>

Expand All @@ -26,7 +28,6 @@
#define SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE \
SONIC_JSON_POINTER_NODE_STRING
#else
#include <string>
#define SONIC_JSON_POINTER_NODE_STRING_DEFAULT_TYPE std::string
#endif

Expand Down Expand Up @@ -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<size_t>(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<size_t>(data[i] - '0');
if (value > (std::numeric_limits<size_t>::max() - digit) / 10) {
return false;
}
value = value * 10 + digit;
}
index = value;
return true;
}

private:
StringType str_{};
int num_{};
Expand Down Expand Up @@ -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 <typename T = StringType,
typename std::enable_if<!std::is_same<T, StringView>::value,
int>::type = 0>
bool Parse(StringView pointer) {
this->clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve aliased input before clearing tokens

When the supplied StringView is backed by one of this pointer's own tokens—for example, p.Parse(p[0].GetStr()) with a heap-backed token—clear() frees that token before pointer[0] is read, causing a heap-use-after-free and undefined parsing. Copy the input or parse into a temporary pointer before mutating *this.

AGENTS.md reference: AGENTS.md:L178-L181

Useful? React with 👍 / 👎.

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;
Expand Down
21 changes: 15 additions & 6 deletions include/sonic/internal/arch/simd_skip.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include <cstdint>
#include <functional>
#include <limits>
#include <optional>
#include <vector>

Expand Down Expand Up @@ -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<size_t>(std::numeric_limits<int>::max())) {
return -kParseErrorArrIndexOutOfRange;
}
err = GetArrayElem(data, pos, len, static_cast<int>(index));
if (err) return -err;
goto query;
}
goto err_mismatch_type;
}
return SkipOne(data, pos, len);

Expand Down
Loading