#!/bin/sh
""":" # Shell script (in docstring to appease pylint)
# Find and invoke hermetic python3 interpreter
. "`dirname $0`/../../../vendor/google/aosp/scripts/envsetup.sh"
exec "$PY3" "$0" "$@"
# Shell script end

This program will take trusted application's manifest config JSON file as
input. Processes the JSON config file and creates packed data
mapping to C structures and dumps in binary format.

USAGE:
    manifest_compiler.py \
        --input <input_filename_1> \
        --input <input_filename_2> \
        --output <output_filename> \
        --constants <config_constants_file_1> \
        --constants <config_constants_file_2> \
        --header-dir <header_file_path>

    Arguments:
    input_filename  - Trusted app manifest config file in JSON format.
                      At least one manifest config should be provided.
                      Options from additional configs override scalars
                      and merge to lists of the main manifest config,
                      processing manifests in the order specified on the
                      command line.
    output_filename - Binary file containing packed manifest config data mapped
                      to C structres.
    config_constant_file - This is optional
                    Config file with constants in JSON format
                    Corresponding header file will be
                    created with its constants defined in it
    header_file_path - Directory in which header files to be generated.

    example:
        manifest_compiler.py \
                --input manifest.json \
                --input manifest_extra_mem_maps.json \
                --output output.bin \
                --constants manifest_constants.json \
                --header-dir \
                <build_dir>/user_tasks/trusty/user/app/sample/hwcrypto/include

    If the output filename is omitted, the compiler will only generate constants
    headers for the given constants files.


   Input sample JSON Manifest config file content -
   {
        "uuid": "SECURE_STORAGE_SERVER_APP_UUID",
        "min_heap": 4096,
        "min_stack": 4096,
        "mem_map": [
            {
                "id": 1,
                "addr": "0x70000000",
                "size": "0x1000"
            },
            {
                "id": 2,
                "addr": "0x70010000",
                "size": "0x100"
            },
            {
                "id": 3,
                "addr": "0x70020000",
                "size": "0x4",
                "type": "uncached_device",
                "non_secure": false
            }
        ],
        "mgmt_flags": {
            "restart_on_exit": true,
            "deferred_start": false,
            "non_critical_app": false
        },
        "start_ports": [
            {
                "name": "LOADABLE_START_PORT",
                "flags": {
                    "allow_ta_connect": true,
                    "allow_ns_connect": false
                }
            }
        ],
        "pinned_cpu": 3,
        "priority" : 10,
        "version": 1,
        "min_version": 1,
        "apploader_flags": {
            "requires_encryption": false
        }
   }

   JSON manifest constant config -
   {
        "header": "storage_constants.h",
        "constants": [
            {
                "name": "LOADABLE_START_PORT",
                "value": "com.android.trusty.appmgmt.loadable.start",
                "type": "port"
            },
            {
                "name": "SECURE_STORAGE_SERVER_APP_UUID",
                "value": "eca48f94-00aa-560e-8f8c-d94b50d484f3",
                "type": "uuid"
            }
        ]
    }
"""

import argparse
import io
import json
import os.path
import struct
import sys

assert (sys.version_info.major, sys.version_info.minor) >= (3, 6), (
        # pylint: disable-next=consider-using-f-string
        "Python 3.6 or newer is required; found {}. Did you forget to set PY3?"
        .format(sys.version))

# Manifest properties
UUID = "uuid"
MIN_HEAP = "min_heap"
MIN_STACK = "min_stack"
MIN_SHADOW_STACK = "min_shadow_stack"
MEM_MAP = "mem_map"
MEM_MAP_ID = "id"
MEM_MAP_ADDR = "addr"
MEM_MAP_SIZE = "size"
MEM_MAP_TYPE = "type"
MEM_MAP_TYPE_CACHED = "cached"
MEM_MAP_TYPE_UNCACHED = "uncached"
MEM_MAP_TYPE_UNCACHED_DEVICE = "uncached_device"
MEM_MAP_NON_SECURE = "non_secure"
MGMT_FLAGS = "mgmt_flags"
MGMT_FLAG_RESTART_ON_EXIT = "restart_on_exit"
MGMT_FLAG_DEFERRED_START = "deferred_start"
MGMT_FLAG_NON_CRITICAL_APP = "non_critical_app"
START_PORTS = "start_ports"
START_PORT_FLAGS = "flags"
START_PORT_NAME = "name"
START_PORT_ALLOW_TA_CONNECT = "allow_ta_connect"
START_PORT_ALLOW_NS_CONNECT = "allow_ns_connect"
APP_NAME = "app_name"
PINNED_CPU = "pinned_cpu"
PRIORITY = "priority"
VERSION = "version"
MIN_VERSION = "min_version"
APPLOADER_FLAGS = "apploader_flags"
APPLOADER_FLAGS_REQUIRES_ENCRYPTION = "requires_encryption"

# constants configs
CONSTANTS = "constants"
HEADER = "header"
CONST_NAME = "name"
CONST_VALUE = "value"
CONST_TYPE = "type"
CONST_UNSIGNED = "unsigned"
CONST_PORT = "port"
CONST_UUID = "uuid"
CONST_INT = "int"
CONST_BOOL = "bool"
CONST_ID = "identifier"

# CONFIG TAGS
# These values need to be kept in sync with lib/app_manifest/app_manifest.h
TRUSTY_APP_CONFIG_KEY_MIN_STACK_SIZE = 1
TRUSTY_APP_CONFIG_KEY_MIN_HEAP_SIZE = 2
TRUSTY_APP_CONFIG_KEY_MAP_MEM = 3
TRUSTY_APP_CONFIG_KEY_MGMT_FLAGS = 4
TRUSTY_APP_CONFIG_KEY_START_PORT = 5
TRUSTY_APP_CONFIG_KEY_PINNED_CPU = 6
TRUSTY_APP_CONFIG_KEY_VERSION = 7
TRUSTY_APP_CONFIG_KEY_MIN_SHADOW_STACK_SIZE = 8
TRUSTY_APP_CONFIG_KEY_APPLOADER_FLAGS = 9
TRUSTY_APP_CONFIG_KEY_PRIORITY = 10
TRUSTY_APP_CONFIG_KEY_MIN_VERSION = 11

