1## @file
2#
3# Copyright (c) 2011 - 2018, Intel Corporation. All rights reserved.<BR>
4#
5# SPDX-License-Identifier: BSD-2-Clause-Patent
6#
7
8from __future__ import absolute_import
9from .message import *
10import re
11import os
12
13section_re = re.compile(r'^\[([\w., "]+)\]')
14
15class BaseINIFile(object):
16    _objs = {}
17    def __new__(cls, *args, **kwargs):
18        """Maintain only a single instance of this object
19        @return: instance of this class
20
21        """
22        if len(args) == 0: return object.__new__(cls)
23        filename = args[0]
24        parent   = None
25        if len(args) > 1:
26            parent = args[1]
27
28        key = os.path.normpath(filename)
29        if key not in cls._objs.keys():
30            cls._objs[key] = object.__new__(cls)
31
32        if parent is not None:
33            cls._objs[key].AddParent(parent)
34
35        return cls._objs[key]
36
37    def __init__(self, filename=None, parent=None):
38        self._lines    = []
39        self._sections = {}
40        self._filename = filename
41        self._globals  = []
42        self._isModify = True
43
44    def AddParent(self, parent):
45        if parent is None: return
46        if not hasattr(self, "_parents"):
47            self._parents = []
48
49        if parent in self._parents:
50            ErrorMsg("Duplicate parent is found for INI file %s" % self._filename)
51            return
52        self._parents.append(parent)
53
54    def GetFilename(self):
55        return os.path.normpath(self._filename)
56
57    def IsModified(self):
58        return self._isModify
59
60    def Modify(self, modify=True, obj=None):
61        if modify == self._isModify: return
62        self._isModify = modify
63        if modify:
64            for parent in self._parents:
65                parent.Modify(True, self)
66
67    def _ReadLines(self, filename):
68        #
69        # try to open file
70        #
71        if not os.path.exists(filename):
72            return False
73
74        try:
75            handle = open(filename, 'r')
76            self._lines  = handle.readlines()
77            handle.close()
78        except:
79            raise EdkException("Fail to open file %s" % filename)
80
81        return True
82
83    def GetSectionInstance(self, parent, name, isCombined=False):
84        return BaseINISection(parent, name, isCombined)
85
86    def GetSectionByName(self, name):
87        arr = []
88        for key in self._sections.keys():
89            if '.private' in key:
90                continue
91            for item in self._sections[key]:
92                if item.GetBaseName().lower().find(name.lower()) != -1:
93                    arr.append(item)
94        return arr
95
96    def GetSectionObjectsByName(self, name):
97        arr = []
98        sects = self.GetSectionByName(name)
99        for sect in sects:
100            for obj in sect.GetObjects():
101                arr.append(obj)
102        return arr
103
104    def Parse(self):
105        if not self._isModify: return True
106        if not self._ReadLines(self._filename): return False
107
108        sObjs    = []
109        inGlobal = True
110        # process line
111        for index in range(len(self._lines)):
112            templine = self._lines[index].strip()
113            # skip comments
114            if len(templine) == 0: continue
115            if re.match("^\[=*\]", templine) or re.match("^#", templine) or \
116               re.match("\*+/", templine):
117                continue
118
119            m = section_re.match(templine)
120            if m is not None: # found a section
121                inGlobal = False
122                # Finish the latest section first
123                if len(sObjs) != 0:
124                    for sObj in sObjs:
125                        sObj._end = index - 1
126                        if not sObj.Parse():
127                            ErrorMsg("Fail to parse section %s" % sObj.GetBaseName(),
128                                     self._filename,
129                                     sObj._start)
130
131                # start new section
132                sname_arr = m.groups()[0].split(',')
133                sObjs = []
134                for name in sname_arr:
135                    sObj = self.GetSectionInstance(self, name, (len(sname_arr) > 1))
136                    sObj._start = index
137                    sObjs.append(sObj)
138                    if name.lower() not in self._sections:
139                        self._sections[name.lower()] = [sObj]
140                    else:
141                        self._sections[name.lower()].append(sObj)
142            elif inGlobal:  # not start any section and find global object
143                gObj = BaseINIGlobalObject(self)
144                gObj._start = index
145                gObj.Parse()
146                self._globals.append(gObj)
147
148        # Finish the last section
149        if len(sObjs) != 0:
150            for sObj in sObjs:
151                sObj._end = index
152                if not sObj.Parse():
153                    ErrorMsg("Fail to parse section %s" % sObj.GetBaseName(),
154                             self._filename,
155                             sObj._start)
156
157        self._isModify = False
158        return True
159
160    def Destroy(self, parent):
161
162        # check referenced parent
163        if parent is not None:
164            assert parent in self._parents, "when destory ini object, can not found parent reference!"
165            self._parents.remove(parent)
166
167        if len(self._parents) != 0: return
168
169        for sects in self._sections.values():
170            for sect in sects:
171                sect.Destroy()
172
173        # dereference from _objs array
174        assert self.GetFilename() in self._objs.keys(), "When destroy ini object, can not find obj reference!"
175        assert self in self._objs.values(), "When destroy ini object, can not find obj reference!"
176        del self._objs[self.GetFilename()]
177
178        # dereference self
179        self.Clear()
180
181    def GetDefine(self, name):
182        sects = self.GetSectionByName('Defines')
183        for sect in sects:
184            for obj in sect.GetObjects():
185                line = obj.GetLineByOffset(obj._start).split('#')[0].strip()
186                arr = line.split('=')
187                if arr[0].strip().lower() == name.strip().lower():
188                    return arr[1].strip()
189        return None
190
191    def Clear(self):
192        for sects in self._sections.values():
193            for sect in sects:
194                del sect
195        self._sections.clear()
196        for gObj in self._globals:
197            del gObj
198
199        del self._globals[:]
200        del self._lines[:]
201
202    def Reload(self):
203        self.Clear()
204        ret = self.Parse()
205        if ret:
206            self._isModify = False
207        return ret
208
209    def AddNewSection(self, sectName):
210        if sectName.lower() in self._sections.keys():
211            ErrorMsg('Section %s can not be created for conflict with existing section')
212            return None
213
214        sectionObj = self.GetSectionInstance(self, sectName)
215        sectionObj._start = len(self._lines)
216        sectionObj._end   = len(self._lines) + 1
217        self._lines.append('[%s]\n' % sectName)
218        self._lines.append('\n\n')
219        self._sections[sectName.lower()] = sectionObj
220        return sectionObj
221
222    def CopySectionsByName(self, oldDscObj, nameStr):
223        sects = oldDscObj.GetSectionByName(nameStr)
224        for sect in sects:
225            sectObj = self.AddNewSection(sect.GetName())
226            sectObj.Copy(sect)
227
228    def __str__(self):
229        return ''.join(self._lines)
230
231    ## Get file header's comment from basic INI file.
232    #  The file comments has two style:
233    #  1) #/** @file
234    #  2) ## @file
235    #
236    def GetFileHeader(self):
237        desc = []
238        lineArr  = self._lines
239        inHeader = False
240        for num in range(len(self._lines)):
241            line = lineArr[num].strip()
242            if not inHeader and (line.startswith("#/**") or line.startswith("##")) and \
243                line.find("@file") != -1:
244                inHeader = True
245                continue
246            if inHeader and (line.startswith("#**/") or line.startswith('##')):
247                inHeader = False
248                break
249            if inHeader:
250                prefixIndex = line.find('#')
251                if prefixIndex == -1:
252                    desc.append(line)
253                else:
254                    desc.append(line[prefixIndex + 1:])
255        return '<br>\n'.join(desc)
256
257class BaseINISection(object):
258    def __init__(self, parent, name, isCombined=False):
259        self._parent     = parent
260        self._name       = name
261        self._isCombined = isCombined
262        self._start      = 0
263        self._end        = 0
264        self._objs       = []
265
266    def __del__(self):
267        for obj in self._objs:
268            del obj
269        del self._objs[:]
270
271    def GetName(self):
272        return self._name
273
274    def GetObjects(self):
275        return self._objs
276
277    def GetParent(self):
278        return self._parent
279
280    def GetStartLinenumber(self):
281        return self._start
282
283    def GetEndLinenumber(self):
284        return self._end
285
286    def GetLine(self, linenumber):
287        return self._parent._lines[linenumber]
288
289    def GetFilename(self):
290        return self._parent.GetFilename()
291
292    def GetSectionINIObject(self, parent):
293        return BaseINISectionObject(parent)
294
295    def Parse(self):
296        # skip first line in section, it is used by section name
297        visit = self._start + 1
298        iniObj = None
299        while (visit <= self._end):
300            line = self.GetLine(visit).strip()
301            if re.match("^\[=*\]", line) or re.match("^#", line) or len(line) == 0:
302                visit += 1
303                continue
304            line = line.split('#')[0].strip()
305            if iniObj is not None:
306                if line.endswith('}'):
307                    iniObj._end = visit - self._start
308                    if not iniObj.Parse():
309                        ErrorMsg("Fail to parse ini object",
310                                 self.GetFilename(),
311                                 iniObj.GetStartLinenumber())
312                    else:
313                        self._objs.append(iniObj)
314                    iniObj = None
315            else:
316                iniObj = self.GetSectionINIObject(self)
317                iniObj._start = visit - self._start
318                if not line.endswith('{'):
319                    iniObj._end = visit - self._start
320                    if not iniObj.Parse():
321                        ErrorMsg("Fail to parse ini object",
322                                 self.GetFilename(),
323                                 iniObj.GetStartLinenumber())
324                    else:
325                        self._objs.append(iniObj)
326                    iniObj = None
327            visit += 1
328        return True
329
330    def Destroy(self):
331        for obj in self._objs:
332            obj.Destroy()
333
334    def GetBaseName(self):
335        return self._name
336
337    def AddLine(self, line):
338        end = self.GetEndLinenumber()
339        self._parent._lines.insert(end, line)
340        self._end += 1
341
342    def Copy(self, sectObj):
343        index = sectObj.GetStartLinenumber() + 1
344        while index < sectObj.GetEndLinenumber():
345            line = sectObj.GetLine(index)
346            if not line.strip().startswith('#'):
347                self.AddLine(line)
348            index += 1
349
350    def AddObject(self, obj):
351        lines = obj.GenerateLines()
352        for line in lines:
353            self.AddLine(line)
354
355    def GetComment(self):
356        comments = []
357        start  = self._start - 1
358        bFound = False
359
360        while (start > 0):
361            line = self.GetLine(start).strip()
362            if len(line) == 0:
363                start -= 1
364                continue
365            if line.startswith('##'):
366                bFound = True
367                index = line.rfind('#')
368                if (index + 1) < len(line):
369                    comments.append(line[index + 1:])
370                break
371            if line.startswith('#'):
372                start -= 1
373                continue
374            break
375        if bFound:
376            end = start + 1
377            while (end < self._start):
378                line = self.GetLine(end).strip()
379                if len(line) == 0: break
380                if not line.startswith('#'): break
381                index = line.rfind('#')
382                if (index + 1) < len(line):
383                    comments.append(line[index + 1:])
384                end += 1
385        return comments
386
387class BaseINIGlobalObject(object):
388    def __init__(self, parent):
389        self._start = 0
390        self._end   = 0
391
392    def Parse(self):
393        return True
394
395    def __str__(self):
396        return parent._lines[self._start]
397
398    def __del__(self):
399        pass
400
401class BaseINISectionObject(object):
402    def __init__(self, parent):
403        self._start  = 0
404        self._end    = 0
405        self._parent = parent
406
407    def __del__(self):
408        self._parent = None
409
410    def GetParent(self):
411        return self._parent
412
413    def GetFilename(self):
414        return self.GetParent().GetFilename()
415
416    def GetPackageName(self):
417        return self.GetFilename()
418
419    def GetFileObj(self):
420        return self.GetParent().GetParent()
421
422    def GetStartLinenumber(self):
423        return self.GetParent()._start + self._start
424
425    def GetLineByOffset(self, offset):
426        sect_start = self._parent.GetStartLinenumber()
427        linenumber = sect_start + offset
428        return self._parent.GetLine(linenumber)
429
430    def GetLinenumberByOffset(self, offset):
431        return offset + self._parent.GetStartLinenumber()
432
433    def Parse(self):
434        return True
435
436    def Destroy(self):
437        pass
438
439    def __str__(self):
440        return self.GetLineByOffset(self._start).strip()
441
442    def GenerateLines(self):
443        return ['default setion object string\n']
444
445    def GetComment(self):
446        comments = []
447        start  = self.GetStartLinenumber() - 1
448        bFound = False
449
450        while (start > 0):
451            line = self.GetParent().GetLine(start).strip()
452            if len(line) == 0:
453                start -= 1
454                continue
455            if line.startswith('##'):
456                bFound = True
457                index = line.rfind('#')
458                if (index + 1) < len(line):
459                    comments.append(line[index + 1:])
460                break
461            if line.startswith('#'):
462                start -= 1
463                continue
464            break
465        if bFound:
466            end = start + 1
467            while (end <= self.GetStartLinenumber() - 1):
468                line = self.GetParent().GetLine(end).strip()
469                if len(line) == 0: break
470                if not line.startswith('#'): break
471                index = line.rfind('#')
472                if (index + 1) < len(line):
473                    comments.append(line[index + 1:])
474                end += 1
475        return comments
476