#!/usr/bin/env python
import os
import sys
import glob
import shutil
import subprocess
from distutils.core import setup, Command
from distutils.dir_util import remove_tree

MODULE_NAME = "binwalk"
MODULE_VERSION = "2.2.0"
SCRIPT_NAME = MODULE_NAME
MODULE_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
VERSION_FILE = os.path.join(MODULE_DIRECTORY, "src", "binwalk", "core", "version.py")

# Python3 has a built-in DEVNULL; for Python2, we have to open
# os.devnull to redirect subprocess stderr output to the ether.
try:
    from subprocess import DEVNULL
except ImportError:
    DEVNULL = open(os.devnull, 'wb')

# If this version of binwalk was checked out from the git repository,
# include the git commit hash as part of the version number reported
# by binwalk.
try:
    label = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], stderr=DEVNULL).decode('utf-8')
    MODULE_VERSION = "%s-%s" % (MODULE_VERSION, label.strip())
except KeyboardInterrupt as e:
    raise e
except Exception:
    pass

# Python2/3 compliance
try:
    raw_input
except NameError:
    raw_input = input


def which(command):
    # /usr/local/bin is usually the default install path, though it may not be in $PATH
    usr_local_bin = os.path.sep.join([os.path.sep, 'usr', 'local', 'bin', command])

    try:
        location = subprocess.Popen(
            ["which", command],
            shell=False, stdout=subprocess.PIPE).communicate()[0].strip()
    except KeyboardInterrupt as e:
        raise e
    except Exception as e:
        pass

    if not location and os.path.exists(usr_local_bin):
        location = usr_local_bin

    return location


def find_binwalk_module_paths():
    paths = []

    try:
        import binwalk
        paths = binwalk.__path__
    except KeyboardInterrupt as e:
        raise e
    except Exception:
        pass

    return paths


def remove_binwalk_module(pydir=None, pybin=None):
    if pydir:
        module_paths = [pydir]
    else:
        module_paths = find_binwalk_module_paths()

    for path in module_paths:
        try:
            remove_tree(path)
        except OSError:
            pass

    if not pybin:
        pybin = which(MODULE_NAME)

    if pybin:
        try:
            sys.stdout.write("removing '%s'\n" % pybin)
            os.remove(pybin)
        except KeyboardInterrupt:
            pass
        except Exception:
            pass


class IDAUnInstallCommand(Command):
    description = "Uninstalls the binwalk IDA plugin module"
    user_options = [
        ('idadir=', None, 'Specify the path to your IDA install directory.'),
    ]

    def initialize_options(self):
        self.idadir = None
        self.mydir = os.path.join(os.path.dirname(
            os.path.realpath(__file__)), "src")

    def finalize_options(self):
        pass

    def run(self):
        if self.idadir is None:
            sys.stderr.write(
                "Please specify the path to your IDA install directory with the '--idadir' option!\n")
            return

        binida_dst_path = os.path.join(self.idadir, 'plugins', 'binida.py')
        binwalk_dst_path = os.path.join(self.idadir, 'python', 'binwalk')

        if os.path.exists(binida_dst_path):
            sys.stdout.write("removing '%s'\n" % binida_dst_path)
            os.remove(binida_dst_path)
        if os.path.exists(binwalk_dst_path):
            sys.stdout.write("removing '%s'\n" % binwalk_dst_path)
            shutil.rmtree(binwalk_dst_path)


class IDAInstallCommand(Command):
    description = "Installs the binwalk IDA plugin module"
    user_options = [
        ('idadir=', None, 'Specify the path to your IDA install directory.'),
    ]

    def initialize_options(self):
        self.idadir = None
        self.mydir = os.path.join(os.path.dirname(
            os.path.realpath(__file__)), "src")

    def finalize_options(self):
        pass

    def run(self):
        if self.idadir is None:
            sys.stderr.write(
                "Please specify the path to your IDA install directory with the '--idadir' option!\n")
            return

        binida_src_path = os.path.join(self.mydir, 'scripts', 'binida.py')
        binida_dst_path = os.path.join(self.idadir, 'plugins')

        if not os.path.exists(binida_src_path):
            sys.stderr.write(
                "ERROR: could not locate IDA plugin file '%s'!\n" %
                binida_src_path)
            return
        if not os.path.exists(binida_dst_path):
            sys.stderr.write(
                "ERROR: could not locate the IDA plugins directory '%s'! Check your --idadir option.\n" %
                binida_dst_path)
            return

        binwalk_src_path = os.path.join(self.mydir, 'binwalk')
        binwalk_dst_path = os.path.join(self.idadir, 'python')

        if not os.path.exists(binwalk_src_path):
            sys.stderr.write(
                "ERROR: could not locate binwalk source directory '%s'!\n" %
                binwalk_src_path)
            return
        if not os.path.exists(binwalk_dst_path):
            sys.stderr.write(
                "ERROR: could not locate the IDA python directory '%s'! Check your --idadir option.\n" %
                binwalk_dst_path)
            return

        binida_dst_path = os.path.join(binida_dst_path, 'binida.py')
        binwalk_dst_path = os.path.join(binwalk_dst_path, 'binwalk')

        if os.path.exists(binida_dst_path):
            os.remove(binida_dst_path)
        if os.path.exists(binwalk_dst_path):
            shutil.rmtree(binwalk_dst_path)

        sys.stdout.write("copying %s -> %s\n" %
                         (binida_src_path, binida_dst_path))
        shutil.copyfile(binida_src_path, binida_dst_path)

        sys.stdout.write("copying %s -> %s\n" %
                         (binwalk_src_path, binwalk_dst_path))
        shutil.copytree(binwalk_src_path, binwalk_dst_path)


