mirror of
https://github.com/felt/tippecanoe.git
synced 2026-10-02 08:25:40 +02:00
Consolidate the jsonpull comments
Comments were 25% of the added lines, and the ownership model was spelled out in five places. Collect it into one block at the top of jsonpull.h and point at it from the rest, cutting the ratio to 14% and the total by about 200 lines. Removed the duplicate explanations of the deleter dispatch, of what detach does to the back-pointers, and of "json_read returns intermediate containers, do not free them". Trimmed the comments that argued for a choice rather than described the code -- the reserve(2) / reserve(4) rationales, the string-buffer copy, the pmtiles check ordering -- to a line each, and shortened the test preambles, keeping the parts that say why a test is shaped the way it is. No code changes; the test suite is unchanged in both configurations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KNxyHKasyWrWcvre2yK4r
This commit is contained in:
+3
-6
@@ -261,12 +261,9 @@ sqlite3 *dirmeta2tmp(const char *fname) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &e : o->entries()) {
|
for (const auto &e : o->entries()) {
|
||||||
// Skip, rather than just warn about, anything that isn't a
|
// Skip, not just warn: a metadata.json from another tool may
|
||||||
// string/string pair: reading a non-string through string()
|
// have a numeric minzoom or a nested object, and string()
|
||||||
// would assert in a debug build and misinterpret the node's
|
// asserts on the type.
|
||||||
// storage in a release build. (A metadata.json written by
|
|
||||||
// something other than tippecanoe may well have numeric
|
|
||||||
// minzoom/maxzoom or a nested "json" object.)
|
|
||||||
if (e.key->type != JSON_STRING || e.value->type != JSON_STRING) {
|
if (e.key->type != JSON_STRING || e.value->type != JSON_STRING) {
|
||||||
fprintf(stderr, "%s: non-string in metadata\n", name.c_str());
|
fprintf(stderr, "%s: non-string in metadata\n", name.c_str());
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+1
-3
@@ -238,9 +238,7 @@ struct json_serialize_action : json_feature_action {
|
|||||||
std::string layername;
|
std::string layername;
|
||||||
|
|
||||||
int add_feature(json_object *geometry, bool geometrycollection, json_object *properties, json_object *id, json_object *tippecanoe, json_object *feature) {
|
int add_feature(json_object *geometry, bool geometrycollection, json_object *properties, json_object *id, json_object *tippecanoe, json_object *feature) {
|
||||||
// This only ever receives json_read results from geojson-loop, whose
|
// Only json_read results reach this; a detached tree has no parser.
|
||||||
// parser is still attached. json_read_tree / json_disconnect clear
|
|
||||||
// every parser pointer, so a detached tree would null-deref here.
|
|
||||||
assert(geometry->parser != nullptr);
|
assert(geometry->parser != nullptr);
|
||||||
sst->line = geometry->parser->line;
|
sst->line = geometry->parser->line;
|
||||||
if (geometrycollection) {
|
if (geometrycollection) {
|
||||||
|
|||||||
+20
-82
@@ -88,13 +88,8 @@ static inline int read_wrap(json_pull *j) {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct an instance of the right subclass for the given type.
|
|
||||||
// JSON_TRUE / JSON_FALSE / JSON_NULL and the parse-token types are bare
|
// JSON_TRUE / JSON_FALSE / JSON_NULL and the parse-token types are bare
|
||||||
// json_objects; the value-bearing types each get their own subclass.
|
// json_objects; the value-bearing types each get their own subclass.
|
||||||
//
|
|
||||||
// Returns a json_object_ptr (unique_ptr with a type-dispatching deleter,
|
|
||||||
// see jsonpull.h), so the caller doesn't have to remember which subclass
|
|
||||||
// was constructed when it eventually deletes.
|
|
||||||
static json_object_ptr make_object(json_type type, json_object *parent, json_pull *jp) {
|
static json_object_ptr make_object(json_type type, json_object *parent, json_pull *jp) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case JSON_NUMBER:
|
case JSON_NUMBER:
|
||||||
@@ -114,11 +109,8 @@ static inline json_pull::parse_frame *current_frame(json_pull *j) {
|
|||||||
return j->container_stack.empty() ? nullptr : &j->container_stack.back();
|
return j->container_stack.empty() ? nullptr : &j->container_stack.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a new node of `type` and install it as a child of the
|
// Install a new node of `type` in the current container, or as the parser's
|
||||||
// current container (or as the parser's root, if the container stack
|
// root if the stack is empty. Returns it borrowed, or nullptr after setting
|
||||||
// is empty). Returns a borrowed pointer into the parser-owned tree;
|
|
||||||
// the unique_ptr that owns the node lives in whichever vector slot
|
|
||||||
// we just pushed it into. Returns nullptr on error after setting
|
|
||||||
// j->error.
|
// j->error.
|
||||||
static json_object *add_object(json_pull *j, json_type type) {
|
static json_object *add_object(json_pull *j, json_type type) {
|
||||||
json_pull::parse_frame *f = current_frame(j);
|
json_pull::parse_frame *f = current_frame(j);
|
||||||
@@ -137,9 +129,8 @@ static json_object *add_object(json_pull *j, json_type type) {
|
|||||||
}
|
}
|
||||||
} else if (c->type == JSON_HASH) {
|
} else if (c->type == JSON_HASH) {
|
||||||
if (f->expect == JSON_VALUE) {
|
if (f->expect == JSON_VALUE) {
|
||||||
// JSON_VALUE is only set by a colon, a colon requires
|
// A colon is the only thing that sets JSON_VALUE, and it
|
||||||
// JSON_COLON, and only pushing a key sets that, so there
|
// requires a key already pushed.
|
||||||
// is always an entry waiting for its value here.
|
|
||||||
assert(!c->entries().empty());
|
assert(!c->entries().empty());
|
||||||
c->entries().back().value = std::move(o);
|
c->entries().back().value = std::move(o);
|
||||||
f->expect = JSON_COMMA;
|
f->expect = JSON_COMMA;
|
||||||
@@ -237,8 +228,6 @@ again:
|
|||||||
if (o == nullptr) {
|
if (o == nullptr) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
// add_object already installed `o` in the parent (or the
|
|
||||||
// parser's root) as a unique_ptr; the frame just borrows.
|
|
||||||
j->container_stack.push_back({o, JSON_ITEM});
|
j->container_stack.push_back({o, JSON_ITEM});
|
||||||
|
|
||||||
if (cb != nullptr) {
|
if (cb != nullptr) {
|
||||||
@@ -268,8 +257,6 @@ again:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pop the frame; ownership of `cc` stays with whatever
|
|
||||||
// surrounding container (or jp->root) installed it.
|
|
||||||
j->container_stack.pop_back();
|
j->container_stack.pop_back();
|
||||||
return cc;
|
return cc;
|
||||||
}
|
}
|
||||||
@@ -520,9 +507,7 @@ again:
|
|||||||
/////////////////////////// Strings
|
/////////////////////////// Strings
|
||||||
|
|
||||||
case '"': {
|
case '"': {
|
||||||
// Reuse the parser-wide string buffer so we don't construct a
|
// Reused across tokens; see json_pull::string_buffer.
|
||||||
// fresh std::string (with its inevitable SSO->heap promotion
|
|
||||||
// and capacity doublings) for every JSON_STRING token.
|
|
||||||
std::string &val = j->string_buffer;
|
std::string &val = j->string_buffer;
|
||||||
val.clear();
|
val.clear();
|
||||||
|
|
||||||
@@ -648,11 +633,7 @@ again:
|
|||||||
|
|
||||||
json_object *s = add_object(j, JSON_STRING);
|
json_object *s = add_object(j, JSON_STRING);
|
||||||
if (s != nullptr) {
|
if (s != nullptr) {
|
||||||
// Copy (don't move) so j->string_buffer retains its
|
// Copy, not move, so the buffer keeps its capacity.
|
||||||
// grown capacity for the next token. The copy is a
|
|
||||||
// single right-sized allocation plus one memcpy, which
|
|
||||||
// is cheaper than the multiple capacity doublings the
|
|
||||||
// per-token std::string would otherwise incur.
|
|
||||||
s->string() = val;
|
s->string() = val;
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
@@ -667,10 +648,6 @@ json_object *json_read(json_pull_ptr &j) {
|
|||||||
return json_read_separators(j, nullptr, nullptr);
|
return json_read_separators(j, nullptr, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward declaration so json_read_tree can sever the tree it hands out
|
|
||||||
// from the parser -- this lets callers (like the filter loaders) keep the
|
|
||||||
// returned tree past the parser's lifetime without having to follow up
|
|
||||||
// with a separate json_disconnect call.
|
|
||||||
static void detach_subtree(json_object *o);
|
static void detach_subtree(json_object *o);
|
||||||
|
|
||||||
json_object_ptr json_read_tree(json_pull_ptr &p) {
|
json_object_ptr json_read_tree(json_pull_ptr &p) {
|
||||||
@@ -678,10 +655,6 @@ json_object_ptr json_read_tree(json_pull_ptr &p) {
|
|||||||
|
|
||||||
while ((j = json_read(p)) != nullptr) {
|
while ((j = json_read(p)) != nullptr) {
|
||||||
if (j->parent == nullptr) {
|
if (j->parent == nullptr) {
|
||||||
// The parser owns the top-level value via p->root;
|
|
||||||
// transfer ownership out to the caller and detach
|
|
||||||
// the subtree from the parser so the caller can
|
|
||||||
// outlive the json_pull.
|
|
||||||
json_object_ptr tree = std::move(p->root);
|
json_object_ptr tree = std::move(p->root);
|
||||||
detach_subtree(tree.get());
|
detach_subtree(tree.get());
|
||||||
return tree;
|
return tree;
|
||||||
@@ -691,20 +664,13 @@ json_object_ptr json_read_tree(json_pull_ptr &p) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take ownership of `o` away from its parent (or from the parser's
|
// Move the owning json_object_ptr out of whatever vector slot or hash entry
|
||||||
// root) by moving the owning json_object_ptr out of whatever vector
|
// holds `o`. Empty if nothing tracked owns it -- already detached, or borrowed
|
||||||
// slot or hash entry holds it. Returns the unique_ptr to the caller,
|
// from elsewhere.
|
||||||
// who is now solely responsible for it. Returns an empty
|
|
||||||
// json_object_ptr if `o` is not currently owned by a parent or by
|
|
||||||
// the parser (e.g. already detached, or only borrowed from somewhere
|
|
||||||
// untracked).
|
|
||||||
//
|
//
|
||||||
// For a hash, removing a single key or value individually would
|
// Detaching one half of a hash entry would disturb the surrounding key/value
|
||||||
// disturb the surrounding key/value pairing, so we replace the
|
// pairing, so the extracted half is replaced by a JSON_NULL placeholder and the
|
||||||
// extracted half with a fresh JSON_NULL placeholder and only erase
|
// entry is erased only once both halves are gone.
|
||||||
// the entry once both halves have been detached. This matches the
|
|
||||||
// historical json_disconnect semantics for partially-disconnected
|
|
||||||
// pairs.
|
|
||||||
static json_object_ptr take_from_owner(json_object *o) {
|
static json_object_ptr take_from_owner(json_object *o) {
|
||||||
if (o == nullptr) {
|
if (o == nullptr) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -756,37 +722,13 @@ static json_object_ptr take_from_owner(json_object *o) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// json_free splices `o` out of its parent (if any), or out of the
|
// Splice `o` out of its owner and destroy it; `o` dangles afterwards. No need
|
||||||
// parser's root (if `o` is the most recently completed top-level
|
// to clear back-pointers, since nothing will observe them again.
|
||||||
// value), and destroys the subtree. After this call, `o` is a
|
|
||||||
// dangling pointer and must not be used.
|
|
||||||
//
|
|
||||||
// geojson-loop.cpp relies on this to release each feature after it
|
|
||||||
// has been serialized, so that already-serialized features don't sit
|
|
||||||
// in memory while subsequent features are parsed.
|
|
||||||
//
|
|
||||||
// Unlike json_disconnect, this does NOT walk the subtree clearing
|
|
||||||
// parent/parser back-pointers, because the subtree is about to be
|
|
||||||
// destroyed and those pointers will never be observed again -- the
|
|
||||||
// unique_ptr returned by take_from_owner goes out of scope at the end
|
|
||||||
// of this function and runs the type-dispatching deleter.
|
|
||||||
void json_free(json_object *o) {
|
void json_free(json_object *o) {
|
||||||
(void) take_from_owner(o);
|
(void) take_from_owner(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk the subtree clearing the parser back-pointers, so the detached
|
// See Ownership model in jsonpull.h for why only `parser` is cleared here.
|
||||||
// subtree can outlive the json_pull it was parsed from. Every `parser`
|
|
||||||
// pointer has to go: the json_pull may be destroyed while the subtree
|
|
||||||
// lives on, and a stale one would dangle.
|
|
||||||
//
|
|
||||||
// The `parent` pointers *inside* the subtree are deliberately left alone.
|
|
||||||
// They are non-owning raw pointers, so keeping them cannot create a
|
|
||||||
// reference cycle or hold anything alive, and they point at nodes the
|
|
||||||
// caller now owns as one unit -- they stay valid for exactly as long as
|
|
||||||
// the subtree does. Keeping them also means the tree stays navigable
|
|
||||||
// upwards, and that json_free() / json_disconnect() keep working on
|
|
||||||
// interior nodes of a detached tree, both of which need o->parent to
|
|
||||||
// find the node's owner.
|
|
||||||
static void clear_parser_pointers(json_object *o) {
|
static void clear_parser_pointers(json_object *o) {
|
||||||
if (o == nullptr) {
|
if (o == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -807,11 +749,9 @@ static void clear_parser_pointers(json_object *o) {
|
|||||||
o->parser = nullptr;
|
o->parser = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sever a subtree that take_from_owner() has just moved out of the tree it
|
// The root's `parent` pointed out of the subtree, at a node the parser still
|
||||||
// belonged to. The root's own `parent` is the one back-pointer that must be
|
// owns; leaving it set would make a later json_free look for this node in a
|
||||||
// cleared: it pointed *out* of the subtree, at a node the parser still owns
|
// container that no longer holds it.
|
||||||
// and may destroy, and leaving it set would make a later json_free() on this
|
|
||||||
// root hunt for itself in a container that no longer holds it.
|
|
||||||
static void detach_subtree(json_object *o) {
|
static void detach_subtree(json_object *o) {
|
||||||
if (o == nullptr) {
|
if (o == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -835,10 +775,8 @@ static void json_print_one(std::string &val, const json_object *o) {
|
|||||||
} else if (o->type == JSON_STRING) {
|
} else if (o->type == JSON_STRING) {
|
||||||
val.push_back('\"');
|
val.push_back('\"');
|
||||||
|
|
||||||
// Range over the string rather than walking c_str(): the value is a
|
// Range, not c_str(): the value may contain an embedded NUL, which the
|
||||||
// std::string now and may legitimately contain an embedded NUL, which
|
// control-character branch below escapes like any other.
|
||||||
// the control-character branch below escapes as a \u sequence like any
|
|
||||||
// other control character.
|
|
||||||
for (char c : o->string()) {
|
for (char c : o->string()) {
|
||||||
if (c == '\\' || c == '"') {
|
if (c == '\\' || c == '"') {
|
||||||
val.push_back('\\');
|
val.push_back('\\');
|
||||||
|
|||||||
+48
-129
@@ -31,70 +31,42 @@ typedef enum json_type {
|
|||||||
struct json_object;
|
struct json_object;
|
||||||
struct json_pull;
|
struct json_pull;
|
||||||
|
|
||||||
// json_object is non-virtual so that JSON_TRUE / JSON_FALSE / JSON_NULL
|
// Ownership model
|
||||||
// nodes don't have to pay for a vptr, but the typed subclasses
|
//
|
||||||
// (json_number, json_string, json_array, json_hash) have non-trivial
|
// Every node has exactly one owner: its parent (a json_object_ptr in the
|
||||||
// destructors that need to run to free their std::vector / std::string
|
// parent's vector or hash entry), the parser (jp->root) for a top-level value,
|
||||||
// members. So json_object_ptr is given a custom empty deleter that
|
// or the caller once json_read_tree / json_disconnect hands the tree over.
|
||||||
// dispatches on `type` and static_casts to the right subclass before
|
// json_read and json_hash_get return borrowed pointers, valid while the owning
|
||||||
// `delete`. The deleter is stateless, so the unique_ptr stays one
|
// container is intact.
|
||||||
// pointer wide.
|
//
|
||||||
|
// `parent` and `parser` are non-owning, so they cannot form a cycle. Detaching
|
||||||
|
// clears every `parser` in the subtree, since the json_pull may die first, and
|
||||||
|
// clears `parent` only on the detached root, which pointed out of the subtree.
|
||||||
|
// Interior `parent` links stay, so a detached tree is still walkable upwards
|
||||||
|
// and json_free / json_disconnect still work inside it -- both find a node's
|
||||||
|
// owner through `parent`.
|
||||||
|
//
|
||||||
|
// json_object has no vptr; json_object_deleter switches on `type` and
|
||||||
|
// static_casts so the right subclass destructor runs. Payloads live in those
|
||||||
|
// subclasses rather than one wide struct, and the accessors assert on `type`
|
||||||
|
// before downcasting.
|
||||||
|
//
|
||||||
|
// json_pull_ptr is a shared_ptr: a parser is created and freed once.
|
||||||
|
|
||||||
|
// Stateless, so json_object_ptr stays one pointer wide. See Ownership model.
|
||||||
struct json_object_deleter {
|
struct json_object_deleter {
|
||||||
void operator()(json_object *p) const noexcept;
|
void operator()(json_object *p) const noexcept;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ownership of a JSON subtree is unique: every node has a single owner,
|
|
||||||
// which is either its parent (via a json_object_ptr in the parent's
|
|
||||||
// vector or hash entry) or, for the root, the parser (via jp->root) or
|
|
||||||
// the caller (after json_read_tree / json_disconnect).
|
|
||||||
//
|
|
||||||
// Callers receive borrowed `json_object *` views from json_read,
|
|
||||||
// json_hash_get, etc.; those pointers stay valid as long as the owning
|
|
||||||
// container is intact (which, for json_read results, means "until the
|
|
||||||
// next json_read, json_free, or json_disconnect call on that subtree").
|
|
||||||
//
|
|
||||||
// json_pull_ptr stays a shared_ptr because the parser is created once
|
|
||||||
// and freed once and the cost of shared_ptr there is irrelevant.
|
|
||||||
typedef std::unique_ptr<json_object, json_object_deleter> json_object_ptr;
|
typedef std::unique_ptr<json_object, json_object_deleter> json_object_ptr;
|
||||||
typedef std::shared_ptr<json_pull> json_pull_ptr;
|
typedef std::shared_ptr<json_pull> json_pull_ptr;
|
||||||
|
|
||||||
// A single key/value pair inside a JSON_HASH. The pairs are stored in
|
// One key/value pair in a JSON_HASH, held in source order.
|
||||||
// insertion order in a single std::vector<json_entry> on json_hash, so
|
|
||||||
// callers can range-for over `o->entries()` with structured bindings
|
|
||||||
// (`for (auto &[k, v] : o->entries()) ...`) while still preserving the
|
|
||||||
// order keys appeared in the source document.
|
|
||||||
struct json_entry {
|
struct json_entry {
|
||||||
json_object_ptr key;
|
json_object_ptr key;
|
||||||
json_object_ptr value;
|
json_object_ptr value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// json_object is a small base type that just records the JSON type and
|
|
||||||
// the back-pointers to its parent and parser. The actual value payload
|
|
||||||
// lives in a type-specific subclass (json_number, json_string, json_array,
|
|
||||||
// json_hash), so that JSON_TRUE / JSON_FALSE / JSON_NULL nodes pay only
|
|
||||||
// the base-class cost and a JSON_HASH does not also drag along a string
|
|
||||||
// or a number field. Type-tagged accessor methods on the base class
|
|
||||||
// downcast and return references to the underlying subclass storage.
|
|
||||||
//
|
|
||||||
// Children are owned by their parent (via std::vector<json_object_ptr>
|
|
||||||
// inside json_array / json_hash); the raw `parent` and `parser`
|
|
||||||
// back-pointers own nothing. json_disconnect() splices a node out of its
|
|
||||||
// parent and walks the detached subtree clearing every `parser` pointer,
|
|
||||||
// so the subtree can outlive the original parser. The `parent` pointers
|
|
||||||
// within the subtree survive -- they refer to nodes the caller now owns
|
|
||||||
// as one unit -- so a detached tree can still be walked upwards, and
|
|
||||||
// json_free() / json_disconnect() still work on its interior nodes. Only
|
|
||||||
// the detached root's `parent`, which pointed out of the subtree, is
|
|
||||||
// cleared.
|
|
||||||
//
|
|
||||||
// json_object intentionally has no virtual functions and no virtual
|
|
||||||
// destructor; the json_object_ptr deleter (see below in this header)
|
|
||||||
// switches on `type` and static_casts to the correct subclass before
|
|
||||||
// `delete`, so each subclass's destructor still runs without costing
|
|
||||||
// a vptr per node. Dispatch on `type` is what the rest of the code
|
|
||||||
// already does. The accessor methods assert at debug time that the
|
|
||||||
// type matches before downcasting.
|
|
||||||
|
|
||||||
struct json_object {
|
struct json_object {
|
||||||
json_object *parent = nullptr;
|
json_object *parent = nullptr;
|
||||||
json_pull *parser = nullptr;
|
json_pull *parser = nullptr;
|
||||||
@@ -108,17 +80,11 @@ struct json_object {
|
|||||||
: parent(p), parser(pl), type(t) {
|
: parent(p), parser(pl), type(t) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type-tagged accessors. Each one asserts that the receiver is of
|
|
||||||
// the right kind, then downcasts to the storage in the appropriate
|
|
||||||
// subclass. Inline so the assert and cast disappear at -O.
|
|
||||||
inline std::string &string();
|
inline std::string &string();
|
||||||
inline const std::string &string() const;
|
inline const std::string &string() const;
|
||||||
|
|
||||||
// Numbers are stored in a discriminated union (double / unsigned /
|
// large_unsigned() / large_signed() return 0 when the number is not
|
||||||
// signed) so a json_number is only 32 bytes instead of 48. The
|
// held in that representation, the convention callers already expect.
|
||||||
// large_*() accessors return 0 when the number is not currently
|
|
||||||
// stored in that representation, matching the prior convention
|
|
||||||
// where "0" meant "not set, fall through to the next slot".
|
|
||||||
inline double number() const;
|
inline double number() const;
|
||||||
inline unsigned long long large_unsigned() const;
|
inline unsigned long long large_unsigned() const;
|
||||||
inline long long large_signed() const;
|
inline long long large_signed() const;
|
||||||
@@ -170,12 +136,7 @@ struct json_string : json_object {
|
|||||||
struct json_array : json_object {
|
struct json_array : json_object {
|
||||||
std::vector<json_object_ptr> array_value;
|
std::vector<json_object_ptr> array_value;
|
||||||
|
|
||||||
// Coordinate-heavy GeoJSON dominates the parse workload, and every
|
// 2 slots: coordinate pairs dominate the parse workload.
|
||||||
// `[x, y]` (or `[x, y, z]`) pair would otherwise force the inner
|
|
||||||
// vector through 0 -> 1 -> 2 -> 4 growths plus the matching
|
|
||||||
// shared_ptr copies. Reserving 2 slots up front eliminates those
|
|
||||||
// reallocations for the common case and adds only a single small
|
|
||||||
// allocation for larger rings (which still grow geometrically).
|
|
||||||
json_array()
|
json_array()
|
||||||
: json_object(JSON_ARRAY) {
|
: json_object(JSON_ARRAY) {
|
||||||
array_value.reserve(2);
|
array_value.reserve(2);
|
||||||
@@ -189,10 +150,7 @@ struct json_array : json_object {
|
|||||||
struct json_hash : json_object {
|
struct json_hash : json_object {
|
||||||
std::vector<json_entry> entries_value;
|
std::vector<json_entry> entries_value;
|
||||||
|
|
||||||
// Most GeoJSON property hashes have a handful of keys (type, id,
|
// 4 slots: the typical GeoJSON property hash.
|
||||||
// properties, geometry, plus a few attribute fields). Reserving 4
|
|
||||||
// slots avoids the 0 -> 1 -> 2 -> 4 growth chain for the typical
|
|
||||||
// case while only modestly over-allocating for one-key hashes.
|
|
||||||
json_hash()
|
json_hash()
|
||||||
: json_object(JSON_HASH) {
|
: json_object(JSON_HASH) {
|
||||||
entries_value.reserve(4);
|
entries_value.reserve(4);
|
||||||
@@ -276,9 +234,6 @@ inline void json_object_deleter::operator()(json_object *p) const noexcept {
|
|||||||
if (p == nullptr) {
|
if (p == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Dispatch on the discriminator so the correct subclass destructor
|
|
||||||
// runs. json_object has no virtual destructor, so a bare `delete p`
|
|
||||||
// would skip the std::vector / std::string members of the subclass.
|
|
||||||
switch (p->type) {
|
switch (p->type) {
|
||||||
case JSON_NUMBER:
|
case JSON_NUMBER:
|
||||||
delete static_cast<json_number *>(p);
|
delete static_cast<json_number *>(p);
|
||||||
@@ -293,9 +248,7 @@ inline void json_object_deleter::operator()(json_object *p) const noexcept {
|
|||||||
delete static_cast<json_hash *>(p);
|
delete static_cast<json_hash *>(p);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// JSON_TRUE / JSON_FALSE / JSON_NULL (and the parse-token
|
// JSON_TRUE / JSON_FALSE / JSON_NULL: no extra fields.
|
||||||
// types, which never appear as owned nodes) are bare
|
|
||||||
// json_objects with no extra fields.
|
|
||||||
delete p;
|
delete p;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -311,33 +264,21 @@ struct json_pull {
|
|||||||
ssize_t buffer_tail = 0;
|
ssize_t buffer_tail = 0;
|
||||||
ssize_t buffer_head = 0;
|
ssize_t buffer_head = 0;
|
||||||
|
|
||||||
// Stack of currently-open containers; the top is the innermost
|
// Currently-open containers, innermost last, each with the token it
|
||||||
// container being parsed. Each frame also remembers what token is
|
// expects next. `container` is borrowed; the owner is the surrounding
|
||||||
// expected next (an item, a comma, a key, a colon, or a value).
|
// container, or `root` for the outermost.
|
||||||
// The frame's `container` is a borrowed raw pointer; actual
|
|
||||||
// ownership of the in-progress container lives in either the
|
|
||||||
// surrounding container's vector (for nested containers) or
|
|
||||||
// `root` (for the outermost container).
|
|
||||||
struct parse_frame {
|
struct parse_frame {
|
||||||
json_object *container;
|
json_object *container;
|
||||||
json_type expect;
|
json_type expect;
|
||||||
};
|
};
|
||||||
std::vector<parse_frame> container_stack;
|
std::vector<parse_frame> container_stack;
|
||||||
|
|
||||||
// The most recently completed top-level value. The parser owns
|
// Most recently completed top-level value, owned until the next one
|
||||||
// it (as a unique_ptr) until either: the next top-level value
|
// starts parsing or the caller takes it.
|
||||||
// starts parsing (the old root is destroyed), the caller calls
|
|
||||||
// json_read_tree (ownership is transferred out), or the caller
|
|
||||||
// calls json_free / json_disconnect (the parser's reference is
|
|
||||||
// dropped explicitly).
|
|
||||||
json_object_ptr root;
|
json_object_ptr root;
|
||||||
|
|
||||||
// Scratch buffers reused across tokens so we don't reallocate per
|
// Reused across tokens, cleared but not shrunk, so they stop
|
||||||
// number/string. number_buffer accumulates raw digits before atof();
|
// reallocating once grown to the largest token seen.
|
||||||
// string_buffer accumulates decoded bytes before being copied into
|
|
||||||
// the final json_string. Both are cleared (capacity preserved) at
|
|
||||||
// the start of each token, so once they grow to the largest seen
|
|
||||||
// size they stop reallocating entirely.
|
|
||||||
std::string number_buffer;
|
std::string number_buffer;
|
||||||
std::string string_buffer;
|
std::string string_buffer;
|
||||||
};
|
};
|
||||||
@@ -347,32 +288,20 @@ json_pull_ptr json_begin_string(const char *s);
|
|||||||
|
|
||||||
json_pull_ptr json_begin(ssize_t (*read)(struct json_pull *, char *buffer, size_t n), void *source);
|
json_pull_ptr json_begin(ssize_t (*read)(struct json_pull *, char *buffer, size_t n), void *source);
|
||||||
|
|
||||||
// json_end is a thin convenience that resets the caller's json_pull_ptr.
|
// Resets the caller's pointer. Optional: the parser frees itself when the
|
||||||
// The parser (and any tree it still owns) is freed when the last
|
// last json_pull_ptr to it goes away.
|
||||||
// shared_ptr to it is dropped, so calling json_end is optional if the
|
|
||||||
// json_pull_ptr will go out of scope on its own.
|
|
||||||
void json_end(json_pull_ptr &p);
|
void json_end(json_pull_ptr &p);
|
||||||
|
|
||||||
typedef void (*json_separator_callback)(json_type type, json_pull *j, void *state);
|
typedef void (*json_separator_callback)(json_type type, json_pull *j, void *state);
|
||||||
|
|
||||||
// json_read returns a borrowed pointer to the next completed JSON node
|
// The next completed node, borrowed. Valid until the next call that extends
|
||||||
// in the stream. The returned pointer is valid until the next call that
|
// or trims the parser's tree. nullptr at end of input or on error.
|
||||||
// extends or trims the parser's tree (the next json_read on the same
|
|
||||||
// parser, a json_free on the same node, or a json_disconnect that
|
|
||||||
// extracts the node). Returns nullptr at end of input or on error.
|
|
||||||
//
|
|
||||||
// For top-level values, ownership stays with the parser (via jp->root);
|
|
||||||
// for nested values, ownership stays with the enclosing container.
|
|
||||||
json_object *json_read(json_pull_ptr &j);
|
json_object *json_read(json_pull_ptr &j);
|
||||||
json_object *json_read_separators(json_pull_ptr &j, json_separator_callback cb, void *state);
|
json_object *json_read_separators(json_pull_ptr &j, json_separator_callback cb, void *state);
|
||||||
|
|
||||||
// json_read_tree drains the next top-level value out of the parser
|
// Drains the next top-level value out of the parser and hands it over. The
|
||||||
// and hands ownership to the caller. After it returns, jp->root is
|
// returned tree can outlive the json_pull. See Ownership model for what the
|
||||||
// empty, every `parser` back-pointer in the subtree has been cleared
|
// detach does and does not clear.
|
||||||
// (as has the root's `parent`), and the caller's json_object_ptr is the
|
|
||||||
// only thing keeping the tree alive. The returned tree can outlive the
|
|
||||||
// json_pull it was parsed from, and stays internally navigable: the
|
|
||||||
// `parent` pointers between its nodes are left intact.
|
|
||||||
json_object_ptr json_read_tree(json_pull_ptr &j);
|
json_object_ptr json_read_tree(json_pull_ptr &j);
|
||||||
|
|
||||||
// json_free splices `o` out of its parent (if any), or clears the
|
// json_free splices `o` out of its parent (if any), or clears the
|
||||||
@@ -381,23 +310,13 @@ json_object_ptr json_read_tree(json_pull_ptr &j);
|
|||||||
// that must not be used. Safe to call with nullptr.
|
// that must not be used. Safe to call with nullptr.
|
||||||
void json_free(json_object *o);
|
void json_free(json_object *o);
|
||||||
|
|
||||||
// Splice `o` out of its parent's array/object (or out of the parser's
|
// Splices `o` out of whatever owns it and hands it over, same detach as
|
||||||
// root), walk the detached subtree clearing every `parser` back-pointer
|
// json_read_tree. See Ownership model.
|
||||||
// (and the root's `parent`, which pointed out of the subtree), and return
|
|
||||||
// ownership of the subtree to the caller as a json_object_ptr. After this
|
|
||||||
// returns, the parser no longer references any node in the subtree, and
|
|
||||||
// the subtree can outlive the original parser. The `parent` pointers
|
|
||||||
// among the subtree's own nodes are preserved, so the detached tree can
|
|
||||||
// still be walked upwards and json_free() / json_disconnect() still work
|
|
||||||
// on its interior nodes.
|
|
||||||
json_object_ptr json_disconnect(json_object *o);
|
json_object_ptr json_disconnect(json_object *o);
|
||||||
|
|
||||||
// Look up `s` in the hash `o`. Returns a borrowed pointer; ownership
|
// Borrowed value for `s`. nullptr if `o` is not a hash, `s` is absent, or the
|
||||||
// stays with the hash. nullptr if `o` is not a hash, or `s` is absent,
|
// hash is mid-parse with that value slot unfilled -- a JSON `null` is none of
|
||||||
// or the hash is still being parsed and the value slot for `s` is not
|
// those, and comes back as a JSON_NULL node.
|
||||||
// yet filled. A JSON `null` value is *not* one of those cases: it comes
|
|
||||||
// back as a JSON_NULL node. Accepts a json_object_ptr by reference as a
|
|
||||||
// convenience so callers don't have to write `.get()`.
|
|
||||||
json_object *json_hash_get(const json_object_ptr &o, const char *s);
|
json_object *json_hash_get(const json_object_ptr &o, const char *s);
|
||||||
json_object *json_hash_get(json_object *o, const char *s);
|
json_object *json_hash_get(json_object *o, const char *s);
|
||||||
|
|
||||||
|
|||||||
+4
-9
@@ -147,15 +147,10 @@ serial_feature parse_feature(json_pull_ptr &jp, int z, unsigned x, unsigned y, s
|
|||||||
serial_feature sf;
|
serial_feature sf;
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
// json_read returns each token as the parser produces it, including
|
// json_read also returns the intermediate containers of a feature
|
||||||
// intermediate (still incomplete) container nodes. We must NOT free
|
// still being assembled, so the `continue` paths leave `j` alone;
|
||||||
// these intermediates here: they belong to the larger feature hash
|
// freeing one would splice it out of its parent. `j` is only freed
|
||||||
// still being assembled, and freeing them would splice them out of
|
// once it is a complete Feature.
|
||||||
// the parent and corrupt the in-progress tree. So the `continue`
|
|
||||||
// paths below all leave `j` alone; `j` is only freed once it is a
|
|
||||||
// complete Feature hash -- either just before returning it, or at
|
|
||||||
// the bottom of the loop if its geometry turned out to be empty --
|
|
||||||
// or as `jp->root` when the stream ends.
|
|
||||||
json_object *j = json_read(jp);
|
json_object *j = json_read(jp);
|
||||||
if (j == nullptr) {
|
if (j == nullptr) {
|
||||||
if (jp->error != nullptr) {
|
if (jp->error != nullptr) {
|
||||||
|
|||||||
+1
-5
@@ -416,11 +416,7 @@ sqlite3 *pmtilesmeta2tmp(const char *fname, const char *pmtiles_map) {
|
|||||||
state.json_write_hash();
|
state.json_write_hash();
|
||||||
|
|
||||||
for (const auto &e : o->entries()) {
|
for (const auto &e : o->entries()) {
|
||||||
// Establish that the key really is a string before reading it as
|
// Check before reading, since string() asserts on the type.
|
||||||
// one, rather than after: string() asserts on the type, so the
|
|
||||||
// check has to come first to be the thing that catches a bad key.
|
|
||||||
// (The parser rejects non-string hash keys, so this is belt and
|
|
||||||
// braces, but the ordering is what makes it meaningful.)
|
|
||||||
if (e.key->type != JSON_STRING) {
|
if (e.key->type != JSON_STRING) {
|
||||||
fprintf(stderr, "%s: non-string key in metadata\n", fname);
|
fprintf(stderr, "%s: non-string key in metadata\n", fname);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+1
-5
@@ -321,11 +321,7 @@ std::vector<mvt_layer> parse_layers(FILE *fp, int z, unsigned x, unsigned y, int
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// json_read returns each parser token in sequence, including
|
// Only complete Features are freed; see plugin.cpp::parse_feature.
|
||||||
// intermediate (still-incomplete) container nodes. Freeing those
|
|
||||||
// here would splice them out of the feature hash being built
|
|
||||||
// up, so only free `j` once we have processed a complete
|
|
||||||
// Feature (or `jp->root` when the stream ends).
|
|
||||||
json_object *type = json_hash_get(j, "type");
|
json_object *type = json_hash_get(j, "type");
|
||||||
if (type == nullptr || type->type != JSON_STRING) {
|
if (type == nullptr || type->type != JSON_STRING) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -138,14 +138,10 @@ TEST_CASE("line_is_too_small") {
|
|||||||
REQUIRE(line_is_too_small(dv, 0, 10));
|
REQUIRE(line_is_too_small(dv, 0, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regression test for the surrogate-decoding bug that compared the leftover
|
// A high surrogate followed by a non-surrogate used to be combined into one
|
||||||
// outer-loop byte `c` against `0xdfff` instead of the parsed code unit `ch`.
|
// code point, because the range check tested the outer-loop byte instead of
|
||||||
// For a string like "\uD83D\uE000" (a valid high surrogate followed by a
|
// the parsed code unit. The stale surrogate should come out as standalone
|
||||||
// non-surrogate BMP code point) the buggy version would mis-classify
|
// CESU-8, then U+E000 encoded normally.
|
||||||
// U+E000 as a low surrogate and combine the two units into the four-byte
|
|
||||||
// UTF-8 sequence F0 9F 90 80 (U+1F400). The fixed version flushes the
|
|
||||||
// stale high surrogate as standalone CESU-8 (ED A0 BD) and then encodes
|
|
||||||
// U+E000 normally as EE 80 80.
|
|
||||||
TEST_CASE("jsonpull surrogate-pair regression", "[jsonpull][surrogate]") {
|
TEST_CASE("jsonpull surrogate-pair regression", "[jsonpull][surrogate]") {
|
||||||
json_pull_ptr jp = json_begin_string("\"\\uD83D\\uE000\"");
|
json_pull_ptr jp = json_begin_string("\"\\uD83D\\uE000\"");
|
||||||
json_object_ptr o = json_read_tree(jp);
|
json_object_ptr o = json_read_tree(jp);
|
||||||
@@ -163,22 +159,13 @@ TEST_CASE("jsonpull surrogate-pair regression", "[jsonpull][surrogate]") {
|
|||||||
REQUIRE(o->string() != buggy);
|
REQUIRE(o->string() != buggy);
|
||||||
}
|
}
|
||||||
|
|
||||||
// geojson-loop.cpp calls json_free(j) after jfa->add_feature has
|
// What geojson-loop does: free each feature once serialized, so they do not
|
||||||
// serialized the feature, intending to drop the JSON subtree from the
|
// accumulate while the rest of the document is parsed.
|
||||||
// in-progress parse tree so that already-serialized features don't sit
|
|
||||||
// in memory while subsequent features are parsed. That intent was
|
|
||||||
// never tested; this test pins it down. The pre-fix behavior of
|
|
||||||
// json_free was a bare unique_ptr/shared_ptr reset that only dropped
|
|
||||||
// the caller's local reference; the parent container kept the subtree
|
|
||||||
// alive, so memory grew until the top-level parse completed.
|
|
||||||
//
|
//
|
||||||
// Note what this shape can and cannot catch. Because json_read hands back
|
// This shape cannot catch the array-splicing bug, at any size: json_read hands
|
||||||
// each container as it completes, the node freed here is always the most
|
// back each container as it completes, so the node freed here is always the
|
||||||
// recently added element of its parent, which is the one case the old
|
// last-added element of its parent, which the old memmove got right by moving
|
||||||
// element-count-vs-byte-count memmove got right (it moved zero bytes).
|
// zero bytes. "json_free prunes a non-final element" below covers that.
|
||||||
// Widening this test to more elements does not change that. The
|
|
||||||
// "json_free prunes a non-final element" case below is what pins the
|
|
||||||
// array-splicing fix.
|
|
||||||
TEST_CASE("json_free prunes a subtree from its parent", "[jsonpull][memory]") {
|
TEST_CASE("json_free prunes a subtree from its parent", "[jsonpull][memory]") {
|
||||||
json_pull_ptr jp = json_begin_string("[[1, 2], [3, 4], [5, 6]]");
|
json_pull_ptr jp = json_begin_string("[[1, 2], [3, 4], [5, 6]]");
|
||||||
|
|
||||||
@@ -222,15 +209,9 @@ TEST_CASE("json_free prunes a subtree from its parent", "[jsonpull][memory]") {
|
|||||||
REQUIRE(outer->array()[1]->array()[1]->number() == 6);
|
REQUIRE(outer->array()[1]->array()[1]->number() == 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The companion case to the pruning test above: in a line-delimited
|
// A top-level value has no parent, so json_free has to drop the parser's
|
||||||
// stream, each feature returned by json_read is a top-level value
|
// reference instead. jp->root being empty afterwards is also proof the subtree
|
||||||
// with no parent, but the parser still owns it via jp->root.
|
// was destroyed, since jp->root was its only owner.
|
||||||
// json_free must drop that parser reference too, otherwise the
|
|
||||||
// just-serialized feature would sit in memory until the next feature
|
|
||||||
// started parsing. Under the unique_ptr ownership model, the only
|
|
||||||
// owner is jp->root, so verifying that jp->root is empty after the
|
|
||||||
// json_free call is also a guarantee that the subtree itself has
|
|
||||||
// been destroyed.
|
|
||||||
TEST_CASE("json_free releases a top-level value held by the parser", "[jsonpull][memory]") {
|
TEST_CASE("json_free releases a top-level value held by the parser", "[jsonpull][memory]") {
|
||||||
json_pull_ptr jp = json_begin_string(R"({"a": 1, "b": [2, 3]})");
|
json_pull_ptr jp = json_begin_string(R"({"a": 1, "b": [2, 3]})");
|
||||||
|
|
||||||
@@ -255,12 +236,9 @@ TEST_CASE("json_free releases a top-level value held by the parser", "[jsonpull]
|
|||||||
REQUIRE(jp->root == nullptr);
|
REQUIRE(jp->root == nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// json_disconnect() is the documented way to splice a subtree out of the
|
// Nothing in tippecanoe calls json_disconnect today -- the filter loaders get
|
||||||
// parser's tree and take ownership of it so that it can outlive the
|
// the same guarantee from json_read_tree -- so cover it here rather than leave
|
||||||
// json_pull it came from. Nothing in tippecanoe calls it today -- the
|
// a documented ownership primitive untested.
|
||||||
// filter loaders get the same guarantee from json_read_tree, which clears
|
|
||||||
// back-pointers on the way out -- so cover it here rather than leave a
|
|
||||||
// documented ownership primitive untested.
|
|
||||||
TEST_CASE("json_disconnect hands a subtree to the caller", "[jsonpull][ownership]") {
|
TEST_CASE("json_disconnect hands a subtree to the caller", "[jsonpull][ownership]") {
|
||||||
json_object_ptr taken;
|
json_object_ptr taken;
|
||||||
json_object *outer = nullptr;
|
json_object *outer = nullptr;
|
||||||
@@ -317,11 +295,8 @@ TEST_CASE("json_disconnect hands a subtree to the caller", "[jsonpull][ownership
|
|||||||
REQUIRE(taken->array()[1]->number() == 4);
|
REQUIRE(taken->array()[1]->number() == 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserving the parent links inside a detached tree is what lets
|
// json_free finds a node's owner through o->parent, so clearing the interior
|
||||||
// json_free() keep working on its interior nodes: json_free() finds a
|
// parent links on detach would make this a silent no-op.
|
||||||
// node's owner through o->parent, so a detached tree whose parent links
|
|
||||||
// had been cleared would silently ignore the request and leave the node
|
|
||||||
// in place.
|
|
||||||
TEST_CASE("json_free prunes an interior node of a detached tree", "[jsonpull][ownership]") {
|
TEST_CASE("json_free prunes an interior node of a detached tree", "[jsonpull][ownership]") {
|
||||||
json_pull_ptr jp = json_begin_string("[[1, 2], [3, 4], [5, 6]]");
|
json_pull_ptr jp = json_begin_string("[[1, 2], [3, 4], [5, 6]]");
|
||||||
json_object_ptr tree = json_read_tree(jp);
|
json_object_ptr tree = json_read_tree(jp);
|
||||||
@@ -350,10 +325,8 @@ TEST_CASE("json_free prunes an interior node of a detached tree", "[jsonpull][ow
|
|||||||
REQUIRE(tree->array()[1]->array()[0]->number() == 5);
|
REQUIRE(tree->array()[1]->array()[0]->number() == 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The hash case is not a removal: json_free() of a hash value leaves the
|
// Not a removal: the key stays with a JSON_NULL stand-in so the surrounding
|
||||||
// key in place with a JSON_NULL stand-in, so that detaching one half of a
|
// pairs keep their alignment, and the entry goes only when both halves do.
|
||||||
// pair cannot disturb the key/value pairing of the entries around it. The
|
|
||||||
// entry is only erased once both halves are gone.
|
|
||||||
TEST_CASE("json_free of a hash value leaves a null placeholder", "[jsonpull][ownership]") {
|
TEST_CASE("json_free of a hash value leaves a null placeholder", "[jsonpull][ownership]") {
|
||||||
json_pull_ptr jp = json_begin_string(R"({"keep": 1, "drop": [2, 3]})");
|
json_pull_ptr jp = json_begin_string(R"({"keep": 1, "drop": [2, 3]})");
|
||||||
json_object_ptr tree = json_read_tree(jp);
|
json_object_ptr tree = json_read_tree(jp);
|
||||||
@@ -379,20 +352,11 @@ TEST_CASE("json_free of a hash value leaves a null placeholder", "[jsonpull][own
|
|||||||
REQUIRE(json_hash_get(tree, "keep")->number() == 1);
|
REQUIRE(json_hash_get(tree, "keep")->number() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The array-splicing fix. The old code removed an element with
|
// The array-splicing fix. The old code passed an element count to memmove
|
||||||
//
|
// where a byte count was wanted, so pruning element 0 of eight left
|
||||||
// memmove(arr + i, arr + i + 1, length - i - 1)
|
// arr[0] == arr[1] -- one node owned twice -- and dropped the last element.
|
||||||
//
|
// Asserting each survivor's identity is what discriminates; checking only the
|
||||||
// passing an element count where memmove wants a byte count. Pruning
|
// size would not.
|
||||||
// element 0 of an eight-element array therefore copied seven bytes over an
|
|
||||||
// eight-byte pointer, which on a little-endian machine whose heap pointers
|
|
||||||
// share a zero high byte left arr[0] == arr[1] -- one node owned twice, so
|
|
||||||
// a double free at teardown -- and dropped the last element, since the
|
|
||||||
// length was decremented without anything having moved into place.
|
|
||||||
//
|
|
||||||
// Asserting the identity of every survivor is what makes this discriminate;
|
|
||||||
// checking only the resulting size would not. Verified to fail against the
|
|
||||||
// pre-fix code.
|
|
||||||
TEST_CASE("json_free prunes a non-final element", "[jsonpull][ownership]") {
|
TEST_CASE("json_free prunes a non-final element", "[jsonpull][ownership]") {
|
||||||
json_pull_ptr jp = json_begin_string("[[1], [2], [3], [4], [5], [6], [7], [8]]");
|
json_pull_ptr jp = json_begin_string("[[1], [2], [3], [4], [5], [6], [7], [8]]");
|
||||||
json_object_ptr tree = json_read_tree(jp);
|
json_object_ptr tree = json_read_tree(jp);
|
||||||
@@ -417,9 +381,7 @@ TEST_CASE("json_free prunes a non-final element", "[jsonpull][ownership]") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// json_free of a hash *key*, the mirror of the value case above. Detaching
|
// The mirror of the value case above.
|
||||||
// one half of a pair leaves a JSON_NULL stand-in so the surrounding pairs
|
|
||||||
// keep their alignment; the entry only disappears once both halves are gone.
|
|
||||||
TEST_CASE("json_free of a hash key, then of both halves", "[jsonpull][ownership]") {
|
TEST_CASE("json_free of a hash key, then of both halves", "[jsonpull][ownership]") {
|
||||||
json_pull_ptr jp = json_begin_string(R"({"a": 1, "b": 2, "c": 3})");
|
json_pull_ptr jp = json_begin_string(R"({"a": 1, "b": 2, "c": 3})");
|
||||||
json_object_ptr tree = json_read_tree(jp);
|
json_object_ptr tree = json_read_tree(jp);
|
||||||
@@ -449,9 +411,8 @@ TEST_CASE("json_free of a hash key, then of both halves", "[jsonpull][ownership]
|
|||||||
REQUIRE(json_hash_get(tree, "c")->number() == 3);
|
REQUIRE(json_hash_get(tree, "c")->number() == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the filter loaders and the -L / -E arguments do: pull several
|
// What the filter loaders and -L / -E do. Each detached tree has to survive
|
||||||
// top-level values off one parser in sequence. Each detached tree has to
|
// the next json_read_tree call and the parser's destruction.
|
||||||
// survive both the next json_read_tree call and the parser's destruction.
|
|
||||||
TEST_CASE("repeated json_read_tree on a line-delimited stream", "[jsonpull][ownership]") {
|
TEST_CASE("repeated json_read_tree on a line-delimited stream", "[jsonpull][ownership]") {
|
||||||
json_pull_ptr jp = json_begin_string("{\"n\": 1}\n{\"n\": 2}\n{\"n\": 3}\n");
|
json_pull_ptr jp = json_begin_string("{\"n\": 1}\n{\"n\": 2}\n{\"n\": 3}\n");
|
||||||
|
|
||||||
@@ -475,9 +436,8 @@ TEST_CASE("repeated json_read_tree on a line-delimited stream", "[jsonpull][owne
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// json_stringify on a partially-parsed tree, which is what json_context()
|
// What json_context() prints on the error paths: a hash whose last key has no
|
||||||
// prints on the error paths in geojson-loop / read_json / plugin. A hash
|
// value yet renders that slot as "...".
|
||||||
// whose last key has no value yet renders that slot as "...".
|
|
||||||
TEST_CASE("json_stringify of a partially-parsed tree", "[jsonpull][stringify]") {
|
TEST_CASE("json_stringify of a partially-parsed tree", "[jsonpull][stringify]") {
|
||||||
json_pull_ptr jp = json_begin_string("{\"a\": [1, 2], \"b\":");
|
json_pull_ptr jp = json_begin_string("{\"a\": [1, 2], \"b\":");
|
||||||
|
|
||||||
@@ -492,9 +452,8 @@ TEST_CASE("json_stringify of a partially-parsed tree", "[jsonpull][stringify]")
|
|||||||
REQUIRE(s == "{\"a\":[1,2],\"b\":...}");
|
REQUIRE(s == "{\"a\":[1,2],\"b\":...}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// An embedded NUL is a legal part of a JSON string once values are
|
// Values are std::string now, so an embedded NUL is legal and stringify has to
|
||||||
// std::string, so stringify has to walk the whole value rather than stop at
|
// walk past it rather than stop the way a c_str() loop would.
|
||||||
// the first NUL the way a c_str() loop would.
|
|
||||||
TEST_CASE("json_stringify keeps text after an embedded NUL", "[jsonpull][stringify]") {
|
TEST_CASE("json_stringify keeps text after an embedded NUL", "[jsonpull][stringify]") {
|
||||||
json_pull_ptr jp = json_begin_string("\"a\\u0000b\"");
|
json_pull_ptr jp = json_begin_string("\"a\\u0000b\"");
|
||||||
json_object_ptr o = json_read_tree(jp);
|
json_object_ptr o = json_read_tree(jp);
|
||||||
@@ -507,11 +466,8 @@ TEST_CASE("json_stringify keeps text after an embedded NUL", "[jsonpull][stringi
|
|||||||
REQUIRE(s == "\"a\\u0000b\"");
|
REQUIRE(s == "\"a\\u0000b\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A \uXXXX escape can only name a code point up to U+FFFF, and U+FFFF
|
// U+FFFF used to fall through the `< 0xFFFF` test into the four-byte branch
|
||||||
// itself used to fall through the `< 0xFFFF` test into the four-byte
|
// and come out as the overlong F0 8F BF BF, which check_utf8 does not catch.
|
||||||
// branch, which emitted the overlong sequence F0 8F BF BF. check_utf8()
|
|
||||||
// only validates continuation-byte structure, so that invalid UTF-8 was
|
|
||||||
// copied into tiles unnoticed.
|
|
||||||
TEST_CASE("jsonpull encodes U+FFFF as three bytes", "[jsonpull][utf8]") {
|
TEST_CASE("jsonpull encodes U+FFFF as three bytes", "[jsonpull][utf8]") {
|
||||||
json_pull_ptr jp = json_begin_string("\"a\\uFFFFb\"");
|
json_pull_ptr jp = json_begin_string("\"a\\uFFFFb\"");
|
||||||
json_object_ptr o = json_read_tree(jp);
|
json_object_ptr o = json_read_tree(jp);
|
||||||
@@ -520,8 +476,12 @@ TEST_CASE("jsonpull encodes U+FFFF as three bytes", "[jsonpull][utf8]") {
|
|||||||
REQUIRE(o != nullptr);
|
REQUIRE(o != nullptr);
|
||||||
REQUIRE(o->type == JSON_STRING);
|
REQUIRE(o->type == JSON_STRING);
|
||||||
|
|
||||||
REQUIRE(o->string() == "a\xEF\xBF\xBF" "b");
|
REQUIRE(o->string() ==
|
||||||
REQUIRE(o->string() != "a\xF0\x8F\xBF\xBF" "b");
|
"a\xEF\xBF\xBF"
|
||||||
|
"b");
|
||||||
|
REQUIRE(o->string() !=
|
||||||
|
"a\xF0\x8F\xBF\xBF"
|
||||||
|
"b");
|
||||||
|
|
||||||
// The boundary below it, and a genuine supplementary code point built
|
// The boundary below it, and a genuine supplementary code point built
|
||||||
// from a surrogate pair, both keep their existing encodings.
|
// from a surrogate pair, both keep their existing encodings.
|
||||||
@@ -544,8 +504,8 @@ TEST_CASE("Polygon cleaning drops a hole that no ring can parent", "[wagyu]") {
|
|||||||
// through the "Could not properly place hole to a parent." handler in
|
// through the "Could not properly place hole to a parent." handler in
|
||||||
// clean_or_clip_poly instead of returning.
|
// clean_or_clip_poly instead of returning.
|
||||||
static const std::vector<std::vector<std::pair<long long, long long>>> rings = {
|
static const std::vector<std::vector<std::pair<long long, long long>>> rings = {
|
||||||
{{0, 5}, {5, 4}, {5, 1}, {4, 4}, {4, 2}, {7, 1}, {0, 5}},
|
{{0, 5}, {5, 4}, {5, 1}, {4, 4}, {4, 2}, {7, 1}, {0, 5}},
|
||||||
{{0, 5}, {7, 1}, {4, 2}, {4, 4}, {5, 1}, {5, 4}, {0, 0}, {0, 5}},
|
{{0, 5}, {7, 1}, {4, 2}, {4, 4}, {5, 1}, {5, 4}, {0, 0}, {0, 5}},
|
||||||
};
|
};
|
||||||
|
|
||||||
drawvec geom;
|
drawvec geom;
|
||||||
|
|||||||
Reference in New Issue
Block a user