Merge https://gitlab.denx.de/u-boot/custodians/u-boot-riscv
[platform/kernel/u-boot.git] / tools / binman / entry.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2016 Google, Inc
3 #
4 # Base class for all entries
5 #
6
7 from collections import namedtuple
8 import importlib
9 import os
10 import sys
11
12 from dtoc import fdt_util
13 from patman import tools
14 from patman.tools import ToHex, ToHexSize
15 from patman import tout
16
17 modules = {}
18
19 our_path = os.path.dirname(os.path.realpath(__file__))
20
21
22 # An argument which can be passed to entries on the command line, in lieu of
23 # device-tree properties.
24 EntryArg = namedtuple('EntryArg', ['name', 'datatype'])
25
26 # Information about an entry for use when displaying summaries
27 EntryInfo = namedtuple('EntryInfo', ['indent', 'name', 'etype', 'size',
28                                      'image_pos', 'uncomp_size', 'offset',
29                                      'entry'])
30
31 class Entry(object):
32     """An Entry in the section
33
34     An entry corresponds to a single node in the device-tree description
35     of the section. Each entry ends up being a part of the final section.
36     Entries can be placed either right next to each other, or with padding
37     between them. The type of the entry determines the data that is in it.
38
39     This class is not used by itself. All entry objects are subclasses of
40     Entry.
41
42     Attributes:
43         section: Section object containing this entry
44         node: The node that created this entry
45         offset: Offset of entry within the section, None if not known yet (in
46             which case it will be calculated by Pack())
47         size: Entry size in bytes, None if not known
48         pre_reset_size: size as it was before ResetForPack(). This allows us to
49             keep track of the size we started with and detect size changes
50         uncomp_size: Size of uncompressed data in bytes, if the entry is
51             compressed, else None
52         contents_size: Size of contents in bytes, 0 by default
53         align: Entry start offset alignment, or None
54         align_size: Entry size alignment, or None
55         align_end: Entry end offset alignment, or None
56         pad_before: Number of pad bytes before the contents, 0 if none
57         pad_after: Number of pad bytes after the contents, 0 if none
58         data: Contents of entry (string of bytes)
59         compress: Compression algoithm used (e.g. 'lz4'), 'none' if none
60         orig_offset: Original offset value read from node
61         orig_size: Original size value read from node
62     """
63     def __init__(self, section, etype, node, name_prefix=''):
64         # Put this here to allow entry-docs and help to work without libfdt
65         global state
66         from binman import state
67
68         self.section = section
69         self.etype = etype
70         self._node = node
71         self.name = node and (name_prefix + node.name) or 'none'
72         self.offset = None
73         self.size = None
74         self.pre_reset_size = None
75         self.uncomp_size = None
76         self.data = None
77         self.contents_size = 0
78         self.align = None
79         self.align_size = None
80         self.align_end = None
81         self.pad_before = 0
82         self.pad_after = 0
83         self.offset_unset = False
84         self.image_pos = None
85         self._expand_size = False
86         self.compress = 'none'
87
88     @staticmethod
89     def Lookup(node_path, etype):
90         """Look up the entry class for a node.
91
92         Args:
93             node_node: Path name of Node object containing information about
94                        the entry to create (used for errors)
95             etype:   Entry type to use
96
97         Returns:
98             The entry class object if found, else None
99         """
100         # Convert something like 'u-boot@0' to 'u_boot' since we are only
101         # interested in the type.
102         module_name = etype.replace('-', '_')
103         if '@' in module_name:
104             module_name = module_name.split('@')[0]
105         module = modules.get(module_name)
106
107         # Also allow entry-type modules to be brought in from the etype directory.
108
109         # Import the module if we have not already done so.
110         if not module:
111             try:
112                 module = importlib.import_module('binman.etype.' + module_name)
113             except ImportError as e:
114                 raise ValueError("Unknown entry type '%s' in node '%s' (expected etype/%s.py, error '%s'" %
115                                  (etype, node_path, module_name, e))
116             modules[module_name] = module
117
118         # Look up the expected class name
119         return getattr(module, 'Entry_%s' % module_name)
120
121     @staticmethod
122     def Create(section, node, etype=None):
123         """Create a new entry for a node.
124
125         Args:
126             section: Section object containing this node
127             node:    Node object containing information about the entry to
128                      create
129             etype:   Entry type to use, or None to work it out (used for tests)
130
131         Returns:
132             A new Entry object of the correct type (a subclass of Entry)
133         """
134         if not etype:
135             etype = fdt_util.GetString(node, 'type', node.name)
136         obj = Entry.Lookup(node.path, etype)
137
138         # Call its constructor to get the object we want.
139         return obj(section, etype, node)
140
141     def ReadNode(self):
142         """Read entry information from the node
143
144         This must be called as the first thing after the Entry is created.
145
146         This reads all the fields we recognise from the node, ready for use.
147         """
148         if 'pos' in self._node.props:
149             self.Raise("Please use 'offset' instead of 'pos'")
150         self.offset = fdt_util.GetInt(self._node, 'offset')
151         self.size = fdt_util.GetInt(self._node, 'size')
152         self.orig_offset = fdt_util.GetInt(self._node, 'orig-offset')
153         self.orig_size = fdt_util.GetInt(self._node, 'orig-size')
154         if self.GetImage().copy_to_orig:
155             self.orig_offset = self.offset
156             self.orig_size = self.size
157
158         # These should not be set in input files, but are set in an FDT map,
159         # which is also read by this code.
160         self.image_pos = fdt_util.GetInt(self._node, 'image-pos')
161         self.uncomp_size = fdt_util.GetInt(self._node, 'uncomp-size')
162
163         self.align = fdt_util.GetInt(self._node, 'align')
164         if tools.NotPowerOfTwo(self.align):
165             raise ValueError("Node '%s': Alignment %s must be a power of two" %
166                              (self._node.path, self.align))
167         self.pad_before = fdt_util.GetInt(self._node, 'pad-before', 0)
168         self.pad_after = fdt_util.GetInt(self._node, 'pad-after', 0)
169         self.align_size = fdt_util.GetInt(self._node, 'align-size')
170         if tools.NotPowerOfTwo(self.align_size):
171             self.Raise("Alignment size %s must be a power of two" %
172                        self.align_size)
173         self.align_end = fdt_util.GetInt(self._node, 'align-end')
174         self.offset_unset = fdt_util.GetBool(self._node, 'offset-unset')
175         self.expand_size = fdt_util.GetBool(self._node, 'expand-size')
176
177     def GetDefaultFilename(self):
178         return None
179
180     def GetFdts(self):
181         """Get the device trees used by this entry
182
183         Returns:
184             Empty dict, if this entry is not a .dtb, otherwise:
185             Dict:
186                 key: Filename from this entry (without the path)
187                 value: Tuple:
188                     Fdt object for this dtb, or None if not available
189                     Filename of file containing this dtb
190         """
191         return {}
192
193     def ExpandEntries(self):
194         pass
195
196     def AddMissingProperties(self):
197         """Add new properties to the device tree as needed for this entry"""
198         for prop in ['offset', 'size', 'image-pos']:
199             if not prop in self._node.props:
200                 state.AddZeroProp(self._node, prop)
201         if self.GetImage().allow_repack:
202             if self.orig_offset is not None:
203                 state.AddZeroProp(self._node, 'orig-offset', True)
204             if self.orig_size is not None:
205                 state.AddZeroProp(self._node, 'orig-size', True)
206
207         if self.compress != 'none':
208             state.AddZeroProp(self._node, 'uncomp-size')
209         err = state.CheckAddHashProp(self._node)
210         if err:
211             self.Raise(err)
212
213     def SetCalculatedProperties(self):
214         """Set the value of device-tree properties calculated by binman"""
215         state.SetInt(self._node, 'offset', self.offset)
216         state.SetInt(self._node, 'size', self.size)
217         base = self.section.GetRootSkipAtStart() if self.section else 0
218         state.SetInt(self._node, 'image-pos', self.image_pos - base)
219         if self.GetImage().allow_repack:
220             if self.orig_offset is not None:
221                 state.SetInt(self._node, 'orig-offset', self.orig_offset, True)
222             if self.orig_size is not None:
223                 state.SetInt(self._node, 'orig-size', self.orig_size, True)
224         if self.uncomp_size is not None:
225             state.SetInt(self._node, 'uncomp-size', self.uncomp_size)
226         state.CheckSetHashValue(self._node, self.GetData)
227
228     def ProcessFdt(self, fdt):
229         """Allow entries to adjust the device tree
230
231         Some entries need to adjust the device tree for their purposes. This
232         may involve adding or deleting properties.
233
234         Returns:
235             True if processing is complete
236             False if processing could not be completed due to a dependency.
237                 This will cause the entry to be retried after others have been
238                 called
239         """
240         return True
241
242     def SetPrefix(self, prefix):
243         """Set the name prefix for a node
244
245         Args:
246             prefix: Prefix to set, or '' to not use a prefix
247         """
248         if prefix:
249             self.name = prefix + self.name
250
251     def SetContents(self, data):
252         """Set the contents of an entry
253
254         This sets both the data and content_size properties
255
256         Args:
257             data: Data to set to the contents (bytes)
258         """
259         self.data = data
260         self.contents_size = len(self.data)
261
262     def ProcessContentsUpdate(self, data):
263         """Update the contents of an entry, after the size is fixed
264
265         This checks that the new data is the same size as the old. If the size
266         has changed, this triggers a re-run of the packing algorithm.
267
268         Args:
269             data: Data to set to the contents (bytes)
270
271         Raises:
272             ValueError if the new data size is not the same as the old
273         """
274         size_ok = True
275         new_size = len(data)
276         if state.AllowEntryExpansion() and new_size > self.contents_size:
277             # self.data will indicate the new size needed
278             size_ok = False
279         elif state.AllowEntryContraction() and new_size < self.contents_size:
280             size_ok = False
281
282         # If not allowed to change, try to deal with it or give up
283         if size_ok:
284             if new_size > self.contents_size:
285                 self.Raise('Cannot update entry size from %d to %d' %
286                         (self.contents_size, new_size))
287
288             # Don't let the data shrink. Pad it if necessary
289             if size_ok and new_size < self.contents_size:
290                 data += tools.GetBytes(0, self.contents_size - new_size)
291
292         if not size_ok:
293             tout.Debug("Entry '%s' size change from %s to %s" % (
294                 self._node.path, ToHex(self.contents_size),
295                 ToHex(new_size)))
296         self.SetContents(data)
297         return size_ok
298
299     def ObtainContents(self):
300         """Figure out the contents of an entry.
301
302         Returns:
303             True if the contents were found, False if another call is needed
304             after the other entries are processed.
305         """
306         # No contents by default: subclasses can implement this
307         return True
308
309     def ResetForPack(self):
310         """Reset offset/size fields so that packing can be done again"""
311         self.Detail('ResetForPack: offset %s->%s, size %s->%s' %
312                     (ToHex(self.offset), ToHex(self.orig_offset),
313                      ToHex(self.size), ToHex(self.orig_size)))
314         self.pre_reset_size = self.size
315         self.offset = self.orig_offset
316         self.size = self.orig_size
317
318     def Pack(self, offset):
319         """Figure out how to pack the entry into the section
320
321         Most of the time the entries are not fully specified. There may be
322         an alignment but no size. In that case we take the size from the
323         contents of the entry.
324
325         If an entry has no hard-coded offset, it will be placed at @offset.
326
327         Once this function is complete, both the offset and size of the
328         entry will be know.
329
330         Args:
331             Current section offset pointer
332
333         Returns:
334             New section offset pointer (after this entry)
335         """
336         self.Detail('Packing: offset=%s, size=%s, content_size=%x' %
337                     (ToHex(self.offset), ToHex(self.size),
338                      self.contents_size))
339         if self.offset is None:
340             if self.offset_unset:
341                 self.Raise('No offset set with offset-unset: should another '
342                            'entry provide this correct offset?')
343             self.offset = tools.Align(offset, self.align)
344         needed = self.pad_before + self.contents_size + self.pad_after
345         needed = tools.Align(needed, self.align_size)
346         size = self.size
347         if not size:
348             size = needed
349         new_offset = self.offset + size
350         aligned_offset = tools.Align(new_offset, self.align_end)
351         if aligned_offset != new_offset:
352             size = aligned_offset - self.offset
353             new_offset = aligned_offset
354
355         if not self.size:
356             self.size = size
357
358         if self.size < needed:
359             self.Raise("Entry contents size is %#x (%d) but entry size is "
360                        "%#x (%d)" % (needed, needed, self.size, self.size))
361         # Check that the alignment is correct. It could be wrong if the
362         # and offset or size values were provided (i.e. not calculated), but
363         # conflict with the provided alignment values
364         if self.size != tools.Align(self.size, self.align_size):
365             self.Raise("Size %#x (%d) does not match align-size %#x (%d)" %
366                   (self.size, self.size, self.align_size, self.align_size))
367         if self.offset != tools.Align(self.offset, self.align):
368             self.Raise("Offset %#x (%d) does not match align %#x (%d)" %
369                   (self.offset, self.offset, self.align, self.align))
370         self.Detail('   - packed: offset=%#x, size=%#x, content_size=%#x, next_offset=%x' %
371                     (self.offset, self.size, self.contents_size, new_offset))
372
373         return new_offset
374
375     def Raise(self, msg):
376         """Convenience function to raise an error referencing a node"""
377         raise ValueError("Node '%s': %s" % (self._node.path, msg))
378
379     def Detail(self, msg):
380         """Convenience function to log detail referencing a node"""
381         tag = "Node '%s'" % self._node.path
382         tout.Detail('%30s: %s' % (tag, msg))
383
384     def GetEntryArgsOrProps(self, props, required=False):
385         """Return the values of a set of properties
386
387         Args:
388             props: List of EntryArg objects
389
390         Raises:
391             ValueError if a property is not found
392         """
393         values = []
394         missing = []
395         for prop in props:
396             python_prop = prop.name.replace('-', '_')
397             if hasattr(self, python_prop):
398                 value = getattr(self, python_prop)
399             else:
400                 value = None
401             if value is None:
402                 value = self.GetArg(prop.name, prop.datatype)
403             if value is None and required:
404                 missing.append(prop.name)
405             values.append(value)
406         if missing:
407             self.Raise('Missing required properties/entry args: %s' %
408                        (', '.join(missing)))
409         return values
410
411     def GetPath(self):
412         """Get the path of a node
413
414         Returns:
415             Full path of the node for this entry
416         """
417         return self._node.path
418
419     def GetData(self):
420         self.Detail('GetData: size %s' % ToHexSize(self.data))
421         return self.data
422
423     def GetOffsets(self):
424         """Get the offsets for siblings
425
426         Some entry types can contain information about the position or size of
427         other entries. An example of this is the Intel Flash Descriptor, which
428         knows where the Intel Management Engine section should go.
429
430         If this entry knows about the position of other entries, it can specify
431         this by returning values here
432
433         Returns:
434             Dict:
435                 key: Entry type
436                 value: List containing position and size of the given entry
437                     type. Either can be None if not known
438         """
439         return {}
440
441     def SetOffsetSize(self, offset, size):
442         """Set the offset and/or size of an entry
443
444         Args:
445             offset: New offset, or None to leave alone
446             size: New size, or None to leave alone
447         """
448         if offset is not None:
449             self.offset = offset
450         if size is not None:
451             self.size = size
452
453     def SetImagePos(self, image_pos):
454         """Set the position in the image
455
456         Args:
457             image_pos: Position of this entry in the image
458         """
459         self.image_pos = image_pos + self.offset
460
461     def ProcessContents(self):
462         """Do any post-packing updates of entry contents
463
464         This function should call ProcessContentsUpdate() to update the entry
465         contents, if necessary, returning its return value here.
466
467         Args:
468             data: Data to set to the contents (bytes)
469
470         Returns:
471             True if the new data size is OK, False if expansion is needed
472
473         Raises:
474             ValueError if the new data size is not the same as the old and
475                 state.AllowEntryExpansion() is False
476         """
477         return True
478
479     def WriteSymbols(self, section):
480         """Write symbol values into binary files for access at run time
481
482         Args:
483           section: Section containing the entry
484         """
485         pass
486
487     def CheckOffset(self):
488         """Check that the entry offsets are correct
489
490         This is used for entries which have extra offset requirements (other
491         than having to be fully inside their section). Sub-classes can implement
492         this function and raise if there is a problem.
493         """
494         pass
495
496     @staticmethod
497     def GetStr(value):
498         if value is None:
499             return '<none>  '
500         return '%08x' % value
501
502     @staticmethod
503     def WriteMapLine(fd, indent, name, offset, size, image_pos):
504         print('%s  %s%s  %s  %s' % (Entry.GetStr(image_pos), ' ' * indent,
505                                     Entry.GetStr(offset), Entry.GetStr(size),
506                                     name), file=fd)
507
508     def WriteMap(self, fd, indent):
509         """Write a map of the entry to a .map file
510
511         Args:
512             fd: File to write the map to
513             indent: Curent indent level of map (0=none, 1=one level, etc.)
514         """
515         self.WriteMapLine(fd, indent, self.name, self.offset, self.size,
516                           self.image_pos)
517
518     def GetEntries(self):
519         """Return a list of entries contained by this entry
520
521         Returns:
522             List of entries, or None if none. A normal entry has no entries
523                 within it so will return None
524         """
525         return None
526
527     def GetArg(self, name, datatype=str):
528         """Get the value of an entry argument or device-tree-node property
529
530         Some node properties can be provided as arguments to binman. First check
531         the entry arguments, and fall back to the device tree if not found
532
533         Args:
534             name: Argument name
535             datatype: Data type (str or int)
536
537         Returns:
538             Value of argument as a string or int, or None if no value
539
540         Raises:
541             ValueError if the argument cannot be converted to in
542         """
543         value = state.GetEntryArg(name)
544         if value is not None:
545             if datatype == int:
546                 try:
547                     value = int(value)
548                 except ValueError:
549                     self.Raise("Cannot convert entry arg '%s' (value '%s') to integer" %
550                                (name, value))
551             elif datatype == str:
552                 pass
553             else:
554                 raise ValueError("GetArg() internal error: Unknown data type '%s'" %
555                                  datatype)
556         else:
557             value = fdt_util.GetDatatype(self._node, name, datatype)
558         return value
559
560     @staticmethod
561     def WriteDocs(modules, test_missing=None):
562         """Write out documentation about the various entry types to stdout
563
564         Args:
565             modules: List of modules to include
566             test_missing: Used for testing. This is a module to report
567                 as missing
568         """
569         print('''Binman Entry Documentation
570 ===========================
571
572 This file describes the entry types supported by binman. These entry types can
573 be placed in an image one by one to build up a final firmware image. It is
574 fairly easy to create new entry types. Just add a new file to the 'etype'
575 directory. You can use the existing entries as examples.
576
577 Note that some entries are subclasses of others, using and extending their
578 features to produce new behaviours.
579
580
581 ''')
582         modules = sorted(modules)
583
584         # Don't show the test entry
585         if '_testing' in modules:
586             modules.remove('_testing')
587         missing = []
588         for name in modules:
589             module = Entry.Lookup('WriteDocs', name)
590             docs = getattr(module, '__doc__')
591             if test_missing == name:
592                 docs = None
593             if docs:
594                 lines = docs.splitlines()
595                 first_line = lines[0]
596                 rest = [line[4:] for line in lines[1:]]
597                 hdr = 'Entry: %s: %s' % (name.replace('_', '-'), first_line)
598                 print(hdr)
599                 print('-' * len(hdr))
600                 print('\n'.join(rest))
601                 print()
602                 print()
603             else:
604                 missing.append(name)
605
606         if missing:
607             raise ValueError('Documentation is missing for modules: %s' %
608                              ', '.join(missing))
609
610     def GetUniqueName(self):
611         """Get a unique name for a node
612
613         Returns:
614             String containing a unique name for a node, consisting of the name
615             of all ancestors (starting from within the 'binman' node) separated
616             by a dot ('.'). This can be useful for generating unique filesnames
617             in the output directory.
618         """
619         name = self.name
620         node = self._node
621         while node.parent:
622             node = node.parent
623             if node.name == 'binman':
624                 break
625             name = '%s.%s' % (node.name, name)
626         return name
627
628     def ExpandToLimit(self, limit):
629         """Expand an entry so that it ends at the given offset limit"""
630         if self.offset + self.size < limit:
631             self.size = limit - self.offset
632             # Request the contents again, since changing the size requires that
633             # the data grows. This should not fail, but check it to be sure.
634             if not self.ObtainContents():
635                 self.Raise('Cannot obtain contents when expanding entry')
636
637     def HasSibling(self, name):
638         """Check if there is a sibling of a given name
639
640         Returns:
641             True if there is an entry with this name in the the same section,
642                 else False
643         """
644         return name in self.section.GetEntries()
645
646     def GetSiblingImagePos(self, name):
647         """Return the image position of the given sibling
648
649         Returns:
650             Image position of sibling, or None if the sibling has no position,
651                 or False if there is no such sibling
652         """
653         if not self.HasSibling(name):
654             return False
655         return self.section.GetEntries()[name].image_pos
656
657     @staticmethod
658     def AddEntryInfo(entries, indent, name, etype, size, image_pos,
659                      uncomp_size, offset, entry):
660         """Add a new entry to the entries list
661
662         Args:
663             entries: List (of EntryInfo objects) to add to
664             indent: Current indent level to add to list
665             name: Entry name (string)
666             etype: Entry type (string)
667             size: Entry size in bytes (int)
668             image_pos: Position within image in bytes (int)
669             uncomp_size: Uncompressed size if the entry uses compression, else
670                 None
671             offset: Entry offset within parent in bytes (int)
672             entry: Entry object
673         """
674         entries.append(EntryInfo(indent, name, etype, size, image_pos,
675                                  uncomp_size, offset, entry))
676
677     def ListEntries(self, entries, indent):
678         """Add files in this entry to the list of entries
679
680         This can be overridden by subclasses which need different behaviour.
681
682         Args:
683             entries: List (of EntryInfo objects) to add to
684             indent: Current indent level to add to list
685         """
686         self.AddEntryInfo(entries, indent, self.name, self.etype, self.size,
687                           self.image_pos, self.uncomp_size, self.offset, self)
688
689     def ReadData(self, decomp=True):
690         """Read the data for an entry from the image
691
692         This is used when the image has been read in and we want to extract the
693         data for a particular entry from that image.
694
695         Args:
696             decomp: True to decompress any compressed data before returning it;
697                 False to return the raw, uncompressed data
698
699         Returns:
700             Entry data (bytes)
701         """
702         # Use True here so that we get an uncompressed section to work from,
703         # although compressed sections are currently not supported
704         tout.Debug("ReadChildData section '%s', entry '%s'" %
705                    (self.section.GetPath(), self.GetPath()))
706         data = self.section.ReadChildData(self, decomp)
707         return data
708
709     def ReadChildData(self, child, decomp=True):
710         """Read the data for a particular child entry
711
712         This reads data from the parent and extracts the piece that relates to
713         the given child.
714
715         Args:
716             child: Child entry to read data for (must be valid)
717             decomp: True to decompress any compressed data before returning it;
718                 False to return the raw, uncompressed data
719
720         Returns:
721             Data for the child (bytes)
722         """
723         pass
724
725     def LoadData(self, decomp=True):
726         data = self.ReadData(decomp)
727         self.contents_size = len(data)
728         self.ProcessContentsUpdate(data)
729         self.Detail('Loaded data size %x' % len(data))
730
731     def GetImage(self):
732         """Get the image containing this entry
733
734         Returns:
735             Image object containing this entry
736         """
737         return self.section.GetImage()
738
739     def WriteData(self, data, decomp=True):
740         """Write the data to an entry in the image
741
742         This is used when the image has been read in and we want to replace the
743         data for a particular entry in that image.
744
745         The image must be re-packed and written out afterwards.
746
747         Args:
748             data: Data to replace it with
749             decomp: True to compress the data if needed, False if data is
750                 already compressed so should be used as is
751
752         Returns:
753             True if the data did not result in a resize of this entry, False if
754                  the entry must be resized
755         """
756         if self.size is not None:
757             self.contents_size = self.size
758         else:
759             self.contents_size = self.pre_reset_size
760         ok = self.ProcessContentsUpdate(data)
761         self.Detail('WriteData: size=%x, ok=%s' % (len(data), ok))
762         section_ok = self.section.WriteChildData(self)
763         return ok and section_ok
764
765     def WriteChildData(self, child):
766         """Handle writing the data in a child entry
767
768         This should be called on the child's parent section after the child's
769         data has been updated. It
770
771         This base-class implementation does nothing, since the base Entry object
772         does not have any children.
773
774         Args:
775             child: Child Entry that was written
776
777         Returns:
778             True if the section could be updated successfully, False if the
779                 data is such that the section could not updat
780         """
781         return True
782
783     def GetSiblingOrder(self):
784         """Get the relative order of an entry amoung its siblings
785
786         Returns:
787             'start' if this entry is first among siblings, 'end' if last,
788                 otherwise None
789         """
790         entries = list(self.section.GetEntries().values())
791         if entries:
792             if self == entries[0]:
793                 return 'start'
794             elif self == entries[-1]:
795                 return 'end'
796         return 'middle'