# MEM_MAP ARCH_MMU_FLAGS
# These values need to be kept in sync with $LKROOT/include/arch/mmu.h
ARCH_MMU_FLAG_CACHED = 0 << 0
ARCH_MMU_FLAG_UNCACHED = 1 << 0
ARCH_MMU_FLAG_UNCACHED_DEVICE = 2 << 0
ARCH_MMU_FLAG_CACHE_MASK = 3 << 0
ARCH_MMU_FLAG_NS = 1 << 5

# MGMT FLAGS
# These values need to be kept in sync with lib/app_manifest/app_manifest.h
TRUSTY_APP_MGMT_FLAGS_NONE = 0
TRUSTY_APP_MGMT_FLAGS_RESTART_ON_EXIT = 1 << 0
TRUSTY_APP_MGMT_FLAGS_DEFERRED_START = 1 << 1
TRUSTY_APP_MGMT_FLAGS_NON_CRITICAL_APP = 1 << 2

# APPLOADER FLAGS
# These values need to be kept in sync with lib/app_manifest/app_manifest.h
TRUSTY_APP_APPLOADER_FLAGS_NONE = 0
TRUSTY_APP_APPLOADER_FLAGS_REQUIRES_ENCRYPTION = 1 << 0

# START_PORT flags
# These values need to be kept in sync with user/base/include/user/trusty_ipc.h
IPC_PORT_ALLOW_TA_CONNECT = 0x1
IPC_PORT_ALLOW_NS_CONNECT = 0x2

IPC_PORT_PATH_MAX = 64


class Constant(object):
    def __init__(self, name, value, type_, unsigned=False, hex_num=False):
        self.name = name
        self.value = value
        self.type = type_
        self.unsigned = unsigned
        self.hex_num = hex_num


class ConfigConstants(object):
    def __init__(self, constants, header):
        self.constants = constants
        self.header = header


class StartPortFlags(object):
    def __init__(self, allow_ta_connect, allow_ns_connect):
        self.allow_ta_connect = allow_ta_connect
        self.allow_ns_connect = allow_ns_connect


class StartPort(object):
    def __init__(self, name, name_size, start_port_flags):
        self.name = name
        self.name_size = name_size
        self.start_port_flags = start_port_flags


class MemIOMap(object):
    def __init__(self, id_, addr, size, type_, non_secure):
        self.id = id_
        self.addr = addr
        self.size = size
        self.type = type_
        self.non_secure = non_secure


class MgmtFlags(object):
    def __init__(self, restart_on_exit, deferred_start, non_critical_app):
        self.restart_on_exit = restart_on_exit
        self.deferred_start = deferred_start
        self.non_critical_app = non_critical_app


class ApploaderFlags(object):
    def __init__(self, requires_encryption):
        self.requires_encryption = requires_encryption


class Manifest(object):
    """Holds Manifest data to be used for packing"""

    def __init__(
            self,
            uuid,
            app_name,
            min_heap,
            min_stack,
            min_shadow_stack,
            mem_io_maps,
            mgmt_flags,
            start_ports,
            pinned_cpu,
            priority,
            version,
            min_version,
            apploader_flags,
    ):
        self.uuid = uuid
        self.app_name = app_name
        self.min_heap = min_heap
        self.min_stack = min_stack
        self.min_shadow_stack = min_shadow_stack
        self.mem_io_maps = mem_io_maps
        self.mgmt_flags = mgmt_flags
        self.start_ports = start_ports
        self.pinned_cpu = pinned_cpu
        self.priority = priority
        self.version = version
        self.min_version = min_version
        self.apploader_flags = apploader_flags


class Log(object):
    """Tracks errors during manifest compilation"""

    def __init__(self):
        self.error_count = 0

    def error(self, msg):
        sys.stderr.write(f"Error: {msg}\n")
        self.error_count += 1

    def error_occurred(self):
        return self.error_count > 0


def get_string_sub_type(field):
    """For the given manifest JSON field it returns its literal value type
    mapped.
    """
    if field == UUID:
        return CONST_UUID
    if field == START_PORT_NAME:
        return CONST_PORT
    # field with string value but doesn't support a constant
    return None


def get_constant(constants, key, type_, log):
    const = constants.get(key)
    if const is None:
        return None

    if const.type != type_:
        log.error(f"{key} constant type mismatch, expected type is {type_}")
        return None

    return const.value


def get_string(manifest_dict, key, constants, log, optional=False,
               default=None):
    """Determines whether the value for the given key in dictionary is of type
    string and if it is a string then returns the value.
    """
    if key not in manifest_dict:
        if not optional:
            log.error(f"Manifest is missing required attribute - {key}")
        return default

    value = manifest_dict.pop(key)

    # try to check is this field holding a constant
    type_ = get_string_sub_type(key)
    if type_:
        const_value = get_constant(constants, value, type_, log)
        if const_value is not None:
            return const_value

    return coerce_to_string(value, key, log)


def coerce_to_string(value, key, log):
    if not isinstance(value, str):
        log.error(
            "Invalid value for" +
            f" {key} - \"{value}\", Valid string value is expected")
        return None

    return value


def get_int(manifest_dict, key, constants, log, optional=False,
            default=None):
    """Determines whether the value for the given key in dictionary is of type
    integer and if it is int then returns the value
    """
    if key not in manifest_dict:
        if not optional:
            log.error(f"Manifest is missing required attribute - {key}")
        return default

    value = manifest_dict.pop(key)
    const_value = get_constant(constants, value, CONST_INT, log)
    if const_value is not None:
        return const_value

    return coerce_to_int(value, key, log)


