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