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