1--
  2-- json.lua
  3--
  4-- Copyright (c) 2020 rxi
  5--
  6-- Permission is hereby granted, free of charge, to any person obtaining a copy of
  7-- this software and associated documentation files (the "Software"), to deal in
  8-- the Software without restriction, including without limitation the rights to
  9-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 10-- of the Software, and to permit persons to whom the Software is furnished to do
 11-- so, subject to the following conditions:
 12--
 13-- The above copyright notice and this permission notice shall be included in all
 14-- copies or substantial portions of the Software.
 15--
 16-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 22-- SOFTWARE.
 23--
 24
 25local json = { _version = "0.1.2" }
 26
 27-------------------------------------------------------------------------------
 28-- Encode
 29-------------------------------------------------------------------------------
 30
 31local encode
 32
 33local escape_char_map = {
 34	[ "\\" ] = "\\",
 35	[ "\"" ] = "\"",
 36	[ "\b" ] = "b",
 37	[ "\f" ] = "f",
 38	[ "\n" ] = "n",
 39	[ "\r" ] = "r",
 40	[ "\t" ] = "t",
 41}
 42
 43local escape_char_map_inv = { [ "/" ] = "/" }
 44for k, v in pairs(escape_char_map) do
 45	escape_char_map_inv[v] = k
 46end
 47
 48local function escape_char(c)
 49	return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
 50end
 51
 52local function encode_nil(val)
 53	return "null"
 54end
 55
 56local function encode_table(val, stack)
 57	local res = {}
 58	stack = stack or {}
 59
 60	-- Circular reference?
 61	if stack[val] then error("circular reference") end
 62
 63	stack[val] = true
 64
 65	if rawget(val, 1) ~= nil or next(val) == nil then
 66		-- Treat as array -- check keys are valid and it is not sparse
 67		local n = 0
 68		for k in pairs(val) do
 69			if type(k) ~= "number" then
 70				error("invalid table: mixed or invalid key types")
 71			end
 72			n = n + 1
 73		end
 74		if n ~= #val then
 75			error("invalid table: sparse array")
 76		end
 77		-- Encode
 78		for i, v in ipairs(val) do
 79			table.insert(res, encode(v, stack))
 80		end
 81		stack[val] = nil
 82		return "[" .. table.concat(res, ",") .. "]"
 83
 84	else
 85		-- Treat as an object
 86		for k, v in pairs(val) do
 87			if type(k) ~= "string" then
 88				error("invalid table: mixed or invalid key types")
 89			end
 90			table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
 91		end
 92		stack[val] = nil
 93		return "{" .. table.concat(res, ",") .. "}"
 94	end
 95end
 96
 97local function encode_string(val)
 98	return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
 99end
