X-Git-Url: https://git.archive.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=libs%2Fluci-lib-json%2Fluasrc%2Fjson.lua;fp=libs%2Fluci-lib-json%2Fluasrc%2Fjson.lua;h=8dbaf91836422dbfc1fbb90636f588227ae13cc0;hp=0000000000000000000000000000000000000000;hb=1bb4822dca6113f73e3bc89e2acf15935e6f8e92;hpb=9edd0e46c3f880727738ce8ca6ff1c8b85f99ef4 diff --git a/libs/luci-lib-json/luasrc/json.lua b/libs/luci-lib-json/luasrc/json.lua new file mode 100644 index 000000000..8dbaf9183 --- /dev/null +++ b/libs/luci-lib-json/luasrc/json.lua @@ -0,0 +1,600 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2008 Steven Barth +Copyright 2008 Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +Decoder: + Info: + null will be decoded to luci.json.null if first parameter of Decoder() is true + + Example: + decoder = luci.json.Decoder() + luci.ltn12.pump.all(luci.ltn12.source.string("decodableJSON"), decoder:sink()) + luci.util.dumptable(decoder:get()) + + Known issues: + does not support unicode conversion \uXXYY with XX != 00 will be ignored + + +Encoder: + Info: + Accepts numbers, strings, nil, booleans as they are + Accepts luci.json.null as replacement for nil + Accepts full associative and full numerically indexed tables + Mixed tables will loose their associative values during conversion + Iterator functions will be encoded as an array of their return values + Non-iterator functions will probably corrupt the encoder + + Example: + encoder = luci.json.Encoder(encodableData) + luci.ltn12.pump.all(encoder:source(), luci.ltn12.sink.file(io.open("someFile", w))) +]]-- + +local nixio = require "nixio" +local util = require "luci.util" +local table = require "table" +local string = require "string" +local coroutine = require "coroutine" + +local assert = assert +local tonumber = tonumber +local tostring = tostring +local error = error +local type = type +local pairs = pairs +local ipairs = ipairs +local next = next +local pcall = pcall + +local band = nixio.bit.band +local bor = nixio.bit.bor +local rshift = nixio.bit.rshift +local char = string.char + +local getmetatable = getmetatable + +--- LuCI JSON-Library +-- @cstyle instance +module "luci.json" + + +--- Directly decode a JSON string +-- @param json JSON-String +-- @return Lua object +function decode(json, ...) + local a = ActiveDecoder(function() return nil end, ...) + a.chunk = json + local s, obj = pcall(a.get, a) + return s and obj or nil +end + + +--- Direcly encode a Lua object into a JSON string. +-- @param obj Lua Object +-- @return JSON string +function encode(obj, ...) + local out = {} + local e = Encoder(obj, 1, ...):source() + local chnk, err + repeat + chnk, err = e() + out[#out+1] = chnk + until not chnk + return not err and table.concat(out) or nil +end + + +--- Null replacement function +-- @return null +function null() + return null +end + +--- Create a new JSON-Encoder. +-- @class function +-- @name Encoder +-- @param data Lua-Object to be encoded. +-- @param buffersize Blocksize of returned data source. +-- @param fastescape Use non-standard escaping (don't escape control chars) +-- @return JSON-Encoder +Encoder = util.class() + +function Encoder.__init__(self, data, buffersize, fastescape) + self.data = data + self.buffersize = buffersize or 512 + self.buffer = "" + self.fastescape = fastescape + + getmetatable(self).__call = Encoder.source +end + +--- Create an LTN12 source providing the encoded JSON-Data. +-- @return LTN12 source +function Encoder.source(self) + local source = coroutine.create(self.dispatch) + return function() + local res, data = coroutine.resume(source, self, self.data, true) + if res then + return data + else + return nil, data + end + end +end + +function Encoder.dispatch(self, data, start) + local parser = self.parsers[type(data)] + + parser(self, data) + + if start then + if #self.buffer > 0 then + coroutine.yield(self.buffer) + end + + coroutine.yield() + end +end + +function Encoder.put(self, chunk) + if self.buffersize < 2 then + coroutine.yield(chunk) + else + if #self.buffer + #chunk > self.buffersize then + local written = 0 + local fbuffer = self.buffersize - #self.buffer + + coroutine.yield(self.buffer .. chunk:sub(written + 1, fbuffer)) + written = fbuffer + + while #chunk - written > self.buffersize do + fbuffer = written + self.buffersize + coroutine.yield(chunk:sub(written + 1, fbuffer)) + written = fbuffer + end + + self.buffer = chunk:sub(written + 1) + else + self.buffer = self.buffer .. chunk + end + end +end + +function Encoder.parse_nil(self) + self:put("null") +end + +function Encoder.parse_bool(self, obj) + self:put(obj and "true" or "false") +end + +function Encoder.parse_number(self, obj) + self:put(tostring(obj)) +end + +function Encoder.parse_string(self, obj) + if self.fastescape then + self:put('"' .. obj:gsub('\\', '\\\\'):gsub('"', '\\"') .. '"') + else + self:put('"' .. + obj:gsub('[%c\\"]', + function(char) + return '\\u00%02x' % char:byte() + end + ) + .. '"') + end +end + +function Encoder.parse_iter(self, obj) + if obj == null then + return self:put("null") + end + + if type(obj) == "table" and (#obj == 0 and next(obj)) then + self:put("{") + local first = true + + for key, entry in pairs(obj) do + first = first or self:put(",") + first = first and false + self:parse_string(tostring(key)) + self:put(":") + self:dispatch(entry) + end + + self:put("}") + else + self:put("[") + local first = true + + if type(obj) == "table" then + for i=1, #obj do + first = first or self:put(",") + first = first and nil + self:dispatch(obj[i]) + end + else + for entry in obj do + first = first or self:put(",") + first = first and nil + self:dispatch(entry) + end + end + + self:put("]") + end +end + +Encoder.parsers = { + ['nil'] = Encoder.parse_nil, + ['table'] = Encoder.parse_iter, + ['number'] = Encoder.parse_number, + ['string'] = Encoder.parse_string, + ['boolean'] = Encoder.parse_bool, + ['function'] = Encoder.parse_iter +} + + +--- Create a new JSON-Decoder. +-- @class function +-- @name Decoder +-- @param customnull Use luci.json.null instead of nil for decoding null +-- @return JSON-Decoder +Decoder = util.class() + +function Decoder.__init__(self, customnull) + self.cnull = customnull + getmetatable(self).__call = Decoder.sink +end + +--- Create an LTN12 sink from the decoder object which accepts the JSON-Data. +-- @return LTN12 sink +function Decoder.sink(self) + local sink = coroutine.create(self.dispatch) + return function(...) + return coroutine.resume(sink, self, ...) + end +end + + +--- Get the decoded data packets after the rawdata has been sent to the sink. +-- @return Decoded data +function Decoder.get(self) + return self.data +end + +function Decoder.dispatch(self, chunk, src_err, strict) + local robject, object + local oset = false + + while chunk do + while chunk and #chunk < 1 do + chunk = self:fetch() + end + + assert(not strict or chunk, "Unexpected EOS") + if not chunk then break end + + local char = chunk:sub(1, 1) + local parser = self.parsers[char] + or (char:match("%s") and self.parse_space) + or (char:match("[0-9-]") and self.parse_number) + or error("Unexpected char '%s'" % char) + + chunk, robject = parser(self, chunk) + + if parser ~= self.parse_space then + assert(not oset, "Scope violation: Too many objects") + object = robject + oset = true + + if strict then + return chunk, object + end + end + end + + assert(not src_err, src_err) + assert(oset, "Unexpected EOS") + + self.data = object +end + + +function Decoder.fetch(self) + local tself, chunk, src_err = coroutine.yield() + assert(chunk or not src_err, src_err) + return chunk +end + + +function Decoder.fetch_atleast(self, chunk, bytes) + while #chunk < bytes do + local nchunk = self:fetch() + assert(nchunk, "Unexpected EOS") + chunk = chunk .. nchunk + end + + return chunk +end + + +function Decoder.fetch_until(self, chunk, pattern) + local start = chunk:find(pattern) + + while not start do + local nchunk = self:fetch() + assert(nchunk, "Unexpected EOS") + chunk = chunk .. nchunk + start = chunk:find(pattern) + end + + return chunk, start +end + + +function Decoder.parse_space(self, chunk) + local start = chunk:find("[^%s]") + + while not start do + chunk = self:fetch() + if not chunk then + return nil + end + start = chunk:find("[^%s]") + end + + return chunk:sub(start) +end + + +function Decoder.parse_literal(self, chunk, literal, value) + chunk = self:fetch_atleast(chunk, #literal) + assert(chunk:sub(1, #literal) == literal, "Invalid character sequence") + return chunk:sub(#literal + 1), value +end + + +function Decoder.parse_null(self, chunk) + return self:parse_literal(chunk, "null", self.cnull and null) +end + + +function Decoder.parse_true(self, chunk) + return self:parse_literal(chunk, "true", true) +end + + +function Decoder.parse_false(self, chunk) + return self:parse_literal(chunk, "false", false) +end + + +function Decoder.parse_number(self, chunk) + local chunk, start = self:fetch_until(chunk, "[^0-9eE.+-]") + local number = tonumber(chunk:sub(1, start - 1)) + assert(number, "Invalid number specification") + return chunk:sub(start), number +end + + +function Decoder.parse_string(self, chunk) + local str = "" + local object = nil + assert(chunk:sub(1, 1) == '"', 'Expected "') + chunk = chunk:sub(2) + + while true do + local spos = chunk:find('[\\"]') + if spos then + str = str .. chunk:sub(1, spos - 1) + + local char = chunk:sub(spos, spos) + if char == '"' then -- String end + chunk = chunk:sub(spos + 1) + break + elseif char == "\\" then -- Escape sequence + chunk, object = self:parse_escape(chunk:sub(spos)) + str = str .. object + end + else + str = str .. chunk + chunk = self:fetch() + assert(chunk, "Unexpected EOS while parsing a string") + end + end + + return chunk, str +end + + +function Decoder.utf8_encode(self, s1, s2) + local n = s1 * 256 + s2 + + if n >= 0 and n <= 0x7F then + return char(n) + elseif n >= 0 and n <= 0x7FF then + return char( + bor(band(rshift(n, 6), 0x1F), 0xC0), + bor(band(n, 0x3F), 0x80) + ) + elseif n >= 0 and n <= 0xFFFF then + return char( + bor(band(rshift(n, 12), 0x0F), 0xE0), + bor(band(rshift(n, 6), 0x3F), 0x80), + bor(band(n, 0x3F), 0x80) + ) + elseif n >= 0 and n <= 0x10FFFF then + return char( + bor(band(rshift(n, 18), 0x07), 0xF0), + bor(band(rshift(n, 12), 0x3F), 0x80), + bor(band(rshift(n, 6), 0x3F), 0x80), + bor(band(n, 0x3F), 0x80) + ) + else + return "?" + end +end + + +function Decoder.parse_escape(self, chunk) + local str = "" + chunk = self:fetch_atleast(chunk:sub(2), 1) + local char = chunk:sub(1, 1) + chunk = chunk:sub(2) + + if char == '"' then + return chunk, '"' + elseif char == "\\" then + return chunk, "\\" + elseif char == "u" then + chunk = self:fetch_atleast(chunk, 4) + local s1, s2 = chunk:sub(1, 2), chunk:sub(3, 4) + s1, s2 = tonumber(s1, 16), tonumber(s2, 16) + assert(s1 and s2, "Invalid Unicode character") + + return chunk:sub(5), self:utf8_encode(s1, s2) + elseif char == "/" then + return chunk, "/" + elseif char == "b" then + return chunk, "\b" + elseif char == "f" then + return chunk, "\f" + elseif char == "n" then + return chunk, "\n" + elseif char == "r" then + return chunk, "\r" + elseif char == "t" then + return chunk, "\t" + else + error("Unexpected escaping sequence '\\%s'" % char) + end +end + + +function Decoder.parse_array(self, chunk) + chunk = chunk:sub(2) + local array = {} + local nextp = 1 + + local chunk, object = self:parse_delimiter(chunk, "%]") + + if object then + return chunk, array + end + + repeat + chunk, object = self:dispatch(chunk, nil, true) + table.insert(array, nextp, object) + nextp = nextp + 1 + + chunk, object = self:parse_delimiter(chunk, ",%]") + assert(object, "Delimiter expected") + until object == "]" + + return chunk, array +end + + +function Decoder.parse_object(self, chunk) + chunk = chunk:sub(2) + local array = {} + local name + + local chunk, object = self:parse_delimiter(chunk, "}") + + if object then + return chunk, array + end + + repeat + chunk = self:parse_space(chunk) + assert(chunk, "Unexpected EOS") + + chunk, name = self:parse_string(chunk) + + chunk, object = self:parse_delimiter(chunk, ":") + assert(object, "Separator expected") + + chunk, object = self:dispatch(chunk, nil, true) + array[name] = object + + chunk, object = self:parse_delimiter(chunk, ",}") + assert(object, "Delimiter expected") + until object == "}" + + return chunk, array +end + + +function Decoder.parse_delimiter(self, chunk, delimiter) + while true do + chunk = self:fetch_atleast(chunk, 1) + local char = chunk:sub(1, 1) + if char:match("%s") then + chunk = self:parse_space(chunk) + assert(chunk, "Unexpected EOS") + elseif char:match("[%s]" % delimiter) then + return chunk:sub(2), char + else + return chunk, nil + end + end +end + + +Decoder.parsers = { + ['"'] = Decoder.parse_string, + ['t'] = Decoder.parse_true, + ['f'] = Decoder.parse_false, + ['n'] = Decoder.parse_null, + ['['] = Decoder.parse_array, + ['{'] = Decoder.parse_object +} + + +--- Create a new Active JSON-Decoder. +-- @class function +-- @name ActiveDecoder +-- @param customnull Use luci.json.null instead of nil for decoding null +-- @return Active JSON-Decoder +ActiveDecoder = util.class(Decoder) + +function ActiveDecoder.__init__(self, source, customnull) + Decoder.__init__(self, customnull) + self.source = source + self.chunk = nil + getmetatable(self).__call = self.get +end + + +--- Fetches one JSON-object from given source +-- @return Decoded object +function ActiveDecoder.get(self) + local chunk, src_err, object + if not self.chunk then + chunk, src_err = self.source() + else + chunk = self.chunk + end + + self.chunk, object = self:dispatch(chunk, src_err, true) + return object +end + + +function ActiveDecoder.fetch(self) + local chunk, src_err = self.source() + assert(chunk or not src_err, src_err) + return chunk +end