forked from Egor-Skriptunoff/json4lua
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjson.lua
executable file
·741 lines (691 loc) · 36 KB
/
json.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
-----------------------------------------------------------------------------
-- JSON4Lua: JSON encoding / decoding / traversing support for the Lua language.
-- json Module.
-- Authors: Craig Mason-Jones, Egor Skriptunoff
-- Version: 1.2.1
-- 2017-05-10
-- This module is released under the MIT License (MIT).
-- Please see LICENCE.txt for details:
-- https://github.com/craigmj/json4lua/blob/master/doc/LICENCE.txt
--
-- USAGE:
-- This module exposes three functions:
-- json.encode(obj)
-- Accepts Lua value (table/string/boolean/number/nil/json.null/json.empty) and returns JSON string.
-- json.decode(s)
-- Accepts JSON (as string or as loader function) and returns Lua object.
-- json.traverse(s, callback)
-- Accepts JSON (as string or as loader function) and user-supplied callback function, returns nothing
-- Traverses the JSON, sends each item to callback function, no memory-consuming Lua objects are being created.
--
-- REQUIREMENTS:
-- Lua 5.1, Lua 5.2, Lua 5.3 or LuaJIT
--
-- CHANGELOG
-- 1.2.1 Now you can partially decode JSON while traversing it (callback function should return true).
-- 1.2.0 Some improvements made to be able to use this module on RAM restricted devices:
-- To read large JSONs, you can now provide "loader function" instead of preloading whole JSON as Lua string.
-- Added json.traverse() to traverse JSON using callback function (without creating arrays/objects in Lua).
-- Now, instead of decoding whole JSON, you can decode its arbitrary element (e.g, array or object)
-- by specifying the position where this element starts.
-- In order to do that, at first you have to traverse JSON to get all positions you need.
-- Most of the code rewritten to improve performance.
-- Decoder now understands extended syntax beyond strict JSON standard:
-- In arrays:
-- trailing comma is ignored: [1,2,3,] -> [1,2,3]
-- missing values are nulls: [,,42,,] -> [null,null,42,null]
-- In objects:
-- missing values are ignored: {,,"a":42,,} -> {"a":42}
-- unquoted identifiers are valid keys: {a$b_5:42} -> {"a$b_5":42}
-- Encoder now accepts both 0-based and 1-based Lua arrays (but decoder always converts JSON arrays to 1-based Lua arrays).
-- Some minor bugs fixed.
-- 1.1.0 Modifications made by Egor Skriptunoff, based on version 1.0.0 taken from
-- https://github.com/craigmj/json4lua/blob/40fb13b0ec4a70e36f88812848511c5867bed857/json/json.lua.
-- Added Lua 5.2 and Lua 5.3 compatibility.
-- Removed Lua 5.0 compatibility.
-- Introduced json.empty (Lua counterpart for empty JSON object)
-- Bugs fixed:
-- Attempt to encode Lua table {[10^9]=0} raises an out-of-memory error.
-- Zero bytes '\0' in Lua strings are not escaped by encoder.
-- JSON numbers with capital "E" (as in 1E+100) are not accepted by decoder.
-- All nulls in a JSON arrays are skipped by decoder, sparse arrays could not be loaded correctly.
-- UTF-16 surrogate pairs in JSON strings are not recognised by decoder.
-- 1.0.0 Merged Amr Hassan's changes
-- 0.9.30 Changed to MIT Licence.
-- 0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix).
-- Fixed Lua 5.1 compatibility issues.
-- Introduced json.null to have null values in associative arrays.
-- json.encode() performance improvement (more than 50%) through table_concat rather than ..
-- Introduced decode ability to ignore /**/ comments in the JSON string.
-- 0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays.
-- 0.9.00 First release
--
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Module declaration
-----------------------------------------------------------------------------
local json = {}
do
-----------------------------------------------------------------------------
-- Imports and dependencies
-----------------------------------------------------------------------------
local math, string, table = require'math', require'string', require'table'
local math_floor, math_max, math_type = math.floor, math.max, math.type or function() end
local string_char, string_sub, string_find, string_match, string_gsub, string_format
= string.char, string.sub, string.find, string.match, string.gsub, string.format
local table_insert, table_remove, table_concat = table.insert, table.remove, table.concat
local type, tostring, pairs, assert, error = type, tostring, pairs, assert, error
local loadstring = loadstring or load
-----------------------------------------------------------------------------
-- Public functions
-----------------------------------------------------------------------------
-- function json.encode(obj) encodes Lua value to JSON, returns JSON as string.
-- function json.decode(s, pos) decodes JSON, returns the decoded result as Lua value (may be very memory-consuming).
-- Both functions json.encode() and json.decode() work with "special" Lua values json.null and json.empty
-- special Lua value json.null = JSON value null
-- special Lua value json.empty = JSON value {} (empty JSON object)
-- regular Lua empty table = JSON value [] (empty JSON array)
-- Empty JSON objects and JSON nulls require special handling upon sending (encoding).
-- Please make sure that you send empty JSON objects as json.empty (instead of empty Lua table).
-- Empty Lua tables will be encoded as empty JSON arrays, not as empty JSON objects!
-- json.encode( {empt_obj = json.empty, empt_arr = {}} ) --> {"empt_obj":{},"empt_arr":[]}
-- Also make sure you send JSON nulls as json.null (instead of nil).
-- json.encode( {correct = json.null, incorrect = nil} ) --> {"correct":null}
-- Empty JSON objects and JSON nulls require special handling upon receiving (decoding).
-- After receiving the result of decoding, every Lua table returned (including nested tables) should firstly
-- be compared with special Lua values json.empty/json.null prior to making operations on these values.
-- If you don't need to distinguish between empty JSON objects and empty JSON arrays,
-- json.empty may be replaced with newly created regular empty Lua table.
-- v = (v == json.empty) and {} or v
-- If you don't need special handling of JSON nulls, you may replace json.null with nil to make them disappear.
-- if v == json.null then v = nil end
-- Function json.traverse(s, callback, pos) traverses JSON using user-supplied callback function, returns nothing.
-- Traverse is useful to reduce memory usage: no memory-consuming objects are being created in Lua while traversing.
-- Each item found inside JSON will be sent to callback function passing the following arguments:
-- (path, json_type, value, pos, pos_last)
-- path is array of nested JSON identifiers/indices, "path" is empty for root JSON element
-- json_type is one of "null"/"boolean"/"number"/"string"/"array"/"object"
-- value is defined when json_type is "null"/"boolean"/"number"/"string", value == nil for "object"/"array"
-- pos is 1-based index of first character of current JSON element
-- pos_last is 1-based index of last character of current JSON element (defined only when "value" ~= nil)
-- "path" table reference is the same on each callback invocation, but its content differs every time.
-- Do not modify "path" array inside your callback function, use it as read-only.
-- Do not save reference to "path" for future use (create shallow table copy instead).
-- callback function should return a value, when it is invoked with argument "value" == nil
-- a truthy value means user wants to decode this JSON object/array and create its Lua counterpart (this may be memory-consuming)
-- a falsy value (or no value returned) means user wants to traverse through this JSON object/array
-- (returned value is ignored when callback function is invoked with value ~= nil)
-- Traverse examples:
-- json.traverse([[ 42 ]], callback)
-- will invoke callback 1 time:
-- path json_type value pos pos_last
-- ---------- --------- -------------- --- --------
-- callback( {}, "number", 42, 2, 3 )
--
-- json.traverse([[ {"a":true, "b":null, "c":["one","two"], "d":{ "e":{}, "f":[] } } ]], callback)
-- will invoke callback 9 times:
-- path json_type value pos pos_last
-- ---------- --------- -------------- --- --------
-- callback( {}, "object", nil, 2, nil )
-- callback( {"a"}, "boolean", true, 7, 10 )
-- callback( {"b"}, "null", json.null, 17, 20 ) -- special Lua value for JSON null
-- callback( {"c"}, "array", nil, 27, nil )
-- callback( {"c", 1}, "string", "one", 28, 32 )
-- callback( {"c", 2}, "string", "two", 34, 38 )
-- callback( {"d"}, "object", nil, 46, nil )
-- callback( {"d", "e"}, "object", nil, 52, nil )
-- callback( {"d", "f"}, "array", nil, 60, nil )
--
-- json.traverse([[ {"a":true, "b":null, "c":["one","two"], "d":{ "e":{}, "f":[] } } ]], callback)
-- will invoke callback 9 times if callback returns true when invoked for array "c" and object "e":
-- path json_type value pos pos_last
-- ---------- --------- -------------- --- --------
-- callback( {}, "object", nil, 2, nil )
-- callback( {"a"}, "boolean", true, 7, 10 )
-- callback( {"b"}, "null", json.null, 17, 20 )
-- callback( {"c"}, "array", nil, 27, nil ) -- this callback returned true (user wants to decode this array)
-- callback( {"c"}, "array", {"one", "two"}, 27, 39 ) -- the next invocation brings the result of decoding
-- callback( {"d"}, "object", nil, 46, nil )
-- callback( {"d", "e"}, "object", nil, 52, nil ) -- this callback returned true (user wants to decode this object)
-- callback( {"d", "e"}, "object", json.empty, 52, 53 ) -- the next invocation brings the result of decoding (special Lua value for empty JSON object)
-- callback( {"d", "f"}, "array", nil, 60, nil )
-- Both decoder functions json.decode(s) and json.traverse(s, callback) can accept JSON (argument s)
-- as a "loader function" instead of a string.
-- This function will be called repeatedly to return next parts (substrings) of JSON.
-- An empty string, nil, or no value returned from "loader function" means the end of JSON.
-- This may be useful for low-memory devices or for traversing huge JSON files.
--- The json.null table allows one to specify a null value in an associative array (which is otherwise
-- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null }
local null = {"This Lua table is used to designate JSON null value, compare your values with json.null to determine JSON nulls"}
json.null = setmetatable(null, {
__tostring = function() return 'null' end
})
--- The json.empty table allows one to specify an empty JSON object.
-- To encode empty JSON array use usual empty Lua table.
-- Example: t = { empty_object=json.empty, empty_array={} }
local empty = {}
json.empty = setmetatable(empty, {
__tostring = function() return '{}' end,
__newindex = function() error("json.empty is an read-only Lua table", 2) end
})
-----------------------------------------------------------------------------
-- Private functions
-----------------------------------------------------------------------------
local decode
local decode_scanArray
local decode_scanConstant
local decode_scanNumber
local decode_scanObject
local decode_scanString
local decode_scanIdentifier
local decode_scanWhitespace
local encodeString
local isArray
local isEncodable
local isConvertibleToString
local isRegularNumber
-----------------------------------------------------------------------------
-- PUBLIC FUNCTIONS
-----------------------------------------------------------------------------
--- Encodes an arbitrary Lua object / variable.
-- @param obj Lua value (table/string/boolean/number/nil/json.null/json.empty) to be JSON-encoded.
-- @return string String containing the JSON encoding.
function json.encode(obj)
-- Handle nil and null values
if obj == nil or obj == null then
return 'null'
end
-- Handle empty JSON object
if obj == empty then
return '{}'
end
local obj_type = type(obj)
-- Handle strings
if obj_type == 'string' then
return '"'..encodeString(obj)..'"'
end
-- Handle booleans
if obj_type == 'boolean' then
return tostring(obj)
end
-- Handle numbers
if obj_type == 'number' then
assert(isRegularNumber(obj), 'numeric values Inf and NaN are unsupported')
return math_type(obj) == 'integer' and tostring(obj) or string_format('%.17g', obj)
end
-- Handle tables
if obj_type == 'table' then
local rval = {}
-- Consider arrays separately
local bArray, maxCount = isArray(obj)
if bArray then
for i = obj[0] ~= nil and 0 or 1, maxCount do
table_insert(rval, json.encode(obj[i]))
end
else -- An object, not an array
for i, j in pairs(obj) do
if isConvertibleToString(i) and isEncodable(j) then
table_insert(rval, '"'..encodeString(i)..'":'..json.encode(j))
end
end
end
if bArray then
return '['..table_concat(rval, ',')..']'
else
return '{'..table_concat(rval, ',')..'}'
end
end
error('Unable to JSON-encode Lua value of unsupported type "'..obj_type..'": '..tostring(obj))
end
local function create_state(s)
-- Argument s may be "whole JSON string" or "JSON loader function"
-- Returns "state" object which holds current state of reading long JSON:
-- part = current part (substring of long JSON string)
-- disp = number of bytes before current part inside long JSON
-- more = function to load next substring (more == nil if all substrings are already read)
local state = {disp = 0}
if type(s) == "string" then
-- s is whole JSON string
state.part = s
else
-- s is loader function
state.part = ""
state.more = s
end
return state
end
--- Decodes a JSON string and returns the decoded value as a Lua data structure / value.
-- @param s The string to scan (or "loader function" for getting next substring).
-- @param pos (optional) The position inside s to start scan, default = 1.
-- @return Lua object The object that was scanned, as a Lua table / string / number / boolean / json.null / json.empty.
function json.decode(s, pos)
return (decode(create_state(s), pos or 1))
end
--- Traverses a JSON string, sends everything to user-supplied callback function, returns nothing
-- @param s The string to scan (or "loader function" for getting next substring).
-- @param callback The user-supplied callback function which accepts arguments (path, json_type, value, pos, pos_last).
-- @param pos (optional) The position inside s to start scan, default = 1.
function json.traverse(s, callback, pos)
decode(create_state(s), pos or 1, {path = {}, callback = callback})
end
local function read_ahead(state, startPos)
-- Make sure there are at least 32 bytes read ahead
local endPos = startPos + 31
local part = state.part -- current part (substring of "whole JSON" string)
local disp = state.disp -- number of bytes before current part inside "whole JSON" string
local more = state.more -- function to load next substring
assert(startPos > disp)
while more and disp + #part < endPos do
-- (disp + 1) ... (disp + #part) - we already have this segment now
-- startPos ... endPos - we need to have this segment
local next_substr = more()
if not next_substr or next_substr == "" then
more = nil
else
disp, part = disp + #part, string_sub(part, startPos - disp)
disp, part = disp - #part, part..next_substr
end
end
state.disp, state.part, state.more = disp, part, more
end
local function get_word(state, startPos, length)
-- 1 <= length <= 32
if state.more then read_ahead(state, startPos) end
local idx = startPos - state.disp
return string_sub(state.part, idx, idx + length - 1)
end
local function skip_until_word(state, startPos, word)
-- #word < 30
-- returns position after that word (nil if not found)
repeat
if state.more then read_ahead(state, startPos) end
local part, disp = state.part, state.disp
local b, e = string_find(part, word, startPos - disp, true)
if b then
return disp + e + 1
end
startPos = disp + #part + 2 - #word
until not state.more
end
local function match_with_pattern(state, startPos, pattern, operation)
-- pattern must be
-- "^[some set of chars]+"
-- returns
-- matched_string, endPos for operation "read" (matched_string == "" if no match found)
-- endPos for operation "skip"
if operation == "read" then
local t = {}
repeat
if state.more then read_ahead(state, startPos) end
local part, disp = state.part, state.disp
local str = string_match(part, pattern, startPos - disp)
if str then
table_insert(t, str)
startPos = startPos + #str
end
until not str or startPos <= disp + #part
return table_concat(t), startPos
elseif operation == "skip" then
repeat
if state.more then read_ahead(state, startPos) end
local part, disp = state.part, state.disp
local b, e = string_find(part, pattern, startPos - disp)
if b then
startPos = startPos + e - b + 1
end
until not b or startPos <= disp + #part
return startPos
else
error("Wrong operation name")
end
end
--- Decodes a JSON string and returns the decoded value as a Lua data structure / value.
-- @param state The state of JSON reader.
-- @param startPos Starting position where the JSON string is located.
-- @param traverse (optional) table with fields "path" and "callback" for traversing JSON.
-- @param decode_key (optional) boolean flag for decoding key inside JSON object.
-- @return Lua_object,int The object that was scanned, as a Lua table / string / number / boolean / json.null / json.empty,
-- and the position of the first character after the scanned JSON object.
function decode(state, startPos, traverse, decode_key)
local curChar, value, nextPos
startPos, curChar = decode_scanWhitespace(state, startPos)
if curChar == '{' and not decode_key then
-- Object
if traverse and traverse.callback(traverse.path, "object", nil, startPos, nil) then
-- user wants to decode this JSON object (and get it as Lua value) while traversing
local object, endPos = decode_scanObject(state, startPos)
traverse.callback(traverse.path, "object", object, startPos, endPos - 1)
return false, endPos
end
return decode_scanObject(state, startPos, traverse)
elseif curChar == '[' and not decode_key then
-- Array
if traverse and traverse.callback(traverse.path, "array", nil, startPos, nil) then
-- user wants to decode this JSON array (and get it as Lua value) while traversing
local array, endPos = decode_scanArray(state, startPos)
traverse.callback(traverse.path, "array", array, startPos, endPos - 1)
return false, endPos
end
return decode_scanArray(state, startPos, traverse)
elseif curChar == '"' then
-- String
value, nextPos = decode_scanString(state, startPos)
if traverse then
traverse.callback(traverse.path, "string", value, startPos, nextPos - 1)
end
elseif decode_key then
-- Unquoted string as key name
return decode_scanIdentifier(state, startPos)
elseif string_find(curChar, "^[%d%-]") then
-- Number
value, nextPos = decode_scanNumber(state, startPos)
if traverse then
traverse.callback(traverse.path, "number", value, startPos, nextPos - 1)
end
else
-- Otherwise, it must be a constant
value, nextPos = decode_scanConstant(state, startPos)
if traverse then
traverse.callback(traverse.path, value == null and "null" or "boolean", value, startPos, nextPos - 1)
end
end
return value, nextPos
end
-----------------------------------------------------------------------------
-- Internal, PRIVATE functions.
-- Following a Python-like convention, I have prefixed all these 'PRIVATE'
-- functions with an underscore.
-----------------------------------------------------------------------------
--- Scans an array from JSON into a Lua object
-- startPos begins at the start of the array.
-- Returns the array and the next starting position
-- @param state The state of JSON reader.
-- @param startPos The starting position for the scan.
-- @param traverse (optional) table with fields "path" and "callback" for traversing JSON.
-- @return table,int The scanned array as a table, and the position of the next character to scan.
function decode_scanArray(state, startPos, traverse)
local array = not traverse and {} -- The return value
local elem_index, elem_ready, object = 1
startPos = startPos + 1
-- Infinite loop for array elements
while true do
repeat
local curChar
startPos, curChar = decode_scanWhitespace(state, startPos)
if curChar == ']' then
return array, startPos + 1
elseif curChar == ',' then
if not elem_ready then
-- missing value in JSON array
if traverse then
table_insert(traverse.path, elem_index)
traverse.callback(traverse.path, "null", null, startPos, startPos - 1) -- empty substring: pos_last = pos - 1
table_remove(traverse.path)
else
array[elem_index] = null
end
end
elem_ready = false
elem_index = elem_index + 1
startPos = startPos + 1
end
until curChar ~= ','
if elem_ready then
error('Comma is missing in JSON array at position '..startPos)
end
if traverse then
table_insert(traverse.path, elem_index)
end
object, startPos = decode(state, startPos, traverse)
if traverse then
table_remove(traverse.path)
else
array[elem_index] = object
end
elem_ready = true
end
end
--- Scans for given constants: true, false or null
-- Returns the appropriate Lua type, and the position of the next character to read.
-- @param state The state of JSON reader.
-- @param startPos The position in the string at which to start scanning.
-- @return object, int The object (true, false or json.null) and the position at which the next character should be scanned.
function decode_scanConstant(state, startPos)
local w5 = get_word(state, startPos, 5)
local w4 = string_sub(w5, 1, 4)
if w5 == "false" then
return false, startPos + 5
elseif w4 == "true" then
return true, startPos + 4
elseif w4 == "null" then
return null, startPos + 4
end
error('Failed to parse JSON at position '..startPos)
end
--- Scans a number from the JSON encoded string.
-- (in fact, also is able to scan numeric +- eqns, which is not in the JSON spec.)
-- Returns the number, and the position of the next character after the number.
-- @param state The state of JSON reader.
-- @param startPos The position at which to start scanning.
-- @return number,int The extracted number and the position of the next character to scan.
function decode_scanNumber(state, startPos)
local stringValue, endPos = match_with_pattern(state, startPos, '^[%+%-%d%.eE]+', "read")
local stringEval = loadstring('return '..stringValue)
if not stringEval then
error('Failed to scan number '..stringValue..' in JSON string at position '..startPos)
end
return stringEval(), endPos
end
--- Scans a JSON object into a Lua object.
-- startPos begins at the start of the object.
-- Returns the object and the next starting position.
-- @param state The state of JSON reader.
-- @param startPos The starting position of the scan.
-- @param traverse (optional) table with fields "path" and "callback" for traversing JSON
-- @return table,int The scanned object as a table and the position of the next character to scan.
function decode_scanObject(state, startPos, traverse)
local object, elem_ready = not traverse and empty
startPos = startPos + 1
while true do
repeat
local curChar
startPos, curChar = decode_scanWhitespace(state, startPos)
if curChar == '}' then
return object, startPos + 1
elseif curChar == ',' then
startPos = startPos + 1
elem_ready = false
end
until curChar ~= ','
if elem_ready then
error('Comma is missing in JSON object at '..startPos)
end
-- Scan the key as string or unquoted identifier such as in {"a":1,b:2}
local key, value
key, startPos = decode(state, startPos, nil, true)
local colon
startPos, colon = decode_scanWhitespace(state, startPos)
if colon ~= ':' then
error('JSON object key-value assignment mal-formed at '..startPos)
end
startPos = decode_scanWhitespace(state, startPos + 1)
if traverse then
table_insert(traverse.path, key)
end
value, startPos = decode(state, startPos, traverse)
if traverse then
table_remove(traverse.path)
else
if object == empty then
object = {}
end
object[key] = value
end
elem_ready = true
end -- infinite loop while key-value pairs are found
end
--- Scans JSON string for an identifier (unquoted key name inside object)
-- Returns the string extracted as a Lua string, and the position after the closing quote.
-- @param state The state of JSON reader.
-- @param startPos The starting position of the scan.
-- @return string,int The extracted string as a Lua string, and the next character to parse.
function decode_scanIdentifier(state, startPos)
local identifier, idx = match_with_pattern(state, startPos, '^[%w_%-%$]+', "read")
if identifier == "" then
error('JSON String decoding failed: missing key name at position '..startPos)
end
return identifier, idx
end
-- START SoniEx2
-- Initialize some things used by decode_scanString
-- You know, for efficiency
local escapeSequences = { t = "\t", f = "\f", r = "\r", n = "\n", b = "\b" }
-- END SoniEx2
--- Scans a JSON string from the opening quote to the end of the string.
-- Returns the string extracted as a Lua string, and the position after the closing quote.
-- @param state The state of JSON reader.
-- @param startPos The starting position of the scan.
-- @return string,int The extracted string as a Lua string, and the next character to parse.
function decode_scanString(state, startPos)
local t, idx, surrogate_pair_started, regular_part = {}, startPos + 1
while true do
regular_part, idx = match_with_pattern(state, idx, '^[^"\\]+', "read")
table_insert(t, regular_part)
local w6 = get_word(state, idx, 6)
local c = string_sub(w6, 1, 1)
if c == '"' then
return table_concat(t), idx + 1
elseif c == '\\' then
local esc = string_sub(w6, 2, 2)
if esc == "u" then
local n = tonumber(string_sub(w6, 3), 16)
if not n then
error("String decoding failed: bad Unicode escape "..w6.." at position "..idx)
end
-- Handling of UTF-16 surrogate pairs
if n >= 0xD800 and n < 0xDC00 then
surrogate_pair_started, n = n
elseif n >= 0xDC00 and n < 0xE000 then
n, surrogate_pair_started = surrogate_pair_started and (surrogate_pair_started - 0xD800) * 0x400 + (n - 0xDC00) + 0x10000
end
if n then
-- Convert unicode codepoint n (0..0x10FFFF) to UTF-8 string
local x
if n < 0x80 then
x = string_char(n % 0x80)
elseif n < 0x800 then
-- [110x xxxx] [10xx xxxx]
x = string_char(0xC0 + (math_floor(n/64) % 0x20), 0x80 + (n % 0x40))
elseif n < 0x10000 then
-- [1110 xxxx] [10xx xxxx] [10xx xxxx]
x = string_char(0xE0 + (math_floor(n/64/64) % 0x10), 0x80 + (math_floor(n/64) % 0x40), 0x80 + (n % 0x40))
else
-- [1111 0xxx] [10xx xxxx] [10xx xxxx] [10xx xxxx]
x = string_char(0xF0 + (math_floor(n/64/64/64) % 8), 0x80 + (math_floor(n/64/64) % 0x40), 0x80 + (math_floor(n/64) % 0x40), 0x80 + (n % 0x40))
end
table_insert(t, x)
end
idx = idx + 6
else
table_insert(t, escapeSequences[esc] or esc)
idx = idx + 2
end
else
error('String decoding failed: missing closing " for string at position '..startPos)
end
end
end
--- Scans a JSON string skipping all whitespace from the current start position.
-- Returns the position of the first non-whitespace character.
-- @param state The state of JSON reader.
-- @param startPos The starting position where we should begin removing whitespace.
-- @return int,char The first position where non-whitespace was encountered, non-whitespace char.
function decode_scanWhitespace(state, startPos)
while true do
startPos = match_with_pattern(state, startPos, '^[ \n\r\t]+', "skip")
local w2 = get_word(state, startPos, 2)
if w2 == '/*' then
local endPos = skip_until_word(state, startPos + 2, '*/')
if not endPos then
error("Unterminated comment in JSON string at "..startPos)
end
startPos = endPos
else
local next_char = string_sub(w2, 1, 1)
if next_char == '' then
error('Unexpected end of JSON')
end
return startPos, next_char
end
end
end
--- Encodes a string to be JSON-compatible.
-- This just involves backslash-escaping of quotes, slashes and control codes
-- @param s The string to return as a JSON encoded (i.e. backquoted string)
-- @return string The string appropriately escaped.
local escapeList = {
['"'] = '\\"',
['\\'] = '\\\\',
['/'] = '\\/',
['\b'] = '\\b',
['\f'] = '\\f',
['\n'] = '\\n',
['\r'] = '\\r',
['\t'] = '\\t',
['\127'] = '\\u007F'
}
function encodeString(s)
if type(s) == 'number' then
s = math_type(s) == 'integer' and tostring(s) or string_format('%.f', s)
end
return string_gsub(s, ".", function(c) return escapeList[c] or c:byte() < 32 and string_format('\\u%04X', c:byte()) end)
end
-- Determines whether the given Lua type is an array or a table / dictionary.
-- We consider any table an array if it has indexes 1..n for its n items, and no other data in the table.
-- I think this method is currently a little 'flaky', but can't think of a good way around it yet...
-- @param t The table to evaluate as an array
-- @return boolean,number True if the table can be represented as an array, false otherwise.
-- If true, the second returned value is the maximum number of indexed elements in the array.
function isArray(t)
-- Next we count all the elements, ensuring that any non-indexed elements are not-encodable
-- (with the possible exception of 'n')
local maxIndex = 0
for k, v in pairs(t) do
if type(k) == 'number' and math_floor(k) == k and 0 <= k and k <= 1e6 then -- k,v is an indexed pair
if not isEncodable(v) then -- All array elements must be encodable
return false
end
maxIndex = math_max(maxIndex, k)
elseif not (k == 'n' and v == #t) then -- if it is n, then n does not hold the number of elements
if isConvertibleToString(k) and isEncodable(v) then
return false
end
end -- End of k,v not an indexed pair
end -- End of loop across all pairs
return true, maxIndex
end
--- Determines whether the given Lua object / table / value can be JSON encoded.
-- The only types that are JSON encodable are: string, boolean, number, nil, table and special tables json.null and json.empty.
-- @param o The object to examine.
-- @return boolean True if the object should be JSON encoded, false if it should be ignored.
function isEncodable(o)
local t = type(o)
return t == 'string' or t == 'boolean' or t == 'number' and isRegularNumber(o) or t == 'nil' or t == 'table'
end
--- Determines whether the given Lua object / table / variable can be a JSON key.
-- Integer Lua numbers are allowed to be considered as valid string keys in JSON.
-- @param o The object to examine.
-- @return boolean True if the object can be converted to a string, false if it should be ignored.
function isConvertibleToString(o)
local t = type(o)
return t == 'string' or t == 'number' and isRegularNumber(o) and (math_type(o) == 'integer' or math_floor(o) == o)
end
local is_Inf_or_NaN = {[tostring(1/0)]=true, [tostring(-1/0)]=true, [tostring(0/0)]=true, [tostring(-(0/0))]=true}
--- Determines whether the given Lua number is a regular number or Inf/Nan.
-- @param v The number to examine.
-- @return boolean True if the number is a regular number which may be encoded in JSON.
function isRegularNumber(v)
return not is_Inf_or_NaN[tostring(v)]
end
end
return json