Skip to content

Commit 0360295

Browse files
committed
feature: add option to sort encoded object keys
1 parent c305d55 commit 0360295

3 files changed

Lines changed: 249 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
* [decode_allow_comment](#decode_allow_comment)
2223

@@ -228,6 +229,16 @@ print(cjson.encode({ a = 1, b = { c = 2 } }))
228229

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

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

lua_cjson.c

Lines changed: 210 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
#define DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH 1
9494
#define DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES 0
9595
#define DEFAULT_ENCODE_INDENT NULL
96+
#define DEFAULT_ENCODE_SORT_KEYS 0
9697

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

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

195+
/* encode_keybuf is only allocated and used when
196+
* sort_keys is set */
197+
keybuf_t encode_keybuf;
198+
168199
int encode_sparse_convert;
169200
int encode_sparse_ratio;
170201
int encode_sparse_safe;
@@ -175,6 +206,7 @@ typedef struct {
175206
int encode_empty_table_as_object;
176207
int encode_escape_forward_slash;
177208
const char *encode_indent;
209+
int encode_sort_keys;
178210

179211
int decode_invalid_numbers;
180212
int decode_max_depth;
@@ -487,6 +519,31 @@ static int json_cfg_encode_escape_forward_slash(lua_State *l)
487519
return ret;
488520
}
489521

522+
/* Configures JSON encoding keys sorting */
523+
static int json_cfg_encode_sort_keys(lua_State *l)
524+
{
525+
json_config_t *cfg = json_arg_init(l, 1);
526+
int old_value;
527+
528+
old_value = cfg->encode_sort_keys;
529+
530+
json_enum_option(l, 1, &cfg->encode_sort_keys, NULL, 1);
531+
532+
/* Init / free the keybuf if the setting has changed */
533+
if (old_value ^ cfg->encode_sort_keys) {
534+
if (cfg->encode_sort_keys) {
535+
strbuf_init(&cfg->encode_keybuf.buf, 0);
536+
cfg->encode_keybuf.size = 0;
537+
cfg->encode_keybuf.capacity = 0;
538+
} else {
539+
strbuf_free(&cfg->encode_keybuf.buf);
540+
free(cfg->encode_keybuf.keys);
541+
}
542+
}
543+
544+
return 1;
545+
}
546+
490547
static int json_destroy_config(lua_State *l)
491548
{
492549
json_config_t *cfg;
@@ -531,6 +588,7 @@ static void json_create_config(lua_State *l)
531588
cfg->encode_escape_forward_slash = DEFAULT_ENCODE_ESCAPE_FORWARD_SLASH;
532589
cfg->encode_skip_unsupported_value_types = DEFAULT_ENCODE_SKIP_UNSUPPORTED_VALUE_TYPES;
533590
cfg->encode_indent = DEFAULT_ENCODE_INDENT;
591+
cfg->encode_sort_keys = DEFAULT_ENCODE_SORT_KEYS;
534592

535593
#if DEFAULT_ENCODE_KEEP_BUFFER > 0
536594
strbuf_init(&cfg->encode_buf, 0);
@@ -589,17 +647,12 @@ static void json_encode_exception(lua_State *l, json_config_t *cfg, strbuf_t *js
589647
{
590648
if (!cfg->encode_keep_buffer)
591649
strbuf_free(json);
650+
592651
luaL_error(l, "Cannot serialise %s: %s",
593652
lua_typename(l, lua_type(l, lindex)), reason);
594653
}
595654

596-
/* json_append_string args:
597-
* - lua_State
598-
* - JSON strbuf
599-
* - String (Lua stack index)
600-
*
601-
* Returns nothing. Doesn't remove string from Lua stack */
602-
static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
655+
static void json_append_string_contents(lua_State *l, strbuf_t *json, int lindex)
603656
{
604657
const char *escstr;
605658
const char *str;
@@ -612,19 +665,30 @@ static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
612665
* This buffer is reused constantly for small strings
613666
* If there are any excess pages, they won't be hit anyway.
614667
* This gains ~5% speedup. */
615-
if (len > SIZE_MAX / 6 - 3)
668+
if (len >= SIZE_MAX / 6)
616669
abort(); /* Overflow check */
617-
strbuf_ensure_empty_length(json, len * 6 + 2);
670+
strbuf_ensure_empty_length(json, len * 6);
618671

619-
strbuf_append_char_unsafe(json, '\"');
620672
for (i = 0; i < len; i++) {
621673
escstr = char2escape[(unsigned char)str[i]];
622674
if (escstr)
623675
strbuf_append_string(json, escstr);
624676
else
625677
strbuf_append_char_unsafe(json, str[i]);
626678
}
627-
strbuf_append_char_unsafe(json, '\"');
679+
}
680+
681+
/* json_append_string args:
682+
* - lua_State
683+
* - JSON strbuf
684+
* - String (Lua stack index)
685+
*
686+
* Returns nothing. Doesn't remove string from Lua stack */
687+
static void json_append_string(lua_State *l, strbuf_t *json, int lindex)
688+
{
689+
strbuf_append_char(json, '\"');
690+
json_append_string_contents(l, json, lindex);
691+
strbuf_append_char(json, '\"');
628692
}
629693

630694
/* Find the size of the array on the top of the Lua stack
@@ -804,6 +868,17 @@ static void json_append_number(lua_State *l, json_config_t *cfg,
804868
strbuf_extend_length(json, len);
805869
}
806870

871+
/* Compare key_entry_t for qsort. */
872+
static int cmp_key_entries(const void *a, const void *b) {
873+
const key_entry_t *ka = a;
874+
const key_entry_t *kb = b;
875+
const size_t min_length = ka->length < kb->length ? ka->length : kb->length;
876+
int res = memcmp(ka->buf->buf + ka->offset,
877+
kb->buf->buf + kb->offset,
878+
min_length);
879+
return res ? res : (ka->length - kb->length);
880+
}
881+
807882
static void json_append_object(lua_State *l, json_config_t *cfg,
808883
int current_depth, strbuf_t *json)
809884
{
@@ -813,48 +888,133 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
813888
/* Object */
814889
strbuf_append_char(json, '{');
815890

816-
lua_pushnil(l);
817891
/* table, startkey */
818892
comma = 0;
819-
while (lua_next(l, -2) != 0) {
820-
has_items = 1;
893+
lua_pushnil(l);
894+
if (cfg->encode_sort_keys) {
895+
keybuf_t *keybuf = &cfg->encode_keybuf;
896+
size_t init_keybuf_size = keybuf->size;
897+
size_t init_keybuf_length = strbuf_length(&keybuf->buf);
898+
899+
/* Collect keys into keybuf */
900+
while (lua_next(l, -2) != 0) {
901+
has_items = 1;
902+
if (keybuf->size == keybuf->capacity) {
903+
if (!keybuf->capacity) {
904+
keybuf->capacity = KEYBUF_DEFAULT_CAPACITY;
905+
keybuf->keys = malloc(keybuf->capacity * sizeof(key_entry_t));
906+
if (!keybuf->keys)
907+
json_encode_exception(l, cfg, json, -1, "out of memory");
908+
} else {
909+
keybuf->capacity *= 2;
910+
key_entry_t *tmp = realloc(keybuf->keys,
911+
keybuf->capacity * sizeof(key_entry_t));
912+
if (!tmp)
913+
json_encode_exception(l, cfg, json, -1, "out of memory");
914+
keybuf->keys = tmp;
915+
}
916+
}
821917

822-
json_pos = strbuf_length(json);
823-
if (comma++ > 0)
824-
strbuf_append_char(json, ',');
918+
keytype = lua_type(l, -2);
919+
key_entry_t key_entry = {
920+
.buf = &keybuf->buf,
921+
.offset = strbuf_length(&keybuf->buf),
922+
.raw_typ = keytype,
923+
};
924+
if (keytype == LUA_TSTRING) {
925+
json_append_string_contents(l, &keybuf->buf, -2);
926+
key_entry.raw.string = lua_tostring(l, -2);
927+
} else if (keytype == LUA_TNUMBER) {
928+
json_append_number(l, cfg, &keybuf->buf, -2);
929+
key_entry.raw.number = lua_tointeger(l, -2);
930+
} else {
931+
json_encode_exception(l, cfg, json, -2,
932+
"table key must be number or string");
933+
}
934+
key_entry.length = strbuf_length(&keybuf->buf) - key_entry.offset;
935+
keybuf->keys[keybuf->size++] = key_entry;
936+
lua_pop(l, 1);
937+
}
825938

826-
if (cfg->encode_indent)
827-
json_append_newline_and_indent(json, cfg, current_depth);
939+
size_t keys_count = keybuf->size - init_keybuf_size;
940+
qsort(keybuf->keys + init_keybuf_size, keys_count,
941+
sizeof (key_entry_t), cmp_key_entries);
828942

829-
/* table, key, value */
830-
keytype = lua_type(l, -2);
831-
if (keytype == LUA_TNUMBER) {
832-
strbuf_append_char(json, '"');
833-
json_append_number(l, cfg, json, -2);
834-
strbuf_append_mem(json, "\":", 2);
835-
} else if (keytype == LUA_TSTRING) {
836-
json_append_string(l, json, -2);
837-
strbuf_append_char(json, ':');
838-
} else {
839-
json_encode_exception(l, cfg, json, -2,
840-
"table key must be a number or string");
841-
/* never returns */
842-
}
843-
if (cfg->encode_indent)
844-
strbuf_append_char(json, ' ');
943+
for (size_t i = init_keybuf_size; i < init_keybuf_size + keys_count; i++) {
944+
key_entry_t *current_key = &keybuf->keys[i];
945+
json_pos = strbuf_length(json);
946+
if (comma++ > 0)
947+
strbuf_append_char(json, ',');
845948

949+
if (cfg->encode_indent)
950+
json_append_newline_and_indent(json, cfg, current_depth);
846951

847-
/* table, key, value */
848-
err = json_append_data(l, cfg, current_depth, json);
849-
if (err) {
850-
strbuf_set_length(json, json_pos);
851-
if (comma == 1) {
852-
comma = 0;
952+
strbuf_ensure_empty_length(json, current_key->length + 3);
953+
strbuf_append_char_unsafe(json, '"');
954+
strbuf_append_mem_unsafe(json, keybuf->buf.buf + current_key->offset,
955+
current_key->length);
956+
strbuf_append_mem_unsafe(json, "\":", 2);
957+
958+
if (cfg->encode_indent)
959+
strbuf_append_char(json, ' ');
960+
961+
if (current_key->raw_typ == LUA_TSTRING)
962+
lua_pushstring(l, current_key->raw.string);
963+
else
964+
lua_pushnumber(l, current_key->raw.number);
965+
966+
lua_gettable(l, -2);
967+
err = json_append_data(l, cfg, current_depth, json);
968+
if (err) {
969+
strbuf_set_length(json, json_pos);
970+
if (comma == 1)
971+
comma = 0;
853972
}
973+
lua_pop(l, 1);
854974
}
975+
/* resize encode_keybuf to reuse allocated memory for forward keys */
976+
strbuf_set_length(&keybuf->buf, init_keybuf_length);
977+
keybuf->size = init_keybuf_size;
978+
} else {
979+
while (lua_next(l, -2) != 0) {
980+
has_items = 1;
981+
982+
json_pos = strbuf_length(json);
983+
if (comma++ > 0)
984+
strbuf_append_char(json, ',');
985+
986+
if (cfg->encode_indent)
987+
json_append_newline_and_indent(json, cfg, current_depth);
988+
989+
/* table, key, value */
990+
keytype = lua_type(l, -2);
991+
if (keytype == LUA_TNUMBER) {
992+
strbuf_append_char(json, '"');
993+
json_append_number(l, cfg, json, -2);
994+
strbuf_append_mem(json, "\":", 2);
995+
} else if (keytype == LUA_TSTRING) {
996+
json_append_string(l, json, -2);
997+
strbuf_append_char(json, ':');
998+
} else {
999+
json_encode_exception(l, cfg, json, -2,
1000+
"table key must be a number or string");
1001+
/* never returns */
1002+
}
1003+
if (cfg->encode_indent)
1004+
strbuf_append_char(json, ' ');
1005+
1006+
/* table, key, value */
1007+
err = json_append_data(l, cfg, current_depth, json);
1008+
if (err) {
1009+
strbuf_set_length(json, json_pos);
1010+
if (comma == 1) {
1011+
comma = 0;
1012+
}
1013+
}
8551014

856-
lua_pop(l, 1);
857-
/* table, key */
1015+
lua_pop(l, 1);
1016+
/* table, key */
1017+
}
8581018
}
8591019

8601020
if (has_items && cfg->encode_indent)
@@ -982,6 +1142,12 @@ static int json_encode(lua_State *l)
9821142
strbuf_reset(encode_buf);
9831143
}
9841144

1145+
if (cfg->encode_sort_keys) {
1146+
/* Reuse existing keybuf */
1147+
strbuf_reset(&cfg->encode_keybuf.buf);
1148+
cfg->encode_keybuf.size = 0;
1149+
}
1150+
9851151
json_append_data(l, cfg, 0, encode_buf);
9861152
json = strbuf_string(encode_buf, &len);
9871153

@@ -1682,6 +1848,7 @@ static int lua_cjson_new(lua_State *l)
16821848
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
16831849
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
16841850
{ "encode_indent", json_cfg_encode_indent },
1851+
{ "encode_sort_keys", json_cfg_encode_sort_keys },
16851852
{ "new", lua_cjson_new },
16861853
{ NULL, NULL }
16871854
};

0 commit comments

Comments
 (0)