Skip to content

Commit 91af5a5

Browse files
committed
feature: add option to sort encoded object keys
1 parent 91ca29d commit 91af5a5

File tree

3 files changed

+229
-38
lines changed

3 files changed

+229
-38
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Table of Contents
1616
* [encode_number_precision](#encode_number_precision)
1717
* [encode_escape_forward_slash](#encode_escape_forward_slash)
1818
* [encode_skip_unsupported_value_types](#encode_skip_unsupported_value_types)
19+
* [encode_sort_keys](#encode_sort_keys)
1920
* [decode_array_with_array_mt](#decode_array_with_array_mt)
2021

2122
Description
@@ -201,6 +202,16 @@ This will generate:
201202

202203
[Back to TOC](#table-of-contents)
203204

205+
encode_sort_keys
206+
---------------------------
207+
**syntax:** `cjson.encode_sort_keys(enabled)`
208+
209+
**default:** false
210+
211+
If enabled, keys in encoded objects will be sorted in alphabetical order.
212+
213+
[Back to TOC](#table-of-contents)
214+
204215
decode_array_with_array_mt
205216
--------------------------
206217
**syntax:** `cjson.decode_array_with_array_mt(enabled)`

lua_cjson.c

Lines changed: 190 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
#define DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT 0
9292
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
9393
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
94+
#define DEFAULT_ENCODE_SORT_KEYS 0
9495

9596
#ifdef DISABLE_INVALID_NUMBERS
9697
#undef DEFAULT_DECODE_INVALID_NUMBERS
@@ -155,6 +156,32 @@ static const char *json_token_type_name[] = {
155156
NULL
156157
};
157158

159+
typedef struct {
160+
strbuf_t *buf;
161+
size_t offset;
162+
size_t length;
163+
int raw_typ;
164+
union {
165+
lua_Number number;
166+
const char *string;
167+
} raw;
168+
} key_entry_t;
169+
170+
/* Stores all keys for a table when key sorting is enabled.
171+
* - buf: buffer holding serialized key strings
172+
* - keys: array of key_entry_t pointing into buf
173+
* - size: number of keys stored
174+
* - capacity: allocated capacity of keys array
175+
*/
176+
typedef struct {
177+
strbuf_t buf;
178+
key_entry_t *keys;
179+
size_t size;
180+
size_t capacity;
181+
} keybuf_t;
182+
183+
#define KEYBUF_DEFAULT_CAPACITY 32
184+
158185
typedef struct {
159186
json_token_type_t ch2token[256];
160187
char escape2char[256]; /* Decoding */
@@ -163,6 +190,10 @@ typedef struct {
163190
* encode_keep_buffer is set */
164191
strbuf_t encode_buf;
165192

193+
/* encode_keybuf is only allocated and used when
194+
* sort_keys is set */
195+
keybuf_t encode_keybuf;
196+
166197
int encode_sparse_convert;
167198
int encode_sparse_ratio;
168199
int encode_sparse_safe;
@@ -172,6 +203,7 @@ typedef struct {
172203
int encode_keep_buffer;
173204
int encode_empty_table_as_object;
174205
int encode_escape_forward_slash;
206+
int encode_sort_keys;
175207

176208
int decode_invalid_numbers;
177209
int decode_max_depth;
@@ -449,6 +481,15 @@ static int json_cfg_encode_escape_forward_slash(lua_State *l)
449481
return ret;
450482
}
451483

484+
static int json_cfg_encode_sort_keys(lua_State *l)
485+
{
486+
json_config_t *cfg = json_arg_init(l, 1);
487+
488+
json_enum_option(l, 1, &cfg->encode_sort_keys, NULL, 1);
489+
490+
return 1;
491+
}
492+
452493
static int json_destroy_config(lua_State *l)
453494
{
454495
json_config_t *cfg;
@@ -491,6 +532,7 @@ static void json_create_config(lua_State *l)
491532
cfg->decode_array_with_array_mt = DEFAULT_DECODE_ARRAY_WITH_ARRAY_MT;
492533
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
493534
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
535+
cfg->encode_sort_keys = DEFAULT_ENCODE_SORT_KEYS;
494536

495537
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
496538
strbuf_init(&cfg->encode_buf, 0);
@@ -549,17 +591,17 @@ static void json_encode_exception(lua_State *l, json_config_t *cfg, strbuf_t *js
549591
{
550592
if (!cfg->encode_keep_buffer)
551593
strbuf_free(json);
594+
595+
if (cfg->encode_sort_keys) {
596+
strbuf_free(&cfg->encode_keybuf.buf);
597+
free(cfg->encode_keybuf.keys);
598+
}
599+
552600
luaL_error(l, "Cannot serialise %s: %s",
553601
lua_typename(l, lua_type(l, lindex)), reason);
554602
}
555603

556-
/* json_append_string args:
557-
* - lua_State
558-
* - JSON strbuf
559-
* - String (Lua stack index)
560-
*
561-
* Returns nothing. Doesn't remove string from Lua stack */
562-
static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
604+
static void json_append_string_contents(lua_State *l, strbuf_t *json, int lindex)
563605
{
564606
const char *escstr;
565607
const char *str;
@@ -572,19 +614,30 @@ static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
572614
* This buffer is reused constantly for small strings
573615
* If there are any excess pages, they won't be hit anyway.
574616
* This gains ~5% speedup. */
575-
if (len > SIZE_MAX / 6 - 3)
617+
if (len >= SIZE_MAX / 6)
576618
abort(); /* Overflow check */
577-
strbuf_ensure_empty_length(json, len * 6 + 2);
619+
strbuf_ensure_empty_length(json, len * 6);
578620

579-
strbuf_append_char_unsafe(json, '\"');
580621
for (i = 0; i < len; i++) {
581622
escstr = char2escape[(unsigned char)str[i]];
582623
if (escstr)
583624
strbuf_append_string(json, escstr);
584625
else
585626
strbuf_append_char_unsafe(json, str[i]);
586627
}
587-
strbuf_append_char_unsafe(json, '\"');
628+
}
629+
630+
/* json_append_string args:
631+
* - lua_State
632+
* - JSON strbuf
633+
* - String (Lua stack index)
634+
*
635+
* Returns nothing. Doesn't remove string from Lua stack */
636+
static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
637+
{
638+
strbuf_append_char(json, '\"');
639+
json_append_string_contents(l, json, lindex);
640+
strbuf_append_char(json, '\"');
588641
}
589642

590643
/* Find the size of the array on the top of the Lua stack
@@ -748,6 +801,15 @@ static void json_append_number(lua_State *l, json_config_t *cfg,
748801
strbuf_extend_length(json, len);
749802
}
750803

804+
/* Compare key_entry_t for qsort. */
805+
static int cmp_key_entries(const void *a, const void *b) {
806+
const key_entry_t *ka = (const key_entry_t *)a;
807+
const key_entry_t *kb = (const key_entry_t *)b;
808+
return memcmp(ka->buf->buf + ka->offset,
809+
kb->buf->buf + kb->offset,
810+
(ka->length < kb->length ? ka->length: kb->length));
811+
}
812+
751813
static void json_append_object(lua_State *l, json_config_t *cfg,
752814
int current_depth, strbuf_t *json)
753815
{
@@ -756,40 +818,118 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
756818
/* Object */
757819
strbuf_append_char(json, '{');
758820

759-
lua_pushnil(l);
760-
/* table, startkey */
761821
comma = 0;
762-
while (lua_next(l, -2) != 0) {
763-
json_pos = strbuf_length(json);
764-
if (comma++ > 0)
765-
strbuf_append_char(json, ',');
822+
if (cfg->encode_sort_keys) {
823+
keybuf_t *keybuf = &cfg->encode_keybuf;
824+
size_t init_keybuf_size = cfg->encode_keybuf.size;
825+
size_t init_keybuf_length = strbuf_length(&cfg->encode_keybuf.buf);
766826

767-
/* table, key, value */
768-
keytype = lua_type(l, -2);
769-
if (keytype == LUA_TNUMBER) {
770-
strbuf_append_char(json, '"');
771-
json_append_number(l, cfg, json, -2);
772-
strbuf_append_mem(json, "\":", 2);
773-
} else if (keytype == LUA_TSTRING) {
774-
json_append_string(l, json, -2);
775-
strbuf_append_char(json, ':');
776-
} else {
777-
json_encode_exception(l, cfg, json, -2,
778-
"table key must be a number or string");
779-
/* never returns */
827+
lua_pushnil(l);
828+
while (lua_next(l, -2) != 0) {
829+
if (keybuf->size == keybuf->capacity){
830+
if (!keybuf->capacity) {
831+
keybuf->capacity = KEYBUF_DEFAULT_CAPACITY;
832+
keybuf->keys = malloc(keybuf->capacity * sizeof(key_entry_t));
833+
if (!keybuf->keys)
834+
json_encode_exception(l, cfg, json, -1, "out of memory");
835+
} else {
836+
keybuf->capacity *= 2;
837+
key_entry_t *tmp = realloc(keybuf->keys,
838+
keybuf->capacity * sizeof(key_entry_t));
839+
if (!tmp)
840+
json_encode_exception(l, cfg, json, -1, "out of memory");
841+
keybuf->keys = tmp;
842+
}
843+
}
844+
845+
keytype = lua_type(l, -2);
846+
key_entry_t key_entry = {
847+
.buf = &keybuf->buf,
848+
.offset = strbuf_length(&keybuf->buf),
849+
.raw_typ = keytype,
850+
};
851+
if (keytype == LUA_TSTRING) {
852+
json_append_string_contents(l, &keybuf->buf, -2);
853+
key_entry.raw.string = lua_tostring(l, -2);
854+
} else if (keytype == LUA_TNUMBER) {
855+
json_append_number(l, cfg, &keybuf->buf, -2);
856+
key_entry.raw.number = lua_tointeger(l, -2);
857+
} else {
858+
json_encode_exception(l, cfg, json, -2,
859+
"table key must be number or string");
860+
}
861+
key_entry.length = strbuf_length(&keybuf->buf) - key_entry.offset;
862+
keybuf->keys[keybuf->size++] = key_entry;
863+
lua_pop(l, 1);
780864
}
781865

782-
/* table, key, value */
783-
err = json_append_data(l, cfg, current_depth, json);
784-
if (err) {
785-
strbuf_set_length(json, json_pos);
786-
if (comma == 1) {
787-
comma = 0;
866+
size_t keys_count = keybuf->size - init_keybuf_size;
867+
qsort(keybuf->keys + init_keybuf_size, keys_count,
868+
sizeof (key_entry_t), cmp_key_entries);
869+
870+
for (size_t i = init_keybuf_size; i < init_keybuf_size + keys_count; i++) {
871+
key_entry_t *current_key = &keybuf->keys[i];
872+
json_pos = strbuf_length(json);
873+
if (comma++ > 0)
874+
strbuf_append_char(json, ',');
875+
876+
strbuf_ensure_empty_length(json, current_key->length + 3);
877+
strbuf_append_char_unsafe(json, '"');
878+
strbuf_append_mem_unsafe(json, keybuf->buf.buf + current_key->offset,
879+
current_key->length);
880+
strbuf_append_mem(json, "\":", 2);
881+
882+
if (current_key->raw_typ == LUA_TSTRING)
883+
lua_pushstring(l, current_key->raw.string);
884+
else
885+
lua_pushnumber(l, current_key->raw.number);
886+
887+
lua_gettable(l, -2);
888+
err = json_append_data(l, cfg, current_depth, json);
889+
if (err) {
890+
strbuf_set_length(json, json_pos);
891+
if (comma == 1)
892+
comma = 0;
788893
}
894+
lua_pop(l, 1);
789895
}
896+
/* resize encode_keybuf to reuse allocated memory for forward keys */
897+
strbuf_set_length(&keybuf->buf, init_keybuf_length);
898+
keybuf->size = init_keybuf_size;
899+
} else {
900+
lua_pushnil(l);
901+
/* table, startkey */
902+
while (lua_next(l, -2) != 0) {
903+
json_pos = strbuf_length(json);
904+
if (comma++ > 0)
905+
strbuf_append_char(json, ',');
906+
907+
/* table, key, value */
908+
keytype = lua_type(l, -2);
909+
if (keytype == LUA_TNUMBER) {
910+
strbuf_append_char(json, '"');
911+
json_append_number(l, cfg, json, -2);
912+
strbuf_append_mem(json, "\":", 2);
913+
} else if (keytype == LUA_TSTRING) {
914+
json_append_string(l, json, -2);
915+
strbuf_append_char(json, ':');
916+
} else {
917+
json_encode_exception(l, cfg, json, -2,
918+
"table key must be a number or string");
919+
/* never returns */
920+
}
790921

791-
lua_pop(l, 1);
792-
/* table, key */
922+
/* table, key, value */
923+
err = json_append_data(l, cfg, current_depth, json);
924+
if (err) {
925+
strbuf_set_length(json, json_pos);
926+
if (comma == 1)
927+
comma = 0;
928+
}
929+
930+
lua_pop(l, 1);
931+
/* table, key */
932+
}
793933
}
794934

795935
strbuf_append_char(json, '}');
@@ -914,6 +1054,12 @@ static int json_encode(lua_State *l)
9141054
strbuf_reset(encode_buf);
9151055
}
9161056

1057+
if (cfg->encode_sort_keys) {
1058+
strbuf_init(&cfg->encode_keybuf.buf, 0);
1059+
cfg->encode_keybuf.size = 0;
1060+
cfg->encode_keybuf.capacity = 0;
1061+
}
1062+
9171063
json_append_data(l, cfg, 0, encode_buf);
9181064
json = strbuf_string(encode_buf, &len);
9191065

@@ -922,6 +1068,11 @@ static int json_encode(lua_State *l)
9221068
if (!cfg->encode_keep_buffer)
9231069
strbuf_free(encode_buf);
9241070

1071+
if (cfg->encode_sort_keys) {
1072+
strbuf_free(&cfg->encode_keybuf.buf);
1073+
free(cfg->encode_keybuf.keys);
1074+
}
1075+
9251076
return 1;
9261077
}
9271078

@@ -1571,6 +1722,7 @@ static int lua_cjson_new(lua_State *l)
15711722
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
15721723
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
15731724
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
1725+
{ "encode_sort_keys", json_cfg_encode_sort_keys },
15741726
{ "new", lua_cjson_new },
15751727
{ NULL, NULL }
15761728
};

tests/test.lua

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,34 @@ local cjson_tests = {
333333
json.decode, { [["\uDB00\uD"]] },
334334
false, { "Expected value but found invalid unicode escape code at character 2" } },
335335

336+
-- Test keys sorting
337+
{ "Set encode_sort_keys(true)",
338+
json.encode_sort_keys, { true }, true, { true } },
339+
{ "Encode empty object with sorting",
340+
json.encode, { {} },
341+
true, { '{}' } },
342+
{ "Encode object with sorting",
343+
json.encode, { { a = 0, b = 0, [1] = 0, ["$"] = 0, [4] = 0, ["%"] = 0 } },
344+
true, { '{"$":0,"%":0,"1":0,"4":0,"a":0,"b":0}' } },
345+
{ "Encode object with string keys with sorting",
346+
json.encode, { { aa = 1, ba = 3, ab = 2, bc = 4, cc = 5 } },
347+
true, { '{"aa":1,"ab":2,"ba":3,"bc":4,"cc":5}' } },
348+
{ "Encode nested objects with sorting",
349+
json.encode, { { a = { b = 2, a = 1, c = 3 }, c = 0, b = { b = { a = 0, b = 0 }, a = { a = 0, b = 0 } } } },
350+
true, { '{"a":{"a":1,"b":2,"c":3},"b":{"a":{"a":0,"b":0},"b":{"a":0,"b":0}},"c":0}' } },
351+
{ "Encode array of objects with sorting",
352+
json.encode, { {
353+
{ a = 0, [1] = 0, [4] = 0, b = 0 },
354+
{ f = 0, [5] = 0, [10] = 0, x = 0 },
355+
{ c = 0, [-2] = 0, [2] = 0, d = 0 },
356+
} },
357+
true, { '[{"1":0,"4":0,"a":0,"b":0},{"10":0,"5":0,"f":0,"x":0},{"-2":0,"2":0,"c":0,"d":0}]' } },
358+
{ "Encode object with unicode keys",
359+
json.encode, { { ["é"] = 1, ["a"] = 2, ["ß"] = 3, [""] = 4 } },
360+
true, { '{"a":2,"ß":3,"é":1,"中":4}' } },
361+
{ "Set encode_sort_keys(false)",
362+
json.encode_sort_keys, { false }, true, { false } },
363+
336364
-- Test locale support
337365
--
338366
-- The standard Lua interpreter is ANSI C online doesn't support locales

0 commit comments

Comments
 (0)