Skip to content

Commit

Permalink
jmap_util: guard array patch with PATCH_ALLOW_ARRAY
Browse files Browse the repository at this point in the history
And reject invalid array indexes.

Signed-off-by: Robert Stepanek <[email protected]>
  • Loading branch information
rsto committed Jul 26, 2023
1 parent d793325 commit 89d49a9
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 24 deletions.
113 changes: 112 additions & 1 deletion cunit/jmap_util.testc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ static void test_patchobject(void)
char *swant = json_dumps(jwant, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
char *sdiff = json_dumps(jdiff, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
CU_ASSERT_STRING_EQUAL(swant, sdiff); \
json_t *jback = jmap_patchobject_apply(jfrom, jdiff, NULL); \
json_t *jback = jmap_patchobject_apply(jfrom, jdiff, NULL, myflags); \
char *sback = json_dumps(jback, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
char *sdest = json_dumps(jdest, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
CU_ASSERT_STRING_EQUAL(sdest, sback); \
Expand Down Expand Up @@ -182,6 +182,117 @@ static void test_patchobject(void)
#undef TESTCASE
}

static void test_patchobject_invalid(void)
{
#define TESTCASE(_from, _patch, _want_invalid, _flags) \
{ \
unsigned myflags = (_flags); \
json_t *jfrom = json_loads((_from), JSON_DECODE_ANY, NULL); \
json_t *jpatch = json_loads((_patch), JSON_DECODE_ANY, NULL); \
json_t *jwant_invalid = json_loads((_want_invalid), JSON_DECODE_ANY, NULL); \
json_t *jhave_invalid = json_array(); \
json_t *jhave = jmap_patchobject_apply(jfrom, jpatch, jhave_invalid, myflags); \
CU_ASSERT_PTR_NULL(jhave); \
char *want_invalid = json_dumps(jwant_invalid, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
char *have_invalid = json_dumps(jhave_invalid, JSON_SORT_KEYS|JSON_ENCODE_ANY); \
CU_ASSERT_STRING_EQUAL(want_invalid, have_invalid); \
free(have_invalid); \
free(want_invalid); \
json_decref(jhave_invalid); \
json_decref(jwant_invalid); \
json_decref(jpatch); \
json_decref(jfrom); \
}

const char *from, *patch, *want_invalid;

/* Set non-existent member */
from = "{"
" \"a\": \"foo\""
"}";
patch = "{"
" \"x/y\": \"bar\""
"}";
want_invalid = "["
"\"x/y\""
"]";
TESTCASE(from, patch, want_invalid, 0);

/* Remove non-existent member */
from = "{"
" \"a\": \"foo\""
"}";
patch = "{"
" \"x/y\": null"
"}";
want_invalid = "["
"\"x/y\""
"]";
TESTCASE(from, patch, want_invalid, 0);

/* Patch inside array - but no PATCH_ALLOW_ARRAY flag */
from = "{"
" \"a\": ["
" \"foo\","
" \"bar\""
" ]"
"}";
patch = "{"
" \"a/1\": \"bam\""
"}";
want_invalid = "["
" \"a/1\" "
"]";
TESTCASE(from, patch, want_invalid, 0);

/* Delete from array */
from = "{"
" \"a\": ["
" \"foo\","
" \"bar\""
" ]"
"}";
patch = "{"
" \"a/1\": null"
"}";
want_invalid = "["
" \"a/1\" "
"]";
TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY);

/* Patch non-existent array entry */
from = "{"
" \"a\": ["
" \"foo\","
" \"bar\""
" ]"
"}";
patch = "{"
" \"a/2\": \"bam\""
"}";
want_invalid = "["
" \"a/2\" "
"]";
TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY);

/* Patch with special JSON pointer '-' */
from = "{"
" \"a\": ["
" \"foo\","
" \"bar\""
" ]"
"}";
patch = "{"
" \"a/-\": \"bam\""
"}";
want_invalid = "["
" \"a/-\" "
"]";
TESTCASE(from, patch, want_invalid, PATCH_ALLOW_ARRAY);

#undef TESTCASE
}

static void test_decode_to_utf8(void)
{
struct testcase {
Expand Down
65 changes: 46 additions & 19 deletions imap/jmap_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ EXPORTED char *jmap_pointer_decode(const char *src, size_t len)
return buf_release(&buf);
}

EXPORTED json_t* jmap_patchobject_apply(json_t *val, json_t *patch, json_t *invalid)
EXPORTED json_t* jmap_patchobject_apply(json_t *val,
json_t *patch,
json_t *invalid,
unsigned flags)
{
const char *path;
json_t *newval, *dst;
Expand All @@ -173,33 +176,57 @@ EXPORTED json_t* jmap_patchobject_apply(json_t *val, json_t *patch, json_t *inva
json_t *it = dst;
const char *base = path, *top;
/* Find path in object tree */
while ((top = strchr(base, '/'))) {
char *name = jmap_pointer_decode(base, top-base);
if (json_is_array(it)) {
it = json_array_get(it, atoi(name));
while (it && (top = strchr(base, '/'))) {
char *ref = jmap_pointer_decode(base, top-base);
if (json_is_array(it) && (flags & PATCH_ALLOW_ARRAY)) {
const char *err = NULL;
bit64 idx;
if (!parsenum(ref, &err, 0, &idx) && !*err &&
idx < json_array_size(it)) {
it = json_array_get(it, idx);
}
else it = NULL;
}
else {
it = json_object_get(it, name);
it = json_object_get(it, ref);
}
free(name);
free(ref);
base = top + 1;
}
if (!it) {
/* No such path in 'val' */
if (invalid) {
json_array_append_new(invalid, json_string(path));

/* Set value at path */
int is_valid = 0;

if (it) {
char *ref = jmap_pointer_decode(base, strlen(base));

if (json_is_object(it)) {
is_valid = 1;
if (newval == json_null()) {
json_object_del(it, ref);
} else {
json_object_set(it, ref, newval);
}
}
else if (json_is_array(it) && !json_is_null(newval) &&
(flags & PATCH_ALLOW_ARRAY)) {
const char *err = NULL;
bit64 idx;
if (!parsenum(ref, &err, 0, &idx) && !*err &&
idx < json_array_size(it)) {
is_valid = 1;
json_array_set(it, idx, newval);
}
}

free(ref);
}

if (!is_valid) {
if (invalid) json_array_append_new(invalid, json_string(path));
json_decref(dst);
return NULL;
}
/* Replace value at path */
char *name = jmap_pointer_decode(base, strlen(base));
if (newval == json_null()) {
json_object_del(it, name);
} else {
json_object_set(it, name, newval);
}
free(name);
}

return dst;
Expand Down
9 changes: 5 additions & 4 deletions imap/jmap_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,14 @@ extern int jmap_readprop_full(json_t *root, const char *prefix, const char *name
int mandatory, json_t *invalid, const char *fmt,
void *dst);

#define PATCH_NO_REMOVE (1<<0) // only relevant for create
#define PATCH_ALLOW_ARRAY (1<<1)

/* Apply patch to a deep copy of val and return the result.
* Return NULL on error. If invalid is a JSON array, then
* the erroneous path in patch is appended as JSON string */
extern json_t* jmap_patchobject_apply(json_t *val, json_t *patch, json_t *invalid);

#define PATCH_NO_REMOVE (1<<0)
#define PATCH_ALLOW_ARRAY (1<<1)
extern json_t* jmap_patchobject_apply(json_t *val, json_t *patch,
json_t *invalid, unsigned flags);

/* Create a patch-object that transforms src into dst. */
extern json_t *jmap_patchobject_create(json_t *src, json_t *dst, unsigned flags);
Expand Down

0 comments on commit 89d49a9

Please sign in to comment.