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