#!/usr/bin/python3
# SPDX-License-Identifier: BSD-2-Clause-Patent
import os
import sys
import json
import shutil
import pprint
import argparse
import subprocess

def openssl_configure(openssldir, target, ec = True):
    """ Run openssl Configure script. """
    cmdline = [
        'perl',
        'Configure',
        '--config=../UefiAsm.conf',
        '--api=1.1.1',
        '--with-rand-seed=none',
        target,
        'no-afalgeng',
        'no-aria',
        'no-async',
        'no-autoerrinit',
        'no-autoload-config',
        'no-bf',
        'no-blake2',
        'no-camellia',
        'no-capieng',
        'no-cast',
        'no-chacha',
        'no-cmac',
        'no-cmp',
        'no-cms',
        'no-ct',
        'no-deprecated',
        'no-des',
        'no-dgram',
        'no-dsa',
        'no-dso',
        'no-dtls',
        'no-dtls1-method',
        'no-dtls1_2-method',
        'no-dynamic-engine',
        'no-ec2m',
        'no-engine',
        'no-err',
        'no-filenames',
        'no-gost',
        'no-hw',
        'no-idea',
        'no-ktls',
        'no-makedepend',
        'no-module',
        'no-md4',
        'no-mdc2',
        'no-multiblock',
        'no-nextprotoneg',
        'no-pic',
        'no-psk',
        'no-ocb',
        'no-ocsp',
        'no-padlockeng',
        'no-poly1305',
        'no-posix-io',
        'no-rc2',
        'no-rc4',
        'no-rc5',
        'no-rfc3779',
        'no-rmd160',
        'no-scrypt',
        'no-seed',
        'no-shared',
        'no-siphash',
        'no-siv',
        'no-sm2',
        'no-sm4',
        'no-sock',
        'no-srp',
        'no-srtp',
        'no-ssl',
        'no-ssl3-method',
        'no-ssl-trace',
        'no-static-engine',
        'no-stdio',
        'no-threads',
        'no-tls1_3',
        'no-ts',
        'no-ui-console',
        'no-whirlpool',
        'disable-legacy',
    ]
    if not ec:
        cmdline += [ 'no-ec', ]
    print('')
    print(f'# -*-  configure openssl for {target} (ec={ec})  -*-')
    rc = subprocess.run(cmdline, cwd = openssldir,
                        stdout = subprocess.PIPE,
                        stderr = subprocess.PIPE)
    if rc.returncode:
        print(rc.stdout)
        print(rc.stderr)
        sys.exit(rc.returncode)

def openssl_run_make(openssldir, target):
    """
    Run make utility to generate files or cleanup.
    Target can be either a string or a list of strings.
    """
    cmdline = [ 'make', '--silent' ]
    if isinstance(target, list):
        cmdline += target
    else:
        cmdline += [ target, ]
    rc = subprocess.run(cmdline, cwd = openssldir)
    rc.check_returncode()

def get_configdata(openssldir):
    """
    Slurp openssl config data as JSON,
    using a little perl helper script.
    """
    cmdline = [
        'perl',
        'perl2json.pl',
        openssldir,
    ]
    rc = subprocess.run(cmdline, stdout = subprocess.PIPE)
    rc.check_returncode()
    return json.loads(rc.stdout)

def is_asm(filename):
    """ Check whenevr the passed file is an assembler file """
    if filename.endswith('.s') or filename.endswith('.S'):
        return True
    return False

def copy_generated_file(src, dst):
    src_file = []
    with open(src, 'r') as fsrc:
        src_file = fsrc.readlines()
    with open(dst, 'w') as fdst:
        for lines in range(len(src_file)):
            s = src_file[lines]
            s = s.rstrip() + "\r\n"
            fdst.write(s.expandtabs())

def generate_files(openssldir, opensslgendir, asm, filelist):
    """
    Generate files, using make, and copy over the results to the
    directory tree for generated openssl files.  Creates
    subdirectories as needed.
    """
    openssl_run_make(openssldir, filelist)
    for filename in filelist:
        src = os.path.join(openssldir, filename)
        if is_asm(filename):
            """ rename MSFT asm files to .nasm """
            if 'IA32-MSFT' in asm:
                filename = filename.replace('.S', '.nasm')
            elif 'X64-MSFT' in asm:
                filename = filename.replace('.s', '.nasm')
            dst = os.path.join(opensslgendir, asm, filename)
        else:
            dst = os.path.join(opensslgendir, filename)
        os.makedirs(os.path.dirname(dst), exist_ok = True)
        copy_generated_file(src, dst)

