diff --git a/Makefile b/Makefile index 2304db3..9fa0feb 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ YYJSON_LDFLAGS=$(shell pkg-config --libs yyjson) include config.mk -all: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union +all: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union test-json-audit-bugs test-json-crash-vectors YYJSON_TESTS=test-yyjson @@ -43,10 +43,16 @@ test-json-get-array-big-index: tests/json-c/get-array-big-index.c csonpath_json- test-json-union: tests/json-c/union.c csonpath_json-c.h csonpath.h csonpath_do.h $(CC) tests/json-c/union.c $(EXTRA_FILES) $(JSON_C_CFLAGS) $(CFLAGS) -Wno-format -I./ -o test-json-union $(JSON_C_LDFLAGS) $(LDFLAGS) +test-json-audit-bugs: tests/json-c/audit-bugs.c csonpath_json-c.h csonpath.h csonpath_do.h + $(CC) tests/json-c/audit-bugs.c $(EXTRA_FILES) $(JSON_C_CFLAGS) $(CFLAGS) -Wno-format -I./ -o test-json-audit-bugs $(JSON_C_LDFLAGS) $(LDFLAGS) + +test-json-crash-vectors: tests/json-c/crash-vectors.c csonpath_json-c.h csonpath.h csonpath_do.h + $(CC) tests/json-c/crash-vectors.c $(EXTRA_FILES) $(JSON_C_CFLAGS) $(CFLAGS) -Wno-format -I./ -o test-json-crash-vectors $(JSON_C_LDFLAGS) $(LDFLAGS) + test-yyjson: tests/yyjson/test-yyjson.c csonpath_yyjson.h csonpath.h csonpath_do.h $(CC) tests/yyjson/test-yyjson.c $(EXTRA_FILES) $(YYJSON_CFLAGS) $(CFLAGS) -Wno-format -I./ -o test-yyjson $(YYJSON_LDFLAGS) $(LDFLAGS) - tests-c: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union test-yyjson + tests-c: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union test-json-audit-bugs test-json-crash-vectors test-yyjson ./test-json-c-get-a ./test-json-update ./test-json-filter @@ -55,6 +61,8 @@ test-yyjson: tests/yyjson/test-yyjson.c csonpath_yyjson.h csonpath.h csonpath_do ./test-json-filter-and-missing-key ./test-json-get-array-big-index ./test-json-union + ./test-json-audit-bugs + ./test-json-crash-vectors ./test-yyjson pip-dev: @@ -66,5 +74,5 @@ tests-py: pip-dev tests: tests-py tests-c clean: - rm -rvf test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union test-yyjson + rm -rvf test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union test-json-audit-bugs test-json-crash-vectors test-yyjson diff --git a/csonpath.h b/csonpath.h index f7695f6..d2e63c4 100644 --- a/csonpath.h +++ b/csonpath.h @@ -231,6 +231,14 @@ struct csonpath_child_info { #define CSONPATH_ERROR_MAX_SIZE 1024 #define CSONPATH_TMP_BUF_SIZE 256 +#define CSONPATH_FILTER_PUSH(dst, cnt, c, walk_ptr) do { \ + if ((cnt + 1) >= CSONPATH_TMP_BUF_SIZE) { \ + CSONPATH_COMPILE_ERR(tmp, walk_ptr - orig, "%s", "filter key too long"); \ + goto error; \ + } \ + (dst)[(cnt)++] = (c); \ + } while (0) + /* I'm assuming error message won't be longer than 125 */ #define CSONPATH_COMPILE_ERR(tmp, idx, args...) do { \ int ltmp = strlen(tmp), lidx, oidx = idx; \ @@ -455,7 +463,6 @@ static void push_filter_getter(struct csonpath *cjp, int *inst_idx, int nb_gette csonpath_push_char(cjp, filter_getter[i], inst_idx); } *filter_end = (nb_getter_inst - 1); - } static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], @@ -547,7 +554,7 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], } filter_again_root: inst = CSONPATH_INST_GET_OBJ; - filter_getter[nb_getter_inst++] = CSONPATH_INST_GET_OBJ; + CSONPATH_FILTER_PUSH(filter_getter, nb_getter_inst, CSONPATH_INST_GET_OBJ, walker); ++walker; /* skipp blank */ @@ -585,12 +592,12 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], } ++walker; for (next = walker; *next != getter_end; ++next) - filter_getter[nb_getter_inst++] = *next; - filter_getter[nb_getter_inst++] = 0; + CSONPATH_FILTER_PUSH(filter_getter, nb_getter_inst, *next, next); + CSONPATH_FILTER_PUSH(filter_getter, nb_getter_inst, 0, next); } else { for (next = walker; csonpath_is_dot_operand(*next); ++next) - filter_getter[nb_getter_inst++] = *next; - filter_getter[nb_getter_inst++] = 0; + CSONPATH_FILTER_PUSH(filter_getter, nb_getter_inst, *next, next); + CSONPATH_FILTER_PUSH(filter_getter, nb_getter_inst, 0, next); } if (!*next) { CSONPATH_COMPILE_ERR(tmp, next - orig, @@ -606,11 +613,11 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], } if (to_check == '.') { walker = next + 1; - filter_getter[nb_getter_inst++] = CSONPATH_INST_GET_OBJ; + CSONPATH_FILTER_PUSH(filter_getter, nb_getter_inst, CSONPATH_INST_GET_OBJ, walker); goto filter_again; } else if (to_check == '[') { walker = next + 1; - filter_getter[nb_getter_inst++] = CSONPATH_INST_GET_OBJ; + CSONPATH_FILTER_PUSH(filter_getter, nb_getter_inst, CSONPATH_INST_GET_OBJ, walker); getter_end = *walker; goto filter_again; } @@ -896,13 +903,17 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], ++walker; next = walker; csonpath_push_char(cjp, inst, inst_idx); - while (*next != end) { - /* \" should be ignored */ + while (*next && *next != end) { + /* TODO: \" should be ignored */ csonpath_push_char(cjp, *next, inst_idx); ++next; while (*next == '\\') ++next; } + if (!*next) { + CSONPATH_COMPILE_ERR(tmp, walker - orig, + "unclosed string in bracket"); + } ++next; // skipp end csonpath_push_char(cjp, 0, inst_idx); if (in_union && *next == ',') { @@ -1103,7 +1114,7 @@ need_reloop_in = 0; #define CSONPATH_DO_FIND_ALL_OUT if (end_sentinel) *end_sentinel = walker; return CSONPATH_NULL -#define CSONPATH_DO_EXTRA_DECLATION , const char **end_sentinel +#define CSONPATH_DO_EXTRA_DECLARATION , const char **end_sentinel #define CSONPATH_DO_EXTRA_ARGS_IN , NULL #define CSONPATH_DO_EXTRA_ARGS_NEESTED , end_sentinel @@ -1136,7 +1147,7 @@ need_reloop_in = 0; return ret_ar; #define CSONPATH_DO_EXTRA_ARGS_IN , ret_ar -#define CSONPATH_DO_EXTRA_DECLATION , CSONPATH_FIND_ALL_RET ret_ar +#define CSONPATH_DO_EXTRA_DECLARATION , CSONPATH_FIND_ALL_RET ret_ar #include "csonpath_do.h" @@ -1158,10 +1169,12 @@ need_reloop_in = 0; #define CSONPATH_DO_RET_TYPE int #define CSONPATH_DO_RETURN \ - ({if (ctx == in_ctx && need_reloop && \ - CSONPATH_NEED_FOREACH_REDO(ctx)) \ - *need_reloop = 1; \ - CSONPATH_REMOVE_CHILD(ctx, child_info); return 1;}) + ({if (ctx == in_ctx && need_reloop && \ + CSONPATH_NEED_FOREACH_REDO(ctx)) \ + *need_reloop = 1; \ + if (child_info.type == CSONPATH_NONE) \ + return 0; \ + CSONPATH_REMOVE_CHILD(ctx, child_info); return 1;}) #define CSONPATH_PRE_GET_OBJ(val) \ const char *to_del = val; @@ -1200,7 +1213,7 @@ need_reloop_in = 0; nb_res += tret; \ }) -#define CSONPATH_DO_EXTRA_DECLATION , struct csonpath_child_info child_info, int *need_reloop +#define CSONPATH_DO_EXTRA_DECLARATION , struct csonpath_child_info child_info, int *need_reloop #define CSONPATH_DO_EXTRA_ARGS_IN , (struct csonpath_child_info) {.type = CSONPATH_NONE}, NULL @@ -1273,7 +1286,7 @@ need_reloop_in = 0; csonpath_child_info_set(&(struct csonpath_child_info ){}, tmp, (intptr_t)key_idx), &need_reloop_in #define CSONPATH_DO_EXTRA_ARGS , CSONPATH_JSON to_update #define CSONPATH_DO_EXTRA_ARGS_IN , to_update, &(struct csonpath_child_info ){}, NULL -#define CSONPATH_DO_EXTRA_DECLATION CSONPATH_DO_EXTRA_ARGS, struct csonpath_child_info *child_info, int *need_reloop +#define CSONPATH_DO_EXTRA_DECLARATION CSONPATH_DO_EXTRA_ARGS, struct csonpath_child_info *child_info, int *need_reloop #define CSONPATH_DO_FIND_ALL nb_res += tret; #define CSONPATH_DO_FILTER_FIND CSONPATH_GOTO_ON_RELOOP(filter_again) @@ -1381,18 +1394,26 @@ static int csonpath_sync_root_obj(CSONPATH_JSON parent, CSONPATH_JSON to_update) #define CSONPATH_DO_RET_TYPE int #define CSONPATH_DO_FUNC_NAME callback #define CSONPATH_DO_RETURN do { \ - CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata); return 1;} \ - while (0) + if (CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata) < 0) \ + return -1; \ + return 1;} \ + while (0) + +#define CSONPATH_DO_PRE_OPERATION \ + if (callback == NULL) { \ + CSONPATH_GETTER_ERR("callback is NULL\n"); \ + return -1; \ + } #define CSONPATH_DO_EXTRA_ARGS_FIND_ALL , callback, udata, child_info #define CSONPATH_DO_EXTRA_ARGS_NEESTED , callback, udata, \ csonpath_child_info_set(child_info, tmp, (intptr_t)key_idx) #define CSONPATH_DO_EXTRA_ARGS , CSONPATH_CALLBACK callback, CSONPATH_CALLBACK_DATA udata #define CSONPATH_DO_EXTRA_ARGS_IN , callback, udata, &(struct csonpath_child_info ){} -#define CSONPATH_DO_EXTRA_DECLATION CSONPATH_DO_EXTRA_ARGS, struct csonpath_child_info *child_info +#define CSONPATH_DO_EXTRA_DECLARATION CSONPATH_DO_EXTRA_ARGS, struct csonpath_child_info *child_info -#define CSONPATH_DO_FIND_ALL nb_res += tret; -#define CSONPATH_DO_FILTER_FIND nb_res += tret; +#define CSONPATH_DO_FIND_ALL do { if (tret < 0) return tret; nb_res += tret; } while (0) +#define CSONPATH_DO_FILTER_FIND do { if (tret < 0) return tret; nb_res += tret; } while (0) #define CSONPATH_DO_FIND_ALL_OUT return nb_res; @@ -1408,24 +1429,31 @@ static int csonpath_sync_root_obj(CSONPATH_JSON parent, CSONPATH_JSON to_update) #define CSONPATH_DO_DECLARATION \ int nb_res = 0; +#define CSONPATH_DO_PRE_OPERATION \ + if (callback == NULL) { \ + CSONPATH_GETTER_ERR("callback is NULL\n"); \ + return -1; \ + } + #define CSONPATH_DO_RET_TYPE int #define CSONPATH_DO_FUNC_NAME update_or_create_callback #define CSONPATH_DO_RETURN \ - if (tmp == value) { \ - *need_reloop = 1; \ - } \ - CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata); \ - return 1; + if (need_reloop && tmp == value) { \ + *need_reloop = 1; \ + } \ + if (CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata) < 0) \ + return -1; \ + return 1; #define CSONPATH_DO_EXTRA_ARGS_FIND_ALL , callback, udata, NULL, need_reloop -#define CSONPATH_DO_EXTRA_ARGS_NEESTED , callback, udata, \ +#define CSONPATH_DO_EXTRA_ARGS_NEESTED , callback, udata, \ csonpath_child_info_set(&(struct csonpath_child_info ){}, tmp, (intptr_t)key_idx), \ &need_reloop_in #define CSONPATH_DO_EXTRA_ARGS , CSONPATH_CALLBACK callback, CSONPATH_CALLBACK_DATA udata #define CSONPATH_DO_EXTRA_ARGS_IN , callback, udata, &(struct csonpath_child_info ){}, NULL -#define CSONPATH_DO_EXTRA_DECLATION CSONPATH_DO_EXTRA_ARGS, struct csonpath_child_info *child_info, int *need_reloop -#define CSONPATH_DO_FIND_ALL nb_res += tret; -#define CSONPATH_DO_FILTER_FIND CSONPATH_GOTO_ON_RELOOP(filter_again) +#define CSONPATH_DO_EXTRA_DECLARATION CSONPATH_DO_EXTRA_ARGS, struct csonpath_child_info *child_info, int *need_reloop +#define CSONPATH_DO_FIND_ALL do { if (tret < 0) return tret; nb_res += tret; } while (0) +#define CSONPATH_DO_FILTER_FIND do { if (tret < 0) return tret; CSONPATH_GOTO_ON_RELOOP(filter_again); } while (0) #define CSONPATH_DO_FIND_ALL_PRE_LOOP int need_reloop_in = 0; @@ -1434,7 +1462,7 @@ static int csonpath_sync_root_obj(CSONPATH_JSON parent, CSONPATH_JSON to_update) #define CSONPATH_DO_FIND_ALL_OUT return nb_res; #define CSONPATH_PRE_GET_ROOT \ - int to_check = *walker; \ + int to_check = walker[1]; \ if (csonpath_is_endish_inst(to_check)) { \ CSONPATH_GETTER_ERR("can't update root ($)\n"); \ return CSONPATH_NONE_FOUND_RET; \ diff --git a/csonpath_do.h b/csonpath_do.h index 0e904f2..f892be6 100644 --- a/csonpath_do.h +++ b/csonpath_do.h @@ -36,8 +36,8 @@ #define CSONPATH_DO_POST_FIND_ARRAY #endif -#ifndef CSONPATH_DO_EXTRA_DECLATION -#define CSONPATH_DO_EXTRA_DECLATION +#ifndef CSONPATH_DO_EXTRA_DECLARATION +#define CSONPATH_DO_EXTRA_DECLARATION #endif #ifndef CSONPATH_DO_FIND_ALL_CLEAUP @@ -121,7 +121,7 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const CSONPATH_JSON origin, CSONPATH_JSON value, CSONPATH_JSON ctx, - const char *walker CSONPATH_DO_EXTRA_DECLATION); + const char *walker CSONPATH_DO_EXTRA_DECLARATION); #define csonpath_do_dotdot__(name) CATCAT(csonpath_, name, _dotdot) @@ -132,7 +132,7 @@ static CSONPATH_DO_RET_TYPE csonpath_do_dotdot(const struct csonpath cjp[const s CSONPATH_JSON origin, CSONPATH_JSON tmp, CSONPATH_JSON ctx, - const char *walker CSONPATH_DO_EXTRA_DECLATION) + const char *walker CSONPATH_DO_EXTRA_DECLARATION) { CSONPATH_JSON el; CSONPATH_DO_DECLARATION; @@ -181,7 +181,7 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const CSONPATH_JSON origin, CSONPATH_JSON value, CSONPATH_JSON ctx, - const char *walker CSONPATH_DO_EXTRA_DECLATION) + const char *walker CSONPATH_DO_EXTRA_DECLARATION) { CSONPATH_JSON tmp = value; CSONPATH_DO_DECLARATION; @@ -218,7 +218,7 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const CSONPATH_JSON el; int operation_in = *walker, operation = *walker; const char *owalker; - int filter_next_in = *(walker+1), filter_next = *(walker+1); + int filter_next_in = (unsigned char)*(walker+1), filter_next = (unsigned char)*(walker+1); const char *next = walker + 2; int foreach_idx; @@ -504,7 +504,7 @@ static CSONPATH_DO_RET_TYPE csonpath_do_(struct csonpath cjp[static 1], #undef CSONPATH_DO_EXTRA_ARGS_IN #undef CSONPATH_DO_EXTRA_ARGS_FIND_ALL #undef CSONPATH_DO_EXTRA_ARGS -#undef CSONPATH_DO_EXTRA_DECLATION +#undef CSONPATH_DO_EXTRA_DECLARATION #undef CSONPATH_DO_FIND_ALL_PRE_LOOP #undef CSONPATH_DO_FOREACH_PRE_SET #undef CSONPATH_DO_GET_ALL_OUT diff --git a/csonpath_json-c.h b/csonpath_json-c.h index 51f4767..3829be9 100644 --- a/csonpath_json-c.h +++ b/csonpath_json-c.h @@ -23,7 +23,7 @@ typedef void (*json_c_callback)(json_object *, struct csonpath_child_info *, jso #define CSONPATH_CALLBACK_DATA void * #define CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata) \ - callback(ctx, child_info, tmp, udata) + (callback(ctx, child_info, tmp, udata), 0) #define CSONPATH_NEED_FOREACH_REDO(o) \ diff --git a/csonpath_python.c b/csonpath_python.c index 3306003..922d66b 100644 --- a/csonpath_python.c +++ b/csonpath_python.c @@ -68,15 +68,23 @@ #define CSONPATH_CALLBACK_DATA PyObject * -#define CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata) do { \ +#define CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata) \ + ({ \ + int _cb_ok = 0; \ PyObject *arglist; \ + PyObject *_cb_ret; \ if (child_info->type == CSONPATH_STR) \ arglist = Py_BuildValue("(OsOO)", ctx, child_info->key, tmp, udata); \ else \ arglist = Py_BuildValue("(OiOO)", ctx, child_info->idx, tmp, udata); \ - PyObject_CallObject(callback, arglist); \ + _cb_ret = PyObject_CallObject(callback, arglist); \ Py_DECREF(arglist); \ - } while (0) + if (!_cb_ret) \ + _cb_ok = -1; \ + else \ + Py_DECREF(_cb_ret); \ + _cb_ok; \ + }) /* assuming each modification of the object need to go out of the loop */ #define CSONPATH_NEED_FOREACH_REDO(o) 1 @@ -107,6 +115,10 @@ static int pydict_try_setitemstring(PyObject *obj, const char * const at, PyObj Py_XDECREF(str); return -1; } + if (!at) { + PyErr_SetString(PyExc_TypeError, "dict keys must be strings"); + return -1; + } PyDict_SetItemString(obj, at, el); return 1; } @@ -193,6 +205,11 @@ static int python_set_or_insert_item(PyObject *array, Py_ssize_t at, PyObject * CSONPATH_PRAGMA("GCC unroll 8") \ for (intptr_t key_idx = 0; key_idx < array_len_; ++key_idx) { \ el = PyList_GetItem(obj, key_idx); \ + if (!el) { \ + PyErr_SetString(PyExc_RuntimeError, \ + "list was modified during iteration"); \ + break; \ + } \ code \ } \ } @@ -298,6 +315,10 @@ static PyObject *find_first(PyCsonPathObject *self, PyObject* args) static PyObject *print_instructions(PyCsonPathObject *self, PyObject *args, PyObject *kwds) { + if (!self->cp) { + PyErr_SetString(PyExc_RuntimeError, "compiled path is NULL"); + return NULL; + } csonpath_print_instruction(self->cp); Py_RETURN_NONE; } @@ -309,8 +330,9 @@ static PyObject *callback(PyCsonPathObject *self, PyObject* args) if (!PyArg_ParseTuple(args, "OO|O", &json, &callback, &udata)) BAD_ARG(); int ret = csonpath_callback(self->cp, json, callback, udata); - if (PyErr_Occurred()) + if (PyErr_Occurred() || ret < 0) { return NULL; + } return PyLong_FromLong(ret); } @@ -321,7 +343,7 @@ static PyObject *do_remove(PyCsonPathObject *self, PyObject* args) if (!PyArg_ParseTuple(args, "O", &json)) BAD_ARG(); int ret = csonpath_remove(self->cp, json); - if (PyErr_Occurred()) + if (PyErr_Occurred() || ret < 0) return NULL; return PyLong_FromLong(ret); } @@ -365,10 +387,22 @@ static void PyCsonPath_dealloc(PyCsonPathObject *self) { static PyObject *PyCsonPath_set_path(PyCsonPathObject *self, PyObject* args) { const char *new_path; if (!PyArg_ParseTuple(args, "s", &new_path)) - return Py_False; + return NULL; if (!new_path) return Py_False; - self->cp = csonpath_set_path(self->cp, new_path); + struct csonpath *new_cjp = csonpath_new_ex(new_path, CSONPATH_NO_DETROY); + if (!new_cjp) { + PyErr_NoMemory(); + return NULL; + } + if (new_cjp->compile_error) { + PyErr_Format(PyExc_ValueError, "compilation fail %s", + new_cjp->compile_error); + csonpath_destroy(new_cjp); + return NULL; + } + csonpath_destroy(self->cp); + self->cp = new_cjp; return Py_True; } diff --git a/csonpath_yyjson.h b/csonpath_yyjson.h index 4e37643..5daf05f 100644 --- a/csonpath_yyjson.h +++ b/csonpath_yyjson.h @@ -63,7 +63,7 @@ typedef void (*yyjson_val_callback)(yyjson_val *, struct csonpath_child_info *, }) #define CSONPATH_CALL_CALLBACK(callback, ctx, child_info, tmp, udata) \ - callback(ctx, child_info, tmp, udata) + (callback(ctx, child_info, tmp, udata), 0) #define CSONPATH_FOREACH(obj, el, code) \ diff --git a/pyproject.toml b/pyproject.toml index c005f2d..aa329e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "csonpath" -version = "0.15.0" +version = "0.16.0" readme = "README.md" [tool.setuptools] @@ -27,7 +27,7 @@ test-command = "pytest {project}/tests/python" before-build = "choco install mingw -y" [tool.bumpversion] -current_version = "0.15.0" +current_version = "0.16.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/tests/json-c/audit-bugs.c b/tests/json-c/audit-bugs.c new file mode 100644 index 0000000..0923935 --- /dev/null +++ b/tests/json-c/audit-bugs.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include "csonpath_json-c.h" + +/* Reproducer tests for bugs found during audit. + * These tests describe desired behaviour. + * Some currently crash or return incorrect results. + */ + +/* Bug 1: remove("$") segfaults because child_info is uninitialized + * when the path is just ROOT -> END. + */ +void test_remove_root_does_not_crash(void) +{ + struct csonpath *p = csonpath_new("$"); + struct json_object *jobj = json_tokener_parse("{\"a\": 1}"); + int removed; + + assert(p); + assert(jobj); + /* This call must not crash and should remove nothing (root is not deletable). */ + removed = csonpath_remove(p, jobj); + printf("remove($) returned %d (expect 0)\n", removed); + assert(removed == 0); + json_object_put(jobj); + csonpath_destroy(p); +} + +/* Bug 2: remove on deep path should only delete target key. */ +void test_remove_deep_path_on_nested_obj(void) +{ + struct csonpath *p = csonpath_new("$.a.b"); + struct json_object *jobj = json_tokener_parse("{\"a\": {\"b\": 1, \"c\": 2}}"); + int removed; + + assert(p); + assert(jobj); + removed = csonpath_remove(p, jobj); + printf("remove($.a.b) returned %d\n", removed); + assert(removed == 1); + assert(json_object_object_get(json_object_object_get(jobj, "a"), "b") == NULL); + assert(json_object_object_get(json_object_object_get(jobj, "a"), "c") != NULL); + json_object_put(jobj); + csonpath_destroy(p); +} + +int main(void) +{ + printf("=== Audit bug reproducers ===\n"); + + test_remove_root_does_not_crash(); + test_remove_deep_path_on_nested_obj(); + + printf("=== All audit tests passed ===\n"); + return 0; +} diff --git a/tests/json-c/crash-vectors.c b/tests/json-c/crash-vectors.c new file mode 100644 index 0000000..101d5e3 --- /dev/null +++ b/tests/json-c/crash-vectors.c @@ -0,0 +1,473 @@ +#include +#include +#include +#include +#include "csonpath_json-c.h" + +/* Exhaustive crash-vector tests for the C json-c backend. + * Each test must either pass gracefully or report a compile error. + * No SIGSEGV, no ASAN/UBSAN violation. + */ + +static int verbose = 1; + +#define RUN(test_fn) \ + do { \ + if (verbose) { printf(" %s\n", #test_fn); fflush(stdout); } \ + test_fn(); \ + } while (0) + +/* -------------------------------------------------------------------- */ +/* 1. Compile-time rejects / broken paths */ +/* -------------------------------------------------------------------- */ + +static void test_compile_empty_path(void) +{ + struct csonpath *p = csonpath_new(""); + assert(p == NULL); /* empty path is invalid */ +} + +static void test_compile_double_root(void) +{ + struct csonpath *p = csonpath_new("$$"); + assert(p == NULL); +} + +static void test_compile_unclosed_bracket(void) +{ + struct csonpath *p = csonpath_new("$["); + assert(p == NULL); +} + +static void test_compile_unclosed_quote_bracket(void) +{ + struct csonpath *p = csonpath_new("$['a]"); + assert(p == NULL); +} + +static void test_compile_recursive_descent_no_key(void) +{ + struct csonpath *p = csonpath_new("$.."); + assert(p == NULL); +} + +static void test_compile_dot_no_key(void) +{ + struct csonpath *p = csonpath_new("$."); + assert(p == NULL); +} + +static void test_compile_bracket_star_in_find_all(void) +{ + /* [*] is valid as a getter but not as a FIND_ALL path */ + struct csonpath *p = csonpath_new("$..[*]"); + assert(p == NULL); +} + + + +/* -------------------------------------------------------------------- */ +/* 2. NULL value tolerance */ +/* -------------------------------------------------------------------- */ + +static void test_null_value_find_first(void) +{ + struct csonpath *p = csonpath_new("$"); + assert(p); + CSONPATH_JSON ret = csonpath_find_first(p, NULL); + /* returns NULL, no crash */ + (void)ret; + csonpath_destroy(p); +} + +static void test_null_value_find_all(void) +{ + struct csonpath *p = csonpath_new("$"); + assert(p); + CSONPATH_JSON ret = csonpath_find_all(p, NULL); + /* returns [NULL] -- weird but no crash */ + if (ret) json_object_put(ret); + csonpath_destroy(p); +} + +static void test_null_value_remove(void) +{ + struct csonpath *p = csonpath_new("$.a"); + assert(p); + int ret = csonpath_remove(p, NULL); + assert(ret == 0); + csonpath_destroy(p); +} + +static void test_null_value_update_or_create(void) +{ + struct csonpath *p = csonpath_new("$.a"); + struct json_object *jobj = json_tokener_parse("{\"a\":1}"); + assert(p); + int ret = csonpath_update_or_create(p, NULL, NULL); + /* returns -1 with error, no crash */ + (void)ret; + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 3. Index edge cases */ +/* -------------------------------------------------------------------- */ + +static void test_array_negative_index(void) +{ + /* json-c backend: does negative index wrap around or underflow? */ + struct csonpath *p = csonpath_new("$.a[-1]"); + if (!p) return; /* rejected at compile time is fine */ + struct json_object *jobj = json_tokener_parse("{\"a\":[1,2,3]}"); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + (void)ret; /* just don't crash */ + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_array_huge_index(void) +{ + struct csonpath *p = csonpath_new("$.a[999999999]"); + if (!p) return; + struct json_object *jobj = json_tokener_parse("{\"a\":[1,2,3]}"); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + assert(ret == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_array_index_on_scalar(void) +{ + struct csonpath *p = csonpath_new("$.a[0]"); + assert(p); + struct json_object *jobj = json_tokener_parse("{\"a\":\"hello\"}"); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + assert(ret == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_array_index_on_null(void) +{ + struct csonpath *p = csonpath_new("$.a[0]"); + assert(p); + struct json_object *jobj = json_tokener_parse("{\"a\":null}"); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + assert(ret == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 4. Deep / recursive descent on scalars */ +/* -------------------------------------------------------------------- */ + +static void test_recursive_descent_on_scalar(void) +{ + struct csonpath *p = csonpath_new("$..a"); + assert(p); + struct json_object *jobj = json_tokener_parse("\"hello\""); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + assert(ret == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_recursive_descent_on_empty_obj(void) +{ + struct csonpath *p = csonpath_new("$..a"); + assert(p); + struct json_object *jobj = json_tokener_parse("{}"); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + assert(ret == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 5. Union edge cases */ +/* -------------------------------------------------------------------- */ + +static void test_empty_union(void) +{ + /* empty union bracket - should be rejected */ + struct csonpath *p = csonpath_new("$[]"); + assert(p == NULL); +} + +static void test_union_mixed_types(void) +{ + struct csonpath *p = csonpath_new("$['a',0]"); + if (!p) return; /* reject is fine */ + struct json_object *jobj = json_tokener_parse("{\"a\":1,\"0\":2}"); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + (void)ret; + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 6. Object getter on non-object */ +/* -------------------------------------------------------------------- */ + +static void test_get_obj_on_array(void) +{ + struct csonpath *p = csonpath_new("$.a"); + assert(p); + struct json_object *jobj = json_tokener_parse("[1,2,3]"); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + assert(ret == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_get_obj_on_scalar(void) +{ + struct csonpath *p = csonpath_new("$.a"); + assert(p); + struct json_object *jobj = json_tokener_parse("\"hello\""); + CSONPATH_JSON ret = csonpath_find_first(p, jobj); + assert(ret == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 7. Remove / update on array */ +/* -------------------------------------------------------------------- */ + +static void test_remove_array_element(void) +{ + struct csonpath *p = csonpath_new("$.a[1]"); + assert(p); + struct json_object *jobj = json_tokener_parse("{\"a\":[1,2,3]}"); + int ret = csonpath_remove(p, jobj); + (void)ret; /* just don't crash */ + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_remove_array_all(void) +{ + struct csonpath *p = csonpath_new("$.a[*]"); + assert(p); + struct json_object *jobj = json_tokener_parse("{\"a\":[1,2,3]}"); + int ret = csonpath_remove(p, jobj); + assert(ret == 3); /* one deletion per element */ + /* json-c backend replaces array elements with NULL rather than + * shifting the array, so length stays 3 but values are NULL. */ + struct json_object *arr = json_object_object_get(jobj, "a"); + assert(json_object_array_length(arr) == 3); + for (size_t i = 0; i < 3; ++i) + assert(json_object_array_get_idx(arr, i) == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_remove_array_out_of_bounds(void) +{ + struct csonpath *p = csonpath_new("$.a[5]"); + assert(p); + struct json_object *jobj = json_tokener_parse("{\"a\":[1,2,3]}"); + int ret = csonpath_remove(p, jobj); + assert(ret < 1); /* nothing deleted */ + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_update_array_element(void) +{ + struct csonpath *p = csonpath_new("$.a[1]"); + assert(p); + struct json_object *jobj = json_tokener_parse("{\"a\":[1,2,3]}"); + struct json_object *val = json_object_new_int(42); + int ret = csonpath_update_or_create(p, jobj, val); + assert(ret == 1); + assert(json_object_get_int( + json_object_array_get_idx( + json_object_object_get(jobj, "a"), 1)) == 42); + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_update_array_out_of_bounds_gaps(void) +{ + struct csonpath *p = csonpath_new("$.a[5]"); + assert(p); + struct json_object *jobj = json_tokener_parse("{\"a\":[1,2,3]}"); + struct json_object *val = json_object_new_int(42); + int ret = csonpath_update_or_create(p, jobj, val); + assert(ret == 1); + struct json_object *arr = json_object_object_get(jobj, "a"); + assert(json_object_array_length(arr) == 6); + assert(json_object_array_get_idx(arr, 3) == NULL); + assert(json_object_array_get_idx(arr, 4) == NULL); + assert(json_object_get_int(json_object_array_get_idx(arr, 5)) == 42); + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 8. Mutation during callback iteration */ +/* -------------------------------------------------------------------- */ + +static void json_c_callback_delete_next(struct json_object *ctx, + struct csonpath_child_info *child_info, + struct json_object *val, void *udata) +{ + (void)child_info; (void)val; (void)udata; + /* delete the key 'c' regardless of which key we are on */ + json_object_object_del(ctx, "c"); +} + +static void test_callback_mutation_next_key(void) +{ + struct csonpath *p = csonpath_new("$.a.*"); + struct json_object *jobj = json_tokener_parse( + "{\"a\":{\"b\":1,\"c\":2,\"d\":3}}"); + int ret; + + assert(p); + assert(jobj); + /* deleting an entry that has NOT been visited yet must not crash. + * Number of callbacks is not deterministic because deleting an entry + * may corrupt the iterator's cached next pointer (UAF). */ + ret = csonpath_callback(p, jobj, json_c_callback_delete_next, NULL); + (void)ret; + assert(json_object_object_get( + json_object_object_get(jobj, "a"), "c") == NULL); + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 9. Deep nesting / stack stress */ +/* -------------------------------------------------------------------- */ + +static struct json_object *make_nested(int depth) +{ + if (depth <= 0) + return json_object_new_int(1); + struct json_object *o = json_object_new_object(); + json_object_object_add(o, "a", make_nested(depth - 1)); + return o; +} + +static void test_deep_recursive_descent(void) +{ + /* 200 levels deep — recursive descent must not stack-overflow */ + struct json_object *jobj = make_nested(200); + struct csonpath *p = csonpath_new("$..a"); + CSONPATH_JSON ret; + + assert(p); + /* find_first returns the first match (top-level nested object) */ + ret = csonpath_find_first(p, jobj); + assert(ret != NULL); + assert(json_object_is_type(ret, json_type_object)); + /* find_all should collect every nested level (200 items) */ + ret = csonpath_find_all(p, jobj); + assert(ret != NULL); + assert(json_object_array_length(ret) == 200); + json_object_put(ret); + csonpath_destroy(p); + json_object_put(jobj); +} + +static void test_wide_array_get_all(void) +{ + /* array with 64k elements — GET_ALL must return every element */ + struct json_object *arr = json_object_new_array(); + for (int i = 0; i < 65536; ++i) + json_object_array_add(arr, json_object_new_int(i)); + struct json_object *jobj = json_object_new_object(); + json_object_object_add(jobj, "a", arr); + struct csonpath *p = csonpath_new("$.a[*]"); + CSONPATH_JSON ret; + + assert(p); + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_array_length(ret) == 65536); + json_object_put(ret); + csonpath_destroy(p); + json_object_put(jobj); +} + +/* -------------------------------------------------------------------- */ +/* 10. csonpath_destroy / set_path double-free style */ +/* -------------------------------------------------------------------- */ + +static void test_set_path_invalid_then_valid(void) +{ + struct csonpath *p = csonpath_new("$.a"); + assert(p); + /* set_path with invalid string destroys old p and returns NULL */ + struct csonpath *q = csonpath_set_path(p, "$$"); + assert(q == NULL); + /* p is already freed by set_path; don't touch it again */ +} + +/* -------------------------------------------------------------------- */ +/* Main */ +/* -------------------------------------------------------------------- */ + +int main(void) +{ + printf("=== Crash-vector C tests ===\n"); fflush(stdout); + + printf("\n-- Compile-time rejects --\n"); fflush(stdout); + RUN(test_compile_empty_path); + RUN(test_compile_double_root); + RUN(test_compile_unclosed_bracket); + RUN(test_compile_unclosed_quote_bracket); + RUN(test_compile_recursive_descent_no_key); + RUN(test_compile_dot_no_key); + RUN(test_compile_bracket_star_in_find_all); + + printf("\n-- NULL value tolerance --\n"); fflush(stdout); + RUN(test_null_value_find_first); + RUN(test_null_value_find_all); + RUN(test_null_value_remove); + RUN(test_null_value_update_or_create); + + printf("\n-- Index edge cases --\n"); fflush(stdout); + RUN(test_array_negative_index); + RUN(test_array_huge_index); + RUN(test_array_index_on_scalar); + RUN(test_array_index_on_null); + + printf("\n-- Recursive descent on scalars --\n"); fflush(stdout); + RUN(test_recursive_descent_on_scalar); + RUN(test_recursive_descent_on_empty_obj); + + printf("\n-- Union edge cases --\n"); fflush(stdout); + RUN(test_empty_union); + RUN(test_union_mixed_types); + + printf("\n-- Getter on non-object --\n"); fflush(stdout); + RUN(test_get_obj_on_array); + RUN(test_get_obj_on_scalar); + + printf("\n-- Array remove / update --\n"); fflush(stdout); + RUN(test_remove_array_element); + RUN(test_remove_array_all); + RUN(test_remove_array_out_of_bounds); + RUN(test_update_array_element); + RUN(test_update_array_out_of_bounds_gaps); + + printf("\n-- Mutation during callback iteration --\n"); fflush(stdout); + RUN(test_callback_mutation_next_key); + + printf("\n-- Deep nesting / stack stress --\n"); fflush(stdout); + RUN(test_deep_recursive_descent); + RUN(test_wide_array_get_all); + + printf("\n-- set_path --\n"); fflush(stdout); + RUN(test_set_path_invalid_then_valid); + + printf("\n=== All crash-vector tests passed ===\n"); fflush(stdout); + return 0; +} diff --git a/tests/json-c/filter.c b/tests/json-c/filter.c index c4d70db..9e5d119 100644 --- a/tests/json-c/filter.c +++ b/tests/json-c/filter.c @@ -1,4 +1,6 @@ #include +#include +#include #include #include "csonpath_json-c.h" @@ -254,5 +256,63 @@ int main(void) json_object_put(ret); json_object_put(jobj); + + /* Test filter with a medium key (200 chars): must match. + * If char is signed and csonpath_do.h lacks an unsigned char cast, + * the runtime filter_next becomes negative and silently skips the key. */ + { +#define KEY_LEN 200 + char key[KEY_LEN + 1]; + memset(key, 'k', KEY_LEN); + key[KEY_LEN] = '\0'; + + struct json_object *obj = json_object_new_object(); + struct json_object *arr = json_object_new_array(); + struct json_object *el = json_object_new_object(); + json_object_object_add(el, key, json_object_new_string("y")); + json_object_array_add(arr, el); + json_object_object_add(obj, "arr", arr); + + char path[KEY_LEN + 50]; + snprintf(path, sizeof(path), "$.arr[?['%s']=\"y\"]", key); + p = csonpath_new(path); + assert(p); + ret = csonpath_find_all(p, obj); + assert(ret && json_object_is_type(ret, json_type_array) + && json_object_array_length(ret) == 1); + json_object_put(ret); + json_object_put(obj); + csonpath_destroy(p); +#undef KEY_LEN + } + + /* Test filter with a large key (>255 chars) to overflow a char length storage */ + { + const int key_len = 300; + char *big_key = malloc(key_len + 1); + memset(big_key, 'k', key_len); + big_key[key_len] = '\0'; + + struct json_object *big_obj = json_object_new_object(); + struct json_object *big_arr = json_object_new_array(); + struct json_object *el1 = json_object_new_object(); + struct json_object *el2 = json_object_new_object(); + json_object_object_add(el1, big_key, json_object_new_string("match")); + json_object_object_add(el2, big_key, json_object_new_string("no")); + json_object_array_add(big_arr, el1); + json_object_array_add(big_arr, el2); + json_object_object_add(big_obj, "arr", big_arr); + + char *path = malloc(key_len + 50); + sprintf(path, "$.arr[?['%s'] = \"match\"]", big_key); + p = csonpath_new(path); + assert(p == NULL); /* clé trop longue : doit refuser de compiler */ + (void)ret; + + free(path); + json_object_put(big_obj); + free(big_key); + } + csonpath_destroy(p); } diff --git a/tests/json-c/get-a.c b/tests/json-c/get-a.c index d04572e..6cbb143 100644 --- a/tests/json-c/get-a.c +++ b/tests/json-c/get-a.c @@ -1,4 +1,6 @@ #include +#include +#include #include #include "csonpath_json-c.h" @@ -145,6 +147,28 @@ int main(void) assert(iret == 1); assert(!json_object_object_get(jobj, "array")); + /* Test with a large key (>255 chars) to overflow a char length storage */ + { + const int key_len = 300; + char *big_key = malloc(key_len + 1); + memset(big_key, 'k', key_len); + big_key[key_len] = '\0'; + + struct json_object *big_obj = json_object_new_object(); + json_object_object_add(big_obj, big_key, json_object_new_string("big_value")); + + char *path = malloc(key_len + 10); + sprintf(path, "$['%s']", big_key); + TRY(p = csonpath_set_path(p, path)); + ret = csonpath_find_first(p, big_obj); + assert(ret); + assert(!strcmp(json_object_get_string(ret), "big_value")); + + free(path); + json_object_put(big_obj); + free(big_key); + } + json_object_put(jobj); csonpath_destroy(p); } diff --git a/tests/python/test_segfault.py b/tests/python/test_segfault.py new file mode 100644 index 0000000..967e93e --- /dev/null +++ b/tests/python/test_segfault.py @@ -0,0 +1,188 @@ +""" +TDD regression tests for known segfaults. + +These tests describe the *desired* behaviour. Right now many of them crash +(SIGSEGV) so they will kill the pytest runner if executed in-process. +Run individual tests in a subprocess while debugging, e.g.: + + python -c "import tests.python.test_segfault as t; t.test_set_path_bad_then_find()" + +Once the underlying C bugs are fixed every test below should pass without +any change to this file. +""" + +import pytest +import csonpath + + +# --------------------------------------------------------------------------- +# update_or_create with non-string keys in the replacement dict +# --------------------------------------------------------------------------- + +def test_update_or_create_int_key_raises_type_error(): + """Only string keys are valid in a JSON/dict update value.""" + cp = csonpath.CsonPath('$') + with pytest.raises(TypeError): + cp.update_or_create({'a': 1}, {1: 'a'}) + + +def test_update_or_create_tuple_key_raises_type_error(): + cp = csonpath.CsonPath('$') + with pytest.raises(TypeError): + cp.update_or_create({'a': 1}, {(1, 2): 'a'}) + + +def test_update_or_create_none_key_raises_type_error(): + cp = csonpath.CsonPath('$') + with pytest.raises(TypeError): + cp.update_or_create({'a': 1}, {None: 'a'}) + + +# --------------------------------------------------------------------------- +# set_path with an invalid path must raise and leave the object usable +# --------------------------------------------------------------------------- + +def test_set_path_bad_then_find_still_works(): + """A failed set_path must not corrupt the compiled path object.""" + cp = csonpath.CsonPath('$.a') + with pytest.raises(ValueError): + cp.set_path('bad[') + # original path must still be functional + assert cp.find_first({'a': 42}) == 42 + + +def test_multiple_set_path_bad_keeps_original(): + cp = csonpath.CsonPath('$.a') + for bad in ('bad1[', 'bad2[', 'bad3['): + with pytest.raises(ValueError): + cp.set_path(bad) + assert cp.find_first({'a': 42}) == 42 + + +def test_find_all_after_failed_set_path(): + cp = csonpath.CsonPath('$.a') + with pytest.raises(ValueError): + cp.set_path('bad[') + assert cp.find_all({'a': 1}) == [1] + + +def test_remove_after_failed_set_path(): + cp = csonpath.CsonPath('$.a') + with pytest.raises(ValueError): + cp.set_path('bad[') + d = {'a': 1} + cp.remove(d) + assert 'a' not in d + + +def test_callback_after_failed_set_path(): + cp = csonpath.CsonPath('$.a') + with pytest.raises(ValueError): + cp.set_path('bad[') + called = [] + cp.callback({'a': 1}, lambda ctx, k, v, ud: called.append((k, v))) + assert called == [('a', 1)] + + +def test_update_or_create_after_failed_set_path(): + cp = csonpath.CsonPath('$.a') + with pytest.raises(ValueError): + cp.set_path('bad[') + cp.update_or_create({'a': 1}, 2) + assert cp.find_first({'a': 2}) == 2 + + +# --------------------------------------------------------------------------- +# print_instructions must be safe even after a failed set_path +# --------------------------------------------------------------------------- + +def test_print_instructions_after_failed_set_path(): + cp = csonpath.CsonPath('$.a') + with pytest.raises(ValueError): + cp.set_path('bad[') + # must not segfault + cp.print_instructions() + + +def test_print_instructions_twice_after_failed_set_path(): + cp = csonpath.CsonPath('$.a') + with pytest.raises(ValueError): + cp.set_path('bad[') + cp.print_instructions() + cp.print_instructions() + + +# --------------------------------------------------------------------------- +# Callbacks must not segfault when the user mutates the container +# --------------------------------------------------------------------------- + +def test_callback_clearing_list_during_iteration(): + """Mutating a list while CsonPath iterates over it must raise RuntimeError.""" + l = [1, 2, 3] + cp = csonpath.CsonPath('$[*]') + + def cb(ctx, idx, val, ud): + if isinstance(ctx, list): + ctx.clear() + + with pytest.raises(RuntimeError): + cp.callback(l, cb) + + +def test_callback_popping_list_during_iteration(): + l = [1, 2, 3] + cp = csonpath.CsonPath('$[*]') + + def cb(ctx, idx, val, ud): + if isinstance(ctx, list) and len(ctx) > 0: + ctx.pop(0) + + with pytest.raises(RuntimeError): + cp.callback(l, cb) + + +# --------------------------------------------------------------------------- +# update_or_create_callback on root must raise, not crash +# --------------------------------------------------------------------------- + +def test_update_or_create_callback_on_root(): + """Root ($) cannot be updated via callback; it must raise ValueError.""" + cp = csonpath.CsonPath('$') + with pytest.raises(ValueError, match="can't update root"): + cp.update_or_create_callback({'a': 1}, lambda *a: None) + + +def test_callback_exception_with_multiple_matches(): + """A callback that raises on the first match must stop iteration.""" + cp = csonpath.CsonPath('$.a[*]') + d = {'a': [1, 2]} + + def cb(ctx, idx, val, ud): + raise ValueError('boom') + + with pytest.raises(ValueError, match='boom'): + cp.callback(d, cb) + + +def test_callback_exception_recursive_descent(): + """Same via recursive descent with multiple matches.""" + cp = csonpath.CsonPath('$..b') + d = {'x': {'b': 1}, 'y': {'b': 2}} + + def cb(ctx, k, val, ud): + raise ValueError('boom') + + with pytest.raises(ValueError, match='boom'): + cp.callback(d, cb) + + +def test_update_or_create_callback_exception_with_multiple_matches(): + """update_or_create_callback with multiple matches raising.""" + cp = csonpath.CsonPath('$.a[*]') + d = {'a': [1, 2]} + + def cb(ctx, idx, val, ud): + raise ValueError('boom') + + with pytest.raises(ValueError, match='boom'): + cp.update_or_create_callback(d, cb)