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