binman: Add support for flashrom FMAP

Add an entry which can hold an FMAP region as used by flashrom, an
open-source flashing tool used on Linux x86 machines. This provides a
simplified non-hierarchical view of the entries in the image and has a
signature at the start to allow flashrom to find it in the image.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/binman/README.entries b/tools/binman/README.entries
index 60cb248..0b3be69 100644
--- a/tools/binman/README.entries
+++ b/tools/binman/README.entries
@@ -26,6 +26,26 @@
 
 
 
+Entry: fmap: An entry which contains an Fmap section
+----------------------------------------------------
+
+Properties / Entry arguments:
+    None
+
+FMAP is a simple format used by flashrom, an open-source utility for
+reading and writing the SPI flash, typically on x86 CPUs. The format
+provides flashrom with a list of areas, so it knows what it in the flash.
+It can then read or write just a single area, instead of the whole flash.
+
+The format is defined by the flashrom project, in the file lib/fmap.h -
+see www.flashrom.org/Flashrom for more information.
+
+When used, this entry will be populated with an FMAP which reflects the
+entries in the current image. Note that any hierarchy is squashed, since
+FMAP does not support this.
+
+
+
 Entry: intel-cmc: Entry containing an Intel Chipset Micro Code (CMC) file
 -------------------------------------------------------------------------
 
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index dc09b81..8b910fe 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -361,6 +361,15 @@
         """
         self.WriteMapLine(fd, indent, self.name, self.offset, self.size)
 
+    def GetEntries(self):
+        """Return a list of entries contained by this entry
+
+        Returns:
+            List of entries, or None if none. A normal entry has no entries
+                within it so will return None
+        """
+        return None
+
     def GetArg(self, name, datatype=str):
         """Get the value of an entry argument or device-tree-node property
 
diff --git a/tools/binman/etype/fmap.py b/tools/binman/etype/fmap.py
new file mode 100644
index 0000000..f1dd81e
--- /dev/null
+++ b/tools/binman/etype/fmap.py
@@ -0,0 +1,61 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2018 Google, Inc
+# Written by Simon Glass <sjg@chromium.org>
+#
+# Entry-type module for a Flash map, as used by the flashrom SPI flash tool
+#
+
+from entry import Entry
+import fmap_util
+
+
+class Entry_fmap(Entry):
+    """An entry which contains an Fmap section
+
+    Properties / Entry arguments:
+        None
+
+    FMAP is a simple format used by flashrom, an open-source utility for
+    reading and writing the SPI flash, typically on x86 CPUs. The format
+    provides flashrom with a list of areas, so it knows what it in the flash.
+    It can then read or write just a single area, instead of the whole flash.
+
+    The format is defined by the flashrom project, in the file lib/fmap.h -
+    see www.flashrom.org/Flashrom for more information.
+
+    When used, this entry will be populated with an FMAP which reflects the
+    entries in the current image. Note that any hierarchy is squashed, since
+    FMAP does not support this.
+    """
+    def __init__(self, section, etype, node):
+        Entry.__init__(self, section, etype, node)
+
+    def _GetFmap(self):
+        """Build an FMAP from the entries in the current image
+
+        Returns:
+            FMAP binary data
+        """
+        def _AddEntries(areas, entry):
+            entries = entry.GetEntries()
+            if entries:
+                for subentry in entries.values():
+                    _AddEntries(areas, subentry)
+            else:
+                areas.append(fmap_util.FmapArea(entry.image_pos or 0,
+                                                entry.size or 0, entry.name, 0))
+
+        entries = self.section.GetEntries()
+        areas = []
+        for entry in entries.values():
+            _AddEntries(areas, entry)
+        return fmap_util.EncodeFmap(self.section.GetSize() or 0, self.name,
+                                    areas)
+
+    def ObtainContents(self):
+        """Obtain a placeholder for the fmap contents"""
+        self.SetContents(self._GetFmap())
+        return True
+
+    def ProcessContents(self):
+        self.SetContents(self._GetFmap())
diff --git a/tools/binman/etype/section.py b/tools/binman/etype/section.py
index 2e68f27..f5b2ed6 100644
--- a/tools/binman/etype/section.py
+++ b/tools/binman/etype/section.py
@@ -30,8 +30,8 @@
     hierarchical images to be created. See 'Sections and hierarchical images'
     in the binman README for more information.
     """
-    def __init__(self, image, etype, node):
-        Entry.__init__(self, image, etype, node)
+    def __init__(self, section, etype, node):
+        Entry.__init__(self, section, etype, node)
         self._section = bsection.Section(node.name, node)
 
     def ProcessFdt(self, fdt):
@@ -89,3 +89,6 @@
             fd: File to write the map to
         """
         self._section.WriteMap(fd, indent)
