local error = error local byte, char, find, gsub, match, sub = string.byte, string.char, string.find, string.gsub, string.match, string.sub local tonumber = tonumber local tostring, type, unpack = tostring, type, table.unpack or unpack -- The function that interprets JSON strings is separated into another file so as to -- use bitwise operation to speedup unicode codepoints processing on Lua 5.3. local genstrlib if _VERSION == "Lua 5.3" then genstrlib = require 'lunajson._str_lib_lua53' else genstrlib = require 'lunajson._str_lib' end local _ENV = nil local function nop() end local function newparser(src, saxtbl) local json, jsonnxt local jsonlen, pos, acc = 0, 1, 0 -- `f` is the temporary for dispatcher[c] and -- the dummy for the first return value of `find` local dispatcher, f -- initialize if type(src) == 'string' then json = src jsonlen = #json jsonnxt = function() json = '' jsonlen = 0 jsonnxt = nop end else jsonnxt = function() acc = acc + jsonlen pos = 1 repeat json = src() if not json then json = '' jsonlen = 0 jsonnxt = nop return end jsonlen = #json until jsonlen > 0 end jsonnxt() end local sax_startobject = saxtbl.startobject or nop local sax_key = saxtbl.key or nop local sax_endobject = saxtbl.endobject or nop local sax_startarray = saxtbl.startarray or nop local sax_endarray = saxtbl.endarray or nop local sax_string = saxtbl.string or nop local sax_number = saxtbl.number or nop local sax_boolean = saxtbl.boolean or nop local sax_null = saxtbl.null or nop --[[ Helper --]] local function tryc() local c = byte(json, pos) if not c then jsonnxt() c = byte(json, pos) end return c end local function parseerror(errmsg) error("parse error at " .. acc + pos .. ": " .. errmsg) end local function tellc() return tryc() or parseerror("unexpected termination") end local function spaces() -- skip spaces and prepare the next char while true do f, pos = find(json, '^[ \n\r\t]*', pos) if pos ~= jsonlen then pos = pos+1 return end if jsonlen == 0 then parseerror("unexpected termination") end jsonnxt() end end --[[ Invalid --]] local function f_err() parseerror('invalid value') end --[[ Constants --]] -- fallback slow constants parser local function generic_constant(target, targetlen, ret, sax_f) for i = 1, targetlen do local c = tellc() if byte(target, i) ~= c then parseerror("invalid char") end pos = pos+1 end return sax_f(ret) end -- null local function f_nul() if sub(json, pos, pos+2) == 'ull' then pos = pos+3 return sax_null(nil) end return generic_constant('ull', 3, nil, sax_null) end -- false local function f_fls() if sub(json, pos, pos+3) == 'alse' then pos = pos+4 return sax_boolean(false) end return generic_constant('alse', 4, false, sax_boolean) end -- true local function f_tru() if sub(json, pos, pos+2) == 'rue' then pos = pos+3 return sax_boolean(true) end return generic_constant('rue', 3, true, sax_boolean) end --[[ Numbers Conceptually, the longest prefix that matches to `(0|[1-9][0-9]*)(\.[0-9]*)?([eE][+-]?[0-9]*)?` (in regexp) is captured as a number and its conformance to the JSON spec is checked. --]] -- deal with non-standard locales local radixmark = match(tostring(0.5), '[^0-9]') local fixedtonumber = tonumber if radixmark ~= '.' then -- deals with non-standard locales if find(radixmark, '%W') then radixmark = '%' .. radixmark end fixedtonumber = function(s) return tonumber(gsub(s, '.', radixmark)) end end -- fallback slow parser local function generic_number(mns) local buf = {} local i = 1 local c = byte(json, pos) pos = pos+1 local function nxt() buf[i] = c i = i+1 c = tryc() pos = pos+1 end if c == 0x30 then nxt() else repeat nxt() until not (c and 0x30 <= c and c < 0x3A) end if c == 0x2E then nxt() if not (c and 0x30 <= c and c < 0x3A) then parseerror('invalid number') end repeat nxt() until not (c and 0x30 <= c and c < 0x3A) end if c == 0x45 or c == 0x65 then nxt() if c == 0x2B or c == 0x2D then nxt() end if not (c and 0x30 <= c and c < 0x3A) then parseerror('invalid number') end repeat nxt() until not (c and 0x30 <= c and c < 0x3A) end pos = pos-1 local num = char(unpack(buf)) num = fixedtonumber(num)-0.0 if mns then num = -num end return sax_number(num) end -- `0(\.[0-9]*)?([eE][+-]?[0-9]*)?` local function f_zro(mns) local postmp = pos local num local c = byte(json, postmp) if c == 0x2E then -- is this `.`? num = match(json, '^.[0-9]*', pos) -- skipping 0 local numlen = #num if numlen == 1 then pos = pos-1 return generic_number(mns) end postmp = pos + numlen c = byte(json, postmp) end if c == 0x45 or c == 0x65 then -- is this e or E? local numexp = match(json, '^[^eE]*[eE][-+]?[0-9]+', pos) if not numexp then pos = pos-1 return generic_number(mns) end if num then -- since `0e.*` is always 0.0, ignore those num = numexp end postmp = pos + #numexp end if postmp > jsonlen then pos = pos-1 return generic_number(mns) end pos = postmp if num then num = fixedtonumber(num) else num = 0.0 end if mns then num = -num end return sax_number(num) end -- `[1-9][0-9]*(\.[0-9]*)?([eE][+-]?[0-9]*)?` local function f_num(mns) pos = pos-1 local num = match(json, '^.[0-9]*%.?[0-9]*', pos) if byte(num, -1) == 0x2E then return generic_number(mns) end local postmp = pos + #num local c = byte(json, postmp) if c == 0x45 or c == 0x65 then -- e or E? num = match(json, '^[^eE]*[eE][-+]?[0-9]+', pos) if not num then return generic_number(mns) end postmp = pos + #num end if postmp > jsonlen then return generic_number(mns) end pos = postmp num = fixedtonumber(num)-0.0 if mns then num = -num end return sax_number(num) end -- skip minus sign local function f_mns() local c = byte(json, pos) or tellc() if c then pos = pos+1 if c > 0x30 then if c < 0x3A then return f_num(true) end else if c > 0x2F then return f_zro(true) end end end parseerror("invalid number") end --[[ Strings --]] local f_str_lib = genstrlib(parseerror) local f_str_surrogateok = f_str_lib.surrogateok -- whether codepoints for surrogate pair are correctly paired local f_str_subst = f_str_lib.subst -- the function passed to gsub that interprets escapes local function f_str(iskey) local pos2 = pos local newpos local str = '' local bs while true do while true do -- search '\' or '"' newpos = find(json, '[\\"]', pos2) if newpos then break end str = str .. sub(json, pos, jsonlen) if pos2 == jsonlen+2 then pos2 = 2 else pos2 = 1 end jsonnxt() end if byte(json, newpos) == 0x22 then -- break if '"' break end pos2 = newpos+2 -- skip '\' bs = true -- remember that backslash occurs end str = str .. sub(json, pos, newpos-1) pos = newpos+1 if bs then -- check if backslash occurs str = gsub(str, '\\(.)([^\\]*)', f_str_subst) -- interpret escapes if not f_str_surrogateok() then parseerror("invalid surrogate pair") end end if iskey then return sax_key(str) end return sax_string(str) end --[[ Arrays, Objects --]] -- arrays local function f_ary() sax_startarray() spaces() if byte(json, pos) ~= 0x5D then -- check the closing bracket ']', that consists an empty array local newpos while true do f = dispatcher[byte(json, pos)] -- parse value pos = pos+1 f() f, newpos = find(json, '^[ \n\r\t]*,[ \n\r\t]*', pos) -- check comma if not newpos then f, newpos = find(json, '^[ \n\r\t]*%]', pos) -- check closing bracket if newpos then pos = newpos break end spaces() -- since the current chunk can be ended, skip spaces toward following chunks local c = byte(json, pos) if c == 0x2C then -- check comma again pos = pos+1 spaces() newpos = pos-1 elseif c == 0x5D then -- check closing bracket again break else parseerror("no closing bracket of an array") end end pos = newpos+1 if pos > jsonlen then spaces() end end end pos = pos+1 return sax_endarray() end -- objects local function f_obj() sax_startobject() spaces() if byte(json, pos) ~= 0x7D then -- check the closing bracket `}`, that consists an empty object local newpos while true do if byte(json, pos) ~= 0x22 then parseerror("not key") end pos = pos+1 f_str(true) f, newpos = find(json, '^[ \n\r\t]*:[ \n\r\t]*', pos) -- check colon if not newpos then spaces() -- since the current chunk can be ended, skip spaces toward following chunks if byte(json, pos) ~= 0x3A then -- check colon again parseerror("no colon after a key") end pos = pos+1 spaces() newpos = pos-1 end pos = newpos+1 if pos > jsonlen then spaces() end f = dispatcher[byte(json, pos)] -- parse value pos = pos+1 f() f, newpos = find(json, '^[ \n\r\t]*,[ \n\r\t]*', pos) -- check comma if not newpos then f, newpos = find(json, '^[ \n\r\t]*}', pos) -- check closing bracket if newpos then pos = newpos break end spaces() -- since the current chunk can be ended, skip spaces toward following chunks local c = byte(json, pos) if c == 0x2C then -- check comma again pos = pos+1 spaces() newpos = pos-1 elseif c == 0x7D then -- check closing bracket again break else parseerror("no closing bracket of an object") end end pos = newpos+1 if pos > jsonlen then spaces() end end end pos = pos+1 return sax_endobject() end --[[ The jump table to dispatch a parser for a value, indexed by the code of the value's first char. Key should be non-nil. --]] dispatcher = { f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_str, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_mns, f_err, f_err, f_zro, f_num, f_num, f_num, f_num, f_num, f_num, f_num, f_num, f_num, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_ary, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_fls, f_err, f_err, f_err, f_err, f_err, f_err, f_err, f_nul, f_err, f_err, f_err, f_err, f_err, f_tru, f_err, f_err, f_err, f_err, f_err, f_err, f_obj, f_err, f_err, f_err, f_err, } dispatcher[0] = f_err --[[ public funcitons --]] local function run() spaces() f = dispatcher[byte(json, pos)] pos = pos+1 f() end local function read(n) if n < 0 then error("the argument must be non-negative") end local pos2 = (pos-1) + n local str = sub(json, pos, pos2) while pos2 > jsonlen and jsonlen ~= 0 do jsonnxt() pos2 = pos2 - (jsonlen - (pos-1)) str = str .. sub(json, pos, pos2) end if jsonlen ~= 0 then pos = pos2+1 end return str end local function tellpos() return acc + pos end return { run = run, tryc = tryc, read = read, tellpos = tellpos, } end local function newfileparser(fn, saxtbl) local fp = io.open(fn) local function gen() local s if fp then s = fp:read(8192) if not s then fp:close() fp = nil end end return s end return newparser(gen, saxtbl) end return { newparser = newparser, newfileparser = newfileparser }