binman: Support reading an image with entry args
[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
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         sizes_ok = True
140         for entry in self._entries.values():
141             if not entry.ProcessContents():
142                 sizes_ok = False
143                 tout.Debug("Entry '%s' size change" % self._node.path)
144         return sizes_ok
145
146     def WriteSymbols(self):
147         """Write symbol values into binary files for access at run time"""
148         super().WriteSymbols(self)
149
150     def BuildImage(self):
151         """Write the image to a file"""
152         fname = tools.GetOutputFilename(self._filename)
153         tout.Info("Writing image to '%s'" % fname)
154         with open(fname, 'wb') as fd:
155             data = self.GetPaddedData()
156             fd.write(data)
157         tout.Info("Wrote %#x bytes" % len(data))
158
159     def WriteMap(self):
160         """Write a map of the image to a .map file
161
162         Returns:
163             Filename of map file written
164         """
165         filename = '%s.map' % self.image_name
166         fname = tools.GetOutputFilename(filename)
167         with open(fname, 'w') as fd:
168             print('%8s  %8s  %8s  %s' % ('ImagePos', 'Offset', 'Size', 'Name'),
169                   file=fd)
170             super().WriteMap(fd, 0)
171         return fname
172
173     def BuildEntryList(self):
174         """List the files in an image
175
176         Returns:
177             List of entry.EntryInfo objects describing all entries in the image
178         """
179         entries = []
180         self.ListEntries(entries, 0)
181         return entries
182
183     def FindEntryPath(self, entry_path):
184         """Find an entry at a given path in the image
185
186         Args:
187             entry_path: Path to entry (e.g. /ro-section/u-boot')
188
189         Returns:
190             Entry object corresponding to that past
191
192         Raises:
193             ValueError if no entry found
194         """
195         parts = entry_path.split('/')
196         entries = self.GetEntries()
197         parent = '/'
198         for part in parts:
199             entry = entries.get(part)
200             if not entry:
201                 raise ValueError("Entry '%s' not found in '%s'" %
202                                  (part, parent))
203             parent = entry.GetPath()
204             entries = entry.GetEntries()
205         return entry
206
207     def ReadData(self, decomp=True):
208         tout.Debug("Image '%s' ReadData(), size=%#x" %
209                    (self.GetPath(), len(self._data)))
210         return self._data
211
212     def GetListEntries(self, entry_paths):
213         """List the entries in an image
214
215         This decodes the supplied image and returns a list of entries from that
216         image, preceded by a header.
217
218         Args:
219             entry_paths: List of paths to match (each can have wildcards). Only
220                 entries whose names match one of these paths will be printed
221
222         Returns:
223             String error message if something went wrong, otherwise
224             3-Tuple:
225                 List of EntryInfo objects
226                 List of lines, each
227                     List of text columns, each a string
228                 List of widths of each column
229         """
230         def _EntryToStrings(entry):
231             """Convert an entry to a list of strings, one for each column
232
233             Args:
234                 entry: EntryInfo object containing information to output
235
236             Returns:
237                 List of strings, one for each field in entry
238             """
239             def _AppendHex(val):
240                 """Append a hex value, or an empty string if val is None
241
242                 Args:
243                     val: Integer value, or None if none
244                 """
245                 args.append('' if val is None else '>%x' % val)
246
247             args = ['  ' * entry.indent + entry.name]
248             _AppendHex(entry.image_pos)
249             _AppendHex(entry.size)
250             args.append(entry.etype)
251             _AppendHex(entry.offset)
252             _AppendHex(entry.uncomp_size)
253             return args
254
255         def _DoLine(lines, line):
256             """Add a line to the output list
257
258             This adds a line (a list of columns) to the output list. It also updates
259             the widths[] array with the maximum width of each column
260
261             Args:
262                 lines: List of lines to add to
263                 line: List of strings, one for each column
264             """
265             for i, item in enumerate(line):
266                 widths[i] = max(widths[i], len(item))
267             lines.append(line)
268
269         def _NameInPaths(fname, entry_paths):
270             """Check if a filename is in a list of wildcarded paths
271
272             Args:
273                 fname: Filename to check
274                 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
275                                                              'section/u-boot'])
276
277             Returns:
278                 True if any wildcard matches the filename (using Unix filename
279                     pattern matching, not regular expressions)
280                 False if not
281             """
282             for path in entry_paths:
283                 if fnmatch.fnmatch(fname, path):
284                     return True
285             return False
286
287         entries = self.BuildEntryList()
288
289         # This is our list of lines. Each item in the list is a list of strings, one
290         # for each column
291         lines = []
292         HEADER = ['Name', 'Image-pos', 'Size', 'Entry-type', 'Offset',
293                   'Uncomp-size']
294         num_columns = len(HEADER)
295
296         # This records the width of each column, calculated as the maximum width of
297         # all the strings in that column
298         widths = [0] * num_columns
299         _DoLine(lines, HEADER)
300
301         # We won't print anything unless it has at least this indent. So at the
302         # start we will print nothing, unless a path matches (or there are no
303         # entry paths)
304         MAX_INDENT = 100
305         min_indent = MAX_INDENT
306         path_stack = []
307         path = ''
308         indent = 0
309         selected_entries = []
310         for entry in entries:
311             if entry.indent > indent:
312                 path_stack.append(path)
313             elif entry.indent < indent:
314                 path_stack.pop()
315             if path_stack:
316                 path = path_stack[-1] + '/' + entry.name
317             indent = entry.indent
318
319             # If there are entry paths to match and we are not looking at a
320             # sub-entry of a previously matched entry, we need to check the path
321             if entry_paths and indent <= min_indent:
322                 if _NameInPaths(path[1:], entry_paths):
323                     # Print this entry and all sub-entries (=higher indent)
324                     min_indent = indent
325                 else:
326                     # Don't print this entry, nor any following entries until we get
327                     # a path match
328                     min_indent = MAX_INDENT
329                     continue
330             _DoLine(lines, _EntryToStrings(entry))
331             selected_entries.append(entry)
332         return selected_entries, lines, widths
333
334     def LookupImageSymbol(self, sym_name, optional, msg, base_addr):
335         """Look up a symbol in an ELF file
336
337         Looks up a symbol in an ELF file. Only entry types which come from an
338         ELF image can be used by this function.
339
340         This searches through this image including all of its subsections.
341
342         At present the only entry properties supported are:
343             offset
344             image_pos - 'base_addr' is added if this is not an end-at-4gb image
345             size
346
347         Args:
348             sym_name: Symbol name in the ELF file to look up in the format
349                 _binman_<entry>_prop_<property> where <entry> is the name of
350                 the entry and <property> is the property to find (e.g.
351                 _binman_u_boot_prop_offset). As a special case, you can append
352                 _any to <entry> to have it search for any matching entry. E.g.
353                 _binman_u_boot_any_prop_offset will match entries called u-boot,
354                 u-boot-img and u-boot-nodtb)
355             optional: True if the symbol is optional. If False this function
356                 will raise if the symbol is not found
357             msg: Message to display if an error occurs
358             base_addr: Base address of image. This is added to the returned
359                 image_pos in most cases so that the returned position indicates
360                 where the targeted entry/binary has actually been loaded. But
361                 if end-at-4gb is used, this is not done, since the binary is
362                 already assumed to be linked to the ROM position and using
363                 execute-in-place (XIP).
364
365         Returns:
366             Value that should be assigned to that symbol, or None if it was
367                 optional and not found
368
369         Raises:
370             ValueError if the symbol is invalid or not found, or references a
371                 property which is not supported
372         """
373         entries = OrderedDict()
374         entries_by_name = {}
375         self._CollectEntries(entries, entries_by_name, self)
376         return self.LookupSymbol(sym_name, optional, msg, base_addr,
377                                  entries_by_name)