json.lua

main 420 lines · 11.1 KB Raw
Alex Alex Commit Client Upload Part 2 19/10/2026 00:06
1--
2-- json.lua
3--
4-- Copyright (c) 2019 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
25json = { _version = "0.1.1" }
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
48
49local function make_indent(state)
50 return string.rep(" ", state.currentIndentLevel * state.indent)
51end
52
53
54local function escape_char(c)
55 return escape_char_map[c] or string.format("\\u%04x", c:byte())
56end
57
58
59local function encode_nil()
60 return "null"
61end
62
63
64local function encode_table(val, state)
65 local res = {}
66 local stack = state.stack
67 local pretty = state.indent > 0
68
69 local close_indent = make_indent(state)
70 local comma = pretty and ",\n" or ","
71 local colon = pretty and ": " or ":"
72 local open_brace = pretty and "{\n" or "{"
73 local close_brace = pretty and ("\n" .. close_indent .. "}") or "}"
74 local open_bracket = pretty and "[\n" or "["
75 local close_bracket = pretty and ("\n" .. close_indent .. "]") or "]"
76
77 -- Circular reference?
78 if stack[val] then error("circular reference") end
79
80 stack[val] = true
81
82 if rawget(val, 1) ~= nil or next(val) == nil then
83 -- Treat as array -- check keys are valid and it is not sparse
84 local n = 0
85 for k in pairs(val) do
86 if type(k) ~= "number" then
87 error("invalid table: mixed or invalid key types")
88 end
89 n = n + 1
90 end
91 if n ~= #val then
92 error("invalid table: sparse array")
93 end
94 -- Encode
95 for _, v in ipairs(val) do
96 state.currentIndentLevel = state.currentIndentLevel + 1
97 table.insert(res, make_indent(state) .. encode(v, state))
98 state.currentIndentLevel = state.currentIndentLevel - 1
99 end
100 stack[val] = nil
101 return open_bracket .. table.concat(res, comma) .. close_bracket
102
103 else
104 -- Treat as an object
105 for k, v in pairs(val) do
106 if type(k) ~= "string" then
107 error("invalid table: mixed or invalid key types")
108 end
109 state.currentIndentLevel = state.currentIndentLevel + 1
110 table.insert(res, make_indent(state) .. encode(k, state) .. colon .. encode(v, state))
111 state.currentIndentLevel = state.currentIndentLevel - 1
112 end
113 stack[val] = nil
114 return open_brace .. table.concat(res, comma) .. close_brace
115 end
116end
117
118
119local function encode_string(val)
120 return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
121end
122
123
124local function encode_number(val)
125 -- Check for NaN, -inf and inf
126 if val ~= val or val <= -math.huge or val >= math.huge then
127 error("unexpected number value '" .. tostring(val) .. "'")
128 end
129 return string.format("%.14g", val)
130end
131
132
133local type_func_map = {
134 [ "nil" ] = encode_nil,
135 [ "table" ] = encode_table,
136 [ "string" ] = encode_string,
137 [ "number" ] = encode_number,
138 [ "boolean" ] = tostring,
139}
140
141
142encode = function(val, state)
143 local t = type(val)
144 local f = type_func_map[t]
145 if f then
146 return f(val, state)
147 end
148 error("unexpected type '" .. t .. "'")
149end
150
151function json.encode(val, indent)
152 local state = {
153 indent = indent or 0,
154 currentIndentLevel = 0,
155 stack = {}
156 }
157 return encode(val, state)
158end
159
160
161-------------------------------------------------------------------------------
162-- Decode
163-------------------------------------------------------------------------------
164
165local parse
166
167local function create_set(...)
168 local res = {}
169 for i = 1, select("#", ...) do
170 res[ select(i, ...) ] = true
171 end
172 return res
173end
174
175local space_chars = create_set(" ", "\t", "\r", "\n")
176local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
177local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
178local literals = create_set("true", "false", "null")
179
180local literal_map = {
181 [ "true" ] = true,
182 [ "false" ] = false,
183 [ "null" ] = nil,
184}
185
186
187local function next_char(str, idx, set, negate)
188 for i = idx, #str do
189 if set[str:sub(i, i)] ~= negate then
190 return i
191 end
192 end
193 return #str + 1
194end
195
196
197local function decode_error(str, idx, msg)
198 local line_count = 1
199 local col_count = 1
200 for i = 1, idx - 1 do
201 col_count = col_count + 1
202 if str:sub(i, i) == "\n" then
203 line_count = line_count + 1
204 col_count = 1
205 end
206 end
207 error( string.format("%s at line %d col %d", msg, line_count, col_count) )
208end
209
210
211local function codepoint_to_utf8(n)
212 -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
213 local f = math.floor
214 if n <= 0x7f then
215 return string.char(n)
216 elseif n <= 0x7ff then
217 return string.char(f(n / 64) + 192, n % 64 + 128)
218 elseif n <= 0xffff then
219 return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
220 elseif n <= 0x10ffff then
221 return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
222 f(n % 4096 / 64) + 128, n % 64 + 128)
223 end
224 error( string.format("invalid unicode codepoint '%x'", n) )
225end
226
227
228local function parse_unicode_escape(s)
229 local n1 = tonumber( s:sub(3, 6), 16 )
230 local n2 = tonumber( s:sub(9, 12), 16 )
231 -- Surrogate pair?
232 if n2 then
233 return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
234 else
235 return codepoint_to_utf8(n1)
236 end
237end
238
239
240local function parse_string(str, i)
241 local has_unicode_escape = false
242 local has_surrogate_escape = false
243 local has_escape = false
244 local last
245 for j = i + 1, #str do
246 local x = str:byte(j)
247
248 if x < 32 then
249 decode_error(str, j, "control character in string")
250 end
251
252 if last == 92 then -- "\\" (escape char)
253 if x == 117 then -- "u" (unicode escape sequence)
254 local hex = str:sub(j + 1, j + 5)
255 if not hex:find("%x%x%x%x") then
256 decode_error(str, j, "invalid unicode escape in string")
257 end
258 if hex:find("^[dD][89aAbB]") then
259 has_surrogate_escape = true
260 else
261 has_unicode_escape = true
262 end
263 else
264 local c = string.char(x)
265 if not escape_chars[c] then
266 decode_error(str, j, "invalid escape char '" .. c .. "' in string")
267 end
268 has_escape = true
269 end
270 last = nil
271
272 elseif x == 34 then -- '"' (end of string)
273 local s = str:sub(i + 1, j - 1)
274 if has_surrogate_escape then
275 s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
276 end
277 if has_unicode_escape then
278 s = s:gsub("\\u....", parse_unicode_escape)
279 end
280 if has_escape then
281 s = s:gsub("\\.", escape_char_map_inv)
282 end
283 return s, j + 1
284
285 else
286 last = x
287 end
288 end
289 decode_error(str, i, "expected closing quote for string")
290end
291
292
293local function parse_number(str, i)
294 local x = next_char(str, i, delim_chars)
295 local s = str:sub(i, x - 1)
296 local n = tonumber(s)
297 if not n then
298 decode_error(str, i, "invalid number '" .. s .. "'")
299 end
300 return n, x
301end
302
303
304local function parse_literal(str, i)
305 local x = next_char(str, i, delim_chars)
306 local word = str:sub(i, x - 1)
307 if not literals[word] then
308 decode_error(str, i, "invalid literal '" .. word .. "'")
309 end
310 return literal_map[word], x
311end
312
313
314local function parse_array(str, i)
315 local res = {}
316 local n = 1
317 i = i + 1
318 while 1 do
319 local x
320 i = next_char(str, i, space_chars, true)
321 -- Empty / end of array?
322 if str:sub(i, i) == "]" then
323 i = i + 1
324 break
325 end
326 -- Read token
327 x, i = parse(str, i)
328 res[n] = x
329 n = n + 1
330 -- Next token
331 i = next_char(str, i, space_chars, true)
332 local chr = str:sub(i, i)
333 i = i + 1
334 if chr == "]" then break end
335 if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
336 end
337 return res, i
338end
339
340
341local function parse_object(str, i)
342 local res = {}
343 i = i + 1
344 while 1 do
345 local key, val
346 i = next_char(str, i, space_chars, true)
347 -- Empty / end of object?
348 if str:sub(i, i) == "}" then
349 i = i + 1
350 break
351 end
352 -- Read key
353 if str:sub(i, i) ~= '"' then
354 decode_error(str, i, "expected string for key")
355 end
356 key, i = parse(str, i)
357 -- Read ':' delimiter
358 i = next_char(str, i, space_chars, true)
359 if str:sub(i, i) ~= ":" then
360 decode_error(str, i, "expected ':' after key")
361 end
362 i = next_char(str, i + 1, space_chars, true)
363 -- Read value
364 val, i = parse(str, i)
365 -- Set
366 res[key] = val
367 -- Next token
368 i = next_char(str, i, space_chars, true)
369 local chr = str:sub(i, i)
370 i = i + 1
371 if chr == "}" then break end
372 if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
373 end
374 return res, i
375end
376
377
378local char_func_map = {
379 [ '"' ] = parse_string,
380 [ "0" ] = parse_number,
381 [ "1" ] = parse_number,
382 [ "2" ] = parse_number,
383 [ "3" ] = parse_number,
384 [ "4" ] = parse_number,
385 [ "5" ] = parse_number,
386 [ "6" ] = parse_number,
387 [ "7" ] = parse_number,
388 [ "8" ] = parse_number,
389 [ "9" ] = parse_number,
390 [ "-" ] = parse_number,
391 [ "t" ] = parse_literal,
392 [ "f" ] = parse_literal,
393 [ "n" ] = parse_literal,
394 [ "[" ] = parse_array,
395 [ "{" ] = parse_object,
396}
397
398
399parse = function(str, idx)
400 local chr = str:sub(idx, idx)
401 local f = char_func_map[chr]
402 if f then
403 return f(str, idx)
404 end
405 decode_error(str, idx, "unexpected character '" .. chr .. "'")
406end
407
408
409function json.decode(str)
410 if type(str) ~= "string" then
411 error("expected argument of type string, got " .. type(str))
412 end
413 local res, idx = parse(str, next_char(str, 1, space_chars, true))
414 idx = next_char(str, idx, space_chars, true)
415 if idx <= #str then
416 decode_error(str, idx, "trailing garbage")
417 end
418 return res
419end
420
Top