artd: delete optimized artifacts.

Bug: 225827974
Test: adb shell pm art delete-optimized-artifacts com.google.android.youtube
Ignore-AOSP-First: ART Services
Change-Id: I4d168ae17d67422139258f1968c717765e1ffeab
diff --git a/artd/Android.bp b/artd/Android.bp
index d4e2d47..292f09b 100644
--- a/artd/Android.bp
+++ b/artd/Android.bp
@@ -27,6 +27,7 @@
     defaults: ["art_defaults"],
     srcs: [
         "artd.cc",
+        "path_utils.cc",
     ],
     shared_libs: [
         "libarttools",
@@ -35,6 +36,7 @@
     ],
     static_libs: [
         "artd-aidl-ndk",
+        "libc++fs",
     ],
 }
 
@@ -64,8 +66,13 @@
 art_cc_defaults {
     name: "art_artd_tests_defaults",
     defaults: ["artd_defaults"],
+    static_libs: [
+        "libcap",
+        "libgmock",
+    ],
     srcs: [
         "artd_test.cc",
+        "path_utils_test.cc",
     ],
 }
 
diff --git a/artd/artd.cc b/artd/artd.cc
index 98693cb..e471954 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -19,8 +19,11 @@
 #include <stdlib.h>
 #include <unistd.h>
 
+#include <cstdint>
+#include <filesystem>
 #include <memory>
 #include <string>
+#include <system_error>
 #include <utility>
 #include <vector>
 
@@ -29,6 +32,7 @@
 #include "android-base/logging.h"
 #include "android-base/properties.h"
 #include "android-base/result.h"
+#include "android-base/stringprintf.h"
 #include "android-base/strings.h"
 #include "android/binder_auto_utils.h"
 #include "android/binder_manager.h"
@@ -36,6 +40,7 @@
 #include "base/array_ref.h"
 #include "base/file_utils.h"
 #include "oat_file_assistant.h"
+#include "path_utils.h"
 #include "runtime.h"
 #include "tools/tools.h"
 
@@ -50,6 +55,7 @@
 using ::android::base::GetBoolProperty;
 using ::android::base::Result;
 using ::android::base::Split;
+using ::android::base::StringPrintf;
 using ::ndk::ScopedAStatus;
 
 constexpr const char* kServiceName = "artd";
@@ -92,6 +98,28 @@
   return !GetBoolProperty("odsign.verification.success", /*default_value=*/false);
 }
 