def coerce_to_int(value, key, log):
    if isinstance(value, int) and not isinstance(value, bool):
        return value
    if isinstance(value, str):
        try:
            return int(value, 0)
        except ValueError:
            log.error(f"Invalid value for {key} - \"{value}\", " +
                      "valid integer or hex string is expected")
            return None
    else:
        log.error("Invalid value for" +
                  f" {key} - \"{value}\", valid integer value is expected")
        return None


def get_list(manifest_dict, key, log, optional=False, default=None):
    """Determines whether the value for the given key in dictionary is of type
    List and if it is List then returns the value
    """
    if key not in manifest_dict:
        if not optional:
            log.error(f"Manifest is missing required attribute - {key}")
        return default

    return coerce_to_list(manifest_dict.pop(key), key, log)


def coerce_to_list(value, key, log):
    if not isinstance(value, list):
        log.error("Invalid value for" +
                  f" {key} - \"{value}\", valid list is expected")
        return None

    return value


def get_dict(manifest_dict, key, log, optional=False, default=None):
    """Determines whether the value for the given key in dictionary is of type
    Dictionary and if it is Dictionary then returns the value
    """
    if key not in manifest_dict:
        if not optional:
            log.error(f"Manifest is missing required attribute - {key}")
        return default

    return coerce_to_dict(manifest_dict.pop(key), key, log)


def coerce_to_dict(value, key, log):
    if not isinstance(value, dict):
        log.error("Invalid value for" +
                  f" {key} - \"{value}\", valid dict is expected")
        return None

    return value


def get_boolean(manifest_dict, key, constants, log, optional=False,
                default=None):
    """Determines whether the value for the given key in dictionary is of type
    boolean and if it is boolean then returns the value
    """
    if key not in manifest_dict:
        if not optional:
            log.error(f"Manifest is missing required attribute - {key}")
        return default

    value = manifest_dict.pop(key)
    const_value = get_constant(constants, value, CONST_BOOL, log)
    if const_value is not None:
        return const_value

    return coerce_to_boolean(value, key, log)


def coerce_to_boolean(value, key, log):
    if not isinstance(value, bool):
        log.error(
            "Invalid value for" +
            f" {key} - \"{value}\", Valid boolean value is expected")
        return None

    return value


def get_uuid(manifest_dict, key, constants, log, optional=False, default=None):
    if key not in manifest_dict:
        if not optional:
            log.error(f"Manifest is missing required attribute - {key}")
        return default

    uuid = get_string(manifest_dict, key, {}, log, optional, default)
    const_value = get_constant(constants, uuid, CONST_UUID, log)
    if const_value is not None:
        return const_value

    return parse_uuid(uuid, log)


def get_port(port, key, constants, log, optional=False, default=None):
    return get_string(port, key, constants, log, optional, default)


def parse_uuid(uuid, log):
    """Validate and arrange UUID byte order. If it is valid UUID then return 16
    byte UUID
    """
    if uuid is None:
        return None

    # Example UUID: "5f902ace-5e5c-4cd8-ae54-87b88c22ddaf"
    if len(uuid) != 36:
        log.error(f"Invalid UUID {uuid}. UUID should be 16 bytes long")
        return None

    uuid_data = uuid.split("-")
    if len(uuid_data) != 5:
        log.error(
            f"Invalid UUID {uuid}. UUID should be 16 hexadecimal numbers"
            " divided into 5 groups by hyphens (-)"
        )
        return None

    try:
        uuid_data = [bytearray.fromhex(part) for part in uuid_data]
    except ValueError:
        log.error(
            f"Invalid UUID {uuid}. UUID should only contain hexadecimal"
            " numbers (separated by hyphens)"
        )
        return None

    if len(uuid_data[0]) != 4 or \
            len(uuid_data[1]) != 2 or \
            len(uuid_data[2]) != 2 or \
            len(uuid_data[3]) != 2 or \
            len(uuid_data[4]) != 6:
        log.error(f"Wrong grouping of UUID {uuid}")
        return None

    return b"".join(uuid_data)


def parse_memory_size(memory_size, memory_kind, log, zero_is_ok=True):
    """Validate memory size value. If valid, return memory size value else
    return None
    """
    if memory_size is None:
        return None

    if memory_size == 0 and not zero_is_ok:
        log.error(f"{memory_kind}: Minimum memory size cannot be zero.")
        return None
    if memory_size < 0 or memory_size % 4096 != 0:
        log.error(f"{memory_kind}: {memory_size}, Minimum memory size should " +
                  "be a non-negative multiple of 4096")
        return None

    return memory_size


def parse_shadow_stack_size(stack_size, log):
    """Validate the shadow stack size

    :returns: validated shadow stack size or None
    """
    if stack_size is None:
        return None

    # shadow call stack is only supported on arm64 where pointers are 8 bytes
    ptr_size = 8
    if stack_size < 0 or stack_size % ptr_size != 0:
        log.error(f"{MIN_SHADOW_STACK}: {stack_size}, Minimum shadow stack " +
                  "size should be a non-negative multiple of the native " +
                  "pointer size")
        return None

    return stack_size


def parse_mem_map_type(mem_map_type, log):
    if mem_map_type not in {MEM_MAP_TYPE_CACHED,
                            MEM_MAP_TYPE_UNCACHED,
                            MEM_MAP_TYPE_UNCACHED_DEVICE}:
        log.error(f"Unknown mem_map.type entry in manifest: {mem_map_type}")

    return mem_map_type


def parse_mem_map(mem_maps, key, constants, log):
    if mem_maps is None:
        return None

    mem_io_maps = []
    for mem_map_entry in mem_maps:
        mem_map_entry = coerce_to_dict(mem_map_entry, key, log)
        if mem_map_entry is None:
            continue
        mem_map = MemIOMap(
            get_int(mem_map_entry, MEM_MAP_ID, constants, log),
            get_int(mem_map_entry, MEM_MAP_ADDR, constants, log),
            get_int(mem_map_entry, MEM_MAP_SIZE, constants, log),
            parse_mem_map_type(
                get_string(mem_map_entry, MEM_MAP_TYPE, constants, log,
                           optional=True,
                           default=MEM_MAP_TYPE_UNCACHED_DEVICE), log),
            get_boolean(mem_map_entry, MEM_MAP_NON_SECURE, constants, log,
                        optional=True)
        )
        if mem_map_entry:
            log.error("Unknown attributes in mem_map entries in "
                      f"manifest: {mem_map_entry}")
        if any(item.id == mem_map.id for item in mem_io_maps):
            log.error(f"Duplicate mem_map ID found: {mem_map.id}")
        mem_io_maps.append(mem_map)

    return mem_io_maps


