diff options
-rw-r--r-- | src/jsonf.lua | 326 | ||||
-rw-r--r-- | src/lua.c | 2 |
2 files changed, 328 insertions, 0 deletions
diff --git a/src/jsonf.lua b/src/jsonf.lua new file mode 100644 index 0000000..ecf1eea --- /dev/null +++ b/src/jsonf.lua @@ -0,0 +1,326 @@ +-- +-- variant of https://github.com/rxi/json.lua decoding from channels of +-- characters rather than strings +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local jsonf = { _version = "0.1.2" } + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function skip_spaces(infile) + while true do + local c = infile:recv() + if c == nil then break end + if space_chars[c] == nil then return c end + end + return nil +end + + +local function next_chars(infile, set, firstc) + local res = {firstc} + local nextc + while true do + nextc = infile:recv() + if nextc == nil then break end + if set[nextc] then break end + table.insert(res, nextc) + end + return table.concat(res), nextc +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(infile, firstc) + local res = {} + + while true do + local chr = infile:recv() + if chr == nil then break end + local x = chr:byte() + + if x < 32 then + error("control character in string") + elseif c == '\\' then + local c = infile:recv() + if c == nil then break end + if c == "u" then + local hex = '' + c = infile:recv() + if c == nil then break end + hex = hex..c + c = infile:recv() + if c == nil then break end + hex = hex..c + c = infile:recv() + if c == nil then break end + hex = hex..c + c = infile:recv() + if c == nil then break end + hex = hex..c + if not hex:match('^%x%x%x%x') then + error('invalid unicode escape in string') + end + table.insert(res, parse_unicode_escape(hex)) + else + if not escape_chars[c] then + error("invalid escape char '" .. c .. "' in string") + end + table.insert(escape_char_map_inv[c]) + end + elseif chr == '"' then + return table.concat(res), infile:recv() + else + table.insert(res, chr) + end + end + + error("expected closing quote for string") +end + + +local function parse_number(infile, firstc) +--? print('parse_number') + local res = {firstc} + local nextc + while true do + nextc = infile:recv() + if nextc == nil then break end + if delim_chars[nextc] then break end + table.insert(res, nextc) + end + local s = table.concat(res) +--? print('parse_number: '..s) + local n = tonumber(s) + if not n then + error("invalid number '" .. s .. "'") + end + return n, nextc +end + + +local function parse_literal(infile, firstc) +--? print('parse_literal') + local word, nextc = next_chars(infile, delim_chars, firstc) + if not literals[word] then + error("invalid literal '" .. word .. "'") + end +--? print('parse_literal: '..word) + return literal_map[word], nextc +end + + +local function parse_array(infile, firstc) + local res = {} + local x, nextc + while true do + nextc = skip_spaces(infile) + if nextc == nil then + error("expected ']' or ','") + end + if nextc == ']' then break end -- empty array + -- Read token + x, nextc = parse(infile, nextc) +--? print('array elem: '..str(x)) + table.insert(res, x) + -- Next token + if space_chars[nextc] then + nextc = skip_spaces(infile) + end + if nextc == ']' then break end + if nextc ~= ',' then + error("expected ']' or ','") + end + end + return res, skip_spaces(infile) +end + + +local function parse_object(infile, firstc) + local res = {} + local nextc + while true do + local key, val + nextc = skip_spaces(infile) + if nextc == nil then + error("expected '}' or ','") + end + if nextc == '}' then break end -- empty object + -- Read key + if nextc ~= '"' then + error("expected string for key") + end + key, nextc = parse(infile, nextc) +--? print('object key: '..key) + -- Read ':' delimiter + if space_chars[nextc] then + nextc = skip_spaces(infile) + end + if nextc ~= ':' then + error("expected ':' after key") + end + -- Read value + nextc = skip_spaces(infile) + val, nextc = parse(infile, nextc) +--? print('object val: '..str(val)) + -- Set + res[key] = val + -- Next token + if space_chars[nextc] then + nextc = skip_spaces(infile) + end + if nextc == '}' then break end + if nextc ~= ',' then + error("expected '}' or ','") + end + end + return res, skip_spaces(infile) +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(infile, chr) + local f = char_func_map[chr] + if f then + return f(infile, chr) + end + error("unexpected character '" .. chr .. "'") +end + + +function jsonf.decode(infile) + return decode2(character_by_character(infile)) +end + + +function decode2(infile) + if not ischannel(infile) then + error("expected channel, got " .. type(f)) + end + local firstc = skip_spaces(infile) + local res, nextc = parse(infile, firstc) + if nextc then + error("trailing garbage") + end + return res +end + + +-- test cases: +-- "abc" +-- 234 +-- true +-- false +-- nil +-- ["abc", 234, true, false, nil] +-- ["abc", 234, true, false, nil +-- ["abc", +-- {"abc": 234, "def": true} + +return jsonf diff --git a/src/lua.c b/src/lua.c index a553315..fab1ce3 100644 --- a/src/lua.c +++ b/src/lua.c @@ -234,6 +234,8 @@ static int pmain (lua_State *L) { if (status != 0) return 0; status = dorequire(L, "src/json.lua", "json"); if (status != 0) return 0; + status = dorequire(L, "src/jsonf.lua", "jsonf"); + if (status != 0) return 0; status = dorequire(L, "src/task.lua", "task"); if (status != 0) return 0; status = dorequire(L, "src/file.lua", "file"); |