imageloader: add GetComponentMetadata D-Bus method

This allows us to pack some information into the imageloader
manifest that will be signed but which we can access before we
actually load components. This information might be used to make
decisions about whether or not we actually do need to load the
component.

BUG=chromium:773471
TEST=deploy on device and test, new unit tests

Change-Id: Ia5e9f963e12dcd443851773f428d0d9f6c2104d8
Reviewed-on: https://chromium-review.googlesource.com/737428
Commit-Ready: Eric Caruso <ejcaruso@chromium.org>
Tested-by: Eric Caruso <ejcaruso@chromium.org>
Reviewed-by: Greg Kerr <kerrnel@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/component.cc b/component.cc
index a74128d..57ac327 100644
--- a/component.cc
+++ b/component.cc
@@ -48,6 +48,8 @@
 constexpr char kImageHashField[] = "image-sha256-hash";
 // The name of the bool field indicating whether component is removable.
 constexpr char kIsRemovableField[] = "is-removable";
+// The name of the metadata field.
+constexpr char kMetadataField[] = "metadata";
 // The name of the image file (squashfs).
 constexpr char kImageFileNameSquashFS[] = "image.squash";
 // The name of the image file (ext4).
@@ -160,6 +162,30 @@
   return true;
 }
 
+// Ensure the metadata entry is a dictionary mapping strings to strings and
+// parse it into |out_metadata| and return true if so.
+bool ParseMetadata(const base::Value* metadata_element,
+                   std::map<std::string, std::string>* out_metadata) {
+  DCHECK(out_metadata);
+
+  const base::DictionaryValue* metadata_dict = nullptr;
+  if (!metadata_element->GetAsDictionary(&metadata_dict))
+    return false;
+
+  base::DictionaryValue::Iterator it(*metadata_dict);
+  for (; !it.IsAtEnd(); it.Advance()) {
+    std::string parsed_value;
+    if (!it.value().GetAsString(&parsed_value)) {
+      LOG(ERROR) << "Key \"" << it.key() << "\" did not map to string value";
+      return false;
+    }
+
+    (*out_metadata)[it.key()] = std::move(parsed_value);
+  }
+
+  return true;
+}
+
 }  // namespace
 
 Component::Component(const base::FilePath& component_dir, int key_number)
@@ -290,6 +316,15 @@
     manifest_.is_removable = false;
   }
 
+  // Copy out the metadata, if it's there.
+  const base::Value* metadata = nullptr;
+  if (manifest_dict->Get(kMetadataField, &metadata)) {
+    if (!ParseMetadata(metadata, &(manifest_.metadata))) {
+      LOG(ERROR) << "Manifest metadata was malformed";
+      return false;
+    }
+  }
+
   return true;
 }
 
diff --git a/component.h b/component.h
index a463c83..0a1a522 100644
--- a/component.h
+++ b/component.h
@@ -15,6 +15,7 @@
 #ifndef IMAGELOADER_COMPONENT_H_
 #define IMAGELOADER_COMPONENT_H_
 
+#include <map>
 #include <memory>
 #include <string>
 #include <vector>
@@ -48,6 +49,7 @@
     std::string version;
     FileSystem fs_type;
     bool is_removable;
+    std::map<std::string, std::string> metadata;
   };
 
   // Creates a Component. Returns nullptr if initialization and verification
diff --git a/dbus_adaptors/org.chromium.ImageLoaderInterface.xml b/dbus_adaptors/org.chromium.ImageLoaderInterface.xml
index e667dda..b8a2a63 100644
--- a/dbus_adaptors/org.chromium.ImageLoaderInterface.xml
+++ b/dbus_adaptors/org.chromium.ImageLoaderInterface.xml
@@ -102,5 +102,20 @@
       </tp:docstring>
     </arg>
   </method>
+  <method name="GetComponentMetadata">
+    <tp:docstring>
+      Get the metadata for a registered component.
+    </tp:docstring>
+    <arg name="name" type="s" direction="in">
+      <tp:docstring>
+        The name of the component.
+      </tp:docstring>
+    </arg>
+    <arg name="metadata" type="a{ss}" direction="out">
+      <tp:docstring>
+        The metadata associated with this component.
+      </tp:docstring>
+    </arg>
+  </method>
 </interface>
 </node>
diff --git a/imageloader.cc b/imageloader.cc
index 6bdbb62..6a53c38 100644
--- a/imageloader.cc
+++ b/imageloader.cc
@@ -134,4 +134,14 @@
   return true;
 }
 
