## @file
# This file is for converting package information data file to xml file.
#
# Copyright (c) 2011 - 2018, Intel Corporation. All rights reserved.<BR>
#
# SPDX-License-Identifier: BSD-2-Clause-Patent
#

'''
IniToXml
'''

import os.path
import re
from time import strftime
from time import localtime

import edk2basetools.UPT.Logger.Log as Logger
from edk2basetools.UPT.Logger.ToolError import UPT_INI_PARSE_ERROR
from edk2basetools.UPT.Logger.ToolError import FILE_NOT_FOUND
from edk2basetools.UPT.Library.Xml.XmlRoutines import CreateXmlElement
from edk2basetools.UPT.Library.DataType import TAB_VALUE_SPLIT
from edk2basetools.UPT.Library.DataType import TAB_EQUAL_SPLIT
from edk2basetools.UPT.Library.DataType import TAB_SECTION_START
from edk2basetools.UPT.Library.DataType import TAB_SECTION_END
from edk2basetools.UPT.Logger import StringTable as ST
from edk2basetools.UPT.Library.StringUtils import ConvertSpecialChar
from edk2basetools.UPT.Library.ParserValidate import IsValidPath
from edk2basetools.UPT.Library import GlobalData

## log error:
#
# @param error: error
# @param File: File
# @param Line: Line
#
def IniParseError(Error, File, Line):
    Logger.Error("UPT", UPT_INI_PARSE_ERROR, File=File,
                 Line=Line, ExtraData=Error)

## __ValidatePath
#
# @param Path: Path to be checked
#
def __ValidatePath(Path, Root):
    Path = Path.strip()
    if os.path.isabs(Path) or not IsValidPath(Path, Root):
        return False, ST.ERR_FILELIST_LOCATION % (Root, Path)
    return True, ''

## ValidateMiscFile
#
# @param Filename: File to be checked
#
def ValidateMiscFile(Filename):
    Root = GlobalData.gWORKSPACE
    return __ValidatePath(Filename, Root)

## ValidateToolsFile
#
# @param Filename: File to be checked
#
def ValidateToolsFile(Filename):
    Valid, Cause = False, ''
    if not Valid and 'EDK_TOOLS_PATH' in os.environ:
        Valid, Cause = __ValidatePath(Filename, os.environ['EDK_TOOLS_PATH'])
    if not Valid:
        Valid, Cause = __ValidatePath(Filename, GlobalData.gWORKSPACE)
    return Valid, Cause

## ParseFileList
#
# @param Line: Line
# @param Map: Map
# @param CurrentKey: CurrentKey
# @param PathFunc: Path validate function
#
def ParseFileList(Line, Map, CurrentKey, PathFunc):
    FileList = ["", {}]
    TokenList = Line.split(TAB_VALUE_SPLIT)
    if len(TokenList) > 0:
        Path = TokenList[0].strip().replace('\\', '/')
        if not Path:
            return False, ST.ERR_WRONG_FILELIST_FORMAT
        Valid, Cause = PathFunc(Path)
        if not Valid:
            return Valid, Cause
        FileList[0] = TokenList[0].strip()
        for Token in TokenList[1:]:
            Attr = Token.split(TAB_EQUAL_SPLIT)
            if len(Attr) != 2 or not Attr[0].strip() or not Attr[1].strip():
                return False, ST.ERR_WRONG_FILELIST_FORMAT

            Key = Attr[0].strip()
            Val = Attr[1].strip()
            if Key not in ['OS', 'Executable']:
                return False, ST.ERR_UNKNOWN_FILELIST_ATTR % Key

            if Key == 'OS' and Val not in ["Win32", "Win64", "Linux32",
                                           "Linux64", "OS/X32", "OS/X64",
                                           "GenericWin", "GenericNix"]:
                return False, ST.ERR_FILELIST_ATTR % 'OS'
            elif Key == 'Executable' and Val not in ['true', 'false']:
                return False, ST.ERR_FILELIST_ATTR % 'Executable'
            FileList[1][Key] = Val

        Map[CurrentKey].append(FileList)
    return True, ''

