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