1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-01-05 15:00:29 +00:00

Add textutils.unserialiseJSON (#407)

This is relatively unoptimised right now, but should be efficient enough
for most practical applications.

 - Add textutils.json_null. This will be serialized into a literal
   `null`. When deserializing, and parse_null is true, this will be
   returned instead of a nil.

 - Add textutils.unserializeJSON (and textutils.unserializeJSON). This
   is a standard compliant JSON parser (hopefully).

 - Passing in nbt_style to textutils.unserializeJSON will handle
   stringified NBT (no quotes around object keys, numeric suffices). We
   don't currently support byte/long/int arrays - something to add in
   a future commit.
This commit is contained in:
Jonathan Coates 2020-04-19 15:08:46 +01:00 committed by GitHub
parent eead8b5755
commit b14c7842fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
325 changed files with 666 additions and 13 deletions

View File

@ -3,7 +3,8 @@
--
-- @module textutils
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local expect = dofile("rom/modules/main/cc/expect.lua")
local expect, field = expect.expect, expect.field
--- Slowly writes string text at current cursor position,
-- character-by-character.
@ -307,6 +308,14 @@ local function serializeImpl(t, tTracking, sIndent)
end
end
local function mk_tbl(str, name)
local msg = "attempt to mutate textutils." .. name
return setmetatable({}, {
__newindex = function() error(msg, 2) end,
__tostring = function() return str end,
})
end
--- A table representing an empty JSON array, in order to distinguish it from an
-- empty JSON object.
--
@ -314,16 +323,22 @@ end
--
-- @usage textutils.serialiseJSON(textutils.empty_json_array)
-- @see textutils.serialiseJSON
empty_json_array = setmetatable({}, {
__newindex = function()
error("attempt to mutate textutils.empty_json_array", 2)
end,
})
-- @see textutils.unserialiseJSON
empty_json_array = mk_tbl("[]", "empty_json_array")
--- A table representing the JSON null value.
--
-- The contents of this table should not be modified.
--
-- @usage textutils.serialiseJSON(textutils.json_null)
-- @see textutils.serialiseJSON
-- @see textutils.unserialiseJSON
json_null = mk_tbl("null", "json_null")
local function serializeJSONImpl(t, tTracking, bNBTStyle)
local sType = type(t)
if t == empty_json_array then
return "[]"
if t == empty_json_array then return "[]"
elseif t == json_null then return "null"
elseif sType == "table" then
if tTracking[t] ~= nil then
@ -386,6 +401,209 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
end
end
local unserialise_json
do
local sub, find, match, concat, tonumber = string.sub, string.find, string.match, table.concat, tonumber
--- Skip any whitespace
local function skip(str, pos)
local _, last = find(str, "^[ \n\r\v]+", pos)
if last then return last + 1 else return pos end
end
local escapes = {
["b"] = '\b', ["f"] = '\f', ["n"] = '\n', ["r"] = '\r', ["t"] = '\t',
["\""] = "\"", ["/"] = "/", ["\\"] = "\\",
}
local mt = {}
local function error_at(pos, msg, ...)
if select('#', ...) > 0 then msg = msg:format(...) end
error(setmetatable({ pos = pos, msg = msg }, mt))
end
local function expected(pos, actual, exp)
if actual == "" then actual = "end of input" else actual = ("%q"):format(actual) end
error_at(pos, "Unexpected %s, expected %s.", actual, exp)
end
local function parse_string(str, pos)
local buf, n = {}, 1
while true do
local c = sub(str, pos, pos)
if c == "" then error_at(pos, "Unexpected end of input, expected '\"'.") end
if c == '"' then break end
if c == '\\' then
-- Handle the various escapes
c = sub(str, pos + 1, pos + 1)
if c == "" then error_at(pos, "Unexpected end of input, expected escape sequence.") end
if c == "u" then
local num_str = match(str, "^%x%x%x%x", pos + 2)
if not num_str then error_at(pos, "Malformed unicode escape %q.", sub(str, pos + 2, pos + 5)) end
buf[n], n, pos = utf8.char(tonumber(num_str, 16)), n + 1, pos + 6
else
local unesc = escapes[c]
if not unesc then error_at(pos + 1, "Unknown escape character %q.", unesc) end
buf[n], n, pos = unesc, n + 1, pos + 2
end
elseif c >= '\x20' then
buf[n], n, pos = c, n + 1, pos + 1
else
error_at(pos + 1, "Unescaped whitespace %q.", c)
end
end
return concat(buf, "", 1, n - 1), pos + 1
end
local valid = { b = true, B = true, s = true, S = true, l = true, L = true, f = true, F = true, d = true, D = true }
local function parse_number(str, pos, opts)
local _, last, num_str = find(str, '^(-?%d+%.?%d*[eE]?[+-]?%d*)', pos)
local val = tonumber(num_str)
if not val then error_at(pos, "Malformed number %q.", num_str) end
if opts.nbt_style and valid[sub(str, pos + 1, pos + 1)] then return val, last + 2 end
return val, last + 1
end
local function parse_ident(str, pos)
local _, last, val = find(str, '^([%a][%w_]*)', pos)
return val, last + 1
end
local function decode_impl(str, pos, opts)
local c = sub(str, pos, pos)
if c == '"' then return parse_string(str, pos + 1)
elseif c == "-" or c >= "0" and c <= "9" then return parse_number(str, pos, opts)
elseif c == "t" then
if sub(str, pos + 1, pos + 3) == "rue" then return true, pos + 4 end
elseif c == 'f' then
if sub(str, pos + 1, pos + 4) == "alse" then return false, pos + 5 end
elseif c == 'n' then
if sub(str, pos + 1, pos + 3) == "ull" then
if opts.parse_null then
return json_null, pos + 4
else
return nil, pos + 4
end
end
elseif c == "{" then
local obj = {}
pos = skip(str, pos + 1)
c = sub(str, pos, pos)
if c == "" then return error_at(pos, "Unexpected end of input, expected '}'.") end
if c == "}" then return obj, pos + 1 end
while true do
local key, value
if c == "\"" then key, pos = parse_string(str, pos + 1)
elseif opts.nbt_style then key, pos = parse_ident(str, pos)
else return expected(pos, c, "object key")
end
pos = skip(str, pos)
c = sub(str, pos, pos)
if c ~= ":" then return expected(pos, c, "':'") end
value, pos = decode_impl(str, skip(str, pos + 1), opts)
obj[key] = value
-- Consume the next delimiter
pos = skip(str, pos)
c = sub(str, pos, pos)
if c == "}" then break
elseif c == "," then pos = skip(str, pos + 1)
else return expected(pos, c, "',' or '}'")
end
c = sub(str, pos, pos)
end
return obj, pos + 1
elseif c == "[" then
local arr, n = {}, 1
pos = skip(str, pos + 1)
c = sub(str, pos, pos)
if c == "" then return expected(pos, c, "']'") end
if c == "]" then return empty_json_array, pos + 1 end
while true do
n, arr[n], pos = n + 1, decode_impl(str, pos, opts)
-- Consume the next delimiter
pos = skip(str, pos)
c = sub(str, pos, pos)
if c == "]" then break
elseif c == "," then pos = skip(str, pos + 1)
else return expected(pos, c, "',' or ']'")
end
end
return arr, pos + 1
elseif c == "" then error_at(pos, 'Unexpected end of input.')
end
error_at(pos, "Unexpected character %q.", c)
end
--- Converts a serialised JSON string back into a reassembled Lua object.
--
-- This may be used with @{textutils.serializeJSON}, or when communicating
-- with command blocks or web APIs.
--
-- @tparam string s The serialised string to deserialise.
-- @tparam[opt] { nbt_style? = boolean, parse_null? = boolean } options
-- Options which control how this JSON object is parsed.
--
-- - `nbt_style`: When true, this will accept [stringified NBT][nbt] strings,
-- as produced by many commands.
-- - `parse_null`: When true, `null` will be parsed as @{json_null}, rather
-- than `nil`.
--
-- [nbt]: https://minecraft.gamepedia.com/NBT_format
-- @return[1] The deserialised object
-- @treturn[2] nil If the object could not be deserialised.
-- @treturn string A message describing why the JSON string is invalid.
unserialise_json = function(s, options)
expect(1, s, "string")
expect(2, options, "table", "nil")
if options then
field(options, "nbt_style", "boolean", "nil")
field(options, "nbt_style", "boolean", "nil")
else
options = {}
end
local ok, res, pos = pcall(decode_impl, s, skip(s, 1), options)
if not ok then
if type(res) == "table" and getmetatable(res) == mt then
return nil, ("Malformed JSON at position %d: %s"):format(res.pos, res.msg)
end
error(res, 0)
end
pos = skip(s, pos)
if pos <= #s then
return nil, ("Malformed JSON at position %d: Unexpected trailing character %q."):format(pos, sub(s, pos, pos))
end
return res
end
end
--- Convert a Lua object into a textual representation, suitable for
-- saving in a file or pretty-printing.
--
@ -449,6 +667,9 @@ end
serialiseJSON = serializeJSON -- GB version
unserializeJSON = unserialise_json
unserialiseJSON = unserialise_json
--- Replaces certain characters in a string to make it safe for use in URLs or POST data.
--
-- @tparam string str The string to encode

View File

@ -96,7 +96,7 @@ public class ComputerTestDelegate
try( WritableByteChannel channel = mount.openChannelForWrite( "startup.lua" );
Writer writer = Channels.newWriter( channel, StandardCharsets.UTF_8.newEncoder(), -1 ) )
{
writer.write( "loadfile('test/mcfly.lua', nil, _ENV)('test/spec') cct_test.finish()" );
writer.write( "loadfile('test-rom/mcfly.lua', nil, _ENV)('test-rom/spec') cct_test.finish()" );
}
computer = new Computer( new BasicEnvironment( mount ), term, 0 );
@ -122,7 +122,7 @@ public class ComputerTestDelegate
try
{
computer.getAPIEnvironment().getFileSystem().mount(
"test-rom", "test",
"test-rom", "test-rom",
BasicEnvironment.createMount( ComputerTestDelegate.class, "test-rom", "test" )
);
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Nicolas Seriot
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.

View File

@ -0,0 +1,9 @@
# JSON Parsing Test Suite
This is a collection of JSON test cases from [nst/JSONTestSuite][gh]. We simply
determine whether an object is succesfully parsed or not, and do not check the
contents.
See `LICENSE` for copyright information.
[gh]: https://github.com/nst/JSONTestSuite

View File

@ -0,0 +1 @@
[123.456e-789]

View File

@ -0,0 +1 @@
[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006]

View File

@ -0,0 +1 @@
[-1e+9999]

View File

@ -0,0 +1 @@
[-123123e100000]

View File

@ -0,0 +1 @@
[123123e100000]

View File

@ -0,0 +1 @@
[123e-10000000]

View File

@ -0,0 +1 @@
[-123123123123123123123123123123]

View File

@ -0,0 +1 @@
[100000000000000000000]

View File

@ -0,0 +1 @@
[-237462374673276894279832749832423479823246327846]

View File

@ -0,0 +1 @@
["譌・ム淫"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD>"]

View File

@ -0,0 +1 @@
["\ud800abc"]

View File

@ -0,0 +1 @@
["<22>"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD>"]

View File

@ -0,0 +1 @@
["<22>ソソソソ"]

View File

@ -0,0 +1 @@
["<22><>"]

View File

@ -0,0 +1 @@
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]

View File

@ -0,0 +1 @@
["x"]]

View File

@ -0,0 +1 @@
[<EFBFBD>]

View File

@ -0,0 +1 @@
[ , ""]

View File

@ -0,0 +1,3 @@
["a",
4
,1,

View File

@ -0,0 +1 @@
[fals]

View File

@ -0,0 +1 @@
[nul]

View File

@ -0,0 +1 @@
[tru]

View File

@ -0,0 +1 @@
[++1234]

View File

@ -0,0 +1 @@
[+1]

View File

@ -0,0 +1 @@
[+Inf]

View File

@ -0,0 +1 @@
[-01]

View File

@ -0,0 +1 @@
[-1.0.]

View File

@ -0,0 +1 @@
[-2.]

View File

@ -0,0 +1 @@
[-NaN]

View File

@ -0,0 +1 @@
[.-1]

View File

@ -0,0 +1 @@
[.2e-3]

View File

@ -0,0 +1 @@
[0.1.2]

View File

@ -0,0 +1 @@
[0.3e+]

View File

@ -0,0 +1 @@
[0.3e]

View File

@ -0,0 +1 @@
[0.e1]

View File

@ -0,0 +1 @@
[0e+]

View File

@ -0,0 +1 @@
[0e]

View File

@ -0,0 +1 @@
[1.0e+]

View File

@ -0,0 +1 @@
[1.0e-]

View File

@ -0,0 +1 @@
[1.0e]

View File

@ -0,0 +1 @@
[1 000.0]

View File

@ -0,0 +1 @@
[1eE2]

View File

@ -0,0 +1 @@
[2.e+3]

View File

@ -0,0 +1 @@
[2.e-3]

View File

@ -0,0 +1 @@
[2.e3]

View File

@ -0,0 +1 @@
[9.e+]

View File

@ -0,0 +1 @@
[Inf]

View File

@ -0,0 +1 @@
[NaN]

Some files were not shown because too many files have changed in this diff Show More