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