1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2016 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# Class for an image, the output of binman
6#
7
8from collections import OrderedDict
9import fnmatch
10from operator import attrgetter
11import os
12import re
13import sys
14
15from binman.entry import Entry
16from binman.etype import fdtmap
17from binman.etype import image_header
18from binman.etype import section
19from dtoc import fdt
20from dtoc import fdt_util
21from u_boot_pylib import tools
22from u_boot_pylib import tout
23
24class Image(section.Entry_section):
25    """A Image, representing an output from binman
26
27    An image is comprised of a collection of entries each containing binary
28    data. The image size must be large enough to hold all of this data.
29
30    This class implements the various operations needed for images.
31
32    Attributes:
33        filename: Output filename for image
34        image_node: Name of node containing the description for this image
35        fdtmap_dtb: Fdt object for the fdtmap when loading from a file
36        fdtmap_data: Contents of the fdtmap when loading from a file
37        allow_repack: True to add properties to allow the image to be safely
38            repacked later
39        test_section_timeout: Use a zero timeout for section multi-threading
40            (for testing)
41        symlink: Name of symlink to image
42
43    Args:
44        copy_to_orig: Copy offset/size to orig_offset/orig_size after reading
45            from the device tree
46        test: True if this is being called from a test of Images. This this case
47            there is no device tree defining the structure of the section, so
48            we create a section manually.
49        ignore_missing: Ignore any missing entry arguments (i.e. don't raise an
50            exception). This should be used if the Image is being loaded from
51            a file rather than generated. In that case we obviously don't need
52            the entry arguments since the contents already exists.
53        use_expanded: True if we are updating the FDT wth entry offsets, etc.
54            and should use the expanded versions of the U-Boot entries.
55            Any entry type that includes a devicetree must put it in a
56            separate entry so that it will be updated. For example. 'u-boot'
57            normally just picks up 'u-boot.bin' which includes the
58            devicetree, but this is not updateable, since it comes into
59            binman as one piece and binman doesn't know that it is actually
60            an executable followed by a devicetree. Of course it could be
61            taught this, but then when reading an image (e.g. 'binman ls')
62            it may need to be able to split the devicetree out of the image
63            in order to determine the location of things. Instead we choose
64            to ignore 'u-boot-bin' in this case, and build it ourselves in
65            binman with 'u-boot-dtb.bin' and 'u-boot.dtb'. See
66            Entry_u_boot_expanded and Entry_blob_phase for details.
67        missing_etype: Use a default entry type ('blob') if the requested one
68            does not exist in binman. This is useful if an image was created by
69            binman a newer version of binman but we want to list it in an older
70            version which does not support all the entry types.
71        generate: If true, generator nodes are processed. If false they are
72            ignored which is useful when an existing image is read back from a
73            file.
74    """
75    def __init__(self, name, node, copy_to_orig=True, test=False,
76                 ignore_missing=False, use_expanded=False, missing_etype=False,
77                 generate=True):
78        super().__init__(None, 'section', node, test=test)
79        self.copy_to_orig = copy_to_orig
80        self.name = name
81        self.image_name = name
82        self._filename = '%s.bin' % self.image_name
83        self.fdtmap_dtb = None
84        self.fdtmap_data = None
85        self.allow_repack = False
86        self._ignore_missing = ignore_missing
87        self.missing_etype = missing_etype
88        self.use_expanded = use_expanded
89        self.test_section_timeout = False
90        self.bintools = {}
91        self.generate = generate
92        if not test:
93            self.ReadNode()
94
95    def ReadNode(self):
96        super().ReadNode()
97        self.allow_repack = fdt_util.GetBool(self._node, 'allow-repack')
98        self._symlink = fdt_util.GetString(self._node, 'symlink')
99
100    @classmethod
101    def FromFile(cls, fname):
102        """Convert an image file into an Image for use in binman
103
104        Args:
105            fname: Filename of image file to read
106
107        Returns:
108            Image object on success
109
110        Raises:
111            ValueError if something goes wrong
112        """
113        data = tools.read_file(fname)
114        size = len(data)
115
116        # First look for an image header
117        pos = image_header.LocateHeaderOffset(data)
118        if pos is None:
119            # Look for the FDT map
120            pos = fdtmap.LocateFdtmap(data)
121        if pos is None:
122            raise ValueError('Cannot find FDT map in image')
123
124        # We don't know the FDT size, so check its header first
125        probe_dtb = fdt.Fdt.FromData(
126            data[pos + fdtmap.FDTMAP_HDR_LEN:pos + 256])
127        dtb_size = probe_dtb.GetFdtObj().totalsize()
128        fdtmap_data = data[pos:pos + dtb_size + fdtmap.FDTMAP_HDR_LEN]
129        fdt_data = fdtmap_data[fdtmap.FDTMAP_HDR_LEN:]
130        out_fname = tools.get_output_filename('fdtmap.in.dtb')
131        tools.write_file(out_fname, fdt_data)
132        dtb = fdt.Fdt(out_fname)
133        dtb.Scan()
134
135        # Return an Image with the associated nodes
136        root = dtb.GetRoot()
137        image = Image('image', root, copy_to_orig=False, ignore_missing=True,
138                      missing_etype=True, generate=False)
139
140        image.image_node = fdt_util.GetString(root, 'image-node', 'image')
141        image.fdtmap_dtb = dtb
142        image.fdtmap_data = fdtmap_data
143        image._data = data
144        image._filename = fname
145        image.image_name, _ = os.path.splitext(fname)
146        return image
147
148    def Raise(self, msg):
149        """Convenience function to raise an error referencing an image"""
150        raise ValueError("Image '%s': %s" % (self._node.path, msg))
151
152    def PackEntries(self):
153        """Pack all entries into the image"""
154        super().Pack(0)
155
156    def SetImagePos(self):
157        # This first section in the image so it starts at 0
158        super().SetImagePos(0)
159
160    def ProcessEntryContents(self):
161        """Call the ProcessContents() method for each entry
162
163        This is intended to adjust the contents as needed by the entry type.
164
165        Returns:
166            True if the new data size is OK, False if expansion is needed
167        """
168        return super().ProcessContents()
169
170    def WriteSymbols(self):
171        """Write symbol values into binary files for access at run time"""
172        super().WriteSymbols(self)
173
174    def BuildImage(self):
175        """Write the image to a file"""
176        fname = tools.get_output_filename(self._filename)
177        tout.info("Writing image to '%s'" % fname)
178        with open(fname, 'wb') as fd:
179            data = self.GetPaddedData()
180            fd.write(data)
181        tout.info("Wrote %#x bytes" % len(data))
182        # Create symlink to file if symlink given
183        if self._symlink is not None:
184            sname = tools.get_output_filename(self._symlink)
185            os.symlink(fname, sname)
186
187    def WriteMap(self):
188        """Write a map of the image to a .map file
189
190        Returns:
191            Filename of map file written
192        """
193        filename = '%s.map' % self.image_name
194        fname = tools.get_output_filename(filename)
195        with open(fname, 'w') as fd:
196            print('%8s  %8s  %8s  %s' % ('ImagePos', 'Offset', 'Size', 'Name'),
197                  file=fd)
198            super().WriteMap(fd, 0)
199        return fname
200
201    def BuildEntryList(self):
202        """List the files in an image
203
204        Returns:
205            List of entry.EntryInfo objects describing all entries in the image
206        """
207        entries = []
208        self.ListEntries(entries, 0)
209        return entries
210
211    def FindEntryPath(self, entry_path):
212        """Find an entry at a given path in the image
213
214        Args:
215            entry_path: Path to entry (e.g. /ro-section/u-boot')
216
217        Returns:
218            Entry object corresponding to that past
219
220        Raises:
221            ValueError if no entry found
222        """
223        parts = entry_path.split('/')
224        entries = self.GetEntries()
225        parent = '/'
226        for part in parts:
227            entry = entries.get(part)
228            if not entry:
229                raise ValueError("Entry '%s' not found in '%s'" %
230                                 (part, parent))
231            parent = entry.GetPath()
232            entries = entry.GetEntries()
233        return entry
234
235    def ReadData(self, decomp=True, alt_format=None):
236        tout.debug("Image '%s' ReadData(), size=%#x" %
237                   (self.GetPath(), len(self._data)))
238        return self._data
239
240    def GetListEntries(self, entry_paths):
241        """List the entries in an image
242
243        This decodes the supplied image and returns a list of entries from that
244        image, preceded by a header.
245
246        Args:
247            entry_paths: List of paths to match (each can have wildcards). Only
248                entries whose names match one of these paths will be printed
249
250        Returns:
251            String error message if something went wrong, otherwise
252            3-Tuple:
253                List of EntryInfo objects
254                List of lines, each
255                    List of text columns, each a string
256                List of widths of each column
257        """
258        def _EntryToStrings(entry):
259            """Convert an entry to a list of strings, one for each column
260
261            Args:
262                entry: EntryInfo object containing information to output
263
264            Returns:
265                List of strings, one for each field in entry
266            """
267            def _AppendHex(val):
268                """Append a hex value, or an empty string if val is None
269
270                Args:
271                    val: Integer value, or None if none
272                """
273                args.append('' if val is None else '>%x' % val)
274
275            args = ['  ' * entry.indent + entry.name]
276            _AppendHex(entry.image_pos)
277            _AppendHex(entry.size)
278            args.append(entry.etype)
279            _AppendHex(entry.offset)
280            _AppendHex(entry.uncomp_size)
281            return args
282
283        def _DoLine(lines, line):
284            """Add a line to the output list
285
286            This adds a line (a list of columns) to the output list. It also updates
287            the widths[] array with the maximum width of each column
288
289            Args:
290                lines: List of lines to add to
291                line: List of strings, one for each column
292            """
293            for i, item in enumerate(line):
294                widths[i] = max(widths[i], len(item))
295            lines.append(line)
296
297        def _NameInPaths(fname, entry_paths):
298            """Check if a filename is in a list of wildcarded paths
299
300            Args:
301                fname: Filename to check
302                entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
303                                                             'section/u-boot'])
304
305            Returns:
306                True if any wildcard matches the filename (using Unix filename
307                    pattern matching, not regular expressions)
308                False if not
309            """
310            for path in entry_paths:
311                if fnmatch.fnmatch(fname, path):
312                    return True
313            return False
314
315        entries = self.BuildEntryList()
316
317        # This is our list of lines. Each item in the list is a list of strings, one
318        # for each column
319        lines = []
320        HEADER = ['Name', 'Image-pos', 'Size', 'Entry-type', 'Offset',
321                  'Uncomp-size']
322        num_columns = len(HEADER)
323
324        # This records the width of each column, calculated as the maximum width of
325        # all the strings in that column
326        widths = [0] * num_columns
327        _DoLine(lines, HEADER)
328
329        # We won't print anything unless it has at least this indent. So at the
330        # start we will print nothing, unless a path matches (or there are no
331        # entry paths)
332        MAX_INDENT = 100
333        min_indent = MAX_INDENT
334        path_stack = []
335        path = ''
336        indent = 0
337        selected_entries = []
338        for entry in entries:
339            if entry.indent > indent:
340                path_stack.append(path)
341            elif entry.indent < indent:
342                path_stack.pop()
343            if path_stack:
344                path = path_stack[-1] + '/' + entry.name
345            indent = entry.indent
346
347            # If there are entry paths to match and we are not looking at a
348            # sub-entry of a previously matched entry, we need to check the path
349            if entry_paths and indent <= min_indent:
350                if _NameInPaths(path[1:], entry_paths):
351                    # Print this entry and all sub-entries (=higher indent)
352                    min_indent = indent
353                else:
354                    # Don't print this entry, nor any following entries until we get
355                    # a path match
356                    min_indent = MAX_INDENT
357                    continue
358            _DoLine(lines, _EntryToStrings(entry))
359            selected_entries.append(entry)
360        return selected_entries, lines, widths
361
362    def LookupImageSymbol(self, sym_name, optional, msg, base_addr):
363        """Look up a symbol in an ELF file
364
365        Looks up a symbol in an ELF file. Only entry types which come from an
366        ELF image can be used by this function.
367
368        This searches through this image including all of its subsections.
369
370        At present the only entry properties supported are:
371            offset
372            image_pos - 'base_addr' is added if this is not an end-at-4gb image
373            size
374
375        Args:
376            sym_name: Symbol name in the ELF file to look up in the format
377                _binman_<entry>_prop_<property> where <entry> is the name of
378                the entry and <property> is the property to find (e.g.
379                _binman_u_boot_prop_offset). As a special case, you can append
380                _any to <entry> to have it search for any matching entry. E.g.
381                _binman_u_boot_any_prop_offset will match entries called u-boot,
382                u-boot-img and u-boot-nodtb)
383            optional: True if the symbol is optional. If False this function
384                will raise if the symbol is not found
385            msg: Message to display if an error occurs
386            base_addr: Base address of image. This is added to the returned
387                image_pos in most cases so that the returned position indicates
388                where the targeted entry/binary has actually been loaded. But
389                if end-at-4gb is used, this is not done, since the binary is
390                already assumed to be linked to the ROM position and using
391                execute-in-place (XIP).
392
393        Returns:
394            Value that should be assigned to that symbol, or None if it was
395                optional and not found
396
397        Raises:
398            ValueError if the symbol is invalid or not found, or references a
399                property which is not supported
400        """
401        entries = OrderedDict()
402        entries_by_name = {}
403        self._CollectEntries(entries, entries_by_name, self)
404        return self.LookupSymbol(sym_name, optional, msg, base_addr,
405                                 entries_by_name)
406
407    def CollectBintools(self):
408        """Collect all the bintools used by this image
409
410        Returns:
411            Dict of bintools:
412                key: name of tool
413                value: Bintool object
414        """
415        bintools = {}
416        super().AddBintools(bintools)
417        self.bintools = bintools
418        return bintools
419