Provide APEX list information to clients.

Convert the XML APEX information into objects, identifying APEX type (system, vendor,..)
based on the original path of APEX.

Add apex info cache to check for changes based on apex-info-list.xml file timestamp.

Bug: 239055387
Test: atest ApexUtil
Change-Id: I486aeac27b53882ea9a5c90d417252540aa834b3
diff --git a/libs/libapexutil/Android.bp b/libs/libapexutil/Android.bp
index 5ba3181..4bb78ae 100644
--- a/libs/libapexutil/Android.bp
+++ b/libs/libapexutil/Android.bp
@@ -19,38 +19,46 @@
 }
 
 cc_defaults {
-  name: "libapexutil-deps",
-  static_libs: [
-    "libbase",
-    "liblog",
-    "libprotobuf-cpp-lite",
-    "lib_apex_manifest_proto_lite",
-  ],
+    name: "libapexutil-deps",
+    static_libs: [
+        "libbase",
+        "liblog",
+        "libprotobuf-cpp-lite",
+        "lib_apex_manifest_proto_lite",
+        "libtinyxml2",
+    ],
 }
 
 cc_library_static {
-  name: "libapexutil",
-  defaults: ["libapexutil-deps"],
-  export_include_dirs: ["."],
-  srcs: ["apexutil.cpp"],
-  host_supported: true,
-  apex_available: [
-      "//apex_available:platform",
-      "com.android.runtime",
-  ],
+    name: "libapexutil",
+    defaults: ["libapexutil-deps"],
+    export_include_dirs: ["."],
+    srcs: [
+        "apex_info.cpp",
+        "apex_info_cache.cpp",
+        "apexutil.cpp",
+    ],
+    host_supported: true,
+    recovery_available: true,
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.runtime",
+    ],
+    generated_sources: ["apex-info-list-tinyxml"],
 }
 
 cc_test {
-  name: "libapexutil_tests",
-  srcs: ["*_test.cpp"],
-  defaults: ["libapexutil-deps"],
-  static_libs: [
-    "libapexutil",
-    "libgmock",
-  ],
-  test_suites: [
-    "device-tests",
-    "general-tests",
-  ],
-  host_supported: true,
+    name: "libapexutil_tests",
+    srcs: ["*_test.cpp"],
+    defaults: ["libapexutil-deps"],
+    static_libs: [
+        "libapexutil",
+        "libgmock",
+    ],
+    test_suites: [
+        "device-tests",
+        "general-tests",
+    ],
+    generated_sources: ["apex-info-list-tinyxml"],
+    host_supported: true,
 }
