isaacscript-common 6.10.2 → 6.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/functions/deepCopyTests.lua +35 -45
- package/dist/functions/jsonHelpers.d.ts +6 -0
- package/dist/functions/jsonHelpers.d.ts.map +1 -1
- package/dist/functions/jsonHelpers.lua +9 -3
- package/dist/lib/jsonLua.lua +388 -0
- package/package.json +1 -1
- package/src/functions/deepCopyTests.ts +44 -48
- package/src/functions/jsonHelpers.ts +9 -3
- package/src/lib/jsonLua.d.ts +10 -0
- package/src/lib/jsonLua.lua +388 -0
|
@@ -3,10 +3,8 @@ local __TS__TypeOf = ____lualib.__TS__TypeOf
|
|
|
3
3
|
local Map = ____lualib.Map
|
|
4
4
|
local __TS__New = ____lualib.__TS__New
|
|
5
5
|
local Set = ____lualib.Set
|
|
6
|
-
local __TS__Spread = ____lualib.__TS__Spread
|
|
7
|
-
local __TS__ArrayIncludes = ____lualib.__TS__ArrayIncludes
|
|
8
6
|
local ____exports = {}
|
|
9
|
-
local copiedObjectIsTable, copiedObjectHasKeyAndValueString, copiedTableHasKeyAndValueNumber, copiedTableDoesNotCoerceTypes, copiedObjectHasNoReferencesForPrimitivesForward, copiedObjectHasNoReferencesForPrimitivesBackward, copiedObjectHasNoReferencesForArray, copiedObjectHasChildObject, copiedMapIsMap, copiedMapHasValue, copiedSetIsSet, copiedSetHasValue, copiedMapHasChildMap, copiedDefaultMapHasChildDefaultMap, copiedDefaultMapHasBrand,
|
|
7
|
+
local copiedObjectIsTable, copiedObjectHasKeyAndValueString, copiedTableHasKeyAndValueNumber, copiedTableDoesNotCoerceTypes, copiedObjectHasNoReferencesForPrimitivesForward, copiedObjectHasNoReferencesForPrimitivesBackward, copiedObjectHasNoReferencesForArray, copiedObjectHasChildObject, copiedMapIsMap, copiedMapHasValue, copiedSetIsSet, copiedSetHasValue, copiedMapHasChildMap, copiedDefaultMapHasChildDefaultMap, copiedDefaultMapHasBrand, copiedSerializedMapHasStringKey, copiedSerializedMapHasNumberKey, copiedSerializedDefaultMapHasStringKey, copiedSerializedDefaultMapHasNumberKey
|
|
10
8
|
local ____DefaultMap = require("classes.DefaultMap")
|
|
11
9
|
local DefaultMap = ____DefaultMap.DefaultMap
|
|
12
10
|
local ____SerializationBrand = require("enums.private.SerializationBrand")
|
|
@@ -299,62 +297,54 @@ function copiedDefaultMapHasBrand(self)
|
|
|
299
297
|
error("The copied DefaultMap does not have the brand: " .. SerializationBrand.DEFAULT_MAP)
|
|
300
298
|
end
|
|
301
299
|
end
|
|
302
|
-
function
|
|
300
|
+
function copiedSerializedMapHasStringKey(self)
|
|
303
301
|
local mapKey = "123"
|
|
304
302
|
local mapValue = 456
|
|
305
303
|
local oldMap = __TS__New(Map)
|
|
306
304
|
oldMap:set(mapKey, mapValue)
|
|
307
|
-
local
|
|
308
|
-
local newTable = deepCopy(nil,
|
|
305
|
+
local serializedOldMap = deepCopy(nil, oldMap, SerializationType.SERIALIZE, "copiedSerializedMapHasStringKey-serialize")
|
|
306
|
+
local newTable = deepCopy(nil, serializedOldMap, SerializationType.DESERIALIZE, "copiedSerializedMapHasStringKey-deserialize")
|
|
309
307
|
local newMap = newTable
|
|
310
|
-
if not
|
|
311
|
-
|
|
312
|
-
mapKey
|
|
313
|
-
) then
|
|
314
|
-
error((("The copied Map did not have a key of: " .. mapKey) .. " with type ") .. type(mapKey))
|
|
308
|
+
if not newMap:has(mapKey) then
|
|
309
|
+
local keyType = type(mapKey)
|
|
310
|
+
error((("The copied Map did not have a key of: " .. mapKey) .. " with type ") .. keyType)
|
|
315
311
|
end
|
|
316
312
|
end
|
|
317
|
-
function
|
|
313
|
+
function copiedSerializedMapHasNumberKey(self)
|
|
318
314
|
local mapKey = 123
|
|
319
315
|
local mapValue = 456
|
|
320
316
|
local oldMap = __TS__New(Map)
|
|
321
317
|
oldMap:set(mapKey, mapValue)
|
|
322
|
-
local
|
|
323
|
-
local newTable = deepCopy(nil,
|
|
318
|
+
local serializedOldMap = deepCopy(nil, oldMap, SerializationType.SERIALIZE, "copiedSerializedMapHasNumberKey-serialize")
|
|
319
|
+
local newTable = deepCopy(nil, serializedOldMap, SerializationType.DESERIALIZE, "copiedSerializedMapHasNumberKey-deserialize")
|
|
324
320
|
local newMap = newTable
|
|
325
|
-
if not
|
|
326
|
-
|
|
327
|
-
mapKey
|
|
328
|
-
) then
|
|
329
|
-
error((("The copied Map did not have a key of: " .. tostring(mapKey)) .. " with type ") .. type(mapKey))
|
|
321
|
+
if not newMap:has(mapKey) then
|
|
322
|
+
local keyType = type(mapKey)
|
|
323
|
+
error((("The copied Map did not have a key of: " .. tostring(mapKey)) .. " with type ") .. keyType)
|
|
330
324
|
end
|
|
331
325
|
end
|
|
332
|
-
function
|
|
326
|
+
function copiedSerializedDefaultMapHasStringKey(self)
|
|
333
327
|
local mapKey = "123"
|
|
334
|
-
local
|
|
335
|
-
|
|
336
|
-
local
|
|
337
|
-
local newTable = deepCopy(nil,
|
|
338
|
-
local
|
|
339
|
-
if not
|
|
340
|
-
|
|
341
|
-
mapKey
|
|
342
|
-
) then
|
|
343
|
-
error((("The copied DefaultMap did not have a key of: " .. mapKey) .. " with type ") .. type(mapKey))
|
|
328
|
+
local oldDefaultMap = __TS__New(DefaultMap, 456)
|
|
329
|
+
oldDefaultMap:getAndSetDefault(mapKey)
|
|
330
|
+
local serializedOldDefaultMap = deepCopy(nil, oldDefaultMap, SerializationType.SERIALIZE, "copiedSerializedDefaultMapHasStringKey-serialize")
|
|
331
|
+
local newTable = deepCopy(nil, serializedOldDefaultMap, SerializationType.DESERIALIZE, "copiedSerializedDefaultMapHasStringKey-deserialize")
|
|
332
|
+
local newDefaultMap = newTable
|
|
333
|
+
if not newDefaultMap:has(mapKey) then
|
|
334
|
+
local keyType = type(mapKey)
|
|
335
|
+
error((("The copied DefaultMap did not have a key of \"" .. mapKey) .. "\" with type: ") .. keyType)
|
|
344
336
|
end
|
|
345
337
|
end
|
|
346
|
-
function
|
|
338
|
+
function copiedSerializedDefaultMapHasNumberKey(self)
|
|
347
339
|
local mapKey = 123
|
|
348
|
-
local
|
|
349
|
-
|
|
350
|
-
local
|
|
351
|
-
local newTable = deepCopy(nil,
|
|
352
|
-
local
|
|
353
|
-
if not
|
|
354
|
-
|
|
355
|
-
mapKey
|
|
356
|
-
) then
|
|
357
|
-
error((("The copied DefaultMap did not have a key of: " .. tostring(mapKey)) .. " with type ") .. type(mapKey))
|
|
340
|
+
local oldDefaultMap = __TS__New(DefaultMap, 456)
|
|
341
|
+
oldDefaultMap:getAndSetDefault(mapKey)
|
|
342
|
+
local serializedOldDefaultMap = deepCopy(nil, oldDefaultMap, SerializationType.SERIALIZE, "copiedSerializedDefaultMapHasNumberKey-serialize")
|
|
343
|
+
local newTable = deepCopy(nil, serializedOldDefaultMap, SerializationType.DESERIALIZE, "copiedSerializedDefaultMapHasNumberKey-deserialize")
|
|
344
|
+
local newDefaultMap = newTable
|
|
345
|
+
if not newDefaultMap:has(mapKey) then
|
|
346
|
+
local keyType = type(mapKey)
|
|
347
|
+
error((("The copied DefaultMap did not have a key of: " .. tostring(mapKey)) .. " with type ") .. keyType)
|
|
358
348
|
end
|
|
359
349
|
end
|
|
360
350
|
--- Run the suite of tests that prove that the "deepCopy" helper function works properly.
|
|
@@ -376,10 +366,10 @@ function ____exports.runDeepCopyTests(self)
|
|
|
376
366
|
copiedMapHasChildMap(nil)
|
|
377
367
|
copiedDefaultMapHasChildDefaultMap(nil)
|
|
378
368
|
copiedDefaultMapHasBrand(nil)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
369
|
+
copiedSerializedMapHasStringKey(nil)
|
|
370
|
+
copiedSerializedMapHasNumberKey(nil)
|
|
371
|
+
copiedSerializedDefaultMapHasStringKey(nil)
|
|
372
|
+
copiedSerializedDefaultMapHasNumberKey(nil)
|
|
383
373
|
log("All deep copy tests passed!")
|
|
384
374
|
end
|
|
385
375
|
return ____exports
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* fails, it will return a blank Lua table instead of throwing an error. (This allows execution to
|
|
7
7
|
* continue in cases where users have no current save data or have manually removed their existing
|
|
8
8
|
* save data.)
|
|
9
|
+
*
|
|
10
|
+
* Under the hood, this uses a custom JSON parser that was measured to be 11.8 times faster than the
|
|
11
|
+
* vanilla JSON parser.
|
|
9
12
|
*/
|
|
10
13
|
export declare function jsonDecode(jsonString: string): LuaMap<AnyNotNil, unknown>;
|
|
11
14
|
/**
|
|
@@ -14,6 +17,9 @@ export declare function jsonDecode(jsonString: string): LuaMap<AnyNotNil, unknow
|
|
|
14
17
|
* In most cases, this function will be used for writing data to a "save#.dat" file. If encoding
|
|
15
18
|
* fails, it will throw an error to prevent writing a blank string or corrupted data to a user's
|
|
16
19
|
* "save#.dat" file.
|
|
20
|
+
*
|
|
21
|
+
* Under the hood, this uses a custom JSON parser that was measured to be 11.8 times faster than the
|
|
22
|
+
* vanilla JSON parser.
|
|
17
23
|
*/
|
|
18
24
|
export declare function jsonEncode(luaTable: unknown): string;
|
|
19
25
|
//# sourceMappingURL=jsonHelpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonHelpers.d.ts","sourceRoot":"","sources":["../../src/functions/jsonHelpers.ts"],"names":[],"mappings":";AAWA
|
|
1
|
+
{"version":3,"file":"jsonHelpers.d.ts","sourceRoot":"","sources":["../../src/functions/jsonHelpers.ts"],"names":[],"mappings":";AAWA;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CASzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAOpD"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
local ____exports = {}
|
|
2
|
-
local
|
|
2
|
+
local jsonLua = require("lib.jsonLua")
|
|
3
3
|
local ____log = require("functions.log")
|
|
4
4
|
local logError = ____log.logError
|
|
5
5
|
local function tryDecode(jsonString)
|
|
6
|
-
return
|
|
6
|
+
return jsonLua.decode(jsonString)
|
|
7
7
|
end
|
|
8
8
|
local function tryEncode(luaTable)
|
|
9
|
-
return
|
|
9
|
+
return jsonLua.encode(luaTable)
|
|
10
10
|
end
|
|
11
11
|
--- Converts a JSON string to a Lua table.
|
|
12
12
|
--
|
|
@@ -14,6 +14,9 @@ end
|
|
|
14
14
|
-- fails, it will return a blank Lua table instead of throwing an error. (This allows execution to
|
|
15
15
|
-- continue in cases where users have no current save data or have manually removed their existing
|
|
16
16
|
-- save data.)
|
|
17
|
+
--
|
|
18
|
+
-- Under the hood, this uses a custom JSON parser that was measured to be 11.8 times faster than the
|
|
19
|
+
-- vanilla JSON parser.
|
|
17
20
|
function ____exports.jsonDecode(self, jsonString)
|
|
18
21
|
local ok, luaTableOrErrMsg = pcall(tryDecode, jsonString)
|
|
19
22
|
if not ok then
|
|
@@ -27,6 +30,9 @@ end
|
|
|
27
30
|
-- In most cases, this function will be used for writing data to a "save#.dat" file. If encoding
|
|
28
31
|
-- fails, it will throw an error to prevent writing a blank string or corrupted data to a user's
|
|
29
32
|
-- "save#.dat" file.
|
|
33
|
+
--
|
|
34
|
+
-- Under the hood, this uses a custom JSON parser that was measured to be 11.8 times faster than the
|
|
35
|
+
-- vanilla JSON parser.
|
|
30
36
|
function ____exports.jsonEncode(self, luaTable)
|
|
31
37
|
local ok, jsonStringOrErrMsg = pcall(tryEncode, luaTable)
|
|
32
38
|
if not ok then
|
|
@@ -0,0 +1,388 @@
|
|
|
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
|
+
|
|
25
|
+
local json = { _version = "0.1.2" }
|
|
26
|
+
|
|
27
|
+
-------------------------------------------------------------------------------
|
|
28
|
+
-- Encode
|
|
29
|
+
-------------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
local encode
|
|
32
|
+
|
|
33
|
+
local escape_char_map = {
|
|
34
|
+
[ "\\" ] = "\\",
|
|
35
|
+
[ "\"" ] = "\"",
|
|
36
|
+
[ "\b" ] = "b",
|
|
37
|
+
[ "\f" ] = "f",
|
|
38
|
+
[ "\n" ] = "n",
|
|
39
|
+
[ "\r" ] = "r",
|
|
40
|
+
[ "\t" ] = "t",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
local escape_char_map_inv = { [ "/" ] = "/" }
|
|
44
|
+
for k, v in pairs(escape_char_map) do
|
|
45
|
+
escape_char_map_inv[v] = k
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
local function escape_char(c)
|
|
50
|
+
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
local function encode_nil(val)
|
|
55
|
+
return "null"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
local function encode_table(val, stack)
|
|
60
|
+
local res = {}
|
|
61
|
+
stack = stack or {}
|
|
62
|
+
|
|
63
|
+
-- Circular reference?
|
|
64
|
+
if stack[val] then error("circular reference") end
|
|
65
|
+
|
|
66
|
+
stack[val] = true
|
|
67
|
+
|
|
68
|
+
if rawget(val, 1) ~= nil or next(val) == nil then
|
|
69
|
+
-- Treat as array -- check keys are valid and it is not sparse
|
|
70
|
+
local n = 0
|
|
71
|
+
for k in pairs(val) do
|
|
72
|
+
if type(k) ~= "number" then
|
|
73
|
+
error("invalid table: mixed or invalid key types")
|
|
74
|
+
end
|
|
75
|
+
n = n + 1
|
|
76
|
+
end
|
|
77
|
+
if n ~= #val then
|
|
78
|
+
error("invalid table: sparse array")
|
|
79
|
+
end
|
|
80
|
+
-- Encode
|
|
81
|
+
for i, v in ipairs(val) do
|
|
82
|
+
table.insert(res, encode(v, stack))
|
|
83
|
+
end
|
|
84
|
+
stack[val] = nil
|
|
85
|
+
return "[" .. table.concat(res, ",") .. "]"
|
|
86
|
+
|
|
87
|
+
else
|
|
88
|
+
-- Treat as an object
|
|
89
|
+
for k, v in pairs(val) do
|
|
90
|
+
if type(k) ~= "string" then
|
|
91
|
+
error("invalid table: mixed or invalid key types")
|
|
92
|
+
end
|
|
93
|
+
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
|
94
|
+
end
|
|
95
|
+
stack[val] = nil
|
|
96
|
+
return "{" .. table.concat(res, ",") .. "}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
local function encode_string(val)
|
|
102
|
+
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
local function encode_number(val)
|
|
107
|
+
-- Check for NaN, -inf and inf
|
|
108
|
+
if val ~= val or val <= -math.huge or val >= math.huge then
|
|
109
|
+
error("unexpected number value '" .. tostring(val) .. "'")
|
|
110
|
+
end
|
|
111
|
+
return string.format("%.14g", val)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
local type_func_map = {
|
|
116
|
+
[ "nil" ] = encode_nil,
|
|
117
|
+
[ "table" ] = encode_table,
|
|
118
|
+
[ "string" ] = encode_string,
|
|
119
|
+
[ "number" ] = encode_number,
|
|
120
|
+
[ "boolean" ] = tostring,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
encode = function(val, stack)
|
|
125
|
+
local t = type(val)
|
|
126
|
+
local f = type_func_map[t]
|
|
127
|
+
if f then
|
|
128
|
+
return f(val, stack)
|
|
129
|
+
end
|
|
130
|
+
error("unexpected type '" .. t .. "'")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
function json.encode(val)
|
|
135
|
+
return ( encode(val) )
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
-------------------------------------------------------------------------------
|
|
140
|
+
-- Decode
|
|
141
|
+
-------------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
local parse
|
|
144
|
+
|
|
145
|
+
local function create_set(...)
|
|
146
|
+
local res = {}
|
|
147
|
+
for i = 1, select("#", ...) do
|
|
148
|
+
res[ select(i, ...) ] = true
|
|
149
|
+
end
|
|
150
|
+
return res
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
local space_chars = create_set(" ", "\t", "\r", "\n")
|
|
154
|
+
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
|
155
|
+
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
|
156
|
+
local literals = create_set("true", "false", "null")
|
|
157
|
+
|
|
158
|
+
local literal_map = {
|
|
159
|
+
[ "true" ] = true,
|
|
160
|
+
[ "false" ] = false,
|
|
161
|
+
[ "null" ] = nil,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
local function next_char(str, idx, set, negate)
|
|
166
|
+
for i = idx, #str do
|
|
167
|
+
if set[str:sub(i, i)] ~= negate then
|
|
168
|
+
return i
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
return #str + 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
local function decode_error(str, idx, msg)
|
|
176
|
+
local line_count = 1
|
|
177
|
+
local col_count = 1
|
|
178
|
+
for i = 1, idx - 1 do
|
|
179
|
+
col_count = col_count + 1
|
|
180
|
+
if str:sub(i, i) == "\n" then
|
|
181
|
+
line_count = line_count + 1
|
|
182
|
+
col_count = 1
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
local function codepoint_to_utf8(n)
|
|
190
|
+
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
|
191
|
+
local f = math.floor
|
|
192
|
+
if n <= 0x7f then
|
|
193
|
+
return string.char(n)
|
|
194
|
+
elseif n <= 0x7ff then
|
|
195
|
+
return string.char(f(n / 64) + 192, n % 64 + 128)
|
|
196
|
+
elseif n <= 0xffff then
|
|
197
|
+
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
|
198
|
+
elseif n <= 0x10ffff then
|
|
199
|
+
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
|
200
|
+
f(n % 4096 / 64) + 128, n % 64 + 128)
|
|
201
|
+
end
|
|
202
|
+
error( string.format("invalid unicode codepoint '%x'", n) )
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
local function parse_unicode_escape(s)
|
|
207
|
+
local n1 = tonumber( s:sub(1, 4), 16 )
|
|
208
|
+
local n2 = tonumber( s:sub(7, 10), 16 )
|
|
209
|
+
-- Surrogate pair?
|
|
210
|
+
if n2 then
|
|
211
|
+
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
|
212
|
+
else
|
|
213
|
+
return codepoint_to_utf8(n1)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
local function parse_string(str, i)
|
|
219
|
+
local res = ""
|
|
220
|
+
local j = i + 1
|
|
221
|
+
local k = j
|
|
222
|
+
|
|
223
|
+
while j <= #str do
|
|
224
|
+
local x = str:byte(j)
|
|
225
|
+
|
|
226
|
+
if x < 32 then
|
|
227
|
+
decode_error(str, j, "control character in string")
|
|
228
|
+
|
|
229
|
+
elseif x == 92 then -- `\`: Escape
|
|
230
|
+
res = res .. str:sub(k, j - 1)
|
|
231
|
+
j = j + 1
|
|
232
|
+
local c = str:sub(j, j)
|
|
233
|
+
if c == "u" then
|
|
234
|
+
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
|
|
235
|
+
or str:match("^%x%x%x%x", j + 1)
|
|
236
|
+
or decode_error(str, j - 1, "invalid unicode escape in string")
|
|
237
|
+
res = res .. parse_unicode_escape(hex)
|
|
238
|
+
j = j + #hex
|
|
239
|
+
else
|
|
240
|
+
if not escape_chars[c] then
|
|
241
|
+
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
|
|
242
|
+
end
|
|
243
|
+
res = res .. escape_char_map_inv[c]
|
|
244
|
+
end
|
|
245
|
+
k = j + 1
|
|
246
|
+
|
|
247
|
+
elseif x == 34 then -- `"`: End of string
|
|
248
|
+
res = res .. str:sub(k, j - 1)
|
|
249
|
+
return res, j + 1
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
j = j + 1
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
decode_error(str, i, "expected closing quote for string")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
local function parse_number(str, i)
|
|
260
|
+
local x = next_char(str, i, delim_chars)
|
|
261
|
+
local s = str:sub(i, x - 1)
|
|
262
|
+
local n = tonumber(s)
|
|
263
|
+
if not n then
|
|
264
|
+
decode_error(str, i, "invalid number '" .. s .. "'")
|
|
265
|
+
end
|
|
266
|
+
return n, x
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
local function parse_literal(str, i)
|
|
271
|
+
local x = next_char(str, i, delim_chars)
|
|
272
|
+
local word = str:sub(i, x - 1)
|
|
273
|
+
if not literals[word] then
|
|
274
|
+
decode_error(str, i, "invalid literal '" .. word .. "'")
|
|
275
|
+
end
|
|
276
|
+
return literal_map[word], x
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
local function parse_array(str, i)
|
|
281
|
+
local res = {}
|
|
282
|
+
local n = 1
|
|
283
|
+
i = i + 1
|
|
284
|
+
while 1 do
|
|
285
|
+
local x
|
|
286
|
+
i = next_char(str, i, space_chars, true)
|
|
287
|
+
-- Empty / end of array?
|
|
288
|
+
if str:sub(i, i) == "]" then
|
|
289
|
+
i = i + 1
|
|
290
|
+
break
|
|
291
|
+
end
|
|
292
|
+
-- Read token
|
|
293
|
+
x, i = parse(str, i)
|
|
294
|
+
res[n] = x
|
|
295
|
+
n = n + 1
|
|
296
|
+
-- Next token
|
|
297
|
+
i = next_char(str, i, space_chars, true)
|
|
298
|
+
local chr = str:sub(i, i)
|
|
299
|
+
i = i + 1
|
|
300
|
+
if chr == "]" then break end
|
|
301
|
+
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
|
302
|
+
end
|
|
303
|
+
return res, i
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
local function parse_object(str, i)
|
|
308
|
+
local res = {}
|
|
309
|
+
i = i + 1
|
|
310
|
+
while 1 do
|
|
311
|
+
local key, val
|
|
312
|
+
i = next_char(str, i, space_chars, true)
|
|
313
|
+
-- Empty / end of object?
|
|
314
|
+
if str:sub(i, i) == "}" then
|
|
315
|
+
i = i + 1
|
|
316
|
+
break
|
|
317
|
+
end
|
|
318
|
+
-- Read key
|
|
319
|
+
if str:sub(i, i) ~= '"' then
|
|
320
|
+
decode_error(str, i, "expected string for key")
|
|
321
|
+
end
|
|
322
|
+
key, i = parse(str, i)
|
|
323
|
+
-- Read ':' delimiter
|
|
324
|
+
i = next_char(str, i, space_chars, true)
|
|
325
|
+
if str:sub(i, i) ~= ":" then
|
|
326
|
+
decode_error(str, i, "expected ':' after key")
|
|
327
|
+
end
|
|
328
|
+
i = next_char(str, i + 1, space_chars, true)
|
|
329
|
+
-- Read value
|
|
330
|
+
val, i = parse(str, i)
|
|
331
|
+
-- Set
|
|
332
|
+
res[key] = val
|
|
333
|
+
-- Next token
|
|
334
|
+
i = next_char(str, i, space_chars, true)
|
|
335
|
+
local chr = str:sub(i, i)
|
|
336
|
+
i = i + 1
|
|
337
|
+
if chr == "}" then break end
|
|
338
|
+
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
|
339
|
+
end
|
|
340
|
+
return res, i
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
local char_func_map = {
|
|
345
|
+
[ '"' ] = parse_string,
|
|
346
|
+
[ "0" ] = parse_number,
|
|
347
|
+
[ "1" ] = parse_number,
|
|
348
|
+
[ "2" ] = parse_number,
|
|
349
|
+
[ "3" ] = parse_number,
|
|
350
|
+
[ "4" ] = parse_number,
|
|
351
|
+
[ "5" ] = parse_number,
|
|
352
|
+
[ "6" ] = parse_number,
|
|
353
|
+
[ "7" ] = parse_number,
|
|
354
|
+
[ "8" ] = parse_number,
|
|
355
|
+
[ "9" ] = parse_number,
|
|
356
|
+
[ "-" ] = parse_number,
|
|
357
|
+
[ "t" ] = parse_literal,
|
|
358
|
+
[ "f" ] = parse_literal,
|
|
359
|
+
[ "n" ] = parse_literal,
|
|
360
|
+
[ "[" ] = parse_array,
|
|
361
|
+
[ "{" ] = parse_object,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
parse = function(str, idx)
|
|
366
|
+
local chr = str:sub(idx, idx)
|
|
367
|
+
local f = char_func_map[chr]
|
|
368
|
+
if f then
|
|
369
|
+
return f(str, idx)
|
|
370
|
+
end
|
|
371
|
+
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
function json.decode(str)
|
|
376
|
+
if type(str) ~= "string" then
|
|
377
|
+
error("expected argument of type string, got " .. type(str))
|
|
378
|
+
end
|
|
379
|
+
local res, idx = parse(str, next_char(str, 1, space_chars, true))
|
|
380
|
+
idx = next_char(str, idx, space_chars, true)
|
|
381
|
+
if idx <= #str then
|
|
382
|
+
decode_error(str, idx, "trailing garbage")
|
|
383
|
+
end
|
|
384
|
+
return res
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
return json
|
package/package.json
CHANGED
|
@@ -32,11 +32,11 @@ export function runDeepCopyTests(): void {
|
|
|
32
32
|
copiedDefaultMapHasChildDefaultMap();
|
|
33
33
|
copiedDefaultMapHasBrand();
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
copiedSerializedMapHasStringKey();
|
|
36
|
+
copiedSerializedMapHasNumberKey();
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
copiedSerializedDefaultMapHasStringKey();
|
|
39
|
+
copiedSerializedDefaultMapHasNumberKey();
|
|
40
40
|
|
|
41
41
|
log("All deep copy tests passed!");
|
|
42
42
|
}
|
|
@@ -464,112 +464,108 @@ function copiedDefaultMapHasBrand() {
|
|
|
464
464
|
}
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
function
|
|
467
|
+
function copiedSerializedMapHasStringKey() {
|
|
468
468
|
const mapKey = "123";
|
|
469
469
|
const mapValue = 456;
|
|
470
470
|
const oldMap = new Map<string, number>();
|
|
471
471
|
oldMap.set(mapKey, mapValue);
|
|
472
472
|
|
|
473
|
-
const
|
|
473
|
+
const serializedOldMap = deepCopy(
|
|
474
474
|
oldMap,
|
|
475
475
|
SerializationType.SERIALIZE,
|
|
476
|
-
"
|
|
476
|
+
"copiedSerializedMapHasStringKey-serialize",
|
|
477
477
|
);
|
|
478
478
|
|
|
479
479
|
const newTable = deepCopy(
|
|
480
|
-
|
|
480
|
+
serializedOldMap,
|
|
481
481
|
SerializationType.DESERIALIZE,
|
|
482
|
-
"
|
|
482
|
+
"copiedSerializedMapHasStringKey-deserialize",
|
|
483
483
|
);
|
|
484
484
|
|
|
485
485
|
const newMap = newTable as Map<string, number>;
|
|
486
|
-
if (!
|
|
486
|
+
if (!newMap.has(mapKey)) {
|
|
487
|
+
const keyType = type(mapKey);
|
|
487
488
|
error(
|
|
488
|
-
`The copied Map did not have a key of: ${mapKey} with type ${
|
|
489
|
-
mapKey,
|
|
490
|
-
)}`,
|
|
489
|
+
`The copied Map did not have a key of: ${mapKey} with type ${keyType}`,
|
|
491
490
|
);
|
|
492
491
|
}
|
|
493
492
|
}
|
|
494
493
|
|
|
495
|
-
function
|
|
494
|
+
function copiedSerializedMapHasNumberKey() {
|
|
496
495
|
const mapKey = 123;
|
|
497
496
|
const mapValue = 456;
|
|
498
497
|
const oldMap = new Map<number, number>();
|
|
499
498
|
oldMap.set(mapKey, mapValue);
|
|
500
499
|
|
|
501
|
-
const
|
|
500
|
+
const serializedOldMap = deepCopy(
|
|
502
501
|
oldMap,
|
|
503
502
|
SerializationType.SERIALIZE,
|
|
504
|
-
"
|
|
503
|
+
"copiedSerializedMapHasNumberKey-serialize",
|
|
505
504
|
);
|
|
506
505
|
|
|
507
506
|
const newTable = deepCopy(
|
|
508
|
-
|
|
507
|
+
serializedOldMap,
|
|
509
508
|
SerializationType.DESERIALIZE,
|
|
510
|
-
"
|
|
509
|
+
"copiedSerializedMapHasNumberKey-deserialize",
|
|
511
510
|
);
|
|
512
511
|
|
|
513
512
|
const newMap = newTable as Map<number, number>;
|
|
514
|
-
if (!
|
|
513
|
+
if (!newMap.has(mapKey)) {
|
|
514
|
+
const keyType = type(mapKey);
|
|
515
515
|
error(
|
|
516
|
-
`The copied Map did not have a key of: ${mapKey} with type ${
|
|
517
|
-
mapKey,
|
|
518
|
-
)}`,
|
|
516
|
+
`The copied Map did not have a key of: ${mapKey} with type ${keyType}`,
|
|
519
517
|
);
|
|
520
518
|
}
|
|
521
519
|
}
|
|
522
520
|
|
|
523
|
-
function
|
|
521
|
+
function copiedSerializedDefaultMapHasStringKey() {
|
|
524
522
|
const mapKey = "123";
|
|
525
|
-
const
|
|
526
|
-
|
|
523
|
+
const oldDefaultMap = new DefaultMap<string, number>(456);
|
|
524
|
+
oldDefaultMap.getAndSetDefault(mapKey);
|
|
527
525
|
|
|
528
|
-
const
|
|
529
|
-
|
|
526
|
+
const serializedOldDefaultMap = deepCopy(
|
|
527
|
+
oldDefaultMap,
|
|
530
528
|
SerializationType.SERIALIZE,
|
|
531
|
-
"
|
|
529
|
+
"copiedSerializedDefaultMapHasStringKey-serialize",
|
|
532
530
|
);
|
|
533
531
|
|
|
534
532
|
const newTable = deepCopy(
|
|
535
|
-
|
|
533
|
+
serializedOldDefaultMap,
|
|
536
534
|
SerializationType.DESERIALIZE,
|
|
537
|
-
"
|
|
535
|
+
"copiedSerializedDefaultMapHasStringKey-deserialize",
|
|
538
536
|
);
|
|
539
537
|
|
|
540
|
-
const
|
|
541
|
-
if (!
|
|
538
|
+
const newDefaultMap = newTable as DefaultMap<string, number>;
|
|
539
|
+
if (!newDefaultMap.has(mapKey)) {
|
|
540
|
+
const keyType = type(mapKey);
|
|
542
541
|
error(
|
|
543
|
-
`The copied DefaultMap did not have a key of
|
|
544
|
-
mapKey,
|
|
545
|
-
)}`,
|
|
542
|
+
`The copied DefaultMap did not have a key of "${mapKey}" with type: ${keyType}`,
|
|
546
543
|
);
|
|
547
544
|
}
|
|
548
545
|
}
|
|
549
546
|
|
|
550
|
-
function
|
|
547
|
+
function copiedSerializedDefaultMapHasNumberKey() {
|
|
551
548
|
const mapKey = 123;
|
|
552
|
-
const
|
|
553
|
-
|
|
549
|
+
const oldDefaultMap = new DefaultMap<number, number>(456);
|
|
550
|
+
oldDefaultMap.getAndSetDefault(mapKey);
|
|
554
551
|
|
|
555
|
-
const
|
|
556
|
-
|
|
552
|
+
const serializedOldDefaultMap = deepCopy(
|
|
553
|
+
oldDefaultMap,
|
|
557
554
|
SerializationType.SERIALIZE,
|
|
558
|
-
"
|
|
555
|
+
"copiedSerializedDefaultMapHasNumberKey-serialize",
|
|
559
556
|
);
|
|
560
557
|
|
|
561
558
|
const newTable = deepCopy(
|
|
562
|
-
|
|
559
|
+
serializedOldDefaultMap,
|
|
563
560
|
SerializationType.DESERIALIZE,
|
|
564
|
-
"
|
|
561
|
+
"copiedSerializedDefaultMapHasNumberKey-deserialize",
|
|
565
562
|
);
|
|
566
563
|
|
|
567
|
-
const
|
|
568
|
-
if (!
|
|
564
|
+
const newDefaultMap = newTable as DefaultMap<number, number>;
|
|
565
|
+
if (!newDefaultMap.has(mapKey)) {
|
|
566
|
+
const keyType = type(mapKey);
|
|
569
567
|
error(
|
|
570
|
-
`The copied DefaultMap did not have a key of: ${mapKey} with type ${
|
|
571
|
-
mapKey,
|
|
572
|
-
)}`,
|
|
568
|
+
`The copied DefaultMap did not have a key of: ${mapKey} with type ${keyType}`,
|
|
573
569
|
);
|
|
574
570
|
}
|
|
575
571
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as jsonLua from "../lib/jsonLua";
|
|
2
2
|
import { logError } from "./log";
|
|
3
3
|
|
|
4
4
|
function tryDecode(this: void, jsonString: string) {
|
|
5
|
-
return
|
|
5
|
+
return jsonLua.decode(jsonString) as LuaMap;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
function tryEncode(this: void, luaTable: unknown) {
|
|
9
|
-
return
|
|
9
|
+
return jsonLua.encode(luaTable);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -16,6 +16,9 @@ function tryEncode(this: void, luaTable: unknown) {
|
|
|
16
16
|
* fails, it will return a blank Lua table instead of throwing an error. (This allows execution to
|
|
17
17
|
* continue in cases where users have no current save data or have manually removed their existing
|
|
18
18
|
* save data.)
|
|
19
|
+
*
|
|
20
|
+
* Under the hood, this uses a custom JSON parser that was measured to be 11.8 times faster than the
|
|
21
|
+
* vanilla JSON parser.
|
|
19
22
|
*/
|
|
20
23
|
export function jsonDecode(jsonString: string): LuaMap<AnyNotNil, unknown> {
|
|
21
24
|
const [ok, luaTableOrErrMsg] = pcall(tryDecode, jsonString);
|
|
@@ -34,6 +37,9 @@ export function jsonDecode(jsonString: string): LuaMap<AnyNotNil, unknown> {
|
|
|
34
37
|
* In most cases, this function will be used for writing data to a "save#.dat" file. If encoding
|
|
35
38
|
* fails, it will throw an error to prevent writing a blank string or corrupted data to a user's
|
|
36
39
|
* "save#.dat" file.
|
|
40
|
+
*
|
|
41
|
+
* Under the hood, this uses a custom JSON parser that was measured to be 11.8 times faster than the
|
|
42
|
+
* vanilla JSON parser.
|
|
37
43
|
*/
|
|
38
44
|
export function jsonEncode(luaTable: unknown): string {
|
|
39
45
|
const [ok, jsonStringOrErrMsg] = pcall(tryEncode, luaTable);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is the custom JSON parser library called "json.lua". It is located at:
|
|
3
|
+
* https://github.com/rxi/json.lua
|
|
4
|
+
*
|
|
5
|
+
* This parser was measured to be 11.8 times faster than the vanilla parser at decoding a sample
|
|
6
|
+
* "save1.dat" file.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function encode(this: void, data: unknown): string;
|
|
10
|
+
export function decode(this: void, data: string): unknown;
|
|
@@ -0,0 +1,388 @@
|
|
|
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
|
+
|
|
25
|
+
local json = { _version = "0.1.2" }
|
|
26
|
+
|
|
27
|
+
-------------------------------------------------------------------------------
|
|
28
|
+
-- Encode
|
|
29
|
+
-------------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
local encode
|
|
32
|
+
|
|
33
|
+
local escape_char_map = {
|
|
34
|
+
[ "\\" ] = "\\",
|
|
35
|
+
[ "\"" ] = "\"",
|
|
36
|
+
[ "\b" ] = "b",
|
|
37
|
+
[ "\f" ] = "f",
|
|
38
|
+
[ "\n" ] = "n",
|
|
39
|
+
[ "\r" ] = "r",
|
|
40
|
+
[ "\t" ] = "t",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
local escape_char_map_inv = { [ "/" ] = "/" }
|
|
44
|
+
for k, v in pairs(escape_char_map) do
|
|
45
|
+
escape_char_map_inv[v] = k
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
local function escape_char(c)
|
|
50
|
+
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
local function encode_nil(val)
|
|
55
|
+
return "null"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
local function encode_table(val, stack)
|
|
60
|
+
local res = {}
|
|
61
|
+
stack = stack or {}
|
|
62
|
+
|
|
63
|
+
-- Circular reference?
|
|
64
|
+
if stack[val] then error("circular reference") end
|
|
65
|
+
|
|
66
|
+
stack[val] = true
|
|
67
|
+
|
|
68
|
+
if rawget(val, 1) ~= nil or next(val) == nil then
|
|
69
|
+
-- Treat as array -- check keys are valid and it is not sparse
|
|
70
|
+
local n = 0
|
|
71
|
+
for k in pairs(val) do
|
|
72
|
+
if type(k) ~= "number" then
|
|
73
|
+
error("invalid table: mixed or invalid key types")
|
|
74
|
+
end
|
|
75
|
+
n = n + 1
|
|
76
|
+
end
|
|
77
|
+
if n ~= #val then
|
|
78
|
+
error("invalid table: sparse array")
|
|
79
|
+
end
|
|
80
|
+
-- Encode
|
|
81
|
+
for i, v in ipairs(val) do
|
|
82
|
+
table.insert(res, encode(v, stack))
|
|
83
|
+
end
|
|
84
|
+
stack[val] = nil
|
|
85
|
+
return "[" .. table.concat(res, ",") .. "]"
|
|
86
|
+
|
|
87
|
+
else
|
|
88
|
+
-- Treat as an object
|
|
89
|
+
for k, v in pairs(val) do
|
|
90
|
+
if type(k) ~= "string" then
|
|
91
|
+
error("invalid table: mixed or invalid key types")
|
|
92
|
+
end
|
|
93
|
+
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
|
94
|
+
end
|
|
95
|
+
stack[val] = nil
|
|
96
|
+
return "{" .. table.concat(res, ",") .. "}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
local function encode_string(val)
|
|
102
|
+
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
local function encode_number(val)
|
|
107
|
+
-- Check for NaN, -inf and inf
|
|
108
|
+
if val ~= val or val <= -math.huge or val >= math.huge then
|
|
109
|
+
error("unexpected number value '" .. tostring(val) .. "'")
|
|
110
|
+
end
|
|
111
|
+
return string.format("%.14g", val)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
local type_func_map = {
|
|
116
|
+
[ "nil" ] = encode_nil,
|
|
117
|
+
[ "table" ] = encode_table,
|
|
118
|
+
[ "string" ] = encode_string,
|
|
119
|
+
[ "number" ] = encode_number,
|
|
120
|
+
[ "boolean" ] = tostring,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
encode = function(val, stack)
|
|
125
|
+
local t = type(val)
|
|
126
|
+
local f = type_func_map[t]
|
|
127
|
+
if f then
|
|
128
|
+
return f(val, stack)
|
|
129
|
+
end
|
|
130
|
+
error("unexpected type '" .. t .. "'")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
function json.encode(val)
|
|
135
|
+
return ( encode(val) )
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
-------------------------------------------------------------------------------
|
|
140
|
+
-- Decode
|
|
141
|
+
-------------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
local parse
|
|
144
|
+
|
|
145
|
+
local function create_set(...)
|
|
146
|
+
local res = {}
|
|
147
|
+
for i = 1, select("#", ...) do
|
|
148
|
+
res[ select(i, ...) ] = true
|
|
149
|
+
end
|
|
150
|
+
return res
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
local space_chars = create_set(" ", "\t", "\r", "\n")
|
|
154
|
+
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
|
155
|
+
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
|
156
|
+
local literals = create_set("true", "false", "null")
|
|
157
|
+
|
|
158
|
+
local literal_map = {
|
|
159
|
+
[ "true" ] = true,
|
|
160
|
+
[ "false" ] = false,
|
|
161
|
+
[ "null" ] = nil,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
local function next_char(str, idx, set, negate)
|
|
166
|
+
for i = idx, #str do
|
|
167
|
+
if set[str:sub(i, i)] ~= negate then
|
|
168
|
+
return i
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
return #str + 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
local function decode_error(str, idx, msg)
|
|
176
|
+
local line_count = 1
|
|
177
|
+
local col_count = 1
|
|
178
|
+
for i = 1, idx - 1 do
|
|
179
|
+
col_count = col_count + 1
|
|
180
|
+
if str:sub(i, i) == "\n" then
|
|
181
|
+
line_count = line_count + 1
|
|
182
|
+
col_count = 1
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
local function codepoint_to_utf8(n)
|
|
190
|
+
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
|
191
|
+
local f = math.floor
|
|
192
|
+
if n <= 0x7f then
|
|
193
|
+
return string.char(n)
|
|
194
|
+
elseif n <= 0x7ff then
|
|
195
|
+
return string.char(f(n / 64) + 192, n % 64 + 128)
|
|
196
|
+
elseif n <= 0xffff then
|
|
197
|
+
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
|
198
|
+
elseif n <= 0x10ffff then
|
|
199
|
+
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
|
200
|
+
f(n % 4096 / 64) + 128, n % 64 + 128)
|
|
201
|
+
end
|
|
202
|
+
error( string.format("invalid unicode codepoint '%x'", n) )
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
local function parse_unicode_escape(s)
|
|
207
|
+
local n1 = tonumber( s:sub(1, 4), 16 )
|
|
208
|
+
local n2 = tonumber( s:sub(7, 10), 16 )
|
|
209
|
+
-- Surrogate pair?
|
|
210
|
+
if n2 then
|
|
211
|
+
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
|
212
|
+
else
|
|
213
|
+
return codepoint_to_utf8(n1)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
local function parse_string(str, i)
|
|
219
|
+
local res = ""
|
|
220
|
+
local j = i + 1
|
|
221
|
+
local k = j
|
|
222
|
+
|
|
223
|
+
while j <= #str do
|
|
224
|
+
local x = str:byte(j)
|
|
225
|
+
|
|
226
|
+
if x < 32 then
|
|
227
|
+
decode_error(str, j, "control character in string")
|
|
228
|
+
|
|
229
|
+
elseif x == 92 then -- `\`: Escape
|
|
230
|
+
res = res .. str:sub(k, j - 1)
|
|
231
|
+
j = j + 1
|
|
232
|
+
local c = str:sub(j, j)
|
|
233
|
+
if c == "u" then
|
|
234
|
+
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
|
|
235
|
+
or str:match("^%x%x%x%x", j + 1)
|
|
236
|
+
or decode_error(str, j - 1, "invalid unicode escape in string")
|
|
237
|
+
res = res .. parse_unicode_escape(hex)
|
|
238
|
+
j = j + #hex
|
|
239
|
+
else
|
|
240
|
+
if not escape_chars[c] then
|
|
241
|
+
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
|
|
242
|
+
end
|
|
243
|
+
res = res .. escape_char_map_inv[c]
|
|
244
|
+
end
|
|
245
|
+
k = j + 1
|
|
246
|
+
|
|
247
|
+
elseif x == 34 then -- `"`: End of string
|
|
248
|
+
res = res .. str:sub(k, j - 1)
|
|
249
|
+
return res, j + 1
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
j = j + 1
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
decode_error(str, i, "expected closing quote for string")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
local function parse_number(str, i)
|
|
260
|
+
local x = next_char(str, i, delim_chars)
|
|
261
|
+
local s = str:sub(i, x - 1)
|
|
262
|
+
local n = tonumber(s)
|
|
263
|
+
if not n then
|
|
264
|
+
decode_error(str, i, "invalid number '" .. s .. "'")
|
|
265
|
+
end
|
|
266
|
+
return n, x
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
local function parse_literal(str, i)
|
|
271
|
+
local x = next_char(str, i, delim_chars)
|
|
272
|
+
local word = str:sub(i, x - 1)
|
|
273
|
+
if not literals[word] then
|
|
274
|
+
decode_error(str, i, "invalid literal '" .. word .. "'")
|
|
275
|
+
end
|
|
276
|
+
return literal_map[word], x
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
local function parse_array(str, i)
|
|
281
|
+
local res = {}
|
|
282
|
+
local n = 1
|
|
283
|
+
i = i + 1
|
|
284
|
+
while 1 do
|
|
285
|
+
local x
|
|
286
|
+
i = next_char(str, i, space_chars, true)
|
|
287
|
+
-- Empty / end of array?
|
|
288
|
+
if str:sub(i, i) == "]" then
|
|
289
|
+
i = i + 1
|
|
290
|
+
break
|
|
291
|
+
end
|
|
292
|
+
-- Read token
|
|
293
|
+
x, i = parse(str, i)
|
|
294
|
+
res[n] = x
|
|
295
|
+
n = n + 1
|
|
296
|
+
-- Next token
|
|
297
|
+
i = next_char(str, i, space_chars, true)
|
|
298
|
+
local chr = str:sub(i, i)
|
|
299
|
+
i = i + 1
|
|
300
|
+
if chr == "]" then break end
|
|
301
|
+
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
|
302
|
+
end
|
|
303
|
+
return res, i
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
local function parse_object(str, i)
|
|
308
|
+
local res = {}
|
|
309
|
+
i = i + 1
|
|
310
|
+
while 1 do
|
|
311
|
+
local key, val
|
|
312
|
+
i = next_char(str, i, space_chars, true)
|
|
313
|
+
-- Empty / end of object?
|
|
314
|
+
if str:sub(i, i) == "}" then
|
|
315
|
+
i = i + 1
|
|
316
|
+
break
|
|
317
|
+
end
|
|
318
|
+
-- Read key
|
|
319
|
+
if str:sub(i, i) ~= '"' then
|
|
320
|
+
decode_error(str, i, "expected string for key")
|
|
321
|
+
end
|
|
322
|
+
key, i = parse(str, i)
|
|
323
|
+
-- Read ':' delimiter
|
|
324
|
+
i = next_char(str, i, space_chars, true)
|
|
325
|
+
if str:sub(i, i) ~= ":" then
|
|
326
|
+
decode_error(str, i, "expected ':' after key")
|
|
327
|
+
end
|
|
328
|
+
i = next_char(str, i + 1, space_chars, true)
|
|
329
|
+
-- Read value
|
|
330
|
+
val, i = parse(str, i)
|
|
331
|
+
-- Set
|
|
332
|
+
res[key] = val
|
|
333
|
+
-- Next token
|
|
334
|
+
i = next_char(str, i, space_chars, true)
|
|
335
|
+
local chr = str:sub(i, i)
|
|
336
|
+
i = i + 1
|
|
337
|
+
if chr == "}" then break end
|
|
338
|
+
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
|
339
|
+
end
|
|
340
|
+
return res, i
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
local char_func_map = {
|
|
345
|
+
[ '"' ] = parse_string,
|
|
346
|
+
[ "0" ] = parse_number,
|
|
347
|
+
[ "1" ] = parse_number,
|
|
348
|
+
[ "2" ] = parse_number,
|
|
349
|
+
[ "3" ] = parse_number,
|
|
350
|
+
[ "4" ] = parse_number,
|
|
351
|
+
[ "5" ] = parse_number,
|
|
352
|
+
[ "6" ] = parse_number,
|
|
353
|
+
[ "7" ] = parse_number,
|
|
354
|
+
[ "8" ] = parse_number,
|
|
355
|
+
[ "9" ] = parse_number,
|
|
356
|
+
[ "-" ] = parse_number,
|
|
357
|
+
[ "t" ] = parse_literal,
|
|
358
|
+
[ "f" ] = parse_literal,
|
|
359
|
+
[ "n" ] = parse_literal,
|
|
360
|
+
[ "[" ] = parse_array,
|
|
361
|
+
[ "{" ] = parse_object,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
parse = function(str, idx)
|
|
366
|
+
local chr = str:sub(idx, idx)
|
|
367
|
+
local f = char_func_map[chr]
|
|
368
|
+
if f then
|
|
369
|
+
return f(str, idx)
|
|
370
|
+
end
|
|
371
|
+
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
function json.decode(str)
|
|
376
|
+
if type(str) ~= "string" then
|
|
377
|
+
error("expected argument of type string, got " .. type(str))
|
|
378
|
+
end
|
|
379
|
+
local res, idx = parse(str, next_char(str, 1, space_chars, true))
|
|
380
|
+
idx = next_char(str, idx, space_chars, true)
|
|
381
|
+
if idx <= #str then
|
|
382
|
+
decode_error(str, idx, "trailing garbage")
|
|
383
|
+
end
|
|
384
|
+
return res
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
return json
|