## @file
# This file is a script to build fit image.
# It generate a dtb header and combine a binary file after this header.
#
# Copyright (c) 2023, Intel Corporation. All rights reserved.<BR>
# SPDX-License-Identifier: BSD-2-Clause-Patent
##

from os.path import exists
import libfdt
from ctypes import *
import time
import os

class FIT_IMAGE_INFO_HEADER:
    """Class for user setting data to use MakeFitImage()
    """
    _pack_ = 1
    _fields_ = [
        ('Compatible',    str),
        ('UplVersion',    int),
        ('Description',   str),
        ('Type',          str),
        ('Arch',          str),
        ('Compression',   str),
        ('Revision',      int),
        ('BuildType',     str),
        ('Capabilities',  str),
        ('Producer',      str),
        ('ImageId',       str),
        ('DataOffset',    int),
        ('DataSize',      int),
        ('RelocStart',    int),
        ('LoadAddr',      int),
        ('Entry',         int),
        ('Binary',        str),
        ('TargetPath',    str),
        ('UefifvPath',    str),
        ('BdsfvPath',     str),
        ('NetworkfvPath', str),
        ('Project',       str),
        ]

    def __init__(self):
        self.Compatible     = 'universal-payload'
        self.UplVersion     = 0x0100
        self.TargetPath     = 'mkimage.fit'

def CreatFdt(Fdt):
    FdtEmptyTree = libfdt.fdt_create_empty_tree(Fdt, len(Fdt))
    if FdtEmptyTree != 0:
        print('\n- Failed - Create Fdt failed!')
        return False
    return True

def BuildConfNode(Fdt, ParentNode, MultiImage):
    ConfNode1     = libfdt.fdt_add_subnode(Fdt, ParentNode, 'conf-1')

    libfdt.fdt_setprop(Fdt, ConfNode1, 'require-fit', b'', 0)
    libfdt.fdt_setprop(Fdt, ConfNode1, 'firmware', bytes('tianocore', 'utf-8'), len('tianocore') + 1)

def BuildFvImageNode(Fdt, InfoHeader, ParentNode, DataOffset, DataSize, Description, Arch):
    libfdt.fdt_setprop_u32(Fdt, ParentNode, 'data-size', DataSize)
    libfdt.fdt_setprop_u32(Fdt, ParentNode, 'data-offset', DataOffset)
    libfdt.fdt_setprop(Fdt, ParentNode, 'compression', bytes('none',                'utf-8'), len('none') + 1)
    libfdt.fdt_setprop(Fdt, ParentNode, 'project',    bytes('tianocore',           'utf-8'), len('tianocore') + 1)
    libfdt.fdt_setprop(Fdt, ParentNode, 'arch',        bytes(Arch,                  'utf-8'), len(Arch) + 1)
    libfdt.fdt_setprop(Fdt, ParentNode, 'type',        bytes('flat-binary',         'utf-8'), len('flat-binary') + 1)
    libfdt.fdt_setprop(Fdt, ParentNode, 'description', bytes(Description,           'utf-8'), len(Description) + 1)

def BuildTianoImageNode(Fdt, InfoHeader, ParentNode, DataOffset, DataSize, Description, Arch):
    #
    # Set 'load' and 'data-offset' to reserve the memory first.
    # They would be set again when Fdt completes or this function parses target binary file.
    #
    if InfoHeader.LoadAddr is not None:
        libfdt.fdt_setprop_u64(Fdt, ParentNode, 'load', InfoHeader.LoadAddr)
    if InfoHeader.Entry is not None:
        libfdt.fdt_setprop_u64(Fdt, ParentNode, 'entry-start', InfoHeader.Entry)
    if InfoHeader.RelocStart is not None:
        libfdt.fdt_setprop_u32(Fdt, ParentNode, 'reloc-start', InfoHeader.RelocStart)
    if InfoHeader.DataSize is not None:
       libfdt.fdt_setprop_u32(Fdt, ParentNode, 'data-size', DataSize)
    if InfoHeader.DataOffset is not None:
        libfdt.fdt_setprop_u32(Fdt, ParentNode, 'data-offset', DataOffset)
    if InfoHeader.Producer is not None:
        libfdt.fdt_setprop(Fdt, ParentNode, 'producer', bytes(InfoHeader.Producer, 'utf-8'), len(InfoHeader.Producer) + 1)
    if InfoHeader.Capabilities is not None:
        CapStrs = ','.join(InfoHeader.Capabilities)
        libfdt.fdt_setprop(Fdt, ParentNode, 'capabilities', bytes(CapStrs, 'utf-8'), len(CapStrs) + 1)
    if InfoHeader.Type is not None:
        libfdt.fdt_setprop(Fdt, ParentNode, 'type', bytes(InfoHeader.Type, 'utf-8'), len(InfoHeader.Type) + 1)
    if InfoHeader.Arch is not None:
        libfdt.fdt_setprop(Fdt, ParentNode, 'arch', bytes(InfoHeader.Arch, 'utf-8'), len(InfoHeader.Arch) + 1)
    if InfoHeader.Project is not None:
        libfdt.fdt_setprop(Fdt, ParentNode, 'project', bytes(InfoHeader.Project, 'utf-8'), len(InfoHeader.Project) + 1)
    if InfoHeader.Description is not None:
        libfdt.fdt_setprop(Fdt, ParentNode, 'description', bytes(Description, 'utf-8'), len(Description) + 1)