100
101local function encode_number(val)
102	-- Check for NaN, -inf and inf
103	if val ~= val or val <= -math.huge or val >= math.huge then
104		error("unexpected number value '" .. tostring(val) .. "'")
105	end
106	return string.format("%.14g", val)
107end
108
109local type_func_map = {
110	[ "nil"     ] = encode_nil,
111	[ "table"   ] = encode_table,
112	[ "string"  ] = encode_string,
113	[ "number"  ] = encode_number,
114	[ "boolean" ] = tostring,
115}
116
117encode = function(val, stack)
118	local t = type(val)
119	local f = type_func_map[t]
120	if f then
121		return f(val, stack)
122	end
123	error("unexpected type '" .. t .. "'")
124end
125
126function json.encode(val)
127	return ( encode(val) )
128end
129
130-------------------------------------------------------------------------------
131-- Decode
132-------------------------------------------------------------------------------
133
134local parse
135
136local function create_set(...)
137	local res = {}
138	for i = 1, select("#", ...) do
139		res[ select(i, ...) ] = true
140	end
141	return res
142end
143
144local space_chars   = create_set(" ", "\t", "\r", "\n")
145local delim_chars   = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
146local escape_chars  = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
147local literals      = create_set("true", "false", "null")
148
149local literal_map = {
150	[ "true"  ] = true,
151	[ "false" ] = false,
152	[ "null"  ] = nil,
153}
154
155local function next_char(str, idx, set, negate)
156	for i = idx, #str do
157		if set[str:sub(i, i)] ~= negate then
158			return i
159		end
160	end
161	return #str + 1
162end
163
164local function decode_error(str, idx, msg)
165	local line_count = 1
166	local col_count = 1
167	for i = 1, idx - 1 do
168		col_count = col_count + 1
169		if str:sub(i, i) == "\n" then
170			line_count = line_count + 1
171			col_count = 1
172		end
173	end
174	error( string.format("%s at line %d col %d", msg, line_count, col_count) )
175end
176
177local function codepoint_to_utf8(n)
178	-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
179	local f = math.floor
180	if n <= 0x7f then
181		return string.char(n)
182	elseif n <= 0x7ff then
183		return string.char(f(n / 64) + 192, n % 64 + 128)
184	elseif n <= 0xffff then
185		return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
186	elseif n <= 0x10ffff then
187		return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
188		f(n % 4096 / 64) + 128, n % 64 + 128)
189	end
190	error( string.format("invalid unicode codepoint '%x'", n) )
191end
192
193local function parse_unicode_escape(s)
194	local n1 = tonumber( s:sub(1, 4),  16 )
195	local n2 = tonumber( s:sub(7, 10), 16 )
196	-- Surrogate pair?
197	if n2 then
198		return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
199	else
200		return codepoint_to_utf8(n1)
201	end
202end
203
204local function parse_string(str, i)
205	local res = ""
206	local j = i + 1
207	local k = j
208
209	while j <= #str do
210		local x = str:byte(j)
211
212		if x < 32 then
213			decode_error(str, j, "control character in string")
214
215		elseif x == 92 then -- `\`: Escape
216			res = res .. str:sub(k, j - 1)
217			j = j + 1
218			local c = str:sub(j, j)
219			if c == "u" then
220				local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
221				or str:match("^%x%x%x%x", j + 1)
222				or decode_error(str, j - 1, "invalid unicode escape in string")
223				res = res .. parse_unicode_escape(hex)
224				j = j + #hex
225			else
226				if not escape_chars[c] then
227					decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
228				end
229				res = res .. escape_char_map_inv[c]
230			end
231			k = j + 1
232
233		elseif x == 34 then -- `"`: End of string
234			res = res .. str:sub(k, j - 1)
235			return res, j + 1
236		end
237
238		j = j + 1
239	end
240
241	decode_error(str, i, "expected closing quote for string")
242end
243
244local function parse_number(str, i)
245	local x = next_char(str, i, delim_chars)
246	local s = str:sub(i, x - 1)
247	local n = tonumber(s)
248	if not n then
249		decode_error(str, i, "invalid number '" .. s .. "'")
250	end
251	return n, x
252end
253
254local function parse_literal(str, i)
255	local x = next_char(str, i, delim_chars)
256	local word = str:sub(i, x - 1)
257	if not literals[word] then
258		decode_error(str, i, "invalid literal '" .. word .. "'")
259	end
260	return literal_map[word], x
261end
262
263local function parse_array(str, i)
264	local res = {}
265	local n = 1
266	i = i + 1
267	while 1 do
268		local x
269		i = next_char(str, i, space_chars, true)
270		-- Empty / end of array?
271		if str:sub(i, i) == "]" then
272			i = i + 1
273			break
274		end
275		-- Read token
276		x, i = parse(str, i)
277		res[n] = x
278		n = n + 1
279		-- Next token
280		i = next_char(str, i, space_chars, true)
281		local chr = str:sub(i, i)
282		i = i + 1
283		if chr == "]" then break end
284		if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
285	end
286	return res, i
287end
288
289local function parse_object(str, i)
290	local res = {}
291	i = i + 1
292	while 1 do
293		local key, val
294		i = next_char(str, i, space_chars, true)
295		-- Empty / end of object?
296		if str:sub(i, i) == "}" then
297			i = i + 1
298			break
299		end
300		-- Read key
301		if str:sub(i, i) ~= '"' then
302			decode_error(str, i, "expected string for key")
303		end
304		key, i = parse(str, i)
305		-- Read ':' delimiter
306		i = next_char(str, i, space_chars, true)
307		if str:sub(i, i) ~= ":" then
308			decode_error(str, i, "expected ':' after key")
309		end
310		i = next_char(str, i + 1, space_chars, true)
311		-- Read value
312		val, i = parse(str, i)
313		-- Set
314		res[key] = val
315		-- Next token
316		i = next_char(str, i, space_chars, true)
317		local chr = str:sub(i, i)
318		i = i + 1
319		if chr == "}" then break end
320		if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
321	end
322	return res, i
323end
324
325local char_func_map = {
326	[ '"' ] = parse_string,
327	[ "0" ] = parse_number,
328	[ "1" ] = parse_number,
329	[ "2" ] = parse_number,
330	[ "3" ] = parse_number,
331	[ "4" ] = parse_number,
332	[ "5" ] = parse_number,
333	[ "6" ] = parse_number,
334	[ "7" ] = parse_number,
335	[ "8" ] = parse_number,
336	[ "9" ] = parse_number,
337	[ "-" ] = parse_number,
338	[ "t" ] = parse_literal,
339	[ "f" ] = parse_literal,
340	[ "n" ] = parse_literal,
341	[ "[" ] = parse_array,
342	[ "{" ] = parse_object,
343}
344
345parse = function(str, idx)
346	local chr = str:sub(idx, idx)
347	local f = char_func_map[chr]
348	if f then
349		return f(str, idx)
350	end
351	decode_error(str, idx, "unexpected character '" .. chr .. "'")
352end
353
354function json.decode(str)
355	if type(str) ~= "string" then
356		error("expected argument of type string, got " .. type(str))
357	end
358	local res, idx = parse(str, next_char(str, 1, space_chars, true))
359	idx = next_char(str, idx, space_chars, true)
360	if idx <= #str then
361		decode_error(str, idx, "trailing garbage")
362	end
363	return res
364end
365
366return json