Add dynamic_partition_metadata to update manifest

At build time, it will be filled with BOARD_SUPER_PARTITION_GROUPS,
BOARD_*_SIZE, and BOARD_*_PARTITION_LIST.

Only the information from the new target_files package is used. The
META/dynamic_partition_info.txt from old target_files is ignored.

Test: builds and manual OTA
Bug: 117182932
Change-Id: I02ce99caaf7d01cec1470f7262c45490c15dfcb7
diff --git a/payload_generator/generate_delta_main.cc b/payload_generator/generate_delta_main.cc
index 8240518..a7ec6da 100644
--- a/payload_generator/generate_delta_main.cc
+++ b/payload_generator/generate_delta_main.cc
@@ -404,6 +404,10 @@
   DEFINE_string(new_postinstall_config_file, "",
                 "A config file specifying postinstall related metadata. "
                 "Only allowed in major version 2 or newer.");
+  DEFINE_string(dynamic_partition_info_file,
+                "",
+                "An info file specifying dynamic partition metadata. "
+                "Only allowed in major version 2 or newer.");
 
   brillo::FlagHelper::Init(argc, argv,
       "Generates a payload to provide to ChromeOS' update_engine.\n\n"
@@ -553,6 +557,16 @@
   }
   CHECK(payload_config.target.LoadImageSize());
 
+  if (!FLAGS_dynamic_partition_info_file.empty()) {
+    LOG_IF(FATAL, FLAGS_major_version == kChromeOSMajorPayloadVersion)
+        << "Dynamic partition info is only allowed in major version 2 or "
+           "newer.";
+    brillo::KeyValueStore store;
+    CHECK(store.Load(base::FilePath(FLAGS_dynamic_partition_info_file)));
+    CHECK(payload_config.target.LoadDynamicPartitionMetadata(store));
+    CHECK(payload_config.target.ValidateDynamicPartitionMetadata());
+  }
+
   CHECK(!FLAGS_out_file.empty());
 
   // Ignore failures. These are optional arguments.
diff --git a/payload_generator/payload_file.cc b/payload_generator/payload_file.cc
index 13cf489..8f4399e 100644
--- a/payload_generator/payload_file.cc
+++ b/payload_generator/payload_file.cc
@@ -75,6 +75,13 @@
 
   manifest_.set_block_size(config.block_size);
   manifest_.set_max_timestamp(config.max_timestamp);
+
+  if (major_version_ == kBrilloMajorPayloadVersion) {
+    if (config.target.dynamic_partition_metadata != nullptr)
+      *(manifest_.mutable_dynamic_partition_metadata()) =
+          *(config.target.dynamic_partition_metadata);
+  }
+
   return true;
 }
 
diff --git a/payload_generator/payload_generation_config.cc b/payload_generator/payload_generation_config.cc
index c49fdb5..e7d8ae5 100644
--- a/payload_generator/payload_generation_config.cc
+++ b/payload_generator/payload_generation_config.cc
@@ -16,7 +16,13 @@
 
 #include "update_engine/payload_generator/payload_generation_config.h"
 
+#include <algorithm>
+#include <map>
+#include <utility>
+
 #include <base/logging.h>
+#include <base/strings/string_number_conversions.h>
+#include <brillo/strings/string_utils.h>
 
 #include "update_engine/common/utils.h"
 #include "update_engine/payload_consumer/delta_performer.h"
@@ -27,6 +33,8 @@
 #include "update_engine/payload_generator/mapfile_filesystem.h"
 #include "update_engine/payload_generator/raw_filesystem.h"
 
