aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Pulford <mark@kyne.com.au>2011-12-30 14:17:44 +1030
committerMark Pulford <mark@kyne.com.au>2011-12-30 14:17:44 +1030
commit2416b145073211b840781da6abf4b6d97f4657a6 (patch)
tree6e92a13a7cc8ef8357245bc3ef320f5841350991
parent6cc88e3ac5275868e168acaf60203563f131355b (diff)
downloadlua-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.txt2
-rw-r--r--Makefile2
-rw-r--r--fpconv.c155
-rw-r--r--fpconv.h11
-rw-r--r--lua-cjson-1.0devel-1.rockspec4
-rw-r--r--lua_cjson.c36
-rw-r--r--strbuf.h14
-rwxr-xr-xtests/test.lua2
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")
31endif() 31endif()
32 32
33add_library(cjson MODULE lua_cjson.c strbuf.c) 33add_library(cjson MODULE lua_cjson.c strbuf.c fpconv.c)
34set_target_properties(cjson PROPERTIES PREFIX "") 34set_target_properties(cjson PROPERTIES PREFIX "")
35install(TARGETS cjson DESTINATION "${_lua_module_dir}") 35install(TARGETS cjson DESTINATION "${_lua_module_dir}")
36 36
diff --git a/Makefile b/Makefile
index 7685363..fb63d99 100644
--- a/Makefile
+++ b/Makefile
@@ -35,7 +35,7 @@ INSTALL_CMD = install
35## End platform specific section 35## End platform specific section
36 36
37BUILD_CFLAGS = -fpic -I$(LUA_INCLUDE_DIR) $(CJSON_CFLAGS) 37BUILD_CFLAGS = -fpic -I$(LUA_INCLUDE_DIR) $(CJSON_CFLAGS)
38OBJS := lua_cjson.o strbuf.o 38OBJS := 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
8static 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. */
16void 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. */
36static 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 */
57static 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 */
69double 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 */
108static 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 */
129int 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
6extern void fpconv_update_locale();
7extern int fpconv_g_fmt(char*, double, int);
8extern 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
64typedef enum { 66typedef 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
256static 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 */
263static int json_cfg_encode_number_precision(lua_State *l) 258static 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
340static int json_update_locale(lua_State *l)
341{
342 fpconv_update_locale();
343
344 return 0;
345}
346
345static int json_destroy_config(lua_State *l) 347static 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);
diff --git a/strbuf.h b/strbuf.h
index f856543..fbc8651 100644
--- a/strbuf.h
+++ b/strbuf.h
@@ -62,7 +62,9 @@ extern void strbuf_resize(strbuf_t *s, int len);
62static int strbuf_empty_length(strbuf_t *s); 62static int strbuf_empty_length(strbuf_t *s);
63static int strbuf_length(strbuf_t *s); 63static int strbuf_length(strbuf_t *s);
64static char *strbuf_string(strbuf_t *s, int *len); 64static char *strbuf_string(strbuf_t *s, int *len);
65static void strbuf_ensure_empty_length(strbuf_t *s, int len); 65static void strbuf_ensure_empty_length(strbuf_t *s, int len);
66static char *strbuf_empty_ptr(strbuf_t *s);
67static void strbuf_extend_length(strbuf_t *s, int len);
66 68
67/* Update */ 69/* Update */
68extern void strbuf_append_fmt(strbuf_t *s, int len, const char *fmt, ...); 70extern 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
101static inline char *strbuf_empty_ptr(strbuf_t *s)
102{
103 return s->buf + s->length;
104}
105
106static inline void strbuf_extend_length(strbuf_t *s, int len)
107{
108 s->length += len;
109}
110
99static inline int strbuf_length(strbuf_t *s) 111static 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 = {
211local locale_tests = { 211local 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}