From a64b944942d828fe98e4843929662aad7f47bcca Mon Sep 17 00:00:00 2001 From: "Chen, Christine" Date: Thu, 28 Apr 2022 20:49:37 +0800 Subject: [PATCH] BaseTools: Add FMMT Python Tool The FMMT python tool is used for firmware files operation, which has the Fv/FFs-based 'View'&'Add'&'Delete'&'Replace' operation function: 1.Parse a FD(Firmware Device) / FV(Firmware Volume) / FFS(Firmware Files) 2.Add a new FFS into a FV file (both included in a FD file or not) 3.Replace an FFS in a FV file with a new FFS file 4.Delete an FFS in a FV file (both included in a FD file or not) 5.Extract the FFS from a FV file (both included in a FD file or not) This version of FMMT Python tool does not support PEIM rebase feature, this feature will be added in future update. Currently the FMMT C tool is saved in edk2-staging repo, but its quality and coding style can't meet the Edk2 quality, which is hard to maintain (Hard/Duplicate Code; Regression bugs; Restrict usage). The new Python version keeps same functions with origin C version. It has higher quality and better coding style, and it is much easier to extend new functions and to maintain. REF: https://bugzilla.tianocore.org/show_bug.cgi?id=1847 RFC Link: https://edk2.groups.io/g/devel/message/82877 Staging Link: https://github.com/tianocore/edk2-staging/tree/PyFMMT Cc: Bob Feng Cc: Liming Gao Signed-off-by: Yuwei Chen Reviewed-by: Bob Feng Acked-by: Liming Gao --- BaseTools/BinWrappers/PosixLike/FMMT | 14 + BaseTools/BinWrappers/WindowsLike/FMMT.bat | 4 + BaseTools/Source/Python/FMMT/FMMT.py | 153 +++++ BaseTools/Source/Python/FMMT/FmmtConf.ini | 11 + .../Python/FMMT/Img/FirmwareVolumeFormat.png | Bin 0 -> 29515 bytes .../Source/Python/FMMT/Img/NodeTreeFormat.png | Bin 0 -> 79906 bytes BaseTools/Source/Python/FMMT/README.md | 184 +++++ BaseTools/Source/Python/FMMT/__init__.py | 6 + .../Python/FMMT/core/BinaryFactoryProduct.py | 380 +++++++++++ BaseTools/Source/Python/FMMT/core/BiosTree.py | 198 ++++++ .../Source/Python/FMMT/core/BiosTreeNode.py | 194 ++++++ .../Source/Python/FMMT/core/FMMTOperation.py | 197 ++++++ .../Source/Python/FMMT/core/FMMTParser.py | 87 +++ .../Source/Python/FMMT/core/FvHandler.py | 641 ++++++++++++++++++ .../Source/Python/FMMT/core/GuidTools.py | 179 +++++ .../Source/Python/FMMT/utils/FmmtLogger.py | 31 + .../Source/Python/FMMT/utils/FvLayoutPrint.py | 55 ++ .../Python/FirmwareStorageFormat/Common.py | 85 +++ .../FirmwareStorageFormat/FfsFileHeader.py | 66 ++ .../Python/FirmwareStorageFormat/FvHeader.py | 112 +++ .../FirmwareStorageFormat/SectionHeader.py | 110 +++ .../Python/FirmwareStorageFormat/__init__.py | 6 + 22 files changed, 2713 insertions(+) create mode 100755 BaseTools/BinWrappers/PosixLike/FMMT create mode 100644 BaseTools/BinWrappers/WindowsLike/FMMT.bat create mode 100644 BaseTools/Source/Python/FMMT/FMMT.py create mode 100644 BaseTools/Source/Python/FMMT/FmmtConf.ini create mode 100644 BaseTools/Source/Python/FMMT/Img/FirmwareVolumeFormat.png create mode 100644 BaseTools/Source/Python/FMMT/Img/NodeTreeFormat.png create mode 100644 BaseTools/Source/Python/FMMT/README.md create mode 100644 BaseTools/Source/Python/FMMT/__init__.py create mode 100644 BaseTools/Source/Python/FMMT/core/BinaryFactoryProduct.py create mode 100644 BaseTools/Source/Python/FMMT/core/BiosTree.py create mode 100644 BaseTools/Source/Python/FMMT/core/BiosTreeNode.py create mode 100644 BaseTools/Source/Python/FMMT/core/FMMTOperation.py create mode 100644 BaseTools/Source/Python/FMMT/core/FMMTParser.py create mode 100644 BaseTools/Source/Python/FMMT/core/FvHandler.py create mode 100644 BaseTools/Source/Python/FMMT/core/GuidTools.py create mode 100644 BaseTools/Source/Python/FMMT/utils/FmmtLogger.py create mode 100644 BaseTools/Source/Python/FMMT/utils/FvLayoutPrint.py create mode 100644 BaseTools/Source/Python/FirmwareStorageFormat/Common.py create mode 100644 BaseTools/Source/Python/FirmwareStorageFormat/FfsFileHeader.py create mode 100644 BaseTools/Source/Python/FirmwareStorageFormat/FvHeader.py create mode 100644 BaseTools/Source/Python/FirmwareStorageFormat/SectionHeader.py create mode 100644 BaseTools/Source/Python/FirmwareStorageFormat/__init__.py diff --git a/BaseTools/BinWrappers/PosixLike/FMMT b/BaseTools/BinWrappers/PosixLike/FMMT new file mode 100755 index 0000000000..86b2c6555c --- /dev/null +++ b/BaseTools/BinWrappers/PosixLike/FMMT @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +#python `dirname $0`/RunToolFromSource.py `basename $0` $* + +# If a ${PYTHON_COMMAND} command is available, use it in preference to python +if command -v ${PYTHON_COMMAND} >/dev/null 2>&1; then + python_exe=${PYTHON_COMMAND} +fi + +full_cmd=${BASH_SOURCE:-$0} # see http://mywiki.wooledge.org/BashFAQ/028 for a discussion of why $0 is not a good choice here +dir=$(dirname "$full_cmd") +cmd=${full_cmd##*/} + +export PYTHONPATH="$dir/../../Source/Python:$dir/../../Source/Python/FMMT:$dir/../../Source/Python${PYTHONPATH:+:"$PYTHONPATH"}" +exec "${python_exe:-python}" -m $cmd.$cmd "$@" diff --git a/BaseTools/BinWrappers/WindowsLike/FMMT.bat b/BaseTools/BinWrappers/WindowsLike/FMMT.bat new file mode 100644 index 0000000000..f0551c4ac5 --- /dev/null +++ b/BaseTools/BinWrappers/WindowsLike/FMMT.bat @@ -0,0 +1,4 @@ +@setlocal +@set ToolName=%~n0% +@set PYTHONPATH=%PYTHONPATH%;%BASE_TOOLS_PATH%\Source\Python;%BASE_TOOLS_PATH%\Source\Python\FMMT +@%PYTHON_COMMAND% -m %ToolName%.%ToolName% %* diff --git a/BaseTools/Source/Python/FMMT/FMMT.py b/BaseTools/Source/Python/FMMT/FMMT.py new file mode 100644 index 0000000000..10800e776a --- /dev/null +++ b/BaseTools/Source/Python/FMMT/FMMT.py @@ -0,0 +1,153 @@ +# @file +# Firmware Module Management Tool. +# +# Copyright (c) 2021, Intel Corporation. All rights reserved.
+# +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +# Import Modules +# +import argparse +from core.FMMTOperation import * + +parser = argparse.ArgumentParser(description=''' +View the Binary Structure of FD/FV/Ffs/Section, and Delete/Extract/Add/Replace a Ffs from/into a FV. +''') +parser.add_argument("--version", action="version", version='%(prog)s Version 1.0', + help="Print debug information.") +parser.add_argument("-v", "--View", dest="View", nargs='+', + help="View each FV and the named files within each FV: '-v inputfile outputfile, inputfiletype(.Fd/.Fv/.ffs/.sec)'") +parser.add_argument("-d", "--Delete", dest="Delete", nargs='+', + help="Delete a Ffs from FV: '-d inputfile TargetFvName(Optional) TargetFfsName outputfile\ + If not given TargetFvName, all the existed target Ffs will be deleted'") +parser.add_argument("-e", "--Extract", dest="Extract", nargs='+', + help="Extract a Ffs Info: '-e inputfile TargetFvName(Optional) TargetFfsName outputfile\ + If not given TargetFvName, the first found target Ffs will be extracted'") +parser.add_argument("-a", "--Add", dest="Add", nargs='+', + help="Add a Ffs into a FV:'-a inputfile TargetFvName newffsfile outputfile'") +parser.add_argument("-r", "--Replace", dest="Replace", nargs='+', + help="Replace a Ffs in a FV: '-r inputfile TargetFvName(Optional) TargetFfsName newffsfile outputfile\ + If not given TargetFvName, all the existed target Ffs will be replaced with new Ffs file)'") +parser.add_argument("-l", "--LayoutFileName", dest="LayoutFileName", nargs='+', + help="The output file which saves Binary layout: '-l xxx.txt'/'-l xxx.json'\ + If only provide file format as 'txt', \ + the file will be generated with default name (Layout_'InputFileName'.txt). \ + Currently supports two formats: json, txt. More formats will be added in the future") +parser.add_argument("-c", "--ConfigFilePath", dest="ConfigFilePath", nargs='+', + help="Provide the target FmmtConf.ini file path: '-c C:\Code\FmmtConf.ini' \ + FmmtConf file saves the target guidtool used in compress/uncompress process.\ + If do not provide, FMMT tool will search the inputfile folder for FmmtConf.ini firstly, if not found,\ + the FmmtConf.ini saved in FMMT tool's folder will be used as default.") + +def print_banner(): + print("") + +class FMMT(): + def __init__(self) -> None: + self.firmware_packet = {} + + def SetConfigFilePath(self, configfilepath:str) -> str: + os.environ['FmmtConfPath'] = os.path.abspath(configfilepath) + + def SetDestPath(self, inputfile:str) -> str: + os.environ['FmmtConfPath'] = '' + self.dest_path = os.path.dirname(os.path.abspath(inputfile)) + old_env = os.environ['PATH'] + os.environ['PATH'] = self.dest_path + os.pathsep + old_env + + def CheckFfsName(self, FfsName:str) -> str: + try: + return uuid.UUID(FfsName) + except: + return FfsName + + def GetFvName(self, FvName:str) -> str: + try: + return uuid.UUID(FvName) + except: + return FvName + + def View(self, inputfile: str, layoutfilename: str=None, outputfile: str=None) -> None: + # ViewFile(inputfile, ROOT_TYPE, logfile, outputfile) + self.SetDestPath(inputfile) + filetype = os.path.splitext(inputfile)[1].lower() + if filetype == '.fd': + ROOT_TYPE = ROOT_TREE + elif filetype == '.fv': + ROOT_TYPE = ROOT_FV_TREE + elif filetype == '.ffs': + ROOT_TYPE = ROOT_FFS_TREE + elif filetype == '.sec': + ROOT_TYPE = ROOT_SECTION_TREE + else: + ROOT_TYPE = ROOT_TREE + ViewFile(inputfile, ROOT_TYPE, layoutfilename, outputfile) + + def Delete(self, inputfile: str, TargetFfs_name: str, outputfile: str, Fv_name: str=None) -> None: + self.SetDestPath(inputfile) + if Fv_name: + DeleteFfs(inputfile, self.CheckFfsName(TargetFfs_name), outputfile, self.GetFvName(Fv_name)) + else: + DeleteFfs(inputfile, self.CheckFfsName(TargetFfs_name), outputfile) + + def Extract(self, inputfile: str, Ffs_name: str, outputfile: str, Fv_name: str=None) -> None: + self.SetDestPath(inputfile) + if Fv_name: + ExtractFfs(inputfile, self.CheckFfsName(Ffs_name), outputfile, self.GetFvName(Fv_name)) + else: + ExtractFfs(inputfile, self.CheckFfsName(Ffs_name), outputfile) + + def Add(self, inputfile: str, Fv_name: str, newffsfile: str, outputfile: str) -> None: + self.SetDestPath(inputfile) + AddNewFfs(inputfile, self.CheckFfsName(Fv_name), newffsfile, outputfile) + + def Replace(self,inputfile: str, Ffs_name: str, newffsfile: str, outputfile: str, Fv_name: str=None) -> None: + self.SetDestPath(inputfile) + if Fv_name: + ReplaceFfs(inputfile, self.CheckFfsName(Ffs_name), newffsfile, outputfile, self.GetFvName(Fv_name)) + else: + ReplaceFfs(inputfile, self.CheckFfsName(Ffs_name), newffsfile, outputfile) + + +def main(): + args=parser.parse_args() + status=0 + + try: + fmmt=FMMT() + if args.ConfigFilePath: + fmmt.SetConfigFilePath(args.ConfigFilePath[0]) + if args.View: + if args.LayoutFileName: + fmmt.View(args.View[0], args.LayoutFileName[0]) + else: + fmmt.View(args.View[0]) + elif args.Delete: + if len(args.Delete) == 4: + fmmt.Delete(args.Delete[0],args.Delete[2],args.Delete[3],args.Delete[1]) + else: + fmmt.Delete(args.Delete[0],args.Delete[1],args.Delete[2]) + elif args.Extract: + if len(args.Extract) == 4: + fmmt.Extract(args.Extract[0],args.Extract[2],args.Extract[3], args.Extract[1]) + else: + fmmt.Extract(args.Extract[0],args.Extract[1],args.Extract[2]) + elif args.Add: + fmmt.Add(args.Add[0],args.Add[1],args.Add[2],args.Add[3]) + elif args.Replace: + if len(args.Replace) == 5: + fmmt.Replace(args.Replace[0],args.Replace[2],args.Replace[3],args.Replace[4],args.Replace[1]) + else: + fmmt.Replace(args.Replace[0],args.Replace[1],args.Replace[2],args.Replace[3]) + else: + parser.print_help() + except Exception as e: + print(e) + + return status + + +if __name__ == "__main__": + exit(main()) diff --git a/BaseTools/Source/Python/FMMT/FmmtConf.ini b/BaseTools/Source/Python/FMMT/FmmtConf.ini new file mode 100644 index 0000000000..aa2444f11b --- /dev/null +++ b/BaseTools/Source/Python/FMMT/FmmtConf.ini @@ -0,0 +1,11 @@ +## @file +# This file is used to define the FMMT dependent external tool guid. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +a31280ad-481e-41b6-95e8-127f4c984779 TIANO TianoCompress +ee4e5898-3914-4259-9d6e-dc7bd79403cf LZMA LzmaCompress +fc1bcdb0-7d31-49aa-936a-a4600d9dd083 CRC32 GenCrc32 +d42ae6bd-1352-4bfb-909a-ca72a6eae889 LZMAF86 LzmaF86Compress +3d532050-5cda-4fd0-879e-0f7f630d5afb BROTLI BrotliCompress diff --git a/BaseTools/Source/Python/FMMT/Img/FirmwareVolumeFormat.png b/BaseTools/Source/Python/FMMT/Img/FirmwareVolumeFormat.png new file mode 100644 index 0000000000000000000000000000000000000000..7cc806a0133413a4aa037f1d43d80000546a5246 GIT binary patch literal 29515 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

