#!/usr/bin/env python
# -*- coding: utf-8 -*-

##############################################################################
# decodemif.py - Decodes a Symbian OS v9.x multi-image file (MIF)
# Copyright 2006, 2007 Jussi Ylänen
#
# This program is part of Ensymble developer utilities for Symbian OS(TM).
#
# Ensymble is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Ensymble is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ensymble; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
#
# Version history
# ---------------
#
# v0.03 2006-09-22
# Replaced every possible range(...) with xrange(...) for efficiency
#
# v0.02 2006-08-22
# Added file type recognition (SVG, binary SVG or other)
#
# v0.01 2006-08-14
# Added support for strange index entries (length 0)
#
# v0.00 2006-08-13
# Work started
##############################################################################

VERSION = "v0.02 2006-09-22"

import sys
import os
import struct
import getopt
import random
import tempfile

# Parameters
MAXMIFFILESIZE      = 1024 * 1024       # Arbitrary maximum size of MIF file

tempdir = None
dumpcounter = 0

def mkdtemp(template):
    '''
    Create a unique temporary directory.

    tempfile.mkdtemp() was introduced in Python v2.3. This is for
    backward compatibility.
    '''

    # Cross-platform way to determine a suitable location for temporary files.
    systemp = tempfile.gettempdir()

    if not template.endswith("XXXXXX"):
        raise ValueError("invalid template for mkdtemp(): %s" % template)

    for n in xrange(10000):
        randchars = []
        for m in xrange(6):
            randchars.append(random.choice("abcdefghijklmnopqrstuvwxyz"))

        tempdir = os.path.join(systemp, template[: -6]) + "".join(randchars)

        try:
            os.mkdir(tempdir, 0700)
            return tempdir
        except OSError:
            pass

def dumpdata(data):
    '''Dumps data to a file in a temporary directory.'''

    global tempdir, dumpcounter

    if tempdir == None:
        # Create temporary directory for dumped files.
        tempdir = mkdtemp("decodemif-XXXXXX")
        dumpcounter = 0

    # Determine file type.
    if data[0:5] == "<?xml":
        ext = "svg"
    elif data[0:4] == '\xcc\x56\xfa\x03':
        ext = "svgb"
    else:
        ext = "dat"

    filename = os.path.join(tempdir, "dump%04d.%s" % (dumpcounter, ext))
    dumpcounter += 1
    f = file(filename, "wb")
    f.write(data)
    f.close()
    print "%s written" % filename

def main():
    global tempdir, dumpcounter

    pgmname     = os.path.basename(sys.argv[0])
    pgmversion  = VERSION

    try:
        try:
            gopt = getopt.gnu_getopt
        except:
            # Python <v2.3, GNU-style parameter ordering not supported.
            gopt = getopt.getopt

        # Parse command line using getopt.
        short_opts = "t:h"
        long_opts = [
            "dumpdir", "help"
        ]
        args = gopt(sys.argv[1:], short_opts, long_opts)

        opts = dict(args[0])
        pargs = args[1]

        if "--help" in opts.keys() or "-h" in opts.keys():
            # Help requested.
            print (
'''
DecodeMIF - Symbian OS v9.x MIF file decoder %(pgmversion)s

usage: %(pgmname)s [--dumpdir=DIR] [miffiles...]

        -t, --dumpdir       - Directory to use for dumped files (or automatic)
        miffiles            - MIF files to decode (stdin if not given or -)

''' % locals())
            return 0

        # A temporary directory is generated by default.
        tempdir = opts.get("--dumpdir", opts.get("-t", None))

        if len(pargs) == 0:
            miffilenames = ["-"]
        else:
            miffilenames = pargs

        for miffilename in miffilenames:
            if miffilename == '-':
                miffile = sys.stdin
            else:
                miffile = file(miffilename, "rb")

            try:
                # Load the whole MIF file as a string.
                mifdata = miffile.read(MAXMIFFILESIZE)
                if len(mifdata) == MAXMIFFILESIZE:
                    raise IOError("%s: file too large" % miffilename)
            finally:
                if miffile != sys.stdin:
                    miffile.close()

            # Verify MIF signature.
            if mifdata[:4] != "B##4":
                raise ValueError("%s: not a MIF file" % miffilename)

            if len(mifdata) < 16:
                raise ValueError("%s: file too short" % miffilename)

            entries = struct.unpack("<L", mifdata[12:16])[0] / 2

            # Verify header length:
            # 16-byte header, 16 bytes per index entry
            if len(mifdata) < (16 + 16 * entries):
                raise ValueError("%s: file too short" % miffilename)

            # Read index.
            index = []
            for n in xrange(entries):
                hdroff = 16 + n * 16
                a = struct.unpack("<L", mifdata[hdroff +  0:hdroff + 4])[0]
                b = struct.unpack("<L", mifdata[hdroff +  4:hdroff + 8])[0]
                c = struct.unpack("<L", mifdata[hdroff +  8:hdroff + 12])[0]
                d = struct.unpack("<L", mifdata[hdroff + 12:hdroff + 16])[0]

                if b == 0 and d == 0:
                    # Unknown index entry type, skip it.
                    continue

                if a != c or b != d:
                    raise ValueError("%s: invalid index entry %d" %
                                     (miffilename, n))

                # Check total length of file.
                if a + b > len(mifdata):
                    raise ValueError("%s: index %d out of range" %
                                     (miffilename, n))

                index.append((a, b))

            n = len(index)
            print "%s: %s %s inside" % (miffilename, n or "no",
                                        ((n == 1) and "file") or "files")

            # Extract contents.
            for i in index:
                offset = i[0]
                length = i[1]
                if mifdata[offset:offset + 4] != "C##4":
                    raise ValueError("%s: invalid file header %d" %
                                     (miffilename, n))

                print "0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x" % (
                    tuple(struct.unpack("<LLLLLLL", mifdata[(offset + 4):
                                                            (offset + 32)])))
                dumpdata(mifdata[offset + 32:offset + length + 32])
    except (TypeError, ValueError, IOError, OSError), e:
        return "%s: %s" % (pgmname, str(e))
    except KeyboardInterrupt:
        return ""

# Call main if run as stand-alone executable.
if __name__ == '__main__':
    sys.exit(main())
