Skip to content

Commit f945fde

Browse files
committed
feature: add option to sort encoded object keys
Fixes #66
1 parent ea05d06 commit f945fde

3 files changed

Lines changed: 242 additions & 43 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Table of Contents
1717
* [encode_escape_forward_slash](#encode_escape_forward_slash)
1818
* [encode_skip_unsupported_value_types](#encode_skip_unsupported_value_types)
1919
* [encode_indent](#encode_indent)
20+
* [encode_sort_keys](#encode_sort_keys)
2021
* [decode_array_with_array_mt](#decode_array_with_array_mt)
2122

2223
Description
@@ -227,6 +228,16 @@ print(cjson.encode({ a = 1, b = { c = 2 } }))
227228

228229
[Back to TOC](#table-of-contents)
229230

231+
encode_sort_keys
232+
---------------------------
233+
**syntax:** `cjson.encode_sort_keys(enabled)`
234+
235+
**default:** false
236+
237+
If enabled, keys in encoded objects will be sorted in alphabetical order.
238+
239+
[Back to TOC](#table-of-contents)
240+
230241
decode_array_with_array_mt
231242
--------------------------
232243
**syntax:** `cjson.decode_array_with_array_mt(enabled)`

lua_cjson.c

Lines changed: 203 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
9393
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
9494
#define DEFAULT_ENCODE_INDENT NULL
95+
#define DEFAULT_ENCODE_SORT_KEYS 0
9596

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

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

194+
/* encode_keybuf is only allocated and used when
195+
* sort_keys is set */
196+
keybuf_t encode_keybuf;
197+
167198
int encode_sparse_convert;
168199
int encode_sparse_ratio;
169200
int encode_sparse_safe;
@@ -174,6 +205,7 @@ typedef struct {
174205
int encode_empty_table_as_object;
175206
int encode_escape_forward_slash;
176207
const char *encode_indent;
208+
int encode_sort_keys;
177209

178210
int decode_invalid_numbers;
179211
int decode_max_depth;
@@ -475,6 +507,15 @@ static int json_cfg_encode_escape_forward_slash(lua_State *l)
475507
return ret;
476508
}
477509

510+
static int json_cfg_encode_sort_keys(lua_State *l)
511+
{
512+
json_config_t *cfg = json_arg_init(l, 1);
513+
514+
json_enum_option(l, 1, &cfg->encode_sort_keys, NULL, 1);
515+
516+
return 1;
517+
}
518+
478519
static int json_destroy_config(lua_State *l)
479520
{
480521
json_config_t *cfg;
@@ -518,6 +559,7 @@ static void json_create_config(lua_State *l)
518559
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
519560
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
520561
cfg->encode_indent = DEFAULT_ENCODE_INDENT;
562+
cfg->encode_sort_keys = DEFAULT_ENCODE_SORT_KEYS;
521563

522564
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
523565
strbuf_init(&cfg->encode_buf, 0);
@@ -576,17 +618,17 @@ static void json_encode_exception(lua_State *l, json_config_t *cfg, strbuf_t *js
576618
{
577619
if (!cfg->encode_keep_buffer)
578620
strbuf_free(json);
621+
622+
if (cfg->encode_sort_keys) {
623+
strbuf_free(&cfg->encode_keybuf.buf);
624+
free(cfg->encode_keybuf.keys);
625+
}
626+
579627
luaL_error(l, "Cannot serialise %s: %s",
580628
lua_typename(l, lua_type(l, lindex)), reason);
581629
}
582630

583-
/* json_append_string args:
584-
* - lua_State
585-
* - JSON strbuf
586-
* - String (Lua stack index)
587-
*
588-
* Returns nothing. Doesn't remove string from Lua stack */
589-
static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
631+
static void json_append_string_contents(lua_State *l, strbuf_t *json, int lindex)
590632
{
591633
const char *escstr;
592634
const char *str;
@@ -599,19 +641,30 @@ static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
599641
* This buffer is reused constantly for small strings
600642
* If there are any excess pages, they won't be hit anyway.
601643
* This gains ~5% speedup. */
602-
if (len > SIZE_MAX / 6 - 3)
644+
if (len >= SIZE_MAX / 6)
603645
abort(); /* Overflow check */
604-
strbuf_ensure_empty_length(json, len * 6 + 2);
646+
strbuf_ensure_empty_length(json, len * 6);
605647

606-
strbuf_append_char_unsafe(json, '\"');
607648
for (i = 0; i < len; i++) {
608649
escstr = char2escape[(unsigned char)str[i]];
609650
if (escstr)
610651
strbuf_append_string(json, escstr);
611652
else
612653
strbuf_append_char_unsafe(json, str[i]);
613654
}
614-
strbuf_append_char_unsafe(json, '\"');
655+
}
656+
657+
/* json_append_string args:
658+
* - lua_State
659+
* - JSON strbuf
660+
* - String (Lua stack index)
661+
*
662+
* Returns nothing. Doesn't remove string from Lua stack */
663+
static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
664+
{
665+
strbuf_append_char(json, '\"');
666+
json_append_string_contents(l, json, lindex);
667+
strbuf_append_char(json, '\"');
615668
}
616669

617670
/* Find the size of the array on the top of the Lua stack
@@ -791,6 +844,17 @@ static void json_append_number(lua_State *l, json_config_t *cfg,
791844
strbuf_extend_length(json, len);
792845
}
793846

847+
/* Compare key_entry_t for qsort. */
848+
static int cmp_key_entries(const void *a, const void *b) {
849+
const key_entry_t *ka = a;
850+
const key_entry_t *kb = b;
851+
const size_t min_length = ka->length < kb->length ? ka->length : kb->length;
852+
int res = memcmp(ka->buf->buf + ka->offset,
853+
kb->buf->buf + kb->offset,
854+
min_length);
855+
return res ? res : (ka->length - kb->length);
856+
}
857+
794858
static void json_append_object(lua_State *l, json_config_t *cfg,
795859
int current_depth, strbuf_t *json)
796860
{
@@ -800,48 +864,132 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
800864
/* Object */
801865
strbuf_append_char(json, '{');
802866

803-
lua_pushnil(l);
804867
/* table, startkey */
805868
comma = 0;
806-
while (lua_next(l, -2) != 0) {
807-
has_items = 1;
869+
lua_pushnil(l);
870+
if (cfg->encode_sort_keys) {
871+
keybuf_t *keybuf = &cfg->encode_keybuf;
872+
size_t init_keybuf_size = keybuf->size;
873+
size_t init_keybuf_length = strbuf_length(&keybuf->buf);
874+
875+
while (lua_next(l, -2) != 0) {
876+
has_items = 1;
877+
if (keybuf->size == keybuf->capacity) {
878+
if (!keybuf->capacity) {
879+
keybuf->capacity = KEYBUF_DEFAULT_CAPACITY;
880+
keybuf->keys = malloc(keybuf->capacity * sizeof(key_entry_t));
881+
if (!keybuf->keys)
882+
json_encode_exception(l, cfg, json, -1, "out of memory");
883+
} else {
884+
keybuf->capacity *= 2;
885+
key_entry_t *tmp = realloc(keybuf->keys,
886+
keybuf->capacity * sizeof(key_entry_t));
887+
if (!tmp)
888+
json_encode_exception(l, cfg, json, -1, "out of memory");
889+
keybuf->keys = tmp;
890+
}
891+
}
808892

809-
json_pos = strbuf_length(json);
810-
if (comma++ > 0)
811-
strbuf_append_char(json, ',');
893+
keytype = lua_type(l, -2);
894+
key_entry_t key_entry = {
895+
.buf = &keybuf->buf,
896+
.offset = strbuf_length(&keybuf->buf),
897+
.raw_typ = keytype,
898+
};
899+
if (keytype == LUA_TSTRING) {
900+
json_append_string_contents(l, &keybuf->buf, -2);
901+
key_entry.raw.string = lua_tostring(l, -2);
902+
} else if (keytype == LUA_TNUMBER) {
903+
json_append_number(l, cfg, &keybuf->buf, -2);
904+
key_entry.raw.number = lua_tointeger(l, -2);
905+
} else {
906+
json_encode_exception(l, cfg, json, -2,
907+
"table key must be number or string");
908+
}
909+
key_entry.length = strbuf_length(&keybuf->buf) - key_entry.offset;
910+
keybuf->keys[keybuf->size++] = key_entry;
911+
lua_pop(l, 1);
912+
}
812913

813-
if (cfg->encode_indent)
814-
json_append_newline_and_indent(json, cfg, current_depth);
914+
size_t keys_count = keybuf->size - init_keybuf_size;
915+
qsort(keybuf->keys + init_keybuf_size, keys_count,
916+
sizeof (key_entry_t), cmp_key_entries);
815917

816-
/* table, key, value */
817-
keytype = lua_type(l, -2);
818-
if (keytype == LUA_TNUMBER) {
819-
strbuf_append_char(json, '"');
820-
json_append_number(l, cfg, json, -2);
821-
strbuf_append_mem(json, "\":", 2);
822-
} else if (keytype == LUA_TSTRING) {
823-
json_append_string(l, json, -2);
824-
strbuf_append_char(json, ':');
825-
} else {
826-
json_encode_exception(l, cfg, json, -2,
827-
"table key must be a number or string");
828-
/* never returns */
829-
}
830-
if (cfg->encode_indent)
831-
strbuf_append_char(json, ' ');
918+
for (size_t i = init_keybuf_size; i < init_keybuf_size + keys_count; i++) {
919+
key_entry_t *current_key = &keybuf->keys[i];
920+
json_pos = strbuf_length(json);
921+
if (comma++ > 0)
922+
strbuf_append_char(json, ',');
832923

924+
if (cfg->encode_indent)
925+
json_append_newline_and_indent(json, cfg, current_depth);
833926

834-
/* table, key, value */
835-
err = json_append_data(l, cfg, current_depth, json);
836-
if (err) {
837-
strbuf_set_length(json, json_pos);
838-
if (comma == 1) {
839-
comma = 0;
927+
strbuf_ensure_empty_length(json, current_key->length + 3);
928+
strbuf_append_char_unsafe(json, '"');
929+
strbuf_append_mem_unsafe(json, keybuf->buf.buf + current_key->offset,
930+
current_key->length);
931+
strbuf_append_mem_unsafe(json, "\":", 2);
932+
933+
if (cfg->encode_indent)
934+
strbuf_append_char(json, ' ');
935+
936+
if (current_key->raw_typ == LUA_TSTRING)
937+
lua_pushstring(l, current_key->raw.string);
938+
else
939+
lua_pushnumber(l, current_key->raw.number);
940+
941+
lua_gettable(l, -2);
942+
err = json_append_data(l, cfg, current_depth, json);
943+
if (err) {
944+
strbuf_set_length(json, json_pos);
945+
if (comma == 1)
946+
comma = 0;
840947
}
948+
lua_pop(l, 1);
841949
}
950+
/* resize encode_keybuf to reuse allocated memory for forward keys */
951+
strbuf_set_length(&keybuf->buf, init_keybuf_length);
952+
keybuf->size = init_keybuf_size;
953+
} else {
954+
while (lua_next(l, -2) != 0) {
955+
has_items = 1;
956+
957+
json_pos = strbuf_length(json);
958+
if (comma++ > 0)
959+
strbuf_append_char(json, ',');
960+
961+
if (cfg->encode_indent)
962+
json_append_newline_and_indent(json, cfg, current_depth);
963+
964+
/* table, key, value */
965+
keytype = lua_type(l, -2);
966+
if (keytype == LUA_TNUMBER) {
967+
strbuf_append_char(json, '"');
968+
json_append_number(l, cfg, json, -2);
969+
strbuf_append_mem(json, "\":", 2);
970+
} else if (keytype == LUA_TSTRING) {
971+
json_append_string(l, json, -2);
972+
strbuf_append_char(json, ':');
973+
} else {
974+
json_encode_exception(l, cfg, json, -2,
975+
"table key must be a number or string");
976+
/* never returns */
977+
}
978+
if (cfg->encode_indent)
979+
strbuf_append_char(json, ' ');
980+
981+
/* table, key, value */
982+
err = json_append_data(l, cfg, current_depth, json);
983+
if (err) {
984+
strbuf_set_length(json, json_pos);
985+
if (comma == 1) {
986+
comma = 0;
987+
}
988+
}
842989

843-
lua_pop(l, 1);
844-
/* table, key */
990+
lua_pop(l, 1);
991+
/* table, key */
992+
}
845993
}
846994

847995
if (has_items && cfg->encode_indent)
@@ -969,6 +1117,12 @@ static int json_encode(lua_State *l)
9691117
strbuf_reset(encode_buf);
9701118
}
9711119

1120+
if (cfg->encode_sort_keys) {
1121+
strbuf_init(&cfg->encode_keybuf.buf, 0);
1122+
cfg->encode_keybuf.size = 0;
1123+
cfg->encode_keybuf.capacity = 0;
1124+
}
1125+
9721126
json_append_data(l, cfg, 0, encode_buf);
9731127
json = strbuf_string(encode_buf, &len);
9741128

@@ -977,6 +1131,11 @@ static int json_encode(lua_State *l)
9771131
if (!cfg->encode_keep_buffer)
9781132
strbuf_free(encode_buf);
9791133

1134+
if (cfg->encode_sort_keys) {
1135+
strbuf_free(&cfg->encode_keybuf.buf);
1136+
free(cfg->encode_keybuf.keys);
1137+
}
1138+
9801139
return 1;
9811140
}
9821141

@@ -1635,6 +1794,7 @@ static int lua_cjson_new(lua_State *l)
16351794
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
16361795
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
16371796
{ "encode_indent", json_cfg_encode_indent },
1797+
{ "encode_sort_keys", json_cfg_encode_sort_keys },
16381798
{ "new", lua_cjson_new },
16391799
{ NULL, NULL }
16401800
};

0 commit comments

Comments
 (0)