## Create header XML file
#
# @param DistMap: DistMap
# @param Root: Root
#
def CreateHeaderXml(DistMap, Root):
    Element1 = CreateXmlElement('Name', DistMap['Name'],
                                [], [['BaseName', DistMap['BaseName']]])
    Element2 = CreateXmlElement('GUID', DistMap['GUID'],
                                [], [['Version', DistMap['Version']]])
    AttributeList = [['ReadOnly', DistMap['ReadOnly']],
                     ['RePackage', DistMap['RePackage']]]
    NodeList = [Element1,
                Element2,
                ['Vendor', DistMap['Vendor']],
                ['Date', DistMap['Date']],
                ['Copyright', DistMap['Copyright']],
                ['License', DistMap['License']],
                ['Abstract', DistMap['Abstract']],
                ['Description', DistMap['Description']],
                ['Signature', DistMap['Signature']],
                ['XmlSpecification', DistMap['XmlSpecification']],
                ]
    Root.appendChild(CreateXmlElement('DistributionHeader', '',
                                      NodeList, AttributeList))

## Create tools XML file
#
# @param Map: Map
# @param Root: Root
# @param Tag: Tag
#
def CreateToolsXml(Map, Root, Tag):
    #
    # Check if all elements in this section are empty
    #
    for Key in Map:
        if len(Map[Key]) > 0:
            break
    else:
        return

    NodeList = [['Name', Map['Name']],
                ['Copyright', Map['Copyright']],
                ['License', Map['License']],
                ['Abstract', Map['Abstract']],
                ['Description', Map['Description']],
               ]
    HeaderNode = CreateXmlElement('Header', '', NodeList, [])
    NodeList = [HeaderNode]

    for File in Map['FileList']:
        AttrList = []
        for Key in File[1]:
            AttrList.append([Key, File[1][Key]])
        NodeList.append(CreateXmlElement('Filename', File[0], [], AttrList))
    Root.appendChild(CreateXmlElement(Tag, '', NodeList, []))

## ValidateValues
#
# @param Key: Key
# @param Value: Value
# @param SectionName: SectionName
#
def ValidateValues(Key, Value, SectionName):
    if SectionName == 'DistributionHeader':
        Valid, Cause = ValidateRegValues(Key, Value)
        if not Valid:
            return Valid, Cause
        Valid = __ValidateDistHeader(Key, Value)
        if not Valid:
            return Valid, ST.ERR_VALUE_INVALID % (Key, SectionName)
    else:
        Valid = __ValidateOtherHeader(Key, Value)
        if not Valid:
            return Valid, ST.ERR_VALUE_INVALID % (Key, SectionName)
    return True, ''

## ValidateRegValues
#
# @param Key: Key
# @param Value: Value
#
def ValidateRegValues(Key, Value):
    ValidateMap = {
        'ReadOnly'  :
            ('true|false', ST.ERR_BOOLEAN_VALUE % (Key, Value)),
        'RePackage' :
            ('true|false', ST.ERR_BOOLEAN_VALUE % (Key, Value)),
        'GUID'      :
            ('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}'
            '-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}',
            ST.ERR_GUID_VALUE % Value),
        'Version'   :   ('[0-9]+(\.[0-9]+)?', ST.ERR_VERSION_VALUE % \
                         (Key, Value)),
        'XmlSpecification' : ('1\.1', ST.ERR_VERSION_XMLSPEC % Value)
    }
    if Key not in ValidateMap:
        return True, ''
    Elem = ValidateMap[Key]
    Match = re.compile(Elem[0]).match(Value)
    if Match and Match.start() == 0 and Match.end() == len(Value):
        return True, ''
    return False, Elem[1]

## __ValidateDistHeaderName
#
# @param Name: Name
#
def __ValidateDistHeaderName(Name):
    if len(Name) < 1:
        return False

    for Char in Name:
        if ord(Char) < 0x20 or ord(Char) >= 0x7f:
            return False
    return True

## __ValidateDistHeaderBaseName
#
# @param BaseName: BaseName
#
def __ValidateDistHeaderBaseName(BaseName):
    if not BaseName:
        return False
#    if CheckLen and len(BaseName) < 2:
#        return False
    if not BaseName[0].isalnum() and BaseName[0] != '_':
        return False
    for Char in BaseName[1:]:
        if not Char.isalnum() and Char not in '-_':
            return False
    return True

## __ValidateDistHeaderAbstract
#
# @param Abstract: Abstract
#
def __ValidateDistHeaderAbstract(Abstract):
    return '\t' not in Abstract and len(Abstract.splitlines()) == 1

## __ValidateOtherHeaderAbstract
#
# @param Abstract: Abstract
#
def __ValidateOtherHeaderAbstract(Abstract):
    return __ValidateDistHeaderAbstract(Abstract)

## __ValidateDistHeader
#
# @param Key: Key
# @param Value: Value
#
def __ValidateDistHeader(Key, Value):
    ValidateMap = {
        'Name'      : __ValidateDistHeaderName,
        'BaseName'  : __ValidateDistHeaderBaseName,
        'Abstract'  : __ValidateDistHeaderAbstract,
        'Vendor'    : __ValidateDistHeaderAbstract
    }
    return not (Value and Key in ValidateMap and not ValidateMap[Key](Value))

