diff --git a/Makefile b/Makefile index 16a39a04e..f66dbc439 100644 --- a/Makefile +++ b/Makefile @@ -145,6 +145,7 @@ include tool/args/args.mk include test/tool/args/test.mk include third_party/linenoise/linenoise.mk include third_party/maxmind/maxmind.mk +include third_party/double-conversion/double-conversion.mk include third_party/lua/lua.mk include third_party/make/make.mk include third_party/finger/finger.mk @@ -156,7 +157,6 @@ include third_party/quickjs/quickjs.mk include third_party/lz4cli/lz4cli.mk include third_party/zip/zip.mk include third_party/unzip/unzip.mk -include third_party/double-conversion/double-conversion.mk include tool/build/lib/buildlib.mk include third_party/chibicc/chibicc.mk include third_party/chibicc/test/test.mk diff --git a/libc/fmt/atoi.c b/libc/fmt/atoi.c index e4893db3e..58f3522be 100644 --- a/libc/fmt/atoi.c +++ b/libc/fmt/atoi.c @@ -1,5 +1,5 @@ /*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ -│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ +'-'│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ ╞══════════════════════════════════════════════════════════════════════════════╡ │ Copyright 2020 Justine Alexandra Roberts Tunney │ │ │ diff --git a/test/tool/net/lfuncs_test.lua b/test/tool/net/lfuncs_test.lua index bd414ceff..d4f25f304 100644 --- a/test/tool/net/lfuncs_test.lua +++ b/test/tool/net/lfuncs_test.lua @@ -170,5 +170,5 @@ function JsonSerialization() EncodeJson({2, 1, 10, 3, "hello"}) end -print(Benchmark(LuaSerialization), "LuaSerialization") -print(Benchmark(JsonSerialization), "JsonSerialization") +print("LuaSerialization", Benchmark(LuaSerialization)) +print("JsonSerialization", Benchmark(JsonSerialization)) diff --git a/test/tool/net/ljson_test.c b/test/tool/net/ljson_test.c new file mode 100644 index 000000000..e69de29bb diff --git a/test/tool/net/ljson_test.lua b/test/tool/net/ljson_test.lua new file mode 100644 index 000000000..e7de68024 --- /dev/null +++ b/test/tool/net/ljson_test.lua @@ -0,0 +1,68 @@ +-- Copyright 2022 Justine Alexandra Roberts Tunney +-- +-- Permission to use, copy, modify, and/or distribute this software for +-- any purpose with or without fee is hereby granted, provided that the +-- above copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +-- DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +-- PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +-- TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +assert(EncodeLua(ParseJson[[ ]]) == 'nil') +assert(EncodeLua(ParseJson[[ 0 ]]) == '0' ) +assert(EncodeLua(ParseJson[[ [1] ]]) == '{1}') +assert(EncodeLua(ParseJson[[ 2.3 ]]) == '2.3') +assert(EncodeLua(ParseJson[[ [1,3,2] ]]) == '{1, 3, 2}') +assert(EncodeLua(ParseJson[[ {1: 2, 3: 4} ]]) == '{[1]=2, [3]=4}') +assert(EncodeLua(ParseJson[[ {"foo": 2, "bar": 4} ]]) == '{bar=4, foo=2}') +assert(EncodeLua(ParseJson[[ -123 ]]) == '-123') +assert(EncodeLua(ParseJson[[ 1.e6 ]]) == '1000000') +assert(EncodeLua(ParseJson[[ 1.e-6 ]]) == '1e-06') +assert(EncodeLua(ParseJson[[ 1e-06 ]]) == '1e-06') +assert(EncodeLua(ParseJson[[ 9.123e6 ]]) == '9123000') +assert(EncodeLua(ParseJson[[ [{"heh": [1,3,2]}] ]]) == '{{heh={1, 3, 2}}}') +assert(EncodeLua(ParseJson[[ 3.14159 ]]) == '3.14159') +assert(EncodeLua(ParseJson[[ {3=4} ]]) == '{[3]=4}') +assert(EncodeLua(ParseJson[[ 1e-12 ]]) == '1e-12') +assert(EncodeJson(ParseJson[[ 1e-12 ]]) == '1e-12') + +---------------------------------------------------------------------------------------------------- +-- benchmarks + +function JsonParseEmpty() + ParseJson[[]] +end + +function JsonParseInt() + ParseJson[[ 314159 ]] +end + +function JsonParseDouble() + ParseJson[[ 3.14159 ]] +end + +function JsonParseArray() + ParseJson[[ [3,1,4,1,5,9] ]] +end + +function JsonParseObject() + ParseJson[[ {"3":"1","4":"1","5":"9"} ]] +end + +print('JsonParseEmpty', Benchmark(JsonParseEmpty)) +print('JsonParseInt', Benchmark(JsonParseInt)) +print('JsonParseDouble', Benchmark(JsonParseDouble)) +print('JsonParseArray', Benchmark(JsonParseArray)) +print('JsonParseObject', Benchmark(JsonParseObject)) + +-- nanos ticks +-- JsonParseEmpty 24 77 85 1 +-- JsonParseInt 31 96 82 1 +-- JsonParseDouble 64 199 82 1 +-- JsonParseArray 367 1139 80 1 +-- JsonParseObject 425 1317 79 1 diff --git a/third_party/lua/lua.mk b/third_party/lua/lua.mk index 22c713a8c..f23af6602 100644 --- a/third_party/lua/lua.mk +++ b/third_party/lua/lua.mk @@ -132,6 +132,7 @@ THIRD_PARTY_LUA_A_DIRECTDEPS = \ LIBC_UNICODE \ NET_HTTP \ THIRD_PARTY_LINENOISE \ + THIRD_PARTY_DOUBLECONVERSION \ THIRD_PARTY_GDTOA THIRD_PARTY_LUA_A_DEPS := \ diff --git a/third_party/lua/luaencodejsondata.c b/third_party/lua/luaencodejsondata.c index f3235e981..bb08fe303 100644 --- a/third_party/lua/luaencodejsondata.c +++ b/third_party/lua/luaencodejsondata.c @@ -24,7 +24,9 @@ #include "libc/runtime/gc.internal.h" #include "libc/stdio/append.internal.h" #include "libc/stdio/strlist.internal.h" +#include "libc/str/str.h" #include "net/http/escape.h" +#include "third_party/double-conversion/wrapper.h" #include "third_party/lua/cosmo.h" #include "third_party/lua/lauxlib.h" #include "third_party/lua/lua.h" @@ -38,7 +40,7 @@ static int LuaEncodeJsonDataImpl(lua_State *L, char **buf, int level, bool isarray; size_t tbllen, i, z; struct StrList sl = {0}; - char ibuf[21], fmt[] = "%.14g"; + char ibuf[128], fmt[] = "%.14g"; if (level > 0) { switch (lua_type(L, idx)) { @@ -66,22 +68,8 @@ static int LuaEncodeJsonDataImpl(lua_State *L, char **buf, int level, RETURN_ON_ERROR(appendd( buf, ibuf, FormatInt64(ibuf, luaL_checkinteger(L, idx)) - ibuf)); } else { - // TODO(jart): replace this api - while (*numformat == '%' || *numformat == '.' || - isdigit(*numformat)) { - ++numformat; - } - switch (*numformat) { - case 'a': - case 'g': - case 'f': - fmt[4] = *numformat; - break; - default: - // prevent format string hacking - goto OnError; - } - RETURN_ON_ERROR(appendf(buf, fmt, lua_tonumber(L, idx))); + RETURN_ON_ERROR( + appends(buf, DoubleToEcmascript(ibuf, lua_tonumber(L, idx)))); } return 0; diff --git a/tool/net/help.txt b/tool/net/help.txt index c6735ad98..820f44e3a 100644 --- a/tool/net/help.txt +++ b/tool/net/help.txt @@ -673,6 +673,21 @@ FUNCTIONS URIs that do things like embed a PNG file in a web page. See encodebase64.c. + ParseJson(input:str) + ├─→ value:* + ├─→ true [if useoutput] + └─→ nil, error:str + + Turns JSON string into a Lua data structure. + + This is a very permissive parser. That means it should always + parse correctly formatted JSON correctly. However it will not + complain if the `input` string is weirdly formatted. There is + currently no validation performed, other than what we need to + ensure security. For example `{3=4}` will decode as `{[3]=4}` + even though that structure won't round-trip with `EncodeJson` + since redbean won't generate invalid JSON (see Postel's Law). + EncodeJson(value[,options:table]) ├─→ json:str ├─→ true [if useoutput] diff --git a/tool/net/ljson.c b/tool/net/ljson.c new file mode 100644 index 000000000..731910468 --- /dev/null +++ b/tool/net/ljson.c @@ -0,0 +1,222 @@ +/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ +│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ +╞══════════════════════════════════════════════════════════════════════════════╡ +│ Copyright 2022 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/errno.h" +#include "libc/limits.h" +#include "libc/math.h" +#include "libc/str/str.h" +#include "libc/str/tpenc.h" +#include "third_party/lua/lauxlib.h" +#include "third_party/lua/lua.h" + +struct Rc { + int t; + const char *p; +}; + +static int Accumulate(int x, int c, int d) { + if (!__builtin_mul_overflow(x, 10, &x) && + !__builtin_add_overflow(x, (c - '0') * d, &x)) { + return x; + } else { + errno = ERANGE; + if (d > 0) { + return INT_MAX; + } else { + return INT_MIN; + } + } +} + +static struct Rc Parse(struct lua_State *L, const char *p, const char *e) { + uint64_t w; + struct Rc r; + luaL_Buffer b; + int A, B, C, D; + int c, d, i, j, x, y, z; + for (d = +1; p < e;) { + switch ((c = *p++ & 255)) { + + case '-': + d = -1; + break; + + case ']': + case '}': + return (struct Rc){0, p}; + + case '[': + lua_newtable(L); + i = 0; + do { + r = Parse(L, p, e); + p = r.p; + if (r.t) { + lua_rawseti(L, -2, i++ + 1); + } + } while (r.t); + return (struct Rc){1, p}; + + case '{': + lua_newtable(L); + i = 0; + do { + r = Parse(L, p, e); + p = r.p; + if (r.t) { + r = Parse(L, p, e); + p = r.p; + if (!r.t) { + lua_pushnil(L); + } + lua_settable(L, -3); + } + } while (r.t); + return (struct Rc){1, p}; + + case '"': + luaL_buffinit(L, &b); + while (p < e) { + switch ((c = *p++ & 255)) { + default: + AddChar: + luaL_addchar(&b, c); + break; + case '\\': + if (p < e) { + switch ((c = *p++ & 255)) { + case '"': + case '/': + case '\\': + default: + goto AddChar; + case 'b': + c = '\b'; + goto AddChar; + case 'f': + c = '\f'; + goto AddChar; + case 'n': + c = '\n'; + goto AddChar; + case 'r': + c = '\r'; + goto AddChar; + case 't': + c = '\t'; + goto AddChar; + case 'u': + if (p + 4 <= e && // + (A = kHexToInt[p[0] & 255]) != -1 && // + (B = kHexToInt[p[1] & 255]) != -1 && // + (C = kHexToInt[p[2] & 255]) != -1 && // + (D = kHexToInt[p[3] & 255]) != -1) { + p += 4; + c = A << 12 | B << 8 | C << 4 | D; + w = tpenc(c); + do { + luaL_addchar(&b, w & 255); + } while ((w >>= 8)); + break; + } else { + goto AddChar; + } + } + } + break; + case '"': + goto FinishString; + } + } + FinishString: + luaL_pushresult(&b); + return (struct Rc){1, p}; + + case '0' ... '9': + for (x = (c - '0') * d; p < e; ++p) { + c = *p & 255; + if (isdigit(c)) { + x = Accumulate(x, c, d); + } else if (c == '.') { + ++p; + goto Fraction; + } else if (c == 'e' || c == 'E') { + ++p; + j = 0; + y = 0; + goto Exponent; + } else { + break; + } + } + lua_pushinteger(L, x); + return (struct Rc){1, p}; + + Fraction: + for (j = y = 0; p < e; ++p) { + c = *p & 255; + if (isdigit(c)) { + --j; + y = Accumulate(y, c, d); + } else if (c == 'e' || c == 'E') { + ++p; + goto Exponent; + } else { + break; + } + } + lua_pushnumber(L, x + y * exp10(j)); + return (struct Rc){1, p}; + + Exponent: + d = +1; + for (z = 0; p < e; ++p) { + c = *p & 255; + if (isdigit(c)) { + z = Accumulate(z, c, d); + } else if (c == '-') { + d = -1; + } else if (c == '+') { + d = +1; + } else { + break; + } + } + lua_pushnumber(L, (x + y * exp10(j)) * exp10(z)); + return (struct Rc){1, p}; + + case ',': + case ':': + case ' ': + case '\n': + case '\r': + case '\t': + default: + break; + } + } + return (struct Rc){0, p}; +} + +/** + * Parses JSON data structure string into a Lua data structure. + */ +int ParseJson(struct lua_State *L, const char *p, size_t n) { + if (n == -1) n = p ? strlen(p) : 0; + return Parse(L, p, p + n).t; +} diff --git a/tool/net/ljson.h b/tool/net/ljson.h new file mode 100644 index 000000000..e11b371af --- /dev/null +++ b/tool/net/ljson.h @@ -0,0 +1,11 @@ +#ifndef COSMOPOLITAN_TOOL_NET_LJSON_H_ +#define COSMOPOLITAN_TOOL_NET_LJSON_H_ +#include "third_party/lua/lauxlib.h" +#if !(__ASSEMBLER__ + __LINKER__ + 0) +COSMOPOLITAN_C_START_ + +int ParseJson(struct lua_State *, const char *, size_t); + +COSMOPOLITAN_C_END_ +#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */ +#endif /* COSMOPOLITAN_TOOL_NET_LJSON_H_ */ diff --git a/tool/net/net.mk b/tool/net/net.mk index fc8f355e5..20821b511 100644 --- a/tool/net/net.mk +++ b/tool/net/net.mk @@ -94,6 +94,7 @@ o/$(MODE)/tool/net/redbean.com.dbg: \ o/$(MODE)/tool/net/redbean.o \ o/$(MODE)/tool/net/lfuncs.o \ o/$(MODE)/tool/net/lre.o \ + o/$(MODE)/tool/net/ljson.o \ o/$(MODE)/tool/net/lmaxmind.o \ o/$(MODE)/tool/net/lsqlite3.o \ o/$(MODE)/tool/net/largon2.o \ @@ -212,6 +213,7 @@ o/$(MODE)/tool/net/redbean-demo.com.dbg: \ o/$(MODE)/tool/net/redbean.o \ o/$(MODE)/tool/net/lfuncs.o \ o/$(MODE)/tool/net/lre.o \ + o/$(MODE)/tool/net/ljson.o \ o/$(MODE)/tool/net/lmaxmind.o \ o/$(MODE)/tool/net/lsqlite3.o \ o/$(MODE)/tool/net/largon2.o \ @@ -328,6 +330,7 @@ o/$(MODE)/tool/net/redbean-unsecure.com.dbg: \ o/$(MODE)/tool/net/redbean-unsecure.o \ o/$(MODE)/tool/net/lfuncs.o \ o/$(MODE)/tool/net/lre.o \ + o/$(MODE)/tool/net/ljson.o \ o/$(MODE)/tool/net/lmaxmind.o \ o/$(MODE)/tool/net/lsqlite3.o \ o/$(MODE)/tool/net/largon2.o \ diff --git a/tool/net/redbean.c b/tool/net/redbean.c index 8167df430..4b235eb57 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -112,6 +112,7 @@ #include "tool/args/args.h" #include "tool/build/lib/case.h" #include "tool/net/lfuncs.h" +#include "tool/net/ljson.h" #include "tool/net/luacheck.h" #include "tool/net/sandbox.h" @@ -4258,6 +4259,13 @@ static int LuaEncodeLua(lua_State *L) { return LuaEncodeSmth(L, LuaEncodeLuaData); } +static int LuaParseJson(lua_State *L) { + size_t n; + const char *p; + p = luaL_checklstring(L, 1, &n); + return ParseJson(L, p, n); +} + static int LuaGetUrl(lua_State *L) { char *p; size_t n; @@ -5150,6 +5158,7 @@ static const luaL_Reg kLuaFuncs[] = { {"ParseHost", LuaParseHost}, // {"ParseHttpDateTime", LuaParseHttpDateTime}, // {"ParseIp", LuaParseIp}, // + {"ParseJson", LuaParseJson}, // {"ParseParams", LuaParseParams}, // {"ParseUrl", LuaParseUrl}, // {"Popcnt", LuaPopcnt}, //