Add decompression capability to apexd for compressed APEX

Apexd can now decompress a compressed APEX. We still haven't integrated
this new ability with the boot flow yet. It will be done in next CL.

Bug: 172911820
Test: atest ApexFileTest#Decompress
Test: atest ApexFileTest#DecompressWithoutProperSuffix

Change-Id: If8e6f7de86cbefecdb1e1ed955b39fc3f58521e2
diff --git a/apexd/Android.bp b/apexd/Android.bp
index b8542be..66b546f 100644
--- a/apexd/Android.bp
+++ b/apexd/Android.bp
@@ -386,6 +386,7 @@
     ":com.android.apex.cts.shim.v2_with_pre_install_hook_prebuilt",
     ":com.android.apex.cts.shim.v2_with_post_install_hook_prebuilt",
     ":com.android.apex.compressed.v1",
+    ":com.android.apex.compressed.v1_original",
     "apexd_testdata/com.android.apex.test_package.avbpubkey",
     "apexd_testdata/com.android.apex.compressed.avbpubkey",
   ],
diff --git a/apexd/apex_file.cpp b/apexd/apex_file.cpp
index d40e367..6169ad5 100644
--- a/apexd/apex_file.cpp
+++ b/apexd/apex_file.cpp
@@ -386,5 +386,55 @@
   return verity_data;
 }
 
+Result<void> ApexFile::Decompress(const std::string& dest_path) const {
+  const std::string& src_path = GetPath();
+
+  // We should decompress compressed APEX files only
+  if (!IsCompressed()) {
+    return ErrnoError() << "Cannot decompress an uncompressed APEX";
+  }
+
+  // Get file descriptor of the compressed apex file
+  unique_fd src_fd(open(src_path.c_str(), O_RDONLY | O_CLOEXEC));
+  if (src_fd.get() == -1) {
+    return ErrnoError() << "Failed to open compressed APEX " << GetPath();
+  }
+
+  // Open it as a zip file
+  ZipArchiveHandle handle;
+  int ret = OpenArchiveFd(src_fd.get(), src_path.c_str(), &handle, false);
+  if (ret < 0) {
+    return Error() << "Failed to open package " << src_path << ": "
+                   << ErrorCodeString(ret);
+  }
+  auto handle_guard =
+      android::base::make_scope_guard([&handle] { CloseArchive(handle); });
+
+  // Find the original apex file inside the zip and extract to dest
+  ZipEntry entry;
+  ret = FindEntry(handle, kCompressedApexFilename, &entry);
+  if (ret < 0) {
+    return Error() << "Could not find entry \"" << kCompressedApexFilename
+                   << "\" in package " << src_path << ": "
+                   << ErrorCodeString(ret);
+  }
+
+  // Open destination file descriptor
+  unique_fd dest_fd(
+      open(dest_path.c_str(), O_WRONLY | O_CLOEXEC | O_CREAT, 0644));
+  if (dest_fd.get() == -1) {
+    return ErrnoError() << "Failed to open decompression destination "
+                        << GetPath();
+  }
+  ret = ExtractEntryToFile(handle, &entry, dest_fd.get());
+  if (ret < 0) {
+    return Error() << "Could not decompress to file " << dest_path
+                   << ErrorCodeString(ret);
+  }
+
+  LOG(VERBOSE) << "Decompressed " << src_path << " to " << dest_path;
+  return {};
+}
+
 }  // namespace apex
 }  // namespace android
diff --git a/apexd/apex_file.h b/apexd/apex_file.h
index fcc51c6..2b3494e 100644
--- a/apexd/apex_file.h
+++ b/apexd/apex_file.h
@@ -54,6 +54,7 @@
   android::base::Result<ApexVerityData> VerifyApexVerity(
       const std::string& public_key) const;
   bool IsCompressed() const { return is_compressed_; }
