blob: 84a5724fece10c7299e1594add58798fd0ecaa16 [file] [log] [blame]
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkJSON.h"
#include "SkMalloc.h"
#include "SkStream.h"
#include "SkString.h"
#include <cmath>
#include <tuple>
#include <vector>
namespace skjson {
// #define SK_JSON_REPORT_ERRORS
static_assert( sizeof(Value) == 8, "");
static_assert(alignof(Value) == 8, "");
static constexpr size_t kRecAlign = alignof(Value);
void Value::init_tagged(Tag t) {
memset(fData8, 0, sizeof(fData8));
fData8[Value::kTagOffset] = SkTo<uint8_t>(t);
SkASSERT(this->getTag() == t);
}
// Pointer values store a type (in the upper kTagBits bits) and a pointer.
void Value::init_tagged_pointer(Tag t, void* p) {
*this->cast<uintptr_t>() = reinterpret_cast<uintptr_t>(p);
if (sizeof(Value) == sizeof(uintptr_t)) {
// For 64-bit, we rely on the pointer upper bits being unused/zero.
SkASSERT(!(fData8[kTagOffset] & kTagMask));
fData8[kTagOffset] |= SkTo<uint8_t>(t);
} else {
// For 32-bit, we need to zero-initialize the upper 32 bits
SkASSERT(sizeof(Value) == sizeof(uintptr_t) * 2);
this->cast<uintptr_t>()[kTagOffset >> 2] = 0;
fData8[kTagOffset] = SkTo<uint8_t>(t);
}
SkASSERT(this->getTag() == t);
SkASSERT(this->ptr<void>() == p);
}
NullValue::NullValue() {
this->init_tagged(Tag::kNull);
SkASSERT(this->getTag() == Tag::kNull);
}
BoolValue::BoolValue(bool b) {
this->init_tagged(Tag::kBool);
*this->cast<bool>() = b;
SkASSERT(this->getTag() == Tag::kBool);
}
NumberValue::NumberValue(int32_t i) {
this->init_tagged(Tag::kInt);
*this->cast<int32_t>() = i;
SkASSERT(this->getTag() == Tag::kInt);
}
NumberValue::NumberValue(float f) {
this->init_tagged(Tag::kFloat);
*this->cast<float>() = f;
SkASSERT(this->getTag() == Tag::kFloat);
}
// Vector recs point to externally allocated slabs with the following layout:
//
// [size_t n] [REC_0] ... [REC_n-1] [optional extra trailing storage]
//
// Long strings use extra_alloc_size == 1 to store the \0 terminator.
//
template <typename T, size_t extra_alloc_size = 0>
static void* MakeVector(const void* src, size_t size, SkArenaAlloc& alloc) {
// The Ts are already in memory, so their size should be safe.
const auto total_size = sizeof(size_t) + size * sizeof(T) + extra_alloc_size;
auto* size_ptr = reinterpret_cast<size_t*>(alloc.makeBytesAlignedTo(total_size, kRecAlign));
*size_ptr = size;
sk_careful_memcpy(size_ptr + 1, src, size * sizeof(T));
return size_ptr;
}
ArrayValue::ArrayValue(const Value* src, size_t size, SkArenaAlloc& alloc) {
this->init_tagged_pointer(Tag::kArray, MakeVector<Value>(src, size, alloc));
SkASSERT(this->getTag() == Tag::kArray);
}
// Strings have two flavors:
//
// -- short strings (len <= 7) -> these are stored inline, in the record
// (one byte reserved for null terminator/type):
//
// [str] [\0]|[max_len - actual_len]
//
// Storing [max_len - actual_len] allows the 'len' field to double-up as a
// null terminator when size == max_len (this works 'cause kShortString == 0).
//
// -- long strings (len > 7) -> these are externally allocated vectors (VectorRec<char>).
//
// The string data plus a null-char terminator are copied over.
//
namespace {
// An internal string builder with a fast 8 byte short string load path
// (for the common case where the string is not at the end of the stream).
class FastString final : public Value {
public:
FastString(const char* src, size_t size, const char* eos, SkArenaAlloc& alloc) {
SkASSERT(src <= eos);
if (size > kMaxInlineStringSize) {
this->initLongString(src, size, alloc);
SkASSERT(this->getTag() == Tag::kString);
return;
}
static_assert(static_cast<uint8_t>(Tag::kShortString) == 0, "please don't break this");
static_assert(sizeof(Value) == 8, "");
// TODO: LIKELY
if (src + 7 <= eos) {
this->initFastShortString(src, size);
} else {
this->initShortString(src, size);
}
SkASSERT(this->getTag() == Tag::kShortString);
}
private:
static constexpr size_t kMaxInlineStringSize = sizeof(Value) - 1;
void initLongString(const char* src, size_t size, SkArenaAlloc& alloc) {
SkASSERT(size > kMaxInlineStringSize);
this->init_tagged_pointer(Tag::kString, MakeVector<char, 1>(src, size, alloc));
auto* data = this->cast<VectorValue<char, Value::Type::kString>>()->begin();
const_cast<char*>(data)[size] = '\0';
}
void initShortString(const char* src, size_t size) {
SkASSERT(size <= kMaxInlineStringSize);
this->init_tagged(Tag::kShortString);
sk_careful_memcpy(this->cast<char>(), src, size);
// Null terminator provided by init_tagged() above (fData8 is zero-initialized).
}
void initFastShortString(const char* src, size_t size) {
SkASSERT(size <= kMaxInlineStringSize);
// Load 8 chars and mask out the tag and \0 terminator.
uint64_t* s64 = this->cast<uint64_t>();
memcpy(s64, src, 8);
#if defined(SK_CPU_LENDIAN)
*s64 &= 0x00ffffffffffffffULL >> ((kMaxInlineStringSize - size) * 8);
#else
static_assert(false, "Big-endian builds are not supported at this time.");
#endif
}
};
} // namespace
StringValue::StringValue(const char* src, size_t size, SkArenaAlloc& alloc) {
new (this) FastString(src, size, src, alloc);
}
ObjectValue::ObjectValue(const Member* src, size_t size, SkArenaAlloc& alloc) {
this->init_tagged_pointer(Tag::kObject, MakeVector<Member>(src, size, alloc));
SkASSERT(this->getTag() == Tag::kObject);
}
// Boring public Value glue.
const Value& ObjectValue::operator[](const char* key) const {
// Reverse search for duplicates resolution (policy: return last).
const auto* begin = this->begin();
const auto* member = this->end();
while (member > begin) {
--member;
if (0 == strcmp(key, member->fKey.as<StringValue>().begin())) {
return member->fValue;
}
}
static const Value g_null = NullValue();
return g_null;
}
namespace {
// Lexer/parser inspired by rapidjson [1], sajson [2] and pjson [3].
//
// [1] https://github.com/Tencent/rapidjson/
// [2] https://github.com/chadaustin/sajson
// [3] https://pastebin.com/hnhSTL3h
// bit 0 (0x01) - plain ASCII string character
// bit 1 (0x02) - whitespace
// bit 2 (0x04) - string terminator (" \0 [control chars] **AND } ]** <- see matchString notes)
// bit 3 (0x08) - 0-9
// bit 4 (0x10) - 0-9 e E .
// bit 5 (0x20) - scope terminator (} ])
static constexpr uint8_t g_token_flags[256] = {
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 6, 4, 4, 6, 4, 4, // 0
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // 1
3, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0x11,1, // 2
0x19,0x19,0x19,0x19,0x19,0x19,0x19,0x19, 0x19,0x19, 1, 1, 1, 1, 1, 1, // 3
1, 1, 1, 1, 1, 0x11,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,0x25, 1, 1, // 5
1, 1, 1, 1, 1, 0x11,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,0x25, 1, 1, // 7
// 128-255
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0
};
static inline bool is_ws(char c) { return g_token_flags[static_cast<uint8_t>(c)] & 0x02; }
static inline bool is_eostring(char c) { return g_token_flags[static_cast<uint8_t>(c)] & 0x04; }
static inline bool is_digit(char c) { return g_token_flags[static_cast<uint8_t>(c)] & 0x08; }
static inline bool is_numeric(char c) { return g_token_flags[static_cast<uint8_t>(c)] & 0x10; }
static inline bool is_eoscope(char c) { return g_token_flags[static_cast<uint8_t>(c)] & 0x20; }
static inline const char* skip_ws(const char* p) {
while (is_ws(*p)) ++p;
return p;
}
static inline float pow10(int32_t exp) {
static constexpr float g_pow10_table[63] =
{
1.e-031f, 1.e-030f, 1.e-029f, 1.e-028f, 1.e-027f, 1.e-026f, 1.e-025f, 1.e-024f,
1.e-023f, 1.e-022f, 1.e-021f, 1.e-020f, 1.e-019f, 1.e-018f, 1.e-017f, 1.e-016f,
1.e-015f, 1.e-014f, 1.e-013f, 1.e-012f, 1.e-011f, 1.e-010f, 1.e-009f, 1.e-008f,
1.e-007f, 1.e-006f, 1.e-005f, 1.e-004f, 1.e-003f, 1.e-002f, 1.e-001f, 1.e+000f,
1.e+001f, 1.e+002f, 1.e+003f, 1.e+004f, 1.e+005f, 1.e+006f, 1.e+007f, 1.e+008f,
1.e+009f, 1.e+010f, 1.e+011f, 1.e+012f, 1.e+013f, 1.e+014f, 1.e+015f, 1.e+016f,
1.e+017f, 1.e+018f, 1.e+019f, 1.e+020f, 1.e+021f, 1.e+022f, 1.e+023f, 1.e+024f,
1.e+025f, 1.e+026f, 1.e+027f, 1.e+028f, 1.e+029f, 1.e+030f, 1.e+031f
};
static constexpr int32_t k_exp_offset = SK_ARRAY_COUNT(g_pow10_table) / 2;
// We only support negative exponents for now.
SkASSERT(exp <= 0);
return (exp >= -k_exp_offset) ? g_pow10_table[exp + k_exp_offset]
: std::pow(10.0f, static_cast<float>(exp));
}
class DOMParser {
public:
explicit DOMParser(SkArenaAlloc& alloc)
: fAlloc(alloc) {
fValueStack.reserve(kValueStackReserve);
}
const Value parse(const char* p, size_t size) {
if (!size) {
return this->error(NullValue(), p, "invalid empty input");
}
const char* p_stop = p + size - 1;
// We're only checking for end-of-stream on object/array close('}',']'),
// so we must trim any whitespace from the buffer tail.
while (p_stop > p && is_ws(*p_stop)) --p_stop;
SkASSERT(p_stop >= p && p_stop < p + size);
if (!is_eoscope(*p_stop)) {
return this->error(NullValue(), p_stop, "invalid top-level value");
}
p = skip_ws(p);
switch (*p) {
case '{':
goto match_object;
case '[':
goto match_array;
default:
return this->error(NullValue(), p, "invalid top-level value");
}
match_object:
SkASSERT(*p == '{');
p = skip_ws(p + 1);
this->pushObjectScope();
if (*p == '}') goto pop_object;
// goto match_object_key;
match_object_key:
p = skip_ws(p);
if (*p != '"') return this->error(NullValue(), p, "expected object key");
p = this->matchString(p, p_stop, [this](const char* key, size_t size, const char* eos) {
this->pushObjectKey(key, size, eos);
});
if (!p) return NullValue();
p = skip_ws(p);
if (*p != ':') return this->error(NullValue(), p, "expected ':' separator");
++p;
// goto match_value;
match_value:
p = skip_ws(p);
switch (*p) {
case '\0':
return this->error(NullValue(), p, "unexpected input end");
case '"':
p = this->matchString(p, p_stop, [this](const char* str, size_t size, const char* eos) {
this->pushString(str, size, eos);
});
break;
case '[':
goto match_array;
case 'f':
p = this->matchFalse(p);
break;
case 'n':
p = this->matchNull(p);
break;
case 't':
p = this->matchTrue(p);
break;
case '{':
goto match_object;
default:
p = this->matchNumber(p);
break;
}
if (!p) return NullValue();
// goto match_post_value;
match_post_value:
SkASSERT(!this->inTopLevelScope());
p = skip_ws(p);
switch (*p) {
case ',':
++p;
if (this->inObjectScope()) {
goto match_object_key;
} else {
SkASSERT(this->inArrayScope());
goto match_value;
}
case ']':
goto pop_array;
case '}':
goto pop_object;
default:
return this->error(NullValue(), p - 1, "unexpected value-trailing token");
}
// unreachable
SkASSERT(false);
pop_object:
SkASSERT(*p == '}');
if (this->inArrayScope()) {
return this->error(NullValue(), p, "unexpected object terminator");
}
this->popObjectScope();
// goto pop_common
pop_common:
SkASSERT(is_eoscope(*p));
if (this->inTopLevelScope()) {
SkASSERT(fValueStack.size() == 1);
// Success condition: parsed the top level element and reached the stop token.
return p == p_stop
? fValueStack.front()
: this->error(NullValue(), p + 1, "trailing root garbage");
}
if (p == p_stop) {
return this->error(NullValue(), p, "unexpected end-of-input");
}
++p;
goto match_post_value;
match_array:
SkASSERT(*p == '[');
p = skip_ws(p + 1);
this->pushArrayScope();
if (*p != ']') goto match_value;
// goto pop_array;
pop_array:
SkASSERT(*p == ']');
if (this->inObjectScope()) {
return this->error(NullValue(), p, "unexpected array terminator");
}
this->popArrayScope();
goto pop_common;
SkASSERT(false);
return NullValue();
}
std::tuple<const char*, const SkString> getError() const {
return std::make_tuple(fErrorToken, fErrorMessage);
}
private:
SkArenaAlloc& fAlloc;
// Pending values stack.
static constexpr size_t kValueStackReserve = 256;
std::vector<Value> fValueStack;
// Tracks the current object/array scope, as an index into fStack:
//
// - for objects: fScopeIndex = (index of first value in scope)
// - for arrays : fScopeIndex = -(index of first value in scope)
//
// fScopeIndex == 0 IFF we are at the top level (no current/active scope).
intptr_t fScopeIndex = 0;
// Error reporting.
const char* fErrorToken = nullptr;
SkString fErrorMessage;
bool inTopLevelScope() const { return fScopeIndex == 0; }
bool inObjectScope() const { return fScopeIndex > 0; }
bool inArrayScope() const { return fScopeIndex < 0; }
// Helper for masquerading raw primitive types as Values (bypassing tagging, etc).
template <typename T>
class RawValue final : public Value {
public:
explicit RawValue(T v) {
static_assert(sizeof(T) <= sizeof(Value), "");
*this->cast<T>() = v;
}
T operator *() const { return *this->cast<T>(); }
};
template <typename VectorT>
void popScopeAsVec(size_t scope_start) {
SkASSERT(scope_start > 0);
SkASSERT(scope_start <= fValueStack.size());
using T = typename VectorT::ValueT;
static_assert( sizeof(T) >= sizeof(Value), "");
static_assert( sizeof(T) % sizeof(Value) == 0, "");
static_assert(alignof(T) == alignof(Value), "");
const auto scope_count = fValueStack.size() - scope_start,
count = scope_count / (sizeof(T) / sizeof(Value));
SkASSERT(scope_count % (sizeof(T) / sizeof(Value)) == 0);
const auto* begin = reinterpret_cast<const T*>(fValueStack.data() + scope_start);
// Restore the previous scope index from saved placeholder value,
// and instantiate as a vector of values in scope.
auto& placeholder = fValueStack[scope_start - 1];
fScopeIndex = *static_cast<RawValue<intptr_t>&>(placeholder);
placeholder = VectorT(begin, count, fAlloc);
// Drop the (consumed) values in scope.
fValueStack.resize(scope_start);
}
void pushObjectScope() {
// Save a scope index now, and then later we'll overwrite this value as the Object itself.
fValueStack.push_back(RawValue<intptr_t>(fScopeIndex));
// New object scope.
fScopeIndex = SkTo<intptr_t>(fValueStack.size());
}
void popObjectScope() {
SkASSERT(this->inObjectScope());
this->popScopeAsVec<ObjectValue>(SkTo<size_t>(fScopeIndex));
SkDEBUGCODE(
const auto& obj = fValueStack.back().as<ObjectValue>();
SkASSERT(obj.is<ObjectValue>());
for (const auto& member : obj) {
SkASSERT(member.fKey.is<StringValue>());
}
)
}
void pushArrayScope() {
// Save a scope index now, and then later we'll overwrite this value as the Array itself.
fValueStack.push_back(RawValue<intptr_t>(fScopeIndex));
// New array scope.
fScopeIndex = -SkTo<intptr_t>(fValueStack.size());
}
void popArrayScope() {
SkASSERT(this->inArrayScope());
this->popScopeAsVec<ArrayValue>(SkTo<size_t>(-fScopeIndex));
SkDEBUGCODE(
const auto& arr = fValueStack.back().as<ArrayValue>();
SkASSERT(arr.is<ArrayValue>());
)
}
void pushObjectKey(const char* key, size_t size, const char* eos) {
SkASSERT(this->inObjectScope());
SkASSERT(fValueStack.size() >= SkTo<size_t>(fScopeIndex));
SkASSERT(!((fValueStack.size() - SkTo<size_t>(fScopeIndex)) & 1));
this->pushString(key, size, eos);
}
void pushTrue() {
fValueStack.push_back(BoolValue(true));
}
void pushFalse() {
fValueStack.push_back(BoolValue(false));
}
void pushNull() {
fValueStack.push_back(NullValue());
}
void pushString(const char* s, size_t size, const char* eos) {
fValueStack.push_back(FastString(s, size, eos, fAlloc));
}
void pushInt32(int32_t i) {
fValueStack.push_back(NumberValue(i));
}
void pushFloat(float f) {
fValueStack.push_back(NumberValue(f));
}
template <typename T>
T error(T&& ret_val, const char* p, const char* msg) {
#if defined(SK_JSON_REPORT_ERRORS)
fErrorToken = p;
fErrorMessage.set(msg);
#endif
return ret_val;
}
const char* matchTrue(const char* p) {
SkASSERT(p[0] == 't');
if (p[1] == 'r' && p[2] == 'u' && p[3] == 'e') {
this->pushTrue();
return p + 4;
}
return this->error(nullptr, p, "invalid token");
}
const char* matchFalse(const char* p) {
SkASSERT(p[0] == 'f');
if (p[1] == 'a' && p[2] == 'l' && p[3] == 's' && p[4] == 'e') {
this->pushFalse();
return p + 5;
}
return this->error(nullptr, p, "invalid token");
}
const char* matchNull(const char* p) {
SkASSERT(p[0] == 'n');
if (p[1] == 'u' && p[2] == 'l' && p[3] == 'l') {
this->pushNull();
return p + 4;
}
return this->error(nullptr, p, "invalid token");
}
template <typename MatchFunc>
const char* matchString(const char* p, const char* p_stop, MatchFunc&& func) {
SkASSERT(*p == '"');
const auto* s_begin = p + 1;
// TODO: unescape
do {
// Consume string chars.
for (p = p + 1; !is_eostring(*p); ++p);
if (*p == '"') {
// Valid string found.
func(s_begin, p - s_begin, p_stop);
return p + 1;
}
// End-of-scope chars are special: we use them to tag the end of the input.
// Thus they cannot be consumed indiscriminately -- we need to check if we hit the
// end of the input. To that effect, we treat them as string terminators above,
// then we catch them here.
} while (is_eoscope(*p) && (p != p_stop)); // Safe scope terminator char, keep going.
// Premature end-of-input, or illegal string char.
return this->error(nullptr, s_begin - 1, "invalid string");
}
const char* matchFastFloatDecimalPart(const char* p, int sign, float f, int exp) {
SkASSERT(exp <= 0);
for (;;) {
if (!is_digit(*p)) break;
f = f * 10.f + (*p++ - '0'); --exp;
if (!is_digit(*p)) break;
f = f * 10.f + (*p++ - '0'); --exp;
}
if (is_numeric(*p)) {
SkASSERT(*p == '.' || *p == 'e' || *p == 'E');
// We either have malformed input, or an (unsupported) exponent.
return nullptr;
}
this->pushFloat(sign * f * pow10(exp));
return p;
}
const char* matchFastFloatPart(const char* p, int sign, float f) {
for (;;) {
if (!is_digit(*p)) break;
f = f * 10.f + (*p++ - '0');
if (!is_digit(*p)) break;
f = f * 10.f + (*p++ - '0');
}
if (!is_numeric(*p)) {
// Matched (integral) float.
this->pushFloat(sign * f);
return p;
}
return (*p == '.') ? this->matchFastFloatDecimalPart(p + 1, sign, f, 0)
: nullptr;
}
const char* matchFast32OrFloat(const char* p) {
int sign = 1;
if (*p == '-') {
sign = -1;
++p;
}
const auto* digits_start = p;
int32_t n32 = 0;
// This is the largest absolute int32 value we can handle before
// risking overflow *on the next digit* (214748363).
static constexpr int32_t kMaxInt32 = (std::numeric_limits<int32_t>::max() - 9) / 10;
if (is_digit(*p)) {
n32 = (*p++ - '0');
for (;;) {
if (!is_digit(*p) || n32 > kMaxInt32) break;
n32 = n32 * 10 + (*p++ - '0');
}
}
if (!is_numeric(*p)) {
// Did we actually match any digits?
if (p > digits_start) {
this->pushInt32(sign * n32);
return p;
}
return nullptr;
}
if (*p == '.') {
const auto* decimals_start = ++p;
int exp = 0;
for (;;) {
if (!is_digit(*p) || n32 > kMaxInt32) break;
n32 = n32 * 10 + (*p++ - '0'); --exp;
if (!is_digit(*p) || n32 > kMaxInt32) break;
n32 = n32 * 10 + (*p++ - '0'); --exp;
}
if (!is_numeric(*p)) {
// Did we actually match any digits?
if (p > decimals_start) {
this->pushFloat(sign * n32 * pow10(exp));
return p;
}
return nullptr;
}
if (n32 > kMaxInt32) {
// we ran out on n32 bits
return this->matchFastFloatDecimalPart(p, sign, n32, exp);
}
}
return this->matchFastFloatPart(p, sign, n32);
}
const char* matchNumber(const char* p) {
if (const auto* fast = this->matchFast32OrFloat(p)) return fast;
// slow fallback
char* matched;
float f = strtof(p, &matched);
if (matched > p) {
this->pushFloat(f);
return matched;
}
return this->error(nullptr, p, "invalid numeric token");
}
};
void Write(const Value& v, SkWStream* stream) {
switch (v.getType()) {
case Value::Type::kNull:
stream->writeText("null");
break;
case Value::Type::kBool:
stream->writeText(*v.as<BoolValue>() ? "true" : "false");
break;
case Value::Type::kNumber:
stream->writeScalarAsText(*v.as<NumberValue>());
break;
case Value::Type::kString:
stream->writeText("\"");
stream->writeText(v.as<StringValue>().begin());
stream->writeText("\"");
break;
case Value::Type::kArray: {
const auto& array = v.as<ArrayValue>();
stream->writeText("[");
bool first_value = true;
for (const auto& v : array) {
if (!first_value) stream->writeText(",");
Write(v, stream);
first_value = false;
}
stream->writeText("]");
break;
}
case Value::Type::kObject:
const auto& object = v.as<ObjectValue>();
stream->writeText("{");
bool first_member = true;
for (const auto& member : object) {
SkASSERT(member.fKey.getType() == Value::Type::kString);
if (!first_member) stream->writeText(",");
Write(member.fKey, stream);
stream->writeText(":");
Write(member.fValue, stream);
first_member = false;
}
stream->writeText("}");
break;
}
}
} // namespace
SkString Value::toString() const {
SkDynamicMemoryWStream wstream;
Write(*this, &wstream);
const auto data = wstream.detachAsData();
// TODO: is there a better way to pass data around without copying?
return SkString(static_cast<const char*>(data->data()), data->size());
}
static constexpr size_t kMinChunkSize = 4096;
DOM::DOM(const char* data, size_t size)
: fAlloc(kMinChunkSize) {
DOMParser parser(fAlloc);
fRoot = parser.parse(data, size);
}
void DOM::write(SkWStream* stream) const {
Write(fRoot, stream);
}
} // namespace skjson