#!/usr/bin/env python
#  -*- mode: python; -*-
#
# Volatility
# Authors:
#   Brendan Dolan-Gavitt
#   Mike Auty
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
#

"""
@author:       Brendan Dolan-Gavitt
@license:      GNU General Public License 2.0 or later
@contact:      brendandg@gatech.edu
@organization: Georgia Institute of Technology
"""

from optparse import OptionParser
import hashlib, os, sys

class VtypeHolder(object):

    unstable_var_prefix = "unknown_"

    def __init__(self):
        self.vtypes = None
        self.arrayname = None
        self.filename = None
        self.namemap = {}
        self.dellist = []
        self.basis = None

    def _rename_types(self, vtypes, namemap):
        # Apply the namemap within the types 
        for t in vtypes:
            for m in vtypes[t][1]:
                memb = vtypes[t][1][m]
                d = self._get_deepest(memb)
                if d in namemap:
                    vtypes[t][1][m] = self._deep_replace(memb, d, namemap[d])

        # Rename the types themselves
        for n in namemap:
            if n in vtypes:
                vtypes[namemap[n]] = vtypes[n]
                del vtypes[n]

        return vtypes

    def _deep_replace(self, t, search, repl):
        if t == search:
            return repl
        elif isinstance(t, list):
            return [self._deep_replace(x, search, repl) for x in t]
        else:
            return t

    def _get_deepest(self, t):
        if isinstance(t, list):
            if len(t) == 1:
                return t[0]
            else:
                for part in t:
                    res = self._get_deepest(part)
                    if res:
                        return res
                return None
        return None

    def _tuplify(self, types, t):
        if isinstance(t, list) or isinstance(t, tuple):
            return tuple(sorted([self._tuplify(types, x) for x in t]))
        elif isinstance(t, dict):
            return self._tuplify(types, t.items())
        elif isinstance(t, str) and t.startswith(self.unstable_var_prefix):
            return self._tuplify(types, types[t])
        else:
            return t

    def as_string(self, msizes = True):
        if not self.vtypes:
            return ""

        arrayname = self.arrayname
        if self.basis:
            arrayname += "_additions"

        output = arrayname + " = {\n"
        for t in sorted(self.vtypes):
            output += "  '{0}': [ {1:#x}, {{\n".format(t, self.vtypes[t][0])
            for m in sorted(self.vtypes[t][1], key = lambda m: self.vtypes[t][1][m][0]):
                if msizes:
                    output += "    '{0}': [{1:#x}, {2}],\n".format(m, self.vtypes[t][1][m][0], self.vtypes[t][1][m][1])
                else:
                    output += "    '{0}': [None, {1}],\n".format(m, self.vtypes[t][1][m][1])
            output += "   }],\n"
        output += "}\n"


        if self.basis:
            fn, an = self.basis
            fn = os.path.splitext(os.path.basename(fn))[0]
            output += "\n# We must use deepcopy to avoid overlays affecting multiple profiles\nimport copy\n"
            output += "import {0}\n".format(fn)
            output += "{0} = copy.deepcopy({1}.{2})\n".format(self.arrayname, fn, an)
            if self.dellist:
                for i in self.dellist:
                    output += "del {0}['{1}']\n".format(self.arrayname, i)
            output += "{0}.update({1})\n".format(self.arrayname, arrayname)

        return output

    def load(self, filename):
        self.filename = filename
        locs, globs = {}, {}
        execfile(filename, globs, locs)
        for i in locs.keys():
            if i.endswith('_types'):
                self.arrayname = i
        self.vtypes = locs[self.arrayname]

    def canonicalize(self):
        if not self.vtypes:
            return False
        namemap = {}
        unnamed = [t for t in self.vtypes if t.startswith(self.unstable_var_prefix)]

        # Create the namemap
        for t in unnamed:
            newname = "__volstablename_" + hashlib.md5(str(self._tuplify(self.vtypes, self.vtypes[t]))).hexdigest() #pylint: disable-msg=E1101
            if t in namemap:
                print "Conflicting names for {0}: {1} and {2}".format(t, newname, self.namemap[t])
            if newname in self.vtypes:
                print "Constructed name for {0} ({1}) already exists in vtypes".format(t, newname)
            namemap[t] = newname

        self.namemap = namemap
        self.vtypes = self._rename_types(self.vtypes, namemap)

    def decanonicalize(self, namemap = None):
        if not self.vtypes:
            return False
        if not namemap:
            namemap = self.namemap

        # reverse the namemap
        newnamemap = {}
        for i in namemap:
            newnamemap[namemap[i]] = i

        # Rename the types
        self.vtypes = self._rename_types(self.vtypes, newnamemap)

        # Rename the dellist members
        dellist = [ newnamemap[x] if x in newnamemap else x for x in self.dellist]
        self.dellist = dellist

    def diff(self, base):
        """Compresses these vtypes based on another vtypes"""
        self.basis = base.filename, base.arrayname
        removelist = []
        for i in base.vtypes:
            if i in self.vtypes:
                inithash = hashlib.md5(str(self._tuplify(base.vtypes, base.vtypes[i]))).hexdigest() #pylint: disable-msg=E1101
                diffhash = hashlib.md5(str(self._tuplify(self.vtypes, self.vtypes[i]))).hexdigest() #pylint: disable-msg=E1101
                if inithash == diffhash:
                    removelist.append(i)
            else:
                self.dellist.append(i)
        for i in removelist:
            del self.vtypes[i]

if __name__ == '__main__':
    usage = "usage: %prog [options] <file1> <file2>"
    parser = OptionParser(usage = usage)
    (opts, args) = parser.parse_args()

    if len(args) != 2:
        parser.error("Must provide both vtypes files.")

    # Ensure these can import any modules they require
    sys.path.append(os.path.dirname(args[0]))
    sys.path.append(os.path.dirname(args[1]))

    ### Rename 1
    v1 = VtypeHolder()
    v1.load(args[0])
    v1.canonicalize()
    ### Rename 2
    v2 = VtypeHolder()
    v2.load(args[1])
    v2.canonicalize()
    ### Compress
    v2.diff(v1)
    v2.decanonicalize(v1.namemap)
    # Verify that no two names map to the same value
    for conflict in v1.namemap:
        if conflict in v2.namemap:
            if v1.namemap[conflict] != v2.namemap[conflict]:
                ### Remove possible conflicting unnamed offsets in original naming convention
                del v2.namemap[conflict]
    v2.decanonicalize(v2.namemap)
    ### Print types
    print v2.as_string()
