#!/usr/bin/env python2
#
#  Utility to dump bytecode into a human readable form.
#

import os
import sys
import struct
import optparse

def decode_string(buf, off):
    strlen, = struct.unpack('>L', buf[off:off+4])
    off += 4
    strdata = buf[off:off+strlen]
    off += strlen

    return off, strdata

def sanitize_string(val):
    # Don't try to UTF-8 decode, just escape non-printable ASCII.
    def f(c):
        if ord(c) < 0x20 or ord(c) > 0x7e or c in '\'"':
            return '\\x%02x' % ord(c)
        else:
            return c
    return "'" + ''.join(map(f, val)) + "'"

def decode_sanitize_string(buf, off):
    off, val = decode_string(buf, off)
    return off, sanitize_string(val)

def dump_function(buf, off, ind):
    count_inst, count_const, count_funcs = struct.unpack('>LLL', buf[off:off+12])
    off += 12
    print '%sInstructions: %d' % (ind, count_inst)
    print '%sConstants: %d' % (ind, count_const)
    print '%sInner functions: %d' % (ind, count_funcs)

    nregs, nargs, start_line, end_line = struct.unpack('>HHLL', buf[off:off+12])
    off += 12
    print '%sNregs: %d' % (ind, nregs)
    print '%sNargs: %d' % (ind, nargs)
    print '%sStart line number: %d' % (ind, start_line)
    print '%sEnd line number: %d' % (ind, end_line)

    compfunc_flags, = struct.unpack('>L', buf[off:off+4])
    off += 4
    print '%sduk_hcompiledfunction flags: 0x%08x' % (ind, compfunc_flags)

    for i in xrange(count_inst):
        ins, = struct.unpack('>L', buf[off:off+4])
        off += 4
        print '%s  %06d: %08lx' % (ind, i, ins)

    print '%sConstants:' % ind
    for i in xrange(count_const):
        const_type, = struct.unpack('B', buf[off:off+1])
        off += 1

        if const_type == 0x00:
            off, strdata = decode_sanitize_string(buf, off)
            print '%s  %06d: %s' % (ind, i, strdata)
        elif const_type == 0x01:
            num, = struct.unpack('>d', buf[off:off+8])
            off += 8
            print '%s  %06d: %f' % (ind, i, num)
        else:
            raise Exception('invalid constant type: %d' % const_type)

    for i in xrange(count_funcs):
        print '%sInner function %d:' % (ind, i)
        off = dump_function(buf, off, ind + '  ')

    val, = struct.unpack('>L', buf[off:off+4])
    off += 4
    print '%s.length: %d' % (ind, val)
    off, val = decode_sanitize_string(buf, off)
    print '%s.name: %s' % (ind, val)
    off, val = decode_sanitize_string(buf, off)
    print '%s.fileName: %s' % (ind, val)
    off, val = decode_string(buf, off)  # actually a buffer
    print '%s._Pc2line: %s' % (ind, val.encode('hex'))

    while True:
        off, name = decode_string(buf, off)
        if name == '':
            break
        name = sanitize_string(name)
        val, = struct.unpack('>L', buf[off:off+4])
        off += 4
        print '%s_Varmap[%s] = %d' % (ind, name, val)

    idx = 0
    while True:
        off, name = decode_string(buf, off)
        if name == '':
            break
        name = sanitize_string(name)
        print '%s_Formals[%d] = %s' % (ind, idx, name)
        idx += 1

    return off

def dump_bytecode(buf, off, ind):
    sig, ver = struct.unpack('BB', buf[off:off+2])
    off += 2
    if sig != 0xff:
        raise Exception('invalid signature byte: %d' % sig)
    if ver != 0x00:
        raise Exception('unsupported bytecode version: %d' % ver)
    print '%sBytecode version: 0x%02x' % (ind, ver)

    off = dump_function(buf, off, ind + '  ')

    return off

def main():
    parser = optparse.OptionParser()
    parser.add_option('--hex-decode', dest='hex_decode', default=False, action='store_true', help='Input file is ASCII hex encoded, decode before dump')
    (opts, args) = parser.parse_args()

    with open(args[0], 'rb') as f:
        d = f.read()
        if opts.hex_decode:
            d = d.strip()
            d = d.decode('hex')
    dump_bytecode(d, 0, '')

if __name__ == '__main__':
    main()
