#! /usr/bin/env python3
## vim:set ts=4 sw=4 et: -*- coding: utf-8 -*-
#
#  gpp_inc.py -- Generic PreProcessor: include
#
#  This file is part of the UPX executable compressor.
#
#  Copyright (C) Markus Franz Xaver Johannes Oberhumer
#  All Rights Reserved.
#
#  UPX and the UCL library are free software; you can redistribute them
#  and/or modify them 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; see the file COPYING.
#  If not, write to the Free Software Foundation, Inc.,
#  59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
#  Markus F.X.J. Oberhumer              Laszlo Molnar
#  <markus@oberhumer.com>               <ezerotven+github@gmail.com>
#


import getopt, os, re, sys


class opts:
    dry_run = 0
    verbose = 0
    fatal = 1
    includes = []
    mode = "c"
    target_mf = None
    target_mmd = None


files_md = []
files_mmd = []
files_st = {}

def add_dep(state, fn, mode):
    if mode:
        files = files_md
    else:
        files = files_mmd
    fn = os.path.normpath(fn)
    fn = os.path.normcase(fn)
    if fn in files:
        return
    # FIXME: could use os.path.samestat() etc.
    files.append(fn)
    files_st[fn] = os.stat(fn)


def not_found(state, l, inc, fatal=None):
    if fatal is None:
        fatal = opts.fatal
    if fatal:
        raise Exception("%s:%d: include file %s not found" % (state[0], state[2], inc))
    return l


def parse_comment(state, l, comment):
    cf = {}
    if not comment: return cf
    m = re.search(r"gpp_inc:(.+?):", comment.strip())
    if not m: return cf
    flags = re.split(r"\s+", m.group(1).strip())
    for f in flags:
        assert f, (f, flags, m.groups(), comment)
        if   f == "ignore=0": cf["fatal"] = 1
        elif f == "ignore=1": cf["fatal"] = 0
        else:
            raise Exception("%s:%d: bad flags %s %s" % (state[0], state[2], f, str(flags)))
    return cf


def handle_inc_c(state, l, ofp):
    m = re.search(r"^\s*\#\s*include\s+([\"\<])(.+?)([\"\>])(.*)$", l.decode())
    if not m:
        return l
    q1, inc, q2, comment = m.groups()
    cf = parse_comment(state, l, comment)
    if q1 == '<' and q2 == '>':
        dirs = opts.includes
    elif q1 == '"' and q2 == '"':
        dirs = [state[1]] + opts.includes
    else:
        raise Exception("syntax error: include line " + l.decode())
    for dir in dirs:
        fn = os.path.join(dir, inc)
        if os.path.isfile(fn):
            add_dep(state, fn, q1 == '<')
            handle_file(fn, ofp, state)
            return None
    return not_found(state, l, inc, cf.get("fatal"))


def handle_inc_nasm(state, l, ofp):
    m = re.search(r"^\s*\%\s*include\s+([\"\<])(.+?)([\"\>])(.*)$", l)
    if not m:
        return l
    q1, inc, q2, comment = m.groups()
    cf = parse_comment(state, l, comment)
    if q1 == '<' and q2 == '>':
        pass
    elif q1 == '"' and q2 == '"':
        pass
    else:
        raise Exception("syntax error: include line " + l)
    # info: nasm simply does concat the includes
    for prefix in opts.includes + [""]:
        fn = prefix + inc
        if os.path.isfile(fn):
            add_dep(state, fn, False)
            handle_file(fn, ofp, state)
            return None
    return not_found(state, l, inc, cf.get("fatal"))


def handle_file(ifn, ofp, parent_state=None):
    state = [ifn, os.path.dirname(ifn) or ".", 0, parent_state]
    ifp = open(ifn, "rb")
    for l in ifp.readlines():
        state[2] += 1       # line counter
        l = l.rstrip("\n".encode())
        if opts.mode == "c":
            l = handle_inc_c(state, l, ofp)
        elif opts.mode == "nasm":
            l = handle_inc_nasm(state, l, ofp)
        if l is not None:
            ofp.write(l + "\n".encode())


def main(argv):
    try: assert 0
    except AssertionError: pass
    else: raise Exception("fatal error - assertions not enabled")
    ofile = None
    shortopts, longopts = "qvI:o:", ["dry-run", "MF=", "MMD=", "mode=", "quiet", "verbose"]
    xopts, args = getopt.gnu_getopt(argv[1:], shortopts, longopts)
    for opt, optarg in xopts:
        if 0: pass
        elif opt in ["-q", "--quiet"]: opts.verbose = opts.verbose - 1
        elif opt in ["-v", "--verbose"]: opts.verbose = opts.verbose + 1
        elif opt in ["--dry-run"]: opts.dry_run = opts.dry_run + 1
        elif opt in ["-I"]: opts.includes.append(optarg)
        elif opt in ["-o"]: ofile = optarg
        elif opt in ["--mode"]: opts.mode = optarg.lower()
        elif opt in ["--MF"]: opts.target_mf = optarg
        elif opt in ["--MMD"]: opts.target_mmd = optarg
        else: assert 0, ("getopt problem:", opt, optarg, xopts, args)

    if ofile is None:
        assert len(args) == 2
        ifile = args[0]
        ofile = args[1]
    else:
        assert len(args) == 1
        ifile = args[0]

    assert os.path.isfile(ifile)
    ofp = open(ofile, "wb")
    handle_file(ifile, ofp)
    ofp.close()

    if opts.target_mmd:
        fn = ofile + ".d"
        if opts.target_mf:
            fn = opts.target_mf
        if os.path.isfile(fn):
            os.unlink(fn)
        if files_mmd:
            fp = open(fn, "wb")
            fp.write(("%s : \\\n" % opts.target_mmd).encode())
            for i, f in enumerate(files_mmd):
                if i < len(files_mmd) - 1:
                    fp.write(("  %s \\\n" % f).encode())
                else:
                    fp.write(("  %s\n" % f).encode())
            fp.close()


if __name__ == "__main__":
    sys.exit(main(sys.argv))
