-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local type = type; local t_insert, t_concat, t_remove = table.insert, table.concat, table.remove; local s_char = string.char; local tostring, tonumber = tostring, tonumber; local pairs, ipairs, spairs = pairs, ipairs, require "prosody.util.iterators".sorted_pairs; local next = next; local getmetatable, setmetatable = getmetatable, setmetatable; local print = print; local has_array, array = pcall(require, "prosody.util.array"); local array_mt = has_array and getmetatable(array()) or {}; --module("json") local module = {}; local null = setmetatable({}, { __tostring = function() return "null"; end; }); module.null = null; local escapes = { ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"}; for i=0,31 do local ch = s_char(i); if not escapes[ch] then escapes[ch] = ("\\u%.4X"):format(i); end end local function codepoint_to_utf8(code) if code < 0x80 then return s_char(code); end local bits0_6 = code % 64; if code < 0x800 then local bits6_5 = (code - bits0_6) / 64; return s_char(0x80 + 0x40 + bits6_5, 0x80 + bits0_6); end local bits0_12 = code % 4096; local bits6_6 = (bits0_12 - bits0_6) / 64; local bits12_4 = (code - bits0_12) / 4096; return s_char(0x80 + 0x40 + 0x20 + bits12_4, 0x80 + bits6_6, 0x80 + bits0_6); end local valid_types = { number = true, string = true, table = true, boolean = true }; local special_keys = { __array = true; __hash = true; }; local simplesave, tablesave, arraysave, stringsave; function stringsave(o, buffer) -- FIXME do proper utf-8 and binary data detection t_insert(buffer, "\""..(o:gsub(".", escapes)).."\""); end function arraysave(o, buffer) t_insert(buffer, "["); if next(o) then for _, v in ipairs(o) do simplesave(v, buffer); t_insert(buffer, ","); end t_remove(buffer); end t_insert(buffer, "]"); end function tablesave(o, buffer) local __array = {}; local __hash = {}; local hash = {}; for i,v in ipairs(o) do __array[i] = v; end for k,v in pairs(o) do local ktype, vtype = type(k), type(v); if valid_types[vtype] or v == null then if ktype == "string" and not special_keys[k] then hash[k] = v; elseif (valid_types[ktype] or k == null) and __array[k] == nil then __hash[k] = v; end end end if next(__hash) ~= nil or next(hash) ~= nil or next(__array) == nil then t_insert(buffer, "{"); local mark = #buffer; local _pairs = buffer.ordered and spairs or pairs; for k,v in _pairs(hash) do stringsave(k, buffer); t_insert(buffer, ":"); simplesave(v, buffer); t_insert(buffer, ","); end if next(__hash) ~= nil then t_insert(buffer, "\"__hash\":["); for k,v in pairs(__hash) do simplesave(k, buffer); t_insert(buffer, ","); simplesave(v, buffer); t_insert(buffer, ","); end t_remove(buffer); t_insert(buffer, "]"); t_insert(buffer, ","); end if next(__array) then t_insert(buffer, "\"__array\":"); arraysave(__array, buffer); t_insert(buffer, ","); end if mark ~= #buffer then t_remove(buffer); end t_insert(buffer, "}"); else arraysave(__array, buffer); end end function simplesave(o, buffer) local t = type(o); if o == null then t_insert(buffer, "null"); elseif t == "number" then t_insert(buffer, tostring(o)); elseif t == "string" then stringsave(o, buffer); elseif t == "table" then local mt = getmetatable(o); if mt == array_mt then arraysave(o, buffer); else tablesave(o, buffer); end elseif t == "boolean" then t_insert(buffer, (o and "true" or "false")); else t_insert(buffer, "null"); end end function module.encode(obj) local t = {}; simplesave(obj, t); return t_concat(t); end function module.encode_ordered(obj) local t = { ordered = true }; simplesave(obj, t); return t_concat(t); end function module.encode_array(obj) local t = {}; arraysave(obj, t); return t_concat(t); end ----------------------------------- local function _skip_whitespace(json, index) return json:find("[^ \t\r\n]", index) or index; -- no need to check \r\n, we converted those to \t end local function _fixobject(obj) local __array = obj.__array; if __array then obj.__array = nil; for _, v in ipairs(__array) do t_insert(obj, v); end end local __hash = obj.__hash; if __hash then obj.__hash = nil; local k; for _, v in ipairs(__hash) do if k ~= nil then obj[k] = v; k = nil; else k = v; end end end return obj; end local _readvalue, _readstring; local function _readobject(json, index) local o = {}; while true do local key, val; index = _skip_whitespace(json, index + 1); if json:byte(index) ~= 0x22 then -- "\"" if json:byte(index) == 0x7d then return o, index + 1; end -- "}" return nil, "key expected"; end key, index = _readstring(json, index); if key == nil then return nil, index; end index = _skip_whitespace(json, index); if json:byte(index) ~= 0x3a then return nil, "colon expected"; end -- ":" val, index = _readvalue(json, index + 1); if val == nil then return nil, index; end o[key] = val; index = _skip_whitespace(json, index); local b = json:byte(index); if b == 0x7d then return _fixobject(o), index + 1; end -- "}" if b ~= 0x2c then return nil, "object eof"; end -- "," end end local function _readarray(json, index) local a = {}; while true do local val, terminated; val, index, terminated = _readvalue(json, index + 1, 0x5d); if val == nil then if terminated then -- "]" found instead of value if #a ~= 0 then -- A non-empty array here means we processed a comma, -- but it wasn't followed by a value. JSON doesn't allow -- trailing commas. return nil, "value expected"; end val, index = setmetatable(a, array_mt), index+1; end return val, index; end t_insert(a, val); index = _skip_whitespace(json, index); local b = json:byte(index); if b == 0x5d then return setmetatable(a, array_mt), index + 1; end -- "]" if b ~= 0x2c then return nil, "array eof"; end -- "," end end local _unescape_error; local function _unescape_surrogate_func(x) local lead, trail = tonumber(x:sub(3, 6), 16), tonumber(x:sub(9, 12), 16); local codepoint = lead * 0x400 + trail - 0x35FDC00; local a = codepoint % 64; codepoint = (codepoint - a) / 64; local b = codepoint % 64; codepoint = (codepoint - b) / 64; local c = codepoint % 64; codepoint = (codepoint - c) / 64; return s_char(0xF0 + codepoint, 0x80 + c, 0x80 + b, 0x80 + a); end local function _unescape_func(x) x = x:match("%x%x%x%x", 3); if x then local codepoint = tonumber(x, 16) if codepoint >= 0xD800 and codepoint <= 0xDFFF then _unescape_error = true; end -- bad surrogate pair return codepoint_to_utf8(codepoint); end _unescape_error = true; end function _readstring(json, index) index = index + 1; local endindex = json:find("\"", index, true); if endindex then local s = json:sub(index, endindex - 1); --if s:find("[%z-\31]") then return nil, "control char in string"; end -- FIXME handle control characters _unescape_error = nil; s = s:gsub("\\u[dD][89abAB]%x%x\\u[dD][cdefCDEF]%x%x", _unescape_surrogate_func); -- FIXME handle escapes beyond BMP s = s:gsub("\\u.?.?.?.?", _unescape_func); if _unescape_error then return nil, "invalid escape"; end return s, endindex + 1; end return nil, "string eof"; end local function _readnumber(json, index) local m = json:match("[0-9%.%-eE%+]+", index); -- FIXME do strict checking return tonumber(m), index + #m; end local function _readnull(json, index) local a, b, c = json:byte(index + 1, index + 3); if a == 0x75 and b == 0x6c and c == 0x6c then return null, index + 4; end return nil, "null parse failed"; end local function _readtrue(json, index) local a, b, c = json:byte(index + 1, index + 3); if a == 0x72 and b == 0x75 and c == 0x65 then return true, index + 4; end return nil, "true parse failed"; end local function _readfalse(json, index) local a, b, c, d = json:byte(index + 1, index + 4); if a == 0x61 and b == 0x6c and c == 0x73 and d == 0x65 then return false, index + 5; end return nil, "false parse failed"; end function _readvalue(json, index, terminator) index = _skip_whitespace(json, index); local b = json:byte(index); -- TODO try table lookup instead of if-else? if b == 0x7B then -- "{" return _readobject(json, index); elseif b == 0x5B then -- "[" return _readarray(json, index); elseif b == 0x22 then -- "\"" return _readstring(json, index); elseif b ~= nil and b >= 0x30 and b <= 0x39 or b == 0x2d then -- "0"-"9" or "-" return _readnumber(json, index); elseif b == 0x6e then -- "n" return _readnull(json, index); elseif b == 0x74 then -- "t" return _readtrue(json, index); elseif b == 0x66 then -- "f" return _readfalse(json, index); elseif b == terminator then return nil, index, true; else return nil, "value expected"; end end local first_escape = { ["\\\""] = "\\u0022"; ["\\\\"] = "\\u005c"; ["\\/" ] = "\\u002f"; ["\\b" ] = "\\u0008"; ["\\f" ] = "\\u000C"; ["\\n" ] = "\\u000A"; ["\\r" ] = "\\u000D"; ["\\t" ] = "\\u0009"; ["\\u" ] = "\\u"; }; function module.decode(json) json = json:gsub("\\.", first_escape) -- get rid of all escapes except \uXXXX, making string parsing much simpler --:gsub("[\r\n]", "\t"); -- \r\n\t are equivalent, we care about none of them, and none of them can be in strings -- TODO do encoding verification local val, index = _readvalue(json, 1); if val == nil then return val, index; end if json:find("[^ \t\r\n]", index) then return nil, "garbage at eof"; end return val; end function module.test(object) local encoded = module.encode(object); local decoded = module.decode(encoded); local recoded = module.encode(decoded); if encoded ~= recoded then print("FAILED"); print("encoded:", encoded); print("recoded:", recoded); else print(encoded); end return encoded == recoded; end return module;