class UninstallCommand(Command):
    description = "Uninstalls the Python module"
    user_options = [
        ('pydir=', None, 'Specify the path to the binwalk python module to be removed.'),
        ('pybin=', None, 'Specify the path to the binwalk executable to be removed.'),
    ]

    def initialize_options(self):
        self.pydir = None
        self.pybin = None

    def finalize_options(self):
        pass

    def run(self):
        remove_binwalk_module(self.pydir, self.pybin)


class CleanCommand(Command):
    description = "Clean Python build directories"
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        sys.stdout.write("removing '%s'\n" % (VERSION_FILE))

        try:
            os.remove(VERSION_FILE)
        except OSError as e:
            sys.stderr.write("failed to remove file '%s': %s\n" % (VERSION_FILE, str(e)))

        try:
            remove_tree(os.path.join(MODULE_DIRECTORY, "build"))
        except KeyboardInterrupt as e:
            raise e
        except Exception:
            pass

        try:
            remove_tree(os.path.join(MODULE_DIRECTORY, "dist"))
        except KeyboardInterrupt as e:
            raise e
        except Exception:
            pass


class AutoCompleteCommand(Command):
    description = "Install bash autocomplete file"
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        options = []
        autocomplete_file_path = "/etc/bash_completion.d/%s" % MODULE_NAME
        auto_complete = '''_binwalk()
{
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts="%s"

    if [[ ${cur} == -* ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
}
complete -F _binwalk binwalk'''

        (out, err) = subprocess.Popen(["binwalk", "--help"], stdout=subprocess.PIPE).communicate()
        for line in out.splitlines():
            if b'--' in line:
                long_opt = line.split(b'--')[1].split(b'=')[0].split()[0].strip()
                options.append('--' + long_opt.decode('utf-8'))

        with open(autocomplete_file_path, "w") as fp:
            fp.write(auto_complete % ' '.join(options))

class TestCommand(Command):
    description = "Run unit-tests"
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        # Need the nose module for testing
        import nose

        # cd into the testing directory. Otherwise, the src/binwalk
        # directory gets imported by nose which a) clutters the src
        # directory with a bunch of .pyc files and b) will fail anyway
        # unless a build/install has already been run which creates
        # the version.py file.
        testing_directory = os.path.join(MODULE_DIRECTORY, "testing", "tests")
        os.chdir(testing_directory)

        # Run the tests
        retval = nose.core.run(argv=['--exe','--with-coverage'])

        sys.stdout.write("\n")

        # Clean up the resulting pyc files in the testing directory
        for pyc in glob.glob("%s/*.pyc" % testing_directory):
            sys.stdout.write("removing '%s'\n" % pyc)
            os.remove(pyc)

        input_vectors_directory = os.path.join(testing_directory, "input-vectors")
        for extracted_directory in glob.glob("%s/*.extracted" % input_vectors_directory):
            remove_tree(extracted_directory)

        if retval == True:
           sys.exit(0)
        else:
           sys.exit(1)

# The data files to install along with the module
install_data_files = []
for data_dir in ["magic", "config", "plugins", "modules", "core"]:
    install_data_files.append("%s%s*" % (data_dir, os.path.sep))

# If doing a build or installation, then create a version.py file
# which defines the current binwalk version. This file is excluded
# from git in the .gitignore file.
if 'install' in ' '.join(sys.argv) or 'build' in ' '.join(sys.argv):
    sys.stdout.write("creating %s\n" % (VERSION_FILE))

    try:
        with open(VERSION_FILE, "w") as fp:
            fp.write("# This file has been auto-generated by setup.py\n")
            fp.write('__version__ = "%s"\n' % MODULE_VERSION)
    except IOError as e:
        sys.stderr.write("failed to create file %s: %s\n" % (VERSION_FILE, str(e)))
        sys.exit(1)

# Install the module, script, and support files
setup(
    name=MODULE_NAME,
    version=MODULE_VERSION,
    description="Firmware analysis tool",
    author="Craig Heffner",
    url="https://github.com/ReFirmLabs/%s" % MODULE_NAME,
    requires=[],
    package_dir={"": "src"},
    packages=[MODULE_NAME],
    package_data={MODULE_NAME: install_data_files},
    scripts=[
        os.path.join(
            "src",
            "scripts",
            SCRIPT_NAME)],
    cmdclass={
        'clean': CleanCommand,
        'uninstall': UninstallCommand,
        'idainstall': IDAInstallCommand,
        'idauninstall': IDAUnInstallCommand,
        'autocomplete' : AutoCompleteCommand,
        'test': TestCommand})
