+# SPDX-License-Identifier: GPL-2.0+
# Copyright (c) 2016 Google, Inc
# Written by Simon Glass <sjg@chromium.org>
#
-# SPDX-License-Identifier: GPL-2.0+
-#
# Class for an image, the output of binman
#
+from __future__ import print_function
+
from collections import OrderedDict
+import fnmatch
from operator import attrgetter
-
+import os
+import re
+import sys
+
+from entry import Entry
+from etype import fdtmap
+from etype import image_header
+from etype import section
+import fdt
import fdt_util
import tools
+import tout
-class Image:
+class Image(section.Entry_section):
"""A Image, representing an output from binman
An image is comprised of a collection of entries each containing binary
This class implements the various operations needed for images.
- Atrtributes:
- _node: Node object that contains the image definition in device tree
- _name: Image name
- _size: Image size in bytes, or None if not known yet
- _align_size: Image size alignment, or None
- _pad_before: Number of bytes before the first entry starts. This
- effectively changes the place where entry position 0 starts
- _pad_after: Number of bytes after the last entry ends. The last
- entry will finish on or before this boundary
- _pad_byte: Byte to use to pad the image where there is no entry
- _filename: Output filename for image
- _sort: True if entries should be sorted by position, False if they
- must be in-order in the device tree description
- _skip_at_start: Number of bytes before the first entry starts. These
- effecively adjust the starting position of entries. For example,
- if _pad_before is 16, then the first entry would start at 16.
- An entry with pos = 20 would in fact be written at position 4
- in the image file.
- _end_4gb: Indicates that the image ends at the 4GB boundary. This is
- used for x86 images, which want to use positions such that a
- memory address (like 0xff800000) is the first entry position.
- This causes _skip_at_start to be set to the starting memory
- address.
- _entries: OrderedDict() of entries
+ Attributes:
+ filename: Output filename for image
+ image_node: Name of node containing the description for this image
+ fdtmap_dtb: Fdt object for the fdtmap when loading from a file
+ fdtmap_data: Contents of the fdtmap when loading from a file
+ allow_repack: True to add properties to allow the image to be safely
+ repacked later
+
+ Args:
+ copy_to_orig: Copy offset/size to orig_offset/orig_size after reading
+ from the device tree
+ test: True if this is being called from a test of Images. This this case
+ there is no device tree defining the structure of the section, so
+ we create a section manually.
"""
- def __init__(self, name, node):
- global entry
- global Entry
- import entry
- from entry import Entry
-
- self._node = node
- self._name = name
- self._size = None
- self._align_size = None
- self._pad_before = 0
- self._pad_after = 0
- self._pad_byte = 0
- self._filename = '%s.bin' % self._name
- self._sort = False
- self._skip_at_start = 0
- self._end_4gb = False
- self._entries = OrderedDict()
-
- self._ReadNode()
- self._ReadEntries()
-
- def _ReadNode(self):
- """Read properties from the image node"""
- self._size = fdt_util.GetInt(self._node, 'size')
- self._align_size = fdt_util.GetInt(self._node, 'align-size')
- if tools.NotPowerOfTwo(self._align_size):
- self._Raise("Alignment size %s must be a power of two" %
- self._align_size)
- self._pad_before = fdt_util.GetInt(self._node, 'pad-before', 0)
- self._pad_after = fdt_util.GetInt(self._node, 'pad-after', 0)
- self._pad_byte = fdt_util.GetInt(self._node, 'pad-byte', 0)
+ def __init__(self, name, node, copy_to_orig=True, test=False):
+ section.Entry_section.__init__(self, None, 'section', node, test=test)
+ self.copy_to_orig = copy_to_orig
+ self.name = 'main-section'
+ self.image_name = name
+ self._filename = '%s.bin' % self.image_name
+ self.fdtmap_dtb = None
+ self.fdtmap_data = None
+ self.allow_repack = False
+ if not test:
+ self.ReadNode()
+
+ def ReadNode(self):
+ section.Entry_section.ReadNode(self)
filename = fdt_util.GetString(self._node, 'filename')
if filename:
self._filename = filename
- self._sort = fdt_util.GetBool(self._node, 'sort-by-pos')
- self._end_4gb = fdt_util.GetBool(self._node, 'end-at-4gb')
- if self._end_4gb and not self._size:
- self._Raise("Image size must be provided when using end-at-4gb")
- if self._end_4gb:
- self._skip_at_start = 0x100000000 - self._size
-
- def CheckSize(self):
- """Check that the image contents does not exceed its size, etc."""
- contents_size = 0
- for entry in self._entries.values():
- contents_size = max(contents_size, entry.pos + entry.size)
-
- contents_size -= self._skip_at_start
+ self.allow_repack = fdt_util.GetBool(self._node, 'allow-repack')
- size = self._size
- if not size:
- size = self._pad_before + contents_size + self._pad_after
- size = tools.Align(size, self._align_size)
+ @classmethod
+ def FromFile(cls, fname):
+ """Convert an image file into an Image for use in binman
- if self._size and contents_size > self._size:
- self._Raise("contents size %#x (%d) exceeds image size %#x (%d)" %
- (contents_size, contents_size, self._size, self._size))
- if not self._size:
- self._size = size
- if self._size != tools.Align(self._size, self._align_size):
- self._Raise("Size %#x (%d) does not match align-size %#x (%d)" %
- (self._size, self._size, self._align_size, self._align_size))
+ Args:
+ fname: Filename of image file to read
- def _Raise(self, msg):
- """Raises an error for this image
+ Returns:
+ Image object on success
- Args:
- msg: Error message to use in the raise string
Raises:
- ValueError()
+ ValueError if something goes wrong
"""
+ data = tools.ReadFile(fname)
+ size = len(data)
+
+ # First look for an image header
+ pos = image_header.LocateHeaderOffset(data)
+ if pos is None:
+ # Look for the FDT map
+ pos = fdtmap.LocateFdtmap(data)
+ if pos is None:
+ raise ValueError('Cannot find FDT map in image')
+
+ # We don't know the FDT size, so check its header first
+ probe_dtb = fdt.Fdt.FromData(
+ data[pos + fdtmap.FDTMAP_HDR_LEN:pos + 256])
+ dtb_size = probe_dtb.GetFdtObj().totalsize()
+ fdtmap_data = data[pos:pos + dtb_size + fdtmap.FDTMAP_HDR_LEN]
+ fdt_data = fdtmap_data[fdtmap.FDTMAP_HDR_LEN:]
+ out_fname = tools.GetOutputFilename('fdtmap.in.dtb')
+ tools.WriteFile(out_fname, fdt_data)
+ dtb = fdt.Fdt(out_fname)
+ dtb.Scan()
+
+ # Return an Image with the associated nodes
+ root = dtb.GetRoot()
+ image = Image('image', root, copy_to_orig=False)
+
+ image.image_node = fdt_util.GetString(root, 'image-node', 'image')
+ image.fdtmap_dtb = dtb
+ image.fdtmap_data = fdtmap_data
+ image._data = data
+ image._filename = fname
+ image.image_name, _ = os.path.splitext(fname)
+ return image
+
+ def Raise(self, msg):
+ """Convenience function to raise an error referencing an image"""
raise ValueError("Image '%s': %s" % (self._node.path, msg))
- def _ReadEntries(self):
- for node in self._node.subnodes:
- self._entries[node.name] = Entry.Create(self, node)
+ def PackEntries(self):
+ """Pack all entries into the image"""
+ section.Entry_section.Pack(self, 0)
- def FindEntryType(self, etype):
- """Find an entry type in the image
+ def SetImagePos(self):
+ # This first section in the image so it starts at 0
+ section.Entry_section.SetImagePos(self, 0)
+
+ def ProcessEntryContents(self):
+ """Call the ProcessContents() method for each entry
+
+ This is intended to adjust the contents as needed by the entry type.
- Args:
- etype: Entry type to find
Returns:
- entry matching that type, or None if not found
+ True if the new data size is OK, False if expansion is needed
"""
+ sizes_ok = True
for entry in self._entries.values():
- if entry.etype == etype:
- return entry
- return None
+ if not entry.ProcessContents():
+ sizes_ok = False
+ tout.Debug("Entry '%s' size change" % self._node.path)
+ return sizes_ok
- def GetEntryContents(self):
- """Call ObtainContents() for each entry
+ def WriteSymbols(self):
+ """Write symbol values into binary files for access at run time"""
+ section.Entry_section.WriteSymbols(self, self)
- This calls each entry's ObtainContents() a few times until they all
- return True. We stop calling an entry's function once it returns
- True. This allows the contents of one entry to depend on another.
+ def BuildImage(self):
+ """Write the image to a file"""
+ fname = tools.GetOutputFilename(self._filename)
+ tout.Info("Writing image to '%s'" % fname)
+ with open(fname, 'wb') as fd:
+ data = self.GetData()
+ fd.write(data)
+ tout.Info("Wrote %#x bytes" % len(data))
- After 3 rounds we give up since it's likely an error.
- """
- todo = self._entries.values()
- for passnum in range(3):
- next_todo = []
- for entry in todo:
- if not entry.ObtainContents():
- next_todo.append(entry)
- todo = next_todo
- if not todo:
- break
-
- def _SetEntryPosSize(self, name, pos, size):
- """Set the position and size of an entry
+ def WriteMap(self):
+ """Write a map of the image to a .map file
- Args:
- name: Entry name to update
- pos: New position
- size: New size
+ Returns:
+ Filename of map file written
"""
- entry = self._entries.get(name)
- if not entry:
- self._Raise("Unable to set pos/size for unknown entry '%s'" % name)
- entry.SetPositionSize(self._skip_at_start + pos, size)
+ filename = '%s.map' % self.image_name
+ fname = tools.GetOutputFilename(filename)
+ with open(fname, 'w') as fd:
+ print('%8s %8s %8s %s' % ('ImagePos', 'Offset', 'Size', 'Name'),
+ file=fd)
+ section.Entry_section.WriteMap(self, fd, 0)
+ return fname
- def GetEntryPositions(self):
- """Handle entries that want to set the position/size of other entries
+ def BuildEntryList(self):
+ """List the files in an image
- This calls each entry's GetPositions() method. If it returns a list
- of entries to update, it updates them.
+ Returns:
+ List of entry.EntryInfo objects describing all entries in the image
"""
- for entry in self._entries.values():
- pos_dict = entry.GetPositions()
- for name, info in pos_dict.iteritems():
- self._SetEntryPosSize(name, *info)
+ entries = []
+ self.ListEntries(entries, 0)
+ return entries
- def PackEntries(self):
- """Pack all entries into the image"""
- pos = self._skip_at_start
- for entry in self._entries.values():
- pos = entry.Pack(pos)
+ def FindEntryPath(self, entry_path):
+ """Find an entry at a given path in the image
- def _SortEntries(self):
- """Sort entries by position"""
- entries = sorted(self._entries.values(), key=lambda entry: entry.pos)
- self._entries.clear()
- for entry in entries:
- self._entries[entry._node.name] = entry
-
- def CheckEntries(self):
- """Check that entries do not overlap or extend outside the image"""
- if self._sort:
- self._SortEntries()
- pos = 0
- prev_name = 'None'
- for entry in self._entries.values():
- if (entry.pos < self._skip_at_start or
- entry.pos >= self._skip_at_start + self._size):
- entry.Raise("Position %#x (%d) is outside the image starting "
- "at %#x (%d)" %
- (entry.pos, entry.pos, self._skip_at_start,
- self._skip_at_start))
- if entry.pos < pos:
- entry.Raise("Position %#x (%d) overlaps with previous entry '%s' "
- "ending at %#x (%d)" %
- (entry.pos, entry.pos, prev_name, pos, pos))
- pos = entry.pos + entry.size
- prev_name = entry.GetPath()
+ Args:
+ entry_path: Path to entry (e.g. /ro-section/u-boot')
- def ProcessEntryContents(self):
- """Call the ProcessContents() method for each entry
+ Returns:
+ Entry object corresponding to that past
- This is intended to adjust the contents as needed by the entry type.
+ Raises:
+ ValueError if no entry found
"""
- for entry in self._entries.values():
- entry.ProcessContents()
+ parts = entry_path.split('/')
+ entries = self.GetEntries()
+ parent = '/'
+ for part in parts:
+ entry = entries.get(part)
+ if not entry:
+ raise ValueError("Entry '%s' not found in '%s'" %
+ (part, parent))
+ parent = entry.GetPath()
+ entries = entry.GetEntries()
+ return entry
+
+ def ReadData(self, decomp=True):
+ tout.Debug("Image '%s' ReadData(), size=%#x" %
+ (self.GetPath(), len(self._data)))
+ return self._data
+
+ def GetListEntries(self, entry_paths):
+ """List the entries in an image
+
+ This decodes the supplied image and returns a list of entries from that
+ image, preceded by a header.
- def BuildImage(self):
- """Write the image to a file"""
- fname = tools.GetOutputFilename(self._filename)
- with open(fname, 'wb') as fd:
- fd.write(chr(self._pad_byte) * self._size)
+ Args:
+ entry_paths: List of paths to match (each can have wildcards). Only
+ entries whose names match one of these paths will be printed
- for entry in self._entries.values():
- data = entry.GetData()
- fd.seek(self._pad_before + entry.pos - self._skip_at_start)
- fd.write(data)
+ Returns:
+ String error message if something went wrong, otherwise
+ 3-Tuple:
+ List of EntryInfo objects
+ List of lines, each
+ List of text columns, each a string
+ List of widths of each column
+ """
+ def _EntryToStrings(entry):
+ """Convert an entry to a list of strings, one for each column
+
+ Args:
+ entry: EntryInfo object containing information to output
+
+ Returns:
+ List of strings, one for each field in entry
+ """
+ def _AppendHex(val):
+ """Append a hex value, or an empty string if val is None
+
+ Args:
+ val: Integer value, or None if none
+ """
+ args.append('' if val is None else '>%x' % val)
+
+ args = [' ' * entry.indent + entry.name]
+ _AppendHex(entry.image_pos)
+ _AppendHex(entry.size)
+ args.append(entry.etype)
+ _AppendHex(entry.offset)
+ _AppendHex(entry.uncomp_size)
+ return args
+
+ def _DoLine(lines, line):
+ """Add a line to the output list
+
+ This adds a line (a list of columns) to the output list. It also updates
+ the widths[] array with the maximum width of each column
+
+ Args:
+ lines: List of lines to add to
+ line: List of strings, one for each column
+ """
+ for i, item in enumerate(line):
+ widths[i] = max(widths[i], len(item))
+ lines.append(line)
+
+ def _NameInPaths(fname, entry_paths):
+ """Check if a filename is in a list of wildcarded paths
+
+ Args:
+ fname: Filename to check
+ entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
+ 'section/u-boot'])
+
+ Returns:
+ True if any wildcard matches the filename (using Unix filename
+ pattern matching, not regular expressions)
+ False if not
+ """
+ for path in entry_paths:
+ if fnmatch.fnmatch(fname, path):
+ return True
+ return False
+
+ entries = self.BuildEntryList()
+
+ # This is our list of lines. Each item in the list is a list of strings, one
+ # for each column
+ lines = []
+ HEADER = ['Name', 'Image-pos', 'Size', 'Entry-type', 'Offset',
+ 'Uncomp-size']
+ num_columns = len(HEADER)
+
+ # This records the width of each column, calculated as the maximum width of
+ # all the strings in that column
+ widths = [0] * num_columns
+ _DoLine(lines, HEADER)
+
+ # We won't print anything unless it has at least this indent. So at the
+ # start we will print nothing, unless a path matches (or there are no
+ # entry paths)
+ MAX_INDENT = 100
+ min_indent = MAX_INDENT
+ path_stack = []
+ path = ''
+ indent = 0
+ selected_entries = []
+ for entry in entries:
+ if entry.indent > indent:
+ path_stack.append(path)
+ elif entry.indent < indent:
+ path_stack.pop()
+ if path_stack:
+ path = path_stack[-1] + '/' + entry.name
+ indent = entry.indent
+
+ # If there are entry paths to match and we are not looking at a
+ # sub-entry of a previously matched entry, we need to check the path
+ if entry_paths and indent <= min_indent:
+ if _NameInPaths(path[1:], entry_paths):
+ # Print this entry and all sub-entries (=higher indent)
+ min_indent = indent
+ else:
+ # Don't print this entry, nor any following entries until we get
+ # a path match
+ min_indent = MAX_INDENT
+ continue
+ _DoLine(lines, _EntryToStrings(entry))
+ selected_entries.append(entry)
+ return selected_entries, lines, widths