+  android::base::Result<void> Decompress(const std::string& output_path) const;
 
  private:
   ApexFile(const std::string& apex_path,
diff --git a/apexd/apex_file_test.cpp b/apexd/apex_file_test.cpp
index cbf8051..98f5a1b 100644
--- a/apexd/apex_file_test.cpp
+++ b/apexd/apex_file_test.cpp
@@ -26,6 +26,8 @@
 #include <ziparchive/zip_archive.h>
 
 #include "apex_file.h"
+#include "apexd_test_utils.h"
+#include "apexd_utils.h"
 
 using android::base::GetExecutableDirectory;
 using android::base::Result;
@@ -46,7 +48,7 @@
 
 class ApexFileTest : public ::testing::TestWithParam<ApexFileTestParam> {};
 
-INSTANTIATE_TEST_SUITE_P(Apex, ApexFileTest, testing::ValuesIn(kParameters));
+INSTANTIATE_TEST_SUITE_P(Apex, ApexFileTest, ::testing::ValuesIn(kParameters));
 
 TEST_P(ApexFileTest, GetOffsetOfSimplePackage) {
   const std::string file_path = kTestDataDir + GetParam().prefix + ".apex";
@@ -81,7 +83,7 @@
   Result<ApexFile> apex_file = ApexFile::Open(file_path);
   ASSERT_FALSE(apex_file.ok());
   ASSERT_THAT(apex_file.error().message(),
-              testing::HasSubstr("Failed to open package"));
+              ::testing::HasSubstr("Failed to open package"));
 }
 
 TEST_P(ApexFileTest, GetApexManifest) {
@@ -161,7 +163,7 @@
   Result<ApexFile> apex_file = ApexFile::Open(file_path);
   ASSERT_FALSE(apex_file.ok());
   ASSERT_THAT(apex_file.error().message(),
-              testing::HasSubstr("Failed to retrieve filesystem type"));
+              ::testing::HasSubstr("Failed to retrieve filesystem type"));
 }
 
 TEST(ApexFileTest, OpenCompressedApexFile) {
@@ -182,7 +184,7 @@
   Result<ApexFile> apex_file = ApexFile::Open(file_path);
   ASSERT_FALSE(apex_file.ok());
   ASSERT_THAT(apex_file.error().message(),
-              testing::HasSubstr("Could not find entry"));
+              ::testing::HasSubstr("Could not find entry"));
 }
 
 TEST(ApexFileTest, GetCompressedApexManifest) {
@@ -221,6 +223,50 @@
       ::testing::HasSubstr("Cannot verify ApexVerity of compressed APEX"));
 }
 
+TEST(ApexFileTest, DecompressCompressedApex) {
+  const std::string file_path =
+      kTestDataDir + "com.android.apex.compressed.v1.capex";
+  Result<ApexFile> apex_file = ApexFile::Open(file_path);
+  ASSERT_RESULT_OK(apex_file);
+
+  // Create a temp dir for decompression
+  TemporaryDir tmp_dir;
+
+  const std::string package_name = apex_file->GetManifest().name();
+  const std::string decompression_file_path =
+      tmp_dir.path + package_name + ".capex";
+
+  auto result = apex_file->Decompress(decompression_file_path);
+  ASSERT_RESULT_OK(result);
+
+  // Assert output path is not empty
+  auto exists = PathExists(decompression_file_path);
+  ASSERT_RESULT_OK(exists);
+  ASSERT_TRUE(*exists) << decompression_file_path << " does not exist";
+
+  // Assert that decompressed apex is same as original apex
+  const std::string original_apex_file_path =
+      kTestDataDir + "com.android.apex.compressed.v1_original.apex";
+  auto comparison_result =
+      CompareFiles(original_apex_file_path, decompression_file_path);
+  ASSERT_RESULT_OK(comparison_result);
+  ASSERT_TRUE(*comparison_result);
+}
+
+TEST(ApexFileTest, DecompressFailForNormalApex) {
+  const std::string file_path =
+      kTestDataDir + "com.android.apex.compressed.v1_original.apex";
+  Result<ApexFile> apex_file = ApexFile::Open(file_path);
+  ASSERT_RESULT_OK(apex_file);
+
+  TemporaryFile decompression_file_path;
+
+  auto result = apex_file->Decompress(decompression_file_path.path);
+  ASSERT_FALSE(result.ok());
+  ASSERT_THAT(result.error().message(),
+              ::testing::HasSubstr("Cannot decompress an uncompressed APEX"));
+}
+
 }  // namespace
 }  // namespace apex
 }  // namespace android
diff --git a/apexd/apexd_test_utils.h b/apexd/apexd_test_utils.h
index 0e43477..6ee1927 100644
--- a/apexd/apexd_test_utils.h
+++ b/apexd/apexd_test_utils.h
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include <fstream>
+
 #include <android/apex/ApexInfo.h>
 #include <android/apex/ApexSessionInfo.h>
 #include <binder/IServiceManager.h>
@@ -22,6 +24,8 @@
 
 #include "session_state.pb.h"
 
+using android::base::Error;
+using android::base::Result;
 using apex::proto::SessionState;
 
 namespace android {
@@ -128,5 +132,20 @@
   *os << "}";
 }
 
+inline Result<bool> CompareFiles(const std::string& filename1,
+                                 const std::string& filename2) {
+  std::ifstream file1(filename1, std::ios::binary);
+  std::ifstream file2(filename2, std::ios::binary);
+
+  if (file1.bad() || file2.bad()) {
+    return Error() << "Could not open one of the file";
+  }
+
+  std::istreambuf_iterator<char> begin1(file1);
+  std::istreambuf_iterator<char> begin2(file2);
+
+  return std::equal(begin1, std::istreambuf_iterator<char>(), begin2);
+}
+
 }  // namespace apex
 }  // namespace android