def parse_mgmt_flags(flags, constants, log):
    if flags is None:
        return None

    mgmt_flags = MgmtFlags(
        get_boolean(flags, MGMT_FLAG_RESTART_ON_EXIT, constants, log,
                    optional=True),
        get_boolean(flags, MGMT_FLAG_DEFERRED_START, constants, log,
                    optional=True),
        get_boolean(flags, MGMT_FLAG_NON_CRITICAL_APP, constants, log,
                    optional=True)
    )

    if flags:
        log.error("Unknown attributes in mgmt_flags entries in " +
                  f"manifest: {flags}")

    return mgmt_flags


def parse_apploader_flags(flags, constants, log):
    if flags is None:
        return None

    apploader_flags = ApploaderFlags(
        get_boolean(flags, APPLOADER_FLAGS_REQUIRES_ENCRYPTION, constants, log,
                    optional=True)
    )

    if flags:
        log.error("Unknown attributes in apploader_flags entries in " +
                  f"manifest: {flags}")

    return apploader_flags


def parse_app_start_ports(start_port_list, key, constants, log):
    start_ports = []

    for port_entry in start_port_list:
        port_entry = coerce_to_dict(port_entry, key, log)
        if port_entry is None:
            continue

        name = get_port(port_entry, START_PORT_NAME, constants, log)
        if len(name) >= IPC_PORT_PATH_MAX:
            log.error("Length of start port name should be less than " +
                      str(IPC_PORT_PATH_MAX))

        flags = get_dict(port_entry, START_PORT_FLAGS, log)
        start_ports_flag = None
        if flags:
            start_ports_flag = StartPortFlags(
                get_boolean(flags, START_PORT_ALLOW_TA_CONNECT, constants,
                            log),
                get_boolean(flags, START_PORT_ALLOW_NS_CONNECT, constants,
                            log))

        if port_entry:
            log.error("Unknown attributes in start_ports entries" +
                      f" in manifest: {port_entry}")
        if flags:
            log.error("Unknown attributes in start_ports.flags entries" +
                      f" in manifest: {flags}")

        start_ports.append(StartPort(name, len(name), start_ports_flag))

    return start_ports


def parse_app_name(app_name, log):
    if app_name is None:
        return None

    if not app_name:
        log.error("empty app-name is not allowed in manifest")
        return None

    return app_name.strip()


def parse_manifest_config(manifest_dict, constants, default_app_name, log):
    """validate the manifest config and extract key, values"""
    # UUID
    uuid = get_uuid(manifest_dict, UUID, constants, log)

    # MIN_HEAP
    min_heap = parse_memory_size(get_int(manifest_dict, MIN_HEAP, constants,
                                         log), MIN_HEAP, log)

    # MIN_STACK
    min_stack = parse_memory_size(get_int(manifest_dict, MIN_STACK, constants,
                                          log), MIN_STACK, log, False)

    # MIN_SHADOW_STACK
    min_shadow_stack = parse_shadow_stack_size(get_int(manifest_dict,
                                                       MIN_SHADOW_STACK,
                                                       constants, log,
                                                       optional=True), log)

    # MEM_MAP
    mem_io_maps = parse_mem_map(
        get_list(manifest_dict, MEM_MAP, log, optional=True, default=[]),
        MEM_MAP,
        constants, log)

    # MGMT_FLAGS
    mgmt_flags = parse_mgmt_flags(
        get_dict(manifest_dict, MGMT_FLAGS, log, optional=True,
                 default={
                     MGMT_FLAG_RESTART_ON_EXIT: False,
                     MGMT_FLAG_DEFERRED_START: False,
                     MGMT_FLAG_NON_CRITICAL_APP: False}),
        constants, log)

    # START_PORTS
    start_ports = parse_app_start_ports(
        get_list(manifest_dict, START_PORTS, log,
                 optional=True, default=[]),
        START_PORTS,
        constants,
        log)

    # APP_NAME
    app_name = parse_app_name(
        get_string(manifest_dict, APP_NAME, constants, log,
                   optional=True, default=default_app_name), log)

    # PINNED_CPU
    pinned_cpu = get_int(manifest_dict, PINNED_CPU, constants, log,
                         optional=True)

    # PRIORITY
    priority = get_int(manifest_dict, PRIORITY, constants, log, optional=True)

    # VERSION
    version = get_int(manifest_dict, VERSION, constants, log, optional=True)

    # MIN_VERSION
    min_version = get_int(
        manifest_dict, MIN_VERSION, constants, log, optional=True)

    if min_version is not None:
        if version is None:
            log.error("'min_version' cannot be specified without 'version'")
        elif version < min_version:
            log.error("'version' cannot be less than 'min_version'")

    # APPLOADER_FLAGS
    apploader_flags = parse_apploader_flags(
        get_dict(manifest_dict, APPLOADER_FLAGS, log, optional=True,
                 default={APPLOADER_FLAGS_REQUIRES_ENCRYPTION: False}),
        constants, log)

    # look for any extra attributes
    if manifest_dict:
        log.error(f"Unknown attributes in manifest: {manifest_dict} ")

    if log.error_occurred():
        return None

    return Manifest(uuid, app_name, min_heap, min_stack, min_shadow_stack,
                    mem_io_maps, mgmt_flags, start_ports, pinned_cpu,
                    priority, version, min_version, apploader_flags)


