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