def generate_include_files(openssldir, opensslgendir, asm, cfg):
    """ Generate openssl include files """
    print('# generate include files')
    filelist = cfg['unified_info']['generate'].keys()
    filelist = list(filter(lambda f: 'include' in f, filelist))
    generate_files(openssldir, opensslgendir, asm, filelist)

def generate_library_files(openssldir, opensslgendir, asm, cfg, obj):
    """
    Generate openssl source files for a given library.  Handles
    mostly assembler files, but a few C sources are generated too.
    """
    filelist = get_source_list(cfg, obj, True)
    if filelist:
        print(f'# generate source files for {obj}')
        generate_files(openssldir, opensslgendir, asm, filelist)

def generate_all_files(openssldir, opensslgendir, asm, cfg):
    """ Generate all files needed. """
    generate_include_files(openssldir, opensslgendir, asm, cfg)
    generate_library_files(openssldir, opensslgendir, asm, cfg, 'libcrypto')
    generate_library_files(openssldir, opensslgendir, asm, cfg, 'providers/libcommon.a')
    generate_library_files(openssldir, opensslgendir, asm, cfg, 'libssl')

def get_source_list(cfg, obj, gen):
    """
    Gets the list of source files needed to create a specific object.
     * If 'gen' is True the function returns the list of generated
       files.
     * If 'gen' is False the function returns the list of files not
       generated (which are used from the submodule directly).
    Note: Will call itself recursively to resolve nested dependencies.
    """
    sources = cfg['unified_info']['sources']
    generate = cfg['unified_info']['generate']
    srclist = []
    if sources.get(obj):
        for item in sources.get(obj):
            srclist += get_source_list(cfg, item, gen)
    else:
        is_generated = generate.get(obj) is not None
        if is_generated == gen:
            srclist += [ obj, ]
    return srclist

def asm_filter_fn(filename):
    """
    Filter asm source and define lists.  Drops files we don't want include.
    """
    exclude = [
        '/bn/',
        'OPENSSL_BN_ASM',
        'OPENSSL_IA32_SSE2',
        '/ec/',
        'ECP_NISTZ256_ASM',
        'X25519_ASM',
    ]
    for item in exclude:
        if item in filename:
            return False
    return True

def get_sources(cfg, obj, asm):
    """
    Get the list of all sources files.  Will fetch both generated
    and not generated file lists and update the paths accordingly, so
    the openssl submodule or the sub-tree for generated files is
    referenced as needed.
    """
    srclist = get_source_list(cfg, obj, False)
    genlist = get_source_list(cfg, obj, True)
    srclist = list(map(lambda x: f'$(OPENSSL_PATH)/{x}', srclist))
    c_list = list(map(lambda x: f'$(OPENSSL_GEN_PATH)/{x}',
                      filter(lambda x: not is_asm(x), genlist)))
    asm_list = list(map(lambda x: f'$(OPENSSL_GEN_PATH)/{asm}/{x}',
                        filter(is_asm, genlist)))
    asm_list = list(filter(asm_filter_fn, asm_list))
    return srclist + c_list + asm_list

def sources_filter_fn(filename):
    """
    Filter source lists.  Drops files we don't want include or
    need replace with our own uefi-specific version.
    """
    exclude = [
        'randfile.c',
        '/store/',
        '/storemgmt/',
        '/encode_decode/encode',
        '/pkcs12/',
        'statem_srvr.c',
        'extensions_srvr.c',
        'defltprov.c',
        'baseprov.c',
        'provider_predefined.c',
        'ecp_nistz256.c',
        'x86_64-gcc.c',
    ]
    for item in exclude:
        if item in filename:
            return False
    return True

def libcrypto_sources(cfg, asm = None):
    """ Get source file list for libcrypto """
    files = get_sources(cfg, 'libcrypto', asm)
    files += get_sources(cfg, 'providers/libcommon.a', asm)
    files = list(filter(sources_filter_fn, files))
    return files

def libssl_sources(cfg, asm = None):
    """ Get source file list for libssl """
    files = get_sources(cfg, 'libssl', asm)
    files = list(filter(sources_filter_fn, files))
    return files

