Throw instead of exiting from the tile clipping and overzooming code

The code reachable from overzooming a tile no longer calls exit(). It
reports errors by throwing tippecanoe_error, and owns its FILE * and
json_pull * handles through RAII wrappers so that nothing leaks when an
error unwinds the stack.

New files:

* errors.hpp/errors.cpp: the tippecanoe_error exception class, plus
  throw_tippecanoe_error() and throw_perror() helpers.
* raii.hpp: unique_file and unique_json_pull, both std::unique_ptr
  aliases, for FILE * and json_pull *.

exit() becomes throw in clip.cpp, mvt.cpp, mvt.hpp, read_json.cpp,
evaluator.cpp, text.cpp, geometry.cpp, attribute.cpp, and
write_json.hpp. Each tool's main() is now a try/catch around an
inner_main(), so the command-line tools exit with the status they
always did.

Along the way this fixes a z_stream leaked when compress() failed, an
exit() that could run from json_writer's destructor during unwinding,
and an exit() in mvt_value's hash function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuCSVGssWNUCpVEE8Yp5HT
This commit is contained in:
Claude
2026-08-07 18:46:09 +00:00
parent 734bba7c78
commit 85547150c2
19 changed files with 272 additions and 170 deletions
+13 -14
View File
@@ -14,6 +14,7 @@
#include "milo/dtoa_milo.h"
#include "errors.hpp"
#include "serial.hpp"
#include "raii.hpp"
const char *geometry_names[GEOM_TYPES] = {
"Point",
@@ -99,7 +100,7 @@ void parse_coordinates(int t, json_object *j, drawvec &out, int op, const char *
json_context(j);
fprintf(stderr, "%s:%d: malformed point: ", fname, line);
json_context(feature);
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "%s:%d: malformed point", fname, line);
}
}
@@ -135,7 +136,7 @@ serial_val stringify_value(json_object *value, const char *reading, int line, js
if (err.size() > 0) {
fprintf(stderr, "%s:%d: %s: ", reading, line, err.c_str());
json_context(feature);
exit(EXIT_UTF8);
throw_tippecanoe_error(EXIT_UTF8, "%s:%d: %s", reading, line, err.c_str());
}
} else if (vt == JSON_NUMBER) {
sv.type = mvt_double;
@@ -184,20 +185,20 @@ std::pair<int, drawvec> parse_geometry(json_object *geometry, json_pull *jp, jso
if (geometry_type == NULL) {
fprintf(stderr, "Filter output:%d: null geometry (additional not reported): ", jp->line);
json_context(j);
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "Filter output:%d: null geometry (additional not reported)", jp->line);
}
if (geometry_type->type != JSON_STRING) {
fprintf(stderr, "Filter output:%d: geometry type is not a string: ", jp->line);
json_context(j);
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "Filter output:%d: geometry type is not a string", jp->line);
}
json_object *coordinates = json_hash_get(geometry, "coordinates");
if (coordinates == NULL || coordinates->type != JSON_ARRAY) {
fprintf(stderr, "Filter output:%d: geometry without coordinates array: ", jp->line);
json_context(j);
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "Filter output:%d: geometry without coordinates array", jp->line);
}
int t;
@@ -209,7 +210,7 @@ std::pair<int, drawvec> parse_geometry(json_object *geometry, json_pull *jp, jso
if (t >= GEOM_TYPES) {
fprintf(stderr, "Filter output:%d: Can't handle geometry type %s: ", jp->line, geometry_type->value.string.string);
json_context(j);
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "Filter output:%d: Can't handle geometry type %s", jp->line, geometry_type->value.string.string);
}
drawvec dv;
@@ -305,9 +306,9 @@ std::vector<mvt_layer> parse_layers(FILE *fp, int z, unsigned x, unsigned y, int
std::map<std::string, mvt_layer> ret;
std::shared_ptr<std::string> tile_stringpool = std::make_shared<std::string>();
json_pull *jp = json_begin_file(fp);
unique_json_pull jp(json_begin_file(fp));
while (1) {
json_object *j = json_read(jp);
json_object *j = json_read(jp.get());
if (j == NULL) {
if (jp->error != NULL) {
fprintf(stderr, "Filter output:%d: %s: ", jp->line, jp->error);
@@ -316,7 +317,7 @@ std::vector<mvt_layer> parse_layers(FILE *fp, int z, unsigned x, unsigned y, int
} else {
fprintf(stderr, "\n");
}
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "Filter output:%d: %s", jp->line, jp->error);
}
json_free(jp->root);
@@ -336,7 +337,7 @@ std::vector<mvt_layer> parse_layers(FILE *fp, int z, unsigned x, unsigned y, int
fprintf(stderr, "Filter output:%d: feature without properties hash: ", jp->line);
json_context(j);
json_free(j);
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "Filter output:%d: feature without properties hash", jp->line);
}
std::string layername = "unknown";
@@ -364,10 +365,10 @@ std::vector<mvt_layer> parse_layers(FILE *fp, int z, unsigned x, unsigned y, int
fprintf(stderr, "Filter output:%d: filtered feature with no geometry: ", jp->line);
json_context(j);
json_free(j);
exit(EXIT_JSON);
throw_tippecanoe_error(EXIT_JSON, "Filter output:%d: filtered feature with no geometry", jp->line);
}
std::pair<int, drawvec> parsed_geometry = parse_geometry(geometry, jp, j, z, x, y, extent, fix_longitudes, true);
std::pair<int, drawvec> parsed_geometry = parse_geometry(geometry, jp.get(), j, z, x, y, extent, fix_longitudes, true);
int t = parsed_geometry.first;
drawvec &dv = parsed_geometry.second;
@@ -404,8 +405,6 @@ std::vector<mvt_layer> parse_layers(FILE *fp, int z, unsigned x, unsigned y, int
json_free(j);
}
json_end(jp);
std::vector<mvt_layer> final;
for (auto a : ret) {
final.push_back(a.second);