## __ValidateOtherHeader
#
# @param Key: Key
# @param Value: Value
#
def __ValidateOtherHeader(Key, Value):
    ValidateMap = {
        'Name'      : __ValidateDistHeaderName,
        'Abstract'  : __ValidateOtherHeaderAbstract
    }
    return not (Value and Key in ValidateMap and not ValidateMap[Key](Value))

## Convert ini file to xml file
#
# @param IniFile
#
def IniToXml(IniFile):
    if not os.path.exists(IniFile):
        Logger.Error("UPT", FILE_NOT_FOUND, ST.ERR_TEMPLATE_NOTFOUND % IniFile)

    DistMap = {'ReadOnly' : '', 'RePackage' : '', 'Name' : '',
               'BaseName' : '', 'GUID' : '', 'Version' : '', 'Vendor' : '',
               'Date' : '', 'Copyright' : '', 'License' : '', 'Abstract' : '',
               'Description' : '', 'Signature' : '', 'XmlSpecification' : ''
                }

    ToolsMap = {'Name' : '', 'Copyright' : '', 'License' : '',
                'Abstract' : '', 'Description' : '', 'FileList' : []}
    #
    # Only FileList is a list: [['file1', {}], ['file2', {}], ...]
    #
    MiscMap = {'Name' : '', 'Copyright' : '', 'License' : '',
               'Abstract' : '', 'Description' : '', 'FileList' : []}

    SectionMap = {
                   'DistributionHeader' : DistMap,
                   'ToolsHeader' : ToolsMap,
                   'MiscellaneousFilesHeader' : MiscMap
                   }

    PathValidator = {
                'ToolsHeader' : ValidateToolsFile,
                'MiscellaneousFilesHeader' : ValidateMiscFile
                }

    ParsedSection = []

    SectionName = ''
    CurrentKey = ''
    PreMap = None
    Map = None
    FileContent = ConvertSpecialChar(open(IniFile, 'r').readlines())
    LastIndex = 0
    for Index in range(0, len(FileContent)):
        LastIndex = Index
        Line = FileContent[Index].strip()
        if Line == '' or Line.startswith(';'):
            continue
        if Line[0] == TAB_SECTION_START and Line[-1] == TAB_SECTION_END:
            CurrentKey = ''
            SectionName = Line[1:-1].strip()
            if SectionName not in SectionMap:
                IniParseError(ST.ERR_SECTION_NAME_INVALID % SectionName,
                      IniFile, Index+1)

            if SectionName in ParsedSection:
                IniParseError(ST.ERR_SECTION_REDEFINE % SectionName,
                      IniFile, Index+1)
            else:
                ParsedSection.append(SectionName)

            Map = SectionMap[SectionName]
            continue
        if not Map:
            IniParseError(ST.ERR_SECTION_NAME_NONE, IniFile, Index+1)
        TokenList = Line.split(TAB_EQUAL_SPLIT, 1)
        TempKey = TokenList[0].strip()
        #
        # Value spanned multiple or same keyword appears more than one time
        #
        if len(TokenList) < 2 or TempKey not in Map:
            if CurrentKey == '':
                IniParseError(ST.ERR_KEYWORD_INVALID % TempKey,
                              IniFile, Index+1)
            elif CurrentKey == 'FileList':
                #
                # Special for FileList
                #
                Valid, Cause = ParseFileList(Line, Map, CurrentKey,
                                             PathValidator[SectionName])
                if not Valid:
                    IniParseError(Cause, IniFile, Index+1)

            else:
                #
                # Multiple lines for one key such as license
                # Or if string on the left side of '=' is not a keyword
                #
                Map[CurrentKey] = ''.join([Map[CurrentKey], '\n', Line])
                Valid, Cause = ValidateValues(CurrentKey,
                                              Map[CurrentKey], SectionName)
                if not Valid:
                    IniParseError(Cause, IniFile, Index+1)
            continue

        if (TokenList[1].strip() == ''):
            IniParseError(ST.ERR_EMPTY_VALUE, IniFile, Index+1)

        #
        # A keyword found
        #
        CurrentKey = TempKey
        if Map[CurrentKey]:
            IniParseError(ST.ERR_KEYWORD_REDEFINE % CurrentKey,
                          IniFile, Index+1)

        if id(Map) != id(PreMap) and Map['Copyright']:
            PreMap = Map
            Copyright = Map['Copyright'].lower()
            Pos = Copyright.find('copyright')
            if Pos == -1:
                IniParseError(ST.ERR_COPYRIGHT_CONTENT, IniFile, Index)
            if not Copyright[Pos + len('copyright'):].lstrip(' ').startswith('('):
                IniParseError(ST.ERR_COPYRIGHT_CONTENT, IniFile, Index)

        if CurrentKey == 'FileList':
            Valid, Cause = ParseFileList(TokenList[1], Map, CurrentKey,
                                         PathValidator[SectionName])
            if not Valid:
                IniParseError(Cause, IniFile, Index+1)
        else:
            Map[CurrentKey] = TokenList[1].strip()
            Valid, Cause = ValidateValues(CurrentKey,
                                          Map[CurrentKey], SectionName)
            if not Valid:
                IniParseError(Cause, IniFile, Index+1)

    if id(Map) != id(PreMap) and Map['Copyright'] and 'copyright' not in Map['Copyright'].lower():
        IniParseError(ST.ERR_COPYRIGHT_CONTENT, IniFile, LastIndex)

    #
    # Check mandatory keys
    #
    CheckMdtKeys(DistMap, IniFile, LastIndex,
                 (('ToolsHeader', ToolsMap), ('MiscellaneousFilesHeader', MiscMap))
                 )

    return CreateXml(DistMap, ToolsMap, MiscMap, IniFile)


