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