binman: Support expanding entries

It is useful to have entries which can grow automatically to fill
available space. Add support for this.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/binman/README b/tools/binman/README
index d687194..6aa5b38 100644
--- a/tools/binman/README
+++ b/tools/binman/README
@@ -330,6 +330,10 @@
 	for each entry. This makes it easy to find out exactly where the entry
 	ended up in the image, regardless of parent sections, etc.
 
+expand-size:
+	Expand the size of this entry to fit available space. This space is only
+	limited by the size of the image/section and the position of the next
+	entry.
 
 The attributes supported for images are described below. Several are similar
 to those for entries.
diff --git a/tools/binman/bsection.py b/tools/binman/bsection.py
index 4bf2068..52ac31a 100644
--- a/tools/binman/bsection.py
+++ b/tools/binman/bsection.py
@@ -253,10 +253,26 @@
         for entry in entries:
             self._entries[entry._node.name] = entry
 
+    def _ExpandEntries(self):
+        """Expand any entries that are permitted to"""
+        exp_entry = None
+        for entry in self._entries.values():
+            if exp_entry:
+                exp_entry.ExpandToLimit(entry.offset)
+                exp_entry = None
+            if entry.expand_size:
+                exp_entry = entry
+        if exp_entry:
+            exp_entry.ExpandToLimit(self._size)
+
     def CheckEntries(self):
-        """Check that entries do not overlap or extend outside the section"""
+        """Check that entries do not overlap or extend outside the section
+
+        This also sorts entries, if needed and expands
+        """
         if self._sort:
             self._SortEntries()
+        self._ExpandEntries()
         offset = 0
         prev_name = 'None'
         for entry in self._entries.values():
@@ -419,3 +435,7 @@
                     return None
                 return entry.data
         source_entry.Raise("Cannot find entry for node '%s'" % node.name)
+
+    def ExpandSize(self, size):
+        if size != self._size:
+            self._size = size
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index 7316ad4..0915b47 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -76,6 +76,7 @@
         self.pad_after = 0
         self.offset_unset = False
         self.image_pos = None
+        self._expand_size = False
         if read_node:
             self.ReadNode()
 
@@ -161,6 +162,7 @@
                              "of two" % (self._node.path, self.align_size))
         self.align_end = fdt_util.GetInt(self._node, 'align-end')
         self.offset_unset = fdt_util.GetBool(self._node, 'offset-unset')
+        self.expand_size = fdt_util.GetBool(self._node, 'expand-size')
 
     def GetDefaultFilename(self):
         return None
@@ -507,3 +509,12 @@
                 break
             name = '%s.%s' % (node.name, name)
         return name
+
+    def ExpandToLimit(self, limit):
+        """Expand an entry so that it ends at the given offset limit"""
+        if self.offset + self.size < limit:
+            self.size = limit - self.offset
+            # Request the contents again, since changing the size requires that
+            # the data grows. This should not fail, but check it to be sure.
+            if not self.ObtainContents():
+                self.Raise('Cannot obtain contents when expanding entry')
diff --git a/tools/binman/etype/_testing.py b/tools/binman/etype/_testing.py
index 02c165c..3e345bd 100644
--- a/tools/binman/etype/_testing.py
+++ b/tools/binman/etype/_testing.py
@@ -48,6 +48,8 @@
                                                      'return-unknown-contents')
         self.bad_update_contents = fdt_util.GetBool(self._node,
                                                     'bad-update-contents')
+        self.return_contents_once = fdt_util.GetBool(self._node,
+                                                     'return-contents-once')
 
         # Set to True when the entry is ready to process the FDT.
         self.process_fdt_ready = False
@@ -68,12 +70,15 @@
             EntryArg('test-existing-prop', str)], self.require_args)
         if self.force_bad_datatype:
             self.GetEntryArgsOrProps([EntryArg('test-bad-datatype-arg', bool)])
+        self.return_contents = True
 
     def ObtainContents(self):
