From 85547150c2c4346c6b5b12b1c8bd01997af08bd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 18:35:03 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01DuCSVGssWNUCpVEE8Yp5HT --- CHANGELOG.md | 8 ++++++ Makefile | 14 ++++----- attribute.cpp | 27 +++++++----------- clip.cpp | 36 ++++++++--------------- decode.cpp | 14 ++++++++- errors.cpp | 31 ++++++++++++++++++++ errors.hpp | 36 +++++++++++++++++++++++ evaluator.cpp | 77 ++++++++++++++++++-------------------------------- geometry.cpp | 3 +- jsontool.cpp | 16 +++++++++-- main.cpp | 14 ++++++++- mvt.cpp | 30 ++++++++------------ mvt.hpp | 3 +- overzoom.cpp | 46 +++++++++++++++++------------- raii.hpp | 28 ++++++++++++++++++ read_json.cpp | 27 +++++++++--------- text.cpp | 15 ++++------ tile-join.cpp | 14 ++++++++- write_json.hpp | 3 +- 19 files changed, 272 insertions(+), 170 deletions(-) create mode 100644 errors.cpp create mode 100644 raii.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f5ec49..a84d5789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 2.81.0 +* Report errors from the tile clipping and overzooming code by throwing a + `tippecanoe_error` rather than by calling `exit()`, and give that code + RAII ownership of its `FILE *` and `json_pull *` handles so that nothing + leaks when an error unwinds the stack. Each command-line tool catches the + exception in `main()` and exits with the same status it would have + before. Along the way this fixes a leaked `z_stream` when `compress()` + failed and two `exit()` calls that could run from a destructor or from + `mvt_value`'s hash function. * Add `--drop-by-attribute-as-needed=`*attribute* to drop the features with the lowest values of a numeric attribute from oversized tiles, and `--drop-by-attribute-order=desc` to drop the highest values instead. diff --git a/Makefile b/Makefile index 84ad090c..85f30688 100644 --- a/Makefile +++ b/Makefile @@ -95,25 +95,25 @@ C = $(wildcard *.c) $(wildcard *.cpp) INCLUDES = -I/usr/local/include -I. -Iclipper2/include LIBS = -L/usr/local/lib -tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o serial.o main.o platform.o text.o dirtiles.o pmtiles_file.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o compression.o clip.o sort.o attribute.o thread.o shared_borders.o usage.o clipper2/src/clipper.engine.o +tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o serial.o main.o platform.o text.o dirtiles.o pmtiles_file.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o compression.o clip.o sort.o attribute.o thread.o shared_borders.o usage.o clipper2/src/clipper.engine.o errors.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread tippecanoe-enumerate: enumerate.o usage.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lsqlite3 -tippecanoe-decode: decode.o projection.o mvt.o write_json.o text.o jsonpull/jsonpull.o dirtiles.o pmtiles_file.o usage.o +tippecanoe-decode: decode.o projection.o mvt.o write_json.o text.o jsonpull/jsonpull.o dirtiles.o pmtiles_file.o usage.o errors.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -tile-join: tile-join.o platform.o projection.o mbtiles.o mvt.o memfile.o dirtiles.o jsonpull/jsonpull.o text.o evaluator.o csv.o write_json.o pmtiles_file.o clip.o attribute.o thread.o read_json.o usage.o clipper2/src/clipper.engine.o +tile-join: tile-join.o platform.o projection.o mbtiles.o mvt.o memfile.o dirtiles.o jsonpull/jsonpull.o text.o evaluator.o csv.o write_json.o pmtiles_file.o clip.o attribute.o thread.o read_json.o usage.o clipper2/src/clipper.engine.o errors.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -tippecanoe-json-tool: jsontool.o jsonpull/jsonpull.o csv.o text.o geojson-loop.o usage.o +tippecanoe-json-tool: jsontool.o jsonpull/jsonpull.o csv.o text.o geojson-loop.o usage.o errors.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -unit: unit.o text.o sort.o mvt.o projection.o clip.o attribute.o jsonpull/jsonpull.o evaluator.o read_json.o clipper2/src/clipper.engine.o +unit: unit.o text.o sort.o mvt.o projection.o clip.o attribute.o jsonpull/jsonpull.o evaluator.o read_json.o clipper2/src/clipper.engine.o errors.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -tippecanoe-overzoom: overzoom.o mvt.o clip.o evaluator.o jsonpull/jsonpull.o text.o attribute.o read_json.o projection.o read_json.o usage.o clipper2/src/clipper.engine.o +tippecanoe-overzoom: overzoom.o mvt.o clip.o evaluator.o jsonpull/jsonpull.o text.o attribute.o read_json.o projection.o read_json.o usage.o clipper2/src/clipper.engine.o errors.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -include $(wildcard *.d) @@ -128,7 +128,7 @@ clean: rm -f ./tippecanoe ./tippecanoe-* ./tile-join ./unit *.o *.d */*.o */*.d tests/**/*.mbtiles tests/**/*.check indent: - clang-format -i -style="{BasedOnStyle: Google, IndentWidth: 8, UseTab: Always, AllowShortIfStatementsOnASingleLine: false, ColumnLimit: 0, ContinuationIndentWidth: 8, SpaceAfterCStyleCast: true, IndentCaseLabels: false, AllowShortBlocksOnASingleLine: false, AllowShortFunctionsOnASingleLine: false, SortIncludes: false}" $(filter-out flatgeobuf.cpp,$(C)) $(H) jsonpull/*.[ch] + clang-format -i -style="{BasedOnStyle: Google, IndentWidth: 8, UseTab: Always, AllowShortIfStatementsOnASingleLine: false, ColumnLimit: 0, ContinuationIndentWidth: 8, SpaceAfterCStyleCast: true, IndentCaseLabels: false, AllowShortBlocksOnASingleLine: false, AllowShortFunctionsOnASingleLine: false, SortIncludes: false, PointerAlignment: Right, DerivePointerAlignment: false}" $(filter-out flatgeobuf.cpp,$(C)) $(H) jsonpull/*.[ch] TESTS = $(wildcard tests/*/out/*.json) SPACE = $(NULL) $(NULL) diff --git a/attribute.cpp b/attribute.cpp index 53c9dae5..ced67bb2 100644 --- a/attribute.cpp +++ b/attribute.cpp @@ -5,6 +5,7 @@ #include "serial.hpp" #include "jsonpull/jsonpull.h" #include "milo/dtoa_milo.h" +#include "raii.hpp" std::map numeric_operations = { {"sum", op_sum}, @@ -33,8 +34,7 @@ void set_attribute_accum(std::unordered_map &attribut } else if (type == "count") { t = op_count; } else { - fprintf(stderr, "Attribute method (%s) must be sum, product, mean, max, min, concat, comma, or count\n", type.c_str()); - exit(EXIT_ARGS); + throw_tippecanoe_error(EXIT_ARGS, "Attribute method (%s) must be sum, product, mean, max, min, concat, comma, or count", type.c_str()); } attribute_accum.insert(std::pair(name, t)); @@ -42,17 +42,15 @@ void set_attribute_accum(std::unordered_map &attribut void set_attribute_accum(std::unordered_map &attribute_accum, const char *arg, char **argv) { if (*arg == '{') { - json_pull *jp = json_begin_string(arg); - json_object *o = json_read_tree(jp); + unique_json_pull jp(json_begin_string(arg)); + json_object *o = json_read_tree(jp.get()); if (o == NULL) { - fprintf(stderr, "%s: -E%s: %s\n", *argv, arg, jp->error); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "%s: -E%s: %s", *argv, arg, jp->error); } if (o->type != JSON_HASH) { - fprintf(stderr, "%s: -E%s: not a JSON object\n", *argv, arg); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "%s: -E%s: not a JSON object", *argv, arg); } for (size_t i = 0; i < o->value.object.length; i++) { @@ -60,26 +58,22 @@ void set_attribute_accum(std::unordered_map &attribut json_object *v = o->value.object.values[i]; if (k->type != JSON_STRING) { - fprintf(stderr, "%s: -E%s: key %zu not a string\n", *argv, arg, i); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "%s: -E%s: key %zu not a string", *argv, arg, i); } if (v->type != JSON_STRING) { - fprintf(stderr, "%s: -E%s: value %zu not a string\n", *argv, arg, i); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "%s: -E%s: value %zu not a string", *argv, arg, i); } set_attribute_accum(attribute_accum, k->value.string.string, v->value.string.string); } json_free(o); - json_end(jp); return; } const char *s = strchr(arg, ':'); if (s == NULL) { - fprintf(stderr, "-E%s option must be in the form -Ename:method\n", arg); - exit(EXIT_ARGS); + throw_tippecanoe_error(EXIT_ARGS, "-E%s option must be in the form -Ename:method", arg); } std::string name = std::string(arg, s - arg); @@ -171,8 +165,7 @@ static void preserve_attribute1(attribute_op const &op, std::string const &key, break; default: - fprintf(stderr, "can't happen: operation that isn't used by --accumulate-numeric-attributes\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "can't happen: operation that isn't used by --accumulate-numeric-attributes"); } full_keys.push_back(key_pool.pool(key)); diff --git a/clip.cpp b/clip.cpp index 72a1fb3a..b34998b7 100644 --- a/clip.cpp +++ b/clip.cpp @@ -46,8 +46,7 @@ drawvec simple_clip_poly(drawvec &geom, long long minx, long long miny, long lon tmp = clip_poly1(tmp, minx, miny, maxx, maxy, ax, ay, bx, by, edge_nodes, prevent_simplify_shared_nodes); if (tmp.size() > 0) { if (tmp[0].first != tmp[tmp.size() - 1].first || tmp[0].second != tmp[tmp.size() - 1].second) { - fprintf(stderr, "Internal error: Polygon ring not closed\n"); - exit(EXIT_FAILURE); + throw_tippecanoe_error(EXIT_FAILURE, "Internal error: Polygon ring not closed"); } } for (size_t k = 0; k < tmp.size(); k++) { @@ -60,8 +59,7 @@ drawvec simple_clip_poly(drawvec &geom, long long minx, long long miny, long lon i = j - 1; } else { - fprintf(stderr, "Unexpected operation in polygon %d\n", (int) geom[i].op); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Unexpected operation in polygon %d", (int) geom[i].op); } } @@ -246,8 +244,7 @@ static void decode_clipped(mapbox::geometry::multi_polygon &t, drawve double area = get_area(ring, 0, ring.size()); if ((j == 0 && area < 0) || (j != 0 && area > 0)) { - fprintf(stderr, "Ring area has wrong sign: %f for %zu\n", area, j); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Ring area has wrong sign: %f for %zu", area, j); } for (size_t k = 0; k < ring.size(); k++) { @@ -362,8 +359,7 @@ drawvec clean_or_clip_poly(drawvec &geom, int z, int buffer, bool clip, bool try fprintf(f, "\n\n\n\n\n"); fclose(f); - fprintf(stderr, "Internal error: Polygon cleaning failed. Log in /tmp/wagyu.log\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: Polygon cleaning failed. Log in /tmp/wagyu.log"); } if (scale != 1) { @@ -439,8 +435,7 @@ drawvec clip_poly_poly(drawvec const &geom, drawvec const &bounds) { result.clear(); wagyu.execute(mapbox::geometry::wagyu::clip_type_intersection, result, mapbox::geometry::wagyu::fill_type_positive, mapbox::geometry::wagyu::fill_type_positive); } catch (std::runtime_error &e) { - fprintf(stderr, "Internal error: Polygon clipping failed\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: Polygon clipping failed"); } } @@ -635,8 +630,7 @@ double get_area_scaled(const drawvec &geom, size_t i, size_t j) { } } - fprintf(stderr, "get_area_scaled: can't happen\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "get_area_scaled: can't happen"); } double get_area(const drawvec &geom, size_t i, size_t j) { @@ -754,8 +748,7 @@ static bool inside(std::pair d, int edge, long long minx, long l return d.first > minx; } - fprintf(stderr, "internal error inside\n"); - exit(EXIT_FAILURE); + throw_tippecanoe_error(EXIT_FAILURE, "internal error inside"); } static std::pair intersect(std::pair a, std::pair b, int edge, long long minx, long long miny, long long maxx, long long maxy) { @@ -773,8 +766,7 @@ static std::pair intersect(std::pair a, std::pai return std::pair(minx, (a.second + (double) (b.second - a.second) * (minx - a.first) / (b.first - a.first))); } - fprintf(stderr, "internal error intersecting\n"); - exit(EXIT_FAILURE); + throw_tippecanoe_error(EXIT_FAILURE, "internal error intersecting"); } // http://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm @@ -910,8 +902,7 @@ void douglas_peucker(drawvec &geom, int start, int n, double e, size_t kept, siz std::stack recursion_stack; if (!geom[start + 0].necessary || !geom[start + n - 1].necessary) { - fprintf(stderr, "endpoints not marked necessary\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "endpoints not marked necessary"); } int prev = 0; @@ -1237,12 +1228,10 @@ std::string overzoom(std::vector const &tiles, int nz, int nx, int n try { bool was_compressed; if (!tile.decode(t.tile, was_compressed)) { - fprintf(stderr, "Couldn't parse tile %d/%u/%u\n", t.z, t.x, t.y); - exit(EXIT_MVT); + throw_tippecanoe_error(EXIT_MVT, "Couldn't parse tile %d/%u/%u", t.z, t.x, t.y); } } catch (std::exception const &e) { - fprintf(stderr, "PBF decoding error in tile %d/%u/%u\n", t.z, t.x, t.y); - exit(EXIT_PROTOBUF); + throw_tippecanoe_error(EXIT_PROTOBUF, "PBF decoding error in tile %d/%u/%u", t.z, t.x, t.y); } source_tile out; @@ -1892,8 +1881,7 @@ drawvec fix_polygon(const drawvec &geom, bool use_winding, bool reverse_winding) i = j - 1; outer = 0; } else { - fprintf(stderr, "Internal error: polygon ring begins with %d, not moveto\n", geom[i].op); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: polygon ring begins with %d, not moveto", geom[i].op); } } diff --git a/decode.cpp b/decode.cpp index 6bee8876..6211a885 100644 --- a/decode.cpp +++ b/decode.cpp @@ -589,7 +589,7 @@ void usage(char **argv) { exit(EXIT_ARGS); } -int main(int argc, char **argv) { +int inner_main(int argc, char **argv) { extern int optind; extern char *optarg; int i; @@ -666,3 +666,15 @@ int main(int argc, char **argv) { return 0; } + +int main(int argc, char **argv) { + try { + return inner_main(argc, argv); + } catch (tippecanoe_error &e) { + fprintf(stderr, "%s\n", e.what()); + return e.exit_code; + } catch (std::exception &e) { + fprintf(stderr, "Error: %s\n", e.what()); + return EXIT_FAILURE; + } +} diff --git a/errors.cpp b/errors.cpp new file mode 100644 index 00000000..c5964258 --- /dev/null +++ b/errors.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include "errors.hpp" + +[[noreturn]] void throw_tippecanoe_error(int code, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + + char *msg = NULL; + if (vasprintf(&msg, fmt, ap) < 0) { + va_end(ap); + throw tippecanoe_error(code, "error (could not format message)"); + } + va_end(ap); + + std::string s(msg); + free(msg); + + // Remove trailing newline if present (stderr messages often have one) + while (s.size() > 0 && s.back() == '\n') { + s.pop_back(); + } + + throw tippecanoe_error(code, s); +} + +[[noreturn]] void throw_perror(int code, const char *msg) { + throw tippecanoe_error(code, std::string(msg) + ": " + strerror(errno)); +} diff --git a/errors.hpp b/errors.hpp index 904c4ce2..4907abae 100644 --- a/errors.hpp +++ b/errors.hpp @@ -1,3 +1,6 @@ +#ifndef ERRORS_HPP +#define ERRORS_HPP + #define EXIT_INCOMPLETE 100 #define EXIT_ARGS 101 #define EXIT_CLOSE 102 @@ -21,3 +24,36 @@ #define EXIT_WRITE 120 // avoid 124, 125, 126, 127, 137, which are used by GNU timeout + +#include +#include +#include + +class tippecanoe_error : public std::runtime_error { + public: + int exit_code; + tippecanoe_error(int code, const std::string &message) + : std::runtime_error(message), exit_code(code) { + } +}; + +// Format and throw a tippecanoe_error. Use in place of fprintf(stderr,...)+exit(). +[[noreturn]] void throw_tippecanoe_error(int code, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +// Throw from errno-based errors (replaces perror()+exit() pattern) +[[noreturn]] void throw_perror(int code, const char *msg); + +// Check the void* return value from pthread_join. If a thread caught an +// exception, the return value is a heap-allocated std::exception_ptr. +// This re-throws it on the joining thread, or does nothing if retval is NULL. +inline void rethrow_if_thread_failed(void *retval) { + if (retval != NULL) { + std::exception_ptr *ep = (std::exception_ptr *) retval; + std::exception_ptr copy = *ep; + delete ep; + std::rethrow_exception(copy); + } +} + +#endif diff --git a/evaluator.cpp b/evaluator.cpp index 38566ae2..0dd6c474 100644 --- a/evaluator.cpp +++ b/evaluator.cpp @@ -6,6 +6,7 @@ #include "mvt.hpp" #include "evaluator.hpp" #include "errors.hpp" +#include "raii.hpp" #include "milo/dtoa_milo.h" #include "text.hpp" @@ -48,8 +49,7 @@ int compare(mvt_value const &one, json_object *two, bool &fail) { break; case mvt_no_such_key: default: - fprintf(stderr, "Internal error: bad mvt type %d\n", one.type); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: bad mvt type %d", one.type); } if (v < two->value.number.number) { @@ -84,8 +84,7 @@ int compare(mvt_value const &one, json_object *two, bool &fail) { break; } - fprintf(stderr, "Internal error: bad mvt type %d\n", one.type); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: bad mvt type %d", one.type); } // 0: false @@ -119,39 +118,33 @@ static int eval(std::function feature, json_obje } if (f == NULL || f->type != JSON_ARRAY) { - fprintf(stderr, "Filter is not an array: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Filter is not an array: %s", json_stringify(f)); } if (f->value.array.length < 1) { - fprintf(stderr, "Array too small in filter: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Array too small in filter: %s", json_stringify(f)); } if (f->value.array.array[0]->type != JSON_STRING) { - fprintf(stderr, "Filter operation is not a string: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Filter operation is not a string: %s", json_stringify(f)); } if (strcmp(f->value.array.array[0]->value.string.string, "has") == 0 || strcmp(f->value.array.array[0]->value.string.string, "!has") == 0) { if (f->value.array.length != 2) { - fprintf(stderr, "Wrong number of array elements in filter: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Wrong number of array elements in filter: %s", json_stringify(f)); } if (strcmp(f->value.array.array[0]->value.string.string, "has") == 0) { if (f->value.array.array[1]->type != JSON_STRING) { - fprintf(stderr, "\"has\" key is not a string: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "\"has\" key is not a string: %s", json_stringify(f)); } return feature(std::string(f->value.array.array[1]->value.string.string)).type != mvt_no_such_key; } if (strcmp(f->value.array.array[0]->value.string.string, "!has") == 0) { if (f->value.array.array[1]->type != JSON_STRING) { - fprintf(stderr, "\"!has\" key is not a string: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "\"!has\" key is not a string: %s", json_stringify(f)); } return feature(std::string(f->value.array.array[1]->value.string.string)).type == mvt_no_such_key; } @@ -164,12 +157,10 @@ static int eval(std::function feature, json_obje strcmp(f->value.array.array[0]->value.string.string, "<") == 0 || strcmp(f->value.array.array[0]->value.string.string, "<=") == 0) { if (f->value.array.length != 3) { - fprintf(stderr, "Wrong number of array elements in filter: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Wrong number of array elements in filter: %s", json_stringify(f)); } if (f->value.array.array[1]->type != JSON_STRING) { - fprintf(stderr, "comparison key is not a string: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "comparison key is not a string: %s", json_stringify(f)); } mvt_value ff = feature(std::string(f->value.array.array[1]->value.string.string)); @@ -223,8 +214,7 @@ static int eval(std::function feature, json_obje return cmp <= 0; } - fprintf(stderr, "Internal error: can't happen: %s\n", json_stringify(f)); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: can't happen: %s", json_stringify(f)); } if (strcmp(f->value.array.array[0]->value.string.string, "all") == 0 || @@ -266,13 +256,11 @@ static int eval(std::function feature, json_obje if (strcmp(f->value.array.array[0]->value.string.string, "in") == 0 || strcmp(f->value.array.array[0]->value.string.string, "!in") == 0) { if (f->value.array.length < 2) { - fprintf(stderr, "Array too small in filter: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Array too small in filter: %s", json_stringify(f)); } if (f->value.array.array[1]->type != JSON_STRING) { - fprintf(stderr, "\"!in\" key is not a string: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "\"!in\" key is not a string: %s", json_stringify(f)); } mvt_value ff = feature(std::string(f->value.array.array[1]->value.string.string)); @@ -321,13 +309,11 @@ static int eval(std::function feature, json_obje if (strcmp(f->value.array.array[0]->value.string.string, "attribute-filter") == 0) { if (f->value.array.length != 3) { - fprintf(stderr, "Wrong number of array elements in filter: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Wrong number of array elements in filter: %s", json_stringify(f)); } if (f->value.array.array[1]->type != JSON_STRING) { - fprintf(stderr, "\"attribute-filter\" key is not a string: %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "\"attribute-filter\" key is not a string: %s", json_stringify(f)); } bool ok = eval(feature, f->value.array.array[2], exclude_attributes, unidecode_data) > 0; @@ -338,14 +324,12 @@ static int eval(std::function feature, json_obje return true; } - fprintf(stderr, "Unknown filter %s\n", json_stringify(f)); - exit(EXIT_FILTER); + throw_tippecanoe_error(EXIT_FILTER, "Unknown filter %s", json_stringify(f)); } bool evaluate(std::function feature, std::string const &layer, json_object *filter, std::set &exclude_attributes, std::vector const &unidecode_data) { if (filter == NULL || filter->type != JSON_HASH) { - fprintf(stderr, "Error: filter is not a hash: %s\n", json_stringify(filter)); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "Error: filter is not a hash: %s", json_stringify(filter)); } bool ok = true; @@ -365,34 +349,27 @@ bool evaluate(std::function feature, std::string } json_object *read_filter(const char *fname) { - FILE *fp = fopen(fname, "r"); - if (fp == NULL) { - perror(fname); - exit(EXIT_OPEN); + unique_file fp(fopen(fname, "r")); + if (fp.get() == NULL) { + throw_perror(EXIT_OPEN, fname); } - json_pull *jp = json_begin_file(fp); - json_object *filter = json_read_tree(jp); + unique_json_pull jp(json_begin_file(fp.get())); + json_object *filter = json_read_tree(jp.get()); if (filter == NULL) { - fprintf(stderr, "%s: %s\n", fname, jp->error); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "%s: %s", fname, jp.get()->error); } json_disconnect(filter); - json_end(jp); - fclose(fp); return filter; } json_object *parse_filter(const char *s) { - json_pull *jp = json_begin_string(s); - json_object *filter = json_read_tree(jp); + unique_json_pull jp(json_begin_string(s)); + json_object *filter = json_read_tree(jp.get()); if (filter == NULL) { - fprintf(stderr, "Could not parse filter %s\n", s); - fprintf(stderr, "%s\n", jp->error); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "Could not parse filter %s\n%s", s, jp.get()->error); } json_disconnect(filter); - json_end(jp); return filter; } diff --git a/geometry.cpp b/geometry.cpp index 58d4f163..29e821e8 100644 --- a/geometry.cpp +++ b/geometry.cpp @@ -508,8 +508,7 @@ drawvec stairstep(drawvec &geom, int z, int detail) { // out.push_back(draw(VT_LINETO, xx, yy)); } else { - fprintf(stderr, "Can't happen: stairstepping lineto with no moveto\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Can't happen: stairstepping lineto with no moveto"); } } diff --git a/jsontool.cpp b/jsontool.cpp index 5c4e0abe..0d421e75 100644 --- a/jsontool.cpp +++ b/jsontool.cpp @@ -365,7 +365,7 @@ void join_csv(json_object *j) { vo->value.number.large_unsigned = 0; vo->value.number.large_signed = 0; } else { - abort(); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "unexpected JSON attribute type %d", attr_type); } properties->value.object.keys[properties->value.object.length] = ko; @@ -437,7 +437,7 @@ void usage(char **argv) { exit(EXIT_ARGS); } -int main(int argc, char **argv) { +int inner_main(int argc, char **argv) { const char *csv = NULL; strip_usage_headings(long_options, real_long_options); @@ -520,3 +520,15 @@ int main(int argc, char **argv) { return fail; } + +int main(int argc, char **argv) { + try { + return inner_main(argc, argv); + } catch (tippecanoe_error &e) { + fprintf(stderr, "%s\n", e.what()); + return e.exit_code; + } catch (std::exception &e) { + fprintf(stderr, "Error: %s\n", e.what()); + return EXIT_FAILURE; + } +} diff --git a/main.cpp b/main.cpp index e084e6e1..1a41a371 100644 --- a/main.cpp +++ b/main.cpp @@ -3165,7 +3165,7 @@ void usage(char **argv, int status) { exit(status); } -int main(int argc, char **argv) { +int inner_main(int argc, char **argv) { #ifdef MTRACE mtrace(); #endif @@ -3870,6 +3870,18 @@ int main(int argc, char **argv) { return ret; } +int main(int argc, char **argv) { + try { + return inner_main(argc, argv); + } catch (tippecanoe_error &e) { + fprintf(stderr, "%s\n", e.what()); + return e.exit_code; + } catch (std::exception &e) { + fprintf(stderr, "Error: %s\n", e.what()); + return EXIT_FAILURE; + } +} + int mkstemp_cloexec(char *name) { int fd = mkstemp(name); if (fd >= 0) { diff --git a/mvt.cpp b/mvt.cpp index 18f239d2..de2f6d39 100644 --- a/mvt.cpp +++ b/mvt.cpp @@ -102,6 +102,7 @@ int compress(std::string const &input, std::string &output, bool gz) { deflate_s.next_out = (Bytef *) (output.data() + length); int ret = deflate(&deflate_s, Z_FINISH); if (ret != Z_STREAM_END && ret != Z_OK && ret != Z_BUF_ERROR) { + deflateEnd(&deflate_s); return -1; } length += (increase - deflate_s.avail_out); @@ -118,7 +119,7 @@ bool mvt_tile::decode(const std::string &message, bool &was_compressed) { if (is_compressed(message)) { std::string uncompressed; if (decompress(message, uncompressed) == 0) { - exit(EXIT_MVT); + throw_tippecanoe_error(EXIT_MVT, "Tile decompression failed"); } src = uncompressed; was_compressed = true; @@ -368,11 +369,9 @@ std::string mvt_tile::encode() { value_writer.add_bool(7, pbv.numeric_value.bool_value); break; case mvt_null: - fprintf(stderr, "Internal error: trying to write null attribute to tile\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: trying to write null attribute to tile"); default: - fprintf(stderr, "Internal error: trying to write undefined attribute type to tile\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: trying to write undefined attribute type to tile"); } std::shared_ptr sv = std::make_shared(); @@ -451,8 +450,7 @@ std::string mvt_tile::encode() { long long dy = wwy - py; if (dx < INT_MIN || dx > INT_MAX || dy < INT_MIN || dy > INT_MAX) { - fprintf(stderr, "Internal error: Geometry delta is too big: %lld,%lld\n", dx, dy); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: Geometry delta is too big: %lld,%lld", dx, dy); } geometry.push_back(protozero::encode_zigzag32(dx)); @@ -464,8 +462,7 @@ std::string mvt_tile::encode() { } else if (op == mvt_closepath) { length++; } else { - fprintf(stderr, "\nInternal error: corrupted geometry\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "Internal error: corrupted geometry"); } } @@ -514,8 +511,7 @@ bool mvt_value::operator<(const mvt_value &o) const { return numeric_value.null_value < o.numeric_value.null_value; default: - fprintf(stderr, "mvt_value::operator<<: can't happen\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "mvt_value::operator<<: can't happen"); } } @@ -550,8 +546,7 @@ bool mvt_value::operator==(const mvt_value &o) const { return numeric_value.null_value == o.numeric_value.null_value; default: - fprintf(stderr, "mvt_value::operator==: can't happen\n"); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "mvt_value::operator==: can't happen"); } } @@ -813,8 +808,7 @@ serial_val mvt_value_to_serial_val(mvt_value const &v) { sv.s = "null"; break; default: - fprintf(stderr, "unhandled mvt_type %d\n", v.type); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "unhandled mvt_type %d", v.type); } return sv; @@ -840,8 +834,7 @@ long long mvt_value_to_long_long(mvt_value const &v) { case mvt_null: return 0; default: - fprintf(stderr, "unhandled mvt_type %d\n", v.type); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "unhandled mvt_type %d", v.type); } } @@ -865,8 +858,7 @@ double mvt_value_to_double(mvt_value const &v) { case mvt_null: return 0; default: - fprintf(stderr, "unhandled mvt_type %d\n", v.type); - exit(EXIT_IMPOSSIBLE); + throw_tippecanoe_error(EXIT_IMPOSSIBLE, "unhandled mvt_type %d", v.type); } } diff --git a/mvt.hpp b/mvt.hpp index 0c36b74f..9fc532b9 100644 --- a/mvt.hpp +++ b/mvt.hpp @@ -204,8 +204,7 @@ struct std::hash { return fnv1a(sizeof(int), (void *) &k.numeric_value.null_value); default: - fprintf(stderr, "mvt_value hash can't happen\n"); - exit(EXIT_IMPOSSIBLE); + throw tippecanoe_error(EXIT_IMPOSSIBLE, "mvt_value hash: unexpected type"); } } }; diff --git a/overzoom.cpp b/overzoom.cpp index 5fa4a53f..f1b19cc1 100644 --- a/overzoom.cpp +++ b/overzoom.cpp @@ -12,6 +12,7 @@ #include "read_json.hpp" #include "projection.hpp" #include "usage.hpp" +#include "raii.hpp" extern char *optarg; extern int optind; @@ -91,24 +92,21 @@ void usage(char **argv) { std::string read_json_file(const char *fname) { std::string out; - FILE *f = fopen(fname, "r"); - if (f == NULL) { - perror(optarg); - exit(EXIT_OPEN); + unique_file f(fopen(fname, "r")); + if (!f) { + throw_perror(EXIT_OPEN, fname); } char buf[2000]; size_t nread; - while ((nread = fread(buf, sizeof(char), 2000, f)) != 0) { + while ((nread = fread(buf, sizeof(char), 2000, f.get())) != 0) { out += std::string(buf, nread); } - fclose(f); - return out; } -int main(int argc, char **argv) { +int inner_main(int argc, char **argv) { int i; const char *outtile = NULL; const char *outfile = NULL; @@ -273,16 +271,14 @@ int main(int argc, char **argv) { char buf[1000]; int len; - FILE *f = fopen(s.tile.c_str(), "rb"); - if (f == NULL) { - perror(s.tile.c_str()); - exit(EXIT_FAILURE); + unique_file f(fopen(s.tile.c_str(), "rb")); + if (!f) { + throw_perror(EXIT_OPEN, s.tile.c_str()); } - while ((len = fread(buf, sizeof(char), 1000, f)) > 0) { + while ((len = fread(buf, sizeof(char), 1000, f.get())) > 0) { tile.append(std::string(buf, len)); } - fclose(f); input_tile t = s; t.tile = std::move(tile); @@ -292,14 +288,24 @@ int main(int argc, char **argv) { out = overzoom(its, nz, nx, ny, detail, buffer, keep, exclude, exclude_prefix, do_compress, NULL, demultiply, json_filter, preserve_input_order, attribute_accum, unidecode_data, simplification, tiny_polygon_size, std::vector(), "", "", SIZE_MAX, std::vector(), deduplicate_by_id); } - FILE *f = fopen(outfile, "wb"); - if (f == NULL) { - perror(outfile); - exit(EXIT_FAILURE); + unique_file f(fopen(outfile, "wb")); + if (!f) { + throw_perror(EXIT_OPEN, outfile); } - fwrite(out.c_str(), sizeof(char), out.size(), f); - fclose(f); + fwrite(out.c_str(), sizeof(char), out.size(), f.get()); return 0; } + +int main(int argc, char **argv) { + try { + return inner_main(argc, argv); + } catch (tippecanoe_error &e) { + fprintf(stderr, "%s\n", e.what()); + return e.exit_code; + } catch (std::exception &e) { + fprintf(stderr, "Error: %s\n", e.what()); + return EXIT_FAILURE; + } +} diff --git a/raii.hpp b/raii.hpp new file mode 100644 index 00000000..116af3f3 --- /dev/null +++ b/raii.hpp @@ -0,0 +1,28 @@ +#ifndef RAII_HPP +#define RAII_HPP + +#include +#include +#include "jsonpull/jsonpull.h" + +// FILE* → unique_file +struct file_closer { + void operator()(FILE *f) { + if (f) { + fclose(f); + } + } +}; +using unique_file = std::unique_ptr; + +// json_pull* → unique_json_pull +struct json_pull_closer { + void operator()(json_pull *jp) { + if (jp) { + json_end(jp); + } + } +}; +using unique_json_pull = std::unique_ptr; + +#endif diff --git a/read_json.cpp b/read_json.cpp index 63329acb..1cac247e 100644 --- a/read_json.cpp +++ b/read_json.cpp @@ -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 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 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 parse_layers(FILE *fp, int z, unsigned x, unsigned y, int std::map ret; std::shared_ptr tile_stringpool = std::make_shared(); - 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 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 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 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 parsed_geometry = parse_geometry(geometry, jp, j, z, x, y, extent, fix_longitudes, true); + std::pair 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 parse_layers(FILE *fp, int z, unsigned x, unsigned y, int json_free(j); } - json_end(jp); - std::vector final; for (auto a : ret) { final.push_back(a.second); diff --git a/text.cpp b/text.cpp index 629000d8..3b60a80a 100644 --- a/text.cpp +++ b/text.cpp @@ -8,6 +8,7 @@ #include "milo/dtoa_milo.h" #include "milo/milo.h" #include "errors.hpp" +#include "raii.hpp" /** * Returns an empty string if `s` is valid utf8; @@ -135,7 +136,7 @@ int integer_zoom(std::string where, std::string text) { double d = atof(text.c_str()); if (!std::isfinite(d) || d != floor(d) || d < 0 || d > 32) { fprintf(stderr, "%s: Expected integer zoom level in \"tippecanoe\" GeoJSON extension, not %s\n", where.c_str(), text.c_str()); - exit(EXIT_JSON); + throw_tippecanoe_error(EXIT_JSON, "%s: Expected integer zoom level in \"tippecanoe\" GeoJSON extension, not %s", where.c_str(), text.c_str()); } return d; } @@ -181,8 +182,7 @@ char *dtoa_milo(double val) { std::string s = milo::dtoa_milo(val); char *dup = strdup(s.c_str()); if (dup == NULL) { - perror("strdup"); - exit(EXIT_MEMORY); + throw_perror(EXIT_MEMORY, "strdup"); } return dup; } @@ -191,25 +191,22 @@ char *dtoa_milo(double val) { std::vector read_unidecode(const char *fname) { std::string data; - FILE *f = fopen(fname, "rb"); + unique_file f(fopen(fname, "rb")); if (f == NULL) { - perror(fname); - exit(EXIT_OPEN); + throw_perror(EXIT_OPEN, fname); } std::string buf; buf.resize(2000); while (true) { - size_t nread = fread((void *) buf.c_str(), sizeof(char), buf.size(), f); + size_t nread = fread((void *) buf.c_str(), sizeof(char), buf.size(), f.get()); if (nread == 0) { break; } data.append(buf.c_str(), nread); } - fclose(f); - std::vector out; out.emplace_back(); // because the data file is 1-indexed out.emplace_back(); // ascii 001 diff --git a/tile-join.cpp b/tile-join.cpp index 017e04b8..647150ab 100644 --- a/tile-join.cpp +++ b/tile-join.cpp @@ -1332,7 +1332,7 @@ void usage(char **argv) { exit(EXIT_ARGS); } -int main(int argc, char **argv) { +int inner_main(int argc, char **argv) { char *out_mbtiles = NULL; char *out_dir = NULL; sqlite3 *outdb = NULL; @@ -1683,3 +1683,15 @@ int main(int argc, char **argv) { return 0; } + +int main(int argc, char **argv) { + try { + return inner_main(argc, argv); + } catch (tippecanoe_error &e) { + fprintf(stderr, "%s\n", e.what()); + return e.exit_code; + } catch (std::exception &e) { + fprintf(stderr, "Error: %s\n", e.what()); + return EXIT_FAILURE; + } +} diff --git a/write_json.hpp b/write_json.hpp index a2ae56bc..410c357e 100644 --- a/write_json.hpp +++ b/write_json.hpp @@ -25,8 +25,9 @@ struct json_writer { ~json_writer() { if (state.size() > 0) { if (state.size() != 1 || state[0] != JSON_WRITE_TOP) { + // Log but don't throw — throwing from a destructor + // during stack unwinding calls std::terminate(). fprintf(stderr, "JSON not closed at end\n"); - exit(EXIT_FAILURE); } } }