odrefresh: move file-system utilities into odr_fs_utils.{h,cc}

Preparation for metrics support which will use the same functionality.

Add tests for odrefresh file-system utilities too.

Bug: 160683548
Test: atest art_odrefresh_tests
Change-Id: I1da2f477f51b1d445bbee3b22a9c1fb268d25e80
diff --git a/odrefresh/Android.bp b/odrefresh/Android.bp
index 8e39edf..6f84e8f 100644
--- a/odrefresh/Android.bp
+++ b/odrefresh/Android.bp
@@ -29,6 +29,7 @@
     defaults: ["art_defaults"],
     srcs: [
         "odrefresh.cc",
+        "odr_fs_utils.cc",
     ],
     local_include_dirs: ["include"],
     header_libs: ["dexoptanalyzer_headers"],
@@ -128,6 +129,8 @@
     header_libs: ["odrefresh_headers"],
     srcs: [
         "odr_artifacts_test.cc",
+        "odr_fs_utils.cc",
+        "odr_fs_utils_test.cc",
         "odrefresh_test.cc",
     ],
     shared_libs: [
diff --git a/odrefresh/odr_config.h b/odrefresh/odr_config.h
index 6b0f212..4161944 100644
--- a/odrefresh/odr_config.h
+++ b/odrefresh/odr_config.h
@@ -36,7 +36,7 @@
   kZygote32_64 = 1,
   // 64-bit primary zygote, 32-bit secondary zygote.
   kZygote64_32 = 2,
-  // 64-bit praimry zygote, no secondary zygote.
+  // 64-bit primary zygote, no secondary zygote.
   kZygote64 = 3
 };
 
