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