def swap_uuid_bytes(uuid):
    """This script represents UUIDs in a purely big endian order.
    Trusty stores the first three components of the UUID in little endian order.
    Rearrange the byte order accordingly by doing inverse
    on first three components of UUID
    """
    return uuid[3::-1] + uuid[5:3:-1] + uuid[7:5:-1] + uuid[8:]


def pack_mem_map_arch_mmu_flags(mem_map):
    arch_mmu_flags = 0

    if mem_map.type == MEM_MAP_TYPE_CACHED:
        arch_mmu_flags |= ARCH_MMU_FLAG_CACHED
    elif mem_map.type == MEM_MAP_TYPE_UNCACHED:
        arch_mmu_flags |= ARCH_MMU_FLAG_UNCACHED
    elif mem_map.type == MEM_MAP_TYPE_UNCACHED_DEVICE:
        arch_mmu_flags |= ARCH_MMU_FLAG_UNCACHED_DEVICE

    if mem_map.non_secure:
        arch_mmu_flags |= ARCH_MMU_FLAG_NS

    return arch_mmu_flags


def pack_mgmt_flags(mgmt_flags):
    flags = TRUSTY_APP_MGMT_FLAGS_NONE
    if mgmt_flags.restart_on_exit:
        flags |= TRUSTY_APP_MGMT_FLAGS_RESTART_ON_EXIT
    if mgmt_flags.deferred_start:
        flags |= TRUSTY_APP_MGMT_FLAGS_DEFERRED_START
    if mgmt_flags.non_critical_app:
        flags |= TRUSTY_APP_MGMT_FLAGS_NON_CRITICAL_APP

    return flags


def pack_apploader_flags(apploader_flags):
    flags = TRUSTY_APP_APPLOADER_FLAGS_NONE
    if apploader_flags.requires_encryption:
        flags |= TRUSTY_APP_APPLOADER_FLAGS_REQUIRES_ENCRYPTION

    return flags


def pack_start_port_flags(flags):
    start_port_flags = TRUSTY_APP_MGMT_FLAGS_NONE
    if flags.allow_ta_connect:
        start_port_flags |= IPC_PORT_ALLOW_TA_CONNECT
    if flags.allow_ns_connect:
        start_port_flags |= IPC_PORT_ALLOW_NS_CONNECT

    return start_port_flags


def pack_inline_string(value):
    """Pack a given string with null padding to make its size
    multiple of 4.
    packed data includes length + string + null + padding
    """
    size = len(value) + 1
    pad_len = 3 - (size + 3) % 4
    packed = struct.pack("I", size) + value.encode() + b"\0" + pad_len * b"\0"
    assert len(packed) % 4 == 0
    return packed


def pack_manifest_data(manifest):
    """Creates Packed data from extracted manifest data.
    Writes the packed data to binary file
    """
    # PACK {
    #        uuid, app_name_size, app_name,
    #        TRUSTY_APP_CONFIG_KEY_MIN_HEAP_SIZE, min_heap,
    #        TRUSTY_APP_CONFIG_KEY_MIN_STACK_SIZE, min_stack,
    #        TRUSTY_APP_CONFIG_KEY_MAP_MEM, id, addr, size,
    #        TRUSTY_APP_CONFIG_KEY_MGMT_FLAGS, mgmt_flags
    #        TRUSTY_APP_CONFIG_KEY_START_PORT, flag, name_size, name
    #        TRUSTY_APP_CONFIG_KEY_PINNED_CPU, pinned_cpu
    #        TRUSTY_APP_CONFIG_KEY_PRIORITY, priority
    #        TRUSTY_APP_CONFIG_KEY_VERSION, version
    #        TRUSTY_APP_CONFIG_KEY_MIN_VERSION, min_version
    #        TRUSTY_APP_CONFIG_KEY_MIN_SHADOW_STACK_SIZE, min_shadow_stack,
    #        TRUSTY_APP_CONFIG_KEY_APPLOADER_FLAGS, apploader_flags,
    #      }
    out = io.BytesIO()

    uuid = swap_uuid_bytes(manifest.uuid)
    out.write(uuid)

    out.write(pack_inline_string(manifest.app_name))

    if manifest.min_heap is not None:
        out.write(struct.pack("II", TRUSTY_APP_CONFIG_KEY_MIN_HEAP_SIZE,
                              manifest.min_heap))

    if manifest.min_stack is not None:
        out.write(struct.pack("II", TRUSTY_APP_CONFIG_KEY_MIN_STACK_SIZE,
                              manifest.min_stack))

    for memio_map in manifest.mem_io_maps:
        out.write(struct.pack("IIQQI",
                              TRUSTY_APP_CONFIG_KEY_MAP_MEM,
                              memio_map.id,
                              memio_map.addr,
                              memio_map.size,
                              pack_mem_map_arch_mmu_flags(memio_map)))

    if manifest.mgmt_flags is not None:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_MGMT_FLAGS,
                              pack_mgmt_flags(manifest.mgmt_flags)))

    for port_entry in manifest.start_ports:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_START_PORT,
                              pack_start_port_flags(
                                  port_entry.start_port_flags)))
        out.write(pack_inline_string(port_entry.name))

    if manifest.pinned_cpu is not None:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_PINNED_CPU,
                              manifest.pinned_cpu))

    if manifest.priority is not None:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_PRIORITY,
                              manifest.priority))

    if manifest.version is not None:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_VERSION,
                              manifest.version))

    if manifest.min_version is not None:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_MIN_VERSION,
                              manifest.min_version))

    if manifest.min_shadow_stack is not None:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_MIN_SHADOW_STACK_SIZE,
                              manifest.min_shadow_stack))

    if manifest.apploader_flags is not None:
        out.write(struct.pack("II",
                              TRUSTY_APP_CONFIG_KEY_APPLOADER_FLAGS,
                              pack_apploader_flags(manifest.apploader_flags)))

    return out.getvalue()


def unpack_binary_manifest_to_json(packed_data):
    """Creates manifest JSON string from packed manifest data"""
    return manifest_data_to_json(unpack_binary_manifest_to_data(packed_data))


