/
tnetstrings.lua
264 lines (212 loc) · 6.71 KB
/
tnetstrings.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
--[[
tnetstring implementation
Joshua Simmons
--]]
-- We don't have to local these since we're not using module, but we will
-- anyway for performance' sake.
local concat = table.concat
local error = error
local find = string.find
local len = string.len
local sub = string.sub
local tonumber = tonumber
local tostring = tostring
local type = type
-- Since nil can't be stored in a Lua table, we need a sentinel value to take
-- its place.
local function null()
return null -- Trick stolen from Json4Lua, returns itself.
end
-- We need access to the parse function from the parsers, so do it this way
local parse
local parsers = {
-- Blob, plain ol data.
[','] = function(data, offset, length)
return sub(data, offset, offset + length - 1)
end;
-- Number, well, integer, but we're going to use lua's tonumber anyway.
['#'] = function(data, offset, length)
local n = tonumber(sub(data, offset, offset + length - 1))
if not n then
return nil, 'could not parse number payload'
end
return n
end;
-- Boolean, we check the text even though it's not strictly necessary for
-- a reasonable implementation.
['!'] = function(data, offset, length)
local blob = sub(data, offset, offset + length - 1)
if blob == 'true' then
return true, extra
elseif blob == 'false'
then return false, extra
else
return nil, 'invalid boolean payload'
end
end;
-- Null, has to be 0 in length.
['~'] = function(data, offset, length)
if length ~= 0 then
return nil, 'null must have 0 length'
end
return null
end;
-- List, we just put it in a table.
[']'] = function(data, offset, length)
if length == 0 then
return {}
end
local result, n = {}, 1
local val, ext_pos = nil, offset
repeat
val, ext_pos = parse(data, nil, ext_pos)
-- If val is nil, then ext is actually an error message.
if val == nil then
return val, ext_pos
end
result[n] = val
n = n + 1
until ext_pos == (offset + length)
return result
end;
-- Dictionary, we just put it in a table too.
['}'] = function(data, offset, length)
if length == 0 then
return {}
end
local result = {}
local key, val, ext_pos = nil, nil, offset
repeat
key, ext_pos = parse(data, ',', ext_pos)
if key == nil then
return nil, ext
end
if not ext_pos then
return nil, 'unbalanced dict'
end
val, ext_pos = parse(data, nil, ext_pos)
if val == nil then
return nil, ext
end
result[key] = val
until ext_pos == (offset + length)
return result
end;
}
-- Takes a data string and returns a single tns value from it. Any remaining
-- data is also returned. In case of parsing errors, or if the expected type
-- does not match the type found, then the function returns nil followed by an
-- error message. For simplicities sake, the expected type is given as a tns
-- string type code.
parse = function(data, expected, offset)
offset = offset or 1
-- Find the interesting points in the data.
local colon_pos = find(data, ':', offset, true)
if not colon_pos then
return nil, 'could not find colon'
end
local length = tonumber(sub(data, offset, colon_pos - 1))
if not length then
return nil, 'no blob length found'
end
local blob_begin = colon_pos + 1
local blob_end = colon_pos + length
local blob_type = sub(data, blob_end + 1, blob_end + 1)
if expected and expected ~= blob_type then
return nil, 'type did not match expected'
end
local parser = parsers[blob_type]
if not parser then
return nil, 'invalid type code'
end
return parser(data, blob_begin, length), blob_end + 2
end
local function insert(into, data)
local n = (into.n or 0) + 1
into[n] = data
into.n = n
end
-- We need access to the dump method from the dumpers so declare this here
local dump
local dumpers = {
['string'] = function(str, insert)
local length = len(str)
insert(tostring(length))
insert(':' .. str .. ',')
end;
['number'] = function(num, insert)
local str = tostring(num)
local length = len(str)
insert(tostring(length))
insert(':' .. str .. '#')
end;
['function'] = function(f, insert)
if f == null then
insert('0:~')
else
error('cannot encode functions')
end
end;
['boolean'] = function(b, insert)
if b then
insert('4:true!')
else
insert('5:false!')
end
end;
-- We treat any tables with an array part as arrays.
['table'] = function(tab, insert)
local payload = {}
do
local n = 1
local function insert(data)
payload[n] = data
n = n + 1
end
-- We already know this is a table, so this is well defined.
local n = #tab
if n > 0 then
-- list
for i = 1, n do
insert(dump(tab[i]))
end
-- be a little bit tricky and append the type char here, remember
-- to take one away from the payload length!
insert(']')
else
-- dict
for k, v in pairs(tab) do
if type(k) ~= 'string' then
error('dict keys must be strings')
end
insert(dump(k))
insert(dump(v))
end
-- same issue as list
insert('}')
end
end
local payload_str = concat(payload, '')
-- We have to take one away because we added the type char to the end
local payload_len = len(payload_str) - 1
insert(tostring(payload_len) .. ':' .. payload_str)
end;
}
-- Takes a Lua object and returns a tnetstring representation of it.
-- Unlike the decode method, this aborts with an error if you feed it bad data.
dump = function(object)
local t = type(object)
local dumper = dumpers[t]
if not dumper then
error('unable to dump type ' .. t, 2)
end
local output = {}
local n = 1
dumper(object, function(data) output[n] = data n = n + 1 end)
return concat(output, '')
end
return {
null = null;
parse = parse;
dump = dump;
}