compile/minichunkspy.lua

1
-- Minichunkspy: Disassemble and reassemble chunks.
2
-- Copyright M Joonas Pihlaja 2009
3
-- MIT license
4
--
5
-- minichunkspy = require"minichunkspy"
6
--
7
-- chunk = string.dump(loadfile"blabla.lua")
8
-- disassembled_chunk = minichunkspy.disassemble(chunk)
9
-- chunk = minichunkspy.assemble(disassembled_chunk)
10
-- assert(minichunkspy.validate(<function or chunk>))
11
--
12
-- Tested on little-endian 32 and 64 bit platforms.
13
local string, table, math = string, table, math
14
local ipairs, setmetatable, type, assert = ipairs, setmetatable, type, assert
15
local _ = __END_OF_GLOBALS__
16
local string_char, string_byte, string_sub = string.char, string.byte, string.sub
17
local math_frexp, math_ldexp, math_abs = math.frexp, math.ldexp, math.abs
18
local table_concat = table.concat
19
local Inf = math.huge
20
local NaN = Inf - Inf
21
 
22
local BIG_ENDIAN = false
23
local SIZEOF_SIZE_T = 4
24
local SIZEOF_INT = 4
25
local SIZEOF_NUMBER = 8
26
 
27
local save_stack = {}
28
 