def manifest_data_to_json(manifest):
    return json.dumps(manifest, sort_keys=True, indent=4)


def unpack_binary_manifest_to_data(packed_data):
    """This method can be used for extracting manifest data from packed binary.
    UUID should be present in packed data.
    """
    manifest = {}

    # Extract UUID
    uuid, packed_data = packed_data[:16], packed_data[16:]
    uuid = swap_uuid_bytes(uuid)
    uuid = uuid.hex()
    uuid = uuid[:8] + "-" \
           + uuid[8:12] + "-" \
           + uuid[12:16] + "-" \
           + uuid[16:20] + "-" \
           + uuid[20:]

    manifest[UUID] = uuid

    # Extract APP_NAME
    # read size of the name, this includes a null character
    (name_size,), packed_data = struct.unpack(
        "I", packed_data[:4]), packed_data[4:]
    # read the name without a trailing null character
    manifest[APP_NAME], packed_data = \
        packed_data[:name_size - 1].decode(), packed_data[name_size - 1:]
    # discard trailing null characters
    # it includes trailing null character of a string and null padding
    pad_len = 1 + 3 - (name_size + 3) % 4
    packed_data = packed_data[pad_len:]

    # Extract remaining app configurations
    while len(packed_data) > 0:
        (tag,), packed_data = struct.unpack(
            "I", packed_data[:4]), packed_data[4:]

        if tag == TRUSTY_APP_CONFIG_KEY_MIN_HEAP_SIZE:
            assert MIN_HEAP not in manifest
            (manifest[MIN_HEAP],), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
        elif tag == TRUSTY_APP_CONFIG_KEY_MIN_STACK_SIZE:
            assert MIN_STACK not in manifest
            (manifest[MIN_STACK],), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
        elif tag == TRUSTY_APP_CONFIG_KEY_MIN_SHADOW_STACK_SIZE:
            assert MIN_SHADOW_STACK not in manifest
            (manifest[MIN_SHADOW_STACK],), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
        elif tag == TRUSTY_APP_CONFIG_KEY_MAP_MEM:
            if MEM_MAP not in manifest:
                manifest[MEM_MAP] = []
            mem_map_entry = {}
            (id_,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            (addr,), packed_data = struct.unpack(
                "Q", packed_data[:8]), packed_data[8:]
            (size,), packed_data = struct.unpack(
                "Q", packed_data[:8]), packed_data[8:]
            (arch_mmu_flags,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            mem_map_entry[MEM_MAP_ID] = id_
            mem_map_entry[MEM_MAP_ADDR] = hex(addr)
            mem_map_entry[MEM_MAP_SIZE] = hex(size)
            mem_map_entry[MEM_MAP_TYPE] = {
                ARCH_MMU_FLAG_CACHED: MEM_MAP_TYPE_CACHED,
                ARCH_MMU_FLAG_UNCACHED: MEM_MAP_TYPE_UNCACHED,
                ARCH_MMU_FLAG_UNCACHED_DEVICE: MEM_MAP_TYPE_UNCACHED_DEVICE,
            }[arch_mmu_flags & ARCH_MMU_FLAG_CACHE_MASK]
            mem_map_entry[MEM_MAP_NON_SECURE] = bool(arch_mmu_flags &
                                                     ARCH_MMU_FLAG_NS)
            manifest[MEM_MAP].append(mem_map_entry)
        elif tag == TRUSTY_APP_CONFIG_KEY_MGMT_FLAGS:
            (flag,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            mgmt_flag = {
                MGMT_FLAG_RESTART_ON_EXIT: False,
                MGMT_FLAG_DEFERRED_START: False,
                MGMT_FLAG_NON_CRITICAL_APP: False
            }
            if flag & TRUSTY_APP_MGMT_FLAGS_RESTART_ON_EXIT:
                mgmt_flag[MGMT_FLAG_RESTART_ON_EXIT] = True
            if flag & TRUSTY_APP_MGMT_FLAGS_DEFERRED_START:
                mgmt_flag[MGMT_FLAG_DEFERRED_START] = True
            if flag & TRUSTY_APP_MGMT_FLAGS_NON_CRITICAL_APP:
                mgmt_flag[MGMT_FLAG_NON_CRITICAL_APP] = True
            manifest[MGMT_FLAGS] = mgmt_flag
        elif tag == TRUSTY_APP_CONFIG_KEY_START_PORT:
            if START_PORTS not in manifest:
                manifest[START_PORTS] = []
            start_port_entry = {}

            (flag,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]

            # read size of the name, this includes a null character
            (name_size,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            # read the name without a trailing null character
            start_port_entry[START_PORT_NAME], packed_data = (
                packed_data[:name_size - 1].decode(),
                packed_data[name_size - 1:]
            )
            # discard trailing null characters
            # it includes trailing null character of a string and null padding
            pad_len = 1 + 3 - (name_size + 3) % 4
            packed_data = packed_data[pad_len:]

            start_port_flags = {
                START_PORT_ALLOW_TA_CONNECT: False,
                START_PORT_ALLOW_NS_CONNECT: False
            }
            if flag & IPC_PORT_ALLOW_TA_CONNECT:
                start_port_flags[START_PORT_ALLOW_TA_CONNECT] = True
            if flag & IPC_PORT_ALLOW_NS_CONNECT:
                start_port_flags[IPC_PORT_ALLOW_NS_CONNECT] = True
            start_port_entry[START_PORT_FLAGS] = start_port_flags

            manifest[START_PORTS].append(start_port_entry)
        elif tag == TRUSTY_APP_CONFIG_KEY_PINNED_CPU:
            assert PINNED_CPU not in manifest
            (pinned_cpu,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            manifest[PINNED_CPU] = pinned_cpu
        elif tag == TRUSTY_APP_CONFIG_KEY_PRIORITY:
            assert PRIORITY not in manifest
            (priority,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            manifest[PRIORITY] = priority
        elif tag == TRUSTY_APP_CONFIG_KEY_VERSION:
            assert VERSION not in manifest
            (version,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            manifest[VERSION] = version
        elif tag == TRUSTY_APP_CONFIG_KEY_MIN_VERSION:
            assert MIN_VERSION not in manifest
            (min_version,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            manifest[MIN_VERSION] = min_version
        elif tag == TRUSTY_APP_CONFIG_KEY_APPLOADER_FLAGS:
            assert APPLOADER_FLAGS not in manifest
            (flag,), packed_data = struct.unpack(
                "I", packed_data[:4]), packed_data[4:]
            apploader_flag = {
                APPLOADER_FLAGS_REQUIRES_ENCRYPTION: False,
            }
            if flag & TRUSTY_APP_APPLOADER_FLAGS_REQUIRES_ENCRYPTION:
                apploader_flag[APPLOADER_FLAGS_REQUIRES_ENCRYPTION] = True
            manifest[APPLOADER_FLAGS] = apploader_flag
        else:
            raise Exception(f"Unknown tag: {tag}")

    return manifest


def write_packed_data_to_bin_file(packed_data, output_file, log):
    """Write packed data to binary file"""
    try:
        with open(output_file, "wb") as out_file:
            out_file.write(packed_data)
            out_file.close()
    except IOError as ex:
        log.error(f"Unable to write to output file: {output_file}\n" + str(ex))


def read_json_config_file(input_file, log):
    try:
        with open(input_file, "r", encoding="utf-8") as read_file:
            manifest_dict = json.load(read_file)
        return manifest_dict
    except IOError as ex:
        log.error(f"{input_file}: unable to open input file: {ex}")
        return None
    except json.JSONDecodeError as jde:
        location = f"{input_file}:{jde.lineno}:{jde.colno}"
        log.error(f"{location}: Unable to parse config JSON: {jde.msg}")
        return None
    except ValueError as ex:
        log.error(f"{input_file}: Unexpected error: {ex}")
        return None


def read_config_constants(const_config_files, log):
    const_configs_list = []
    for const_file in const_config_files:
        const_configs_list.append(read_json_config_file(const_file, log))

    return const_configs_list


def define_integer_const_entry(const):
    text = hex(const.value) if const.hex_num else str(const.value)
    if const.unsigned:
        text += "U"

    return f"#define {const.name} ({text})\n"


def define_string_const_entry(const):
    return f"#define {const.name} {json.dumps(const.value)}\n"

def define_identifier_const_entry(const):
    return f"#define {const.name} {const.value}\n"

def define_bool_const_entry(const):
    return f"#define {const.name} ({json.dumps(const.value)})\n"


def define_uuid_const_entry(const):
    uuid = const.value.hex()

    part = ", ".join(
        ["0x" + uuid[index:index + 2] for index in range(16, len(uuid), 2)])

    value = f"{{0x{uuid[:8]}, 0x{uuid[8:12]}, 0x{uuid[12:16]}, {{ {part} }}}}\n"

    return f"#define {const.name} {value}"


def create_header_entry(constant):
    if constant.type == CONST_PORT:
        return define_string_const_entry(constant)
    if constant.type == CONST_UUID:
        return define_uuid_const_entry(constant)
    if constant.type == CONST_INT:
        return define_integer_const_entry(constant)
    if constant.type == CONST_BOOL:
        return define_bool_const_entry(constant)
    if constant.type == CONST_ID:
        return define_identifier_const_entry(constant)

    raise Exception(f"Unknown tag: {constant.type}")


def write_consts_to_header_file(const_config, header_dir, log):
    """Writes given constants to header file in given header directory."""
    # Construct header file path
    header_file = os.path.join(header_dir, const_config.header)
    # Check whether the output directory of header file exist
    # If it not exists create it.
    dir_name = os.path.dirname(header_file)
    if dir_name and not os.path.exists(dir_name):
        os.makedirs(dir_name)

    try:
        with open(header_file, "w", encoding="utf-8") as out_file:
            out_file.write("#pragma once\n")
            out_file.write("#include <stdbool.h>\n\n")
            for const in const_config.constants:
                header_entries = create_header_entry(const)
                out_file.write(header_entries)
    except IOError as ex:
        log.error(f"Unable to write to header file: {header_file}\n" + str(ex))


def parse_constant(constant, log):
    """Parse a give JSON constant data structure"""
    const_type = get_string(constant, CONST_TYPE, {}, log)
    if const_type is None:
        return None

    name = get_string(constant, CONST_NAME, {}, log)
    if const_type == CONST_PORT or const_type == CONST_ID:
        value = get_string(constant, CONST_VALUE, {}, log)
        return Constant(name, value, const_type)
    if const_type == CONST_UUID:
        value = get_string(constant, CONST_VALUE, {}, log)
        return Constant(name, parse_uuid(value, log), const_type)
    if const_type == CONST_INT:
        unsigned = get_boolean(constant, CONST_UNSIGNED, {}, log)
        text_value = constant.get(CONST_VALUE)
        hex_num = isinstance(text_value, str) and text_value.startswith("0x")
        value = get_int(constant, CONST_VALUE, {}, log)
        return Constant(name, value, const_type, unsigned, hex_num)
    if const_type == CONST_BOOL:
        value = get_boolean(constant, CONST_VALUE, {}, log)
        return Constant(name, value, const_type)

    log.error(f"Unknown constant type: {const_type}")
    return None


def parse_config_constant(const_config, log):
    """Parse a given JSON constant-config data structure containing a header and
    list of constants
    """
    header_file = get_string(const_config, HEADER, {}, log)

    const_list = get_list(const_config, CONSTANTS, log, optional=False,
                          default=[])

    constants = []
    for item in const_list:
        item = coerce_to_dict(item, CONSTANTS, log)
        if item is None:
            continue
        constants.append(parse_constant(item, log))
        if item:
            log.error("Unknown attributes in constant: {item}")

    if const_config:
        log.error(f"Unknown attributes in constants config: {const_config}")

    return ConfigConstants(constants, header_file)


def extract_config_constants(config_consts_list, log):
    """Collects ConfigConstant(s) from list of JSON config constants data"""
    config_constants = []

    for config_const in config_consts_list:
        config_constants.append(parse_config_constant(config_const, log))

    return config_constants


def process_config_constants(const_config_files, header_dir, log):
    """Parse JSON config constants and creates separate header files with
    constants for each JSON config
    """
    if const_config_files is None:
        return []

    config_consts_list = read_config_constants(const_config_files, log)
    if log.error_occurred():
        return []

    config_constants = extract_config_constants(config_consts_list, log)
    if log.error_occurred():
        return []

    # generate header files
    for const_config in config_constants:
        write_consts_to_header_file(const_config, header_dir, log)

    return config_constants

def merge_manifest_dicts(manifests: list, log):
    """Merges multiple manifests
    """
    def merge(base, overlay, log):
        match base, overlay:
            case dict(), dict():
                common_keys = base.keys() & overlay.keys()
                res =  {k: merge(base[k], overlay[k], log) for k in common_keys}
                res |= {k: base[k] for k in base.keys() - common_keys}
                res |= {k: overlay[k] for k in overlay.keys() - common_keys}
                return res
            case list(), list():
                res = base.copy()
                res.extend(i for i in overlay if i not in base)
                return res
            case int() | float() | str(), int() | float() | str():
                return overlay  # overlay overrides base for scalars
            case _:
                log.error(
                    f"Unhandled type pair: {type(base)} and {type(overlay)}")
                return base

    manifest_dict_merged : dict = {}

    for manifest in manifests:
        manifest_dict_merged = merge(manifest_dict_merged, manifest, log)

    if log.error_occurred():
        return None

    return manifest_dict_merged

def process_manifest_files(manifest_files, constants, log):
    """Parse JSON manifest(s)
    """
    assert manifest_files

    manifest_dicts = []

    for manifest_file in manifest_files:
        if not os.path.exists(manifest_file):
            log.error(
                f"Manifest config JSON file doesn't exist: {manifest_file}")
            return None

        manifest_dict = read_json_config_file(manifest_file, log)
        if log.error_occurred():
            return None

        manifest_dicts.append(manifest_dict)

    manifest_dict_merged = merge_manifest_dicts(manifest_dicts, log)

    if manifest_dict_merged is None or log.error_occurred():
        return None

    default_app_name = os.path.basename(os.path.dirname(manifest_files[0]))

    return parse_manifest_config(manifest_dict_merged, constants,
                                 default_app_name, log)


def index_constants(config_constants):
    constants = {}
    for const_config in config_constants:
        for const in const_config.constants:
            constants[const.name] = const

    return constants


def main():
    """Handles the command line arguments. Parses the given manifest input file
    and creates packed data. Writes the packed data to binary output file.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-i", "--input",
        dest="input_filenames",
        required=False,
        action="append",
        type=str,
        help="Trusty app manifest in JSON format. "
             "If the flag is used to provide multiple input files, "
             "subsequent files overwrite the values provided in "
             "the first manifest file."
    )
    parser.add_argument(
        "-o", "--output",
        dest="output_filename",
        required=False,
        type=str,
        help="It will be binary file with packed manifest data"
    )
    parser.add_argument(
        "-c", "--constants",
        dest="constants",
        required=False,
        action="append",
        help="JSON file with manifest config constants"
    )
    parser.add_argument(
        "--header-dir",
        dest="header_dir",
        required=False,
        type=str,
        help="Directory path for generating headers"
    )
    parser.add_argument(
        "--enable-shadow-call-stack",
        dest="shadow_call_stack",
        required=False,
        action="store_true",  # implies default := False
        help="Allow apps to opt into having a shadow call stack. "
             "Without this flag, apps will not have shadow stacks "
             "even if their manifests define \"min_shadow_stack\"."
    )
    parser.add_argument(
        "--default-shadow-call-stack-size",
        dest="default_shadow_call_stack_size",
        required=False,
        default=4096,
        type=int,
        metavar="DEFAULT_SIZE",
        help="Controls the size of the default shadow call stack."
             "This option has no effect unless shadow call stacks "
             "are enabled via the --enable-shadow-call-stack flag."
    )
    # Parse the command line arguments
    args = parser.parse_args()
    if args.constants and not args.header_dir:
        parser.error("--header-dir is required if --constants are specified")

    if args.input_filenames and not args.output_filename:
        parser.error("Input file provided with no manifest output file.")

    if args.output_filename and not args.input_filenames:
        parser.error("Building a manifest output file requires an input file.")

    if args.default_shadow_call_stack_size <= 0:
        parser.error(
            "--default-shadow-call-stack-size expects a positive integer")

    log = Log()

    # collect config constants and create header files for each const config
    config_constants = process_config_constants(args.constants,
                                                args.header_dir, log)
    if log.error_occurred():
        return 1

    if not args.output_filename:
        return 0

    constants = index_constants(config_constants)

    manifest = process_manifest_files(args.input_filenames, constants, log)

    if manifest is None:
        return 1

    if log.error_occurred():
        return 1

    # Optionally adjust min_shadow_stack based on command line arguments
    if args.shadow_call_stack:
        # If shadow callstack is enabled but the size is not specified in the
        # manifest, set it to the default value.
        if manifest.min_shadow_stack is None:
            manifest.min_shadow_stack = args.default_shadow_call_stack_size
    else:
        # If shadow call stack is not enabled, make sure the size is set to
        # zero in the binary manifest. In the future, "not present" may
        # indicate the binary does not use a shadow callstack, but for now
        # we're making sure a value is always present.
        manifest.min_shadow_stack = 0

    assert (args.shadow_call_stack and manifest.min_shadow_stack > 0) != \
           (manifest.min_shadow_stack == 0)

    # Pack the data as per C structures
    packed_data = pack_manifest_data(manifest)
    if log.error_occurred():
        return 1

    # Write to file.
    write_packed_data_to_bin_file(packed_data, args.output_filename, log)
    if log.error_occurred():
        return 1

    return 0


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