+// Deletes a file. Returns the size of the deleted file, or 0 if the deleted file is empty or an
+// error occurs.
+int64_t GetSizeAndDeleteFile(const std::string& path) {
+  std::error_code ec;
+  int64_t size = std::filesystem::file_size(path, ec);
+  if (ec) {
+    // It is okay if the file does not exist. We don't have to log it.
+    if (ec.value() != ENOENT) {
+      LOG(ERROR) << StringPrintf(
+          "Failed to get the file size of '%s': %s", path.c_str(), ec.message().c_str());
+    }
+    return 0;
+  }
+
+  if (!std::filesystem::remove(path, ec)) {
+    LOG(ERROR) << StringPrintf("Failed to remove '%s': %s", path.c_str(), ec.message().c_str());
+    return 0;
+  }
+
+  return size;
+}
+
 }  // namespace
 
 ScopedAStatus Artd::isAlive(bool* _aidl_return) {
@@ -100,9 +128,18 @@
 }
 
 ScopedAStatus Artd::deleteArtifacts(const ArtifactsPath& in_artifactsPath, int64_t* _aidl_return) {
-  (void)in_artifactsPath;
-  (void)_aidl_return;
-  return ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+  Result<std::string> oat_path = BuildOatPath(in_artifactsPath);
+  if (!oat_path.ok()) {
+    return ScopedAStatus::fromExceptionCodeWithMessage(EX_ILLEGAL_STATE,
+                                                       oat_path.error().message().c_str());
+  }
+
+  *_aidl_return = 0;
+  *_aidl_return += GetSizeAndDeleteFile(*oat_path);
+  *_aidl_return += GetSizeAndDeleteFile(OatPathToVdexPath(*oat_path));
+  *_aidl_return += GetSizeAndDeleteFile(OatPathToArtPath(*oat_path));
+
+  return ScopedAStatus::ok();
 }
 
 ScopedAStatus Artd::getOptimizationStatus(const std::string& in_dexFile,
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index 14bccc2..f16a58e 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -16,26 +16,94 @@
 
 #include "artd.h"
 
+#include <sys/capability.h>
+
+#include <filesystem>
+#include <functional>
 #include <memory>
 
+#include "android-base/file.h"
+#include "android-base/logging.h"
+#include "android-base/scopeguard.h"
 #include "android/binder_interface_utils.h"
 #include "base/common_art_test.h"
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
 namespace art {
 namespace artd {
 namespace {
 
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::make_scope_guard;
+using ::android::base::ScopeGuard;
+using ::testing::_;
+using ::testing::ContainsRegex;
+using ::testing::HasSubstr;
+using ::testing::MockFunction;
+
+// A wrapper of `cap_t` that automatically calls `cap_free`.
+class ScopedCap {
+ public:
+  explicit ScopedCap(cap_t cap) : cap_(cap) { CHECK_NE(cap, nullptr); }
+
+  ScopedCap(ScopedCap&& other) : cap_(std::exchange(other.cap_, nullptr)) {}
+
+  ~ScopedCap() {
+    if (cap_ != nullptr) {
+      CHECK_EQ(cap_free(cap_), 0);
+    }
+  }
+
+  cap_t Get() const { return cap_; }
+
+ private:
+  cap_t cap_;
+};
+
+// Temporarily drops all root capabilities when the test is run as root. This is a noop otherwise.
+ScopeGuard<std::function<void()>> ScopedUnroot() {
+  ScopedCap old_cap(cap_get_proc());
+  ScopedCap new_cap(cap_dup(old_cap.Get()));
+  CHECK_EQ(cap_clear_flag(new_cap.Get(), CAP_EFFECTIVE), 0);
+  CHECK_EQ(cap_set_proc(new_cap.Get()), 0);
+  // `old_cap` is actually not shared with anyone else, but we have to wrap it with a `shared_ptr`
+  // because `std::function` requires captures to be copyable.
+  return make_scope_guard([old_cap = std::make_shared<ScopedCap>(std::move(old_cap))]() {
+    CHECK_EQ(cap_set_proc(old_cap->Get()), 0);
+  });
+}
+
+// Temporarily drops all permission on a file/directory.
+ScopeGuard<std::function<void()>> ScopedInaccessible(const std::string& path) {
+  std::filesystem::perms old_perms = std::filesystem::status(path).permissions();
+  std::filesystem::permissions(path, std::filesystem::perms::none);
+  return make_scope_guard([=]() { std::filesystem::permissions(path, old_perms); });
+}
+
+ScopeGuard<std::function<void()>> ScopedSetLogger(android::base::LogFunction&& logger) {
+  android::base::LogFunction old_logger = android::base::SetLogger(std::move(logger));
+  return make_scope_guard([old_logger = std::move(old_logger)]() mutable {
+    android::base::SetLogger(std::move(old_logger));
+  });
+}
+
 class ArtdTest : public CommonArtTest {
  protected:
   void SetUp() override {
     CommonArtTest::SetUp();
     artd_ = ndk::SharedRefBase::make<Artd>();
+    scratch_dir_ = std::make_unique<ScratchDir>();
   }
 
-  void TearDown() override { CommonArtTest::TearDown(); }
+  void TearDown() override {
+    scratch_dir_.reset();
+    CommonArtTest::TearDown();
+  }
 
   std::shared_ptr<Artd> artd_;
+  std::unique_ptr<ScratchDir> scratch_dir_;
+  MockFunction<android::base::LogFunction> mock_logger_;
 };
 
 TEST_F(ArtdTest, isAlive) {
@@ -44,6 +112,130 @@
   EXPECT_TRUE(result);
 }
 
+TEST_F(ArtdTest, deleteArtifacts) {
+  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::filesystem::create_directories(oat_dir);
+  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  android::base::WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 4 + 2 + 1);
+
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.vdex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
+}
+
+TEST_F(ArtdTest, deleteArtifactsMissingFile) {
+  // Missing VDEX file.
+  std::string oat_dir = dalvik_cache_ + "/arm64";
+  std::filesystem::create_directories(oat_dir);
+  android::base::WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex");  // 4 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art");     // 1 byte.
+
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = true,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 4 + 1);
+
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.dex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.art"));
+}
+
+TEST_F(ArtdTest, deleteArtifactsNoFile) {
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = android_data_ + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 0);
+}
+
+TEST_F(ArtdTest, deleteArtifactsPermissionDenied) {
+  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::filesystem::create_directories(oat_dir);
+  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  android::base::WriteStringToFile("ab", oat_dir + "/b.vdex");    // 2 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(3);
+
+  auto scoped_inaccessible = ScopedInaccessible(oat_dir);
+  auto scoped_unroot = ScopedUnroot();
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 0);
+}
+
+TEST_F(ArtdTest, deleteArtifactsFileIsDir) {
+  // VDEX file is a directory.
+  std::string oat_dir = scratch_dir_->GetPath() + "/a/oat/arm64";
+  std::filesystem::create_directories(oat_dir);
+  std::filesystem::create_directories(oat_dir + "/b.vdex");
+  android::base::WriteStringToFile("abcd", oat_dir + "/b.odex");  // 4 bytes.
+  android::base::WriteStringToFile("a", oat_dir + "/b.art");      // 1 byte.
+
+  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
+  EXPECT_CALL(mock_logger_,
+              Call(_, _, _, _, _, ContainsRegex(R"re(Failed to get the file size.*b\.vdex)re")))
+      .Times(1);
+
+  int64_t result = -1;
+  EXPECT_TRUE(artd_
+                  ->deleteArtifacts(
+                      ArtifactsPath{
+                          .dexPath = scratch_dir_->GetPath() + "/a/b.apk",
+                          .isa = "arm64",
+                          .isInDalvikCache = false,
+                      },
+                      &result)
+                  .isOk());
+  EXPECT_EQ(result, 4 + 1);
+
+  // The directory is kept because getting the file size failed.
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
+  EXPECT_TRUE(std::filesystem::exists(oat_dir + "/b.vdex"));
+  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
+}
+
 }  // namespace
 }  // namespace artd
 }  // namespace art
