diff options
author | Mark Pulford <mark@kyne.com.au> | 2011-12-30 14:17:44 +1030 |
---|---|---|
committer | Mark Pulford <mark@kyne.com.au> | 2011-12-30 14:17:44 +1030 |
commit | 2416b145073211b840781da6abf4b6d97f4657a6 (patch) | |
tree | 6e92a13a7cc8ef8357245bc3ef320f5841350991 | |
parent | 6cc88e3ac5275868e168acaf60203563f131355b (diff) | |
download | lua-cjson-2416b145073211b840781da6abf4b6d97f4657a6.tar.gz lua-cjson-2416b145073211b840781da6abf4b6d97f4657a6.tar.bz2 lua-cjson-2416b145073211b840781da6abf4b6d97f4657a6.zip |
Add fpconv to work around comma decimal points
Create a separate buffer and translate comma <> dot before calling
strtod(), and after calling sprintf() as required.
- Add "update_locale" Lua API call and init locale on module load.
- Move sprintf format string to fpconv
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | fpconv.c | 155 | ||||
-rw-r--r-- | fpconv.h | 11 | ||||
-rw-r--r-- | lua-cjson-1.0devel-1.rockspec | 4 | ||||
-rw-r--r-- | lua_cjson.c | 36 | ||||
-rw-r--r-- | strbuf.h | 14 | ||||
-rwxr-xr-x | tests/test.lua | 2 |
8 files changed, 206 insertions, 20 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 349342e..8d8a420 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -30,7 +30,7 @@ else() | |||
30 | set(_lua_module_dir "${_lua_lib_dir}/lua/5.1") | 30 | set(_lua_module_dir "${_lua_lib_dir}/lua/5.1") |
31 | endif() | 31 | endif() |
32 | 32 | ||
33 | add_library(cjson MODULE lua_cjson.c strbuf.c) | 33 | add_library(cjson MODULE lua_cjson.c strbuf.c fpconv.c) |
34 | set_target_properties(cjson PROPERTIES PREFIX "") | 34 | set_target_properties(cjson PROPERTIES PREFIX "") |
35 | install(TARGETS cjson DESTINATION "${_lua_module_dir}") | 35 | install(TARGETS cjson DESTINATION "${_lua_module_dir}") |
36 | 36 | ||
@@ -35,7 +35,7 @@ INSTALL_CMD = install | |||
35 | ## End platform specific section | 35 | ## End platform specific section |
36 | 36 | ||
37 | BUILD_CFLAGS = -fpic -I$(LUA_INCLUDE_DIR) $(CJSON_CFLAGS) | 37 | BUILD_CFLAGS = -fpic -I$(LUA_INCLUDE_DIR) $(CJSON_CFLAGS) |
38 | OBJS := lua_cjson.o strbuf.o | 38 | OBJS := lua_cjson.o strbuf.o fpconv.o |
39 | 39 | ||
40 | .PHONY: all clean install package doc | 40 | .PHONY: all clean install package doc |
41 | 41 | ||
diff --git a/fpconv.c b/fpconv.c new file mode 100644 index 0000000..3ff79dc --- /dev/null +++ b/fpconv.c | |||
@@ -0,0 +1,155 @@ | |||
1 | #include <stdio.h> | ||
2 | #include <stdlib.h> | ||
3 | #include <assert.h> | ||
4 | #include <string.h> | ||
5 | |||
6 | #include "fpconv.h" | ||
7 | |||
8 | static char locale_decimal_point = '.'; | ||
9 | |||
10 | /* In theory multibyte decimal_points are possible, but | ||
11 | * Lua CJSON only supports UTF-8 and known locales only have | ||
12 | * single byte decimal points ([.,]). | ||
13 | * | ||
14 | * localconv() may not be thread safe, and nl_langinfo() is not | ||
15 | * supported on some platforms. Use sprintf() instead. */ | ||
16 | void fpconv_update_locale() | ||
17 | { | ||
18 | char buf[8]; | ||
19 | |||
20 | snprintf(buf, sizeof(buf), "%g", 0.5); | ||
21 | |||
22 | /* Failing this test might imply the platform has a buggy dtoa | ||
23 | * implementation or wide characters */ | ||
24 | if (buf[0] != '0' || buf[2] != '5' || buf[3] != 0) { | ||
25 | fprintf(stderr, "Error: wide characters found or printf() bug."); | ||
26 | abort(); | ||
27 | } | ||
28 | |||
29 | locale_decimal_point = buf[1]; | ||
30 | } | ||
31 | |||
32 | /* Check for a valid number character: [-+0-9a-fA-FpPxX.] | ||
33 | * It doesn't matter if actual invalid characters are counted - strtod() | ||
34 | * will find the valid number if it exists. The risk is that slightly more | ||
35 | * memory might be allocated before a parse error occurs. */ | ||
36 | static int valid_number_character(char ch) | ||
37 | { | ||
38 | char lower_ch; | ||
39 | |||
40 | if ('0' <= ch && ch <= '9') | ||
41 | return 1; | ||
42 | if (ch == '-' || ch == '+' || ch == '.') | ||
43 | return 1; | ||
44 | |||
45 | /* Hex digits, exponent (e), base (p), "infinity",.. | ||
46 | * The main purpose is to not include a "comma". If any other invalid | ||
47 | * characters are included, the will only generate a parse error later. */ | ||
48 | lower_ch = ch | 0x20; | ||
49 | if ('a' <= lower_ch && lower_ch <= 'y') | ||
50 | return 1; | ||
51 | |||
52 | return 0; | ||
53 | } | ||
54 | |||
55 | /* Calculate the size of the buffer required for a locale | ||
56 | * conversion. Returns 0 if conversion is not required */ | ||
57 | static int strtod_buffer_size(const char *s) | ||
58 | { | ||
59 | const char *p = s; | ||
60 | |||
61 | while (valid_number_character(*p)) | ||
62 | p++; | ||
63 | |||
64 | return p - s; | ||
65 | } | ||
66 | |||
67 | /* Similar to strtod(), but must be passed the current locale's decimal point | ||
68 | * character. Guaranteed to be called at the start of any valid number in a string */ | ||
69 | double fpconv_strtod(const char *nptr, char **endptr) | ||
70 | { | ||
71 | char *num, *endnum, *dp; | ||
72 | int numlen; | ||
73 | double value; | ||
74 | |||
75 | /* System strtod() is fine when decimal point is '.' */ | ||
76 | if (locale_decimal_point == '.') | ||
77 | return strtod(nptr, endptr); | ||
78 | |||
79 | numlen = strtod_buffer_size(nptr); | ||
80 | if (!numlen) { | ||
81 | /* No valid characters found, standard strtod() return */ | ||
82 | *endptr = (char *)nptr; | ||
83 | return 0; | ||
84 | } | ||
85 | |||
86 | /* Duplicate number into buffer */ | ||
87 | num = malloc(numlen + 1); | ||
88 | if (!num) { | ||
89 | fprintf(stderr, "Out of memory"); | ||
90 | abort(); | ||
91 | } | ||
92 | memcpy(num, nptr, numlen); | ||
93 | num[numlen] = 0; | ||
94 | |||
95 | /* Update decimal point character if found */ | ||
96 | dp = strchr(num, '.'); | ||
97 | if (dp) | ||
98 | *dp = locale_decimal_point; | ||
99 | |||
100 | value = strtod(num, &endnum); | ||
101 | *endptr = (char *)&nptr[endnum - num]; | ||
102 | free(num); | ||
103 | |||
104 | return value; | ||
105 | } | ||
106 | |||
107 | /* "fmt" must point to a buffer of at least 6 characters */ | ||
108 | static void set_number_format(char *fmt, int precision) | ||
109 | { | ||
110 | int d1, d2, i; | ||
111 | |||
112 | assert(1 <= precision && precision <= 14); | ||
113 | |||
114 | /* Create printf format (%.14g) from precision */ | ||
115 | d1 = precision / 10; | ||
116 | d2 = precision % 10; | ||
117 | fmt[0] = '%'; | ||
118 | fmt[1] = '.'; | ||
119 | i = 2; | ||
120 | if (d1) { | ||
121 | fmt[i++] = '0' + d1; | ||
122 | } | ||
123 | fmt[i++] = '0' + d2; | ||
124 | fmt[i++] = 'g'; | ||
125 | fmt[i++] = 0; | ||
126 | } | ||
127 | |||
128 | /* Assumes there is always at least 32 characters available in the target buffer */ | ||
129 | int fpconv_g_fmt(char *str, double num, int precision) | ||
130 | { | ||
131 | char buf[FPCONV_G_FMT_BUFSIZE]; | ||
132 | char fmt[6]; | ||
133 | int len; | ||
134 | char *b; | ||
135 | |||
136 | set_number_format(fmt, precision); | ||
137 | |||
138 | /* Pass through when decimal point character is dot. */ | ||
139 | if (locale_decimal_point == '.') | ||
140 | return snprintf(str, FPCONV_G_FMT_BUFSIZE, fmt, num); | ||
141 | |||
142 | /* snprintf() to a buffer then translate for other decimal point characters */ | ||
143 | len = snprintf(buf, FPCONV_G_FMT_BUFSIZE, fmt, num); | ||
144 | |||
145 | /* Returned 'len' includes the null terminator */ | ||
146 | b = buf; | ||
147 | do { | ||
148 | *str++ = (*b == locale_decimal_point ? '.' : *b); | ||
149 | } while(*b++); | ||
150 | |||
151 | return len; | ||
152 | } | ||
153 | |||
154 | /* vi:ai et sw=4 ts=4: | ||
155 | */ | ||
diff --git a/fpconv.h b/fpconv.h new file mode 100644 index 0000000..b8a6469 --- /dev/null +++ b/fpconv.h | |||
@@ -0,0 +1,11 @@ | |||
1 | /* Lua CJSON floating point conversion routines */ | ||
2 | |||
3 | /* Buffer larger than required to store the largest %.14g number */ | ||
4 | # define FPCONV_G_FMT_BUFSIZE 32 | ||
5 | |||
6 | extern void fpconv_update_locale(); | ||
7 | extern int fpconv_g_fmt(char*, double, int); | ||
8 | extern double fpconv_strtod(const char*, char**); | ||
9 | |||
10 | /* vi:ai et sw=4 ts=4: | ||
11 | */ | ||
diff --git a/lua-cjson-1.0devel-1.rockspec b/lua-cjson-1.0devel-1.rockspec index 3890ec1..fa20c53 100644 --- a/lua-cjson-1.0devel-1.rockspec +++ b/lua-cjson-1.0devel-1.rockspec | |||
@@ -23,11 +23,11 @@ build = { | |||
23 | type = "builtin", | 23 | type = "builtin", |
24 | modules = { | 24 | modules = { |
25 | cjson = { | 25 | cjson = { |
26 | sources = { "lua_cjson.c", "strbuf.c" }, | 26 | sources = { "lua_cjson.c", "strbuf.c", "fpconv.c" }, |
27 | defines = { | ||
27 | -- Optional workaround: | 28 | -- Optional workaround: |
28 | -- USE_INTERNAL_ISINF: Provide internal isinf() implementation. Required | 29 | -- USE_INTERNAL_ISINF: Provide internal isinf() implementation. Required |
29 | -- on some Solaris platforms. | 30 | -- on some Solaris platforms. |
30 | defines = { | ||
31 | -- LuaRocks does not support platform specific configuration for Solaris. | 31 | -- LuaRocks does not support platform specific configuration for Solaris. |
32 | -- Uncomment the line below on Solaris platforms. | 32 | -- Uncomment the line below on Solaris platforms. |
33 | -- "USE_INTERNAL_ISINF" | 33 | -- "USE_INTERNAL_ISINF" |
diff --git a/lua_cjson.c b/lua_cjson.c index f5ea0dd..8e9b237 100644 --- a/lua_cjson.c +++ b/lua_cjson.c | |||
@@ -43,6 +43,7 @@ | |||
43 | #include <lauxlib.h> | 43 | #include <lauxlib.h> |
44 | 44 | ||
45 | #include "strbuf.h" | 45 | #include "strbuf.h" |
46 | #include "fpconv.h" | ||
46 | 47 | ||
47 | #ifndef CJSON_VERSION | 48 | #ifndef CJSON_VERSION |
48 | #define CJSON_VERSION "1.0devel" | 49 | #define CJSON_VERSION "1.0devel" |
@@ -60,6 +61,7 @@ | |||
60 | #define DEFAULT_ENCODE_REFUSE_BADNUM 1 | 61 | #define DEFAULT_ENCODE_REFUSE_BADNUM 1 |
61 | #define DEFAULT_DECODE_REFUSE_BADNUM 0 | 62 | #define DEFAULT_DECODE_REFUSE_BADNUM 0 |
62 | #define DEFAULT_ENCODE_KEEP_BUFFER 1 | 63 | #define DEFAULT_ENCODE_KEEP_BUFFER 1 |
64 | #define DEFAULT_ENCODE_NUMBER_PRECISION 14 | ||
63 | 65 | ||
64 | typedef enum { | 66 | typedef enum { |
65 | T_OBJ_BEGIN, | 67 | T_OBJ_BEGIN, |
@@ -104,7 +106,6 @@ typedef struct { | |||
104 | char *char2escape[256]; /* Encoding */ | 106 | char *char2escape[256]; /* Encoding */ |
105 | #endif | 107 | #endif |
106 | strbuf_t encode_buf; | 108 | strbuf_t encode_buf; |
107 | char number_fmt[8]; /* "%.XXg\0" */ | ||
108 | int current_depth; | 109 | int current_depth; |
109 | 110 | ||
110 | int encode_sparse_convert; | 111 | int encode_sparse_convert; |
@@ -253,12 +254,6 @@ static int json_cfg_encode_max_depth(lua_State *l) | |||
253 | return 1; | 254 | return 1; |
254 | } | 255 | } |
255 | 256 | ||
256 | static void json_set_number_precision(json_config_t *cfg, int prec) | ||
257 | { | ||
258 | cfg->encode_number_precision = prec; | ||
259 | sprintf(cfg->number_fmt, "%%.%dg", prec); | ||
260 | } | ||
261 | |||
262 | /* Configures number precision when converting doubles to text */ | 257 | /* Configures number precision when converting doubles to text */ |
263 | static int json_cfg_encode_number_precision(lua_State *l) | 258 | static int json_cfg_encode_number_precision(lua_State *l) |
264 | { | 259 | { |
@@ -272,7 +267,7 @@ static int json_cfg_encode_number_precision(lua_State *l) | |||
272 | precision = luaL_checkinteger(l, 1); | 267 | precision = luaL_checkinteger(l, 1); |
273 | luaL_argcheck(l, 1 <= precision && precision <= 14, 1, | 268 | luaL_argcheck(l, 1 <= precision && precision <= 14, 1, |
274 | "expected integer between 1 and 14"); | 269 | "expected integer between 1 and 14"); |
275 | json_set_number_precision(cfg, precision); | 270 | cfg->encode_number_precision = precision; |
276 | } | 271 | } |
277 | 272 | ||
278 | lua_pushinteger(l, cfg->encode_number_precision); | 273 | lua_pushinteger(l, cfg->encode_number_precision); |
@@ -342,6 +337,13 @@ static int json_cfg_refuse_invalid_numbers(lua_State *l) | |||
342 | return 1; | 337 | return 1; |
343 | } | 338 | } |
344 | 339 | ||
340 | static int json_update_locale(lua_State *l) | ||
341 | { | ||
342 | fpconv_update_locale(); | ||
343 | |||
344 | return 0; | ||
345 | } | ||
346 | |||
345 | static int json_destroy_config(lua_State *l) | 347 | static int json_destroy_config(lua_State *l) |
346 | { | 348 | { |
347 | json_config_t *cfg; | 349 | json_config_t *cfg; |
@@ -376,7 +378,7 @@ static void json_create_config(lua_State *l) | |||
376 | cfg->encode_refuse_badnum = DEFAULT_ENCODE_REFUSE_BADNUM; | 378 | cfg->encode_refuse_badnum = DEFAULT_ENCODE_REFUSE_BADNUM; |
377 | cfg->decode_refuse_badnum = DEFAULT_DECODE_REFUSE_BADNUM; | 379 | cfg->decode_refuse_badnum = DEFAULT_DECODE_REFUSE_BADNUM; |
378 | cfg->encode_keep_buffer = DEFAULT_ENCODE_KEEP_BUFFER; | 380 | cfg->encode_keep_buffer = DEFAULT_ENCODE_KEEP_BUFFER; |
379 | json_set_number_precision(cfg, 14); | 381 | cfg->encode_number_precision = DEFAULT_ENCODE_NUMBER_PRECISION; |
380 | 382 | ||
381 | /* Decoding init */ | 383 | /* Decoding init */ |
382 | 384 | ||
@@ -562,6 +564,7 @@ static void json_append_number(lua_State *l, strbuf_t *json, int index, | |||
562 | json_config_t *cfg) | 564 | json_config_t *cfg) |
563 | { | 565 | { |
564 | double num = lua_tonumber(l, index); | 566 | double num = lua_tonumber(l, index); |
567 | int len; | ||
565 | 568 | ||
566 | if (cfg->encode_refuse_badnum && (isinf(num) || isnan(num))) | 569 | if (cfg->encode_refuse_badnum && (isinf(num) || isnan(num))) |
567 | json_encode_exception(l, cfg, index, "must not be NaN or Inf"); | 570 | json_encode_exception(l, cfg, index, "must not be NaN or Inf"); |
@@ -571,11 +574,10 @@ static void json_append_number(lua_State *l, strbuf_t *json, int index, | |||
571 | strbuf_append_mem(json, "nan", 3); | 574 | strbuf_append_mem(json, "nan", 3); |
572 | } else { | 575 | } else { |
573 | /* Longest double printed with %.14g is 21 characters long: | 576 | /* Longest double printed with %.14g is 21 characters long: |
574 | * -1.7976931348623e+308 | 577 | * -1.7976931348623e+308 */ |
575 | * | 578 | strbuf_ensure_empty_length(json, FPCONV_G_FMT_BUFSIZE); |
576 | * Use 32 to include the \0, and a few extra just in case.. | 579 | len = fpconv_g_fmt(strbuf_empty_ptr(json), num, cfg->encode_number_precision); |
577 | */ | 580 | strbuf_extend_length(json, len); |
578 | strbuf_append_fmt(json, 32, cfg->number_fmt, num); | ||
579 | } | 581 | } |
580 | } | 582 | } |
581 | 583 | ||
@@ -963,7 +965,7 @@ static void json_next_number_token(json_parse_t *json, json_token_t *token) | |||
963 | 965 | ||
964 | token->type = T_NUMBER; | 966 | token->type = T_NUMBER; |
965 | startptr = &json->data[json->index]; | 967 | startptr = &json->data[json->index]; |
966 | token->value.number = strtod(&json->data[json->index], &endptr); | 968 | token->value.number = fpconv_strtod(&json->data[json->index], &endptr); |
967 | if (startptr == endptr) | 969 | if (startptr == endptr) |
968 | json_set_token_error(token, json, "invalid number"); | 970 | json_set_token_error(token, json, "invalid number"); |
969 | else | 971 | else |
@@ -1254,9 +1256,13 @@ int luaopen_cjson(lua_State *l) | |||
1254 | { "encode_number_precision", json_cfg_encode_number_precision }, | 1256 | { "encode_number_precision", json_cfg_encode_number_precision }, |
1255 | { "encode_keep_buffer", json_cfg_encode_keep_buffer }, | 1257 | { "encode_keep_buffer", json_cfg_encode_keep_buffer }, |
1256 | { "refuse_invalid_numbers", json_cfg_refuse_invalid_numbers }, | 1258 | { "refuse_invalid_numbers", json_cfg_refuse_invalid_numbers }, |
1259 | { "update_locale", json_update_locale }, | ||
1257 | { NULL, NULL } | 1260 | { NULL, NULL } |
1258 | }; | 1261 | }; |
1259 | 1262 | ||
1263 | /* Update the current locale for g_fmt/strtod */ | ||
1264 | fpconv_update_locale(); | ||
1265 | |||
1260 | /* Use json_config_key as a pointer. | 1266 | /* Use json_config_key as a pointer. |
1261 | * It's faster than using a config string, and more unique */ | 1267 | * It's faster than using a config string, and more unique */ |
1262 | lua_pushlightuserdata(l, &json_config_key); | 1268 | lua_pushlightuserdata(l, &json_config_key); |
@@ -62,7 +62,9 @@ extern void strbuf_resize(strbuf_t *s, int len); | |||
62 | static int strbuf_empty_length(strbuf_t *s); | 62 | static int strbuf_empty_length(strbuf_t *s); |
63 | static int strbuf_length(strbuf_t *s); | 63 | static int strbuf_length(strbuf_t *s); |
64 | static char *strbuf_string(strbuf_t *s, int *len); | 64 | static char *strbuf_string(strbuf_t *s, int *len); |
65 | static void strbuf_ensure_empty_length(strbuf_t *s, int len); | 65 | static void strbuf_ensure_empty_length(strbuf_t *s, int len); |
66 | static char *strbuf_empty_ptr(strbuf_t *s); | ||
67 | static void strbuf_extend_length(strbuf_t *s, int len); | ||
66 | 68 | ||
67 | /* Update */ | 69 | /* Update */ |
68 | extern void strbuf_append_fmt(strbuf_t *s, int len, const char *fmt, ...); | 70 | extern void strbuf_append_fmt(strbuf_t *s, int len, const char *fmt, ...); |
@@ -96,6 +98,16 @@ static inline void strbuf_ensure_empty_length(strbuf_t *s, int len) | |||
96 | strbuf_resize(s, s->length + len); | 98 | strbuf_resize(s, s->length + len); |
97 | } | 99 | } |
98 | 100 | ||
101 | static inline char *strbuf_empty_ptr(strbuf_t *s) | ||
102 | { | ||
103 | return s->buf + s->length; | ||
104 | } | ||
105 | |||
106 | static inline void strbuf_extend_length(strbuf_t *s, int len) | ||
107 | { | ||
108 | s->length += len; | ||
109 | } | ||
110 | |||
99 | static inline int strbuf_length(strbuf_t *s) | 111 | static inline int strbuf_length(strbuf_t *s) |
100 | { | 112 | { |
101 | return s->length; | 113 | return s->length; |
diff --git a/tests/test.lua b/tests/test.lua index bb696a2..bdae6ea 100755 --- a/tests/test.lua +++ b/tests/test.lua | |||
@@ -211,12 +211,14 @@ local escape_tests = { | |||
211 | local locale_tests = { | 211 | local locale_tests = { |
212 | function () | 212 | function () |
213 | os.setlocale("cs_CZ") | 213 | os.setlocale("cs_CZ") |
214 | cjson.update_locale() | ||
214 | return "Setting locale to cs_CZ (comma separator)" | 215 | return "Setting locale to cs_CZ (comma separator)" |
215 | end, | 216 | end, |
216 | { json.encode, { 1.5 }, true, { '1.5' } }, | 217 | { json.encode, { 1.5 }, true, { '1.5' } }, |
217 | { json.decode, { "[ 10, \"test\" ]" }, true, { { 10, "test" } } }, | 218 | { json.decode, { "[ 10, \"test\" ]" }, true, { { 10, "test" } } }, |
218 | function () | 219 | function () |
219 | os.setlocale("C") | 220 | os.setlocale("C") |
221 | cjson.update_locale() | ||
220 | return "Reverting locale to POSIX" | 222 | return "Reverting locale to POSIX" |
221 | end | 223 | end |
222 | } | 224 | } |