diff options
Diffstat (limited to 'src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck')
3 files changed, 342 insertions, 0 deletions
diff --git a/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/GuidCheck.py b/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/GuidCheck.py new file mode 100755 index 00000000..73fdf66d --- /dev/null +++ b/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/GuidCheck.py @@ -0,0 +1,251 @@ +# @file GuidCheck.py +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +import logging +from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin +from edk2toollib.uefi.edk2.guid_list import GuidList +from edk2toolext.environment.var_dict import VarDict + + +class GuidCheck(ICiBuildPlugin): + """ + A CiBuildPlugin that scans the code tree and looks for duplicate guids + from the package being tested. + + Configuration options: + "GuidCheck": { + "IgnoreGuidName": [], # provide in format guidname=guidvalue or just guidname + "IgnoreGuidValue": [], + "IgnoreFoldersAndFiles": [], + "IgnoreDuplicates": [] # Provide in format guidname=guidname=guidname... + } + """ + + def GetTestName(self, packagename: str, environment: VarDict) -> tuple: + """ Provide the testcase name and classname for use in reporting + + Args: + packagename: string containing name of package to build + environment: The VarDict for the test to run in + Returns: + a tuple containing the testcase name and the classname + (testcasename, classname) + testclassname: a descriptive string for the testcase can include whitespace + classname: should be patterned <packagename>.<plugin>.<optionally any unique condition> + """ + return ("Confirm GUIDs are unique in " + packagename, packagename + ".GuidCheck") + + def _FindConflictingGuidValues(self, guidlist: list) -> list: + """ Find all duplicate guids by guid value and report them as errors + """ + # Sort the list by guid + guidsorted = sorted( + guidlist, key=lambda x: x.guid.upper(), reverse=True) + + previous = None # Store previous entry for comparison + error = None + errors = [] + for index in range(len(guidsorted)): + i = guidsorted[index] + if(previous is not None): + if i.guid == previous.guid: # Error + if(error is None): + # Catch errors with more than 1 conflict + error = ErrorEntry("guid") + error.entries.append(previous) + errors.append(error) + error.entries.append(i) + else: + # no match. clear error + error = None + previous = i + return errors + + def _FindConflictingGuidNames(self, guidlist: list) -> list: + """ Find all duplicate guids by name and if they are not all + from inf files report them as errors. It is ok to have + BASE_NAME duplication. + + Is this useful? It would catch two same named guids in dec file + that resolve to different values. + """ + # Sort the list by guid + namesorted = sorted(guidlist, key=lambda x: x.name.upper()) + + previous = None # Store previous entry for comparison + error = None + errors = [] + for index in range(len(namesorted)): + i = namesorted[index] + if(previous is not None): + # If name matches + if i.name == previous.name: + if(error is None): + # Catch errors with more than 1 conflict + error = ErrorEntry("name") + error.entries.append(previous) + errors.append(error) + error.entries.append(i) + else: + # no match. clear error + error = None + previous = i + + # Loop thru and remove any errors where all files are infs as it is ok if + # they have the same inf base name. + for e in errors[:]: + if len( [en for en in e.entries if not en.absfilepath.lower().endswith(".inf")]) == 0: + errors.remove(e) + + return errors + + ## + # External function of plugin. This function is used to perform the task of the MuBuild Plugin + # + # - package is the edk2 path to package. This means workspace/packagepath relative. + # - edk2path object configured with workspace and packages path + # - PkgConfig Object (dict) for the pkg + # - EnvConfig Object + # - Plugin Manager Instance + # - Plugin Helper Obj Instance + # - Junit Logger + # - output_stream the StringIO output stream from this plugin via logging + + def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None): + Errors = [] + + abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSytemFromEdk2RelativePath( + packagename) + + if abs_pkg_path is None: + tc.SetSkipped() + tc.LogStdError("No package {0}".format(packagename)) + return -1 + + All_Ignores = ["/Build", "/Conf"] + # Parse the config for other ignores + if "IgnoreFoldersAndFiles" in pkgconfig: + All_Ignores.extend(pkgconfig["IgnoreFoldersAndFiles"]) + + # Parse the workspace for all GUIDs + gs = GuidList.guidlist_from_filesystem( + Edk2pathObj.WorkspacePath, ignore_lines=All_Ignores) + + # Remove ignored guidvalue + if "IgnoreGuidValue" in pkgconfig: + for a in pkgconfig["IgnoreGuidValue"]: + try: + tc.LogStdOut("Ignoring Guid {0}".format(a.upper())) + for b in gs[:]: + if b.guid == a.upper(): + gs.remove(b) + except: + tc.LogStdError("GuidCheck.IgnoreGuid -> {0} not found. Invalid ignore guid".format(a.upper())) + logging.info("GuidCheck.IgnoreGuid -> {0} not found. Invalid ignore guid".format(a.upper())) + + # Remove ignored guidname + if "IgnoreGuidName" in pkgconfig: + for a in pkgconfig["IgnoreGuidName"]: + entry = a.split("=") + if(len(entry) > 2): + tc.LogStdError("GuidCheck.IgnoreGuidName -> {0} Invalid Format.".format(a)) + logging.info("GuidCheck.IgnoreGuidName -> {0} Invalid Format.".format(a)) + continue + try: + tc.LogStdOut("Ignoring Guid {0}".format(a)) + for b in gs[:]: + if b.name == entry[0]: + if(len(entry) == 1): + gs.remove(b) + elif(len(entry) == 2 and b.guid.upper() == entry[1].upper()): + gs.remove(b) + else: + c.LogStdError("GuidCheck.IgnoreGuidName -> {0} incomplete match. Invalid ignore guid".format(a)) + + except: + tc.LogStdError("GuidCheck.IgnoreGuidName -> {0} not found. Invalid ignore name".format(a)) + logging.info("GuidCheck.IgnoreGuidName -> {0} not found. Invalid ignore name".format(a)) + + # Find conflicting Guid Values + Errors.extend(self._FindConflictingGuidValues(gs)) + + # Check if there are expected duplicates and remove it from the error list + if "IgnoreDuplicates" in pkgconfig: + for a in pkgconfig["IgnoreDuplicates"]: + names = a.split("=") + if len(names) < 2: + tc.LogStdError("GuidCheck.IgnoreDuplicates -> {0} invalid format".format(a)) + logging.info("GuidCheck.IgnoreDuplicates -> {0} invalid format".format(a)) + continue + + for b in Errors[:]: + if b.type != "guid": + continue + ## Make a list of the names that are not in the names list. If there + ## are any in the list then this error should not be ignored. + t = [x for x in b.entries if x.name not in names] + if(len(t) == len(b.entries)): + ## did not apply to any entry + continue + elif(len(t) == 0): + ## full match - ignore duplicate + tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0}".format(a)) + Errors.remove(b) + elif(len(t) < len(b.entries)): + ## partial match + tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0} incomplete match".format(a)) + logging.info("GuidCheck.IgnoreDuplicates -> {0} incomplete match".format(a)) + else: + tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0} unknown error.".format(a)) + logging.info("GuidCheck.IgnoreDuplicates -> {0} unknown error".format(a)) + + + + # Find conflicting Guid Names + Errors.extend(self._FindConflictingGuidNames(gs)) + + # Log errors for anything within the package under test + for er in Errors[:]: + InMyPackage = False + for a in er.entries: + if abs_pkg_path in a.absfilepath: + InMyPackage = True + break + if(not InMyPackage): + Errors.remove(er) + else: + logging.error(str(er)) + tc.LogStdError(str(er)) + + # add result to test case + overall_status = len(Errors) + if overall_status != 0: + tc.SetFailed("GuidCheck {0} Failed. Errors {1}".format( + packagename, overall_status), "CHECK_FAILED") + else: + tc.SetSuccess() + return overall_status + + +class ErrorEntry(): + """ Custom/private class for reporting errors in the GuidList + """ + + def __init__(self, errortype): + self.type = errortype # 'guid' or 'name' depending on error type + self.entries = [] # GuidListEntry that are in error condition + + def __str__(self): + a = f"Error Duplicate {self.type}: " + if(self.type == "guid"): + a += f" {self.entries[0].guid}" + elif(self.type == "name"): + a += f" {self.entries[0].name}" + + a += f" ({len(self.entries)})\n" + + for e in self.entries: + a += "\t" + str(e) + "\n" + return a diff --git a/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/GuidCheck_plug_in.yaml b/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/GuidCheck_plug_in.yaml new file mode 100644 index 00000000..dc40afad --- /dev/null +++ b/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/GuidCheck_plug_in.yaml @@ -0,0 +1,11 @@ +## @file +# CiBuildPlugin used to check guid uniqueness +# +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: BSD-2-Clause-Patent +## +{ + "scope": "cibuild", + "name": "Guid Check Test", + "module": "GuidCheck" +} diff --git a/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/Readme.md b/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/Readme.md new file mode 100644 index 00000000..36c63432 --- /dev/null +++ b/src/VBox/Devices/EFI/Firmware/.pytool/Plugin/GuidCheck/Readme.md @@ -0,0 +1,80 @@ +# Guid Check Plugin + +This CiBuildPlugin scans all the files in a code tree to find all the GUID +definitions. After collection it will then look for duplication in the package +under test. Uniqueness of all GUIDs are critical within the UEFI environment. +Duplication can cause numerous issues including locating the wrong data +structure, calling the wrong function, or decoding the wrong data members. + +Currently Scanned: + +* INF files are scanned for there Module guid +* DEC files are scanned for all of their Protocols, PPIs, and Guids as well as + the one package GUID. + +Any GUID value being equal to two names or even just defined in two files is +considered an error unless in the ignore list. + +Any GUID name that is found more than once is an error unless all occurrences +are Module GUIDs. Since the Module GUID is assigned to the Module name it is +common to have numerous versions of the same module named the same. + +## Configuration + +The plugin has numerous configuration options to support the UEFI codebase. + +``` yaml +"GuidCheck": { + "IgnoreGuidName": [], + "IgnoreGuidValue": [], + "IgnoreFoldersAndFiles": [], + "IgnoreDuplicates": [] + } +``` + +### IgnoreGuidName + +This list allows strings in two formats. + +* _GuidName_ + * This will remove any entry with this GuidName from the list of GUIDs + therefore ignoring any error associated with this name. +* _GuidName=GuidValue_ + * This will also ignore the GUID by name but only if the value equals the + GuidValue. + * GuidValue should be in registry format. + * This is the suggested format to use as it will limit the ignore to only the + defined case. + +### IgnoreGuidValue + +This list allows strings in guid registry format _GuidValue_. + +* This will remove any entry with this GuidValue from the list of GUIDs + therefore ignoring any error associated with this value. +* GuidValue must be in registry format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +### IgnoreFoldersAndFiles + +This supports .gitignore file and folder matching strings including wildcards + +* Any folder or file ignored will not be parsed and therefore any GUID defined + will be ignored. +* The plugin will always ignores the following ["/Build", "/Conf"] + +### IgnoreDuplicates + +This supports strings in the format of _GuidName_=_GuidName_=_GuidName_ + +* For the error with the GuidNames to be ignored the list must match completely + with what is found during the code scan. + * For example if there are two GUIDs that are by design equal within the code + tree then it should be _GuidName_=_GuidName_ + * If instead there are three GUIDs then it must be + _GuidName_=_GuidName_=_GuidName_ +* This is the best ignore list to use because it is the most strict and will + catch new problems when new conflicts are introduced. +* There are numerous places in the UEFI specification in which two GUID names + are assigned the same value. These names should be set in this ignore list so + that they don't cause an error but any additional duplication would still be + caught. |