+bool ImageLoader::GetComponentMetadata(
+    brillo::ErrorPtr* err,
+    const std::string& name,
+    std::map<std::string, std::string>* out_metadata) {
+  if (!impl_.GetComponentMetadata(name, out_metadata))
+    out_metadata->clear();
+  PostponeShutdown();
+  return true;
+}
+
 }  // namespace imageloader
diff --git a/imageloader.h b/imageloader.h
index 88fe788..df53c07 100644
--- a/imageloader.h
+++ b/imageloader.h
@@ -60,6 +60,11 @@
   bool RemoveComponent(brillo::ErrorPtr* err, const std::string& name,
                        bool* out_success) override;
 
+  bool GetComponentMetadata(
+      brillo::ErrorPtr* err,
+      const std::string& name,
+      std::map<std::string, std::string>* out_metadata) override;
+
   // Sandboxes the runtime environment, using minijail. This is publicly exposed
   // so that imageloader_main.cc can sandbox when not running as a daemon.
   static void EnterSandbox();
diff --git a/imageloader_impl.cc b/imageloader_impl.cc
index 4c8d0f5..28081c2 100644
--- a/imageloader_impl.cc
+++ b/imageloader_impl.cc
@@ -15,7 +15,9 @@
 #include <base/files/file_path.h>
 #include <base/files/file_util.h>
 #include <base/files/important_file_writer.h>
+#include <base/json/json_string_value_serializer.h>
 #include <base/logging.h>
+#include <base/values.h>
 #include <base/version.h>
 #include <chromeos/dbus/service_constants.h>
 
@@ -227,6 +229,23 @@
   return component->manifest().version;
 }
 
+bool ImageLoaderImpl::GetComponentMetadata(
+     const std::string& name,
+     std::map<std::string, std::string>* out_metadata) {
+  base::FilePath component_path;
+  if (!GetPathToCurrentComponentVersion(name, &component_path)) {
+    return false;
+  }
+
+  std::unique_ptr<Component> component =
+      Component::Create(component_path, config_.keys);
+  if (!component)
+    return false;
+
+  *out_metadata = component->manifest().metadata;
+  return true;
+}
+
 base::FilePath ImageLoaderImpl::GetLatestVersionFilePath(
     const std::string& component_name) {
   return config_.storage_dir.Append(component_name).Append(kLatestVersionFile);
diff --git a/imageloader_impl.h b/imageloader_impl.h
index d09fb6e..7c84431 100644
--- a/imageloader_impl.h
+++ b/imageloader_impl.h
@@ -4,6 +4,7 @@
 #ifndef IMAGELOADER_IMAGELOADER_IMPL_H_
 #define IMAGELOADER_IMAGELOADER_IMPL_H_
 
+#include <map>
 #include <string>
 #include <vector>
 
@@ -44,6 +45,10 @@
   // Get component version given component name.
   std::string GetComponentVersion(const std::string& name);
 
+  // Get component metadata given component name.
+  bool GetComponentMetadata(const std::string& name,
+                            std::map<std::string, std::string>* out_metadata);
+
   // Load the specified component. This returns the mount point or an empty
   // string on failure.
   std::string LoadComponent(const std::string& name, HelperProcess* process);
diff --git a/imageloader_unittest.cc b/imageloader_unittest.cc
index b3fa252..03ef911 100644
--- a/imageloader_unittest.cc
+++ b/imageloader_unittest.cc
@@ -329,4 +329,47 @@
   ASSERT_TRUE(base::DirectoryExists(version_dir));
 }
 
+TEST_F(ImageLoaderTest, GetMetadata) {
+  ImageLoaderImpl loader(GetConfig(temp_dir_.value().c_str()));
+  ASSERT_TRUE(loader.RegisterComponent(kMetadataComponentName,
+                                       kTestOciComponentVersion,
+                                       GetMetadataComponentPath().value()));
+
+  // We shouldn't need to load the component to get the metadata.
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(loader.GetComponentMetadata(kMetadataComponentName, &metadata));
+  std::map<std::string, std::string> expected_metadata{
+    {"foo", "bar"},
+    {"baz", "quux"},
+  };
+  ASSERT_EQ(expected_metadata, metadata);
+}
+
+TEST_F(ImageLoaderTest, GetEmptyMetadata) {
+  ImageLoaderImpl loader(GetConfig(temp_dir_.value().c_str()));
+  ASSERT_TRUE(loader.RegisterComponent(kTestOciComponentName,
+                                       kTestOciComponentVersion,
+                                       GetTestOciComponentPath().value()));
+
+  // If there's no metadata, we should get nothing.
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(loader.GetComponentMetadata(kTestOciComponentName, &metadata));
+  ASSERT_TRUE(metadata.empty());
+}
+
+TEST_F(ImageLoaderTest, MetadataFailure) {
+  ImageLoaderImpl loader(GetConfig(temp_dir_.value().c_str()));
+  // Metadata is optional, but malformed metadata should not be present in the
+  // manifest. If it is, fail to load the component.
+  ASSERT_FALSE(loader.RegisterComponent(kBadMetadataComponentName,
+                                        kTestOciComponentVersion,
+                                        GetBadMetadataComponentPath().value()));
+
+  ASSERT_FALSE(loader.RegisterComponent(
+      kNonDictMetadataComponentName,
+      kTestOciComponentVersion,
+      GetNonDictMetadataComponentPath().value()));
+
+}
+
 }   // namespace imageloader