+using std::string;
+
 namespace chromeos_update_engine {
 
 bool PostInstallConfig::IsEmpty() const {
@@ -121,6 +129,74 @@
   return true;
 }
 
+bool ImageConfig::LoadDynamicPartitionMetadata(
+    const brillo::KeyValueStore& store) {
+  auto metadata = std::make_unique<DynamicPartitionMetadata>();
+  string buf;
+  if (!store.GetString("super_partition_groups", &buf)) {
+    LOG(ERROR) << "Dynamic partition info missing super_partition_groups.";
+    return false;
+  }
+  auto group_names = brillo::string_utils::Split(buf, " ");
+  for (const auto& group_name : group_names) {
+    DynamicPartitionGroup* group = metadata->add_groups();
+    group->set_name(group_name);
+    if (!store.GetString(group_name + "_size", &buf)) {
+      LOG(ERROR) << "Missing " << group_name + "_size.";
+      return false;
+    }
+
+    uint64_t max_size;
+    if (!base::StringToUint64(buf, &max_size)) {
+      LOG(ERROR) << group_name << "_size=" << buf << " is not an integer.";
+      return false;
+    }
+    group->set_size(max_size);
+
+    if (store.GetString(group_name + "_partition_list", &buf)) {
+      auto partition_names = brillo::string_utils::Split(buf, " ");
+      for (const auto& partition_name : partition_names) {
+        group->add_partition_names()->assign(partition_name);
+      }
+    }
+  }
+  dynamic_partition_metadata = std::move(metadata);
+  return true;
+}
+
+bool ImageConfig::ValidateDynamicPartitionMetadata() const {
+  if (dynamic_partition_metadata == nullptr) {
+    LOG(ERROR) << "dynamic_partition_metadata is not loaded.";
+    return false;
+  }
+
+  for (const auto& group : dynamic_partition_metadata->groups()) {
+    uint64_t sum_size = 0;
+    for (const auto& partition_name : group.partition_names()) {
+      auto partition_config = std::find_if(partitions.begin(),
+                                           partitions.end(),
+                                           [&partition_name](const auto& e) {
+                                             return e.name == partition_name;
+                                           });
+
+      if (partition_config == partitions.end()) {
+        LOG(ERROR) << "Cannot find partition " << partition_name
+                   << " which is in " << group.name() << "_partition_list";
+        return false;
+      }
+      sum_size += partition_config->size;
+    }
+
+    if (sum_size > group.size()) {
+      LOG(ERROR) << "Sum of sizes in " << group.name() << "_partition_list is "
+                 << sum_size << ", which is greater than " << group.name()
+                 << "_size (" << group.size() << ")";
+      return false;
+    }
+  }
+  return true;
+}
+
 bool ImageConfig::ImageInfoIsEmpty() const {
   return image_info.board().empty()
     && image_info.key().empty()
diff --git a/payload_generator/payload_generation_config.h b/payload_generator/payload_generation_config.h
index 38e7b10..2153ab0 100644
--- a/payload_generator/payload_generation_config.h
+++ b/payload_generator/payload_generation_config.h
@@ -137,6 +137,12 @@
   // Load verity config by parsing the partition images.
   bool LoadVerityConfig();
 
+  // Load dynamic partition info from a key value store.
+  bool LoadDynamicPartitionMetadata(const brillo::KeyValueStore& store);
+
+  // Validate |dynamic_partition_metadata| against |partitions|.
+  bool ValidateDynamicPartitionMetadata() const;
+
   // Returns whether the |image_info| field is empty.
   bool ImageInfoIsEmpty() const;
 
@@ -146,6 +152,9 @@
 
   // The updated partitions.
   std::vector<PartitionConfig> partitions;
+
+  // The super partition metadata.
+  std::unique_ptr<DynamicPartitionMetadata> dynamic_partition_metadata;
 };
 
 struct PayloadVersion {
diff --git a/payload_generator/payload_generation_config_unittest.cc b/payload_generator/payload_generation_config_unittest.cc
index 3545056..70a3df3 100644
--- a/payload_generator/payload_generation_config_unittest.cc
+++ b/payload_generator/payload_generation_config_unittest.cc
@@ -16,6 +16,8 @@
 
 #include "update_engine/payload_generator/payload_generation_config.h"
 
+#include <utility>
+
 #include <gtest/gtest.h>
 
 namespace chromeos_update_engine {
@@ -51,4 +53,93 @@
   EXPECT_TRUE(image_config.partitions[0].postinstall.IsEmpty());
 }
 
+TEST_F(PayloadGenerationConfigTest, LoadDynamicPartitionMetadataTest) {
+  ImageConfig image_config;
+  brillo::KeyValueStore store;
+  ASSERT_TRUE(
+      store.LoadFromString("super_partition_groups=group_a group_b\n"
+                           "group_a_size=3221225472\n"
+                           "group_a_partition_list=system product_services\n"
+                           "group_b_size=2147483648\n"
+                           "group_b_partition_list=vendor\n"));
+  EXPECT_TRUE(image_config.LoadDynamicPartitionMetadata(store));
+  ASSERT_NE(nullptr, image_config.dynamic_partition_metadata);
+
+  ASSERT_EQ(2, image_config.dynamic_partition_metadata->groups_size());
+
+  const auto& group_a = image_config.dynamic_partition_metadata->groups(0);
+  EXPECT_EQ("group_a", group_a.name());
+  EXPECT_EQ(3221225472u, group_a.size());
+  ASSERT_EQ(2, group_a.partition_names_size());
+  EXPECT_EQ("system", group_a.partition_names(0));
+  EXPECT_EQ("product_services", group_a.partition_names(1));
+
+  const auto& group_b = image_config.dynamic_partition_metadata->groups(1);
+  EXPECT_EQ("group_b", group_b.name());
+  EXPECT_EQ(2147483648u, group_b.size());
+  ASSERT_EQ(1, group_b.partition_names_size());
+  EXPECT_EQ("vendor", group_b.partition_names(0));
+}
+
+TEST_F(PayloadGenerationConfigTest,
+       LoadDynamicPartitionMetadataMissingSizeTest) {
+  ImageConfig image_config;
+  brillo::KeyValueStore store;
+  ASSERT_TRUE(
+      store.LoadFromString("super_partition_groups=foo\n"
+                           "foo_partition_list=baz\n"));
+  EXPECT_FALSE(image_config.LoadDynamicPartitionMetadata(store));
+  EXPECT_EQ(nullptr, image_config.dynamic_partition_metadata);
+}
+
+TEST_F(PayloadGenerationConfigTest, LoadDynamicPartitionMetadataBadSizeTest) {
+  ImageConfig image_config;
+  brillo::KeyValueStore store;
+  ASSERT_TRUE(
+      store.LoadFromString("super_partition_groups=foo\n"
+                           "foo_size=bar\n"
+                           "foo_partition_list=baz\n"));
+  EXPECT_FALSE(image_config.LoadDynamicPartitionMetadata(store));
+  EXPECT_EQ(nullptr, image_config.dynamic_partition_metadata);
+}
+
+TEST_F(PayloadGenerationConfigTest, ValidateDynamicPartitionMetadata) {
+  ImageConfig image_config;
+
+  PartitionConfig system("system");
+  system.size = 2147483648u;
+  PartitionConfig product_services("product_services");
+  product_services.size = 1073741824u;
+
+  image_config.partitions.push_back(std::move(system));
+  image_config.partitions.push_back(std::move(product_services));
+
+  brillo::KeyValueStore store;
+  ASSERT_TRUE(
+      store.LoadFromString("super_partition_groups=foo\n"
+                           "foo_size=3221225472\n"
+                           "foo_partition_list=system product_services\n"));
+  EXPECT_TRUE(image_config.LoadDynamicPartitionMetadata(store));
+  EXPECT_NE(nullptr, image_config.dynamic_partition_metadata);
+
+  EXPECT_TRUE(image_config.ValidateDynamicPartitionMetadata());
+}
+
+TEST_F(PayloadGenerationConfigTest, ValidateDynamicPartitionMetadataTooBig) {
+  ImageConfig image_config;
+
+  PartitionConfig system("system");
+  system.size = 4294967296u;
+  image_config.partitions.push_back(std::move(system));
+
+  brillo::KeyValueStore store;
+  ASSERT_TRUE(
+      store.LoadFromString("super_partition_groups=foo\n"
+                           "foo_size=3221225472\n"
+                           "foo_partition_list=system\n"));
+  EXPECT_TRUE(image_config.LoadDynamicPartitionMetadata(store));
+  EXPECT_NE(nullptr, image_config.dynamic_partition_metadata);
+
+  EXPECT_FALSE(image_config.ValidateDynamicPartitionMetadata());
+}
 }  // namespace chromeos_update_engine
diff --git a/scripts/brillo_update_payload b/scripts/brillo_update_payload
index 47ac830..e66edae 100755
--- a/scripts/brillo_update_payload
+++ b/scripts/brillo_update_payload
@@ -258,6 +258,9 @@
 # Path to the postinstall config file in target image if exists.
 POSTINSTALL_CONFIG_FILE=""
 
+# Path to the dynamic partition info file in target image if exists.
+DYNAMIC_PARTITION_INFO_FILE=""
+
 # read_option_int <file.txt> <option_key> [default_value]
 #
 # Reads the unsigned integer value associated with |option_key| in a key=value
@@ -512,6 +515,12 @@
         >"${postinstall_config}"; then
       POSTINSTALL_CONFIG_FILE="${postinstall_config}"
     fi
+    local dynamic_partitions_info=$(create_tempfile "dynamic_partitions_info.XXXXXX")
+    CLEANUP_FILES+=("${dynamic_partitions_info}")
+    if unzip -p "${image}" "META/dynamic_partitions_info.txt" \
+        >"${dynamic_partitions_info}"; then
+      DYNAMIC_PARTITION_INFO_FILE="${dynamic_partitions_info}"
+    fi
   fi
 
   local part
@@ -636,6 +645,12 @@
     )
   fi
 