def update_inf(filename, sources, arch = None, defines = []):
    """
    Update inf file, replace source file list and build flags.
    """
    head = ''
    tail = ''
    state = 0

    if arch:
        section = f'Sources.{arch}'
        flags = f'OPENSSL_FLAGS_{arch}'
    else:
        section = None
        flags = f'OPENSSL_FLAGS_NOASM'
        state = 1

    # read and parse file
    with open(filename, 'r') as f:
        while True:
            line = f.readline()
            if line == '':
                break
            if state in [0, 1]:
                if flags in line:
                    (keep, replace) = line.split('=')
                    args = map(lambda x: f'-D{x}', defines)
                    head += keep + '= ' + ' '.join(args)
                    head = head.rstrip() + '\r\n'
                else:
                    head += line.rstrip() + '\r\n'
            if state == 0 and section in line:
                state = 1
            if state == 1 and 'Autogenerated files list starts here' in line:
                state = 2
            if state == 2 and 'Autogenerated files list ends here' in line:
                state = 3
            if state == 3:
                tail += line.rstrip() + '\r\n'

    # write updated file
    with open(filename, 'w') as f:
        f.write(head)
        for src in sources:
            f.write(f'  {src}\r\n')
        f.write(tail)

def update_MSFT_asm_format(asm, filelist):
    """ rename MSFT asm files to .nasm """
    if 'IA32-MSFT' in asm:
        for file_index in range(len(filelist)):
            filelist[file_index] = filelist[file_index].replace('.S', '.nasm')
    elif 'X64-MSFT' in asm:
        for file_index in range(len(filelist)):
            filelist[file_index] = filelist[file_index].replace('.s', '.nasm')

def main():
    # prepare
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    openssldir = os.path.join(os.getcwd(), 'openssl')
    opensslgendir = os.path.join(os.getcwd(), 'OpensslGen')

    # asm accel configs (see UefiAsm.conf)
    for ec in [True, False]:
        if ec:
            inf = 'OpensslLibFullAccel.inf'
            hdr = 'configuration-ec.h'
        else:
            inf = 'OpensslLibAccel.inf'
            hdr = 'configuration-noec.h'
        sources = {}
        defines = {}
        for asm in [ 'UEFI-IA32-MSFT', 'UEFI-IA32-GCC',
                     'UEFI-X64-MSFT', 'UEFI-X64-GCC']:
            (uefi, arch, cc) = asm.split('-')
            archcc = f'{arch}-{cc}'

            openssl_configure(openssldir, asm, ec = ec);
            cfg = get_configdata(openssldir)
            generate_all_files(openssldir, opensslgendir, archcc, cfg)
            shutil.move(os.path.join(opensslgendir, 'include', 'openssl', 'configuration.h'),
                        os.path.join(opensslgendir, 'include', 'openssl', hdr))
            openssl_run_make(openssldir, 'distclean')

            srclist = libcrypto_sources(cfg, archcc) + libssl_sources(cfg, archcc)
            sources[archcc] = list(map(lambda x: f'{x} | {cc}', filter(is_asm, srclist)))
            update_MSFT_asm_format(archcc, sources[archcc])
            sources[arch] = list(filter(lambda x: not is_asm(x), srclist))
            defines[arch] = cfg['unified_info']['defines']['libcrypto']
            defines[arch] = list(filter(asm_filter_fn, defines[arch]))

        ia32accel = sources['IA32'] + sources['IA32-MSFT'] + sources['IA32-GCC']
        x64accel = sources['X64'] + sources['X64-MSFT'] + sources['X64-GCC']
        update_inf(inf, ia32accel, 'IA32', defines['IA32'])
        update_inf(inf, x64accel, 'X64', defines['X64'])

    # noaccel - ec enabled
    openssl_configure(openssldir, 'UEFI', ec = True);
    cfg = get_configdata(openssldir)
    generate_all_files(openssldir, opensslgendir, None, cfg)
    openssl_run_make(openssldir, 'distclean')

    defines = []
    if 'libcrypto' in cfg['unified_info']['defines']:
        defines = cfg['unified_info']['defines']['libcrypto']

    update_inf('OpensslLibFull.inf',
               libcrypto_sources(cfg) + libssl_sources(cfg),
               defines)

    # noaccel - ec disabled
    openssl_configure(openssldir, 'UEFI', ec = False);
    cfg = get_configdata(openssldir)
    generate_all_files(openssldir, opensslgendir, None, cfg)
    openssl_run_make(openssldir, 'distclean')

    update_inf('OpensslLibCrypto.inf',
               libcrypto_sources(cfg),
               None, defines)
    update_inf('OpensslLib.inf',
               libcrypto_sources(cfg) + libssl_sources(cfg),
               None, defines)

    # wrap header file
    confighdr = os.path.join(opensslgendir, 'include', 'openssl', 'configuration.h')
    with open(confighdr, 'w') as f:
        f.write('#ifdef EDK2_OPENSSL_NOEC\r\n'
                '# include "configuration-noec.h"\r\n'
                '#else\r\n'
                '# include "configuration-ec.h"\r\n'
                '#endif\r\n')

if __name__ == '__main__':
    sys.exit(main())
