Skip to content

Commit 51bace7

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

File tree

3 files changed

+233
-38
lines changed

3 files changed

+233
-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: 194 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,19 @@ 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 = a;
807+
const key_entry_t *kb = b;
808+
int res = memcmp(ka->buf->buf + ka->offset,
809+
kb->buf->buf + kb->offset,
810+
ka->length < kb->length ? ka->length : kb->length);
811+
if (res == 0)
812+
return (ka->length - kb->length);
813+
return res;
814+
815+
}
816+
751817
static void json_append_object(lua_State *l, json_config_t *cfg,
752818
int current_depth, strbuf_t *json)
753819
{
@@ -756,40 +822,118 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
756822
/* Object */
757823
strbuf_append_char(json, '{');
758824

759-
lua_pushnil(l);
760-
/* table, startkey */
761825
comma = 0;
762-
while (lua_next(l, -2) != 0) {
763-
json_pos = strbuf_length(json);
764-
if (comma++ > 0)
765-
strbuf_append_char(json, ',');
826+
if (cfg->encode_sort_keys) {
827+
keybuf_t *keybuf = &cfg->encode_keybuf;
828+
size_t init_keybuf_size = cfg->encode_keybuf.size;
829+
size_t init_keybuf_length = strbuf_length(&cfg->encode_keybuf.buf);
766830

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 */
831+
lua_pushnil(l);
832+
while (lua_next(l, -2) != 0) {
833+
if (keybuf->size == keybuf->capacity){
834+
if (!keybuf->capacity) {
835+
keybuf->capacity = KEYBUF_DEFAULT_CAPACITY;
836+
keybuf->keys = malloc(keybuf->capacity * sizeof(key_entry_t));
837+
if (!keybuf->keys)
838+
json_encode_exception(l, cfg, json, -1, "out of memory");
839+
} else {
840+
keybuf->capacity *= 2;
841+
key_entry_t *tmp = realloc(keybuf->keys,
842+
keybuf->capacity * sizeof(key_entry_t));
843+
if (!tmp)
844+
json_encode_exception(l, cfg, json, -1, "out of memory");
845+
keybuf->keys = tmp;
846+
}
847+
}
848+
849+
keytype = lua_type(l, -2);
850+
key_entry_t key_entry = {
851+
.buf = &keybuf->buf,
852+
.offset = strbuf_length(&keybuf->buf),
853+
.raw_typ = keytype,
854+
};
855+
if (keytype == LUA_TSTRING) {
856+
json_append_string_contents(l, &keybuf->buf, -2);
857+
key_entry.raw.string = lua_tostring(l, -2);
858+
} else if (keytype == LUA_TNUMBER) {
859+
json_append_number(l, cfg, &keybuf->buf, -2);
860+
key_entry.raw.number = lua_tointeger(l, -2);
861+
} else {
862+
json_encode_exception(l, cfg, json, -2,
863+
"table key must be number or string");
864+
}
865+
key_entry.length = strbuf_length(&keybuf->buf) - key_entry.offset;
866+
keybuf->keys[keybuf->size++] = key_entry;
867+
lua_pop(l, 1);
780868
}
781869

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

791-
lua_pop(l, 1);
792-
/* table, key */
926+
/* table, key, value */
927+
err = json_append_data(l, cfg, current_depth, json);
928+
if (err) {
929+
strbuf_set_length(json, json_pos);
930+
if (comma == 1)
931+
comma = 0;
932+
}
933+
934+
lua_pop(l, 1);
935+
/* table, key */
936+
}
793937
}
794938

795939
strbuf_append_char(json, '}');
@@ -914,6 +1058,12 @@ static int json_encode(lua_State *l)
9141058
strbuf_reset(encode_buf);
9151059
}
9161060

1061+
if (cfg->encode_sort_keys) {
1062+
strbuf_init(&cfg->encode_keybuf.buf, 0);
1063+
cfg->encode_keybuf.size = 0;
1064+
cfg->encode_keybuf.capacity = 0;
1065+
}
1066+
9171067
json_append_data(l, cfg, 0, encode_buf);
9181068
json = strbuf_string(encode_buf, &len);
9191069

@@ -922,6 +1072,11 @@ static int json_encode(lua_State *l)
9221072
if (!cfg->encode_keep_buffer)
9231073
strbuf_free(encode_buf);
9241074

1075+
if (cfg->encode_sort_keys) {
1076+
strbuf_free(&cfg->encode_keybuf.buf);
1077+
free(cfg->encode_keybuf.keys);
1078+
}
1079+
9251080
return 1;
9261081
}
9271082

@@ -1571,6 +1726,7 @@ static int lua_cjson_new(lua_State *l)
15711726
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
15721727
{ "encode_escape_forward_slash", json_cfg_encode_escape_forward_slash },
15731728
{ "encode_skip_unsupported_value_types", json_cfg_encode_skip_unsupported_value_types },
1729+
{ "encode_sort_keys", json_cfg_encode_sort_keys },
15741730
{ "new", lua_cjson_new },
15751731
{ NULL, NULL }
15761732
};

0 commit comments

Comments
 (0)