#
# The subnode would be inserted from bottom to top of structure block.
#
def BuildFitImage(Fdt, InfoHeader, Arch):
    MultiImage = [
        ["tianocore",   InfoHeader.Binary,        BuildTianoImageNode , InfoHeader.Description,     None, 0 ],
        ["uefi-fv",     InfoHeader.UefifvPath,    BuildFvImageNode,     "UEFI Firmware Volume",     None, 0 ],
        ["bds-fv",      InfoHeader.BdsfvPath,     BuildFvImageNode ,    "BDS Firmware Volume",      None, 0 ],
        ["network-fv",  InfoHeader.NetworkfvPath, BuildFvImageNode ,    "Network Firmware Volume",  None, 0 ],
    ]

    #
    # Set basic information
    #
    libfdt.fdt_setprop_u32(Fdt, 0, 'build-revision', InfoHeader.Revision)
    libfdt.fdt_setprop_u32(Fdt, 0, 'spec-version', InfoHeader.UplVersion)

    #
    # Build configurations node
    #
    ConfNode  = libfdt.fdt_add_subnode(Fdt, 0, 'configurations')
    BuildConfNode(Fdt, ConfNode, MultiImage)

    # Build image
    DataOffset = InfoHeader.DataOffset
    for Index in range (0, len (MultiImage)):
        _, Path, _, _, _, _ = MultiImage[Index]
        if exists(Path) == 1:
            TempBinary = open(Path, 'rb')
            BinaryData = TempBinary.read()
            TempBinary.close()
            MultiImage[Index][-2] = BinaryData
            MultiImage[Index][-1] = DataOffset
            DataOffset += len (BinaryData)
    libfdt.fdt_setprop_u32(Fdt, 0, 'size', DataOffset)
    posix_time = int(time.time())
    libfdt.fdt_setprop_u32(Fdt, 0, 'timestamp', posix_time)
    DescriptionFit = 'Uefi OS Loader'
    libfdt.fdt_setprop(Fdt, 0, 'description', bytes(DescriptionFit, 'utf-8'), len(DescriptionFit) + 1)

    ImageNode = libfdt.fdt_add_subnode(Fdt, 0, 'images')
    for Item in reversed (MultiImage):
        Name, Path, BuildFvNode, Description, BinaryData, DataOffset = Item
        if os.path.exists (Item[1]) == False:
            continue
        FvNode = libfdt.fdt_add_subnode(Fdt, ImageNode, Name)
        BuildFvNode (Fdt, InfoHeader, FvNode, DataOffset, len(BinaryData), Description, Arch)

    #
    # Create new image file and combine all binary.
    #
    DtbFile = open(InfoHeader.TargetPath, "wb")
    DtbFile.truncate()
    DtbFile.write(Fdt)
    for Item in MultiImage:
        _, FilePath, _, _, BinaryData, _ = Item
        if os.path.exists (Item[1]) == False:
            continue
        DtbFile.write(BinaryData)
    DtbFile.close()

    return True

def MakeFitImage(InfoHeader, Arch):
    #
    # Allocate fdt byte array.
    #
    Fdt = bytearray(InfoHeader.DataOffset)

    #
    # Create fdt empty tree.
    #
    if CreatFdt(Fdt) is False:
        return False

    #
    # Parse args to build fit image.
    #
    return BuildFitImage(Fdt, InfoHeader, Arch)

