Add classes and functions for accessing files in artd.

This change includes:
- An AIDL representation of Linux filesystem permission and a function
  to convert it to Linux access mode.
- A class that creates a new file for writing and cleans it up unless
  the file is committed.
- A function that opens a file for reading.

Bug: 229268202
Test: m test-art-host-gtest-art_artd_tests
Ignore-AOSP-First: ART Services
Change-Id: I0e24e8fc31eee5e7004a35649df610b7da4d3178
diff --git a/artd/file_utils_test.cc b/artd/file_utils_test.cc
new file mode 100644
index 0000000..e6219a4
--- /dev/null
+++ b/artd/file_utils_test.cc
@@ -0,0 +1,340 @@
+/*
+ * 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 "file_utils.h"
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <filesystem>
+#include <memory>
+#include <string>
+
+#include "aidl/com/android/server/art/FsPermission.h"
+#include "android-base/errors.h"
+#include "android-base/file.h"
+#include "android-base/result-gmock.h"
+#include "android-base/result.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::FsPermission;
+using ::android::base::Error;
+using ::android::base::ReadFileToString;
+using ::android::base::Result;
+using ::android::base::WriteStringToFd;
+using ::android::base::WriteStringToFile;
+using ::android::base::testing::HasError;
+using ::android::base::testing::HasValue;
+using ::android::base::testing::Ok;
+using ::android::base::testing::WithMessage;
+using ::testing::ContainsRegex;
+using ::testing::IsEmpty;
+using ::testing::NotNull;
+
+void CheckContent(const std::string& path, const std::string& expected_content) {
+  std::string actual_content;
+  ASSERT_TRUE(ReadFileToString(path, &actual_content));
+  EXPECT_EQ(actual_content, expected_content);
+}
+
+// A file that will always fail on `Commit`.
+class UncommittableFile : public NewFile {
+ public:
+  static Result<std::unique_ptr<UncommittableFile>> Create(const std::string& path,
+                                                           const FsPermission& fs_permission) {
+    std::unique_ptr<NewFile> new_file = OR_RETURN(NewFile::Create(path, fs_permission));
+    return std::unique_ptr<UncommittableFile>(new UncommittableFile(std::move(*new_file)));
+  }
+
+  Result<void> Keep() override { return Error() << "Uncommittable file"; }
+
+ private:
+  explicit UncommittableFile(NewFile&& other) : NewFile(std::move(other)) {}
+};
+
+class FileUtilsTest : public CommonArtTest {
+ protected:
+  void SetUp() override {
+    CommonArtTest::SetUp();
+    scratch_dir_ = std::make_unique<ScratchDir>();
+    struct stat st;
+    ASSERT_EQ(stat(scratch_dir_->GetPath().c_str(), &st), 0);
+    fs_permission_ = FsPermission{.uid = static_cast<int32_t>(st.st_uid),
+                                  .gid = static_cast<int32_t>(st.st_gid)};
+  }
+
+  void TearDown() override {
+    scratch_dir_.reset();
+    CommonArtTest::TearDown();
+  }
+
+  FsPermission fs_permission_;
+  std::unique_ptr<ScratchDir> scratch_dir_;
+};
+
+TEST_F(FileUtilsTest, NewFileCreate) {
+  std::string path = scratch_dir_->GetPath() + "/file.tmp";
+
+  Result<std::unique_ptr<NewFile>> new_file = NewFile::Create(path, fs_permission_);
+  ASSERT_THAT(new_file, HasValue(NotNull()));
+  EXPECT_GE((*new_file)->Fd(), 0);
+  EXPECT_EQ((*new_file)->FinalPath(), path);
+  EXPECT_THAT((*new_file)->TempPath(), Not(IsEmpty()));
+  EXPECT_THAT((*new_file)->TempId(), Not(IsEmpty()));
+
+  EXPECT_FALSE(std::filesystem::exists((*new_file)->FinalPath()));
+  EXPECT_TRUE(std::filesystem::exists((*new_file)->TempPath()));
+}
+
+TEST_F(FileUtilsTest, NewFileCreateNonExistentDir) {
+  std::string path = scratch_dir_->GetPath() + "/non_existent_dir/file.tmp";
+
+  EXPECT_THAT(NewFile::Create(path, fs_permission_),
+              HasError(WithMessage(
+                  ContainsRegex("Failed to create temp file for .*/non_existent_dir/file.tmp"))));
+}
+
+TEST_F(FileUtilsTest, NewFileExplicitCleanup) {
+  std::string path = scratch_dir_->GetPath() + "/file.tmp";
+  std::unique_ptr<NewFile> new_file = OR_FATAL(NewFile::Create(path, fs_permission_));
+  new_file->Cleanup();
+
+  EXPECT_FALSE(std::filesystem::exists(path));
+  EXPECT_FALSE(std::filesystem::exists(new_file->TempPath()));
+}
+
+TEST_F(FileUtilsTest, NewFileImplicitCleanup) {
+  std::string path = scratch_dir_->GetPath() + "/file.tmp";
+  std::string temp_path;
+
+  // Cleanup on object destruction.
+  {
+    std::unique_ptr<NewFile> new_file = OR_FATAL(NewFile::Create(path, fs_permission_));
+    temp_path = new_file->TempPath();
+  }
+
+  EXPECT_FALSE(std::filesystem::exists(path));
+  EXPECT_FALSE(std::filesystem::exists(temp_path));
+}
+
+TEST_F(FileUtilsTest, NewFileCommit) {
+  std::string path = scratch_dir_->GetPath() + "/file.tmp";
+  std::string temp_path;
+
+  {
+    std::unique_ptr<NewFile> new_file = OR_FATAL(NewFile::Create(path, fs_permission_));
+    temp_path = new_file->TempPath();
+    new_file->CommitOrAbandon();
+  }
+
+  EXPECT_TRUE(std::filesystem::exists(path));
+  EXPECT_FALSE(std::filesystem::exists(temp_path));
+}
+
+TEST_F(FileUtilsTest, NewFileCommitAllNoOldFile) {
+  std::string file_1_path = scratch_dir_->GetPath() + "/file_1";
+  std::string file_2_path = scratch_dir_->GetPath() + "/file_2";
+
+  std::unique_ptr<NewFile> new_file_1 = OR_FATAL(NewFile::Create(file_1_path, fs_permission_));
+  std::unique_ptr<NewFile> new_file_2 = OR_FATAL(NewFile::Create(file_2_path, fs_permission_));
+
+  ASSERT_TRUE(WriteStringToFd("new_file_1", new_file_1->Fd()));
+  ASSERT_TRUE(WriteStringToFd("new_file_2", new_file_2->Fd()));
+
+  EXPECT_THAT(NewFile::CommitAllOrAbandon({new_file_1.get(), new_file_2.get()}), Ok());
+
+  // New files are committed.
+  CheckContent(file_1_path, "new_file_1");
+  CheckContent(file_2_path, "new_file_2");
+
+  // New files are no longer at the temporary paths.
+  EXPECT_FALSE(std::filesystem::exists(new_file_1->TempPath()));
+  EXPECT_FALSE(std::filesystem::exists(new_file_2->TempPath()));
+}
+
+TEST_F(FileUtilsTest, NewFileCommitAllReplacesOldFiles) {
+  std::string file_1_path = scratch_dir_->GetPath() + "/file_1";
+  std::string file_2_path = scratch_dir_->GetPath() + "/file_2";
+
+  ASSERT_TRUE(WriteStringToFile("old_file_1", file_1_path));
+  ASSERT_TRUE(WriteStringToFile("old_file_2", file_2_path));
+
+  std::unique_ptr<NewFile> new_file_1 = OR_FATAL(NewFile::Create(file_1_path, fs_permission_));
+  std::unique_ptr<NewFile> new_file_2 = OR_FATAL(NewFile::Create(file_2_path, fs_permission_));
+
+  ASSERT_TRUE(WriteStringToFd("new_file_1", new_file_1->Fd()));
+  ASSERT_TRUE(WriteStringToFd("new_file_2", new_file_2->Fd()));
+
+  EXPECT_THAT(NewFile::CommitAllOrAbandon({new_file_1.get(), new_file_2.get()}), Ok());
+
+  // New files are committed.
+  CheckContent(file_1_path, "new_file_1");
+  CheckContent(file_2_path, "new_file_2");
+
+  // New files are no longer at the temporary paths.
+  EXPECT_FALSE(std::filesystem::exists(new_file_1->TempPath()));
+  EXPECT_FALSE(std::filesystem::exists(new_file_2->TempPath()));
+}
+
+TEST_F(FileUtilsTest, NewFileCommitAllReplacesLessOldFiles) {
+  std::string file_1_path = scratch_dir_->GetPath() + "/file_1";
+  std::string file_2_path = scratch_dir_->GetPath() + "/file_2";
+
+  ASSERT_TRUE(WriteStringToFile("old_file_1", file_1_path));  // No old_file_2.
+
+  std::unique_ptr<NewFile> new_file_1 = OR_FATAL(NewFile::Create(file_1_path, fs_permission_));
+  std::unique_ptr<NewFile> new_file_2 = OR_FATAL(NewFile::Create(file_2_path, fs_permission_));
+
+  ASSERT_TRUE(WriteStringToFd("new_file_1", new_file_1->Fd()));
+  ASSERT_TRUE(WriteStringToFd("new_file_2", new_file_2->Fd()));
+
+  EXPECT_THAT(NewFile::CommitAllOrAbandon({new_file_1.get(), new_file_2.get()}), Ok());
+
+  // New files are committed.
+  CheckContent(file_1_path, "new_file_1");
+  CheckContent(file_2_path, "new_file_2");
+
+  // New files are no longer at the temporary paths.
+  EXPECT_FALSE(std::filesystem::exists(new_file_1->TempPath()));
+  EXPECT_FALSE(std::filesystem::exists(new_file_2->TempPath()));
+}
+
+TEST_F(FileUtilsTest, NewFileCommitAllReplacesMoreOldFiles) {
+  std::string file_1_path = scratch_dir_->GetPath() + "/file_1";
+  std::string file_2_path = scratch_dir_->GetPath() + "/file_2";
+  std::string file_3_path = scratch_dir_->GetPath() + "/file_3";
+
+  ASSERT_TRUE(WriteStringToFile("old_file_1", file_1_path));
+  ASSERT_TRUE(WriteStringToFile("old_file_2", file_2_path));
+  ASSERT_TRUE(WriteStringToFile("old_file_3", file_3_path));  // Extra file.
+
+  std::unique_ptr<NewFile> new_file_1 = OR_FATAL(NewFile::Create(file_1_path, fs_permission_));
+  std::unique_ptr<NewFile> new_file_2 = OR_FATAL(NewFile::Create(file_2_path, fs_permission_));
+
+  ASSERT_TRUE(WriteStringToFd("new_file_1", new_file_1->Fd()));
+  ASSERT_TRUE(WriteStringToFd("new_file_2", new_file_2->Fd()));
+
+  EXPECT_THAT(NewFile::CommitAllOrAbandon({new_file_1.get(), new_file_2.get()}, {file_3_path}),
+              Ok());
+
+  // New files are committed.
+  CheckContent(file_1_path, "new_file_1");
+  CheckContent(file_2_path, "new_file_2");
+  EXPECT_FALSE(std::filesystem::exists(file_3_path));  // Extra file removed.
+
+  // New files are no longer at the temporary paths.
+  EXPECT_FALSE(std::filesystem::exists(new_file_1->TempPath()));
+  EXPECT_FALSE(std::filesystem::exists(new_file_2->TempPath()));
+}
+
+TEST_F(FileUtilsTest, NewFileCommitAllFailedToCommit) {
+  std::string file_1_path = scratch_dir_->GetPath() + "/file_1";
+  std::string file_2_path = scratch_dir_->GetPath() + "/file_2";
+  std::string file_3_path = scratch_dir_->GetPath() + "/file_3";
+
+  ASSERT_TRUE(WriteStringToFile("old_file_1", file_1_path));
+  ASSERT_TRUE(WriteStringToFile("old_file_2", file_2_path));
+  ASSERT_TRUE(WriteStringToFile("old_file_3", file_3_path));  // Extra file.
+
+  std::unique_ptr<NewFile> new_file_1 = OR_FATAL(NewFile::Create(file_1_path, fs_permission_));
+  // Uncommittable file.
+  std::unique_ptr<NewFile> new_file_2 =
+      OR_FATAL(UncommittableFile::Create(file_2_path, fs_permission_));
+
+  ASSERT_TRUE(WriteStringToFd("new_file_1", new_file_1->Fd()));
+  ASSERT_TRUE(WriteStringToFd("new_file_2", new_file_2->Fd()));
+
+  EXPECT_THAT(NewFile::CommitAllOrAbandon({new_file_1.get(), new_file_2.get()}, {file_3_path}),
+              HasError(WithMessage("Uncommittable file")));
+
+  // Old files are fine.
+  CheckContent(file_1_path, "old_file_1");
+  CheckContent(file_2_path, "old_file_2");
+  CheckContent(file_3_path, "old_file_3");
+
+  // New files are abandoned.
+  EXPECT_FALSE(std::filesystem::exists(new_file_1->TempPath()));
+  EXPECT_FALSE(std::filesystem::exists(new_file_2->TempPath()));
+}
+
+TEST_F(FileUtilsTest, NewFileCommitAllFailedToMoveOldFile) {
+  std::string file_1_path = scratch_dir_->GetPath() + "/file_1";
+  std::string file_2_path = scratch_dir_->GetPath() + "/file_2";
+  std::filesystem::create_directory(file_2_path);
+  std::string file_3_path = scratch_dir_->GetPath() + "/file_3";
+
+  ASSERT_TRUE(WriteStringToFile("old_file_1", file_1_path));
+  ASSERT_TRUE(WriteStringToFile("old_file_3", file_3_path));  // Extra file.
+
+  std::unique_ptr<NewFile> new_file_1 = OR_FATAL(NewFile::Create(file_1_path, fs_permission_));
+  std::unique_ptr<NewFile> new_file_2 = OR_FATAL(NewFile::Create(file_2_path, fs_permission_));
+
+  ASSERT_TRUE(WriteStringToFd("new_file_1", new_file_1->Fd()));
+  ASSERT_TRUE(WriteStringToFd("new_file_2", new_file_2->Fd()));
+
+  // file_2 is not movable because it is a directory.
+  EXPECT_THAT(NewFile::CommitAllOrAbandon({new_file_1.get(), new_file_2.get()}, {file_3_path}),
+              HasError(WithMessage(ContainsRegex("Old file '.*/file_2' is a directory"))));
+
+  // Old files are fine.
+  CheckContent(file_1_path, "old_file_1");
+  EXPECT_TRUE(std::filesystem::is_directory(file_2_path));
+  CheckContent(file_3_path, "old_file_3");
+
+  // New files are abandoned.
+  EXPECT_FALSE(std::filesystem::exists(new_file_1->TempPath()));
+  EXPECT_FALSE(std::filesystem::exists(new_file_2->TempPath()));
+}
+
+TEST_F(FileUtilsTest, BuildTempPath) {
+  EXPECT_EQ(NewFile::BuildTempPath("/a/b/original_path", "123456"),
+            "/a/b/original_path.123456.tmp");
+}
+
+TEST_F(FileUtilsTest, OpenFileForReading) {
+  std::string path = scratch_dir_->GetPath() + "/foo";
+  ASSERT_TRUE(WriteStringToFile("foo", path));
+
+  EXPECT_THAT(OpenFileForReading(path), HasValue(NotNull()));
+}
+
+TEST_F(FileUtilsTest, OpenFileForReadingFailed) {
+  std::string path = scratch_dir_->GetPath() + "/foo";
+
+  EXPECT_THAT(OpenFileForReading(path),
+              HasError(WithMessage(ContainsRegex("Failed to open file .*/foo"))));
+}
+
+TEST_F(FileUtilsTest, FsPermissionToMode) {
+  EXPECT_EQ(FsPermissionToMode(FsPermission{}), S_IRUSR | S_IWUSR | S_IRGRP);
+  EXPECT_EQ(FsPermissionToMode(FsPermission{.isOtherReadable = true}),
+            S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+  EXPECT_EQ(FsPermissionToMode(FsPermission{.isOtherExecutable = true}),
+            S_IRUSR | S_IWUSR | S_IRGRP | S_IXOTH);
+  EXPECT_EQ(FsPermissionToMode(FsPermission{.isOtherReadable = true, .isOtherExecutable = true}),
+            S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH | S_IXOTH);
+}
+
+}  // namespace
+}  // namespace artd
+}  // namespace art