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