binman: Add an image header

It is useful to be able to quickly locate the FDT map in the image. An
easy way to do this is with a pointer at the start or end of the image.

Add an 'image header' entry, which places a magic number followed by a
pointer to the FDT map. This can be located at the start or end of the
image, or at a chosen location.

As part of this, update GetSiblingImagePos() to detect missing siblings.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/binman/README b/tools/binman/README
index 8a5f332..ef62d1f 100644
--- a/tools/binman/README
+++ b/tools/binman/README
@@ -640,7 +640,9 @@
 
 Alternatively, an FDT map entry can be used to add a special FDT containing
 just the information about the image. This is preceded by a magic string so can
-be located anywhere in the image.
+be located anywhere in the image. An image header (typically at the start or end
+of the image) can be used to point to the FDT map. See fdtmap and image-header
+entries for more information.
 
 
 Compression
@@ -817,7 +819,6 @@
 - Add an option to decode an image into the constituent binaries
 - Support building an image for a board (-b) more completely, with a
   configurable build directory
-- Support putting the FDT in an image with a suitable magic number
 - Support listing files in images
 - Support logging of binman's operations, with different levels of verbosity
 - Support updating binaries in an image (with no size change / repacking)
diff --git a/tools/binman/README.entries b/tools/binman/README.entries
index 7014d36..598d827 100644
--- a/tools/binman/README.entries
+++ b/tools/binman/README.entries
@@ -331,6 +331,25 @@
 
 
 