## CheckMdtKeys
#
# @param MdtDistKeys: All mandatory keys
# @param DistMap: Dist content
# @param IniFile: Ini file
# @param LastIndex: Last index of Ini file
# @param Maps: Tools and Misc section name and map. (('section_name', map),*)
#
def CheckMdtKeys(DistMap, IniFile, LastIndex, Maps):
    MdtDistKeys = ['Name', 'GUID', 'Version', 'Vendor', 'Copyright', 'License', 'Abstract', 'XmlSpecification']
    for Key in MdtDistKeys:
        if Key not in DistMap or DistMap[Key] == '':
            IniParseError(ST.ERR_KEYWORD_MANDATORY % Key, IniFile, LastIndex+1)

    if '.' not in DistMap['Version']:
        DistMap['Version'] = DistMap['Version'] + '.0'

    DistMap['Date'] = str(strftime("%Y-%m-%dT%H:%M:%S", localtime()))

    #
    # Check Tools Surface Area according to UPT Spec
    # <Tools> {0,}
    #     <Header> ... </Header> {0,1}
    #     <Filename> ... </Filename> {1,}
    # </Tools>
    # <Header>
    #    <Name> xs:normalizedString </Name> {1}
    #    <Copyright> xs:string </Copyright> {0,1}
    #    <License> xs:string </License> {0,1}
    #    <Abstract> xs:normalizedString </Abstract> {0,1}
    #    <Description> xs:string </Description> {0,1}
    # </Header>
    #
    for Item in Maps:
        Map = Item[1]
        NonEmptyKey = 0
        for Key in Map:
            if Map[Key]:
                NonEmptyKey += 1

        if NonEmptyKey > 0 and not Map['FileList']:
            IniParseError(ST.ERR_KEYWORD_MANDATORY % (Item[0] + '.FileList'), IniFile, LastIndex+1)

        if NonEmptyKey > 0 and not Map['Name']:
            IniParseError(ST.ERR_KEYWORD_MANDATORY % (Item[0] + '.Name'), IniFile, LastIndex+1)

## CreateXml
#
# @param DistMap:  Dist Content
# @param ToolsMap: Tools Content
# @param MiscMap:  Misc Content
# @param IniFile:  Ini File
#
def CreateXml(DistMap, ToolsMap, MiscMap, IniFile):
    Attrs = [['xmlns', 'http://www.uefi.org/2011/1.1'],
             ['xmlns:xsi', 'http:/www.w3.org/2001/XMLSchema-instance'],
            ]
    Root = CreateXmlElement('DistributionPackage', '', [], Attrs)
    CreateHeaderXml(DistMap, Root)
    CreateToolsXml(ToolsMap, Root, 'Tools')
    CreateToolsXml(MiscMap, Root, 'MiscellaneousFiles')

    FileAndExt = IniFile.rsplit('.', 1)
    if len(FileAndExt) > 1:
        FileName = FileAndExt[0] + '.xml'
    else:
        FileName = IniFile + '.xml'
    File = open(FileName, 'w')

    try:
        File.write(Root.toprettyxml(indent = '  '))
    finally:
        File.close()
    return FileName