GBy~|DLthW;7TXboX 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 literal 0 HcmV?d00001 diff --git a/BaseTools/Source/Python/FMMT/Img/NodeTreeFormat.png b/BaseTools/Source/Python/FMMT/Img/NodeTreeFormat.png new file mode 100644 index 0000000000000000000000000000000000000000..6335653ece1605cd0c53bd2cc2bc21d57d213dae GIT binary patch literal 79906 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 z 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 literal 0 HcmV?d00001 diff --git a/BaseTools/Source/Python/FMMT/README.md b/BaseTools/Source/Python/FMMT/README.md new file mode 100644 index 0000000000..87cbff8d71 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/README.md @@ -0,0 +1,184 @@ +# FMMT +## Overview +This FMMT tool is the python implementation of the edk2 FMMT tool which locates at https://github.com/tianocore/edk2-staging/tree/FceFmmt. +This implementation has the same usage as the edk2 FMMT, but it's more readable and relaiable. + +# FMMT User Guide + +#### Last updated April 28, 2022 + +Important Changes and Updates: + +- Oct 13, 2021 Initial Draft of FMMT Python Tool +- Apr 28, 2022 Optimize functions & Command line + +#### Note: + +- FMMT Python Tool keeps same function with origin FMMT C Tool. It is much easier to maintain and extend other functions. + +#### Known issue: + +- Currently, FMMT Python tool does not support PEIM rebase feature, this feature will be added in future update. + +# 1. Introduction + +## 1.1 Overview + +The Firmware Device is a persistent physical repository that contains firmware code and/or data. The firmware code and/or data stored in Firmware Volumes. Detail layout of Firmware Volumes is described in ?Figure 1. The Firmware Volume Format?. + +![](Img/FirmwareVolumeFormat.png) + +? Figure 1. The Firmware Volume Format + +In firmware development, binary file has its firmware layout following the Platform-Initialization Specification. Thus, operation on FV file / FFS file (Firmware File) is an efficient and convenient way for firmware function testing and developing. FMMT Python tool is used for firmware files operation. + +## 1.2 Tool Capabilities + +The FMMT tool is capable of: + +- Parse a FD (Firmware Device) / FV (Firmware Volume) / FFS (Firmware Files) + +- Add a new FFS into a FV file (both included in a FD file or not) + +- Replace an FFS in a FV file with a new FFS file + +- Delete an FFS in a FV file (both included in a FD file or not) + +- Extract the FFS from a FV file (both included in a FD file or not) + +## 1.3 References + +| Document | +| ------------------------------------------------ | +| UEFI Platform Initialization (PI) Specification | + +# 2. FMMT Python Tool Usage + +## 2.1 Required Files + +### 2.1.1 Independent use + +When independent use the FMMT Python Tool, the following files and settings are required: + +- GuidTool executable files used for Decompress/Compress Firmware data. + +- Environment variables path with GuidTool path setting. + +### 2.1.2 Use with Build System + +When use the FMMT Python Tool with Build System: + +- If only use Edk2 based GuidTool, do not need other preparation. + +- If use other customized GuidTool, need prepare the config file with GuidTool info. The syntax for GuidTool definition shown as follow: + + ***ToolsGuid ShortName Command*** + + -- Example: ***3d532050-5cda-4fd0-879e-0f7f630d5afb BROTLI BrotliCompress*** + +## 2.2 Syntax + +### 2.2.1 Syntax for Parse file + + ***-v < Inputfile > < Outputfile > -l < LogFileType > -c < ConfigFilePath >*** + +- Parse *Inputfile*, show its firmware layout with log file. *Outputfile* is optional, if inputs, the *Inputfile* will be encapsulated into *Outputfile* following the parsed firmware layout. *"-l LogFileType"* is optional, it decides the format of log file which saves Binary layout. Currently supports: json, txt. More formats will be added in the future. *"-c ConfigFilePath "* is optional, target FmmtConf.ini file can be selected with this parameter. If not provided, default FmmtConf.ini file will be used. +- Ex: py -3 FMMT.py -v test.fd + +### 2.2.2 Syntax for Add a new FFS + + ***-a < Inputfile > < TargetFvName/TargetFvGuid > < NewFfsFile > < Outputfile >*** + +- Add the *NewFfsFile* into *Inputfile*. *TargetFvName/TargetFvGuid* (Name or Guid) is the TargetFv which *NewFfsFile* will be added into. +- Ex: py -3 FMMT.py -a Ovmf.fd 6938079b-b503-4e3d-9d24-b28337a25806 NewAdd.ffs output.fd + +### 2.2.3 Syntax for Delete an FFS + + ***-d < Inputfile > < TargetFvName/TargetFvGuid > < TargetFfsName > < Outputfile >*** + +- Delete the Ffs from *Inputfile*. TargetFfsName (Guid) is the TargetFfs which will be deleted. *TargetFvName/TargetFvGuid* is optional, which is the parent of TargetFfs*.* +- Ex: py -3 FMMT.py -d Ovmf.fd 6938079b-b503-4e3d-9d24-b28337a25806 S3Resume2Pei output.fd + +### 2.2.4 Syntax for Replace an FFS + +? ***-r < Inputfile > < TargetFvName/TargetFvGuid > < TargetFfsName > < NewFfsFile > < Outputfile >*** + +- Replace the Ffs with the NewFfsFile. TargetFfsName (Guid) is the TargetFfs which will be replaced. *TargetFvName/TargetFvGuid* is optional, which is the parent of TargetFfs*.* +- Ex: py -3 FMMT.py -r Ovmf.fd 6938079b-b503-4e3d-9d24-b28337a25806 S3Resume2Pei NewS3Resume2Pei.ffs output.fd + +### 2.2.5 Syntax for Extract an FFS + + ***-e < Inputfile > < TargetFvName/TargetFvGuid > < TargetFfsName > < Outputfile >*** + +- Extract the Ffs from the Inputfile. TargetFfsName (Guid) is the TargetFfs which will be extracted. *TargetFvName/TargetFvGuid* is optional, which is the parent of TargetFfs*.* +- Ex: py -3 FMMT.py -e Ovmf.fd 6938079b-b503-4e3d-9d24-b28337a25806 S3Resume2Pei output.fd + +# 3. FMMT Python Tool Design + +FMMT Python Tool uses the NodeTree saves whole Firmware layout. Each Node have its Data field, which saves the FirmwareClass(FD/FV/FFS/SECTION/BINARY) Data. All the parse/add/delete/replace/extract operations are based on the NodeTree (adjusting the layout and data). + +## 3.1 NodeTree + +A whole NodeTree saves all the Firmware info. + +- Parent & Child relationship figured out the Firmware layout. + +- Each Node have several fields. ?Data? field saves an FirmwareClass instance which contains all the data info of the info. + +### 3.1.1 NodeTree Format + +The NodeTree will be created with parse function. When parse a file, a Root Node will be initialized firstly. The Data split and Tree construction process is described with an FD file shown as ?Figure 2. The NodeTree format?: + +- A Root Node is initialized. + +- Use the ?FV Signature? as FV key to split Whole FD Data. ?FV0?, ?FV1?, ?FV2?? Node created. + +- After FV level Node created, use the ?Ffs Data Size? as FFS key to split each FV Data. ?Ffs0?...Node created. + +- After FFS level Node created, use the ?Section Data Size? as Section key to split each Ffs Data. ?Section0?...Node created. + +- If some of Section includes other Sections, continue use the ?Section Data Size? as Section key to split each Section Data. + +- After all Node created, the whole NodeTree saves all the info. (Can be used in other functions or print the whole firmware layout into log file) + +![](Img/NodeTreeFormat.png) + +? Figure 2. The NodeTree format + +### 3.1.2 Node Factory and Product + +As 3.1.1, Each Node is created by data split and recognition. To extend the NodeTree usage, Factory pattern is used in Node created process. + +Each Node have its Factory to create Product and use Product ParserData function to deal with the data. + +## 3.2 GuidTool + +There are two ways to set the GuidTool. One from Config file, another from environment variables. + +Current GuidTool first check if has Config file. + +- If have, load the config GuidTool Information. + +- Else get from environment variables. + +### 3.2.1 Get from Config file + +- Config file should in same folder with FMMT.py or the path in environment variables. + +- Content should follow the format: + + ***ToolsGuid ShortName Command*** + +### 3.2.2 Get from Environment Variables + +- The GuidTool Command used must be set in environment variables. + +### 3.2.3 Edk2 Based GuidTool + +| ***Guid*** | ***ShortName*** | ***Command*** | +| ------------------------------------------ | --------------- | --------------------- | +| ***a31280ad-481e-41b6-95e8-127f4c984779*** | ***TIANO*** | ***TianoCompress*** | +| ***ee4e5898-3914-4259-9d6e-dc7bd79403cf*** | ***LZMA*** | ***LzmaCompress*** | +| ***fc1bcdb0-7d31-49aa-936a-a4600d9dd083*** | ***CRC32*** | ***GenCrc32*** | +| ***d42ae6bd-1352-4bfb-909a-ca72a6eae889*** | ***LZMAF86*** | ***LzmaF86Compress*** | +| ***3d532050-5cda-4fd0-879e-0f7f630d5afb*** | ***BROTLI*** | ***BrotliCompress*** | \ No newline at end of file diff --git a/BaseTools/Source/Python/FMMT/__init__.py b/BaseTools/Source/Python/FMMT/__init__.py new file mode 100644 index 0000000000..04e6ec098d --- /dev/null +++ b/BaseTools/Source/Python/FMMT/__init__.py @@ -0,0 +1,6 @@ +## @file +# This file is used to define the FMMT dependent external tool. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## \ No newline at end of file diff --git a/BaseTools/Source/Python/FMMT/core/BinaryFactoryProduct.py b/BaseTools/Source/Python/FMMT/core/BinaryFactoryProduct.py new file mode 100644 index 0000000000..2d4e6d9276 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/core/BinaryFactoryProduct.py @@ -0,0 +1,380 @@ +## @file +# This file is used to implement of the various bianry parser. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from re import T +import copy +import os +import sys +from FirmwareStorageFormat.Common import * +from core.BiosTreeNode import * +from core.BiosTree import * +from core.GuidTools import GUIDTools +from utils.FmmtLogger import FmmtLogger as logger + +ROOT_TREE = 'ROOT' +ROOT_FV_TREE = 'ROOT_FV_TREE' +ROOT_FFS_TREE = 'ROOT_FFS_TREE' +ROOT_SECTION_TREE = 'ROOT_SECTION_TREE' + +FV_TREE = 'FV' +DATA_FV_TREE = 'DATA_FV' +FFS_TREE = 'FFS' +FFS_PAD = 'FFS_PAD' +FFS_FREE_SPACE = 'FFS_FREE_SPACE' +SECTION_TREE = 'SECTION' +SEC_FV_TREE = 'SEC_FV_IMAGE' +BINARY_DATA = 'BINARY' +Fv_count = 0 + +## Abstract factory +class BinaryFactory(): + type:list = [] + + def Create_Product(): + pass + +class BinaryProduct(): + ## Use GuidTool to decompress data. + def DeCompressData(self, GuidTool, Section_Data: bytes, FileName) -> bytes: + guidtool = GUIDTools().__getitem__(struct2stream(GuidTool)) + if not guidtool.ifexist: + logger.error("GuidTool {} is not found when decompressing {} file.\n".format(guidtool.command, FileName)) + raise Exception("Process Failed: GuidTool not found!") + DecompressedData = guidtool.unpack(Section_Data) + return DecompressedData + + def ParserData(): + pass + +class SectionFactory(BinaryFactory): + type = [SECTION_TREE] + + def Create_Product(): + return SectionProduct() + +class FfsFactory(BinaryFactory): + type = [ROOT_SECTION_TREE, FFS_TREE] + + def Create_Product(): + return FfsProduct() + +class FvFactory(BinaryFactory): + type = [ROOT_FFS_TREE, FV_TREE, SEC_FV_TREE] + + def Create_Product(): + return FvProduct() + +class FdFactory(BinaryFactory): + type = [ROOT_FV_TREE, ROOT_TREE] + + def Create_Product(): + return FdProduct() + +class SectionProduct(BinaryProduct): + ## Decompress the compressed section. + def ParserData(self, Section_Tree, whole_Data: bytes, Rel_Whole_Offset: int=0) -> None: + if Section_Tree.Data.Type == 0x01: + Section_Tree.Data.OriData = Section_Tree.Data.Data + self.ParserSection(Section_Tree, b'') + # Guided Define Section + elif Section_Tree.Data.Type == 0x02: + Section_Tree.Data.OriData = Section_Tree.Data.Data + DeCompressGuidTool = Section_Tree.Data.ExtHeader.SectionDefinitionGuid + Section_Tree.Data.Data = self.DeCompressData(DeCompressGuidTool, Section_Tree.Data.Data, Section_Tree.Parent.Data.Name) + Section_Tree.Data.Size = len(Section_Tree.Data.Data) + Section_Tree.Data.HeaderLength + self.ParserSection(Section_Tree, b'') + elif Section_Tree.Data.Type == 0x03: + Section_Tree.Data.OriData = Section_Tree.Data.Data + self.ParserSection(Section_Tree, b'') + # SEC_FV Section + elif Section_Tree.Data.Type == 0x17: + global Fv_count + Sec_Fv_Info = FvNode(Fv_count, Section_Tree.Data.Data) + Sec_Fv_Tree = BIOSTREE('FV'+ str(Fv_count)) + Sec_Fv_Tree.type = SEC_FV_TREE + Sec_Fv_Tree.Data = Sec_Fv_Info + Sec_Fv_Tree.Data.HOffset = Section_Tree.Data.DOffset + Sec_Fv_Tree.Data.DOffset = Sec_Fv_Tree.Data.HOffset + Sec_Fv_Tree.Data.Header.HeaderLength + Sec_Fv_Tree.Data.Data = Section_Tree.Data.Data[Sec_Fv_Tree.Data.Header.HeaderLength:] + Section_Tree.insertChild(Sec_Fv_Tree) + Fv_count += 1 + + def ParserSection(self, ParTree, Whole_Data: bytes, Rel_Whole_Offset: int=0) -> None: + Rel_Offset = 0 + Section_Offset = 0 + # Get the Data from parent tree, if do not have the tree then get it from the whole_data. + if ParTree.Data != None: + Data_Size = len(ParTree.Data.Data) + Section_Offset = ParTree.Data.DOffset + Whole_Data = ParTree.Data.Data + else: + Data_Size = len(Whole_Data) + # Parser all the data to collect all the Section recorded in its Parent Section. + while Rel_Offset < Data_Size: + # Create a SectionNode and set it as the SectionTree's Data + Section_Info = SectionNode(Whole_Data[Rel_Offset:]) + Section_Tree = BIOSTREE(Section_Info.Name) + Section_Tree.type = SECTION_TREE + Section_Info.Data = Whole_Data[Rel_Offset+Section_Info.HeaderLength: Rel_Offset+Section_Info.Size] + Section_Info.DOffset = Section_Offset + Section_Info.HeaderLength + Rel_Whole_Offset + Section_Info.HOffset = Section_Offset + Rel_Whole_Offset + Section_Info.ROffset = Rel_Offset + if Section_Info.Header.Type == 0: + break + # The final Section in parent Section does not need to add padding, else must be 4-bytes align with parent Section start offset + Pad_Size = 0 + if (Rel_Offset+Section_Info.HeaderLength+len(Section_Info.Data) != Data_Size): + Pad_Size = GetPadSize(Section_Info.Size, SECTION_COMMON_ALIGNMENT) + Section_Info.PadData = Pad_Size * b'\x00' + if Section_Info.Header.Type == 0x02: + Section_Info.DOffset = Section_Offset + Section_Info.ExtHeader.DataOffset + Rel_Whole_Offset + Section_Info.Data = Whole_Data[Rel_Offset+Section_Info.ExtHeader.DataOffset: Rel_Offset+Section_Info.Size] + if Section_Info.Header.Type == 0x14: + ParTree.Data.Version = Section_Info.ExtHeader.GetVersionString() + if Section_Info.Header.Type == 0x15: + ParTree.Data.UiName = Section_Info.ExtHeader.GetUiString() + if Section_Info.Header.Type == 0x19: + if Section_Info.Data.replace(b'\x00', b'') == b'': + Section_Info.IsPadSection = True + Section_Offset += Section_Info.Size + Pad_Size + Rel_Offset += Section_Info.Size + Pad_Size + Section_Tree.Data = Section_Info + ParTree.insertChild(Section_Tree) + +class FfsProduct(BinaryProduct): + # ParserFFs / GetSection + def ParserData(self, ParTree, Whole_Data: bytes, Rel_Whole_Offset: int=0) -> None: + Rel_Offset = 0 + Section_Offset = 0 + # Get the Data from parent tree, if do not have the tree then get it from the whole_data. + if ParTree.Data != None: + Data_Size = len(ParTree.Data.Data) + Section_Offset = ParTree.Data.DOffset + Whole_Data = ParTree.Data.Data + else: + Data_Size = len(Whole_Data) + # Parser all the data to collect all the Section recorded in Ffs. + while Rel_Offset < Data_Size: + # Create a SectionNode and set it as the SectionTree's Data + Section_Info = SectionNode(Whole_Data[Rel_Offset:]) + Section_Tree = BIOSTREE(Section_Info.Name) + Section_Tree.type = SECTION_TREE + Section_Info.Data = Whole_Data[Rel_Offset+Section_Info.HeaderLength: Rel_Offset+Section_Info.Size] + Section_Info.DOffset = Section_Offset + Section_Info.HeaderLength + Rel_Whole_Offset + Section_Info.HOffset = Section_Offset + Rel_Whole_Offset + Section_Info.ROffset = Rel_Offset + if Section_Info.Header.Type == 0: + break + # The final Section in Ffs does not need to add padding, else must be 4-bytes align with Ffs start offset + Pad_Size = 0 + if (Rel_Offset+Section_Info.HeaderLength+len(Section_Info.Data) != Data_Size): + Pad_Size = GetPadSize(Section_Info.Size, SECTION_COMMON_ALIGNMENT) + Section_Info.PadData = Pad_Size * b'\x00' + if Section_Info.Header.Type == 0x02: + Section_Info.DOffset = Section_Offset + Section_Info.ExtHeader.DataOffset + Rel_Whole_Offset + Section_Info.Data = Whole_Data[Rel_Offset+Section_Info.ExtHeader.DataOffset: Rel_Offset+Section_Info.Size] + # If Section is Version or UI type, it saves the version and UI info of its parent Ffs. + if Section_Info.Header.Type == 0x14: + ParTree.Data.Version = Section_Info.ExtHeader.GetVersionString() + if Section_Info.Header.Type == 0x15: + ParTree.Data.UiName = Section_Info.ExtHeader.GetUiString() + if Section_Info.Header.Type == 0x19: + if Section_Info.Data.replace(b'\x00', b'') == b'': + Section_Info.IsPadSection = True + Section_Offset += Section_Info.Size + Pad_Size + Rel_Offset += Section_Info.Size + Pad_Size + Section_Tree.Data = Section_Info + ParTree.insertChild(Section_Tree) + +class FvProduct(BinaryProduct): + ## ParserFv / GetFfs + def ParserData(self, ParTree, Whole_Data: bytes, Rel_Whole_Offset: int=0) -> None: + Ffs_Offset = 0 + Rel_Offset = 0 + # Get the Data from parent tree, if do not have the tree then get it from the whole_data. + if ParTree.Data != None: + Data_Size = len(ParTree.Data.Data) + Ffs_Offset = ParTree.Data.DOffset + Whole_Data = ParTree.Data.Data + else: + Data_Size = len(Whole_Data) + # Parser all the data to collect all the Ffs recorded in Fv. + while Rel_Offset < Data_Size: + # Create a FfsNode and set it as the FFsTree's Data + if Data_Size - Rel_Offset < 24: + Ffs_Tree = BIOSTREE('Free_Space') + Ffs_Tree.type = FFS_FREE_SPACE + Ffs_Tree.Data = FreeSpaceNode(Whole_Data[Rel_Offset:]) + Ffs_Tree.Data.HOffset = Ffs_Offset + Rel_Whole_Offset + Ffs_Tree.Data.DOffset = Ffs_Tree.Data.HOffset + ParTree.Data.Free_Space = Data_Size - Rel_Offset + ParTree.insertChild(Ffs_Tree) + Rel_Offset = Data_Size + else: + Ffs_Info = FfsNode(Whole_Data[Rel_Offset:]) + Ffs_Tree = BIOSTREE(Ffs_Info.Name) + Ffs_Info.HOffset = Ffs_Offset + Rel_Whole_Offset + Ffs_Info.DOffset = Ffs_Offset + Ffs_Info.Header.HeaderLength + Rel_Whole_Offset + Ffs_Info.ROffset = Rel_Offset + if Ffs_Info.Name == PADVECTOR: + Ffs_Tree.type = FFS_PAD + Ffs_Info.Data = Whole_Data[Rel_Offset+Ffs_Info.Header.HeaderLength: Rel_Offset+Ffs_Info.Size] + Ffs_Info.Size = len(Ffs_Info.Data) + Ffs_Info.Header.HeaderLength + # if current Ffs is the final ffs of Fv and full of b'\xff', define it with Free_Space + if struct2stream(Ffs_Info.Header).replace(b'\xff', b'') == b'': + Ffs_Tree.type = FFS_FREE_SPACE + Ffs_Info.Data = Whole_Data[Rel_Offset:] + Ffs_Info.Size = len(Ffs_Info.Data) + ParTree.Data.Free_Space = Ffs_Info.Size + else: + Ffs_Tree.type = FFS_TREE + Ffs_Info.Data = Whole_Data[Rel_Offset+Ffs_Info.Header.HeaderLength: Rel_Offset+Ffs_Info.Size] + # The final Ffs in Fv does not need to add padding, else must be 8-bytes align with Fv start offset + Pad_Size = 0 + if Ffs_Tree.type != FFS_FREE_SPACE and (Rel_Offset+Ffs_Info.Header.HeaderLength+len(Ffs_Info.Data) != Data_Size): + Pad_Size = GetPadSize(Ffs_Info.Size, FFS_COMMON_ALIGNMENT) + Ffs_Info.PadData = Pad_Size * b'\xff' + Ffs_Offset += Ffs_Info.Size + Pad_Size + Rel_Offset += Ffs_Info.Size + Pad_Size + Ffs_Tree.Data = Ffs_Info + ParTree.insertChild(Ffs_Tree) + +class FdProduct(BinaryProduct): + type = [ROOT_FV_TREE, ROOT_TREE] + + ## Create DataTree with first level /fv Info, then parser each Fv. + def ParserData(self, WholeFvTree, whole_data: bytes=b'', offset: int=0) -> None: + # Get all Fv image in Fd with offset and length + Fd_Struct = self.GetFvFromFd(whole_data) + data_size = len(whole_data) + Binary_count = 0 + global Fv_count + # If the first Fv image is the Binary Fv, add it into the tree. + if Fd_Struct[0][1] != 0: + Binary_node = BIOSTREE('BINARY'+ str(Binary_count)) + Binary_node.type = BINARY_DATA + Binary_node.Data = BinaryNode(str(Binary_count)) + Binary_node.Data.Data = whole_data[:Fd_Struct[0][1]] + Binary_node.Data.Size = len(Binary_node.Data.Data) + Binary_node.Data.HOffset = 0 + offset + WholeFvTree.insertChild(Binary_node) + Binary_count += 1 + # Add the first collected Fv image into the tree. + Cur_node = BIOSTREE(Fd_Struct[0][0]+ str(Fv_count)) + Cur_node.type = Fd_Struct[0][0] + Cur_node.Data = FvNode(Fv_count, whole_data[Fd_Struct[0][1]:Fd_Struct[0][1]+Fd_Struct[0][2][0]]) + Cur_node.Data.HOffset = Fd_Struct[0][1] + offset + Cur_node.Data.DOffset = Cur_node.Data.HOffset+Cur_node.Data.Header.HeaderLength + Cur_node.Data.Data = whole_data[Fd_Struct[0][1]+Cur_node.Data.Header.HeaderLength:Fd_Struct[0][1]+Cur_node.Data.Size] + WholeFvTree.insertChild(Cur_node) + Fv_count += 1 + Fv_num = len(Fd_Struct) + # Add all the collected Fv image and the Binary Fv image between them into the tree. + for i in range(Fv_num-1): + if Fd_Struct[i][1]+Fd_Struct[i][2][0] != Fd_Struct[i+1][1]: + Binary_node = BIOSTREE('BINARY'+ str(Binary_count)) + Binary_node.type = BINARY_DATA + Binary_node.Data = BinaryNode(str(Binary_count)) + Binary_node.Data.Data = whole_data[Fd_Struct[i][1]+Fd_Struct[i][2][0]:Fd_Struct[i+1][1]] + Binary_node.Data.Size = len(Binary_node.Data.Data) + Binary_node.Data.HOffset = Fd_Struct[i][1]+Fd_Struct[i][2][0] + offset + WholeFvTree.insertChild(Binary_node) + Binary_count += 1 + Cur_node = BIOSTREE(Fd_Struct[i+1][0]+ str(Fv_count)) + Cur_node.type = Fd_Struct[i+1][0] + Cur_node.Data = FvNode(Fv_count, whole_data[Fd_Struct[i+1][1]:Fd_Struct[i+1][1]+Fd_Struct[i+1][2][0]]) + Cur_node.Data.HOffset = Fd_Struct[i+1][1] + offset + Cur_node.Data.DOffset = Cur_node.Data.HOffset+Cur_node.Data.Header.HeaderLength + Cur_node.Data.Data = whole_data[Fd_Struct[i+1][1]+Cur_node.Data.Header.HeaderLength:Fd_Struct[i+1][1]+Cur_node.Data.Size] + WholeFvTree.insertChild(Cur_node) + Fv_count += 1 + # If the final Fv image is the Binary Fv, add it into the tree + if Fd_Struct[-1][1] + Fd_Struct[-1][2][0] != data_size: + Binary_node = BIOSTREE('BINARY'+ str(Binary_count)) + Binary_node.type = BINARY_DATA + Binary_node.Data = BinaryNode(str(Binary_count)) + Binary_node.Data.Data = whole_data[Fd_Struct[-1][1]+Fd_Struct[-1][2][0]:] + Binary_node.Data.Size = len(Binary_node.Data.Data) + Binary_node.Data.HOffset = Fd_Struct[-1][1]+Fd_Struct[-1][2][0] + offset + WholeFvTree.insertChild(Binary_node) + Binary_count += 1 + + ## Get the first level Fv from Fd file. + def GetFvFromFd(self, whole_data: bytes=b'') -> list: + Fd_Struct = [] + data_size = len(whole_data) + cur_index = 0 + # Get all the EFI_FIRMWARE_FILE_SYSTEM2_GUID_BYTE FV image offset and length. + while cur_index < data_size: + if EFI_FIRMWARE_FILE_SYSTEM2_GUID_BYTE in whole_data[cur_index:]: + target_index = whole_data[cur_index:].index(EFI_FIRMWARE_FILE_SYSTEM2_GUID_BYTE) + cur_index + if whole_data[target_index+24:target_index+28] == FVH_SIGNATURE: + Fd_Struct.append([FV_TREE, target_index - 16, unpack("Q", whole_data[target_index+16:target_index+24])]) + cur_index = Fd_Struct[-1][1] + Fd_Struct[-1][2][0] + else: + cur_index = target_index + 16 + else: + cur_index = data_size + cur_index = 0 + # Get all the EFI_FIRMWARE_FILE_SYSTEM3_GUID_BYTE FV image offset and length. + while cur_index < data_size: + if EFI_FIRMWARE_FILE_SYSTEM3_GUID_BYTE in whole_data[cur_index:]: + target_index = whole_data[cur_index:].index(EFI_FIRMWARE_FILE_SYSTEM3_GUID_BYTE) + cur_index + if whole_data[target_index+24:target_index+28] == FVH_SIGNATURE: + Fd_Struct.append([FV_TREE, target_index - 16, unpack("Q", whole_data[target_index+16:target_index+24])]) + cur_index = Fd_Struct[-1][1] + Fd_Struct[-1][2][0] + else: + cur_index = target_index + 16 + else: + cur_index = data_size + cur_index = 0 + # Get all the EFI_SYSTEM_NVDATA_FV_GUID_BYTE FV image offset and length. + while cur_index < data_size: + if EFI_SYSTEM_NVDATA_FV_GUID_BYTE in whole_data[cur_index:]: + target_index = whole_data[cur_index:].index(EFI_SYSTEM_NVDATA_FV_GUID_BYTE) + cur_index + if whole_data[target_index+24:target_index+28] == FVH_SIGNATURE: + Fd_Struct.append([DATA_FV_TREE, target_index - 16, unpack("Q", whole_data[target_index+16:target_index+24])]) + cur_index = Fd_Struct[-1][1] + Fd_Struct[-1][2][0] + else: + cur_index = target_index + 16 + else: + cur_index = data_size + # Sort all the collect Fv image with offset. + Fd_Struct.sort(key=lambda x:x[1]) + tmp_struct = copy.deepcopy(Fd_Struct) + tmp_index = 0 + Fv_num = len(Fd_Struct) + # Remove the Fv image included in another Fv image. + for i in range(1,Fv_num): + if tmp_struct[i][1]+tmp_struct[i][2][0] < tmp_struct[i-1][1]+tmp_struct[i-1][2][0]: + Fd_Struct.remove(Fd_Struct[i-tmp_index]) + tmp_index += 1 + return Fd_Struct + +class ParserEntry(): + FactoryTable:dict = { + SECTION_TREE: SectionFactory, + ROOT_SECTION_TREE: FfsFactory, + FFS_TREE: FfsFactory, + ROOT_FFS_TREE: FvFactory, + FV_TREE: FvFactory, + SEC_FV_TREE: FvFactory, + ROOT_FV_TREE: FdFactory, + ROOT_TREE: FdFactory, + } + + def GetTargetFactory(self, Tree_type: str) -> BinaryFactory: + if Tree_type in self.FactoryTable: + return self.FactoryTable[Tree_type] + + def Generate_Product(self, TargetFactory: BinaryFactory, Tree, Data: bytes, Offset: int) -> None: + New_Product = TargetFactory.Create_Product() + New_Product.ParserData(Tree, Data, Offset) + + def DataParser(self, Tree, Data: bytes, Offset: int) -> None: + TargetFactory = self.GetTargetFactory(Tree.type) + if TargetFactory: + self.Generate_Product(TargetFactory, Tree, Data, Offset) \ No newline at end of file diff --git a/BaseTools/Source/Python/FMMT/core/BiosTree.py b/BaseTools/Source/Python/FMMT/core/BiosTree.py new file mode 100644 index 0000000000..d8fa474335 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/core/BiosTree.py @@ -0,0 +1,198 @@ +## @file +# This file is used to define the Bios layout tree structure and related operations. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +import collections +from FirmwareStorageFormat.Common import * +from utils.FmmtLogger import FmmtLogger as logger + +ROOT_TREE = 'ROOT' +ROOT_FV_TREE = 'ROOT_FV_TREE' +ROOT_FFS_TREE = 'ROOT_FFS_TREE' +ROOT_SECTION_TREE = 'ROOT_SECTION_TREE' + +FV_TREE = 'FV' +DATA_FV_TREE = 'DATA_FV' +FFS_TREE = 'FFS' +FFS_PAD = 'FFS_PAD' +FFS_FREE_SPACE = 'FFS_FREE_SPACE' +SECTION_TREE = 'SECTION' +SEC_FV_TREE = 'SEC_FV_IMAGE' +BINARY_DATA = 'BINARY' + +RootType = [ROOT_TREE, ROOT_FV_TREE, ROOT_FFS_TREE, ROOT_SECTION_TREE] +FvType = [FV_TREE, SEC_FV_TREE] +FfsType = FFS_TREE +SecType = SECTION_TREE + +class BIOSTREE: + def __init__(self, NodeName: str) -> None: + self.key = NodeName + self.type = None + self.Data = None + self.Child = [] + self.Findlist = [] + self.Parent = None + self.NextRel = None + self.LastRel = None + + def HasChild(self) -> bool: + if self.Child == []: + return False + else: + return True + + def isFinalChild(self) -> bool: + ParTree = self.Parent + if ParTree: + if ParTree.Child[-1] == self: + return True + return False + + # FvTree.insertChild() + def insertChild(self, newNode, pos: int=None) -> None: + if len(self.Child) == 0: + self.Child.append(newNode) + else: + if not pos: + LastTree = self.Child[-1] + self.Child.append(newNode) + LastTree.NextRel = newNode + newNode.LastRel = LastTree + else: + newNode.NextRel = self.Child[pos-1].NextRel + newNode.LastRel = self.Child[pos].LastRel + self.Child[pos-1].NextRel = newNode + self.Child[pos].LastRel = newNode + self.Child.insert(pos, newNode) + newNode.Parent = self + + # lastNode.insertRel(newNode) + def insertRel(self, newNode) -> None: + if self.Parent: + parentTree = self.Parent + new_index = parentTree.Child.index(self) + 1 + parentTree.Child.insert(new_index, newNode) + self.NextRel = newNode + newNode.LastRel = self + + def deleteNode(self, deletekey: str) -> None: + FindStatus, DeleteTree = self.FindNode(deletekey) + if FindStatus: + parentTree = DeleteTree.Parent + lastTree = DeleteTree.LastRel + nextTree = DeleteTree.NextRel + if parentTree: + index = parentTree.Child.index(DeleteTree) + del parentTree.Child[index] + if lastTree and nextTree: + lastTree.NextRel = nextTree + nextTree.LastRel = lastTree + elif lastTree: + lastTree.NextRel = None + elif nextTree: + nextTree.LastRel = None + return DeleteTree + else: + logger.error('Could not find the target tree') + return None + + def FindNode(self, key: str, Findlist: list) -> None: + if self.key == key or (self.Data and self.Data.Name == key) or (self.type == FFS_TREE and self.Data.UiName == key): + Findlist.append(self) + for item in self.Child: + item.FindNode(key, Findlist) + + def GetTreePath(self): + BiosTreePath = [self] + while self.Parent: + BiosTreePath.insert(0, self.Parent) + self = self.Parent + return BiosTreePath + + def parserTree(self, TargetDict: dict=None, Info: list=None, space: int=0, ParFvId="") -> None: + Key = list(TargetDict.keys())[0] + if TargetDict[Key]["Type"] in RootType: + Info.append("Image File: {}".format(Key)) + Info.append("FilesNum: {}".format(TargetDict.get(Key).get('FilesNum'))) + Info.append("\n") + elif TargetDict[Key]["Type"] in FvType: + space += 2 + if TargetDict[Key]["Type"] == SEC_FV_TREE: + Info.append("{}Child FV named {} of {}".format(space*" ", Key, ParFvId)) + space += 2 + else: + Info.append("FvId: {}".format(Key)) + ParFvId = Key + Info.append("{}FvNameGuid: {}".format(space*" ", TargetDict.get(Key).get('FvNameGuid'))) + Info.append("{}Attributes: {}".format(space*" ", TargetDict.get(Key).get('Attributes'))) + Info.append("{}Total Volume Size: {}".format(space*" ", TargetDict.get(Key).get('Size'))) + Info.append("{}Free Volume Size: {}".format(space*" ", TargetDict.get(Key).get('FreeSize'))) + Info.append("{}Volume Offset: {}".format(space*" ", TargetDict.get(Key).get('Offset'))) + Info.append("{}FilesNum: {}".format(space*" ", TargetDict.get(Key).get('FilesNum'))) + elif TargetDict[Key]["Type"] in FfsType: + space += 2 + if TargetDict.get(Key).get('UiName') != "b''": + Info.append("{}File: {} / {}".format(space*" ", Key, TargetDict.get(Key).get('UiName'))) + else: + Info.append("{}File: {}".format(space*" ", Key)) + if "Files" in list(TargetDict[Key].keys()): + for item in TargetDict[Key]["Files"]: + self.parserTree(item, Info, space, ParFvId) + + def ExportTree(self,TreeInfo: dict=None) -> dict: + if TreeInfo is None: + TreeInfo =collections.OrderedDict() + + if self.type == ROOT_TREE or self.type == ROOT_FV_TREE or self.type == ROOT_FFS_TREE or self.type == ROOT_SECTION_TREE: + key = str(self.key) + TreeInfo[self.key] = collections.OrderedDict() + TreeInfo[self.key]["Name"] = key + TreeInfo[self.key]["Type"] = self.type + TreeInfo[self.key]["FilesNum"] = len(self.Child) + elif self.type == FV_TREE or self.type == SEC_FV_TREE: + key = str(self.Data.FvId) + TreeInfo[key] = collections.OrderedDict() + TreeInfo[key]["Name"] = key + if self.Data.FvId != self.Data.Name: + TreeInfo[key]["FvNameGuid"] = str(self.Data.Name) + TreeInfo[key]["Type"] = self.type + TreeInfo[key]["Attributes"] = hex(self.Data.Header.Attributes) + TreeInfo[key]["Size"] = hex(self.Data.Header.FvLength) + TreeInfo[key]["FreeSize"] = hex(self.Data.Free_Space) + TreeInfo[key]["Offset"] = hex(self.Data.HOffset) + TreeInfo[key]["FilesNum"] = len(self.Child) + elif self.type == FFS_TREE: + key = str(self.Data.Name) + TreeInfo[key] = collections.OrderedDict() + TreeInfo[key]["Name"] = key + TreeInfo[key]["UiName"] = '{}'.format(self.Data.UiName) + TreeInfo[key]["Version"] = '{}'.format(self.Data.Version) + TreeInfo[key]["Type"] = self.type + TreeInfo[key]["Size"] = hex(self.Data.Size) + TreeInfo[key]["Offset"] = hex(self.Data.HOffset) + TreeInfo[key]["FilesNum"] = len(self.Child) + elif self.type == SECTION_TREE and self.Data.Type == 0x02: + key = str(self.Data.Name) + TreeInfo[key] = collections.OrderedDict() + TreeInfo[key]["Name"] = key + TreeInfo[key]["Type"] = self.type + TreeInfo[key]["Size"] = hex(len(self.Data.OriData) + self.Data.HeaderLength) + TreeInfo[key]["DecompressedSize"] = hex(self.Data.Size) + TreeInfo[key]["Offset"] = hex(self.Data.HOffset) + TreeInfo[key]["FilesNum"] = len(self.Child) + elif self is not None: + key = str(self.Data.Name) + TreeInfo[key] = collections.OrderedDict() + TreeInfo[key]["Name"] = key + TreeInfo[key]["Type"] = self.type + TreeInfo[key]["Size"] = hex(self.Data.Size) + TreeInfo[key]["Offset"] = hex(self.Data.HOffset) + TreeInfo[key]["FilesNum"] = len(self.Child) + + for item in self.Child: + TreeInfo[key].setdefault('Files',[]).append( item.ExportTree()) + + return TreeInfo \ No newline at end of file diff --git a/BaseTools/Source/Python/FMMT/core/BiosTreeNode.py b/BaseTools/Source/Python/FMMT/core/BiosTreeNode.py new file mode 100644 index 0000000000..20447766c8 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/core/BiosTreeNode.py @@ -0,0 +1,194 @@ +## @file +# This file is used to define the BIOS Tree Node. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from FirmwareStorageFormat.FvHeader import * +from FirmwareStorageFormat.FfsFileHeader import * +from FirmwareStorageFormat.SectionHeader import * +from FirmwareStorageFormat.Common import * +from utils.FmmtLogger import FmmtLogger as logger +import uuid + +SectionHeaderType = { + 0x01:'EFI_COMPRESSION_SECTION', + 0x02:'EFI_GUID_DEFINED_SECTION', + 0x03:'EFI_SECTION_DISPOSABLE', + 0x10:'EFI_SECTION_PE32', + 0x11:'EFI_SECTION_PIC', + 0x12:'EFI_SECTION_TE', + 0x13:'EFI_SECTION_DXE_DEPEX', + 0x14:'EFI_SECTION_VERSION', + 0x15:'EFI_SECTION_USER_INTERFACE', + 0x16:'EFI_SECTION_COMPATIBILITY16', + 0x17:'EFI_SECTION_FIRMWARE_VOLUME_IMAGE', + 0x18:'EFI_FREEFORM_SUBTYPE_GUID_SECTION', + 0x19:'EFI_SECTION_RAW', + 0x1B:'EFI_SECTION_PEI_DEPEX', + 0x1C:'EFI_SECTION_MM_DEPEX' +} +HeaderType = [0x01, 0x02, 0x14, 0x15, 0x18] + +class BinaryNode: + def __init__(self, name: str) -> None: + self.Size = 0 + self.Name = "BINARY" + str(name) + self.HOffset = 0 + self.Data = b'' + +class FvNode: + def __init__(self, name, buffer: bytes) -> None: + self.Header = EFI_FIRMWARE_VOLUME_HEADER.from_buffer_copy(buffer) + Map_num = (self.Header.HeaderLength - 56)//8 + self.Header = Refine_FV_Header(Map_num).from_buffer_copy(buffer) + self.FvId = "FV" + str(name) + self.Name = "FV" + str(name) + if self.Header.ExtHeaderOffset: + self.ExtHeader = EFI_FIRMWARE_VOLUME_EXT_HEADER.from_buffer_copy(buffer[self.Header.ExtHeaderOffset:]) + self.Name = uuid.UUID(bytes_le=struct2stream(self.ExtHeader.FvName)) + self.ExtEntryOffset = self.Header.ExtHeaderOffset + 20 + if self.ExtHeader.ExtHeaderSize != 20: + self.ExtEntryExist = 1 + self.ExtEntry = EFI_FIRMWARE_VOLUME_EXT_ENTRY.from_buffer_copy(buffer[self.ExtEntryOffset:]) + self.ExtTypeExist = 1 + if self.ExtEntry.ExtEntryType == 0x01: + nums = (self.ExtEntry.ExtEntrySize - 8) // 16 + self.ExtEntry = Refine_FV_EXT_ENTRY_OEM_TYPE_Header(nums).from_buffer_copy(buffer[self.ExtEntryOffset:]) + elif self.ExtEntry.ExtEntryType == 0x02: + nums = self.ExtEntry.ExtEntrySize - 20 + self.ExtEntry = Refine_FV_EXT_ENTRY_GUID_TYPE_Header(nums).from_buffer_copy(buffer[self.ExtEntryOffset:]) + elif self.ExtEntry.ExtEntryType == 0x03: + self.ExtEntry = EFI_FIRMWARE_VOLUME_EXT_ENTRY_USED_SIZE_TYPE.from_buffer_copy(buffer[self.ExtEntryOffset:]) + else: + self.ExtTypeExist = 0 + else: + self.ExtEntryExist = 0 + self.Size = self.Header.FvLength + self.HeaderLength = self.Header.HeaderLength + self.HOffset = 0 + self.DOffset = 0 + self.ROffset = 0 + self.Data = b'' + if self.Header.Signature != 1213613663: + logger.error('Invalid Fv Header! Fv {} signature {} is not "_FVH".'.format(struct2stream(self.Header), self.Header.Signature)) + raise Exception("Process Failed: Fv Header Signature!") + self.PadData = b'' + self.Free_Space = 0 + self.ModCheckSum() + + def ModCheckSum(self) -> None: + # Fv Header Sums to 0. + Header = struct2stream(self.Header)[::-1] + Size = self.HeaderLength // 2 + Sum = 0 + for i in range(Size): + Sum += int(Header[i*2: i*2 + 2].hex(), 16) + if Sum & 0xffff: + self.Header.Checksum = 0x10000 - (Sum - self.Header.Checksum) % 0x10000 + + def ModFvExt(self) -> None: + # If used space changes and self.ExtEntry.UsedSize exists, self.ExtEntry.UsedSize need to be changed. + if self.Header.ExtHeaderOffset and self.ExtEntryExist and self.ExtTypeExist and self.ExtEntry.Hdr.ExtEntryType == 0x03: + self.ExtEntry.UsedSize = self.Header.FvLength - self.Free_Space + + def ModFvSize(self) -> None: + # If Fv Size changed, self.Header.FvLength and self.Header.BlockMap[i].NumBlocks need to be changed. + BlockMapNum = len(self.Header.BlockMap) + for i in range(BlockMapNum): + if self.Header.BlockMap[i].Length: + self.Header.BlockMap[i].NumBlocks = self.Header.FvLength // self.Header.BlockMap[i].Length + + def ModExtHeaderData(self) -> None: + if self.Header.ExtHeaderOffset: + ExtHeaderData = struct2stream(self.ExtHeader) + ExtHeaderDataOffset = self.Header.ExtHeaderOffset - self.HeaderLength + self.Data = self.Data[:ExtHeaderDataOffset] + ExtHeaderData + self.Data[ExtHeaderDataOffset+20:] + if self.Header.ExtHeaderOffset and self.ExtEntryExist: + ExtHeaderEntryData = struct2stream(self.ExtEntry) + ExtHeaderEntryDataOffset = self.Header.ExtHeaderOffset + 20 - self.HeaderLength + self.Data = self.Data[:ExtHeaderEntryDataOffset] + ExtHeaderEntryData + self.Data[ExtHeaderEntryDataOffset+len(ExtHeaderEntryData):] + +class FfsNode: + def __init__(self, buffer: bytes) -> None: + self.Header = EFI_FFS_FILE_HEADER.from_buffer_copy(buffer) + # self.Attributes = unpack(" None: + HeaderData = struct2stream(self.Header) + HeaderSum = 0 + for item in HeaderData: + HeaderSum += item + HeaderSum -= self.Header.State + HeaderSum -= self.Header.IntegrityCheck.Checksum.File + if HeaderSum & 0xff: + Header = self.Header.IntegrityCheck.Checksum.Header + 0x100 - HeaderSum % 0x100 + self.Header.IntegrityCheck.Checksum.Header = Header % 0x100 + +class SectionNode: + def __init__(self, buffer: bytes) -> None: + if buffer[0:3] != b'\xff\xff\xff': + self.Header = EFI_COMMON_SECTION_HEADER.from_buffer_copy(buffer) + else: + self.Header = EFI_COMMON_SECTION_HEADER2.from_buffer_copy(buffer) + if self.Header.Type in SectionHeaderType: + self.Name = SectionHeaderType[self.Header.Type] + elif self.Header.Type == 0: + self.Name = "EFI_SECTION_ALL" + else: + self.Name = "SECTION" + if self.Header.Type in HeaderType: + self.ExtHeader = self.GetExtHeader(self.Header.Type, buffer[self.Header.Common_Header_Size():], (self.Header.SECTION_SIZE-self.Header.Common_Header_Size())) + self.HeaderLength = self.Header.Common_Header_Size() + self.ExtHeader.ExtHeaderSize() + else: + self.ExtHeader = None + self.HeaderLength = self.Header.Common_Header_Size() + self.Size = self.Header.SECTION_SIZE + self.Type = self.Header.Type + self.HOffset = 0 + self.DOffset = 0 + self.ROffset = 0 + self.Data = b'' + self.OriData = b'' + self.OriHeader = b'' + self.PadData = b'' + self.IsPadSection = False + self.SectionMaxAlignment = SECTION_COMMON_ALIGNMENT # 4-align + + def GetExtHeader(self, Type: int, buffer: bytes, nums: int=0) -> None: + if Type == 0x01: + return EFI_COMPRESSION_SECTION.from_buffer_copy(buffer) + elif Type == 0x02: + return EFI_GUID_DEFINED_SECTION.from_buffer_copy(buffer) + elif Type == 0x14: + return Get_VERSION_Header((nums - 2)//2).from_buffer_copy(buffer) + elif Type == 0x15: + return Get_USER_INTERFACE_Header(nums//2).from_buffer_copy(buffer) + elif Type == 0x18: + return EFI_FREEFORM_SUBTYPE_GUID_SECTION.from_buffer_copy(buffer) + +class FreeSpaceNode: + def __init__(self, buffer: bytes) -> None: + self.Name = 'Free_Space' + self.Data = buffer + self.Size = len(buffer) + self.HOffset = 0 + self.DOffset = 0 + self.ROffset = 0 + self.PadData = b'' \ No newline at end of file diff --git a/BaseTools/Source/Python/FMMT/core/FMMTOperation.py b/BaseTools/Source/Python/FMMT/core/FMMTOperation.py new file mode 100644 index 0000000000..c2cc2e2467 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/core/FMMTOperation.py @@ -0,0 +1,197 @@ +## @file +# This file is used to define the functions to operate bios binary file. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from core.FMMTParser import * +from core.FvHandler import * +from utils.FvLayoutPrint import * +from utils.FmmtLogger import FmmtLogger as logger + +global Fv_count +Fv_count = 0 + +# The ROOT_TYPE can be 'ROOT_TREE', 'ROOT_FV_TREE', 'ROOT_FFS_TREE', 'ROOT_SECTION_TREE' +def ViewFile(inputfile: str, ROOT_TYPE: str, layoutfile: str=None, outputfile: str=None) -> None: + if not os.path.exists(inputfile): + logger.error("Invalid inputfile, can not open {}.".format(inputfile)) + raise Exception("Process Failed: Invalid inputfile!") + # 1. Data Prepare + with open(inputfile, "rb") as f: + whole_data = f.read() + FmmtParser = FMMTParser(inputfile, ROOT_TYPE) + # 2. DataTree Create + logger.debug('Parsing inputfile data......') + FmmtParser.ParserFromRoot(FmmtParser.WholeFvTree, whole_data) + logger.debug('Done!') + # 3. Log Output + InfoDict = FmmtParser.WholeFvTree.ExportTree() + logger.debug('BinaryTree created, start parsing BinaryTree data......') + FmmtParser.WholeFvTree.parserTree(InfoDict, FmmtParser.BinaryInfo) + logger.debug('Done!') + GetFormatter("").LogPrint(FmmtParser.BinaryInfo) + if layoutfile: + if os.path.splitext(layoutfile)[1]: + layoutfilename = layoutfile + layoutfileformat = os.path.splitext(layoutfile)[1][1:].lower() + else: + layoutfilename = "Layout_{}{}".format(os.path.basename(inputfile),".{}".format(layoutfile.lower())) + layoutfileformat = layoutfile.lower() + GetFormatter(layoutfileformat).dump(InfoDict, FmmtParser.BinaryInfo, layoutfilename) + # 4. Data Encapsulation + if outputfile: + logger.debug('Start encapsulating data......') + FmmtParser.Encapsulation(FmmtParser.WholeFvTree, False) + with open(outputfile, "wb") as f: + f.write(FmmtParser.FinalData) + logger.debug('Encapsulated data is saved in {}.'.format(outputfile)) + +def DeleteFfs(inputfile: str, TargetFfs_name: str, outputfile: str, Fv_name: str=None) -> None: + if not os.path.exists(inputfile): + logger.error("Invalid inputfile, can not open {}.".format(inputfile)) + raise Exception("Process Failed: Invalid inputfile!") + # 1. Data Prepare + with open(inputfile, "rb") as f: + whole_data = f.read() + FmmtParser = FMMTParser(inputfile, ROOT_TREE) + # 2. DataTree Create + logger.debug('Parsing inputfile data......') + FmmtParser.ParserFromRoot(FmmtParser.WholeFvTree, whole_data) + logger.debug('Done!') + # 3. Data Modify + FmmtParser.WholeFvTree.FindNode(TargetFfs_name, FmmtParser.WholeFvTree.Findlist) + # Choose the Specfic DeleteFfs with Fv info + if Fv_name: + for item in FmmtParser.WholeFvTree.Findlist: + if item.Parent.key != Fv_name and item.Parent.Data.Name != Fv_name: + FmmtParser.WholeFvTree.Findlist.remove(item) + Status = False + if FmmtParser.WholeFvTree.Findlist != []: + for Delete_Ffs in FmmtParser.WholeFvTree.Findlist: + FfsMod = FvHandler(None, Delete_Ffs) + Status = FfsMod.DeleteFfs() + else: + logger.error('Target Ffs not found!!!') + # 4. Data Encapsulation + if Status: + logger.debug('Start encapsulating data......') + FmmtParser.Encapsulation(FmmtParser.WholeFvTree, False) + with open(outputfile, "wb") as f: + f.write(FmmtParser.FinalData) + logger.debug('Encapsulated data is saved in {}.'.format(outputfile)) + +def AddNewFfs(inputfile: str, Fv_name: str, newffsfile: str, outputfile: str) -> None: + if not os.path.exists(inputfile): + logger.error("Invalid inputfile, can not open {}.".format(inputfile)) + raise Exception("Process Failed: Invalid inputfile!") + if not os.path.exists(newffsfile): + logger.error("Invalid ffsfile, can not open {}.".format(newffsfile)) + raise Exception("Process Failed: Invalid ffs file!") + # 1. Data Prepare + with open(inputfile, "rb") as f: + whole_data = f.read() + FmmtParser = FMMTParser(inputfile, ROOT_TREE) + # 2. DataTree Create + logger.debug('Parsing inputfile data......') + FmmtParser.ParserFromRoot(FmmtParser.WholeFvTree, whole_data) + logger.debug('Done!') + # Get Target Fv and Target Ffs_Pad + FmmtParser.WholeFvTree.FindNode(Fv_name, FmmtParser.WholeFvTree.Findlist) + # Create new ffs Tree + with open(newffsfile, "rb") as f: + new_ffs_data = f.read() + NewFmmtParser = FMMTParser(newffsfile, ROOT_FFS_TREE) + Status = False + # 3. Data Modify + if FmmtParser.WholeFvTree.Findlist: + for TargetFv in FmmtParser.WholeFvTree.Findlist: + TargetFfsPad = TargetFv.Child[-1] + logger.debug('Parsing newffsfile data......') + if TargetFfsPad.type == FFS_FREE_SPACE: + NewFmmtParser.ParserFromRoot(NewFmmtParser.WholeFvTree, new_ffs_data, TargetFfsPad.Data.HOffset) + else: + NewFmmtParser.ParserFromRoot(NewFmmtParser.WholeFvTree, new_ffs_data, TargetFfsPad.Data.HOffset+TargetFfsPad.Data.Size) + logger.debug('Done!') + FfsMod = FvHandler(NewFmmtParser.WholeFvTree.Child[0], TargetFfsPad) + Status = FfsMod.AddFfs() + else: + logger.error('Target Fv not found!!!') + # 4. Data Encapsulation + if Status: + logger.debug('Start encapsulating data......') + FmmtParser.Encapsulation(FmmtParser.WholeFvTree, False) + with open(outputfile, "wb") as f: + f.write(FmmtParser.FinalData) + logger.debug('Encapsulated data is saved in {}.'.format(outputfile)) + +def ReplaceFfs(inputfile: str, Ffs_name: str, newffsfile: str, outputfile: str, Fv_name: str=None) -> None: + if not os.path.exists(inputfile): + logger.error("Invalid inputfile, can not open {}.".format(inputfile)) + raise Exception("Process Failed: Invalid inputfile!") + # 1. Data Prepare + with open(inputfile, "rb") as f: + whole_data = f.read() + FmmtParser = FMMTParser(inputfile, ROOT_TREE) + # 2. DataTree Create + logger.debug('Parsing inputfile data......') + FmmtParser.ParserFromRoot(FmmtParser.WholeFvTree, whole_data) + logger.debug('Done!') + with open(newffsfile, "rb") as f: + new_ffs_data = f.read() + newFmmtParser = FMMTParser(newffsfile, FV_TREE) + logger.debug('Parsing newffsfile data......') + newFmmtParser.ParserFromRoot(newFmmtParser.WholeFvTree, new_ffs_data) + logger.debug('Done!') + Status = False + # 3. Data Modify + new_ffs = newFmmtParser.WholeFvTree.Child[0] + new_ffs.Data.PadData = GetPadSize(new_ffs.Data.Size, FFS_COMMON_ALIGNMENT) * b'\xff' + FmmtParser.WholeFvTree.FindNode(Ffs_name, FmmtParser.WholeFvTree.Findlist) + if Fv_name: + for item in FmmtParser.WholeFvTree.Findlist: + if item.Parent.key != Fv_name and item.Parent.Data.Name != Fv_name: + FmmtParser.WholeFvTree.Findlist.remove(item) + if FmmtParser.WholeFvTree.Findlist != []: + for TargetFfs in FmmtParser.WholeFvTree.Findlist: + FfsMod = FvHandler(newFmmtParser.WholeFvTree.Child[0], TargetFfs) + Status = FfsMod.ReplaceFfs() + else: + logger.error('Target Ffs not found!!!') + # 4. Data Encapsulation + if Status: + logger.debug('Start encapsulating data......') + FmmtParser.Encapsulation(FmmtParser.WholeFvTree, False) + with open(outputfile, "wb") as f: + f.write(FmmtParser.FinalData) + logger.debug('Encapsulated data is saved in {}.'.format(outputfile)) + +def ExtractFfs(inputfile: str, Ffs_name: str, outputfile: str, Fv_name: str=None) -> None: + if not os.path.exists(inputfile): + logger.error("Invalid inputfile, can not open {}.".format(inputfile)) + raise Exception("Process Failed: Invalid inputfile!") + # 1. Data Prepare + with open(inputfile, "rb") as f: + whole_data = f.read() + FmmtParser = FMMTParser(inputfile, ROOT_TREE) + # 2. DataTree Create + logger.debug('Parsing inputfile data......') + FmmtParser.ParserFromRoot(FmmtParser.WholeFvTree, whole_data) + logger.debug('Done!') + FmmtParser.WholeFvTree.FindNode(Ffs_name, FmmtParser.WholeFvTree.Findlist) + if Fv_name: + for item in FmmtParser.WholeFvTree.Findlist: + if item.Parent.key != Fv_name and item.Parent.Data.Name != Fv_name: + FmmtParser.WholeFvTree.Findlist.remove(item) + if FmmtParser.WholeFvTree.Findlist != []: + TargetNode = FmmtParser.WholeFvTree.Findlist[0] + TargetFv = TargetNode.Parent + if TargetFv.Data.Header.Attributes & EFI_FVB2_ERASE_POLARITY: + TargetNode.Data.Header.State = c_uint8( + ~TargetNode.Data.Header.State) + FinalData = struct2stream(TargetNode.Data.Header) + TargetNode.Data.Data + with open(outputfile, "wb") as f: + f.write(FinalData) + logger.debug('Extract ffs data is saved in {}.'.format(outputfile)) + else: + logger.error('Target Ffs not found!!!') diff --git a/BaseTools/Source/Python/FMMT/core/FMMTParser.py b/BaseTools/Source/Python/FMMT/core/FMMTParser.py new file mode 100644 index 0000000000..e76ac51185 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/core/FMMTParser.py @@ -0,0 +1,87 @@ +## @file +# This file is used to define the interface of Bios Parser. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from FirmwareStorageFormat.Common import * +from core.BinaryFactoryProduct import ParserEntry +from core.BiosTreeNode import * +from core.BiosTree import * +from core.GuidTools import * +from utils.FmmtLogger import FmmtLogger as logger + +class FMMTParser: + def __init__(self, name: str, TYPE: str) -> None: + self.WholeFvTree = BIOSTREE(name) + self.WholeFvTree.type = TYPE + self.FinalData = b'' + self.BinaryInfo = [] + + ## Parser the nodes in WholeTree. + def ParserFromRoot(self, WholeFvTree=None, whole_data: bytes=b'', Reloffset: int=0) -> None: + if WholeFvTree.type == ROOT_TREE or WholeFvTree.type == ROOT_FV_TREE: + ParserEntry().DataParser(self.WholeFvTree, whole_data, Reloffset) + else: + ParserEntry().DataParser(WholeFvTree, whole_data, Reloffset) + for Child in WholeFvTree.Child: + self.ParserFromRoot(Child, "") + + ## Encapuslation all the data in tree into self.FinalData + def Encapsulation(self, rootTree, CompressStatus: bool) -> None: + # If current node is Root node, skip it. + if rootTree.type == ROOT_TREE or rootTree.type == ROOT_FV_TREE or rootTree.type == ROOT_FFS_TREE or rootTree.type == ROOT_SECTION_TREE: + logger.debug('Encapsulated successfully!') + # If current node do not have Header, just add Data. + elif rootTree.type == BINARY_DATA or rootTree.type == FFS_FREE_SPACE: + self.FinalData += rootTree.Data.Data + rootTree.Child = [] + # If current node do not have Child and ExtHeader, just add its Header and Data. + elif rootTree.type == DATA_FV_TREE or rootTree.type == FFS_PAD: + self.FinalData += struct2stream(rootTree.Data.Header) + rootTree.Data.Data + rootTree.Data.PadData + if rootTree.isFinalChild(): + ParTree = rootTree.Parent + if ParTree.type != 'ROOT': + self.FinalData += ParTree.Data.PadData + rootTree.Child = [] + # If current node is not Section node and may have Child and ExtHeader, add its Header,ExtHeader. If do not have Child, add its Data. + elif rootTree.type == FV_TREE or rootTree.type == FFS_TREE or rootTree.type == SEC_FV_TREE: + if rootTree.HasChild(): + self.FinalData += struct2stream(rootTree.Data.Header) + else: + self.FinalData += struct2stream(rootTree.Data.Header) + rootTree.Data.Data + rootTree.Data.PadData + if rootTree.isFinalChild(): + ParTree = rootTree.Parent + if ParTree.type != 'ROOT': + self.FinalData += ParTree.Data.PadData + # If current node is Section, need to consider its ExtHeader, Child and Compressed Status. + elif rootTree.type == SECTION_TREE: + # Not compressed section + if rootTree.Data.OriData == b'' or (rootTree.Data.OriData != b'' and CompressStatus): + if rootTree.HasChild(): + if rootTree.Data.ExtHeader: + self.FinalData += struct2stream(rootTree.Data.Header) + struct2stream(rootTree.Data.ExtHeader) + else: + self.FinalData += struct2stream(rootTree.Data.Header) + else: + Data = rootTree.Data.Data + if rootTree.Data.ExtHeader: + self.FinalData += struct2stream(rootTree.Data.Header) + struct2stream(rootTree.Data.ExtHeader) + Data + rootTree.Data.PadData + else: + self.FinalData += struct2stream(rootTree.Data.Header) + Data + rootTree.Data.PadData + if rootTree.isFinalChild(): + ParTree = rootTree.Parent + self.FinalData += ParTree.Data.PadData + # If compressed section + else: + Data = rootTree.Data.OriData + rootTree.Child = [] + if rootTree.Data.ExtHeader: + self.FinalData += struct2stream(rootTree.Data.Header) + struct2stream(rootTree.Data.ExtHeader) + Data + rootTree.Data.PadData + else: + self.FinalData += struct2stream(rootTree.Data.Header) + Data + rootTree.Data.PadData + if rootTree.isFinalChild(): + ParTree = rootTree.Parent + self.FinalData += ParTree.Data.PadData + for Child in rootTree.Child: + self.Encapsulation(Child, CompressStatus) diff --git a/BaseTools/Source/Python/FMMT/core/FvHandler.py b/BaseTools/Source/Python/FMMT/core/FvHandler.py new file mode 100644 index 0000000000..c81541ec18 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/core/FvHandler.py @@ -0,0 +1,641 @@ +## @file +# This file is used to the implementation of Bios layout handler. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +import os +from core.BiosTree import * +from core.GuidTools import GUIDTools +from core.BiosTreeNode import * +from FirmwareStorageFormat.Common import * +from utils.FmmtLogger import FmmtLogger as logger + +EFI_FVB2_ERASE_POLARITY = 0x00000800 + +def ChangeSize(TargetTree, size_delta: int=0) -> None: + # If Size increase delta, then should be: size_delta = -delta + if type(TargetTree.Data.Header) == type(EFI_FFS_FILE_HEADER2()) or type(TargetTree.Data.Header) == type(EFI_COMMON_SECTION_HEADER2()): + TargetTree.Data.Size -= size_delta + TargetTree.Data.Header.ExtendedSize -= size_delta + elif TargetTree.type == SECTION_TREE and TargetTree.Data.OriData: + OriSize = TargetTree.Data.Header.SECTION_SIZE + OriSize -= size_delta + TargetTree.Data.Header.Size[0] = OriSize % (16**2) + TargetTree.Data.Header.Size[1] = OriSize % (16**4) //(16**2) + TargetTree.Data.Header.Size[2] = OriSize // (16**4) + else: + TargetTree.Data.Size -= size_delta + TargetTree.Data.Header.Size[0] = TargetTree.Data.Size % (16**2) + TargetTree.Data.Header.Size[1] = TargetTree.Data.Size % (16**4) //(16**2) + TargetTree.Data.Header.Size[2] = TargetTree.Data.Size // (16**4) + +def ModifyFfsType(TargetFfs) -> None: + if type(TargetFfs.Data.Header) == type(EFI_FFS_FILE_HEADER()) and TargetFfs.Data.Size > 0xFFFFFF: + ExtendSize = TargetFfs.Data.Header.FFS_FILE_SIZE + 8 + New_Header = EFI_FFS_FILE_HEADER2() + New_Header.Name = TargetFfs.Data.Header.Name + New_Header.IntegrityCheck = TargetFfs.Data.Header.IntegrityCheck + New_Header.Type = TargetFfs.Data.Header.Type + New_Header.Attributes = TargetFfs.Data.Header.Attributes | 0x01 # set the Attribute with FFS_ATTRIB_LARGE_FILE (0x01) + NewSize = 0 + New_Header.Size[0] = NewSize % (16**2) # minus the delta size of Header + New_Header.Size[1] = NewSize % (16**4) //(16**2) + New_Header.Size[2] = NewSize // (16**4) + New_Header.State = TargetFfs.Data.Header.State + New_Header.ExtendedSize = ExtendSize + TargetFfs.Data.Header = New_Header + TargetFfs.Data.Size = TargetFfs.Data.Header.FFS_FILE_SIZE + TargetFfs.Data.HeaderLength = TargetFfs.Data.Header.HeaderLength + TargetFfs.Data.ModCheckSum() + elif type(TargetFfs.Data.Header) == type(EFI_FFS_FILE_HEADER2()) and TargetFfs.Data.Size <= 0xFFFFFF: + New_Header = EFI_FFS_FILE_HEADER() + New_Header.Name = TargetFfs.Data.Header.Name + New_Header.IntegrityCheck = TargetFfs.Data.Header.IntegrityCheck + New_Header.Type = TargetFfs.Data.Header.Type + New_Header.Attributes = TargetFfs.Data.Header.Attributes - 1 # remove the FFS_ATTRIB_LARGE_FILE (0x01) from Attribute + New_Header.Size[0] = (TargetFfs.Data.Size - 8) % (16**2) # minus the delta size of Header + New_Header.Size[1] = (TargetFfs.Data.Size - 8) % (16**4) //(16**2) + New_Header.Size[2] = (TargetFfs.Data.Size - 8) // (16**4) + New_Header.State = TargetFfs.Data.Header.State + TargetFfs.Data.Header = New_Header + TargetFfs.Data.Size = TargetFfs.Data.Header.FFS_FILE_SIZE + TargetFfs.Data.HeaderLength = TargetFfs.Data.Header.HeaderLength + TargetFfs.Data.ModCheckSum() + if struct2stream(TargetFfs.Parent.Data.Header.FileSystemGuid) == EFI_FIRMWARE_FILE_SYSTEM3_GUID_BYTE: + NeedChange = True + for item in TargetFfs.Parent.Child: + if type(item.Data.Header) == type(EFI_FFS_FILE_HEADER2()): + NeedChange = False + if NeedChange: + TargetFfs.Parent.Data.Header.FileSystemGuid = ModifyGuidFormat("8c8ce578-8a3d-4f1c-9935-896185c32dd3") + + if type(TargetFfs.Data.Header) == type(EFI_FFS_FILE_HEADER2()): + TarParent = TargetFfs.Parent + while TarParent: + if TarParent.type == FV_TREE and struct2stream(TarParent.Data.Header.FileSystemGuid) == EFI_FIRMWARE_FILE_SYSTEM2_GUID_BYTE: + TarParent.Data.Header.FileSystemGuid = ModifyGuidFormat("5473C07A-3DCB-4dca-BD6F-1E9689E7349A") + TarParent = TarParent.Parent + +def PadSectionModify(PadSection, Offset) -> None: + # Offset > 0, Size decrease; Offset < 0, Size increase; + ChangeSize(PadSection, Offset) + PadSection.Data.Data = (PadSection.Data.Size - PadSection.Data.HeaderLength) * b'\xff' + +def ModifySectionType(TargetSection) -> None: + # If Section Size is increased larger than 0xFFFFFF, need modify Section Header from EFI_COMMON_SECTION_HEADER to EFI_COMMON_SECTION_HEADER2. + if type(TargetSection.Data.Header) == type(EFI_COMMON_SECTION_HEADER()) and TargetSection.Data.Size >= 0xFFFFFF: + New_Header = EFI_COMMON_SECTION_HEADER2() + New_Header.Type = TargetSection.Data.Header.Type + NewSize = 0xFFFFFF + New_Header.Size[0] = NewSize % (16**2) # minus the delta size of Header + New_Header.Size[1] = NewSize % (16**4) //(16**2) + New_Header.Size[2] = NewSize // (16**4) + New_Header.ExtendedSize = TargetSection.Data.Size + 4 + TargetSection.Data.Header = New_Header + TargetSection.Data.Size = TargetSection.Data.Header.SECTION_SIZE + # Align the Header's added 4 bit to 8-alignment to promise the following Ffs's align correctly. + if TargetSection.LastRel.Data.IsPadSection: + PadSectionModify(TargetSection.LastRel, -4) + else: + SecParent = TargetSection.Parent + Target_index = SecParent.Child.index(TargetSection) + NewPadSection = SectionNode(b'\x00\x00\x00\x19') + SecParent.insertChild(NewPadSection, Target_index) + # If Section Size is decreased smaller than 0xFFFFFF, need modify Section Header from EFI_COMMON_SECTION_HEADER2 to EFI_COMMON_SECTION_HEADER. + elif type(TargetSection.Data.Header) == type(EFI_COMMON_SECTION_HEADER2()) and TargetSection.Data.Size < 0xFFFFFF: + New_Header = EFI_COMMON_SECTION_HEADER() + New_Header.Type = TargetSection.Data.Header.Type + New_Header.Size[0] = (TargetSection.Data.Size - 4) % (16**2) # minus the delta size of Header + New_Header.Size[1] = (TargetSection.Data.Size - 4) % (16**4) //(16**2) + New_Header.Size[2] = (TargetSection.Data.Size - 4) // (16**4) + TargetSection.Data.Header = New_Header + TargetSection.Data.Size = TargetSection.Data.Header.SECTION_SIZE + # Align the Header's added 4 bit to 8-alignment to promise the following Ffs's align correctly. + if TargetSection.LastRel.Data.IsPadSection: + PadSectionModify(TargetSection.LastRel, -4) + else: + SecParent = TargetSection.Parent + Target_index = SecParent.Child.index(TargetSection) + NewPadSection = SectionNode(b'\x00\x00\x00\x19') + SecParent.insertChild(NewPadSection, Target_index) + +def ModifyFvExtData(TreeNode) -> None: + FvExtData = b'' + if TreeNode.Data.Header.ExtHeaderOffset: + FvExtHeader = struct2stream(TreeNode.Data.ExtHeader) + FvExtData += FvExtHeader + if TreeNode.Data.Header.ExtHeaderOffset and TreeNode.Data.ExtEntryExist: + FvExtEntry = struct2stream(TreeNode.Data.ExtEntry) + FvExtData += FvExtEntry + if FvExtData: + InfoNode = TreeNode.Child[0] + InfoNode.Data.Data = FvExtData + InfoNode.Data.Data[TreeNode.Data.ExtHeader.ExtHeaderSize:] + InfoNode.Data.ModCheckSum() + +def ModifyFvSystemGuid(TargetFv) -> None: + if struct2stream(TargetFv.Data.Header.FileSystemGuid) == EFI_FIRMWARE_FILE_SYSTEM2_GUID_BYTE: + TargetFv.Data.Header.FileSystemGuid = ModifyGuidFormat("5473C07A-3DCB-4dca-BD6F-1E9689E7349A") + TargetFv.Data.ModCheckSum() + TargetFv.Data.Data = b'' + for item in TargetFv.Child: + if item.type == FFS_FREE_SPACE: + TargetFv.Data.Data += item.Data.Data + item.Data.PadData + else: + TargetFv.Data.Data += struct2stream(item.Data.Header)+ item.Data.Data + item.Data.PadData + +class FvHandler: + def __init__(self, NewFfs, TargetFfs) -> None: + self.NewFfs = NewFfs + self.TargetFfs = TargetFfs + self.Status = False + self.Remain_New_Free_Space = 0 + + ## Use for Compress the Section Data + def CompressData(self, TargetTree) -> None: + TreePath = TargetTree.GetTreePath() + pos = len(TreePath) + self.Status = False + while pos: + if not self.Status: + if TreePath[pos-1].type == SECTION_TREE and TreePath[pos-1].Data.Type == 0x02: + self.CompressSectionData(TreePath[pos-1], None, TreePath[pos-1].Data.ExtHeader.SectionDefinitionGuid) + else: + if pos == len(TreePath): + self.CompressSectionData(TreePath[pos-1], pos) + else: + self.CompressSectionData(TreePath[pos-1], None) + pos -= 1 + + def CompressSectionData(self, TargetTree, pos: int, GuidTool=None) -> None: + NewData = b'' + temp_save_child = TargetTree.Child + if TargetTree.Data: + # Update current node data as adding all the header and data of its child node. + for item in temp_save_child: + if item.type == SECTION_TREE and not item.Data.OriData and item.Data.ExtHeader: + NewData += struct2stream(item.Data.Header) + struct2stream(item.Data.ExtHeader) + item.Data.Data + item.Data.PadData + elif item.type == SECTION_TREE and item.Data.OriData and not item.Data.ExtHeader: + NewData += struct2stream(item.Data.Header) + item.Data.OriData + item.Data.PadData + elif item.type == SECTION_TREE and item.Data.OriData and item.Data.ExtHeader: + NewData += struct2stream(item.Data.Header) + struct2stream(item.Data.ExtHeader) + item.Data.OriData + item.Data.PadData + elif item.type == FFS_FREE_SPACE: + NewData += item.Data.Data + item.Data.PadData + else: + NewData += struct2stream(item.Data.Header) + item.Data.Data + item.Data.PadData + # If node is FFS_TREE, update Pad data and Header info. + # Remain_New_Free_Space is used for move more free space into lst level Fv. + if TargetTree.type == FFS_TREE: + New_Pad_Size = GetPadSize(len(NewData), 8) + Size_delta = len(NewData) - len(TargetTree.Data.Data) + ChangeSize(TargetTree, -Size_delta) + Delta_Pad_Size = len(TargetTree.Data.PadData) - New_Pad_Size + self.Remain_New_Free_Space += Delta_Pad_Size + TargetTree.Data.PadData = b'\xff' * New_Pad_Size + TargetTree.Data.ModCheckSum() + # If node is FV_TREE, update Pad data and Header info. + # Consume Remain_New_Free_Space is used for move more free space into lst level Fv. + elif TargetTree.type == FV_TREE or TargetTree.type == SEC_FV_TREE and not pos: + if self.Remain_New_Free_Space: + if TargetTree.Data.Free_Space: + TargetTree.Data.Free_Space += self.Remain_New_Free_Space + NewData += self.Remain_New_Free_Space * b'\xff' + TargetTree.Child[-1].Data.Data += self.Remain_New_Free_Space * b'\xff' + else: + TargetTree.Data.Data += self.Remain_New_Free_Space * b'\xff' + New_Free_Space = BIOSTREE('FREE_SPACE') + New_Free_Space.type = FFS_FREE_SPACE + New_Free_Space.Data = FreeSpaceNode(b'\xff' * self.Remain_New_Free_Space) + TargetTree.insertChild(New_Free_Space) + self.Remain_New_Free_Space = 0 + if TargetTree.type == SEC_FV_TREE: + Size_delta = len(NewData) + self.Remain_New_Free_Space - len(TargetTree.Data.Data) + TargetTree.Data.Header.FvLength += Size_delta + TargetTree.Data.ModFvExt() + TargetTree.Data.ModFvSize() + TargetTree.Data.ModExtHeaderData() + ModifyFvExtData(TargetTree) + TargetTree.Data.ModCheckSum() + # If node is SECTION_TREE and not guided section, update Pad data and Header info. + # Remain_New_Free_Space is used for move more free space into lst level Fv. + elif TargetTree.type == SECTION_TREE and TargetTree.Data.Type != 0x02: + New_Pad_Size = GetPadSize(len(NewData), 4) + Size_delta = len(NewData) - len(TargetTree.Data.Data) + ChangeSize(TargetTree, -Size_delta) + if TargetTree.NextRel: + Delta_Pad_Size = len(TargetTree.Data.PadData) - New_Pad_Size + self.Remain_New_Free_Space += Delta_Pad_Size + TargetTree.Data.PadData = b'\x00' * New_Pad_Size + TargetTree.Data.Data = NewData + if GuidTool: + guidtool = GUIDTools().__getitem__(struct2stream(GuidTool)) + if not guidtool.ifexist: + logger.error("GuidTool {} is not found when decompressing {} file.\n".format(guidtool.command, TargetTree.Parent.Data.Name)) + raise Exception("Process Failed: GuidTool not found!") + CompressedData = guidtool.pack(TargetTree.Data.Data) + if len(CompressedData) < len(TargetTree.Data.OriData): + New_Pad_Size = GetPadSize(len(CompressedData), SECTION_COMMON_ALIGNMENT) + Size_delta = len(CompressedData) - len(TargetTree.Data.OriData) + ChangeSize(TargetTree, -Size_delta) + if TargetTree.NextRel: + TargetTree.Data.PadData = b'\x00' * New_Pad_Size + self.Remain_New_Free_Space = len(TargetTree.Data.OriData) + len(TargetTree.Data.PadData) - len(CompressedData) - New_Pad_Size + else: + TargetTree.Data.PadData = b'' + self.Remain_New_Free_Space = len(TargetTree.Data.OriData) - len(CompressedData) + TargetTree.Data.OriData = CompressedData + elif len(CompressedData) == len(TargetTree.Data.OriData): + TargetTree.Data.OriData = CompressedData + elif len(CompressedData) > len(TargetTree.Data.OriData): + New_Pad_Size = GetPadSize(len(CompressedData), SECTION_COMMON_ALIGNMENT) + self.Remain_New_Free_Space = len(CompressedData) + New_Pad_Size - len(TargetTree.Data.OriData) - len(TargetTree.Data.PadData) + self.ModifyTest(TargetTree, self.Remain_New_Free_Space) + self.Status = True + + def ModifyTest(self, ParTree, Needed_Space: int) -> None: + # If have needed space, will find if there have free space in parent tree, meanwhile update the node data. + if Needed_Space > 0: + # If current node is a Fv node + if ParTree.type == FV_TREE or ParTree.type == SEC_FV_TREE: + ParTree.Data.Data = b'' + # First check if Fv free space is enough for needed space. + # If so, use the current Fv free space; + # Else, use all the Free space, and recalculate needed space, continue finding in its parent node. + Needed_Space = Needed_Space - ParTree.Data.Free_Space + if Needed_Space < 0: + ParTree.Child[-1].Data.Data = b'\xff' * (-Needed_Space) + ParTree.Data.Free_Space = (-Needed_Space) + self.Status = True + else: + if ParTree.type == FV_TREE: + self.Status = False + else: + BlockSize = ParTree.Data.Header.BlockMap[0].Length + New_Add_Len = BlockSize - Needed_Space%BlockSize + if New_Add_Len % BlockSize: + ParTree.Child[-1].Data.Data = b'\xff' * New_Add_Len + ParTree.Data.Free_Space = New_Add_Len + Needed_Space += New_Add_Len + else: + ParTree.Child.remove(ParTree.Child[-1]) + ParTree.Data.Free_Space = 0 + ParTree.Data.Size += Needed_Space + ParTree.Data.Header.Fvlength = ParTree.Data.Size + ModifyFvSystemGuid(ParTree) + for item in ParTree.Child: + if item.type == FFS_FREE_SPACE: + ParTree.Data.Data += item.Data.Data + item.Data.PadData + else: + ParTree.Data.Data += struct2stream(item.Data.Header)+ item.Data.Data + item.Data.PadData + ParTree.Data.ModFvExt() + ParTree.Data.ModFvSize() + ParTree.Data.ModExtHeaderData() + ModifyFvExtData(ParTree) + ParTree.Data.ModCheckSum() + # If current node is a Ffs node + elif ParTree.type == FFS_TREE: + ParTree.Data.Data = b'' + OriHeaderLen = ParTree.Data.HeaderLength + # Update its data as adding all the header and data of its child node. + for item in ParTree.Child: + if item.Data.OriData: + if item.Data.ExtHeader: + ParTree.Data.Data += struct2stream(item.Data.Header) + struct2stream(item.Data.ExtHeader) + item.Data.OriData + item.Data.PadData + else: + ParTree.Data.Data += struct2stream(item.Data.Header)+ item.Data.OriData + item.Data.PadData + else: + if item.Data.ExtHeader: + ParTree.Data.Data += struct2stream(item.Data.Header) + struct2stream(item.Data.ExtHeader) + item.Data.Data + item.Data.PadData + else: + ParTree.Data.Data += struct2stream(item.Data.Header)+ item.Data.Data + item.Data.PadData + ChangeSize(ParTree, -Needed_Space) + ModifyFfsType(ParTree) + # Recalculate pad data, update needed space with Delta_Pad_Size. + Needed_Space += ParTree.Data.HeaderLength - OriHeaderLen + New_Pad_Size = GetPadSize(ParTree.Data.Size, FFS_COMMON_ALIGNMENT) + Delta_Pad_Size = New_Pad_Size - len(ParTree.Data.PadData) + Needed_Space += Delta_Pad_Size + ParTree.Data.PadData = b'\xff' * GetPadSize(ParTree.Data.Size, FFS_COMMON_ALIGNMENT) + ParTree.Data.ModCheckSum() + # If current node is a Section node + elif ParTree.type == SECTION_TREE: + OriData = ParTree.Data.Data + OriHeaderLen = ParTree.Data.HeaderLength + ParTree.Data.Data = b'' + # Update its data as adding all the header and data of its child node. + for item in ParTree.Child: + if item.type == SECTION_TREE and item.Data.ExtHeader and item.Data.Type != 0x02: + ParTree.Data.Data += struct2stream(item.Data.Header) + struct2stream(item.Data.ExtHeader) + item.Data.Data + item.Data.PadData + elif item.type == SECTION_TREE and item.Data.ExtHeader and item.Data.Type == 0x02: + ParTree.Data.Data += struct2stream(item.Data.Header) + struct2stream(item.Data.ExtHeader) + item.Data.OriData + item.Data.PadData + else: + ParTree.Data.Data += struct2stream(item.Data.Header) + item.Data.Data + item.Data.PadData + # If the current section is guided section + if ParTree.Data.Type == 0x02: + guidtool = GUIDTools().__getitem__(struct2stream(ParTree.Data.ExtHeader.SectionDefinitionGuid)) + if not guidtool.ifexist: + logger.error("GuidTool {} is not found when decompressing {} file.\n".format(guidtool.command, ParTree.Parent.Data.Name)) + raise Exception("Process Failed: GuidTool not found!") + # Recompress current data, and recalculate the needed space + CompressedData = guidtool.pack(ParTree.Data.Data) + Needed_Space = len(CompressedData) - len(ParTree.Data.OriData) + ParTree.Data.OriData = CompressedData + New_Size = ParTree.Data.HeaderLength + len(CompressedData) + ParTree.Data.Header.Size[0] = New_Size % (16**2) + ParTree.Data.Header.Size[1] = New_Size % (16**4) //(16**2) + ParTree.Data.Header.Size[2] = New_Size // (16**4) + ParTree.Data.Size = ParTree.Data.Header.SECTION_SIZE + ModifySectionType(ParTree) + Needed_Space += ParTree.Data.HeaderLength - OriHeaderLen + # Update needed space with Delta_Pad_Size + if ParTree.NextRel: + New_Pad_Size = GetPadSize(ParTree.Data.Size, SECTION_COMMON_ALIGNMENT) + Delta_Pad_Size = New_Pad_Size - len(ParTree.Data.PadData) + ParTree.Data.PadData = b'\x00' * New_Pad_Size + Needed_Space += Delta_Pad_Size + else: + ParTree.Data.PadData = b'' + if Needed_Space < 0: + self.Remain_New_Free_Space = len(ParTree.Data.OriData) - len(CompressedData) + # If current section is not guided section + elif Needed_Space: + ChangeSize(ParTree, -Needed_Space) + ModifySectionType(ParTree) + # Update needed space with Delta_Pad_Size + Needed_Space += ParTree.Data.HeaderLength - OriHeaderLen + New_Pad_Size = GetPadSize(ParTree.Data.Size, SECTION_COMMON_ALIGNMENT) + Delta_Pad_Size = New_Pad_Size - len(ParTree.Data.PadData) + Needed_Space += Delta_Pad_Size + ParTree.Data.PadData = b'\x00' * New_Pad_Size + NewParTree = ParTree.Parent + ROOT_TYPE = [ROOT_FV_TREE, ROOT_FFS_TREE, ROOT_SECTION_TREE, ROOT_TREE] + if NewParTree and NewParTree.type not in ROOT_TYPE: + self.ModifyTest(NewParTree, Needed_Space) + # If current node have enough space, will recompress all the related node data, return true. + else: + self.CompressData(ParTree) + self.Status = True + + def ReplaceFfs(self) -> bool: + logger.debug('Start Replacing Process......') + TargetFv = self.TargetFfs.Parent + # If the Fv Header Attributes is EFI_FVB2_ERASE_POLARITY, Child Ffs Header State need be reversed. + if TargetFv.Data.Header.Attributes & EFI_FVB2_ERASE_POLARITY: + self.NewFfs.Data.Header.State = c_uint8( + ~self.NewFfs.Data.Header.State) + # NewFfs parsing will not calculate the PadSize, thus recalculate. + self.NewFfs.Data.PadData = b'\xff' * GetPadSize(self.NewFfs.Data.Size, FFS_COMMON_ALIGNMENT) + if self.NewFfs.Data.Size >= self.TargetFfs.Data.Size: + Needed_Space = self.NewFfs.Data.Size + len(self.NewFfs.Data.PadData) - self.TargetFfs.Data.Size - len(self.TargetFfs.Data.PadData) + # If TargetFv have enough free space, just move part of the free space to NewFfs. + if TargetFv.Data.Free_Space >= Needed_Space: + # Modify TargetFv Child info and BiosTree. + TargetFv.Child[-1].Data.Data = b'\xff' * (TargetFv.Data.Free_Space - Needed_Space) + TargetFv.Data.Free_Space -= Needed_Space + Target_index = TargetFv.Child.index(self.TargetFfs) + TargetFv.Child.remove(self.TargetFfs) + TargetFv.insertChild(self.NewFfs, Target_index) + # Modify TargetFv Header and ExtHeader info. + TargetFv.Data.ModFvExt() + TargetFv.Data.ModFvSize() + TargetFv.Data.ModExtHeaderData() + ModifyFvExtData(TargetFv) + TargetFv.Data.ModCheckSum() + # Recompress from the Fv node to update all the related node data. + self.CompressData(TargetFv) + # return the Status + self.Status = True + # If TargetFv do not have enough free space, need move part of the free space of TargetFv's parent Fv to TargetFv/NewFfs. + else: + if TargetFv.type == FV_TREE: + self.Status = False + else: + # Recalculate TargetFv needed space to keep it match the BlockSize setting. + Needed_Space -= TargetFv.Data.Free_Space + BlockSize = TargetFv.Data.Header.BlockMap[0].Length + New_Add_Len = BlockSize - Needed_Space%BlockSize + Target_index = TargetFv.Child.index(self.TargetFfs) + if New_Add_Len % BlockSize: + TargetFv.Child[-1].Data.Data = b'\xff' * New_Add_Len + TargetFv.Data.Free_Space = New_Add_Len + Needed_Space += New_Add_Len + TargetFv.insertChild(self.NewFfs, Target_index) + TargetFv.Child.remove(self.TargetFfs) + else: + TargetFv.Child.remove(self.TargetFfs) + TargetFv.Data.Free_Space = 0 + TargetFv.insertChild(self.NewFfs) + # Encapsulate the Fv Data for update. + TargetFv.Data.Data = b'' + for item in TargetFv.Child: + if item.type == FFS_FREE_SPACE: + TargetFv.Data.Data += item.Data.Data + item.Data.PadData + else: + TargetFv.Data.Data += struct2stream(item.Data.Header)+ item.Data.Data + item.Data.PadData + TargetFv.Data.Size += Needed_Space + # Modify TargetFv Data Header and ExtHeader info. + TargetFv.Data.Header.FvLength = TargetFv.Data.Size + TargetFv.Data.ModFvExt() + TargetFv.Data.ModFvSize() + TargetFv.Data.ModExtHeaderData() + ModifyFvExtData(TargetFv) + TargetFv.Data.ModCheckSum() + # Start free space calculating and moving process. + self.ModifyTest(TargetFv.Parent, Needed_Space) + else: + New_Free_Space = self.TargetFfs.Data.Size - self.NewFfs.Data.Size + # If TargetFv already have free space, move the new free space into it. + if TargetFv.Data.Free_Space: + TargetFv.Child[-1].Data.Data += b'\xff' * New_Free_Space + TargetFv.Data.Free_Space += New_Free_Space + Target_index = TargetFv.Child.index(self.TargetFfs) + TargetFv.Child.remove(self.TargetFfs) + TargetFv.insertChild(self.NewFfs, Target_index) + self.Status = True + # If TargetFv do not have free space, create free space for Fv. + else: + New_Free_Space_Tree = BIOSTREE('FREE_SPACE') + New_Free_Space_Tree.type = FFS_FREE_SPACE + New_Free_Space_Tree.Data = FfsNode(b'\xff' * New_Free_Space) + TargetFv.Data.Free_Space = New_Free_Space + TargetFv.insertChild(New_Free_Space) + Target_index = TargetFv.Child.index(self.TargetFfs) + TargetFv.Child.remove(self.TargetFfs) + TargetFv.insertChild(self.NewFfs, Target_index) + self.Status = True + # Modify TargetFv Header and ExtHeader info. + TargetFv.Data.ModFvExt() + TargetFv.Data.ModFvSize() + TargetFv.Data.ModExtHeaderData() + ModifyFvExtData(TargetFv) + TargetFv.Data.ModCheckSum() + # Recompress from the Fv node to update all the related node data. + self.CompressData(TargetFv) + logger.debug('Done!') + return self.Status + + def AddFfs(self) -> bool: + logger.debug('Start Adding Process......') + # NewFfs parsing will not calculate the PadSize, thus recalculate. + self.NewFfs.Data.PadData = b'\xff' * GetPadSize(self.NewFfs.Data.Size, FFS_COMMON_ALIGNMENT) + if self.TargetFfs.type == FFS_FREE_SPACE: + TargetLen = self.NewFfs.Data.Size + len(self.NewFfs.Data.PadData) - self.TargetFfs.Data.Size - len(self.TargetFfs.Data.PadData) + TargetFv = self.TargetFfs.Parent + # If the Fv Header Attributes is EFI_FVB2_ERASE_POLARITY, Child Ffs Header State need be reversed. + if TargetFv.Data.Header.Attributes & EFI_FVB2_ERASE_POLARITY: + self.NewFfs.Data.Header.State = c_uint8( + ~self.NewFfs.Data.Header.State) + # If TargetFv have enough free space, just move part of the free space to NewFfs, split free space to NewFfs and new free space. + if TargetLen < 0: + self.Status = True + self.TargetFfs.Data.Data = b'\xff' * (-TargetLen) + TargetFv.Data.Free_Space = (-TargetLen) + TargetFv.Data.ModFvExt() + TargetFv.Data.ModExtHeaderData() + ModifyFvExtData(TargetFv) + TargetFv.Data.ModCheckSum() + TargetFv.insertChild(self.NewFfs, -1) + ModifyFfsType(self.NewFfs) + # Recompress from the Fv node to update all the related node data. + self.CompressData(TargetFv) + elif TargetLen == 0: + self.Status = True + TargetFv.Child.remove(self.TargetFfs) + TargetFv.insertChild(self.NewFfs) + ModifyFfsType(self.NewFfs) + # Recompress from the Fv node to update all the related node data. + self.CompressData(TargetFv) + # If TargetFv do not have enough free space, need move part of the free space of TargetFv's parent Fv to TargetFv/NewFfs. + else: + if TargetFv.type == FV_TREE: + self.Status = False + elif TargetFv.type == SEC_FV_TREE: + # Recalculate TargetFv needed space to keep it match the BlockSize setting. + BlockSize = TargetFv.Data.Header.BlockMap[0].Length + New_Add_Len = BlockSize - TargetLen%BlockSize + if New_Add_Len % BlockSize: + self.TargetFfs.Data.Data = b'\xff' * New_Add_Len + self.TargetFfs.Data.Size = New_Add_Len + TargetLen += New_Add_Len + TargetFv.insertChild(self.NewFfs, -1) + TargetFv.Data.Free_Space = New_Add_Len + else: + TargetFv.Child.remove(self.TargetFfs) + TargetFv.insertChild(self.NewFfs) + TargetFv.Data.Free_Space = 0 + ModifyFfsType(self.NewFfs) + ModifyFvSystemGuid(TargetFv) + TargetFv.Data.Data = b'' + for item in TargetFv.Child: + if item.type == FFS_FREE_SPACE: + TargetFv.Data.Data += item.Data.Data + item.Data.PadData + else: + TargetFv.Data.Data += struct2stream(item.Data.Header)+ item.Data.Data + item.Data.PadData + # Encapsulate the Fv Data for update. + TargetFv.Data.Size += TargetLen + TargetFv.Data.Header.FvLength = TargetFv.Data.Size + TargetFv.Data.ModFvExt() + TargetFv.Data.ModFvSize() + TargetFv.Data.ModExtHeaderData() + ModifyFvExtData(TargetFv) + TargetFv.Data.ModCheckSum() + # Start free space calculating and moving process. + self.ModifyTest(TargetFv.Parent, TargetLen) + else: + # If TargetFv do not have free space, need directly move part of the free space of TargetFv's parent Fv to TargetFv/NewFfs. + TargetLen = self.NewFfs.Data.Size + len(self.NewFfs.Data.PadData) + TargetFv = self.TargetFfs.Parent + if TargetFv.Data.Header.Attributes & EFI_FVB2_ERASE_POLARITY: + self.NewFfs.Data.Header.State = c_uint8( + ~self.NewFfs.Data.Header.State) + if TargetFv.type == FV_TREE: + self.Status = False + elif TargetFv.type == SEC_FV_TREE: + BlockSize = TargetFv.Data.Header.BlockMap[0].Length + New_Add_Len = BlockSize - TargetLen%BlockSize + if New_Add_Len % BlockSize: + New_Free_Space = BIOSTREE('FREE_SPACE') + New_Free_Space.type = FFS_FREE_SPACE + New_Free_Space.Data = FreeSpaceNode(b'\xff' * New_Add_Len) + TargetLen += New_Add_Len + TargetFv.Data.Free_Space = New_Add_Len + TargetFv.insertChild(self.NewFfs) + TargetFv.insertChild(New_Free_Space) + else: + TargetFv.insertChild(self.NewFfs) + ModifyFfsType(self.NewFfs) + ModifyFvSystemGuid(TargetFv) + TargetFv.Data.Data = b'' + for item in TargetFv.Child: + if item.type == FFS_FREE_SPACE: + TargetFv.Data.Data += item.Data.Data + item.Data.PadData + else: + TargetFv.Data.Data += struct2stream(item.Data.Header)+ item.Data.Data + item.Data.PadData + TargetFv.Data.Size += TargetLen + TargetFv.Data.Header.FvLength = TargetFv.Data.Size + TargetFv.Data.ModFvExt() + TargetFv.Data.ModFvSize() + TargetFv.Data.ModExtHeaderData() + ModifyFvExtData(TargetFv) + TargetFv.Data.ModCheckSum() + self.ModifyTest(TargetFv.Parent, TargetLen) + logger.debug('Done!') + return self.Status + + def DeleteFfs(self) -> bool: + logger.debug('Start Deleting Process......') + Delete_Ffs = self.TargetFfs + Delete_Fv = Delete_Ffs.Parent + # Calculate free space + Add_Free_Space = Delete_Ffs.Data.Size + len(Delete_Ffs.Data.PadData) + # If Ffs parent Fv have free space, follow the rules to merge the new free space. + if Delete_Fv.Data.Free_Space: + # If Fv is a Section fv, free space need to be recalculated to keep align with BlockSize. + # Other free space saved in self.Remain_New_Free_Space, will be moved to the 1st level Fv. + if Delete_Fv.type == SEC_FV_TREE: + Used_Size = Delete_Fv.Data.Size - Delete_Fv.Data.Free_Space - Add_Free_Space + BlockSize = Delete_Fv.Data.Header.BlockMap[0].Length + New_Free_Space = BlockSize - Used_Size % BlockSize + self.Remain_New_Free_Space += Delete_Fv.Data.Free_Space + Add_Free_Space - New_Free_Space + Delete_Fv.Child[-1].Data.Data = New_Free_Space * b'\xff' + Delete_Fv.Data.Free_Space = New_Free_Space + # If Fv is lst level Fv, new free space will be merged with origin free space. + else: + Used_Size = Delete_Fv.Data.Size - Delete_Fv.Data.Free_Space - Add_Free_Space + Delete_Fv.Child[-1].Data.Data += Add_Free_Space * b'\xff' + Delete_Fv.Data.Free_Space += Add_Free_Space + New_Free_Space = Delete_Fv.Data.Free_Space + # If Ffs parent Fv not have free space, will create new free space node to save the free space. + else: + # If Fv is a Section fv, new free space need to be recalculated to keep align with BlockSize. + # Then create a Free spcae node to save the 0xff data, and insert into the Fv. + # If have more space left, move to 1st level fv. + if Delete_Fv.type == SEC_FV_TREE: + Used_Size = Delete_Fv.Data.Size - Add_Free_Space + BlockSize = Delete_Fv.Data.Header.BlockMap[0].Length + New_Free_Space = BlockSize - Used_Size % BlockSize + self.Remain_New_Free_Space += Add_Free_Space - New_Free_Space + Add_Free_Space = New_Free_Space + # If Fv is lst level Fv, new free space node will be created to save the free space. + else: + Used_Size = Delete_Fv.Data.Size - Add_Free_Space + New_Free_Space = Add_Free_Space + New_Free_Space_Info = FfsNode(Add_Free_Space * b'\xff') + New_Free_Space_Info.Data = Add_Free_Space * b'\xff' + New_Ffs_Tree = BIOSTREE(New_Free_Space_Info.Name) + New_Ffs_Tree.type = FFS_FREE_SPACE + New_Ffs_Tree.Data = New_Free_Space_Info + Delete_Fv.insertChild(New_Ffs_Tree) + Delete_Fv.Data.Free_Space = Add_Free_Space + Delete_Fv.Child.remove(Delete_Ffs) + Delete_Fv.Data.Header.FvLength = Used_Size + New_Free_Space + Delete_Fv.Data.ModFvExt() + Delete_Fv.Data.ModFvSize() + Delete_Fv.Data.ModExtHeaderData() + ModifyFvExtData(Delete_Fv) + Delete_Fv.Data.ModCheckSum() + # Recompress from the Fv node to update all the related node data. + self.CompressData(Delete_Fv) + self.Status = True + logger.debug('Done!') + return self.Status diff --git a/BaseTools/Source/Python/FMMT/core/GuidTools.py b/BaseTools/Source/Python/FMMT/core/GuidTools.py new file mode 100644 index 0000000000..a25681709b --- /dev/null +++ b/BaseTools/Source/Python/FMMT/core/GuidTools.py @@ -0,0 +1,179 @@ +## @file +# This file is used to define the FMMT dependent external tool management class. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +import glob +import logging +import os +import shutil +import sys +import tempfile +import uuid +from FirmwareStorageFormat.Common import * +from utils.FmmtLogger import FmmtLogger as logger +import subprocess + +def ExecuteCommand(cmd: list) -> None: + subprocess.run(cmd,stdout=subprocess.DEVNULL) + +class GUIDTool: + def __init__(self, guid: str, short_name: str, command: str) -> None: + self.guid: str = guid + self.short_name: str = short_name + self.command: str = command + self.ifexist: bool = False + + def pack(self, buffer: bytes) -> bytes: + """ + compress file. + """ + tool = self.command + if tool: + tmp = tempfile.mkdtemp(dir=os.environ.get('tmp')) + ToolInputFile = os.path.join(tmp, "pack_uncompress_sec_file") + ToolOuputFile = os.path.join(tmp, "pack_sec_file") + try: + file = open(ToolInputFile, "wb") + file.write(buffer) + file.close() + command = [tool, '-e', '-o', ToolOuputFile, + ToolInputFile] + ExecuteCommand(command) + buf = open(ToolOuputFile, "rb") + res_buffer = buf.read() + except Exception as msg: + logger.error(msg) + return "" + else: + buf.close() + if os.path.exists(tmp): + shutil.rmtree(tmp) + return res_buffer + else: + logger.error( + "Error parsing section: EFI_SECTION_GUID_DEFINED cannot be parsed at this time.") + logger.info("Its GUID is: %s" % self.guid) + return "" + + + def unpack(self, buffer: bytes) -> bytes: + """ + buffer: remove common header + uncompress file + """ + tool = self.command + if tool: + tmp = tempfile.mkdtemp(dir=os.environ.get('tmp')) + ToolInputFile = os.path.join(tmp, "unpack_sec_file") + ToolOuputFile = os.path.join(tmp, "unpack_uncompress_sec_file") + try: + file = open(ToolInputFile, "wb") + file.write(buffer) + file.close() + command = [tool, '-d', '-o', ToolOuputFile, ToolInputFile] + ExecuteCommand(command) + buf = open(ToolOuputFile, "rb") + res_buffer = buf.read() + except Exception as msg: + logger.error(msg) + return "" + else: + buf.close() + if os.path.exists(tmp): + shutil.rmtree(tmp) + return res_buffer + else: + logger.error("Error parsing section: EFI_SECTION_GUID_DEFINED cannot be parsed at this time.") + logger.info("Its GUID is: %s" % self.guid) + return "" + +class GUIDTools: + ''' + GUIDTools is responsible for reading FMMTConfig.ini, verify the tools and provide interfaces to access those tools. + ''' + default_tools = { + struct2stream(ModifyGuidFormat("a31280ad-481e-41b6-95e8-127f4c984779")): GUIDTool("a31280ad-481e-41b6-95e8-127f4c984779", "TIANO", "TianoCompress"), + struct2stream(ModifyGuidFormat("ee4e5898-3914-4259-9d6e-dc7bd79403cf")): GUIDTool("ee4e5898-3914-4259-9d6e-dc7bd79403cf", "LZMA", "LzmaCompress"), + struct2stream(ModifyGuidFormat("fc1bcdb0-7d31-49aa-936a-a4600d9dd083")): GUIDTool("fc1bcdb0-7d31-49aa-936a-a4600d9dd083", "CRC32", "GenCrc32"), + struct2stream(ModifyGuidFormat("d42ae6bd-1352-4bfb-909a-ca72a6eae889")): GUIDTool("d42ae6bd-1352-4bfb-909a-ca72a6eae889", "LZMAF86", "LzmaF86Compress"), + struct2stream(ModifyGuidFormat("3d532050-5cda-4fd0-879e-0f7f630d5afb")): GUIDTool("3d532050-5cda-4fd0-879e-0f7f630d5afb", "BROTLI", "BrotliCompress"), + } + + def __init__(self, tooldef_file: str=None) -> None: + self.dir = os.path.join(os.path.dirname(__file__), "..") + self.tooldef_file = tooldef_file if tooldef_file else os.path.join(self.dir, "FmmtConf.ini") + self.tooldef = dict() + + def SetConfigFile(self) -> None: + if os.environ['FmmtConfPath']: + self.tooldef_file = os.path.join(os.environ['FmmtConfPath'], 'FmmtConf.ini') + else: + PathList = os.environ['PATH'] + for CurrentPath in PathList: + if os.path.exists(os.path.join(CurrentPath, 'FmmtConf.ini')): + self.tooldef_file = os.path.join(CurrentPath, 'FmmtConf.ini') + break + + def VerifyTools(self, guidtool) -> None: + """ + Verify Tools and Update Tools path. + """ + path_env = os.environ.get("PATH") + path_env_list = path_env.split(os.pathsep) + path_env_list.append(os.path.dirname(__file__)) + path_env_list = list(set(path_env_list)) + cmd = guidtool.command + if os.path.isabs(cmd): + if not os.path.exists(cmd): + if guidtool not in self.default_tools: + logger.error("Tool Not found %s, which causes compress/uncompress process error." % cmd) + logger.error("Please goto edk2 repo in current console, run 'edksetup.bat rebuild' command, and try again.\n") + else: + logger.error("Tool Not found %s, which causes compress/uncompress process error." % cmd) + else: + guidtool.ifexist = True + else: + for syspath in path_env_list: + if glob.glob(os.path.join(syspath, cmd+"*")): + guidtool.ifexist = True + break + else: + if guidtool not in self.default_tools: + logger.error("Tool Not found %s, which causes compress/uncompress process error." % cmd) + logger.error("Please goto edk2 repo in current console, run 'edksetup.bat rebuild' command, and try again.\n") + else: + logger.error("Tool Not found %s, which causes compress/uncompress process error." % cmd) + + def LoadingTools(self) -> None: + self.SetConfigFile() + if os.path.exists(self.tooldef_file): + with open(self.tooldef_file, "r") as fd: + config_data = fd.readlines() + for line in config_data: + try: + if not line.startswith("#"): + guid, short_name, command = line.split() + new_format_guid = struct2stream(ModifyGuidFormat(guid.strip())) + self.tooldef[new_format_guid] = GUIDTool( + guid.strip(), short_name.strip(), command.strip()) + except: + logger.error("GuidTool load error!") + continue + else: + self.tooldef.update(self.default_tools) + + def __getitem__(self, guid): + if not self.tooldef: + self.LoadingTools() + guid_tool = self.tooldef.get(guid) + if guid_tool: + self.VerifyTools(guid_tool) + return guid_tool + else: + logger.error("{} GuidTool is not defined!".format(guid)) + raise Exception("Process Failed: is not defined!") + +guidtools = GUIDTools() + diff --git a/BaseTools/Source/Python/FMMT/utils/FmmtLogger.py b/BaseTools/Source/Python/FMMT/utils/FmmtLogger.py new file mode 100644 index 0000000000..385f098310 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/utils/FmmtLogger.py @@ -0,0 +1,31 @@ +## @file +# This file is used to define the Fmmt Logger. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent + +## + +import logging +import sys +import os + +logfile = 'FMMT_Build.log' +if os.path.exists(logfile): + os.remove(logfile) + +FmmtLogger = logging.getLogger('FMMT') +FmmtLogger.setLevel(logging.DEBUG) + +log_stream_handler=logging.StreamHandler(sys.stdout) +log_file_handler=logging.FileHandler(logfile) +log_stream_handler.setLevel(logging.INFO) + +stream_format=logging.Formatter("%(levelname)-8s: %(message)s") +file_format=logging.Formatter("%(levelname)-8s: %(message)s") + +log_stream_handler.setFormatter(stream_format) +log_file_handler.setFormatter(file_format) + +FmmtLogger.addHandler(log_stream_handler) +FmmtLogger.addHandler(log_file_handler) diff --git a/BaseTools/Source/Python/FMMT/utils/FvLayoutPrint.py b/BaseTools/Source/Python/FMMT/utils/FvLayoutPrint.py new file mode 100644 index 0000000000..7dafcae3b5 --- /dev/null +++ b/BaseTools/Source/Python/FMMT/utils/FvLayoutPrint.py @@ -0,0 +1,55 @@ +## @file +# This file is used to define the printer for Bios layout. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from utils.FmmtLogger import FmmtLogger as logger + +def GetFormatter(layout_format: str): + if layout_format == 'json': + return JsonFormatter() + elif layout_format == 'yaml': + return YamlFormatter() + elif layout_format == 'html': + return HtmlFormatter() + else: + return TxtFormatter() + +class Formatter(object): + def dump(self, layoutdict, layoutlist, outputfile: str=None) -> None: + raise NotImplemented + +class JsonFormatter(Formatter): + def dump(self,layoutdict: dict, layoutlist: list, outputfile: str=None) -> None: + try: + import json + except: + TxtFormatter().dump(layoutdict, layoutlist, outputfile) + return + print(outputfile) + if outputfile: + with open(outputfile,"w") as fw: + json.dump(layoutdict, fw, indent=2) + else: + print(json.dumps(layoutdict,indent=2)) + +class TxtFormatter(Formatter): + def LogPrint(self,layoutlist: list) -> None: + for item in layoutlist: + print(item) + print('\n') + + def dump(self,layoutdict: dict, layoutlist: list, outputfile: str=None) -> None: + logger.info('Binary Layout Info is saved in {} file.'.format(outputfile)) + with open(outputfile, "w") as f: + for item in layoutlist: + f.writelines(item + '\n') + +class YamlFormatter(Formatter): + def dump(self,layoutdict, layoutlist, outputfile = None): + TxtFormatter().dump(layoutdict, layoutlist, outputfile) + +class HtmlFormatter(Formatter): + def dump(self,layoutdict, layoutlist, outputfile = None): + TxtFormatter().dump(layoutdict, layoutlist, outputfile) \ No newline at end of file diff --git a/BaseTools/Source/Python/FirmwareStorageFormat/Common.py b/BaseTools/Source/Python/FirmwareStorageFormat/Common.py new file mode 100644 index 0000000000..5082268a00 --- /dev/null +++ b/BaseTools/Source/Python/FirmwareStorageFormat/Common.py @@ -0,0 +1,85 @@ +## @file +# This file is used to define the common C struct and functions. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from ctypes import * +import uuid + +# ZeroGuid = uuid.UUID('{00000000-0000-0000-0000-000000000000}') +# EFI_FIRMWARE_FILE_SYSTEM2_GUID = uuid.UUID('{8C8CE578-8A3D-4f1c-9935-896185C32DD3}') +# EFI_FIRMWARE_FILE_SYSTEM3_GUID = uuid.UUID('{5473C07A-3DCB-4dca-BD6F-1E9689E7349A}') +# EFI_FFS_VOLUME_TOP_FILE_GUID = uuid.UUID('{1BA0062E-C779-4582-8566-336AE8F78F09}') + +EFI_FIRMWARE_FILE_SYSTEM2_GUID = uuid.UUID("8c8ce578-8a3d-4f1c-9935-896185c32dd3") +EFI_FIRMWARE_FILE_SYSTEM2_GUID_BYTE = b'x\xe5\x8c\x8c=\x8a\x1cO\x995\x89a\x85\xc3-\xd3' +# EFI_FIRMWARE_FILE_SYSTEM2_GUID_BYTE = EFI_FIRMWARE_FILE_SYSTEM2_GUID.bytes +EFI_FIRMWARE_FILE_SYSTEM3_GUID = uuid.UUID("5473C07A-3DCB-4dca-BD6F-1E9689E7349A") +# EFI_FIRMWARE_FILE_SYSTEM3_GUID_BYTE = b'x\xe5\x8c\x8c=\x8a\x1cO\x995\x89a\x85\xc3-\xd3' +EFI_FIRMWARE_FILE_SYSTEM3_GUID_BYTE = b'z\xc0sT\xcb=\xcaM\xbdo\x1e\x96\x89\xe74\x9a' +EFI_SYSTEM_NVDATA_FV_GUID = uuid.UUID("fff12b8d-7696-4c8b-a985-2747075b4f50") +EFI_SYSTEM_NVDATA_FV_GUID_BYTE = b"\x8d+\xf1\xff\x96v\x8bL\xa9\x85'G\x07[OP" +EFI_FFS_VOLUME_TOP_FILE_GUID = uuid.UUID("1ba0062e-c779-4582-8566-336ae8f78f09") +EFI_FFS_VOLUME_TOP_FILE_GUID_BYTE = b'.\x06\xa0\x1by\xc7\x82E\x85f3j\xe8\xf7\x8f\t' +ZEROVECTOR_BYTE = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +PADVECTOR = uuid.UUID("ffffffff-ffff-ffff-ffff-ffffffffffff") +FVH_SIGNATURE = b'_FVH' + +#Alignment +SECTION_COMMON_ALIGNMENT = 4 +FFS_COMMON_ALIGNMENT = 8 + +class GUID(Structure): + _pack_ = 1 + _fields_ = [ + ('Guid1', c_uint32), + ('Guid2', c_uint16), + ('Guid3', c_uint16), + ('Guid4', ARRAY(c_uint8, 8)), + ] + + def from_list(self, listformat: list) -> None: + self.Guid1 = listformat[0] + self.Guid2 = listformat[1] + self.Guid3 = listformat[2] + for i in range(8): + self.Guid4[i] = listformat[i+3] + + def __cmp__(self, otherguid) -> bool: + if not isinstance(otherguid, GUID): + return 'Input is not the GUID instance!' + rt = False + if self.Guid1 == otherguid.Guid1 and self.Guid2 == otherguid.Guid2 and self.Guid3 == otherguid.Guid3: + rt = True + for i in range(8): + rt = rt & (self.Guid4[i] == otherguid.Guid4[i]) + return rt + +def ModifyGuidFormat(target_guid: str) -> GUID: + target_guid = target_guid.replace('-', '') + target_list = [] + start = [0,8,12,16,18,20,22,24,26,28,30] + end = [8,12,16,18,20,22,24,26,28,30,32] + num = len(start) + for pos in range(num): + new_value = int(target_guid[start[pos]:end[pos]], 16) + target_list.append(new_value) + new_format = GUID() + new_format.from_list(target_list) + return new_format + + +# Get data from ctypes to bytes. +def struct2stream(s) -> bytes: + length = sizeof(s) + p = cast(pointer(s), POINTER(c_char * length)) + return p.contents.raw + + + +def GetPadSize(Size: int, alignment: int) -> int: + if Size % alignment == 0: + return 0 + Pad_Size = alignment - Size % alignment + return Pad_Size diff --git a/BaseTools/Source/Python/FirmwareStorageFormat/FfsFileHeader.py b/BaseTools/Source/Python/FirmwareStorageFormat/FfsFileHeader.py new file mode 100644 index 0000000000..e9c619d224 --- /dev/null +++ b/BaseTools/Source/Python/FirmwareStorageFormat/FfsFileHeader.py @@ -0,0 +1,66 @@ +## @file +# This file is used to define the Ffs Header C Struct. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from struct import * +from ctypes import * +from FirmwareStorageFormat.Common import * + +EFI_FFS_FILE_HEADER_LEN = 24 +EFI_FFS_FILE_HEADER2_LEN = 32 + +class CHECK_SUM(Structure): + _pack_ = 1 + _fields_ = [ + ('Header', c_uint8), + ('File', c_uint8), + ] + +class EFI_FFS_INTEGRITY_CHECK(Union): + _pack_ = 1 + _fields_ = [ + ('Checksum', CHECK_SUM), + ('Checksum16', c_uint16), + ] + + +class EFI_FFS_FILE_HEADER(Structure): + _pack_ = 1 + _fields_ = [ + ('Name', GUID), + ('IntegrityCheck', EFI_FFS_INTEGRITY_CHECK), + ('Type', c_uint8), + ('Attributes', c_uint8), + ('Size', ARRAY(c_uint8, 3)), + ('State', c_uint8), + ] + + @property + def FFS_FILE_SIZE(self) -> int: + return self.Size[0] | self.Size[1] << 8 | self.Size[2] << 16 + + @property + def HeaderLength(self) -> int: + return 24 + +class EFI_FFS_FILE_HEADER2(Structure): + _pack_ = 1 + _fields_ = [ + ('Name', GUID), + ('IntegrityCheck', EFI_FFS_INTEGRITY_CHECK), + ('Type', c_uint8), + ('Attributes', c_uint8), + ('Size', ARRAY(c_uint8, 3)), + ('State', c_uint8), + ('ExtendedSize', c_uint64), + ] + + @property + def FFS_FILE_SIZE(self) -> int: + return self.ExtendedSize + + @property + def HeaderLength(self) -> int: + return 32 diff --git a/BaseTools/Source/Python/FirmwareStorageFormat/FvHeader.py b/BaseTools/Source/Python/FirmwareStorageFormat/FvHeader.py new file mode 100644 index 0000000000..078beda9e5 --- /dev/null +++ b/BaseTools/Source/Python/FirmwareStorageFormat/FvHeader.py @@ -0,0 +1,112 @@ +## @file +# This file is used to define the FV Header C Struct. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from ast import Str +from struct import * +from ctypes import * +from FirmwareStorageFormat.Common import * + +class EFI_FV_BLOCK_MAP_ENTRY(Structure): + _pack_ = 1 + _fields_ = [ + ('NumBlocks', c_uint32), + ('Length', c_uint32), + ] + + +class EFI_FIRMWARE_VOLUME_HEADER(Structure): + _fields_ = [ + ('ZeroVector', ARRAY(c_uint8, 16)), + ('FileSystemGuid', GUID), + ('FvLength', c_uint64), + ('Signature', c_uint32), + ('Attributes', c_uint32), + ('HeaderLength', c_uint16), + ('Checksum', c_uint16), + ('ExtHeaderOffset', c_uint16), + ('Reserved', c_uint8), + ('Revision', c_uint8), + ('BlockMap', ARRAY(EFI_FV_BLOCK_MAP_ENTRY, 1)), + ] + +def Refine_FV_Header(nums): + class EFI_FIRMWARE_VOLUME_HEADER(Structure): + _fields_ = [ + ('ZeroVector', ARRAY(c_uint8, 16)), + ('FileSystemGuid', GUID), + ('FvLength', c_uint64), + ('Signature', c_uint32), + ('Attributes', c_uint32), + ('HeaderLength', c_uint16), + ('Checksum', c_uint16), + ('ExtHeaderOffset', c_uint16), + ('Reserved', c_uint8), + ('Revision', c_uint8), + ('BlockMap', ARRAY(EFI_FV_BLOCK_MAP_ENTRY, nums)), + ] + return EFI_FIRMWARE_VOLUME_HEADER + +class EFI_FIRMWARE_VOLUME_EXT_HEADER(Structure): + _fields_ = [ + ('FvName', GUID), + ('ExtHeaderSize', c_uint32) + ] + +class EFI_FIRMWARE_VOLUME_EXT_ENTRY(Structure): + _fields_ = [ + ('ExtEntrySize', c_uint16), + ('ExtEntryType', c_uint16) + ] + +class EFI_FIRMWARE_VOLUME_EXT_ENTRY_OEM_TYPE_0(Structure): + _fields_ = [ + ('Hdr', EFI_FIRMWARE_VOLUME_EXT_ENTRY), + ('TypeMask', c_uint32) + ] + +class EFI_FIRMWARE_VOLUME_EXT_ENTRY_OEM_TYPE(Structure): + _fields_ = [ + ('Hdr', EFI_FIRMWARE_VOLUME_EXT_ENTRY), + ('TypeMask', c_uint32), + ('Types', ARRAY(GUID, 1)) + ] + +def Refine_FV_EXT_ENTRY_OEM_TYPE_Header(nums: int) -> EFI_FIRMWARE_VOLUME_EXT_ENTRY_OEM_TYPE: + class EFI_FIRMWARE_VOLUME_EXT_ENTRY_OEM_TYPE(Structure): + _fields_ = [ + ('Hdr', EFI_FIRMWARE_VOLUME_EXT_ENTRY), + ('TypeMask', c_uint32), + ('Types', ARRAY(GUID, nums)) + ] + return EFI_FIRMWARE_VOLUME_EXT_ENTRY_OEM_TYPE(Structure) + +class EFI_FIRMWARE_VOLUME_EXT_ENTRY_GUID_TYPE_0(Structure): + _fields_ = [ + ('Hdr', EFI_FIRMWARE_VOLUME_EXT_ENTRY), + ('FormatType', GUID) + ] + +class EFI_FIRMWARE_VOLUME_EXT_ENTRY_GUID_TYPE(Structure): + _fields_ = [ + ('Hdr', EFI_FIRMWARE_VOLUME_EXT_ENTRY), + ('FormatType', GUID), + ('Data', ARRAY(c_uint8, 1)) + ] + +def Refine_FV_EXT_ENTRY_GUID_TYPE_Header(nums: int) -> EFI_FIRMWARE_VOLUME_EXT_ENTRY_GUID_TYPE: + class EFI_FIRMWARE_VOLUME_EXT_ENTRY_GUID_TYPE(Structure): + _fields_ = [ + ('Hdr', EFI_FIRMWARE_VOLUME_EXT_ENTRY), + ('FormatType', GUID), + ('Data', ARRAY(c_uint8, nums)) + ] + return EFI_FIRMWARE_VOLUME_EXT_ENTRY_GUID_TYPE(Structure) + +class EFI_FIRMWARE_VOLUME_EXT_ENTRY_USED_SIZE_TYPE(Structure): + _fields_ = [ + ('Hdr', EFI_FIRMWARE_VOLUME_EXT_ENTRY), + ('UsedSize', c_uint32) + ] diff --git a/BaseTools/Source/Python/FirmwareStorageFormat/SectionHeader.py b/BaseTools/Source/Python/FirmwareStorageFormat/SectionHeader.py new file mode 100644 index 0000000000..ee6a63679d --- /dev/null +++ b/BaseTools/Source/Python/FirmwareStorageFormat/SectionHeader.py @@ -0,0 +1,110 @@ +## @file +# This file is used to define the Section Header C Struct. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +from struct import * +from ctypes import * +from FirmwareStorageFormat.Common import * + +EFI_COMMON_SECTION_HEADER_LEN = 4 +EFI_COMMON_SECTION_HEADER2_LEN = 8 + +class EFI_COMMON_SECTION_HEADER(Structure): + _pack_ = 1 + _fields_ = [ + ('Size', ARRAY(c_uint8, 3)), + ('Type', c_uint8), + ] + + @property + def SECTION_SIZE(self) -> int: + return self.Size[0] | self.Size[1] << 8 | self.Size[2] << 16 + + def Common_Header_Size(self) -> int: + return 4 + +class EFI_COMMON_SECTION_HEADER2(Structure): + _pack_ = 1 + _fields_ = [ + ('Size', ARRAY(c_uint8, 3)), + ('Type', c_uint8), + ('ExtendedSize', c_uint32), + ] + + @property + def SECTION_SIZE(self) -> int: + return self.ExtendedSize + + def Common_Header_Size(self) -> int: + return 8 + +class EFI_COMPRESSION_SECTION(Structure): + _pack_ = 1 + _fields_ = [ + ('UncompressedLength', c_uint32), + ('CompressionType', c_uint8), + ] + + def ExtHeaderSize(self) -> int: + return 5 + +class EFI_FREEFORM_SUBTYPE_GUID_SECTION(Structure): + _pack_ = 1 + _fields_ = [ + ('SubTypeGuid', GUID), + ] + + def ExtHeaderSize(self) -> int: + return 16 + +class EFI_GUID_DEFINED_SECTION(Structure): + _pack_ = 1 + _fields_ = [ + ('SectionDefinitionGuid', GUID), + ('DataOffset', c_uint16), + ('Attributes', c_uint16), + ] + + def ExtHeaderSize(self) -> int: + return 20 + +def Get_USER_INTERFACE_Header(nums: int): + class EFI_SECTION_USER_INTERFACE(Structure): + _pack_ = 1 + _fields_ = [ + ('FileNameString', ARRAY(c_uint16, nums)), + ] + + def ExtHeaderSize(self) -> int: + return 2 * nums + + def GetUiString(self) -> str: + UiString = '' + for i in range(nums): + if self.FileNameString[i]: + UiString += chr(self.FileNameString[i]) + return UiString + + return EFI_SECTION_USER_INTERFACE + +def Get_VERSION_Header(nums: int): + class EFI_SECTION_VERSION(Structure): + _pack_ = 1 + _fields_ = [ + ('BuildNumber', c_uint16), + ('VersionString', ARRAY(c_uint16, nums)), + ] + + def ExtHeaderSize(self) -> int: + return 2 * (nums+1) + + def GetVersionString(self) -> str: + VersionString = '' + for i in range(nums): + if self.VersionString[i]: + VersionString += chr(self.VersionString[i]) + return VersionString + + return EFI_SECTION_VERSION diff --git a/BaseTools/Source/Python/FirmwareStorageFormat/__init__.py b/BaseTools/Source/Python/FirmwareStorageFormat/__init__.py new file mode 100644 index 0000000000..335653c6cc --- /dev/null +++ b/BaseTools/Source/Python/FirmwareStorageFormat/__init__.py @@ -0,0 +1,6 @@ +## @file +# This file is used to define the Firmware Storage Format. +# +# Copyright (c) 2021-, Intel Corporation. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## \ No newline at end of file -- 2.39.2