diff --git a/libs/libapexutil/apex_info.cpp b/libs/libapexutil/apex_info.cpp
new file mode 100644
index 0000000..1d0972f
--- /dev/null
+++ b/libs/libapexutil/apex_info.cpp
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "apex_info.h"
+
+#include <android-base/strings.h>
+
+#include "com_android_apex.h"
+
+namespace android {
+namespace apex {
+namespace util {
+
+using android::base::Error;
+using android::base::Result;
+using android::base::StartsWith;
+using namespace std::literals;
+
+namespace {
+
+bool InSystem(const std::string &original_path) {
+  return StartsWith(original_path, "/system/apex/") ||
+         StartsWith(original_path, "/system_ext/apex/") ||
+         // Guest mode Android may have system APEXes from host via block APEXes
+         StartsWith(original_path, "/dev/block/vd");
+}
+
+bool InProduct(const std::string &original_path) {
+  return StartsWith(original_path, "/product/apex/");
+}
+
+bool InVendor(const std::string &original_path) {
+  return StartsWith(original_path, "/vendor/apex/");
+}
+
+bool InOdm(const std::string &original_path) {
+  return StartsWith(original_path, "/odm/apex/");
+}
+
+Result<ApexType> GetType(const std::string &original_path) {
+  if (InSystem(original_path)) {
+    return ApexType::kSystem;
+  } else if (InProduct(original_path)) {
+    return ApexType::kProduct;
+  } else if (InVendor(original_path)) {
+    return ApexType::kVendor;
+  } else if (InOdm(original_path)) {
+    return ApexType::kOdm;
+  }
+
+  return Error() << "Unknown type based on path " << original_path;
+}
+
+} // namespace
+
+ApexInfo::ApexInfo(const std::string &manifest_name, const std::string &path,
+                   ApexType type)
+    : manifest_name_(manifest_name), path_(path), type_(type) {}
+
+Result<ApexInfoData> GetApexes(const std::string &apex_root,
+                               const std::string &info_list_file) {
+  ApexInfoData info;
+  // To avoid the overhead of parsing the apex data via GetActivePackages
+  // we will form the /apex path directly here and rely on the getIsActive
+  // in the info list.
+
+  auto info_list =
+      ::com::android::apex::readApexInfoList(info_list_file.c_str());
+  if (!info_list.has_value()) {
+    return Error() << "Failed to read apex info list " << info_list_file;
+  }
+  for (const auto &apex_info : info_list->getApexInfo()) {
+
+    // Only include active apexes
+    if (apex_info.hasIsActive() && apex_info.getIsActive()) {
+
+      // Get the pre-installed path of the apex. Normally (i.e. in Android),
+      // failing to find the pre-installed path is an assertion failure
+      // because apexd demands that every apex to have a pre-installed one.
+      // However, when this runs in a VM where apexes are seen as virtio block
+      // devices, the situation is different. If the APEX in the host side is
+      // an updated (or staged) one, the block device representing the APEX on
+      // the VM side doesn't have the pre-installed path because the factory
+      // version of the APEX wasn't exported to the VM. Therefore, we use the
+      // module path as original_path when we are running in a VM which can be
+      // guessed by checking if the path is /dev/block/vdN.
+      std::string path;
+      if (apex_info.hasPreinstalledModulePath()) {
+        path = apex_info.getPreinstalledModulePath();
+      } else if (StartsWith(apex_info.getModulePath(), "/dev/block/vd")) {
+        path = apex_info.getModulePath();
+      } else {
+        return Error() << "Failed to determine original path for apex "
+                       << apex_info.getModuleName() << " at " << info_list_file;
+      }
+      auto type = GetType(path);
+      if (!type.ok()) {
+        return type.error();
+      }
+      info.emplace_back(ApexInfo(apex_info.getModuleName(),
+                                 apex_root + "/"s + apex_info.getModuleName(),
+                                 *type));
+    }
+  }
+  return info;
+}
+
+} // namespace util
+} // namespace apex
+} // namespace android
diff --git a/libs/libapexutil/apex_info.h b/libs/libapexutil/apex_info.h
new file mode 100644
index 0000000..5b4cf6d
--- /dev/null
+++ b/libs/libapexutil/apex_info.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "apexutil.h"
+
+#include <list>
+#include <string>
+
+#include <android-base/result.h>
+
+namespace android {
+namespace apex {
+namespace util {
+
+// APEX types
+enum class ApexType {
+  kSystem,
+  kProduct,
+  kVendor,
+  kOdm,
+};
+
+// ApexInfo -- APEX active information
+class ApexInfo {
+public:
+  ApexInfo(const std::string &manifest_name, const std::string &path,
+           ApexType type);
+
+  // Manifest name from apex-info
+  const std::string &ManifestName() const { return manifest_name_; }
+  // Active mount path
+  const std::string &Path() const { return path_; }
+
+  ApexType Type() const { return type_; }
+
+private:
+  std::string manifest_name_;
+  std::string path_;
+  ApexType type_;
+};
+
+// Get list of active APEXs
+using ApexInfoData = std::vector<ApexInfo>;
+android::base::Result<ApexInfoData>
+GetApexes(const std::string &apex_root = kApexRoot,
+          const std::string &info_file_list = kApexInfoFile);
+
+} // namespace util
+} // namespace apex
+} // namespace android
diff --git a/libs/libapexutil/apex_info_cache.cpp b/libs/libapexutil/apex_info_cache.cpp
new file mode 100644
index 0000000..c2c41d0
--- /dev/null
+++ b/libs/libapexutil/apex_info_cache.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "apex_info_cache.h"
+
+namespace android {
+namespace apex {
+namespace util {
+
+using ::android::base::ErrnoError;
+using ::android::base::Error;
+using ::android::base::Result;
+using namespace std::literals;
+
+ApexInfoCache::ApexInfoCache(const std::string &apex_dir,
+                             const std::string &info_file)
+    : dir_(apex_dir), info_file_(info_file) {
+  memset(&mtim_, 0, sizeof(mtim_));
+}
+
+// Read the current modify time of the file, check if any update to stored time
+Result<std::pair<struct timespec, bool>> ApexInfoCache::ModifyTime() const {
+  struct stat cur;
+  auto ret = stat(info_file_.c_str(), &cur);
+  if (ret != 0) {
+    return ErrnoError() << " info file " << info_file_;
+  }
+  const bool is_new_file = (memcmp(&mtim_, &cur.st_mtim, sizeof(mtim_)) != 0);
+  return std::make_pair(cur.st_mtim, is_new_file);
+}
+
+// Check if new data is available
+Result<bool> ApexInfoCache::HasNewData() const {
+  auto ret = ModifyTime();
+  if (!ret.ok()) {
+    return ret.error();
+  }
+  return (*ret).second;
+}
+// Update the stored info held in object along with
+// the current modification timestamp of the info file.
+Result<bool> ApexInfoCache::Update() {
+  auto ret = ModifyTime();
+  if (!ret.ok()) {
+    return ret.error();
+  }
+  if (!(*ret).second) {
+    return false;
+  }
+
+  // Get latest
+  auto latest = GetApexes(dir_, info_file_);
+  if (!latest.ok()) {
+    return Error() << "Failed to read apexes from " << dir_ << " " << info_file_
+                   << " " << latest.error();
+  }
+
+  // stat timestamp could have changed between check and
+  // read this will get picked up on next poll
+  mtim_ = (*ret).first;
+  apex_info_ = std::move(*latest);
+  return true;
+}
+
+} // namespace util
+} // namespace apex
+} // namespace android
diff --git a/libs/libapexutil/apex_info_cache.h b/libs/libapexutil/apex_info_cache.h
new file mode 100644
index 0000000..da6f3b6
--- /dev/null
+++ b/libs/libapexutil/apex_info_cache.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "apex_info.h"
+#include "apexutil.h"
+
+#include <string>
+#include <sys/stat.h>
+
+#include <android-base/result.h>
+
+namespace android {
+namespace apex {
+namespace util {
+
+//
+// Cache current apex information
+//
+class ApexInfoCache {
+public:
+  ApexInfoCache(const std::string &apex_dir = kApexRoot,
+                const std::string &info_file = kApexInfoFile);
+
+  // Check if new data is available
+  android::base::Result<bool> HasNewData() const;
+
+  // Update ApexInfoData data held in the object, if change in
+  // timestamp is detected.  Return true if data has been updated
+  android::base::Result<bool> Update();
+
+  // Get copy of stored information
+  const ApexInfoData &Info() const { return apex_info_; }
+
+protected:
+  // Get the modify time, return true if update detected
+  android::base::Result<std::pair<struct timespec, bool>> ModifyTime() const;
+
+protected:
+  std::string dir_;
+  std::string info_file_;
+  struct timespec mtim_; // last modified time of the file
+  ApexInfoData apex_info_;
+};
+
+} // namespace util
+} // namespace apex
+} // namespace android
diff --git a/libs/libapexutil/apex_info_test.cpp b/libs/libapexutil/apex_info_test.cpp
new file mode 100644
index 0000000..24c3d62
--- /dev/null
+++ b/libs/libapexutil/apex_info_test.cpp
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "apex_info.h"
+#include "apex_info_cache.h"
+
+#include <string>
+#include <unistd.h>
+#include <utility>
+
+#include <android-base/file.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "com_android_apex.h"
+
+using namespace std::literals;
+
+using ::android::apex::util::ApexInfoCache;
+using ::android::apex::util::ApexType;
+using ::android::base::WriteStringToFile;
+
+// Create an ApexInfo object based on inputs, writing data to the apex_dir/name
+// directory
+com::android::apex::ApexInfo
+CreateApex(const std::string &name, const std::string &pre_path, int version) {
+  auto version_name = std::to_string(version);
+
+  std::string path = "/apex/" + name;
+  return (com::android::apex::ApexInfo(name, path, pre_path, version,
+                                       version_name, false, true, 0, false));
+}
+
+// Write info file
+void UpdateApexInfoList(const std::string &info_file,
+                        std::vector<com::android::apex::ApexInfo> &apex_infos) {
+  com::android::apex::ApexInfoList apex_info_list(apex_infos);
+  std::stringstream xml;
+  com::android::apex::write(xml, apex_info_list);
+  ASSERT_TRUE(WriteStringToFile(xml.str(), info_file));
+}
+
+void CreateApexData(const std::string &apex_dir, const std::string &info_file,
+                    std::vector<com::android::apex::ApexInfo> &apex_infos) {
+  ASSERT_NE(-1, mkdir(apex_dir.c_str(), 0755) == -1)
+      << "Failed to create apex directory" << apex_dir;
+
+  // Create partition of a given APEX type and add to apex info
+  auto ASSERT_CREATE_PARTITION_APEX = [&apex_dir, &apex_infos](
+                                          std::set<std::string> &names,
+                                          const std::string &partition) {
+    for (const auto &name : names) {
+      apex_infos.emplace_back(
+          CreateApex(name, std::string(partition).append("/").append(name), 1));
+    }
+  };
+
+  // Partition APEXes
+  std::set<std::string> vendor{"com.test_vendor.1", "com.test_vendor.2",
+                               "com.test_vendor.3"};
+  std::set<std::string> system{"com.test_system.1"};
+  std::set<std::string> system_ext{"com.test_system_ext.1"};
+  std::set<std::string> product{"com.test_product.1"};
+  std::set<std::string> odm{"com.test_odm.1"};
+
+  ASSERT_CREATE_PARTITION_APEX(vendor, "/vendor/apex");
+  ASSERT_CREATE_PARTITION_APEX(system, "/system/apex");
+  ASSERT_CREATE_PARTITION_APEX(system_ext, "/system_ext/apex");
+  ASSERT_CREATE_PARTITION_APEX(product, "/product/apex");
+  ASSERT_CREATE_PARTITION_APEX(odm, "/odm/apex");
+
+  // Write the info list
+  UpdateApexInfoList(info_file, apex_infos);
+
+  auto apex = android::apex::util::GetApexes(apex_dir, info_file);
+  auto exp_size = system.size() + system_ext.size() + product.size() +
+                  vendor.size() + odm.size();
+  ASSERT_TRUE(apex.ok());
+  ASSERT_EQ((*apex).size(), exp_size)
+      << "Got apex size " << (*apex).size() << " expected size " << exp_size;
+
+  for (const auto &obj : *apex) {
+    switch (obj.Type()) {
+    case (ApexType::kSystem):
+      ASSERT_EQ(system.count(obj.ManifestName()) +
+                    system_ext.count(obj.ManifestName()),
+                1);
+      system.erase(obj.ManifestName());
+      system_ext.erase(obj.ManifestName());
+      break;
+    case (ApexType::kProduct):
+      ASSERT_EQ(product.count(obj.ManifestName()), 1);
+      product.erase(obj.ManifestName());
+      break;
+    case (ApexType::kVendor):
+      ASSERT_EQ(vendor.count(obj.ManifestName()), 1);
+      vendor.erase(obj.ManifestName());
+      break;
+    case (ApexType::kOdm):
+      ASSERT_EQ(odm.count(obj.ManifestName()), 1);
+      odm.erase(obj.ManifestName());
+      break;
+    }
+  }
+  // Assert all modules identified
+  ASSERT_EQ(system.size(), 0);
+  ASSERT_EQ(system_ext.size(), 0);
+  ASSERT_EQ(product.size(), 0);
+  ASSERT_EQ(vendor.size(), 0);
+  ASSERT_EQ(odm.size(), 0);
+}
+
+TEST(ApexUtil, ApexInfo) {
+  TemporaryDir td;
+  const std::string apex_dir = td.path + "/apex/"s;
+  const std::string info_file = apex_dir + ::android::apex::kApexInfoFileName;
+
+  std::vector<com::android::apex::ApexInfo> apex_infos;
+  CreateApexData(apex_dir, info_file, apex_infos);
+}
+
+TEST(ApexUtil, ApexInfoCache) {
+  // Monitor based on Update()
+  TemporaryDir td;
+  const std::string apex_dir = td.path + "/apex/"s;
+  const std::string info_file = apex_dir + ::android::apex::kApexInfoFileName;
+
+  ASSERT_EQ(mkdir(apex_dir.c_str(), 0755), 0)
+      << "Failed to create apex directory" << apex_dir;
+
+  ApexInfoCache m(apex_dir, info_file);
+  auto ASSERT_NEW_DATA = [&m](bool bExp) {
+    auto ret = m.HasNewData();
+    ASSERT_EQ(ret.ok(), true);
+    ASSERT_EQ(*ret, bExp);
+  };
+  auto ASSERT_UPDATE = [&m](bool bExp) {
+    auto ret = m.Update();
+    ASSERT_EQ(ret.ok(), true);
+    ASSERT_EQ(*ret, bExp);
+  };
+  auto ASSERT_UPDATE_ERROR = [&m]() {
+    auto ret = m.Update();
+    ASSERT_EQ(ret.ok(), false);
+  };
+
+  ASSERT_UPDATE_ERROR();
+
+  // Need to setup actual data to use update
+  std::vector<com::android::apex::ApexInfo> apex_infos;
+  CreateApexData(apex_dir, info_file, apex_infos);
+
+  // validate new data is available
+  ASSERT_NEW_DATA(true);
+  ASSERT_UPDATE(true);
+
+  // No new updates
+  ASSERT_NEW_DATA(false);
+  ASSERT_UPDATE(false);
+
+  // Check existing file update
+  sleep(1); // make sure timestamps change
+  UpdateApexInfoList(info_file, apex_infos);
+  ASSERT_NEW_DATA(true);
+  ASSERT_UPDATE(true);
+
+  // Check that new data can be detected directly with update
+  sleep(1); // make sure timestamps change
+  UpdateApexInfoList(info_file, apex_infos);
+  ASSERT_UPDATE(true);
+  ASSERT_NEW_DATA(false); // new data should be consumed by above call
+}
diff --git a/libs/libapexutil/apexutil.h b/libs/libapexutil/apexutil.h
index f351294..e6c37e4 100644
--- a/libs/libapexutil/apexutil.h
+++ b/libs/libapexutil/apexutil.h
@@ -32,6 +32,8 @@
 GetActivePackages(const std::string &apex_root);
 
 constexpr const char *const kApexRoot = "/apex";
+constexpr const char *const kApexInfoFileName = "apex-info-list.xml";
+constexpr const char *const kApexInfoFile = "/apex/apex-info-list.xml";
 
 } // namespace apex
-} // namespace android
\ No newline at end of file
+} // namespace android