0cd5b8b9f118700366043cf341635c1897a04cb6
[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 pathlib
11 import sys
12 import time
13
14 from binman import bintool
15 from dtoc import fdt_util
16 from patman import tools
17 from patman.tools import to_hex, to_hex_size
18 from patman import tout
19
20 modules = {}
21
22 # This is imported if needed
23 state = None
24
25 # An argument which can be passed to entries on the command line, in lieu of
26 # device-tree properties.
27 EntryArg = namedtuple('EntryArg', ['name', 'datatype'])
28
29 # Information about an entry for use when displaying summaries
30 EntryInfo = namedtuple('EntryInfo', ['indent', 'name', 'etype', 'size',
31                                      'image_pos', 'uncomp_size', 'offset',
32                                      'entry'])
33
34 class Entry(object):
35     """An Entry in the section
36
37     An entry corresponds to a single node in the device-tree description
38     of the section. Each entry ends up being a part of the final section.
39     Entries can be placed either right next to each other, or with padding
40     between them. The type of the entry determines the data that is in it.
41
42     This class is not used by itself. All entry objects are subclasses of
43     Entry.
44
45     Attributes:
46         section: Section object containing this entry
47         node: The node that created this entry
48         offset: Offset of entry within the section, None if not known yet (in
49             which case it will be calculated by Pack())
50         size: Entry size in bytes, None if not known
51         pre_reset_size: size as it was before ResetForPack(). This allows us to
52             keep track of the size we started with and detect size changes
53         uncomp_size: Size of uncompressed data in bytes, if the entry is
54             compressed, else None
55         contents_size: Size of contents in bytes, 0 by default
56         align: Entry start offset alignment relative to the start of the
57             containing section, or None
58         align_size: Entry size alignment, or None
59         align_end: Entry end offset alignment relative to the start of the
60             containing section, or None
61         pad_before: Number of pad bytes before the contents when it is placed
62             in the containing section, 0 if none. The pad bytes become part of
63             the entry.
64         pad_after: Number of pad bytes after the contents when it is placed in
65             the containing section, 0 if none. The pad bytes become part of
66             the entry.
67         data: Contents of entry (string of bytes). This does not include
68             padding created by pad_before or pad_after. If the entry is
69             compressed, this contains the compressed data.
70         uncomp_data: Original uncompressed data, if this entry is compressed,
71             else None
72         compress: Compression algoithm used (e.g. 'lz4'), 'none' if none
73         orig_offset: Original offset value read from node
74         orig_size: Original size value read from node
75         missing: True if this entry is missing its contents
76         allow_missing: Allow children of this entry to be missing (used by
77             subclasses such as Entry_section)
78         allow_fake: Allow creating a dummy fake file if the blob file is not
79             available. This is mainly used for testing.
80         external: True if this entry contains an external binary blob
81         bintools: Bintools used by this entry (only populated for Image)
82         missing_bintools: List of missing bintools for this entry
83         update_hash: True if this entry's "hash" subnode should be
84             updated with a hash of the entry contents
85         comp_bintool: Bintools used for compress and decompress data
86         fake_fname: Fake filename, if one was created, else None
87         required_props (dict of str): Properties which must be present. This can
88             be added to by subclasses
89     """
90     fake_dir = None
91
92     def __init__(self, section, etype, node, name_prefix=''):
93         # Put this here to allow entry-docs and help to work without libfdt
94         global state
95         from binman import state
96
97         self.section = section
98         self.etype = etype
99         self._node = node
100         self.name = node and (name_prefix + node.name) or 'none'
101         self.offset = None
102         self.size = None
103         self.pre_reset_size = None
104         self.uncomp_size = None
105         self.data = None
106         self.uncomp_data = None
107         self.contents_size = 0
108         self.align = None
109         self.align_size = None
110         self.align_end = None
111         self.pad_before = 0
112         self.pad_after = 0
113         self.offset_unset = False
114         self.image_pos = None
115         self.extend_size = False
116         self.compress = 'none'
117         self.missing = False
118         self.faked = False
119         self.external = False
120         self.allow_missing = False
121         self.allow_fake = False
122         self.bintools = {}
123         self.missing_bintools = []
124         self.update_hash = True
125         self.fake_fname = None
126         self.required_props = []
127         self.comp_bintool = None
128
129     @staticmethod
130     def FindEntryClass(etype, expanded):
131         """Look up the entry class for a node.
132
133         Args:
134             node_node: Path name of Node object containing information about
135                        the entry to create (used for errors)
136             etype:   Entry type to use
137             expanded: Use the expanded version of etype
138
139         Returns:
140             The entry class object if found, else None if not found and expanded
141                 is True, else a tuple:
142                     module name that could not be found
143                     exception received
144         """
145         # Convert something like 'u-boot@0' to 'u_boot' since we are only
146         # interested in the type.
147         module_name = etype.replace('-', '_')
148
149         if '@' in module_name:
150             module_name = module_name.split('@')[0]
151         if expanded:
152             module_name += '_expanded'
153         module = modules.get(module_name)
154
155         # Also allow entry-type modules to be brought in from the etype directory.
156
157         # Import the module if we have not already done so.
158         if not module:
159             try:
160                 module = importlib.import_module('binman.etype.' + module_name)
161             except ImportError as e:
162                 if expanded:
163                     return None
164                 return module_name, e
165             modules[module_name] = module
166
167         # Look up the expected class name
168         return getattr(module, 'Entry_%s' % module_name)
169
170     @staticmethod
171     def Lookup(node_path, etype, expanded, missing_etype=False):
172         """Look up the entry class for a node.
173
174         Args:
175             node_node (str): Path name of Node object containing information
176                 about the entry to create (used for errors)
177             etype (str):   Entry type to use
178             expanded (bool): Use the expanded version of etype
179             missing_etype (bool): True to default to a blob etype if the
180                 requested etype is not found
181
182         Returns:
183             The entry class object if found, else None if not found and expanded
184                 is True
185
186         Raise:
187             ValueError if expanded is False and the class is not found
188         """
189         # Convert something like 'u-boot@0' to 'u_boot' since we are only
190         # interested in the type.
191         cls = Entry.FindEntryClass(etype, expanded)
192         if cls is None:
193             return None
194         elif isinstance(cls, tuple):
195             if missing_etype:
196                 cls = Entry.FindEntryClass('blob', False)
197             if isinstance(cls, tuple): # This should not fail
198                 module_name, e = cls
199                 raise ValueError(
200                     "Unknown entry type '%s' in node '%s' (expected etype/%s.py, error '%s'" %
201                     (etype, node_path, module_name, e))
202         return cls
203
204     @staticmethod
205     def Create(section, node, etype=None, expanded=False, missing_etype=False):
206         """Create a new entry for a node.
207
208         Args:
209             section (entry_Section):  Section object containing this node
210             node (Node): Node object containing information about the entry to
211                 create
212             etype (str): Entry type to use, or None to work it out (used for
213                 tests)
214             expanded (bool): Use the expanded version of etype
215             missing_etype (bool): True to default to a blob etype if the
216                 requested etype is not found
217
218         Returns:
219             A new Entry object of the correct type (a subclass of Entry)
220         """
221         if not etype:
222             etype = fdt_util.GetString(node, 'type', node.name)
223         obj = Entry.Lookup(node.path, etype, expanded, missing_etype)
224         if obj and expanded:
225             # Check whether to use the expanded entry
226             new_etype = etype + '-expanded'
227             can_expand = not fdt_util.GetBool(node, 'no-expanded')
228             if can_expand and obj.UseExpanded(node, etype, new_etype):
229                 etype = new_etype
230             else:
231                 obj = None
232         if not obj:
233             obj = Entry.Lookup(node.path, etype, False, missing_etype)
234
235         # Call its constructor to get the object we want.
236         return obj(section, etype, node)
237
238     def ReadNode(self):
239         """Read entry information from the node
240
241         This must be called as the first thing after the Entry is created.
242
243         This reads all the fields we recognise from the node, ready for use.
244         """
245         self.ensure_props()
246         if 'pos' in self._node.props:
247             self.Raise("Please use 'offset' instead of 'pos'")
248         if 'expand-size' in self._node.props:
249             self.Raise("Please use 'extend-size' instead of 'expand-size'")
250         self.offset = fdt_util.GetInt(self._node, 'offset')
251         self.size = fdt_util.GetInt(self._node, 'size')
252         self.orig_offset = fdt_util.GetInt(self._node, 'orig-offset')
253         self.orig_size = fdt_util.GetInt(self._node, 'orig-size')
254         if self.GetImage().copy_to_orig:
255             self.orig_offset = self.offset
256             self.orig_size = self.size
257
258         # These should not be set in input files, but are set in an FDT map,
259         # which is also read by this code.
260         self.image_pos = fdt_util.GetInt(self._node, 'image-pos')
261         self.uncomp_size = fdt_util.GetInt(self._node, 'uncomp-size')
262
263         self.align = fdt_util.GetInt(self._node, 'align')
264         if tools.not_power_of_two(self.align):
265             raise ValueError("Node '%s': Alignment %s must be a power of two" %
266                              (self._node.path, self.align))
267         if self.section and self.align is None:
268             self.align = self.section.align_default
269         self.pad_before = fdt_util.GetInt(self._node, 'pad-before', 0)
270         self.pad_after = fdt_util.GetInt(self._node, 'pad-after', 0)
271         self.align_size = fdt_util.GetInt(self._node, 'align-size')
272         if tools.not_power_of_two(self.align_size):
273             self.Raise("Alignment size %s must be a power of two" %
274                        self.align_size)
275         self.align_end = fdt_util.GetInt(self._node, 'align-end')
276         self.offset_unset = fdt_util.GetBool(self._node, 'offset-unset')
277         self.extend_size = fdt_util.GetBool(self._node, 'extend-size')
278         self.missing_msg = fdt_util.GetString(self._node, 'missing-msg')
279
280         # This is only supported by blobs and sections at present
281         self.compress = fdt_util.GetString(self._node, 'compress', 'none')
282
283     def GetDefaultFilename(self):
284         return None
285
286     def GetFdts(self):
287         """Get the device trees used by this entry
288
289         Returns:
290             Empty dict, if this entry is not a .dtb, otherwise:
291             Dict:
292                 key: Filename from this entry (without the path)
293                 value: Tuple:
294                     Entry object for this dtb
295                     Filename of file containing this dtb
296         """
297         return {}
298
299     def gen_entries(self):
300         """Allow entries to generate other entries
301
302         Some entries generate subnodes automatically, from which sub-entries
303         are then created. This method allows those to be added to the binman
304         definition for the current image. An entry which implements this method
305         should call state.AddSubnode() to add a subnode and can add properties
306         with state.AddString(), etc.
307
308         An example is 'files', which produces a section containing a list of
309         files.
310         """
311         pass
312
313     def AddMissingProperties(self, have_image_pos):
314         """Add new properties to the device tree as needed for this entry
315
316         Args:
317             have_image_pos: True if this entry has an image position. This can
318                 be False if its parent section is compressed, since compression
319                 groups all entries together into a compressed block of data,
320                 obscuring the start of each individual child entry
321         """
322         for prop in ['offset', 'size']:
323             if not prop in self._node.props:
324                 state.AddZeroProp(self._node, prop)
325         if have_image_pos and 'image-pos' not in self._node.props:
326             state.AddZeroProp(self._node, 'image-pos')
327         if self.GetImage().allow_repack:
328             if self.orig_offset is not None:
329                 state.AddZeroProp(self._node, 'orig-offset', True)
330             if self.orig_size is not None:
331                 state.AddZeroProp(self._node, 'orig-size', True)
332
333         if self.compress != 'none':
334             state.AddZeroProp(self._node, 'uncomp-size')
335
336         if self.update_hash:
337             err = state.CheckAddHashProp(self._node)
338             if err:
339                 self.Raise(err)
340
341     def SetCalculatedProperties(self):
342         """Set the value of device-tree properties calculated by binman"""
343         state.SetInt(self._node, 'offset', self.offset)
344         state.SetInt(self._node, 'size', self.size)
345         base = self.section.GetRootSkipAtStart() if self.section else 0
346         if self.image_pos is not None:
347             state.SetInt(self._node, 'image-pos', self.image_pos - base)
348         if self.GetImage().allow_repack:
349             if self.orig_offset is not None:
350                 state.SetInt(self._node, 'orig-offset', self.orig_offset, True)
351             if self.orig_size is not None:
352                 state.SetInt(self._node, 'orig-size', self.orig_size, True)
353         if self.uncomp_size is not None:
354             state.SetInt(self._node, 'uncomp-size', self.uncomp_size)
355
356         if self.update_hash:
357             state.CheckSetHashValue(self._node, self.GetData)
358
359     def ProcessFdt(self, fdt):
360         """Allow entries to adjust the device tree
361
362         Some entries need to adjust the device tree for their purposes. This
363         may involve adding or deleting properties.
364
365         Returns:
366             True if processing is complete
367             False if processing could not be completed due to a dependency.
368                 This will cause the entry to be retried after others have been
369                 called
370         """
371         return True
372
373     def SetPrefix(self, prefix):
374         """Set the name prefix for a node
375
376         Args:
377             prefix: Prefix to set, or '' to not use a prefix
378         """
379         if prefix:
380             self.name = prefix + self.name
381
382     def SetContents(self, data):
383         """Set the contents of an entry
384
385         This sets both the data and content_size properties
386
387         Args:
388             data: Data to set to the contents (bytes)
389         """
390         self.data = data
391         self.contents_size = len(self.data)
392
393     def ProcessContentsUpdate(self, data):
394         """Update the contents of an entry, after the size is fixed
395
396         This checks that the new data is the same size as the old. If the size
397         has changed, this triggers a re-run of the packing algorithm.
398
399         Args:
400             data: Data to set to the contents (bytes)
401
402         Raises:
403             ValueError if the new data size is not the same as the old
404         """
405         size_ok = True
406         new_size = len(data)
407         if state.AllowEntryExpansion() and new_size > self.contents_size:
408             # self.data will indicate the new size needed
409             size_ok = False
410         elif state.AllowEntryContraction() and new_size < self.contents_size:
411             size_ok = False
412
413         # If not allowed to change, try to deal with it or give up
414         if size_ok:
415             if new_size > self.contents_size:
416                 self.Raise('Cannot update entry size from %d to %d' %
417                         (self.contents_size, new_size))
418
419             # Don't let the data shrink. Pad it if necessary
420             if size_ok and new_size < self.contents_size:
421                 data += tools.get_bytes(0, self.contents_size - new_size)
422
423         if not size_ok:
424             tout.debug("Entry '%s' size change from %s to %s" % (
425                 self._node.path, to_hex(self.contents_size),
426                 to_hex(new_size)))
427         self.SetContents(data)
428         return size_ok
429
430     def ObtainContents(self, skip_entry=None, fake_size=0):
431         """Figure out the contents of an entry.
432
433         Args:
434             skip_entry (Entry): Entry to skip when obtaining section contents
435             fake_size (int): Size of fake file to create if needed
436
437         Returns:
438             True if the contents were found, False if another call is needed
439             after the other entries are processed.
440         """
441         # No contents by default: subclasses can implement this
442         return True
443
444     def ResetForPack(self):
445         """Reset offset/size fields so that packing can be done again"""
446         self.Detail('ResetForPack: offset %s->%s, size %s->%s' %
447                     (to_hex(self.offset), to_hex(self.orig_offset),
448                      to_hex(self.size), to_hex(self.orig_size)))
449         self.pre_reset_size = self.size
450         self.offset = self.orig_offset
451         self.size = self.orig_size
452
453     def Pack(self, offset):
454         """Figure out how to pack the entry into the section
455
456         Most of the time the entries are not fully specified. There may be
457         an alignment but no size. In that case we take the size from the
458         contents of the entry.
459
460         If an entry has no hard-coded offset, it will be placed at @offset.
461
462         Once this function is complete, both the offset and size of the
463         entry will be know.
464
465         Args:
466             Current section offset pointer
467
468         Returns:
469             New section offset pointer (after this entry)
470         """
471         self.Detail('Packing: offset=%s, size=%s, content_size=%x' %
472                     (to_hex(self.offset), to_hex(self.size),
473                      self.contents_size))
474         if self.offset is None:
475             if self.offset_unset:
476                 self.Raise('No offset set with offset-unset: should another '
477                            'entry provide this correct offset?')
478             self.offset = tools.align(offset, self.align)
479         needed = self.pad_before + self.contents_size + self.pad_after
480         needed = tools.align(needed, self.align_size)
481         size = self.size
482         if not size:
483             size = needed
484         new_offset = self.offset + size
485         aligned_offset = tools.align(new_offset, self.align_end)
486         if aligned_offset != new_offset:
487             size = aligned_offset - self.offset
488             new_offset = aligned_offset
489
490         if not self.size:
491             self.size = size
492
493         if self.size < needed:
494             self.Raise("Entry contents size is %#x (%d) but entry size is "
495                        "%#x (%d)" % (needed, needed, self.size, self.size))
496         # Check that the alignment is correct. It could be wrong if the
497         # and offset or size values were provided (i.e. not calculated), but
498         # conflict with the provided alignment values
499         if self.size != tools.align(self.size, self.align_size):
500             self.Raise("Size %#x (%d) does not match align-size %#x (%d)" %
501                   (self.size, self.size, self.align_size, self.align_size))
502         if self.offset != tools.align(self.offset, self.align):
503             self.Raise("Offset %#x (%d) does not match align %#x (%d)" %
504                   (self.offset, self.offset, self.align, self.align))
505         self.Detail('   - packed: offset=%#x, size=%#x, content_size=%#x, next_offset=%x' %
506                     (self.offset, self.size, self.contents_size, new_offset))
507
508         return new_offset
509
510     def Raise(self, msg):
511         """Convenience function to raise an error referencing a node"""
512         raise ValueError("Node '%s': %s" % (self._node.path, msg))
513
514     def Info(self, msg):
515         """Convenience function to log info referencing a node"""
516         tag = "Info '%s'" % self._node.path
517         tout.detail('%30s: %s' % (tag, msg))
518
519     def Detail(self, msg):
520         """Convenience function to log detail referencing a node"""
521         tag = "Node '%s'" % self._node.path
522         tout.detail('%30s: %s' % (tag, msg))
523
524     def GetEntryArgsOrProps(self, props, required=False):
525         """Return the values of a set of properties
526
527         Args:
528             props: List of EntryArg objects
529
530         Raises:
531             ValueError if a property is not found
532         """
533         values = []
534         missing = []
535         for prop in props:
536             python_prop = prop.name.replace('-', '_')
537             if hasattr(self, python_prop):
538                 value = getattr(self, python_prop)
539             else:
540                 value = None
541             if value is None:
542                 value = self.GetArg(prop.name, prop.datatype)
543             if value is None and required:
544                 missing.append(prop.name)
545             values.append(value)
546         if missing:
547             self.GetImage().MissingArgs(self, missing)
548         return values
549
550     def GetPath(self):
551         """Get the path of a node
552
553         Returns:
554             Full path of the node for this entry
555         """
556         return self._node.path
557
558     def GetData(self, required=True):
559         """Get the contents of an entry
560
561         Args:
562             required: True if the data must be present, False if it is OK to
563                 return None
564
565         Returns:
566             bytes content of the entry, excluding any padding. If the entry is
567                 compressed, the compressed data is returned
568         """
569         self.Detail('GetData: size %s' % to_hex_size(self.data))
570         return self.data
571
572     def GetPaddedData(self, data=None):
573         """Get the data for an entry including any padding
574
575         Gets the entry data and uses its section's pad-byte value to add padding
576         before and after as defined by the pad-before and pad-after properties.
577
578         This does not consider alignment.
579
580         Returns:
581             Contents of the entry along with any pad bytes before and
582             after it (bytes)
583         """
584         if data is None:
585             data = self.GetData()
586         return self.section.GetPaddedDataForEntry(self, data)
587
588     def GetOffsets(self):
589         """Get the offsets for siblings
590
591         Some entry types can contain information about the position or size of
592         other entries. An example of this is the Intel Flash Descriptor, which
593         knows where the Intel Management Engine section should go.
594
595         If this entry knows about the position of other entries, it can specify
596         this by returning values here
597
598         Returns:
599             Dict:
600                 key: Entry type
601                 value: List containing position and size of the given entry
602                     type. Either can be None if not known
603         """
604         return {}
605
606     def SetOffsetSize(self, offset, size):
607         """Set the offset and/or size of an entry
608
609         Args:
610             offset: New offset, or None to leave alone
611             size: New size, or None to leave alone
612         """
613         if offset is not None:
614             self.offset = offset
615         if size is not None:
616             self.size = size
617
618     def SetImagePos(self, image_pos):
619         """Set the position in the image
620
621         Args:
622             image_pos: Position of this entry in the image
623         """
624         self.image_pos = image_pos + self.offset
625
626     def ProcessContents(self):
627         """Do any post-packing updates of entry contents
628
629         This function should call ProcessContentsUpdate() to update the entry
630         contents, if necessary, returning its return value here.
631
632         Args:
633             data: Data to set to the contents (bytes)
634
635         Returns:
636             True if the new data size is OK, False if expansion is needed
637
638         Raises:
639             ValueError if the new data size is not the same as the old and
640                 state.AllowEntryExpansion() is False
641         """
642         return True
643
644     def WriteSymbols(self, section):
645         """Write symbol values into binary files for access at run time
646
647         Args:
648           section: Section containing the entry
649         """
650         pass
651
652     def CheckEntries(self):
653         """Check that the entry offsets are correct
654
655         This is used for entries which have extra offset requirements (other
656         than having to be fully inside their section). Sub-classes can implement
657         this function and raise if there is a problem.
658         """
659         pass
660
661     @staticmethod
662     def GetStr(value):
663         if value is None:
664             return '<none>  '
665         return '%08x' % value
666
667     @staticmethod
668     def WriteMapLine(fd, indent, name, offset, size, image_pos):
669         print('%s  %s%s  %s  %s' % (Entry.GetStr(image_pos), ' ' * indent,
670                                     Entry.GetStr(offset), Entry.GetStr(size),
671                                     name), file=fd)
672
673     def WriteMap(self, fd, indent):
674         """Write a map of the entry to a .map file
675
676         Args:
677             fd: File to write the map to
678             indent: Curent indent level of map (0=none, 1=one level, etc.)
679         """
680         self.WriteMapLine(fd, indent, self.name, self.offset, self.size,
681                           self.image_pos)
682
683     # pylint: disable=assignment-from-none
684     def GetEntries(self):
685         """Return a list of entries contained by this entry
686
687         Returns:
688             List of entries, or None if none. A normal entry has no entries
689                 within it so will return None
690         """
691         return None
692
693     def FindEntryByNode(self, find_node):
694         """Find a node in an entry, searching all subentries
695
696         This does a recursive search.
697
698         Args:
699             find_node (fdt.Node): Node to find
700
701         Returns:
702             Entry: entry, if found, else None
703         """
704         entries = self.GetEntries()
705         if entries:
706             for entry in entries.values():
707                 if entry._node == find_node:
708                     return entry
709                 found = entry.FindEntryByNode(find_node)
710                 if found:
711                     return found
712
713         return None
714
715     def GetArg(self, name, datatype=str):
716         """Get the value of an entry argument or device-tree-node property
717
718         Some node properties can be provided as arguments to binman. First check
719         the entry arguments, and fall back to the device tree if not found
720
721         Args:
722             name: Argument name
723             datatype: Data type (str or int)
724
725         Returns:
726             Value of argument as a string or int, or None if no value
727
728         Raises:
729             ValueError if the argument cannot be converted to in
730         """
731         value = state.GetEntryArg(name)
732         if value is not None:
733             if datatype == int:
734                 try:
735                     value = int(value)
736                 except ValueError:
737                     self.Raise("Cannot convert entry arg '%s' (value '%s') to integer" %
738                                (name, value))
739             elif datatype == str:
740                 pass
741             else:
742                 raise ValueError("GetArg() internal error: Unknown data type '%s'" %
743                                  datatype)
744         else:
745             value = fdt_util.GetDatatype(self._node, name, datatype)
746         return value
747
748     @staticmethod
749     def WriteDocs(modules, test_missing=None):
750         """Write out documentation about the various entry types to stdout
751
752         Args:
753             modules: List of modules to include
754             test_missing: Used for testing. This is a module to report
755                 as missing
756         """
757         print('''Binman Entry Documentation
758 ===========================
759
760 This file describes the entry types supported by binman. These entry types can
761 be placed in an image one by one to build up a final firmware image. It is
762 fairly easy to create new entry types. Just add a new file to the 'etype'
763 directory. You can use the existing entries as examples.
764
765 Note that some entries are subclasses of others, using and extending their
766 features to produce new behaviours.
767
768
769 ''')
770         modules = sorted(modules)
771
772         # Don't show the test entry
773         if '_testing' in modules:
774             modules.remove('_testing')
775         missing = []
776         for name in modules:
777             module = Entry.Lookup('WriteDocs', name, False)
778             docs = getattr(module, '__doc__')
779             if test_missing == name:
780                 docs = None
781             if docs:
782                 lines = docs.splitlines()
783                 first_line = lines[0]
784                 rest = [line[4:] for line in lines[1:]]
785                 hdr = 'Entry: %s: %s' % (name.replace('_', '-'), first_line)
786
787                 # Create a reference for use by rST docs
788                 ref_name = f'etype_{module.__name__[6:]}'.lower()
789                 print('.. _%s:' % ref_name)
790                 print()
791                 print(hdr)
792                 print('-' * len(hdr))
793                 print('\n'.join(rest))
794                 print()
795                 print()
796             else:
797                 missing.append(name)
798
799         if missing:
800             raise ValueError('Documentation is missing for modules: %s' %
801                              ', '.join(missing))
802
803     def GetUniqueName(self):
804         """Get a unique name for a node
805
806         Returns:
807             String containing a unique name for a node, consisting of the name
808             of all ancestors (starting from within the 'binman' node) separated
809             by a dot ('.'). This can be useful for generating unique filesnames
810             in the output directory.
811         """
812         name = self.name
813         node = self._node
814         while node.parent:
815             node = node.parent
816             if node.name in ('binman', '/'):
817                 break
818             name = '%s.%s' % (node.name, name)
819         return name
820
821     def extend_to_limit(self, limit):
822         """Extend an entry so that it ends at the given offset limit"""
823         if self.offset + self.size < limit:
824             self.size = limit - self.offset
825             # Request the contents again, since changing the size requires that
826             # the data grows. This should not fail, but check it to be sure.
827             if not self.ObtainContents():
828                 self.Raise('Cannot obtain contents when expanding entry')
829
830     def HasSibling(self, name):
831         """Check if there is a sibling of a given name
832
833         Returns:
834             True if there is an entry with this name in the the same section,
835                 else False
836         """
837         return name in self.section.GetEntries()
838
839     def GetSiblingImagePos(self, name):
840         """Return the image position of the given sibling
841
842         Returns:
843             Image position of sibling, or None if the sibling has no position,
844                 or False if there is no such sibling
845         """
846         if not self.HasSibling(name):
847             return False
848         return self.section.GetEntries()[name].image_pos
849
850     @staticmethod
851     def AddEntryInfo(entries, indent, name, etype, size, image_pos,
852                      uncomp_size, offset, entry):
853         """Add a new entry to the entries list
854
855         Args:
856             entries: List (of EntryInfo objects) to add to
857             indent: Current indent level to add to list
858             name: Entry name (string)
859             etype: Entry type (string)
860             size: Entry size in bytes (int)
861             image_pos: Position within image in bytes (int)
862             uncomp_size: Uncompressed size if the entry uses compression, else
863                 None
864             offset: Entry offset within parent in bytes (int)
865             entry: Entry object
866         """
867         entries.append(EntryInfo(indent, name, etype, size, image_pos,
868                                  uncomp_size, offset, entry))
869
870     def ListEntries(self, entries, indent):
871         """Add files in this entry to the list of entries
872
873         This can be overridden by subclasses which need different behaviour.
874
875         Args:
876             entries: List (of EntryInfo objects) to add to
877             indent: Current indent level to add to list
878         """
879         self.AddEntryInfo(entries, indent, self.name, self.etype, self.size,
880                           self.image_pos, self.uncomp_size, self.offset, self)
881
882     def ReadData(self, decomp=True, alt_format=None):
883         """Read the data for an entry from the image
884
885         This is used when the image has been read in and we want to extract the
886         data for a particular entry from that image.
887
888         Args:
889             decomp: True to decompress any compressed data before returning it;
890                 False to return the raw, uncompressed data
891
892         Returns:
893             Entry data (bytes)
894         """
895         # Use True here so that we get an uncompressed section to work from,
896         # although compressed sections are currently not supported
897         tout.debug("ReadChildData section '%s', entry '%s'" %
898                    (self.section.GetPath(), self.GetPath()))
899         data = self.section.ReadChildData(self, decomp, alt_format)
900         return data
901
902     def ReadChildData(self, child, decomp=True, alt_format=None):
903         """Read the data for a particular child entry
904
905         This reads data from the parent and extracts the piece that relates to
906         the given child.
907
908         Args:
909             child (Entry): Child entry to read data for (must be valid)
910             decomp (bool): True to decompress any compressed data before
911                 returning it; False to return the raw, uncompressed data
912             alt_format (str): Alternative format to read in, or None
913
914         Returns:
915             Data for the child (bytes)
916         """
917         pass
918
919     def LoadData(self, decomp=True):
920         data = self.ReadData(decomp)
921         self.contents_size = len(data)
922         self.ProcessContentsUpdate(data)
923         self.Detail('Loaded data size %x' % len(data))
924
925     def GetAltFormat(self, data, alt_format):
926         """Read the data for an extry in an alternative format
927
928         Supported formats are list in the documentation for each entry. An
929         example is fdtmap which provides .
930
931         Args:
932             data (bytes): Data to convert (this should have been produced by the
933                 entry)
934             alt_format (str): Format to use
935
936         """
937         pass
938
939     def GetImage(self):
940         """Get the image containing this entry
941
942         Returns:
943             Image object containing this entry
944         """
945         return self.section.GetImage()
946
947     def WriteData(self, data, decomp=True):
948         """Write the data to an entry in the image
949
950         This is used when the image has been read in and we want to replace the
951         data for a particular entry in that image.
952
953         The image must be re-packed and written out afterwards.
954
955         Args:
956             data: Data to replace it with
957             decomp: True to compress the data if needed, False if data is
958                 already compressed so should be used as is
959
960         Returns:
961             True if the data did not result in a resize of this entry, False if
962                  the entry must be resized
963         """
964         if self.size is not None:
965             self.contents_size = self.size
966         else:
967             self.contents_size = self.pre_reset_size
968         ok = self.ProcessContentsUpdate(data)
969         self.Detail('WriteData: size=%x, ok=%s' % (len(data), ok))
970         section_ok = self.section.WriteChildData(self)
971         return ok and section_ok
972
973     def WriteChildData(self, child):
974         """Handle writing the data in a child entry
975
976         This should be called on the child's parent section after the child's
977         data has been updated. It should update any data structures needed to
978         validate that the update is successful.
979
980         This base-class implementation does nothing, since the base Entry object
981         does not have any children.
982
983         Args:
984             child: Child Entry that was written
985
986         Returns:
987             True if the section could be updated successfully, False if the
988                 data is such that the section could not update
989         """
990         return True
991
992     def GetSiblingOrder(self):
993         """Get the relative order of an entry amoung its siblings
994
995         Returns:
996             'start' if this entry is first among siblings, 'end' if last,
997                 otherwise None
998         """
999         entries = list(self.section.GetEntries().values())
1000         if entries:
1001             if self == entries[0]:
1002                 return 'start'
1003             elif self == entries[-1]:
1004                 return 'end'
1005         return 'middle'
1006
1007     def SetAllowMissing(self, allow_missing):
1008         """Set whether a section allows missing external blobs
1009
1010         Args:
1011             allow_missing: True if allowed, False if not allowed
1012         """
1013         # This is meaningless for anything other than sections
1014         pass
1015
1016     def SetAllowFakeBlob(self, allow_fake):
1017         """Set whether a section allows to create a fake blob
1018
1019         Args:
1020             allow_fake: True if allowed, False if not allowed
1021         """
1022         self.allow_fake = allow_fake
1023
1024     def CheckMissing(self, missing_list):
1025         """Check if any entries in this section have missing external blobs
1026
1027         If there are missing blobs, the entries are added to the list
1028
1029         Args:
1030             missing_list: List of Entry objects to be added to
1031         """
1032         if self.missing:
1033             missing_list.append(self)
1034
1035     def check_fake_fname(self, fname, size=0):
1036         """If the file is missing and the entry allows fake blobs, fake it
1037
1038         Sets self.faked to True if faked
1039
1040         Args:
1041             fname (str): Filename to check
1042             size (int): Size of fake file to create
1043
1044         Returns:
1045             tuple:
1046                 fname (str): Filename of faked file
1047                 bool: True if the blob was faked, False if not
1048         """
1049         if self.allow_fake and not pathlib.Path(fname).is_file():
1050             if not self.fake_fname:
1051                 outfname = os.path.join(self.fake_dir, os.path.basename(fname))
1052                 with open(outfname, "wb") as out:
1053                     out.truncate(size)
1054                 tout.info(f"Entry '{self._node.path}': Faked blob '{outfname}'")
1055                 self.fake_fname = outfname
1056             self.faked = True
1057             return self.fake_fname, True
1058         return fname, False
1059
1060     def CheckFakedBlobs(self, faked_blobs_list):
1061         """Check if any entries in this section have faked external blobs
1062
1063         If there are faked blobs, the entries are added to the list
1064
1065         Args:
1066             fake_blobs_list: List of Entry objects to be added to
1067         """
1068         # This is meaningless for anything other than blobs
1069         pass
1070
1071     def GetAllowMissing(self):
1072         """Get whether a section allows missing external blobs
1073
1074         Returns:
1075             True if allowed, False if not allowed
1076         """
1077         return self.allow_missing
1078
1079     def record_missing_bintool(self, bintool):
1080         """Record a missing bintool that was needed to produce this entry
1081
1082         Args:
1083             bintool (Bintool): Bintool that was missing
1084         """
1085         if bintool not in self.missing_bintools:
1086             self.missing_bintools.append(bintool)
1087
1088     def check_missing_bintools(self, missing_list):
1089         """Check if any entries in this section have missing bintools
1090
1091         If there are missing bintools, these are added to the list
1092
1093         Args:
1094             missing_list: List of Bintool objects to be added to
1095         """
1096         for bintool in self.missing_bintools:
1097             if bintool not in missing_list:
1098                 missing_list.append(bintool)
1099
1100
1101     def GetHelpTags(self):
1102         """Get the tags use for missing-blob help
1103
1104         Returns:
1105             list of possible tags, most desirable first
1106         """
1107         return list(filter(None, [self.missing_msg, self.name, self.etype]))
1108
1109     def CompressData(self, indata):
1110         """Compress data according to the entry's compression method
1111
1112         Args:
1113             indata: Data to compress
1114
1115         Returns:
1116             Compressed data
1117         """
1118         self.uncomp_data = indata
1119         if self.compress != 'none':
1120             self.uncomp_size = len(indata)
1121             if self.comp_bintool.is_present():
1122                 data = self.comp_bintool.compress(indata)
1123             else:
1124                 self.record_missing_bintool(self.comp_bintool)
1125                 data = tools.get_bytes(0, 1024)
1126         else:
1127             data = indata
1128         return data
1129
1130     def DecompressData(self, indata):
1131         """Decompress data according to the entry's compression method
1132
1133         Args:
1134             indata: Data to decompress
1135
1136         Returns:
1137             Decompressed data
1138         """
1139         if self.compress != 'none':
1140             if self.comp_bintool.is_present():
1141                 data = self.comp_bintool.decompress(indata)
1142                 self.uncomp_size = len(data)
1143             else:
1144                 self.record_missing_bintool(self.comp_bintool)
1145                 data = tools.get_bytes(0, 1024)
1146         else:
1147             data = indata
1148         self.uncomp_data = data
1149         return data
1150
1151     @classmethod
1152     def UseExpanded(cls, node, etype, new_etype):
1153         """Check whether to use an expanded entry type
1154
1155         This is called by Entry.Create() when it finds an expanded version of
1156         an entry type (e.g. 'u-boot-expanded'). If this method returns True then
1157         it will be used (e.g. in place of 'u-boot'). If it returns False, it is
1158         ignored.
1159
1160         Args:
1161             node:     Node object containing information about the entry to
1162                       create
1163             etype:    Original entry type being used
1164             new_etype: New entry type proposed
1165
1166         Returns:
1167             True to use this entry type, False to use the original one
1168         """
1169         tout.info("Node '%s': etype '%s': %s selected" %
1170                   (node.path, etype, new_etype))
1171         return True
1172
1173     def CheckAltFormats(self, alt_formats):
1174         """Add any alternative formats supported by this entry type
1175
1176         Args:
1177             alt_formats (dict): Dict to add alt_formats to:
1178                 key: Name of alt format
1179                 value: Help text
1180         """
1181         pass
1182
1183     def AddBintools(self, btools):
1184         """Add the bintools used by this entry type
1185
1186         Args:
1187             btools (dict of Bintool):
1188
1189         Raise:
1190             ValueError if compression algorithm is not supported
1191         """
1192         algo = self.compress
1193         if algo != 'none':
1194             algos = ['bzip2', 'lz4', 'lzma']
1195             if algo not in algos:
1196                 raise ValueError("Unknown algorithm '%s'" % algo)
1197             names = {'lzma': 'lzma_alone'}
1198             name = names.get(self.compress, self.compress)
1199             self.comp_bintool = self.AddBintool(btools, name)
1200
1201     @classmethod
1202     def AddBintool(self, tools, name):
1203         """Add a new bintool to the tools used by this etype
1204
1205         Args:
1206             name: Name of the tool
1207         """
1208         btool = bintool.Bintool.create(name)
1209         tools[name] = btool
1210         return btool
1211
1212     def SetUpdateHash(self, update_hash):
1213         """Set whether this entry's "hash" subnode should be updated
1214
1215         Args:
1216             update_hash: True if hash should be updated, False if not
1217         """
1218         self.update_hash = update_hash
1219
1220     def collect_contents_to_file(self, entries, prefix, fake_size=0):
1221         """Put the contents of a list of entries into a file
1222
1223         Args:
1224             entries (list of Entry): Entries to collect
1225             prefix (str): Filename prefix of file to write to
1226             fake_size (int): Size of fake file to create if needed
1227
1228         If any entry does not have contents yet, this function returns False
1229         for the data.
1230
1231         Returns:
1232             Tuple:
1233                 bytes: Concatenated data from all the entries (or None)
1234                 str: Filename of file written (or None if no data)
1235                 str: Unique portion of filename (or None if no data)
1236         """
1237         data = b''
1238         for entry in entries:
1239             # First get the input data and put it in a file. If not available,
1240             # try later.
1241             if not entry.ObtainContents(fake_size=fake_size):
1242                 return None, None, None
1243             data += entry.GetData()
1244         uniq = self.GetUniqueName()
1245         fname = tools.get_output_filename(f'{prefix}.{uniq}')
1246         tools.write_file(fname, data)
1247         return data, fname, uniq
1248
1249     @classmethod
1250     def create_fake_dir(cls):
1251         """Create the directory for fake files"""
1252         cls.fake_dir = tools.get_output_filename('binman-fake')
1253         if not os.path.exists(cls.fake_dir):
1254             os.mkdir(cls.fake_dir)
1255         tout.notice(f"Fake-blob dir is '{cls.fake_dir}'")
1256
1257     def ensure_props(self):
1258         """Raise an exception if properties are missing
1259
1260         Args:
1261             prop_list (list of str): List of properties to check for
1262
1263         Raises:
1264             ValueError: Any property is missing
1265         """
1266         not_present = []
1267         for prop in self.required_props:
1268             if not prop in self._node.props:
1269                 not_present.append(prop)
1270         if not_present:
1271             self.Raise(f"'{self.etype}' entry is missing properties: {' '.join(not_present)}")