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