def ReplaceFv (UplBinary, SectionFvFile, SectionName, Arch):
    try:
        #
        # Get Original Multi Fv
        #
        with open (UplBinary, "rb") as File:
            Dtb = File.read ()
        Fit          = libfdt.Fdt (Dtb)
        NewFitHeader = bytearray(Dtb[0:Fit.totalsize()])
        FitSize      = len(Dtb)

        LoadablesList = []
        ImagesNode    = libfdt.fdt_subnode_offset(NewFitHeader, 0, 'images')
        FvNode        = libfdt.fdt_subnode_offset(NewFitHeader, ImagesNode, 'uefi-fv')
        NodeDepth     = libfdt.fdt_node_depth (NewFitHeader, ImagesNode)
        node_name     = libfdt.fdt_get_name(NewFitHeader, FvNode)
        FvNode        = libfdt.fdt_next_node(NewFitHeader, FvNode, NodeDepth)

        while node_name[0][-2:] == 'fv':
            LoadablesList.append (node_name[0])
            node_name = libfdt.fdt_get_name(NewFitHeader, FvNode[0])
            FvNode = libfdt.fdt_next_node(NewFitHeader, FvNode[0], NodeDepth)
        #
        # Get current Fit Binary FV data
        #
        MultiFvList = []
        for Item in LoadablesList:
            ImageNode    = libfdt.fdt_subnode_offset(NewFitHeader, ImagesNode, Item)
            ImageOffset  = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, ImageNode, 'data-offset')[0], 'big')
            ImageSize    = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, ImageNode, 'data-size')[0], 'big')
            MultiFvList.append ([Item, Dtb[ImageOffset:ImageOffset + ImageSize]])

        IsFvExist = False
        for Index in range (0, len (MultiFvList)):
            if MultiFvList[Index][0] == SectionName:
                with open (SectionFvFile, 'rb') as File:
                    MultiFvList[Index][1] = File.read ()
                ImageNode     = libfdt.fdt_subnode_offset(NewFitHeader, ImagesNode, SectionName)
                ImageSize     = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, ImageNode, 'data-size')[0], 'big')
                ReplaceOffset = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, ImageNode, 'data-offset')[0], 'big')
                OffsetDelta   = len(MultiFvList[Index][1]) - ImageSize
                FitSize      += OffsetDelta
                IsFvExist     = True
                libfdt.fdt_setprop_u32(NewFitHeader, ImageNode, 'data-size', len(MultiFvList[Index][1]))

        #
        # Update new fit header
        #
        ImagesNode = libfdt.fdt_subnode_offset(NewFitHeader, 0, 'images')
        if (IsFvExist == False):
            with open (SectionFvFile, 'rb') as File:
                SectionFvFileBinary = File.read ()
            MultiFvList.append ([SectionName, SectionFvFileBinary])
            FvNode = libfdt.fdt_add_subnode(NewFitHeader, ImagesNode, SectionName)
            BuildFvImageNode (NewFitHeader, None, FvNode, FitSize, len(SectionFvFileBinary), SectionName + " Firmware Volume", Arch)
            FitSize += len(SectionFvFileBinary)
        else:
            for Index in range (0, len (MultiFvList)):
                ImageNode    = libfdt.fdt_subnode_offset(NewFitHeader, ImagesNode, MultiFvList[Index][0])
                ImageOffset  = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, ImageNode, 'data-offset')[0], 'big')
                if ImageOffset > ReplaceOffset:
                    libfdt.fdt_setprop_u32(NewFitHeader, ImageNode, 'data-offset', ImageOffset + OffsetDelta)

        ConfNodes     = libfdt.fdt_subnode_offset(NewFitHeader, 0, 'configurations')
        libfdt.fdt_setprop(NewFitHeader, ConfNodes, 'default', bytes('conf-1', 'utf-8'), len('conf-1') + 1)
        ConfNode      = libfdt.fdt_subnode_offset(NewFitHeader, ConfNodes, 'conf-1')

        libfdt.fdt_setprop_u32(NewFitHeader, 0, 'size', FitSize)

        #
        # Generate new fit image
        #
        ImagesNode    = libfdt.fdt_subnode_offset(NewFitHeader, 0, 'images')
        TianoNode     = libfdt.fdt_subnode_offset(NewFitHeader, ImagesNode, 'tianocore')
        TianoOffset   = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, TianoNode, 'data-offset')[0], 'big')
        TianoSize     = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, TianoNode, 'data-size')[0], 'big')
        TianoBinary   = Dtb[TianoOffset:TianoOffset + TianoSize]

        print("\nGenerate new fit image:")
        NewUplBinary = bytearray(FitSize)
        print("Update fit header\t to 0x0\t\t ~ " + str(hex(len(NewFitHeader))))
        NewUplBinary[:len(NewFitHeader)] = NewFitHeader
        print("Update tiano image\t to " + str(hex(len(NewFitHeader))) + "\t ~ " + str(hex(len(NewFitHeader) + len(TianoBinary))))
        NewUplBinary[len(NewFitHeader):len(NewFitHeader) + len(TianoBinary)] = TianoBinary
        for Index in range (0, len (MultiFvList)):
            ImageNode   = libfdt.fdt_subnode_offset(NewFitHeader, ImagesNode, MultiFvList[Index][0])
            ImageOffset = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, ImageNode, 'data-offset')[0], 'big')
            ImageSize   = int.from_bytes (libfdt.fdt_getprop (NewFitHeader, ImageNode, 'data-size')[0], 'big')
            NewUplBinary[ImageOffset:ImageOffset + ImageSize] = MultiFvList[Index][1]
            print("Update " + MultiFvList[Index][0] + "\t\t to " + str(hex(ImageOffset)) + "\t ~ " + str(hex(ImageOffset + ImageSize)))

        with open (UplBinary, "wb") as File:
            File.write (NewUplBinary)

        return 0
    except Exception as Ex:
        print(Ex)
        return 1