+  if [[ -n "{DYNAMIC_PARTITION_INFO_FILE}" ]]; then
+    GENERATOR_ARGS+=(
+      --dynamic_partition_info_file="${DYNAMIC_PARTITION_INFO_FILE}"
+    )
+  fi
+
   echo "Running delta_generator with args: ${GENERATOR_ARGS[@]}"
   "${GENERATOR}" "${GENERATOR_ARGS[@]}"
 
diff --git a/update_metadata.proto b/update_metadata.proto
index f90ec3c..42e7654 100644
--- a/update_metadata.proto
+++ b/update_metadata.proto
@@ -273,6 +273,29 @@
   optional uint32 fec_roots = 16 [default = 2];
 }
 
+message DynamicPartitionGroup {
+  // Name of the group.
+  required string name = 1;
+
+  // Maximum size of the group. The sum of sizes of all partitions in the group
+  // must not exceed the maximum size of the group.
+  optional uint64 size = 2;
+
+  // A list of partitions that belong to the group.
+  repeated string partition_names = 3;
+}
+
+// Metadata related to all dynamic partitions.
+message DynamicPartitionMetadata {
+  // All updateable groups present in |partitions| of this DeltaArchiveManifest.
+  // - If an updatable group is on the device but not in the manifest, it is
+  //   not updated. Hence, the group will not be resized, and partitions cannot
+  //   be added to or removed from the group.
+  // - If an updatable group is in the manifest but not on the device, the group
+  //   is added to the device.
+  repeated DynamicPartitionGroup groups = 1;
+}
+
 message DeltaArchiveManifest {
   // Only present in major version = 1. List of install operations for the
   // kernel and rootfs partitions. For major version = 2 see the |partitions|
@@ -317,4 +340,7 @@
   // The maximum timestamp of the OS allowed to apply this payload.
   // Can be used to prevent downgrading the OS.
   optional int64 max_timestamp = 14;
+
+  // Metadata related to all dynamic partitions.
+  optional DynamicPartitionMetadata dynamic_partition_metadata = 15;
 }