+Entry: image-header: An entry which contains a pointer to the FDT map
+---------------------------------------------------------------------
+
+Properties / Entry arguments:
+    location: Location of header ("start" or "end" of image). This is
+        optional. If omitted then the entry must have an offset property.
+
+This adds an 8-byte entry to the start or end of the image, pointing to the
+location of the FDT map. The format is a magic number followed by an offset
+from the start or end of the image, in twos-compliment format.
+
+This entry must be in the top-level part of the image.
+
+NOTE: If the location is at the start/end, you will probably need to specify
+sort-by-offset for the image, unless you actually put the image header
+first/last in the entry list.
+
+
+
 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 7356c49..e1cd0d3 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -561,3 +561,14 @@
                 else False
         """
         return name in self.section.GetEntries()
+
+    def GetSiblingImagePos(self, name):
+        """Return the image position of the given sibling
+
+        Returns:
+            Image position of sibling, or None if the sibling has no position,
+                or False if there is no such sibling
+        """
+        if not self.HasSibling(name):
+            return False
+        return self.section.GetEntries()[name].image_pos
diff --git a/tools/binman/etype/image_header.py b/tools/binman/etype/image_header.py
new file mode 100644
index 0000000..9bc84ec
--- /dev/null
+++ b/tools/binman/etype/image_header.py
@@ -0,0 +1,76 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2018 Google, Inc
+# Written by Simon Glass <sjg@chromium.org>
+
+"""Entry-type module for an image header which points to the FDT map
+
+This creates an 8-byte entry with a magic number and the offset of the FDT map
+(which is another entry in the image), relative to the start or end of the
+image.
+"""
+
+import struct
+
+from entry import Entry
+import fdt_util
+
+IMAGE_HEADER_MAGIC = b'BinM'
+
+class Entry_image_header(Entry):
+    """An entry which contains a pointer to the FDT map
+
+    Properties / Entry arguments:
+        location: Location of header ("start" or "end" of image). This is
+            optional. If omitted then the entry must have an offset property.
+
+    This adds an 8-byte entry to the start or end of the image, pointing to the
+    location of the FDT map. The format is a magic number followed by an offset
+    from the start or end of the image, in twos-compliment format.
+
+    This entry must be in the top-level part of the image.
+
+    NOTE: If the location is at the start/end, you will probably need to specify
+    sort-by-offset for the image, unless you actually put the image header
+    first/last in the entry list.
+    """
+    def __init__(self, section, etype, node):
+        Entry.__init__(self, section, etype, node)
+        self.location = fdt_util.GetString(self._node, 'location')
+
+    def _GetHeader(self):
+        image_pos = self.GetSiblingImagePos('fdtmap')
+        if image_pos == False:
+            self.Raise("'image_header' section must have an 'fdtmap' sibling")
+        elif image_pos is None:
+            # This will be available when called from ProcessContents(), but not
+            # when called from ObtainContents()
+            offset = 0xffffffff
+        else:
+            image_size = self.section.GetImageSize() or 0
+            base = (0 if self.location != 'end' else image_size)
+            offset = (image_pos - base) & 0xffffffff
+        data = IMAGE_HEADER_MAGIC + struct.pack('<I', offset)
+        return data
+
+    def ObtainContents(self):
+        """Obtain a placeholder for the header contents"""
+        self.SetContents(self._GetHeader())
+        return True
+
+    def Pack(self, offset):
+        """Special pack method to set the offset to start/end of image"""
+        if not self.offset:
+            if self.location not in ['start', 'end']:
+                self.Raise("Invalid location '%s', expected 'start' or 'end'" %
+                           self.location)
+            image_size = self.section.GetImageSize() or 0
+            self.offset = (0 if self.location != 'end' else image_size - 8)
+        return Entry.Pack(self, offset)
+
+    def ProcessContents(self):
+        """Write an updated version of the FDT map to this entry
+
+        This is necessary since image_pos is not available when ObtainContents()
+        is called, since by then the entries have not been packed in the image.
+        """
+        self.SetContents(self._GetHeader())
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 934145c..6fdecb2 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -2076,7 +2076,7 @@
         # Mangle the section name, which should cause a mismatch between the
         # correct FDT path and the one expected by the section
         image = control.images['image']
-        image._section._node.path += '-suffix'
+        image._node.path += '-suffix'
         entries = image.GetEntries()
         fdtmap = entries['fdtmap']
         with self.assertRaises(ValueError) as e:
@@ -2084,6 +2084,51 @@
         self.assertIn("Cannot locate node for path '/binman-suffix'",
                       str(e.exception))
 
+    def testFdtmapHeader(self):
+        """Test an FDT map and image header can be inserted in the image"""
+        data = self.data = self._DoReadFileRealDtb('116_fdtmap_hdr.dts')
+        fdtmap_pos = len(U_BOOT_DATA)
+        fdtmap_data = data[fdtmap_pos:]
+        fdt_data = fdtmap_data[16:]
+        dtb = fdt.Fdt.FromData(fdt_data)
+        fdt_size = dtb.GetFdtObj().totalsize()
+        hdr_data = data[-8:]
+        self.assertEqual('BinM', hdr_data[:4])
+        offset = struct.unpack('<I', hdr_data[4:])[0] & 0xffffffff
+        self.assertEqual(fdtmap_pos - 0x400, offset - (1 << 32))
+
+    def testFdtmapHeaderStart(self):
+        """Test an image header can be inserted at the image start"""
+        data = self.data = self._DoReadFileRealDtb('117_fdtmap_hdr_start.dts')
+        fdtmap_pos = 0x100 + len(U_BOOT_DATA)
+        hdr_data = data[:8]
+        self.assertEqual('BinM', hdr_data[:4])
+        offset = struct.unpack('<I', hdr_data[4:])[0]
+        self.assertEqual(fdtmap_pos, offset)
+
+    def testFdtmapHeaderPos(self):
+        """Test an image header can be inserted at a chosen position"""
+        data = self.data = self._DoReadFileRealDtb('118_fdtmap_hdr_pos.dts')
+        fdtmap_pos = 0x100 + len(U_BOOT_DATA)
+        hdr_data = data[0x80:0x88]
+        self.assertEqual('BinM', hdr_data[:4])
+        offset = struct.unpack('<I', hdr_data[4:])[0]
+        self.assertEqual(fdtmap_pos, offset)
+
+    def testHeaderMissingFdtmap(self):
+        """Test an image header requires an fdtmap"""
+        with self.assertRaises(ValueError) as e:
+            self.data = self._DoReadFileRealDtb('119_fdtmap_hdr_missing.dts')
+        self.assertIn("'image_header' section must have an 'fdtmap' sibling",
+                      str(e.exception))
+
+    def testHeaderNoLocation(self):
+        """Test an image header with a no specified location is detected"""
+        with self.assertRaises(ValueError) as e:
+            self.data = self._DoReadFileRealDtb('120_hdr_no_location.dts')
+        self.assertIn("Invalid location 'None', expected 'start' or 'end'",
+                      str(e.exception))
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/test/116_fdtmap_hdr.dts b/tools/binman/test/116_fdtmap_hdr.dts
new file mode 100644
index 0000000..77a2194
--- /dev/null
+++ b/tools/binman/test/116_fdtmap_hdr.dts
@@ -0,0 +1,17 @@
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		size = <0x400>;
+		u-boot {
+		};
+		fdtmap {
+		};
+		image-header {
+			location = "end";
+		};
+	};
+};
diff --git a/tools/binman/test/117_fdtmap_hdr_start.dts b/tools/binman/test/117_fdtmap_hdr_start.dts
new file mode 100644
index 0000000..17b6be0
--- /dev/null
+++ b/tools/binman/test/117_fdtmap_hdr_start.dts
@@ -0,0 +1,19 @@
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		size = <0x400>;
+		sort-by-offset;
+		u-boot {
+			offset = <0x100>;
+		};
+		fdtmap {
+		};
+		image-header {
+			location = "start";
+		};
+	};
+};
diff --git a/tools/binman/test/118_fdtmap_hdr_pos.dts b/tools/binman/test/118_fdtmap_hdr_pos.dts
new file mode 100644
index 0000000..fd803f5
--- /dev/null
+++ b/tools/binman/test/118_fdtmap_hdr_pos.dts
@@ -0,0 +1,19 @@
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		size = <0x400>;
+		sort-by-offset;
+		u-boot {
+			offset = <0x100>;
+		};
+		fdtmap {
+		};
+		image-header {
+			offset = <0x80>;
+		};
+	};
+};
diff --git a/tools/binman/test/119_fdtmap_hdr_missing.dts b/tools/binman/test/119_fdtmap_hdr_missing.dts
new file mode 100644
index 0000000..41bb680
--- /dev/null
+++ b/tools/binman/test/119_fdtmap_hdr_missing.dts
@@ -0,0 +1,16 @@
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		sort-by-offset;
+		u-boot {
+		};
+		image-header {
+			offset = <0x80>;
+			location = "start";
+		};
+	};
+};
diff --git a/tools/binman/test/120_hdr_no_location.dts b/tools/binman/test/120_hdr_no_location.dts
new file mode 100644
index 0000000..585e21f
--- /dev/null
+++ b/tools/binman/test/120_hdr_no_location.dts
@@ -0,0 +1,16 @@
+/dts-v1/;
+
+/ {
+	#address-cells = <1>;
+	#size-cells = <1>;
+
+	binman {
+		sort-by-offset;
+		u-boot {
+		};
+		fdtmap {
+		};
+		image-header {
+		};
+	};
+};