From 0f3ab84a261292d16f684551e67f2f007199936a Mon Sep 17 00:00:00 2001 From: Mark Pulford <mark@kyne.com.au> Date: Wed, 5 Oct 2011 23:30:27 +1030 Subject: Support locales which use comma decimal separators Some locales (cs_CZ, de_DE,..) use a comma as their decimal separator. This causes CJSON to generate incorrect JSON (Eg, [10,1]), and fail when parsing some valid JSON (Eg, [10,"test"]). Added USE_POSIX_LOCALE #define which harnesses the thread-safe POSIX.1-2008 locale support (newlocale(), uselocale(), freelocale()) to temporarily use the POSIX locale during JSON conversion. Some older POSIX operating systems with xlocale.h (MacOSX) are also supported. --- Makefile | 10 +++++++--- NEWS | 3 +++ TODO | 2 ++ lua_cjson.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.lua | 16 ++++++++++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d34ff6d..5731440 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,17 @@ LDFLAGS += -shared LUA_INCLUDE_DIR ?= $(PREFIX)/include LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) -# Some versions of Solaris are missing isinf(). Add -DMISSING_ISINF to -# CFLAGS to work around this bug. - #CFLAGS ?= -g -Wall -pedantic -fno-inline CFLAGS ?= -g -O3 -Wall -pedantic override CFLAGS += -fpic -I$(LUA_INCLUDE_DIR) -DVERSION=\"$(CJSON_VERSION)\" +## Conditional work arounds +# Handle Solaris platforms that are missing isinf(). +#override CFLAGS += -DMISSING_ISINF +# Handle locales that use comma as a decimal separator on locale aware +# platforms. Requires POSIX-1.2008 support. +override CFLAGS += -DUSE_POSIX_LOCALE + INSTALL ?= install .PHONY: all clean install package diff --git a/NEWS b/NEWS index 3527b04..634d9af 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,6 @@ +Version 1.0.4 (?) +* Handle locales with a comma decimal separator + Version 1.0.3 (Sep 15 2011) * Fixed detection of objects with numeric string keys * Provided work around for missing isinf() on Solaris diff --git a/TODO b/TODO index 1345448..9dbde9c 100644 --- a/TODO +++ b/TODO @@ -2,3 +2,5 @@ - Optionally create an object for settings. Clone function. - Convert documentation into structured source format + +- Add setlocale() support for non-POSIX 2008 operating systems diff --git a/lua_cjson.c b/lua_cjson.c index f765883..151fa39 100644 --- a/lua_cjson.c +++ b/lua_cjson.c @@ -44,7 +44,30 @@ #include "strbuf.h" +#ifdef USE_POSIX_LOCALE +/* Reset locale to POSIX for strtod() / sprintf(). + * Some locales use comma as a decimal separator. This breaks JSON. */ + +/* unistd.h defines _POSIX_VERSION */ +#include <unistd.h> +#if _POSIX_VERSION >= 200809L +/* POSIX.1-2008 adds threadsafe locale support */ +#include <locale.h> +#elif defined(_POSIX_VERSION) +/* Some pre-POSIX.1-2008 operating systems use xlocale.h instead */ +#include <xlocale.h> +#else +#error Missing _POSIX_VERSION define +#endif +#define LOCALE_SET_POSIX(x) (x)->saved_locale = uselocale((x)->posix_locale) +#define LOCALE_RESTORE(x) uselocale((x)->saved_locale) +#else +#define LOCALE_SET_POSIX(x) do { } while(0) +#define LOCALE_RESTORE(x) do { } while(0) +#endif + #ifdef MISSING_ISINF +/* Some Solaris platforms are missing isinf(). Define here. */ #define isinf(x) (!isnan(x) && isnan((x) - (x))) #endif @@ -99,6 +122,10 @@ typedef struct { char *char2escape[256]; /* Encoding */ #endif strbuf_t encode_buf; +#if USE_POSIX_LOCALE + locale_t saved_locale; + locale_t posix_locale; +#endif char number_fmt[8]; /* "%.XXg\0" */ int current_depth; @@ -342,6 +369,10 @@ static int json_destroy_config(lua_State *l) json_config_t *cfg; cfg = lua_touserdata(l, 1); +#ifdef USE_POSIX_LOCALE + if (cfg->posix_locale) + freelocale(cfg->posix_locale); +#endif if (cfg) strbuf_free(&cfg->encode_buf); cfg = NULL; @@ -363,6 +394,13 @@ static void json_create_config(lua_State *l) lua_setmetatable(l, -2); strbuf_init(&cfg->encode_buf, 0); +#if USE_POSIX_LOCALE + cfg->saved_locale = NULL; + /* Must not lua_error() before cfg->posix_locale has been initialised */ + cfg->posix_locale = newlocale(LC_ALL_MASK, "C", NULL); + if (!cfg->posix_locale) + luaL_error(l, "Failed to create POSIX locale for JSON"); +#endif cfg->encode_sparse_convert = DEFAULT_SPARSE_CONVERT; cfg->encode_sparse_ratio = DEFAULT_SPARSE_RATIO; @@ -452,6 +490,7 @@ static void json_encode_exception(lua_State *l, json_config_t *cfg, int lindex, { if (!cfg->encode_keep_buffer) strbuf_free(&cfg->encode_buf); + LOCALE_RESTORE(cfg); luaL_error(l, "Cannot serialise %s: %s", lua_typename(l, lua_type(l, lindex)), reason); } @@ -542,6 +581,7 @@ static void json_encode_descend(lua_State *l, json_config_t *cfg) if (cfg->current_depth > cfg->encode_max_depth) { if (!cfg->encode_keep_buffer) strbuf_free(&cfg->encode_buf); + LOCALE_RESTORE(cfg); luaL_error(l, "Cannot serialise, excessive nesting (%d)", cfg->current_depth); } @@ -701,9 +741,13 @@ static int json_encode(lua_State *l) else strbuf_init(&cfg->encode_buf, 0); + LOCALE_SET_POSIX(cfg); + json_append_data(l, cfg, &cfg->encode_buf); json = strbuf_string(&cfg->encode_buf, &len); + LOCALE_RESTORE(cfg); + lua_pushlstring(l, json, len); if (!cfg->encode_keep_buffer) @@ -1084,6 +1128,8 @@ static void json_throw_parse_error(lua_State *l, json_parse_t *json, else found = json_token_type_name[token->type]; + LOCALE_RESTORE(json->cfg); + /* Note: token->index is 0 based, display starting from 1 */ luaL_error(l, "Expected %s but found %s at character %d", exp, found, token->index + 1); @@ -1095,6 +1141,7 @@ static void json_decode_checkstack(lua_State *l, json_parse_t *json, int n) return; strbuf_free(json->tmp); + LOCALE_RESTORE(json->cfg); luaL_error(l, "Too many nested data structures"); } @@ -1224,6 +1271,8 @@ static void lua_json_decode(lua_State *l, const char *json_text, int json_len) * string must be smaller than the entire json string */ json.tmp = strbuf_new(json_len); + LOCALE_SET_POSIX(json.cfg); + json_next_token(&json, &token); json_process_value(l, &json, &token); @@ -1233,6 +1282,8 @@ static void lua_json_decode(lua_State *l, const char *json_text, int json_len) if (token.type != T_END) json_throw_parse_error(l, &json, "the end", &token); + LOCALE_RESTORE(json.cfg); + strbuf_free(json.tmp); } diff --git a/tests/test.lua b/tests/test.lua index 7a75243..d80dcf0 100755 --- a/tests/test.lua +++ b/tests/test.lua @@ -210,6 +210,21 @@ local escape_tests = { { json.decode, { utf16_escaped }, true, { utf8_raw } } } +-- The standard Lua interpreter is ANSI C online doesn't support locales +-- by default. Force a known problematic locale to test strtod()/sprintf(). +local locale_tests = { + function () + os.setlocale("cs_CZ") + return "Setting locale to cs_CZ (comma separator)" + end, + { json.encode, { 1.5 }, true, { '1.5' } }, + { json.decode, { "[ 10, \"test\" ]" }, true, { { 10, "test" } } }, + function () + os.setlocale("C") + return "Reverting locale to POSIX" + end +} + print(string.format("Testing CJSON v%s\n", cjson.version)) run_test_group("decode simple value", decode_simple_tests) @@ -225,6 +240,7 @@ run_test_group("encode table", encode_table_tests) run_test_group("decode error", decode_error_tests) run_test_group("encode error", encode_error_tests) run_test_group("escape", escape_tests) +run_test_group("locale", locale_tests) cjson.refuse_invalid_numbers(false) cjson.encode_max_depth(20) -- cgit v1.2.3-55-g6feb