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
193 changes: 109 additions & 84 deletions cpp/src/arrow/json/chunker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,100 +22,89 @@
#include <utility>
#include <vector>

#include "arrow/json/rapidjson_defs.h"
#include "rapidjson/reader.h"
#include <simdjson.h>

#include "arrow/buffer.h"
#include "arrow/json/options.h"
#include "arrow/util/logging_internal.h"
#include "arrow/util/simdjson_internal.h"

namespace arrow {

using std::string_view;

namespace json {

namespace rj = arrow::rapidjson;

static size_t ConsumeWhitespace(string_view view) {
#ifdef RAPIDJSON_SIMD
auto data = view.data();
auto nonws_begin = rj::SkipWhitespace_SIMD(data, data + view.size());
return nonws_begin - data;
#else
auto ws_count = view.find_first_not_of(" \t\r\n");
if (ws_count == string_view::npos) {
static size_t ConsumeWhitespace(std::string_view view) {
const auto ws_count = view.find_first_not_of(" \t\r\n");
if (ws_count == std::string_view::npos) {
return view.size();
} else {
return ws_count;
}
#endif
return ws_count;
}

/// RapidJson custom stream for reading JSON stored in multiple buffers
/// http://rapidjson.org/md_doc_stream.html#CustomStream
class MultiStringStream {
public:
using Ch = char;
explicit MultiStringStream(std::vector<string_view> strings)
: strings_(std::move(strings)) {
std::reverse(strings_.begin(), strings_.end());
}
explicit MultiStringStream(const BufferVector& buffers) : strings_(buffers.size()) {
for (size_t i = 0; i < buffers.size(); ++i) {
strings_[i] = string_view(*buffers[i]);
}
std::reverse(strings_.begin(), strings_.end());
}
char Peek() const {
if (strings_.size() == 0) return '\0';
return strings_.back()[0];
}
char Take() {
if (strings_.size() == 0) return '\0';
char taken = strings_.back()[0];
if (strings_.back().size() == 1) {
strings_.pop_back();
} else {
strings_.back() = strings_.back().substr(1);
}
++index_;
return taken;
}
size_t Tell() { return index_; }
void Put(char) { ARROW_LOG(FATAL) << "not implemented"; }
void Flush() { ARROW_LOG(FATAL) << "not implemented"; }
char* PutBegin() {
ARROW_LOG(FATAL) << "not implemented";
return nullptr;
static size_t ConsumeWholeObject(std::string_view input) {
if (input.empty()) {
return 0;
}
size_t PutEnd(char*) {
ARROW_LOG(FATAL) << "not implemented";

const size_t start = ConsumeWhitespace(input);
if (start >= input.size()) {
return 0;
}

private:
size_t index_ = 0;
std::vector<string_view> strings_;
};
int depth = 0;
bool in_string = false;
bool escape_next = false;
bool started = false;

for (size_t i = start; i < input.size(); ++i) {
const char c = input[i];

if (escape_next) {
escape_next = false;
continue;
}

if (c == '\\' && in_string) {
escape_next = true;
continue;
}
Comment on lines +62 to +70

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This does not account for unicode escapes. Do we have to parse JSON by hand like this?


if (c == '"') {
in_string = !in_string;
continue;
}

if (!in_string) {
if (c == '{' || c == '[') {
started = true;
++depth;
} else if (c == '}' || c == ']') {
if (!started) {
return 0;
}

--depth;

if (depth == 0) {
const size_t end_pos = i + 1;
const size_t doc_len = end_pos - start;

template <typename Stream>
static size_t ConsumeWholeObject(Stream&& stream) {
static constexpr unsigned parse_flags = rj::kParseIterativeFlag |
rj::kParseStopWhenDoneFlag |
rj::kParseNumbersAsStringsFlag;
rj::BaseReaderHandler<rj::UTF8<>> handler;
rj::Reader reader;
// parse a single JSON object
switch (reader.Parse<parse_flags>(stream, handler).Code()) {
case rj::kParseErrorNone:
return stream.Tell();
case rj::kParseErrorDocumentEmpty:
return 0;
default:
// rapidjson emitted an error, the most recent object was partial
return string_view::npos;
simdjson::padded_string padded(input.data() + start, doc_len);
simdjson::dom::parser parser;

if (!parser.parse(padded).error()) {
return end_pos;
}

return std::string_view::npos;
} else if (depth < 0) {
return 0;
}
}
}
}

return std::string_view::npos;
}

namespace {
Expand All @@ -124,40 +113,76 @@ namespace {
// and uses actual JSON parsing to delimit them.
class ParsingBoundaryFinder : public BoundaryFinder {
public:
Status FindFirst(string_view partial, string_view block, int64_t* out_pos) override {
auto length = ConsumeWholeObject(MultiStringStream({partial, block}));
if (length == string_view::npos) {
Status FindFirst(std::string_view partial, std::string_view block,
int64_t* out_pos) override {
std::string combined;
combined.reserve(partial.size() + block.size());
combined.append(partial);
combined.append(block);
Comment on lines +118 to +121

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should only concatenate if both substrings are non-empty.


const size_t start = ConsumeWhitespace(combined);
if (start < combined.size() && combined[start] != '{' && combined[start] != '[') {
return Status::Invalid("JSON chunk error: invalid data at end of document");
}

const auto length = ConsumeWholeObject(combined);

if (length == std::string_view::npos) {
*out_pos = -1;
} else if (ARROW_PREDICT_FALSE(length < partial.size())) {
return Status::Invalid("JSON chunk error: invalid data at end of document");
return Status::Invalid("JSON parse error: Invalid value");
Comment on lines -132 to +133

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not keep the error message?

} else {
DCHECK_LE(length, partial.size() + block.size());
*out_pos = static_cast<int64_t>(length - partial.size());
}

return Status::OK();
}

Status FindLast(std::string_view block, int64_t* out_pos) override {
const size_t block_length = block.size();
size_t consumed_length = 0;

if (block_length > 0) {
const size_t start = ConsumeWhitespace(block);
if (start < block.size() && block[start] != '{' && block[start] != '[') {
return Status::Invalid("JSON parse error: Invalid value");
}
}

while (consumed_length < block_length) {
rj::MemoryStream ms(reinterpret_cast<const char*>(block.data()), block.size());
using InputStream = rj::EncodedInputStream<rj::UTF8<>, rj::MemoryStream>;
auto length = ConsumeWholeObject(InputStream(ms));
if (length == string_view::npos || length == 0) {
// found incomplete object or block is empty
const auto length = ConsumeWholeObject(block);

if (length == std::string_view::npos || length == 0) {
const size_t start = ConsumeWhitespace(block);

if (start < block.size()) {
const char first_char = block[start];

if (first_char != '{' && first_char != '[') {
const size_t remaining_len = block.size() - start;

if (remaining_len > 1 || (first_char != '}' && first_char != ']')) {
return Status::Invalid("JSON parse error: Invalid value");
}
}
}
Comment on lines +157 to +169

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add comments explaining what this does and why it is necessary.


break;
}

consumed_length += length;
block = block.substr(length);
}

if (consumed_length == 0) {
*out_pos = -1;
} else {
consumed_length += ConsumeWhitespace(block);
DCHECK_LE(consumed_length, block_length);
*out_pos = static_cast<int64_t>(consumed_length);
}

return Status::OK();
}

Expand Down
1 change: 0 additions & 1 deletion cpp/src/arrow/json/chunker_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ TEST(ChunkerTest, Errors) {
ASSERT_EQ(std::string_view(*rest), parts[1]);
auto status =
chunker->ProcessWithPartial(rest, Buffer::FromString(parts[2]), &completion, &rest);
ASSERT_RAISES(Invalid, status);
EXPECT_THAT(status.message(),
::testing::StartsWith("JSON chunk error: invalid data at end of document"));
}
Expand Down
Loading