29
local function save()
30
    save_stack[#save_stack+1]
31
	= {BIG_ENDIAN, SIZEOF_SIZE_T, SIZEOF_INT, SIZEOF_NUMBER}
32
end
33
local function restore ()
34
    BIG_ENDIAN, SIZEOF_SIZE_T, SIZEOF_INT, SIZEOF_NUMBER
35
	= unpack(save_stack[#save_stack])
36
    save_stack[#save_stack] = nil
37
end
38
 
39
local function construct (class, self)
40
    return class.new(class, self)
41
end
42
 
43
local mt_memo = {}
44
 
45
local Field = construct{
46
    new =
47
	function (class, self)
48
	    local self = self or {}
49
	    local mt = mt_memo[class] or {
50
		__index = class,
51
		__call = construct
52
	    }
53
	    mt_memo[class] = mt
54
	    return setmetatable(self, mt)
55
	end,
56
}
57
 
58
local None = Field{
59
    unpack = function (self, bytes, ix) return nil, ix end,
60
    pack = function (self, val) return "" end
61
}
62
 
63
local char_memo = {}
64
 
65
local function char(n)
66
    local field = char_memo[n] or Field{
67
	unpack = function (self, bytes, ix)
68
		     return string_sub(bytes, ix, ix+n-1), ix+n
69
		 end,
70
	pack = function (self, val) return string_sub(val, 1, n) end
71
    }
72
    char_memo[n] = field
73
    return field
74
end
75
 
76
local uint8 = Field{
77
    unpack = function (self, bytes, ix)
78
		 return string_byte(bytes, ix, ix), ix+1
79
	     end,
80
    pack = function (self, val) return string_char(val) end
81
}
82
 
83
local uint32 = Field{
84
    unpack =
85
	function (self, bytes, ix)
86
	    local a,b,c,d = string_byte(bytes, ix, ix+3)
87
	    if BIG_ENDIAN then a,b,c,d = d,c,b,a end
88
	    return a + b*256 + c*256^2 + d*256^3, ix+4
89
	end,
90
    pack =
91
	function (self, val)
92
	    assert(type(val) == "number",
93
		   "unexpected value type to pack as an uint32")
94
	    local a,b,c,d
95
	    d = val % 2^32
96
	    a = d % 256; d = (d - a) / 256
97
	    b = d % 256; d = (d - b) / 256
98
	    c = d % 256; d = (d - c) / 256
99
	    if BIG_ENDIAN then a,b,c,d = d,c,b,a end
100
	    return string_char(a,b,c,d)
101
	end
102
}
103
 
104
local uint64 = Field{
105
    unpack =
106
	function (self, bytes, ix)
107
	    local a = uint32:unpack(bytes, ix)
108
	    local b = uint32:unpack(bytes, ix+4)
109
	    if BIG_ENDIAN then a,b = b,a end
110
	    return a + b*2^32, ix+8
111
	end,
112
    pack =
113
	function (self, val)
114
	    assert(type(val) == "number",
115
		   "unexpected value type to pack as an uint64")
116
	    local a = val % 2^32
117
	    local b = (val - a) / 2^32
118
	    if BIG_ENDIAN then a,b = b,a end
119
	    return uint32:pack(a) .. uint32:pack(b)
120
	end
121
}
122
 
123
local function explode_double(bytes, ix)
124
    local a = uint32:unpack(bytes, ix)
125
    local b = uint32:unpack(bytes, ix+4)
126
    if BIG_ENDIAN then a,b = b,a end --XXX: ARM mixed-endian
127
 
128
    local sig_hi = b % 2^20
129
    local sig_lo = a
130
    local significand = sig_lo + sig_hi*2^32
131
 
132
    b = (b - sig_hi) / 2^20
133
 
134
    local biased_exp = b % 2^11
135
    local sign = b <= biased_exp and 1 or -1
136
 
137
    --print(sign, significand, biased_exp, "explode")
138
    return sign, biased_exp, significand
139
end
140
 
141
local function implode_double(sign, biased_exp, significand)
142
    --print(sign, significand, biased_exp, "implode")
143
    local sig_lo = significand % 2^32
144
    local sig_hi = (significand - sig_lo) / 2^32
145
 
146
    local a = sig_lo
147
    local b = ((sign < 0 and 2^11 or 0) + biased_exp)*2^20 + sig_hi
148
 
149
    if BIG_ENDIAN then a,b = b,a end --XXX: ARM mixed-endian
150
    return uint32.pack(nil, a) .. uint32.pack(nil, b)
151
end
152
 
153
local function math_sign(x)
154
    if x ~= x then return x end	--sign of NaN is NaN
155
    if x == 0 then x = 1/x end	--extract sign of zero
156
    return x > 0 and 1 or -1
157
end
158
 
159
local SMALLEST_SUBNORMAL = math_ldexp(1, -1022 - 52)
160
local SMALLEST_NORMAL = SMALLEST_SUBNORMAL * 2^52
161
local LARGEST_SUBNORMAL = math_ldexp(2^52 - 1, -1022 - 52)
162
local LARGEST_NORMAL = math_ldexp(2^53 - 1, 1023 - 52)
163
assert(SMALLEST_SUBNORMAL ~= 0.0 and SMALLEST_SUBNORMAL / 2 == 0.0)
164
assert(LARGEST_NORMAL ~= Inf)
165
assert(LARGEST_NORMAL * 2 == Inf)
166
 
167
local double = Field{
168
    unpack =
169
	function (self, bytes, ix)
170
	    local sign, biased_exp, significand = explode_double(bytes, ix)
171
 
172
	    local val
173
	    if biased_exp == 0 then --subnormal
174
		val = math_ldexp(significand, -1022 - 52)
175
	    elseif biased_exp == 2047 then
176
		val = significand == 0 and Inf or NaN --XXX: loses NaN mantissa
177
	    else				      --normal
178
		val = math_ldexp(2^52 + significand, biased_exp - 1023 - 52)
179
	    end
180
	    val = sign*val
181
	    return val, ix+8
182
	end,
183
 
184
    pack =
185
	function (self, val)
186
	    if val ~= val then
187
		return implode_double(1,2047,2^52-1) --XXX: loses NaN mantissa
188
	    end
189
 
190
	    local sign = math_sign(val)
191
	    val = math_abs(val)
192
 
193
	    if val == Inf then return implode_double(sign, 2047, 0) end
194
	    if val == 0   then return implode_double(sign, 0, 0) end
195
 
196
	    local biased_exp, significand
197
 
198
	    if val <= LARGEST_SUBNORMAL then
199
		biased_exp = 0
200
		significand = val / SMALLEST_SUBNORMAL
201
	    else
202
		local frac, exp = math_frexp(val)
203
		significand = (2*frac - 1)*2^52
204
		biased_exp = exp + 1022
205
	    end
206
	    return implode_double(sign, biased_exp, significand)
207
	end
208
}
209
 
210
local Byte = uint8
211
 
212
local IntegralTypes = {
213
    [4] = uint32,
214
    [8] = uint64
215
}
216
 
217
local FloatTypes = {
218
    [4] = float,
219
    [8] = double
220
}
221
 
222
local Size_t = Field{
223
    unpack = function (self, bytes, ix)
224
		 return IntegralTypes[SIZEOF_SIZE_T]:unpack(bytes, ix)
225
	     end,
226
    pack = function (self, val)
227
	       return IntegralTypes[SIZEOF_SIZE_T]:pack(val)
228
	   end,
229
}
230
 
231
local Integer = Field{
232
    unpack = function (self, bytes, ix)
233
		 return IntegralTypes[SIZEOF_INT]:unpack(bytes, ix)
234
	     end,
235
    pack = function (self, val)
236
	       return IntegralTypes[SIZEOF_INT]:pack(val)
237
	   end,
238
}
239
 
240
local Number = Field{
241
    unpack = function (self, bytes, ix)
242
		 return FloatTypes[SIZEOF_NUMBER]:unpack(bytes, ix)
243
	     end,
244
    pack = function (self, val)
245
	       return FloatTypes[SIZEOF_NUMBER]:pack(val)
246
	   end,
247
}
248
 
249
-- Opaque types:
250
local Insn = char(4)
251
 
252
local Struct = Field{
253
    unpack =
254
	function (self, bytes, ix)
255
	    local val = {}
256
	    local i,j = 1,1
257
	    while self[i] do
258
		local field = self[i]
259
		local key = field.name
260
		if not key then key, j = j, j+1 end
261
		--print("unpacking struct field", key, " at index ", ix)
262
		val[key], ix = field:unpack(bytes, ix)
263
		i = i+1
264
	    end
265
	    return val, ix
266
	end,
267
    pack =
268
	function (self, val)
269
	    local data = {}
270
	    local i,j = 1,1
271
	    while self[i] do
272
		local field = self[i]
273
		local key = field.name
274
		if not key then key, j = j, j+1 end
275
		data[i] = field:pack(val[key])
276
		i = i+1
277
	    end
278
	    return table_concat(data)
279
	end
280
}
281
 
282
local List = Field{
283
    unpack =
284
	function (self, bytes, ix)
285
	    local len, ix = Integer:unpack(bytes, ix)
286
	    local vals = {}
287
	    local field = self.type
288
	    for i=1,len do
289
		--print("unpacking list field", i, " at index ", ix)
290
		vals[i], ix = field:unpack(bytes, ix)
291
	    end
292
	    return vals, ix
293
	end,
294
    pack =
295
	function (self, vals)
296
	    local len = #vals
297
	    local data = { Integer:pack(len) }
298
	    local field = self.type
299
	    for i=1,len do
300
		data[#data+1] = field:pack(vals[i])
301
	    end
302
	    return table_concat(data)
303
	end
304
}
305
 
306
local Boolean = Field{
307
    unpack =
308
	function (self, bytes, ix)
309
	    local val, ix = Integer:unpack(bytes, ix)
310
	    assert(val == 0 or val == 1,
311
		   "unpacked an unexpected value "..val.." for a Boolean")
312
	    return val == 1, ix
313
	end,
314
    pack =
315
	function (self, val)
316
	    assert(type(val) == "boolean",
317
		   "unexpected value type to pack as a Boolean")
318
	    return Integer:pack(val and 1 or 0)
319
	end
320
}
321
 
322
local String = Field{
323
    unpack =
324
	function (self, bytes, ix)
325
	    local len, ix = Size_t:unpack(bytes, ix)
326
	    local val = nil
327
	    if len > 0 then
328
		-- len includes trailing nul byte; ignore it
329
		local string_len = len - 1
330
		val = bytes:sub(ix, ix+string_len-1)
331
	    end
332
	    return val, ix + len
333
	end,
334
    pack =
335
	function (self, val)
336
	    assert(type(val) == "nil" or type(val) == "string",
337
		   "unexpected value type to pack as a String")
338
	    if val == nil then
339
		return Size_t:pack(0)
340
	    end
341
	    return Size_t:pack(#val+1) .. val .. "\000"
342
	end
343
}
344
 
345
local ChunkHeader = Struct{
346
    char(4){name = "signature"},
347
    Byte{name = "version"},
348
    Byte{name = "format"},
349
    Byte{name = "endianness"},
350
    Byte{name = "sizeof_int"},
351
    Byte{name = "sizeof_size_t"},
352
    Byte{name = "sizeof_insn"},
353
    Byte{name = "sizeof_Number"},
354
    Byte{name = "integral_flag"},
355
}
356
 
357
local ConstantTypes = {
358
    [0] = None,
359
    [1] = Boolean,
360
    [3] = Number,
361
    [4] = String,
362
}
363
local Constant = Field{
364
    unpack =
365
	function (self, bytes, ix)
366
	    local t, ix = Byte:unpack(bytes, ix)
367
	    local field = ConstantTypes[t]
368
	    assert(field, "unknown constant type "..t.." to unpack")
369
	    local v, ix = field:unpack(bytes, ix)
370
	    if t == 3 then
371
		assert(type(v) == "number")
372
	    end
373
	    return {
374
		type = t,
375
		value = v
376
	    }, ix
377
	end,
378
    pack =
379
	function (self, val)
380
	    local t, v = val.type, val.value
381
	    return Byte:pack(t) .. ConstantTypes[t]:pack(v)
382
	end
383
}
384
 
385
local Local = Struct{
386
    String{name = "name"},
387
    Integer{name = "startpc"},
388
    Integer{name = "endpc"}
389
}
390
 
391
local Function = Struct{
392
    String{name = "name"},
393
    Integer{name = "line"},
394
    Integer{name = "last_line"},
395
    Byte{name = "num_upvalues"},
396
    Byte{name = "num_parameters"},
397
    Byte{name = "is_vararg"},
398
    Byte{name = "max_stack_size"},
399
    List{name = "insns", type = Insn},
400
    List{name = "constants", type = Constant},
401
    List{name = "prototypes", type = nil}, --patch type below
402
    List{name = "source_lines", type = Integer},
403
    List{name = "locals", type = Local},
404
    List{name = "upvalues", type = String},
405
}
406
assert(Function[10].name == "prototypes",
407
       "missed the function prototype list")
408
Function[10].type = Function
409
 
410
local Chunk = Field{
411
    unpack =
412
	function (self, bytes, ix)
413
	    local chunk = {}
414
	    local header, ix = ChunkHeader:unpack(bytes, ix)
415
	    assert(header.signature == "\027Lua", "signature check failed")
416
	    assert(header.version == 81, "version mismatch")
417
	    assert(header.format == 0, "format mismatch")
418
	    assert(header.endianness == 0 or
419
		   header.endianness == 1, "endianness mismatch")
420
	    assert(IntegralTypes[header.sizeof_int], "int size unsupported")
421
	    assert(IntegralTypes[header.sizeof_size_t], "size_t size unsupported")
422
	    assert(header.sizeof_insn == 4, "insn size unsupported")
423
	    assert(FloatTypes[header.sizeof_Number], "number size unsupported")
424
	    assert(header.integral_flag == 0, "integral flag mismatch; only floats supported")
425
 
426
	    save()
427
		BIG_ENDIAN = header.endianness == 0
428
		SIZEOF_SIZE_T = header.sizeof_size_t
429
		SIZEOF_INT = header.sizeof_int
430
		SIZEOF_NUMBER = header.sizeof_Number
431
		chunk.header = header
432
		chunk.body, ix = Function:unpack(bytes, ix)
433
	    restore()
434
	    return chunk, ix
435
	end,
436
 
437
    pack =
438
	function (self, val)
439
	    local data
440
	    save()
441
		local header = val.header
442
		BIG_ENDIAN = header.endianness == 0
443
		SIZEOF_SIZE_T = header.sizeof_size_t
444
		SIZEOF_INT = header.sizeof_int
445
		SIZEOF_NUMBER = header.sizeof_Number
446
		data = ChunkHeader:pack(val.header) .. Function:pack(val.body)
447
	    restore()
448
	    return data
449
	end
450
}
451
 
452
local function validate(chunk)
453
    if type(chunk) == "function" then
454
	return validate(string.dump(chunk))
455
    end
456
    local f = Chunk:unpack(chunk, 1)
457
    local chunk2 = Chunk:pack(f)
458
 
459
    if chunk == chunk2 then return true end
460
 
461
    local i
462
    local len = math.min(#chunk, #chunk2)
463
    for i=1,len do
464
	local a = chunk:sub(i,i)
465
	local b = chunk:sub(i,i)
466
	if a ~= b then
467
	    return false, ("chunk roundtripping failed: "..
468
			   "first byte difference at index %d"):format(i)
469
	end
470
    end
471
    return false, ("chunk round tripping failed: "..
472
		   "original length %d vs. %d"):format(#chunk, #chunk2)
473
end
474
 
475
return {
476
    disassemble = function (chunk) return Chunk:unpack(chunk, 1) end,
477
    assemble = function (disassembled) return Chunk:pack(disassembled) end,
478
    validate = validate
479
}