binman: Refactor much of the image code into 'section'

We want to support multiple sections within a single image. To do this,
move most of the Image class implementation into a new Section class. An
Image contains only a single Section, but at some point we will support
a new 'section' entry, thus allowing Sections within Sections.

Use the name 'bsection' for the module so we can use 'section' for the
etype module.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/binman/bsection.py b/tools/binman/bsection.py
new file mode 100644
index 0000000..07c72d3
--- /dev/null
+++ b/tools/binman/bsection.py
@@ -0,0 +1,302 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2018 Google, Inc
+# Written by Simon Glass <sjg@chromium.org>
+#
+# Base class for sections (collections of entries)
+#
+
+from __future__ import print_function
+
+from collections import OrderedDict
+import sys
+
+import fdt_util
+import re
+import tools
+
+class Section(object):
+    """A section which contains multiple entries
+
+    A section represents a collection of entries. There must be one or more
+    sections in an image. Sections are used to group entries together.
+
+    Attributes:
+        _node: Node object that contains the section definition in device tree
+        _size: Section size in bytes, or None if not known yet
+        _align_size: Section 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 section where there is no entry
+        _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
+            effectively 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 section 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
+    """
+    def __init__(self, name, node, test=False):
+        global entry
+        global Entry
+        import entry
+        from entry import Entry
+
+        self._node = node
+        self._size = None
+        self._align_size = None
+        self._pad_before = 0
+        self._pad_after = 0
+        self._pad_byte = 0
+        self._sort = False
+        self._skip_at_start = 0
+        self._end_4gb = False
+        self._entries = OrderedDict()
+        if not test:
+            self._ReadNode()
+            self._ReadEntries()
+
+    def _ReadNode(self):
+        """Read properties from the section 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)
+        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("Section size must be provided when using end-at-4gb")
+        if self._end_4gb:
+            self._skip_at_start = 0x100000000 - self._size
+
+    def _ReadEntries(self):
+        for node in self._node.subnodes:
+            self._entries[node.name] = Entry.Create(self, node)
+
+    def CheckSize(self):
+        """Check that the section 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
+
+        size = self._size
+        if not size:
+            size = self._pad_before + contents_size + self._pad_after
+            size = tools.Align(size, self._align_size)
+
+        if self._size and contents_size > self._size:
+            self._Raise("contents size %#x (%d) exceeds section 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))
+        return size
+
+    def _Raise(self, msg):
+        """Raises an error for this section
+
+        Args:
+            msg: Error message to use in the raise string
+        Raises:
+            ValueError()
+        """
+        raise ValueError("Section '%s': %s" % (self._node.path, msg))
+
+    def GetPath(self):
+        """Get the path of an image (in the FDT)
+
+        Returns:
+            Full path of the node for this image
+        """
+        return self._node.path
+
+    def FindEntryType(self, etype):
+        """Find an entry type in the section
+
+        Args:
+            etype: Entry type to find
+        Returns:
+            entry matching that type, or None if not found
+        """
+        for entry in self._entries.values():
+            if entry.etype == etype:
+                return entry
+        return None
+
+    def GetEntryContents(self):
+        """Call ObtainContents() for each entry
+
+        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.
+
+        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
+
+        Args:
+            name: Entry name to update
+            pos: New position
+            size: New size
+        """
+        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)
+
+    def GetEntryPositions(self):
+        """Handle entries that want to set the position/size of other entries
+
+        This calls each entry's GetPositions() method. If it returns a list
+        of entries to update, it updates them.
+        """
+        for entry in self._entries.values():
+            pos_dict = entry.GetPositions()
+            for name, info in pos_dict.iteritems():
+                self._SetEntryPosSize(name, *info)
+
+    def PackEntries(self):
+        """Pack all entries into the section"""
+        pos = self._skip_at_start
+        for entry in self._entries.values():
+            pos = entry.Pack(pos)
+
+    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 section"""
+        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 section 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()
+
+    def ProcessEntryContents(self):
+        """Call the ProcessContents() method for each entry
+
+        This is intended to adjust the contents as needed by the entry type.
+        """
+        for entry in self._entries.values():
+            entry.ProcessContents()
+
+    def WriteSymbols(self):
+        """Write symbol values into binary files for access at run time"""
+        for entry in self._entries.values():
+            entry.WriteSymbols(self)
+
+    def BuildSection(self, fd, base_pos):
+        """Write the section to a file"""
+        fd.seek(base_pos)
+        fd.write(self.GetData())
+
+    def GetData(self):
+        """Write the section to a file"""
+        section_data = chr(self._pad_byte) * self._size
+
+        for entry in self._entries.values():
+            data = entry.GetData()
+            base = self._pad_before + entry.pos - self._skip_at_start
+            section_data = (section_data[:base] + data +
+                            section_data[base + len(data):])
+        return section_data
+
+    def LookupSymbol(self, sym_name, optional, msg):
+        """Look up a symbol in an ELF file
+
+        Looks up a symbol in an ELF file. Only entry types which come from an
+        ELF image can be used by this function.
+
+        At present the only entry property supported is pos.
+
+        Args:
+            sym_name: Symbol name in the ELF file to look up in the format
+                _binman_<entry>_prop_<property> where <entry> is the name of
+                the entry and <property> is the property to find (e.g.
+                _binman_u_boot_prop_pos). As a special case, you can append
+                _any to <entry> to have it search for any matching entry. E.g.
+                _binman_u_boot_any_prop_pos will match entries called u-boot,
+                u-boot-img and u-boot-nodtb)
+            optional: True if the symbol is optional. If False this function
+                will raise if the symbol is not found
+            msg: Message to display if an error occurs
+
+        Returns:
+            Value that should be assigned to that symbol, or None if it was
+                optional and not found
+
+        Raises:
+            ValueError if the symbol is invalid or not found, or references a
+                property which is not supported
+        """
+        m = re.match(r'^_binman_(\w+)_prop_(\w+)$', sym_name)
+        if not m:
+            raise ValueError("%s: Symbol '%s' has invalid format" %
+                             (msg, sym_name))
+        entry_name, prop_name = m.groups()
+        entry_name = entry_name.replace('_', '-')
+        entry = self._entries.get(entry_name)
+        if not entry:
+            if entry_name.endswith('-any'):
+                root = entry_name[:-4]
+                for name in self._entries:
+                    if name.startswith(root):
+                        rest = name[len(root):]
+                        if rest in ['', '-img', '-nodtb']:
+                            entry = self._entries[name]
+        if not entry:
+            err = ("%s: Entry '%s' not found in list (%s)" %
+                   (msg, entry_name, ','.join(self._entries.keys())))
+            if optional:
+                print('Warning: %s' % err, file=sys.stderr)
+                return None
+            raise ValueError(err)
+        if prop_name == 'pos':
+            return entry.pos
+        else:
+            raise ValueError("%s: No such property '%s'" % (msg, prop_name))
+
+    def GetEntries(self):
+        return self._entries