diff --git a/test_utilities.cc b/test_utilities.cc
index 54a8974..c3fa278 100644
--- a/test_utilities.cc
+++ b/test_utilities.cc
@@ -44,6 +44,18 @@
   return GetTestDataPath(kTestOciComponentName);
 }
 
+base::FilePath GetMetadataComponentPath() {
+  return GetTestDataPath(kMetadataComponentName);
+}
+
+base::FilePath GetBadMetadataComponentPath() {
+  return GetTestDataPath(kBadMetadataComponentName);
+}
+
+base::FilePath GetNonDictMetadataComponentPath() {
+  return GetTestDataPath(kNonDictMetadataComponentName);
+}
+
 void GetFilesInDir(const base::FilePath& dir, std::list<std::string>* files) {
   base::FileEnumerator file_enum(dir, false, base::FileEnumerator::FILES);
   for (base::FilePath name = file_enum.Next(); !name.empty();
diff --git a/test_utilities.h b/test_utilities.h
index 4dc101a..6596bde 100644
--- a/test_utilities.h
+++ b/test_utilities.h
@@ -47,8 +47,16 @@
 constexpr char kTestOciComponentName[] = "adb";
 constexpr char kTestOciComponentVersion[] = "5.1.1.13";
 
+// Equivalent component but with metadata in the manifest.
+constexpr char kMetadataComponentName[] = "adb-with-metadata";
+constexpr char kBadMetadataComponentName[] = "adb-with-bad-metadata";
+constexpr char kNonDictMetadataComponentName[] = "adb-with-non-dict-metadata";
+
 // Get the path of the test OCI component in the test data folder.
 base::FilePath GetTestOciComponentPath();
+base::FilePath GetMetadataComponentPath();
+base::FilePath GetBadMetadataComponentPath();
+base::FilePath GetNonDictMetadataComponentPath();
 
 // Enumerate all files in a directory and return them as a list of filenames.
 void GetFilesInDir(const base::FilePath& dir, std::list<std::string>* files);
diff --git a/testdata/adb-with-bad-metadata/image.squash b/testdata/adb-with-bad-metadata/image.squash
new file mode 120000
index 0000000..b84226f
--- /dev/null
+++ b/testdata/adb-with-bad-metadata/image.squash
@@ -0,0 +1 @@
+../adb/image.squash
\ No newline at end of file
diff --git a/testdata/adb-with-bad-metadata/imageloader.json b/testdata/adb-with-bad-metadata/imageloader.json
new file mode 100644
index 0000000..322c4cc
--- /dev/null
+++ b/testdata/adb-with-bad-metadata/imageloader.json
@@ -0,0 +1,7 @@
+{
+"manifest-version": 1,
+"version": "5.1.1.13",
+"image-sha256-hash": "f338966c4c716041ee9c98c80bdc22b1d4b9fdf12d9044fb675d263a49cd88fc",
+"table-sha256-hash": "c6c13757602ecba970d14e7da02fb5dae11b7e6b0bf071fd1a7e90d48b29d519",
+"metadata": {"foo": 2}
+}
diff --git a/testdata/adb-with-bad-metadata/imageloader.sig.2 b/testdata/adb-with-bad-metadata/imageloader.sig.2
new file mode 100644
index 0000000..459f48a
--- /dev/null
+++ b/testdata/adb-with-bad-metadata/imageloader.sig.2
Binary files differ
diff --git a/testdata/adb-with-bad-metadata/manifest.json b/testdata/adb-with-bad-metadata/manifest.json
new file mode 120000
index 0000000..c00aae1
--- /dev/null
+++ b/testdata/adb-with-bad-metadata/manifest.json
@@ -0,0 +1 @@
+../adb/manifest.json
\ No newline at end of file
diff --git a/testdata/adb-with-bad-metadata/table b/testdata/adb-with-bad-metadata/table
new file mode 120000
index 0000000..a8faefb
--- /dev/null
+++ b/testdata/adb-with-bad-metadata/table
@@ -0,0 +1 @@
+../adb/table
\ No newline at end of file
diff --git a/testdata/adb-with-metadata/image.squash b/testdata/adb-with-metadata/image.squash
new file mode 120000
index 0000000..b84226f
--- /dev/null
+++ b/testdata/adb-with-metadata/image.squash
@@ -0,0 +1 @@
+../adb/image.squash
\ No newline at end of file
diff --git a/testdata/adb-with-metadata/imageloader.json b/testdata/adb-with-metadata/imageloader.json
new file mode 100644
index 0000000..f76780c
--- /dev/null
+++ b/testdata/adb-with-metadata/imageloader.json
@@ -0,0 +1,7 @@
+{
+"manifest-version": 1,
+"version": "5.1.1.13",
+"image-sha256-hash": "f338966c4c716041ee9c98c80bdc22b1d4b9fdf12d9044fb675d263a49cd88fc",
+"table-sha256-hash": "c6c13757602ecba970d14e7da02fb5dae11b7e6b0bf071fd1a7e90d48b29d519",
+"metadata": {"foo": "bar", "baz": "quux"}
+}
diff --git a/testdata/adb-with-metadata/imageloader.sig.2 b/testdata/adb-with-metadata/imageloader.sig.2
new file mode 100644
index 0000000..4936146
--- /dev/null
+++ b/testdata/adb-with-metadata/imageloader.sig.2
Binary files differ
diff --git a/testdata/adb-with-metadata/manifest.json b/testdata/adb-with-metadata/manifest.json
new file mode 120000
index 0000000..c00aae1
--- /dev/null
+++ b/testdata/adb-with-metadata/manifest.json
@@ -0,0 +1 @@
+../adb/manifest.json
\ No newline at end of file
diff --git a/testdata/adb-with-metadata/table b/testdata/adb-with-metadata/table
new file mode 120000
index 0000000..a8faefb
--- /dev/null
+++ b/testdata/adb-with-metadata/table
@@ -0,0 +1 @@
+../adb/table
\ No newline at end of file
diff --git a/testdata/adb-with-non-dict-metadata/image.squash b/testdata/adb-with-non-dict-metadata/image.squash
new file mode 120000
index 0000000..b84226f
--- /dev/null
+++ b/testdata/adb-with-non-dict-metadata/image.squash
@@ -0,0 +1 @@
+../adb/image.squash
\ No newline at end of file
diff --git a/testdata/adb-with-non-dict-metadata/imageloader.json b/testdata/adb-with-non-dict-metadata/imageloader.json
new file mode 100644
index 0000000..eda8904
--- /dev/null
+++ b/testdata/adb-with-non-dict-metadata/imageloader.json
@@ -0,0 +1,7 @@
+{
+"manifest-version": 1,
+"version": "5.1.1.13",
+"image-sha256-hash": "f338966c4c716041ee9c98c80bdc22b1d4b9fdf12d9044fb675d263a49cd88fc",
+"table-sha256-hash": "c6c13757602ecba970d14e7da02fb5dae11b7e6b0bf071fd1a7e90d48b29d519",
+"metadata": ["foo", "bar"]
+}
diff --git a/testdata/adb-with-non-dict-metadata/imageloader.sig.2 b/testdata/adb-with-non-dict-metadata/imageloader.sig.2
new file mode 100644
index 0000000..c5b4345
--- /dev/null
+++ b/testdata/adb-with-non-dict-metadata/imageloader.sig.2
@@ -0,0 +1 @@
+0D 5ùpÙy(‰¾ÜÈo²µ3ýF+‚?ìKé*Ú$ C>…L+ªzÖØÿGèæ"iŸïôߕÕ]y›‘ž¹
\ No newline at end of file
diff --git a/testdata/adb-with-non-dict-metadata/manifest.json b/testdata/adb-with-non-dict-metadata/manifest.json
new file mode 120000
index 0000000..c00aae1
--- /dev/null
+++ b/testdata/adb-with-non-dict-metadata/manifest.json
@@ -0,0 +1 @@
+../adb/manifest.json
\ No newline at end of file
diff --git a/testdata/adb-with-non-dict-metadata/table b/testdata/adb-with-non-dict-metadata/table
new file mode 120000
index 0000000..a8faefb
--- /dev/null
+++ b/testdata/adb-with-non-dict-metadata/table
@@ -0,0 +1 @@
+../adb/table
\ No newline at end of file