diff --git a/odrefresh/odr_fs_utils.cc b/odrefresh/odr_fs_utils.cc
new file mode 100644
index 0000000..9f3a68b
--- /dev/null
+++ b/odrefresh/odr_fs_utils.cc
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2021 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 "odr_fs_utils.h"
+
+#include <dirent.h>
+#include <ftw.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <unistd.h>
+
+#include <iosfwd>
+#include <memory>
+#include <ostream>
+#include <queue>
+#include <string>
+#include <string_view>
+#include <type_traits>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/macros.h>
+#include <android-base/strings.h>
+#include <base/os.h>
+
+namespace art {
+namespace odrefresh {
+
+// Callback for use with nftw(3) to assist with clearing files and sub-directories.
+// This method removes files and directories below the top-level directory passed to nftw().
+static int NftwCleanUpCallback(const char* fpath,
+                               const struct stat* sb ATTRIBUTE_UNUSED,
+                               int typeflag,
+                               struct FTW* ftwbuf) {
+  switch (typeflag) {
+    case FTW_F:
+      return unlink(fpath);
+    case FTW_DP:
+      return (ftwbuf->level == 0) ? 0 : rmdir(fpath);
+    default:
+      return -1;
+  }
+}
+
+WARN_UNUSED bool CleanDirectory(const std::string& dir_path) {
+  if (!OS::DirectoryExists(dir_path.c_str())) {
+    return true;
+  }
+
+  static constexpr int kMaxDescriptors = 4;  // Limit the need for nftw() to re-open descriptors.
+  if (nftw(dir_path.c_str(), NftwCleanUpCallback, kMaxDescriptors, FTW_DEPTH | FTW_MOUNT) != 0) {
+    LOG(ERROR) << "Failed to clean-up '" << dir_path << "'";
+    return false;
+  }
+  return true;
+}
+
+WARN_UNUSED bool EnsureDirectoryExists(const std::string& absolute_path) {
+  if (absolute_path.empty() || absolute_path[0] != '/') {
+    LOG(ERROR) << "Path not absolute '" << absolute_path << "'";
+    return false;
+  }
+  std::string path;
+  for (const std::string& directory : android::base::Split(absolute_path, "/")) {
+    path.append("/").append(directory);
+    if (!OS::DirectoryExists(path.c_str())) {
+      static constexpr mode_t kDirectoryMode = S_IRWXU | S_IRGRP | S_IXGRP| S_IROTH | S_IXOTH;
+      if (mkdir(path.c_str(), kDirectoryMode) != 0) {
+        PLOG(ERROR) << "Could not create directory: " << path;
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+bool GetFreeSpace(const std::string& path, uint64_t* bytes) {
+  struct statvfs sv;
+  if (statvfs(path.c_str(), &sv) != 0) {
+    PLOG(ERROR) << "statvfs '" << path << "'";
+    return false;
+  }
+  *bytes = sv.f_bfree * sv.f_bsize;
+  return true;
+}
+
+bool GetUsedSpace(const std::string& path, uint64_t* bytes) {
+  static constexpr std::string_view kCurrentDirectory{"."};
+  static constexpr std::string_view kParentDirectory{".."};
+  static constexpr size_t kBytesPerBlock = 512;  // see manual page for stat(2).
+
+  uint64_t file_bytes = 0;
+  std::queue<std::string> unvisited;
+  unvisited.push(path);
+  while (!unvisited.empty()) {
+    std::string current = unvisited.front();
+    unvisited.pop();
+    std::unique_ptr<DIR, int (*)(DIR*)> dir(opendir(current.c_str()), closedir);
+    if (!dir) {
+      continue;
+    }
+    for (auto entity = readdir(dir.get()); entity != nullptr; entity = readdir(dir.get())) {
+      std::string_view name{entity->d_name};
+      if (name == kCurrentDirectory || name == kParentDirectory) {
+        continue;
+      }
+      std::string entity_name = current + "/" + entity->d_name;
+      if (entity->d_type == DT_DIR) {
+        unvisited.push(entity_name.c_str());
+      } else if (entity->d_type == DT_REG) {
+        struct stat sb;
+        if (stat(entity_name.c_str(), &sb) != 0) {
+          PLOG(ERROR) << "Failed to stat() file " << entity_name;
+          continue;
+        }
+        file_bytes += sb.st_blocks * kBytesPerBlock;
+      }
+    }
+  }
+  *bytes = file_bytes;
+  return true;
+}
+
+}  // namespace odrefresh
+}  // namespace art
diff --git a/odrefresh/odr_fs_utils.h b/odrefresh/odr_fs_utils.h
new file mode 100644
index 0000000..cefa11e
--- /dev/null
+++ b/odrefresh/odr_fs_utils.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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_ODREFRESH_ODR_FS_UTILS_H_
+#define ART_ODREFRESH_ODR_FS_UTILS_H_
+
+#include <cstdint>
+#include <iosfwd>
+
+#include <android-base/macros.h>
+
+namespace art {
+namespace odrefresh {
+
+// Cleans directory by removing all files and sub-directories under `dir_path`.
+// Returns true on success, false otherwise.
+WARN_UNUSED bool CleanDirectory(const std::string& dir_path);
+
+// Create all directories on `absolute_dir_path`.
+// Returns true on success, false otherwise.
+WARN_UNUSED bool EnsureDirectoryExists(const std::string& absolute_dir_path);
+
+// Get free space for filesystem containing `path`.
+// Returns true on success, false otherwise.
+WARN_UNUSED bool GetFreeSpace(const std::string& path, uint64_t* bytes);
+
+// Gets space used under directory `dir_path`.
+// Returns true on success, false otherwise.
+WARN_UNUSED bool GetUsedSpace(const std::string& dir_path, uint64_t* bytes);
+
+}  // namespace odrefresh
+}  // namespace art
+
+#endif  // ART_ODREFRESH_ODR_FS_UTILS_H_
diff --git a/odrefresh/odr_fs_utils_test.cc b/odrefresh/odr_fs_utils_test.cc
new file mode 100644
index 0000000..2b3cfaa
--- /dev/null
+++ b/odrefresh/odr_fs_utils_test.cc
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2021 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 <fcntl.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "android-base/stringprintf.h"
+#include "base/bit_utils.h"
+#include "base/common_art_test.h"
+#include "base/os.h"
+#include "base/unix_file/fd_file.h"
+#include "odr_fs_utils.h"
+
+namespace art {
+namespace odrefresh {
+
+class OdrFsUtilsTest : public CommonArtTest {};
+namespace {
+
+static bool CreateFile(const char* file_path, size_t bytes) {
+  std::unique_ptr<File> fp(OS::CreateEmptyFile(file_path));
+  if (!fp) {
+    return false;
+  }
+
+  std::vector<char> buffer(bytes, 0xa5);
+  if (!fp->WriteFully(buffer.data(), buffer.size())) {
+    fp->Erase();
+    return false;
+  }
+
+  if (fp->FlushClose() != 0) {
+    fp->Erase();
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace
+
+TEST_F(OdrFsUtilsTest, CleanDirectory) {
+  ScratchDir scratch_dir(/*keep_files=*/false);
+
+  // Create some sub-directories and files
+  const std::string dir_paths[] = {
+      scratch_dir.GetPath() + "/a",
+      scratch_dir.GetPath() + "/b",
+      scratch_dir.GetPath() + "/b/c",
+      scratch_dir.GetPath() + "/d"
+  };
+  for (const auto& dir_path : dir_paths) {
+    ASSERT_EQ(0, mkdir(dir_path.c_str(), S_IRWXU));
+  }
+
+  const std::string file_paths[] = {
+      scratch_dir.GetPath() + "/zero.txt",
+      scratch_dir.GetPath() + "/a/one.txt",
+      scratch_dir.GetPath() + "/b/two.txt",
+      scratch_dir.GetPath() + "/b/c/three.txt",
+      scratch_dir.GetPath() + "/b/c/four.txt",
+  };
+  for (const auto& file_path : file_paths) {
+    ASSERT_TRUE(CreateFile(file_path.c_str(), 4096));
+  }
+
+  // Clean all files and sub-directories
+  ASSERT_TRUE(CleanDirectory(scratch_dir.GetPath()));
+
+  // Check nothing we created remains.
+  for (const auto& dir_path : dir_paths) {
+    ASSERT_FALSE(OS::DirectoryExists(dir_path.c_str()));
+  }
+
+  for (const auto& file_path : file_paths) {
+    ASSERT_FALSE(OS::FileExists(file_path.c_str(), true));
+  }
+}
+
+TEST_F(OdrFsUtilsTest, EnsureDirectoryExistsBadPath) {
+  // Pick a path where not even a root test runner can write.
+  ASSERT_FALSE(EnsureDirectoryExists("/proc/unlikely/to/be/writable"));
+}
+
+TEST_F(OdrFsUtilsTest, EnsureDirectoryExistsEmptyPath) {
+  ASSERT_FALSE(EnsureDirectoryExists(""));
+}
+
+TEST_F(OdrFsUtilsTest, EnsureDirectoryExistsRelativePath) {
+  ASSERT_FALSE(EnsureDirectoryExists("a/b/c"));
+}
+
+TEST_F(OdrFsUtilsTest, EnsureDirectoryExistsSubDirs) {
+  ScratchDir scratch_dir(/*keep_files=*/false);
+
+  const char* relative_sub_dirs[] = {"a", "b/c", "d/e/f/"};
+  for (const char* relative_sub_dir : relative_sub_dirs) {
+    ASSERT_TRUE(EnsureDirectoryExists(scratch_dir.GetPath() + "/" + relative_sub_dir));
+  }
+}
+
+TEST_F(OdrFsUtilsTest, GetUsedSpace) {
+  static constexpr size_t kFirstFileBytes = 1;
+  static constexpr size_t kSecondFileBytes = 16111;
+  static constexpr size_t kBytesPerBlock = 512;
+
+  ScratchDir scratch_dir(/*keep_files=*/false);
+
+  const std::string first_file_path = scratch_dir.GetPath() + "/1.dat";
+  ASSERT_TRUE(CreateFile(first_file_path.c_str(), kFirstFileBytes));
+
+  struct stat sb;
+  ASSERT_EQ(0, stat(first_file_path.c_str(), &sb));
+  ASSERT_EQ(kFirstFileBytes, static_cast<decltype(kFirstFileBytes)>(sb.st_size));
+
+  uint64_t bytes_used = 0;
+  ASSERT_TRUE(GetUsedSpace(scratch_dir.GetPath().c_str(), &bytes_used));
+  ASSERT_EQ(static_cast<uint64_t>(sb.st_blksize), bytes_used);
+
+  const std::string second_file_path = scratch_dir.GetPath() + "/2.dat";
+  ASSERT_TRUE(CreateFile(second_file_path.c_str(), kSecondFileBytes));
+
+  ASSERT_TRUE(GetUsedSpace(scratch_dir.GetPath().c_str(), &bytes_used));
+  uint64_t expected_bytes_used = RoundUp(kFirstFileBytes, sb.st_blocks * kBytesPerBlock) +
+                                 RoundUp(kSecondFileBytes, sb.st_blocks * kBytesPerBlock);
+  ASSERT_EQ(expected_bytes_used, bytes_used);
+
+  const std::string sub_dir_path = scratch_dir.GetPath() + "/sub";
+  ASSERT_TRUE(EnsureDirectoryExists(sub_dir_path));
+  for (size_t i = 1; i < 32768; i *= 17) {
+    const std::string path = android::base::StringPrintf("%s/%zu", sub_dir_path.c_str(), i);
+    ASSERT_TRUE(CreateFile(path.c_str(), i));
+    expected_bytes_used += RoundUp(i, sb.st_blocks * kBytesPerBlock);
+    ASSERT_TRUE(GetUsedSpace(scratch_dir.GetPath().c_str(), &bytes_used));
+    ASSERT_EQ(expected_bytes_used, bytes_used);
+  }
+}
+
+TEST_F(OdrFsUtilsTest, GetUsedSpaceBadPath) {
+  ScratchDir scratch_dir(/*keep_files=*/false);
+  const std::string bad_path = scratch_dir.GetPath() + "/bad_path";
+  uint64_t bytes_used = ~0ull;
+  ASSERT_TRUE(GetUsedSpace(bad_path, &bytes_used));
+  ASSERT_EQ(0ull, bytes_used);
+}
+
+}  // namespace odrefresh
+}  // namespace art
diff --git a/odrefresh/odrefresh.cc b/odrefresh/odrefresh.cc
index 974b3c6..24ef7d1 100644
--- a/odrefresh/odrefresh.cc
+++ b/odrefresh/odrefresh.cc
@@ -16,16 +16,13 @@
 
 #include "odrefresh/odrefresh.h"
 
-#include <dirent.h>
 #include <errno.h>
 #include <fcntl.h>
-#include <ftw.h>
 #include <inttypes.h>
 #include <limits.h>
 #include <stdio.h>
 #include <string.h>
 #include <sys/stat.h>
-#include <sys/statvfs.h>
 #include <sysexits.h>
 #include <time.h>
 #include <unistd.h>
@@ -41,7 +38,6 @@
 #include <memory>
 #include <optional>
 #include <ostream>
-#include <queue>
 #include <sstream>
 #include <string>
 #include <string_view>
@@ -57,7 +53,6 @@
 #include "android-base/strings.h"
 #include "android/log.h"
 #include "arch/instruction_set.h"
-#include "base/bit_utils.h"
 #include "base/file_utils.h"
 #include "base/globals.h"
 #include "base/macros.h"
@@ -74,6 +69,7 @@
 
 #include "odr_artifacts.h"
 #include "odr_config.h"
+#include "odr_fs_utils.h"
 
 namespace art {
 namespace odrefresh {
@@ -149,23 +145,6 @@
   return Concatenate({"'", path, "'"});
 }
 
-// Create all directory and all required parents.
-static WARN_UNUSED bool EnsureDirectoryExists(const std::string& absolute_path) {
-  CHECK(absolute_path.size() > 0 && absolute_path[0] == '/');
-  std::string path;
-  for (const std::string& directory : android::base::Split(absolute_path, "/")) {
-    path.append("/").append(directory);
-    if (!OS::DirectoryExists(path.c_str())) {
-      static constexpr mode_t kDirectoryMode = S_IRWXU | S_IRGRP | S_IXGRP| S_IROTH | S_IXOTH;
-      if (mkdir(path.c_str(), kDirectoryMode) != 0) {
-        PLOG(ERROR) << "Could not create directory: " << path;
-        return false;
-      }
-    }
-  }
-  return true;
-}
-
 static void EraseFiles(const std::vector<std::unique_ptr<File>>& files) {
   for (auto& file : files) {
     file->Erase(/*unlink=*/true);
@@ -919,41 +898,6 @@
     return exit_code;
   }
 
-  static bool GetFreeSpace(const char* path, uint64_t* bytes) {
-    struct statvfs sv;
-    if (statvfs(path, &sv) != 0) {
-      PLOG(ERROR) << "statvfs '" << path << "'";
-      return false;
-    }
-    *bytes = sv.f_bfree * sv.f_bsize;
-    return true;
-  }
-
-  static bool GetUsedSpace(const char* path, uint64_t* bytes) {
-    *bytes = 0;
-
-    std::queue<std::string> unvisited;
-    unvisited.push(path);
-    while (!unvisited.empty()) {
-      std::string current = unvisited.front();
-      std::unique_ptr<DIR, int (*)(DIR*)> dir(opendir(current.c_str()), closedir);
-      for (auto entity = readdir(dir.get()); entity != nullptr; entity = readdir(dir.get())) {
-        if (entity->d_name[0] == '.') {
-          continue;
-        }
-        std::string entity_name = Concatenate({current, "/", entity->d_name});
-        if (entity->d_type == DT_DIR) {
-          unvisited.push(entity_name.c_str());
-        } else if (entity->d_type == DT_REG) {
-          // RoundUp file size to number of blocks.
-          *bytes += RoundUp(OS::GetFileSizeBytes(entity_name.c_str()), 512);
-        }
-      }
-      unvisited.pop();
-    }
-    return true;
-  }
-
   static void ReportSpace() {
     uint64_t bytes;
     std::string data_dir = GetArtApexData();
@@ -965,44 +909,13 @@
     }
   }
 
-  // Callback for use with nftw(3) to assist with clearing files and sub-directories.
-  // This method removes files and directories below the top-level directory passed to nftw().
-  static int NftwUnlinkRemoveCallback(const char* fpath,
-                                      const struct stat* sb ATTRIBUTE_UNUSED,
-                                      int typeflag,
-                                      struct FTW* ftwbuf) {
-    switch (typeflag) {
-      case FTW_F:
-        return unlink(fpath);
-      case FTW_DP:
-        return (ftwbuf->level == 0) ? 0 : rmdir(fpath);
-      default:
-        return -1;
-    }
-  }
-
-  // Recursively remove files and directories under `top_dir`, but preserve `top_dir` itself.
-  // Returns true on success, false otherwise.
-  WARN_UNUSED bool RecursiveRemoveBelow(const char* top_dir) const {
-    if (config_.GetDryRun()) {
-      LOG(INFO) << "Files under " << QuotePath(top_dir) << " would be removed (dry-run).";
-      return true;
-    }
-
-    if (!OS::DirectoryExists(top_dir)) {
-      return true;
-    }
-
-    static constexpr int kMaxDescriptors = 4;  // Limit the need for nftw() to re-open descriptors.
-    if (nftw(top_dir, NftwUnlinkRemoveCallback, kMaxDescriptors, FTW_DEPTH | FTW_MOUNT) != 0) {
-      LOG(ERROR) << "Failed to clean-up " << QuotePath(top_dir);
-      return false;
-    }
-    return true;
-  }
-
   WARN_UNUSED bool CleanApexdataDirectory() const {
-    return RecursiveRemoveBelow(GetArtApexData().c_str());
+    const std::string& apex_data_path = GetArtApexData();
+    if (config_.GetDryRun()) {
+      LOG(INFO) << "Files under `" << QuotePath(apex_data_path) << " would be removed (dry-run).";
+      return true;
+    }
+    return CleanDirectory(apex_data_path);
   }
 
   WARN_UNUSED bool RemoveArtifacts(const OdrArtifacts& artifacts) const {
@@ -1299,7 +1212,7 @@
         if (!CompileBootExtensionArtifacts(
                 isa, staging_dir, &dex2oat_invocation_count, &error_msg)) {
           LOG(ERROR) << "Compilation of BCP failed: " << error_msg;
-          if (!RecursiveRemoveBelow(staging_dir)) {
+          if (!config_.GetDryRun() && !CleanDirectory(staging_dir)) {
             return ExitCode::kCleanupFailed;
           }
           return ExitCode::kCompilationFailed;
@@ -1310,7 +1223,7 @@
     if (force_compile || !SystemServerArtifactsExistOnData(&error_msg)) {
       if (!CompileSystemServerArtifacts(staging_dir, &dex2oat_invocation_count, &error_msg)) {
         LOG(ERROR) << "Compilation of system_server failed: " << error_msg;
-        if (!RecursiveRemoveBelow(staging_dir)) {
+        if (!config_.GetDryRun() && !CleanDirectory(staging_dir)) {
           return ExitCode::kCleanupFailed;
         }
         return ExitCode::kCompilationFailed;