diff --git a/artd/path_utils.cc b/artd/path_utils.cc
new file mode 100644
index 0000000..c4d9031
--- /dev/null
+++ b/artd/path_utils.cc
@@ -0,0 +1,98 @@
+/*
+ * 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 "path_utils.h"
+
+#include <filesystem>
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/errors.h"
+#include "android-base/result.h"
+#include "android-base/strings.h"
+#include "arch/instruction_set.h"
+#include "base/file_utils.h"
+#include "oat_file_assistant.h"
+
+namespace art {
+namespace artd {
+
+namespace {
+
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::EndsWith;
+using ::android::base::Error;
+using ::android::base::Result;
+
+Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) {
+  if (path_str.empty()) {
+    return Errorf("Path is empty");
+  }
+  std::filesystem::path path(path_str);
+  if (!path.is_absolute()) {
+    return Errorf("Path '{}' is not an absolute path", path_str);
+  }
+  if (path.lexically_normal() != path_str) {
+    return Errorf("Path '{}' is not in normal form", path_str);
+  }
+  return {};
+}
+
+Result<void> ValidateDexPath(const std::string& dex_path) {
+  OR_RETURN(ValidateAbsoluteNormalPath(dex_path));
+  if (!EndsWith(dex_path, ".apk") && !EndsWith(dex_path, ".jar")) {
+    return Errorf("Dex path '{}' has an invalid extension", dex_path);
+  }
+  return {};
+}
+
+}  // namespace
+
+Result<std::string> BuildOatPath(const ArtifactsPath& artifacts_path) {
+  OR_RETURN(ValidateDexPath(artifacts_path.dexPath));
+
+  InstructionSet isa = GetInstructionSetFromString(artifacts_path.isa.c_str());
+  if (isa == InstructionSet::kNone) {
+    return Errorf("Instruction set '{}' is invalid", artifacts_path.isa.c_str());
+  }
+
+  std::string error_msg;
+  std::string path;
+  if (artifacts_path.isInDalvikCache) {
+    // Apps' OAT files are never in ART APEX data.
+    if (!OatFileAssistant::DexLocationToOatFilename(
+            artifacts_path.dexPath, isa, /*deny_art_apex_data_files=*/true, &path, &error_msg)) {
+      return Error() << error_msg;
+    }
+    return path;
+  } else {
+    if (!OatFileAssistant::DexLocationToOdexFilename(
+            artifacts_path.dexPath, isa, &path, &error_msg)) {
+      return Error() << error_msg;
+    }
+    return path;
+  }
+}
+
+std::string OatPathToVdexPath(const std::string& oat_path) {
+  return ReplaceFileExtension(oat_path, "vdex");
+}
+
+std::string OatPathToArtPath(const std::string& oat_path) {
+  return ReplaceFileExtension(oat_path, "art");
+}
+
+}  // namespace artd
+}  // namespace art
diff --git a/artd/path_utils.h b/artd/path_utils.h
new file mode 100644
index 0000000..970143a
--- /dev/null
+++ b/artd/path_utils.h
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#ifndef ART_ARTD_PATH_UTILS_H_
+#define ART_ARTD_PATH_UTILS_H_
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/result.h"
+
+namespace art {
+namespace artd {
+
+// Returns the absolute path to the OAT file built from the `ArtifactsPath`.
+android::base::Result<std::string> BuildOatPath(
+    const aidl::com::android::server::art::ArtifactsPath& artifacts_path);
+
+// Returns the path to the VDEX file that corresponds to the OAT file.
+std::string OatPathToVdexPath(const std::string& oat_path);
+
+// Returns the path to the ART file that corresponds to the OAT file.
+std::string OatPathToArtPath(const std::string& oat_path);
+
+}  // namespace artd
+}  // namespace art
+
+#endif  // ART_ARTD_PATH_UTILS_H_
diff --git a/artd/path_utils_test.cc b/artd/path_utils_test.cc
new file mode 100644
index 0000000..9ce40c5
--- /dev/null
+++ b/artd/path_utils_test.cc
@@ -0,0 +1,86 @@
+/*
+ * 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 "path_utils.h"
+
+#include "aidl/com/android/server/art/BnArtd.h"
+#include "android-base/result-gmock.h"
+#include "base/common_art_test.h"
+#include "gtest/gtest.h"
+
+namespace art {
+namespace artd {
+namespace {
+
+using ::aidl::com::android::server::art::ArtifactsPath;
+using ::android::base::testing::HasError;
+using ::android::base::testing::HasValue;
+using ::android::base::testing::WithMessage;
+
+class PathUtilsTest : public CommonArtTest {};
+
+TEST_F(PathUtilsTest, BuildOatPath) {
+  EXPECT_THAT(
+      BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
+      HasValue("/a/oat/arm64/b.odex"));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathDalvikCache) {
+  EXPECT_THAT(
+      BuildOatPath(ArtifactsPath{.dexPath = "/a/b.apk", .isa = "arm64", .isInDalvikCache = true}),
+      HasValue(android_data_ + "/dalvik-cache/arm64/a@b.apk@classes.dex"));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathEmptyDexPath) {
+  EXPECT_THAT(BuildOatPath(ArtifactsPath{.dexPath = "", .isa = "arm64", .isInDalvikCache = false}),
+              HasError(WithMessage("Path is empty")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathRelativeDexPath) {
+  EXPECT_THAT(
+      BuildOatPath(ArtifactsPath{.dexPath = "a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
+      HasError(WithMessage("Path 'a/b.apk' is not an absolute path")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathNonNormalDexPath) {
+  EXPECT_THAT(BuildOatPath(ArtifactsPath{
+                  .dexPath = "/a/c/../b.apk", .isa = "arm64", .isInDalvikCache = false}),
+              HasError(WithMessage("Path '/a/c/../b.apk' is not in normal form")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathInvalidDexExtension) {
+  EXPECT_THAT(BuildOatPath(ArtifactsPath{
+                  .dexPath = "/a/b.invalid", .isa = "arm64", .isInDalvikCache = false}),
+              HasError(WithMessage("Dex path '/a/b.invalid' has an invalid extension")));
+}
+
+TEST_F(PathUtilsTest, BuildOatPathInvalidIsa) {
+  EXPECT_THAT(BuildOatPath(
+                  ArtifactsPath{.dexPath = "/a/b.apk", .isa = "invalid", .isInDalvikCache = false}),
+              HasError(WithMessage("Instruction set 'invalid' is invalid")));
+}
+
+TEST_F(PathUtilsTest, OatPathToVdexPath) {
+  EXPECT_EQ(OatPathToVdexPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.vdex");
+}
+
+TEST_F(PathUtilsTest, OatPathToArtPath) {
+  EXPECT_EQ(OatPathToArtPath("/a/oat/arm64/b.odex"), "/a/oat/arm64/b.art");
+}
+
+}  // namespace
+}  // namespace artd
+}  // namespace art