1## @file
2# This file is for converting package information data file to xml file.
3#
4# Copyright (c) 2011 - 2018, Intel Corporation. All rights reserved.<BR>
5#
6# SPDX-License-Identifier: BSD-2-Clause-Patent
7#
8
9'''
10IniToXml
11'''
12
13import os.path
14import re
15from time import strftime
16from time import localtime
17
18import Logger.Log as Logger
19from Logger.ToolError import UPT_INI_PARSE_ERROR
20from Logger.ToolError import FILE_NOT_FOUND
21from Library.Xml.XmlRoutines import CreateXmlElement
22from Library.DataType import TAB_VALUE_SPLIT
23from Library.DataType import TAB_EQUAL_SPLIT
24from Library.DataType import TAB_SECTION_START
25from Library.DataType import TAB_SECTION_END
26from Logger import StringTable as ST
27from Library.StringUtils import ConvertSpecialChar
28from Library.ParserValidate import IsValidPath
29from Library import GlobalData
30
31## log error:
32#
33# @param error: error
34# @param File: File
35# @param Line: Line
36#
37def IniParseError(Error, File, Line):
38    Logger.Error("UPT", UPT_INI_PARSE_ERROR, File=File,
39                 Line=Line, ExtraData=Error)
40
41## __ValidatePath
42#
43# @param Path: Path to be checked
44#
45def __ValidatePath(Path, Root):
46    Path = Path.strip()
47    if os.path.isabs(Path) or not IsValidPath(Path, Root):
48        return False, ST.ERR_FILELIST_LOCATION % (Root, Path)
49    return True, ''
50
51## ValidateMiscFile
52#
53# @param Filename: File to be checked
54#
55def ValidateMiscFile(Filename):
56    Root = GlobalData.gWORKSPACE
57    return __ValidatePath(Filename, Root)
58
59## ValidateToolsFile
60#
61# @param Filename: File to be checked
62#
63def ValidateToolsFile(Filename):
64    Valid, Cause = False, ''
65    if not Valid and 'EDK_TOOLS_PATH' in os.environ:
66        Valid, Cause = __ValidatePath(Filename, os.environ['EDK_TOOLS_PATH'])
67    if not Valid:
68        Valid, Cause = __ValidatePath(Filename, GlobalData.gWORKSPACE)
69    return Valid, Cause
70
71## ParseFileList
72#
73# @param Line: Line
74# @param Map: Map
75# @param CurrentKey: CurrentKey
76# @param PathFunc: Path validate function
77#
78def ParseFileList(Line, Map, CurrentKey, PathFunc):
79    FileList = ["", {}]
80    TokenList = Line.split(TAB_VALUE_SPLIT)
81    if len(TokenList) > 0:
82        Path = TokenList[0].strip().replace('\\', '/')
83        if not Path:
84            return False, ST.ERR_WRONG_FILELIST_FORMAT
85        Valid, Cause = PathFunc(Path)
86        if not Valid:
87            return Valid, Cause
88        FileList[0] = TokenList[0].strip()
89        for Token in TokenList[1:]:
90            Attr = Token.split(TAB_EQUAL_SPLIT)
91            if len(Attr) != 2 or not Attr[0].strip() or not Attr[1].strip():
92                return False, ST.ERR_WRONG_FILELIST_FORMAT
93
94            Key = Attr[0].strip()
95            Val = Attr[1].strip()
96            if Key not in ['OS', 'Executable']:
97                return False, ST.ERR_UNKNOWN_FILELIST_ATTR % Key
98
99            if Key == 'OS' and Val not in ["Win32", "Win64", "Linux32",
100                                           "Linux64", "OS/X32", "OS/X64",
101                                           "GenericWin", "GenericNix"]:
102                return False, ST.ERR_FILELIST_ATTR % 'OS'
103            elif Key == 'Executable' and Val not in ['true', 'false']:
104                return False, ST.ERR_FILELIST_ATTR % 'Executable'
105            FileList[1][Key] = Val
106
107        Map[CurrentKey].append(FileList)
108    return True, ''
109
110## Create header XML file
111#
112# @param DistMap: DistMap
113# @param Root: Root
114#
115def CreateHeaderXml(DistMap, Root):
116    Element1 = CreateXmlElement('Name', DistMap['Name'],
117                                [], [['BaseName', DistMap['BaseName']]])
118    Element2 = CreateXmlElement('GUID', DistMap['GUID'],
119                                [], [['Version', DistMap['Version']]])
120    AttributeList = [['ReadOnly', DistMap['ReadOnly']],
121                     ['RePackage', DistMap['RePackage']]]
122    NodeList = [Element1,
123                Element2,
124                ['Vendor', DistMap['Vendor']],
125                ['Date', DistMap['Date']],
126                ['Copyright', DistMap['Copyright']],
127                ['License', DistMap['License']],
128                ['Abstract', DistMap['Abstract']],
129                ['Description', DistMap['Description']],
130                ['Signature', DistMap['Signature']],
131                ['XmlSpecification', DistMap['XmlSpecification']],
132                ]
133    Root.appendChild(CreateXmlElement('DistributionHeader', '',
134                                      NodeList, AttributeList))
135
136## Create tools XML file
137#
138# @param Map: Map
139# @param Root: Root
140# @param Tag: Tag
141#
142def CreateToolsXml(Map, Root, Tag):
143    #
144    # Check if all elements in this section are empty
145    #
146    for Key in Map:
147        if len(Map[Key]) > 0:
148            break
149    else:
150        return
151
152    NodeList = [['Name', Map['Name']],
153                ['Copyright', Map['Copyright']],
154                ['License', Map['License']],
155                ['Abstract', Map['Abstract']],
156                ['Description', Map['Description']],
157               ]
158    HeaderNode = CreateXmlElement('Header', '', NodeList, [])
159    NodeList = [HeaderNode]
160
161    for File in Map['FileList']:
162        AttrList = []
163        for Key in File[1]:
164            AttrList.append([Key, File[1][Key]])
165        NodeList.append(CreateXmlElement('Filename', File[0], [], AttrList))
166    Root.appendChild(CreateXmlElement(Tag, '', NodeList, []))
167
168## ValidateValues
169#
170# @param Key: Key
171# @param Value: Value
172# @param SectionName: SectionName
173#
174def ValidateValues(Key, Value, SectionName):
175    if SectionName == 'DistributionHeader':
176        Valid, Cause = ValidateRegValues(Key, Value)
177        if not Valid:
178            return Valid, Cause
179        Valid = __ValidateDistHeader(Key, Value)
180        if not Valid:
181            return Valid, ST.ERR_VALUE_INVALID % (Key, SectionName)
182    else:
183        Valid = __ValidateOtherHeader(Key, Value)
184        if not Valid:
185            return Valid, ST.ERR_VALUE_INVALID % (Key, SectionName)
186    return True, ''
187
188## ValidateRegValues
189#
190# @param Key: Key
191# @param Value: Value
192#
193def ValidateRegValues(Key, Value):
194    ValidateMap = {
195        'ReadOnly'  :
196            ('true|false', ST.ERR_BOOLEAN_VALUE % (Key, Value)),
197        'RePackage' :
198            ('true|false', ST.ERR_BOOLEAN_VALUE % (Key, Value)),
199        'GUID'      :
200            ('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}'
201            '-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}',
202            ST.ERR_GUID_VALUE % Value),
203        'Version'   :   ('[0-9]+(\.[0-9]+)?', ST.ERR_VERSION_VALUE % \
204                         (Key, Value)),
205        'XmlSpecification' : ('1\.1', ST.ERR_VERSION_XMLSPEC % Value)
206    }
207    if Key not in ValidateMap:
208        return True, ''
209    Elem = ValidateMap[Key]
210    Match = re.compile(Elem[0]).match(Value)
211    if Match and Match.start() == 0 and Match.end() == len(Value):
212        return True, ''
213    return False, Elem[1]
214
215## __ValidateDistHeaderName
216#
217# @param Name: Name
218#
219def __ValidateDistHeaderName(Name):
220    if len(Name) < 1:
221        return False
222
223    for Char in Name:
224        if ord(Char) < 0x20 or ord(Char) >= 0x7f:
225            return False
226    return True
227
228## __ValidateDistHeaderBaseName
229#
230# @param BaseName: BaseName
231#
232def __ValidateDistHeaderBaseName(BaseName):
233    if not BaseName:
234        return False
235#    if CheckLen and len(BaseName) < 2:
236#        return False
237    if not BaseName[0].isalnum() and BaseName[0] != '_':
238        return False
239    for Char in BaseName[1:]:
240        if not Char.isalnum() and Char not in '-_':
241            return False
242    return True
243
244## __ValidateDistHeaderAbstract
245#
246# @param Abstract: Abstract
247#
248def __ValidateDistHeaderAbstract(Abstract):
249    return '\t' not in Abstract and len(Abstract.splitlines()) == 1
250
251## __ValidateOtherHeaderAbstract
252#
253# @param Abstract: Abstract
254#
255def __ValidateOtherHeaderAbstract(Abstract):
256    return __ValidateDistHeaderAbstract(Abstract)
257
258## __ValidateDistHeader
259#
260# @param Key: Key
261# @param Value: Value
262#
263def __ValidateDistHeader(Key, Value):
264    ValidateMap = {
265        'Name'      : __ValidateDistHeaderName,
266        'BaseName'  : __ValidateDistHeaderBaseName,
267        'Abstract'  : __ValidateDistHeaderAbstract,
268        'Vendor'    : __ValidateDistHeaderAbstract
269    }
270    return not (Value and Key in ValidateMap and not ValidateMap[Key](Value))
271
272## __ValidateOtherHeader
273#
274# @param Key: Key
275# @param Value: Value
276#
277def __ValidateOtherHeader(Key, Value):
278    ValidateMap = {
279        'Name'      : __ValidateDistHeaderName,
280        'Abstract'  : __ValidateOtherHeaderAbstract
281    }
282    return not (Value and Key in ValidateMap and not ValidateMap[Key](Value))
283
284## Convert ini file to xml file
285#
286# @param IniFile
287#
288def IniToXml(IniFile):
289    if not os.path.exists(IniFile):
290        Logger.Error("UPT", FILE_NOT_FOUND, ST.ERR_TEMPLATE_NOTFOUND % IniFile)
291
292    DistMap = {'ReadOnly' : '', 'RePackage' : '', 'Name' : '',
293               'BaseName' : '', 'GUID' : '', 'Version' : '', 'Vendor' : '',
294               'Date' : '', 'Copyright' : '', 'License' : '', 'Abstract' : '',
295               'Description' : '', 'Signature' : '', 'XmlSpecification' : ''
296                }
297
298    ToolsMap = {'Name' : '', 'Copyright' : '', 'License' : '',
299                'Abstract' : '', 'Description' : '', 'FileList' : []}
300    #
301    # Only FileList is a list: [['file1', {}], ['file2', {}], ...]
302    #
303    MiscMap = {'Name' : '', 'Copyright' : '', 'License' : '',
304               'Abstract' : '', 'Description' : '', 'FileList' : []}
305
306    SectionMap = {
307                   'DistributionHeader' : DistMap,
308                   'ToolsHeader' : ToolsMap,
309                   'MiscellaneousFilesHeader' : MiscMap
310                   }
311
312    PathValidator = {
313                'ToolsHeader' : ValidateToolsFile,
314                'MiscellaneousFilesHeader' : ValidateMiscFile
315                }
316
317    ParsedSection = []
318
319    SectionName = ''
320    CurrentKey = ''
321    PreMap = None
322    Map = None
323    FileContent = ConvertSpecialChar(open(IniFile, 'r').readlines())
324    LastIndex = 0
325    for Index in range(0, len(FileContent)):
326        LastIndex = Index
327        Line = FileContent[Index].strip()
328        if Line == '' or Line.startswith(';'):
329            continue
330        if Line[0] == TAB_SECTION_START and Line[-1] == TAB_SECTION_END:
331            CurrentKey = ''
332            SectionName = Line[1:-1].strip()
333            if SectionName not in SectionMap:
334                IniParseError(ST.ERR_SECTION_NAME_INVALID % SectionName,
335                      IniFile, Index+1)
336
337            if SectionName in ParsedSection:
338                IniParseError(ST.ERR_SECTION_REDEFINE % SectionName,
339                      IniFile, Index+1)
340            else:
341                ParsedSection.append(SectionName)
342
343            Map = SectionMap[SectionName]
344            continue
345        if not Map:
346            IniParseError(ST.ERR_SECTION_NAME_NONE, IniFile, Index+1)
347        TokenList = Line.split(TAB_EQUAL_SPLIT, 1)
348        TempKey = TokenList[0].strip()
349        #
350        # Value spanned multiple or same keyword appears more than one time
351        #
352        if len(TokenList) < 2 or TempKey not in Map:
353            if CurrentKey == '':
354                IniParseError(ST.ERR_KEYWORD_INVALID % TempKey,
355                              IniFile, Index+1)
356            elif CurrentKey == 'FileList':
357                #
358                # Special for FileList
359                #
360                Valid, Cause = ParseFileList(Line, Map, CurrentKey,
361                                             PathValidator[SectionName])
362                if not Valid:
363                    IniParseError(Cause, IniFile, Index+1)
364
365            else:
366                #
367                # Multiple lines for one key such as license
368                # Or if string on the left side of '=' is not a keyword
369                #
370                Map[CurrentKey] = ''.join([Map[CurrentKey], '\n', Line])
371                Valid, Cause = ValidateValues(CurrentKey,
372                                              Map[CurrentKey], SectionName)
373                if not Valid:
374                    IniParseError(Cause, IniFile, Index+1)
375            continue
376
377        if (TokenList[1].strip() == ''):
378            IniParseError(ST.ERR_EMPTY_VALUE, IniFile, Index+1)
379
380        #
381        # A keyword found
382        #
383        CurrentKey = TempKey
384        if Map[CurrentKey]:
385            IniParseError(ST.ERR_KEYWORD_REDEFINE % CurrentKey,
386                          IniFile, Index+1)
387
388        if id(Map) != id(PreMap) and Map['Copyright']:
389            PreMap = Map
390            Copyright = Map['Copyright'].lower()
391            Pos = Copyright.find('copyright')
392            if Pos == -1:
393                IniParseError(ST.ERR_COPYRIGHT_CONTENT, IniFile, Index)
394            if not Copyright[Pos + len('copyright'):].lstrip(' ').startswith('('):
395                IniParseError(ST.ERR_COPYRIGHT_CONTENT, IniFile, Index)
396
397        if CurrentKey == 'FileList':
398            Valid, Cause = ParseFileList(TokenList[1], Map, CurrentKey,
399                                         PathValidator[SectionName])
400            if not Valid:
401                IniParseError(Cause, IniFile, Index+1)
402        else:
403            Map[CurrentKey] = TokenList[1].strip()
404            Valid, Cause = ValidateValues(CurrentKey,
405                                          Map[CurrentKey], SectionName)
406            if not Valid:
407                IniParseError(Cause, IniFile, Index+1)
408
409    if id(Map) != id(PreMap) and Map['Copyright'] and 'copyright' not in Map['Copyright'].lower():
410        IniParseError(ST.ERR_COPYRIGHT_CONTENT, IniFile, LastIndex)
411
412    #
413    # Check mandatory keys
414    #
415    CheckMdtKeys(DistMap, IniFile, LastIndex,
416                 (('ToolsHeader', ToolsMap), ('MiscellaneousFilesHeader', MiscMap))
417                 )
418
419    return CreateXml(DistMap, ToolsMap, MiscMap, IniFile)
420
421
422## CheckMdtKeys
423#
424# @param MdtDistKeys: All mandatory keys
425# @param DistMap: Dist content
426# @param IniFile: Ini file
427# @param LastIndex: Last index of Ini file
428# @param Maps: Tools and Misc section name and map. (('section_name', map),*)
429#
430def CheckMdtKeys(DistMap, IniFile, LastIndex, Maps):
431    MdtDistKeys = ['Name', 'GUID', 'Version', 'Vendor', 'Copyright', 'License', 'Abstract', 'XmlSpecification']
432    for Key in MdtDistKeys:
433        if Key not in DistMap or DistMap[Key] == '':
434            IniParseError(ST.ERR_KEYWORD_MANDATORY % Key, IniFile, LastIndex+1)
435
436    if '.' not in DistMap['Version']:
437        DistMap['Version'] = DistMap['Version'] + '.0'
438
439    DistMap['Date'] = str(strftime("%Y-%m-%dT%H:%M:%S", localtime()))
440
441    #
442    # Check Tools Surface Area according to UPT Spec
443    # <Tools> {0,}
444    #     <Header> ... </Header> {0,1}
445    #     <Filename> ... </Filename> {1,}
446    # </Tools>
447    # <Header>
448    #    <Name> xs:normalizedString </Name> {1}
449    #    <Copyright> xs:string </Copyright> {0,1}
450    #    <License> xs:string </License> {0,1}
451    #    <Abstract> xs:normalizedString </Abstract> {0,1}
452    #    <Description> xs:string </Description> {0,1}
453    # </Header>
454    #
455    for Item in Maps:
456        Map = Item[1]
457        NonEmptyKey = 0
458        for Key in Map:
459            if Map[Key]:
460                NonEmptyKey += 1
461
462        if NonEmptyKey > 0 and not Map['FileList']:
463            IniParseError(ST.ERR_KEYWORD_MANDATORY % (Item[0] + '.FileList'), IniFile, LastIndex+1)
464
465        if NonEmptyKey > 0 and not Map['Name']:
466            IniParseError(ST.ERR_KEYWORD_MANDATORY % (Item[0] + '.Name'), IniFile, LastIndex+1)
467
468## CreateXml
469#
470# @param DistMap:  Dist Content
471# @param ToolsMap: Tools Content
472# @param MiscMap:  Misc Content
473# @param IniFile:  Ini File
474#
475def CreateXml(DistMap, ToolsMap, MiscMap, IniFile):
476    Attrs = [['xmlns', 'http://www.uefi.org/2011/1.1'],
477             ['xmlns:xsi', 'http:/www.w3.org/2001/XMLSchema-instance'],
478            ]
479    Root = CreateXmlElement('DistributionPackage', '', [], Attrs)
480    CreateHeaderXml(DistMap, Root)
481    CreateToolsXml(ToolsMap, Root, 'Tools')
482    CreateToolsXml(MiscMap, Root, 'MiscellaneousFiles')
483
484    FileAndExt = IniFile.rsplit('.', 1)
485    if len(FileAndExt) > 1:
486        FileName = FileAndExt[0] + '.xml'
487    else:
488        FileName = IniFile + '.xml'
489    File = open(FileName, 'w')
490
491    try:
492        File.write(Root.toprettyxml(indent = '  '))
493    finally:
494        File.close()
495    return FileName
496
497