binman: Support listing an image
[oweals/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 __future__ import print_function
9
10 from collections import OrderedDict
11 import fnmatch
12 from operator import attrgetter
13 import re
14 import sys
15
16 from entry import Entry
17 from etype import fdtmap
18 from etype import image_header
19 from etype import section
20 import fdt
21 import fdt_util
22 import tools
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
35     Args:
36         test: True if this is being called from a test of Images. This this case
37             there is no device tree defining the structure of the section, so
38             we create a section manually.
39     """
40     def __init__(self, name, node, test=False):
41         self.image = self
42         section.Entry_section.__init__(self, None, 'section', node, test)
43         self.name = 'main-section'
44         self.image_name = name
45         self._filename = '%s.bin' % self.image_name
46         if not test:
47             filename = fdt_util.GetString(self._node, 'filename')
48             if filename:
49                 self._filename = filename
50
51     @classmethod
52     def FromFile(cls, fname):
53         """Convert an image file into an Image for use in binman
54
55         Args:
56             fname: Filename of image file to read
57
58         Returns:
59             Image object on success
60
61         Raises:
62             ValueError if something goes wrong
63         """
64         data = tools.ReadFile(fname)
65         size = len(data)
66
67         # First look for an image header
68         pos = image_header.LocateHeaderOffset(data)
69         if pos is None:
70             # Look for the FDT map
71             pos = fdtmap.LocateFdtmap(data)
72         if pos is None:
73             raise ValueError('Cannot find FDT map in image')
74
75         # We don't know the FDT size, so check its header first
76         probe_dtb = fdt.Fdt.FromData(
77             data[pos + fdtmap.FDTMAP_HDR_LEN:pos + 256])
78         dtb_size = probe_dtb.GetFdtObj().totalsize()
79         fdtmap_data = data[pos:pos + dtb_size + fdtmap.FDTMAP_HDR_LEN]
80         dtb = fdt.Fdt.FromData(fdtmap_data[fdtmap.FDTMAP_HDR_LEN:])
81         dtb.Scan()
82
83         # Return an Image with the associated nodes
84         return Image('image', dtb.GetRoot())
85
86     def Raise(self, msg):
87         """Convenience function to raise an error referencing an image"""
88         raise ValueError("Image '%s': %s" % (self._node.path, msg))
89
90     def PackEntries(self):
91         """Pack all entries into the image"""
92         section.Entry_section.Pack(self, 0)
93
94     def SetImagePos(self):
95         # This first section in the image so it starts at 0
96         section.Entry_section.SetImagePos(self, 0)
97
98     def ProcessEntryContents(self):
99         """Call the ProcessContents() method for each entry
100
101         This is intended to adjust the contents as needed by the entry type.
102
103         Returns:
104             True if the new data size is OK, False if expansion is needed
105         """
106         sizes_ok = True
107         for entry in self._entries.values():
108             if not entry.ProcessContents():
109                 sizes_ok = False
110                 print("Entry '%s' size change" % self._node.path)
111         return sizes_ok
112
113     def WriteSymbols(self):
114         """Write symbol values into binary files for access at run time"""
115         section.Entry_section.WriteSymbols(self, self)
116
117     def BuildSection(self, fd, base_offset):
118         """Write the section to a file"""
119         fd.seek(base_offset)
120         fd.write(self.GetData())
121
122     def BuildImage(self):
123         """Write the image to a file"""
124         fname = tools.GetOutputFilename(self._filename)
125         with open(fname, 'wb') as fd:
126             self.BuildSection(fd, 0)
127
128     def WriteMap(self):
129         """Write a map of the image to a .map file
130
131         Returns:
132             Filename of map file written
133         """
134         filename = '%s.map' % self.image_name
135         fname = tools.GetOutputFilename(filename)
136         with open(fname, 'w') as fd:
137             print('%8s  %8s  %8s  %s' % ('ImagePos', 'Offset', 'Size', 'Name'),
138                   file=fd)
139             section.Entry_section.WriteMap(self, fd, 0)
140         return fname
141
142     def BuildEntryList(self):
143         """List the files in an image
144
145         Returns:
146             List of entry.EntryInfo objects describing all entries in the image
147         """
148         entries = []
149         self.ListEntries(entries, 0)
150         return entries
151
152     def FindEntryPath(self, entry_path):
153         """Find an entry at a given path in the image
154
155         Args:
156             entry_path: Path to entry (e.g. /ro-section/u-boot')
157
158         Returns:
159             Entry object corresponding to that past
160
161         Raises:
162             ValueError if no entry found
163         """
164         parts = entry_path.split('/')
165         entries = self.GetEntries()
166         parent = '/'
167         for part in parts:
168             entry = entries.get(part)
169             if not entry:
170                 raise ValueError("Entry '%s' not found in '%s'" %
171                                  (part, parent))
172             parent = entry.GetPath()
173             entries = entry.GetEntries()
174         return entry
175
176     def ReadData(self, decomp=True):
177         return self._data
178
179     def GetListEntries(self, entry_paths):
180         """List the entries in an image
181
182         This decodes the supplied image and returns a list of entries from that
183         image, preceded by a header.
184
185         Args:
186             entry_paths: List of paths to match (each can have wildcards). Only
187                 entries whose names match one of these paths will be printed
188
189         Returns:
190             String error message if something went wrong, otherwise
191             3-Tuple:
192                 List of EntryInfo objects
193                 List of lines, each
194                     List of text columns, each a string
195                 List of widths of each column
196         """
197         def _EntryToStrings(entry):
198             """Convert an entry to a list of strings, one for each column
199
200             Args:
201                 entry: EntryInfo object containing information to output
202
203             Returns:
204                 List of strings, one for each field in entry
205             """
206             def _AppendHex(val):
207                 """Append a hex value, or an empty string if val is None
208
209                 Args:
210                     val: Integer value, or None if none
211                 """
212                 args.append('' if val is None else '>%x' % val)
213
214             args = ['  ' * entry.indent + entry.name]
215             _AppendHex(entry.image_pos)
216             _AppendHex(entry.size)
217             args.append(entry.etype)
218             _AppendHex(entry.offset)
219             _AppendHex(entry.uncomp_size)
220             return args
221
222         def _DoLine(lines, line):
223             """Add a line to the output list
224
225             This adds a line (a list of columns) to the output list. It also updates
226             the widths[] array with the maximum width of each column
227
228             Args:
229                 lines: List of lines to add to
230                 line: List of strings, one for each column
231             """
232             for i, item in enumerate(line):
233                 widths[i] = max(widths[i], len(item))
234             lines.append(line)
235
236         def _NameInPaths(fname, entry_paths):
237             """Check if a filename is in a list of wildcarded paths
238
239             Args:
240                 fname: Filename to check
241                 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
242                                                              'section/u-boot'])
243
244             Returns:
245                 True if any wildcard matches the filename (using Unix filename
246                     pattern matching, not regular expressions)
247                 False if not
248             """
249             for path in entry_paths:
250                 if fnmatch.fnmatch(fname, path):
251                     return True
252             return False
253
254         entries = self.BuildEntryList()
255
256         # This is our list of lines. Each item in the list is a list of strings, one
257         # for each column
258         lines = []
259         HEADER = ['Name', 'Image-pos', 'Size', 'Entry-type', 'Offset',
260                   'Uncomp-size']
261         num_columns = len(HEADER)
262
263         # This records the width of each column, calculated as the maximum width of
264         # all the strings in that column
265         widths = [0] * num_columns
266         _DoLine(lines, HEADER)
267
268         # We won't print anything unless it has at least this indent. So at the
269         # start we will print nothing, unless a path matches (or there are no
270         # entry paths)
271         MAX_INDENT = 100
272         min_indent = MAX_INDENT
273         path_stack = []
274         path = ''
275         indent = 0
276         selected_entries = []
277         for entry in entries:
278             if entry.indent > indent:
279                 path_stack.append(path)
280             elif entry.indent < indent:
281                 path_stack.pop()
282             if path_stack:
283                 path = path_stack[-1] + '/' + entry.name
284             indent = entry.indent
285
286             # If there are entry paths to match and we are not looking at a
287             # sub-entry of a previously matched entry, we need to check the path
288             if entry_paths and indent <= min_indent:
289                 if _NameInPaths(path[1:], entry_paths):
290                     # Print this entry and all sub-entries (=higher indent)
291                     min_indent = indent
292                 else:
293                     # Don't print this entry, nor any following entries until we get
294                     # a path match
295                     min_indent = MAX_INDENT
296                     continue
297             _DoLine(lines, _EntryToStrings(entry))
298             selected_entries.append(entry)
299         return selected_entries, lines, widths