-        if self.return_unknown_contents:
+        if self.return_unknown_contents or not self.return_contents:
             return False
         self.data = 'a'
         self.contents_size = len(self.data)
+        if self.return_contents_once:
+            self.return_contents = False
         return True
 
     def GetOffsets(self):
diff --git a/tools/binman/etype/section.py b/tools/binman/etype/section.py
index a30cc91..005a9f9 100644
--- a/tools/binman/etype/section.py
+++ b/tools/binman/etype/section.py
@@ -40,6 +40,10 @@
     def ProcessFdt(self, fdt):
         return self._section.ProcessFdt(fdt)
 
+    def ExpandEntries(self):
+        Entry.ExpandEntries(self)
+        self._section.ExpandEntries()
+
     def AddMissingProperties(self):
         Entry.AddMissingProperties(self)
         self._section.AddMissingProperties()
@@ -95,3 +99,7 @@
 
     def GetEntries(self):
         return self._section.GetEntries()
+
+    def ExpandToLimit(self, limit):
+        super(Entry_section, self).ExpandToLimit(limit)
+        self._section.ExpandSize(self.size)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index e919e70..b156943 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -1579,6 +1579,35 @@
         self.assertIn("Node '/binman/files': Missing 'pattern' property",
                       str(e.exception))
 
+    def testExpandSize(self):
+        """Test an expanding entry"""
+        data, _, map_data, _ = self._DoReadFileDtb('88_expand_size.dts',
+                                                   map=True)
+        expect = ('a' * 8 + U_BOOT_DATA +
+                  MRC_DATA + 'b' * 1 + U_BOOT_DATA +
+                  'c' * 8 + U_BOOT_DATA +
+                  'd' * 8)
+        self.assertEqual(expect, data)
+        self.assertEqual('''ImagePos    Offset      Size  Name
+00000000  00000000  00000028  main-section
+00000000   00000000  00000008  fill
+00000008   00000008  00000004  u-boot
+0000000c   0000000c  00000004  section
+0000000c    00000000  00000003  intel-mrc
+00000010   00000010  00000004  u-boot2
+00000014   00000014  0000000c  section2
+00000014    00000000  00000008  fill
+0000001c    00000008  00000004  u-boot
+00000020   00000020  00000008  fill2
+''', map_data)
+
+    def testExpandSizeBad(self):
+        """Test an expanding entry which fails to provide contents"""
+        with self.assertRaises(ValueError) as e:
+            self._DoReadFileDtb('89_expand_size_bad.dts', map=True)
+        self.assertIn("Node '/binman/_testing': Cannot obtain contents when "
+                      'expanding entry', str(e.exception))
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/test/88_expand_size.dts b/tools/binman/test/88_expand_size.dts
new file mode 100644
index 0000000..c8a0130
--- /dev/null
+++ b/tools/binman/test/88_expand_size.dts
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		size = <40>;
+		fill {
+			expand-size;
+			fill-byte = [61];
+			size = <0>;
+		};
+		u-boot {
+			offset = <8>;
+		};
+		section {
+			expand-size;
+			pad-byte = <0x62>;
+			intel-mrc {
+			};
+		};
+		u-boot2 {
+			type = "u-boot";
+			offset = <16>;
+		};
+		section2 {
+			type = "section";
+			fill {
+				expand-size;
+				fill-byte = [63];
+				size = <0>;
+			};
+			u-boot {
+				offset = <8>;
+			};
+		};
+		fill2 {
+			type = "fill";
+			expand-size;
+			fill-byte = [64];
+			size = <0>;
+		};
+	};
+};
diff --git a/tools/binman/test/89_expand_size_bad.dts b/tools/binman/test/89_expand_size_bad.dts
new file mode 100644
index 0000000..edc0e5c
--- /dev/null
+++ b/tools/binman/test/89_expand_size_bad.dts
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		_testing {
+			expand-size;
+			return-contents-once;
+		};
+		u-boot {
+			offset = <8>;
+		};
+	};
+};