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 u_boot_pylib import tools 22from u_boot_pylib import tout 23 24# This is imported if needed 25state = None 26 27class Image(section.Entry_section): 28 """A Image, representing an output from binman 29 30 An image is comprised of a collection of entries each containing binary 31 data. The image size must be large enough to hold all of this data. 32 33 This class implements the various operations needed for images. 34 35 Attributes: 36 filename: Output filename for image 37 image_node: Name of node containing the description for this image 38 fdtmap_dtb: Fdt object for the fdtmap when loading from a file 39 fdtmap_data: Contents of the fdtmap when loading from a file 40 allow_repack: True to add properties to allow the image to be safely 41 repacked later 42 test_section_timeout: Use a zero timeout for section multi-threading 43 (for testing) 44 symlink: Name of symlink to image 45 46 Args: 47 copy_to_orig: Copy offset/size to orig_offset/orig_size after reading 48 from the device tree 49 test: True if this is being called from a test of Images. This this case 50 there is no device tree defining the structure of the section, so 51 we create a section manually. 52 ignore_missing: Ignore any missing entry arguments (i.e. don't raise an 53 exception). This should be used if the Image is being loaded from 54 a file rather than generated. In that case we obviously don't need 55 the entry arguments since the contents already exists. 56 use_expanded: True if we are updating the FDT wth entry offsets, etc. 57 and should use the expanded versions of the U-Boot entries. 58 Any entry type that includes a devicetree must put it in a 59 separate entry so that it will be updated. For example. 'u-boot' 60 normally just picks up 'u-boot.bin' which includes the 61 devicetree, but this is not updateable, since it comes into 62 binman as one piece and binman doesn't know that it is actually 63 an executable followed by a devicetree. Of course it could be 64 taught this, but then when reading an image (e.g. 'binman ls') 65 it may need to be able to split the devicetree out of the image 66 in order to determine the location of things. Instead we choose 67 to ignore 'u-boot-bin' in this case, and build it ourselves in 68 binman with 'u-boot-dtb.bin' and 'u-boot.dtb'. See 69 Entry_u_boot_expanded and Entry_blob_phase for details. 70 missing_etype: Use a default entry type ('blob') if the requested one 71 does not exist in binman. This is useful if an image was created by 72 binman a newer version of binman but we want to list it in an older 73 version which does not support all the entry types. 74 generate: If true, generator nodes are processed. If false they are 75 ignored which is useful when an existing image is read back from a 76 file. 77 """ 78 def __init__(self, name, node, copy_to_orig=True, test=False, 79 ignore_missing=False, use_expanded=False, missing_etype=False, 80 generate=True): 81 # Put this here to allow entry-docs and help to work without libfdt 82 global state 83 from binman import state 84 85 super().__init__(None, 'section', node, test=test) 86 self.copy_to_orig = copy_to_orig 87 self.name = name 88 self.image_name = name 89 self._filename = '%s.bin' % self.image_name 90 self.fdtmap_dtb = None 91 self.fdtmap_data = None 92 self.allow_repack = False 93 self._ignore_missing = ignore_missing 94 self.missing_etype = missing_etype 95 self.use_expanded = use_expanded 96 self.test_section_timeout = False 97 self.bintools = {} 98 self.generate = generate 99 if not test: 100 self.ReadNode() 101 102 def ReadNode(self): 103 super().ReadNode() 104 self.allow_repack = fdt_util.GetBool(self._node, 'allow-repack') 105 self._symlink = fdt_util.GetString(self._node, 'symlink') 106 107 @classmethod 108 def FromFile(cls, fname): 109 """Convert an image file into an Image for use in binman 110 111 Args: 112 fname: Filename of image file to read 113 114 Returns: 115 Image object on success 116 117 Raises: 118 ValueError if something goes wrong 119 """ 120 data = tools.read_file(fname) 121 size = len(data) 122 123 # First look for an image header 124 pos = image_header.LocateHeaderOffset(data) 125 if pos is None: 126 # Look for the FDT map 127 pos = fdtmap.LocateFdtmap(data) 128 if pos is None: 129 raise ValueError('Cannot find FDT map in image') 130 131 # We don't know the FDT size, so check its header first 132 probe_dtb = fdt.Fdt.FromData( 133 data[pos + fdtmap.FDTMAP_HDR_LEN:pos + 256]) 134 dtb_size = probe_dtb.GetFdtObj().totalsize() 135 fdtmap_data = data[pos:pos + dtb_size + fdtmap.FDTMAP_HDR_LEN] 136 fdt_data = fdtmap_data[fdtmap.FDTMAP_HDR_LEN:] 137 out_fname = tools.get_output_filename('fdtmap.in.dtb') 138 tools.write_file(out_fname, fdt_data) 139 dtb = fdt.Fdt(out_fname) 140 dtb.Scan() 141 142 # Return an Image with the associated nodes 143 root = dtb.GetRoot() 144 image = Image('image', root, copy_to_orig=False, ignore_missing=True, 145 missing_etype=True, generate=False) 146 147 image.image_node = fdt_util.GetString(root, 'image-node', 'image') 148 image.fdtmap_dtb = dtb 149 image.fdtmap_data = fdtmap_data 150 image._data = data 151 image._filename = fname 152 image.image_name, _ = os.path.splitext(fname) 153 return image 154 155 def Raise(self, msg): 156 """Convenience function to raise an error referencing an image""" 157 raise ValueError("Image '%s': %s" % (self._node.path, msg)) 158 159 def PackEntries(self): 160 """Pack all entries into the image""" 161 super().Pack(0) 162 163 def SetImagePos(self): 164 # This first section in the image so it starts at 0 165 super().SetImagePos(0) 166 167 def ProcessEntryContents(self): 168 """Call the ProcessContents() method for each entry 169 170 This is intended to adjust the contents as needed by the entry type. 171 172 Returns: 173 True if the new data size is OK, False if expansion is needed 174 """ 175 return super().ProcessContents() 176 177 def WriteSymbols(self): 178 """Write symbol values into binary files for access at run time""" 179 super().WriteSymbols(self) 180 181 def BuildImage(self): 182 """Write the image to a file""" 183 fname = tools.get_output_filename(self._filename) 184 tout.info("Writing image to '%s'" % fname) 185 with open(fname, 'wb') as fd: 186 # For final image, don't write absent blobs to file 187 self.drop_absent_optional() 188 data = self.GetPaddedData() 189 fd.write(data) 190 tout.info("Wrote %#x bytes" % len(data)) 191 # Create symlink to file if symlink given 192 if self._symlink is not None: 193 sname = tools.get_output_filename(self._symlink) 194 if os.path.islink(sname): 195 os.remove(sname) 196 os.symlink(fname, sname) 197 198 def WriteAlternates(self): 199 """Write out alternative devicetree blobs, each in its own file""" 200 alt_entry = self.FindEntryType('alternates-fdt') 201 if not alt_entry: 202 return 203 204 for alt in alt_entry.alternates: 205 fname, data = alt_entry.ProcessWithFdt(alt) 206 pathname = tools.get_output_filename(fname) 207 tout.info(f"Writing alternate '{alt}' to '{pathname}'") 208 tools.write_file(pathname, data) 209 tout.info("Wrote %#x bytes" % len(data)) 210 211 def WriteMap(self): 212 """Write a map of the image to a .map file 213 214 Returns: 215 Filename of map file written 216 """ 217 filename = '%s.map' % self.image_name 218 fname = tools.get_output_filename(filename) 219 with open(fname, 'w') as fd: 220 print('%8s %8s %8s %s' % ('ImagePos', 'Offset', 'Size', 'Name'), 221 file=fd) 222 super().WriteMap(fd, 0) 223 return fname 224 225 def BuildEntryList(self): 226 """List the files in an image 227 228 Returns: 229 List of entry.EntryInfo objects describing all entries in the image 230 """ 231 entries = [] 232 self.ListEntries(entries, 0) 233 return entries 234 235 def FindEntryPath(self, entry_path): 236 """Find an entry at a given path in the image 237 238 Args: 239 entry_path: Path to entry (e.g. /ro-section/u-boot') 240 241 Returns: 242 Entry object corresponding to that past 243 244 Raises: 245 ValueError if no entry found 246 """ 247 parts = entry_path.split('/') 248 entries = self.GetEntries() 249 parent = '/' 250 for part in parts: 251 entry = entries.get(part) 252 if not entry: 253 raise ValueError("Entry '%s' not found in '%s'" % 254 (part, parent)) 255 parent = entry.GetPath() 256 entries = entry.GetEntries() 257 return entry 258 259 def ReadData(self, decomp=True, alt_format=None): 260 tout.debug("Image '%s' ReadData(), size=%#x" % 261 (self.GetPath(), len(self._data))) 262 return self._data 263 264 def GetListEntries(self, entry_paths): 265 """List the entries in an image 266 267 This decodes the supplied image and returns a list of entries from that 268 image, preceded by a header. 269 270 Args: 271 entry_paths: List of paths to match (each can have wildcards). Only 272 entries whose names match one of these paths will be printed 273 274 Returns: 275 String error message if something went wrong, otherwise 276 3-Tuple: 277 List of EntryInfo objects 278 List of lines, each 279 List of text columns, each a string 280 List of widths of each column 281 """ 282 def _EntryToStrings(entry): 283 """Convert an entry to a list of strings, one for each column 284 285 Args: 286 entry: EntryInfo object containing information to output 287 288 Returns: 289 List of strings, one for each field in entry 290 """ 291 def _AppendHex(val): 292 """Append a hex value, or an empty string if val is None 293 294 Args: 295 val: Integer value, or None if none 296 """ 297 args.append('' if val is None else '>%x' % val) 298 299 args = [' ' * entry.indent + entry.name] 300 _AppendHex(entry.image_pos) 301 _AppendHex(entry.size) 302 args.append(entry.etype) 303 _AppendHex(entry.offset) 304 _AppendHex(entry.uncomp_size) 305 return args 306 307 def _DoLine(lines, line): 308 """Add a line to the output list 309 310 This adds a line (a list of columns) to the output list. It also updates 311 the widths[] array with the maximum width of each column 312 313 Args: 314 lines: List of lines to add to 315 line: List of strings, one for each column 316 """ 317 for i, item in enumerate(line): 318 widths[i] = max(widths[i], len(item)) 319 lines.append(line) 320 321 def _NameInPaths(fname, entry_paths): 322 """Check if a filename is in a list of wildcarded paths 323 324 Args: 325 fname: Filename to check 326 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*', 327 'section/u-boot']) 328 329 Returns: 330 True if any wildcard matches the filename (using Unix filename 331 pattern matching, not regular expressions) 332 False if not 333 """ 334 for path in entry_paths: 335 if fnmatch.fnmatch(fname, path): 336 return True 337 return False 338 339 entries = self.BuildEntryList() 340 341 # This is our list of lines. Each item in the list is a list of strings, one 342 # for each column 343 lines = [] 344 HEADER = ['Name', 'Image-pos', 'Size', 'Entry-type', 'Offset', 345 'Uncomp-size'] 346 num_columns = len(HEADER) 347 348 # This records the width of each column, calculated as the maximum width of 349 # all the strings in that column 350 widths = [0] * num_columns 351 _DoLine(lines, HEADER) 352 353 # We won't print anything unless it has at least this indent. So at the 354 # start we will print nothing, unless a path matches (or there are no 355 # entry paths) 356 MAX_INDENT = 100 357 min_indent = MAX_INDENT 358 path_stack = [] 359 path = '' 360 indent = 0 361 selected_entries = [] 362 for entry in entries: 363 if entry.indent > indent: 364 path_stack.append(path) 365 elif entry.indent < indent: 366 path_stack.pop() 367 if path_stack: 368 path = path_stack[-1] + '/' + entry.name 369 indent = entry.indent 370 371 # If there are entry paths to match and we are not looking at a 372 # sub-entry of a previously matched entry, we need to check the path 373 if entry_paths and indent <= min_indent: 374 if _NameInPaths(path[1:], entry_paths): 375 # Print this entry and all sub-entries (=higher indent) 376 min_indent = indent 377 else: 378 # Don't print this entry, nor any following entries until we get 379 # a path match 380 min_indent = MAX_INDENT 381 continue 382 _DoLine(lines, _EntryToStrings(entry)) 383 selected_entries.append(entry) 384 return selected_entries, lines, widths 385 386 def GetImageSymbolValue(self, sym_name, optional, msg, base_addr): 387 """Get the value of a Binman symbol 388 389 Look up a Binman symbol and obtain its value. 390 391 This searches through this image including all of its subsections. 392 393 At present the only entry properties supported are: 394 offset 395 image_pos - 'base_addr' is added if this is not an end-at-4gb image 396 size 397 398 Args: 399 sym_name: Symbol name in the ELF file to look up in the format 400 _binman_<entry>_prop_<property> where <entry> is the name of 401 the entry and <property> is the property to find (e.g. 402 _binman_u_boot_prop_offset). As a special case, you can append 403 _any to <entry> to have it search for any matching entry. E.g. 404 _binman_u_boot_any_prop_offset will match entries called u-boot, 405 u-boot-img and u-boot-nodtb) 406 optional: True if the symbol is optional. If False this function 407 will raise if the symbol is not found 408 msg: Message to display if an error occurs 409 base_addr (int): Base address of image. This is added to the 410 returned value of image-pos so that the returned position 411 indicates where the targeted entry/binary has actually been 412 loaded 413 414 Returns: 415 Value that should be assigned to that symbol, or None if it was 416 optional and not found 417 418 Raises: 419 ValueError if the symbol is invalid or not found, or references a 420 property which is not supported 421 """ 422 entries = OrderedDict() 423 entries_by_name = {} 424 self._CollectEntries(entries, entries_by_name, self) 425 return self.GetSymbolValue(sym_name, optional, msg, base_addr, 426 entries_by_name) 427 428 def CollectBintools(self): 429 """Collect all the bintools used by this image 430 431 Returns: 432 Dict of bintools: 433 key: name of tool 434 value: Bintool object 435 """ 436 bintools = {} 437 super().AddBintools(bintools) 438 self.bintools = bintools 439 return bintools 440 441 def FdtContents(self, fdt_etype): 442 """This base-class implementation simply calls the state function""" 443 return state.GetFdtContents(fdt_etype) 444