+
+    def GetEntries(self):
+        return self._section.GetEntries()
diff --git a/tools/binman/fmap_util.py b/tools/binman/fmap_util.py
new file mode 100644
index 0000000..7d520e3
--- /dev/null
+++ b/tools/binman/fmap_util.py
@@ -0,0 +1,109 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2018 Google, Inc
+# Written by Simon Glass <sjg@chromium.org>
+#
+# Support for flashrom's FMAP format. This supports a header followed by a
+# number of 'areas', describing regions of a firmware storage device,
+# generally SPI flash.
+
+import collections
+import struct
+
+# constants imported from lib/fmap.h
+FMAP_SIGNATURE = '__FMAP__'
+FMAP_VER_MAJOR = 1
+FMAP_VER_MINOR = 0
+FMAP_STRLEN = 32
+
+FMAP_AREA_STATIC = 1 << 0
+FMAP_AREA_COMPRESSED = 1 << 1
+FMAP_AREA_RO = 1 << 2
+
+FMAP_HEADER_LEN = 56
+FMAP_AREA_LEN = 42
+
+FMAP_HEADER_FORMAT = '<8sBBQI%dsH'% (FMAP_STRLEN)
+FMAP_AREA_FORMAT = '<II%dsH' % (FMAP_STRLEN)
+
+FMAP_HEADER_NAMES = (
+    'signature',
+    'ver_major',
+    'ver_minor',
+    'base',
+    'image_size',
+    'name',
+    'nareas',
+)
+
+FMAP_AREA_NAMES = (
+    'offset',
+    'size',
+    'name',
+    'flags',
+)
+
+# These are the two data structures supported by flashrom, a header (which
+# appears once at the start) and an area (which is repeated until the end of
+# the list of areas)
+FmapHeader = collections.namedtuple('FmapHeader', FMAP_HEADER_NAMES)
+FmapArea = collections.namedtuple('FmapArea', FMAP_AREA_NAMES)
+
+
+def ConvertName(field_names, fields):
+    """Convert a name to something flashrom likes
+
+    Flashrom requires upper case, underscores instead of hyphens. We remove any
+    null characters as well. This updates the 'name' value in fields.
+
+    Args:
+        field_names: List of field names for this struct
+        fields: Dict:
+            key: Field name
+            value: value of that field (string for the ones we support)
+    """
+    name_index = field_names.index('name')
+    fields[name_index] = fields[name_index].replace('\0', '').replace('-', '_').upper()
+
+def DecodeFmap(data):
+    """Decode a flashmap into a header and list of areas
+
+    Args:
+        data: Data block containing the FMAP
+
+    Returns:
+        Tuple:
+            header: FmapHeader object
+            List of FmapArea objects
+    """
+    fields = list(struct.unpack(FMAP_HEADER_FORMAT, data[:FMAP_HEADER_LEN]))
+    ConvertName(FMAP_HEADER_NAMES, fields)
+    header = FmapHeader(*fields)
+    areas = []
+    data = data[FMAP_HEADER_LEN:]
+    for area in range(header.nareas):
+        fields = list(struct.unpack(FMAP_AREA_FORMAT, data[:FMAP_AREA_LEN]))
+        ConvertName(FMAP_AREA_NAMES, fields)
+        areas.append(FmapArea(*fields))
+        data = data[FMAP_AREA_LEN:]
+    return header, areas
+
+def EncodeFmap(image_size, name, areas):
+    """Create a new FMAP from a list of areas
+
+    Args:
+        image_size: Size of image, to put in the header
+        name: Name of image, to put in the header
+        areas: List of FmapArea objects
+
+    Returns:
+        String containing the FMAP created
+    """
+    def _FormatBlob(fmt, names, obj):
+        params = [getattr(obj, name) for name in names]
+        return struct.pack(fmt, *params)
+
+    values = FmapHeader(FMAP_SIGNATURE, 1, 0, 0, image_size, name, len(areas))
+    blob = _FormatBlob(FMAP_HEADER_FORMAT, FMAP_HEADER_NAMES, values)
+    for area in areas:
+        blob += _FormatBlob(FMAP_AREA_FORMAT, FMAP_AREA_NAMES, area)
+    return blob
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 9c01805..bd4de4e 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -21,6 +21,7 @@
 import elf
 import fdt
 import fdt_util
+import fmap_util
 import test_util
 import tools
 import tout
@@ -1192,6 +1193,37 @@
         self.assertIn('Documentation is missing for modules: u_boot',
                       str(e.exception))
 
+    def testFmap(self):
+        """Basic test of generation of a flashrom fmap"""
+        data = self._DoReadFile('67_fmap.dts')
+        fhdr, fentries = fmap_util.DecodeFmap(data[32:])
+        expected = U_BOOT_DATA + '!' * 12 + U_BOOT_DATA + 'a' * 12
+        self.assertEqual(expected, data[:32])
+        self.assertEqual('__FMAP__', fhdr.signature)
+        self.assertEqual(1, fhdr.ver_major)
+        self.assertEqual(0, fhdr.ver_minor)
+        self.assertEqual(0, fhdr.base)
+        self.assertEqual(16 + 16 +
+                         fmap_util.FMAP_HEADER_LEN +
+                         fmap_util.FMAP_AREA_LEN * 3, fhdr.image_size)
+        self.assertEqual('FMAP', fhdr.name)
+        self.assertEqual(3, fhdr.nareas)
+        for fentry in fentries:
+            self.assertEqual(0, fentry.flags)
+
+        self.assertEqual(0, fentries[0].offset)
+        self.assertEqual(4, fentries[0].size)
+        self.assertEqual('RO_U_BOOT', fentries[0].name)
+
+        self.assertEqual(16, fentries[1].offset)
+        self.assertEqual(4, fentries[1].size)
+        self.assertEqual('RW_U_BOOT', fentries[1].name)
+
+        self.assertEqual(32, fentries[2].offset)
+        self.assertEqual(fmap_util.FMAP_HEADER_LEN +
+                         fmap_util.FMAP_AREA_LEN * 3, fentries[2].size)
+        self.assertEqual('FMAP', fentries[2].name)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/test/67_fmap.dts b/tools/binman/test/67_fmap.dts
new file mode 100644
index 0000000..9c0e293
--- /dev/null
+++ b/tools/binman/test/67_fmap.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		section@0 {
+			read-only;
+			name-prefix = "ro-";
+			size = <0x10>;
+			pad-byte = <0x21>;
+
+			u-boot {
+			};
+		};
+		section@1 {
+			name-prefix = "rw-";
+			size = <0x10>;
+			pad-byte = <0x61>;
+
+			u-boot {
+			};
+		};
+		fmap {
+		};
+	};
+};