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