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/Android.bp b/artd/Android.bp
index 69c7706..58680a5 100644
--- a/artd/Android.bp
+++ b/artd/Android.bp
@@ -27,6 +27,7 @@
defaults: ["art_defaults"],
srcs: [
"artd.cc",
+ "file_utils.cc",
"path_utils.cc",
],
shared_libs: [
@@ -71,6 +72,7 @@
],
srcs: [
"artd_test.cc",
+ "file_utils_test.cc",
"path_utils_test.cc",
],
}
diff --git a/artd/binder/com/android/server/art/FsPermission.aidl b/artd/binder/com/android/server/art/FsPermission.aidl
new file mode 100644
index 0000000..229f182
--- /dev/null
+++ b/artd/binder/com/android/server/art/FsPermission.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package com.android.server.art;
+
+/**
+ * Represents the Linux filesystem permission of a file or a directory.
+ *
+ * If none of the booleans are set, the default permission bits are `rw-r-----` for a file, and
+ * `rwxr-x---` for a directory.
+ *
+ * @hide
+ */
+parcelable FsPermission {
+ int uid;
+ int gid;
+ /**
+ * Whether the file/directory should have the "read" bit for "others" (S_IROTH).
+ */
+ boolean isOtherReadable;
+ /**
+ * Whether the file/directory should have the "execute" bit for "others" (S_IXOTH).
+ */
+ boolean isOtherExecutable;
+}
diff --git a/artd/file_utils.cc b/artd/file_utils.cc
new file mode 100644
index 0000000..67185b8
--- /dev/null
+++ b/artd/file_utils.cc
@@ -0,0 +1,228 @@
+/*
+ * 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 <sys/types.h>
+#include <unistd.h>
+
+#include <filesystem>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <system_error>
+#include <utility>
+
+#include "aidl/com/android/server/art/FsPermission.h"
+#include "android-base/errors.h"
+#include "android-base/logging.h"
+#include "android-base/result.h"
+#include "android-base/scopeguard.h"
+#include "android-base/stringprintf.h"
+#include "base/os.h"
+#include "base/unix_file/fd_file.h"
+
+namespace art {
+namespace artd {
+
+namespace {
+
+using ::aidl::com::android::server::art::FsPermission;
+using ::android::base::make_scope_guard;
+using ::android::base::Result;
+using ::android::base::StringPrintf;
+
+void UnlinkIfExists(const std::string& path) {
+ std::error_code ec;
+ if (!std::filesystem::remove(path, ec)) {
+ if (ec.value() != ENOENT) {
+ LOG(WARNING) << StringPrintf(
+ "Failed to remove file '%s': %s", path.c_str(), ec.message().c_str());
+ }
+ }
+}
+
+} // namespace
+
+Result<std::unique_ptr<NewFile>> NewFile::Create(const std::string& path,
+ const FsPermission& fs_permission) {
+ std::unique_ptr<NewFile> output_file(new NewFile(path, fs_permission));
+ OR_RETURN(output_file->Init());
+ return output_file;
+}
+
+NewFile::~NewFile() { Cleanup(); }
+
+Result<void> NewFile::Keep() {
+ if (close(std::exchange(fd_, -1)) != 0) {
+ return ErrnoErrorf("Failed to close file '{}'", temp_path_);
+ }
+ return {};
+}
+
+Result<void> NewFile::CommitOrAbandon() {
+ auto cleanup = make_scope_guard([this] { Unlink(); });
+ OR_RETURN(Keep());
+ std::error_code ec;
+ std::filesystem::rename(temp_path_, final_path_, ec);
+ if (ec) {
+ return Errorf(
+ "Failed to move new file '{}' to path '{}': {}", temp_path_, final_path_, ec.message());
+ }
+ cleanup.Disable();
+ committed_ = true;
+ return {};
+}
+
+void NewFile::Cleanup() {
+ if (fd_ >= 0) {
+ Unlink();
+ if (close(std::exchange(fd_, -1)) != 0) {
+ // Nothing we can do. If the file is already unlinked, it will go away when the process exits.
+ PLOG(WARNING) << "Failed to close file '" << temp_path_ << "'";
+ }
+ }
+}
+
+Result<void> NewFile::Init() {
+ mode_t mode = FsPermissionToMode(fs_permission_);
+ // "<path_>.XXXXXX.tmp".
+ temp_path_ = BuildTempPath(final_path_, "XXXXXX");
+ fd_ = mkstemps(temp_path_.data(), /*suffixlen=*/4);
+ if (fd_ < 0) {
+ return ErrnoErrorf("Failed to create temp file for '{}'", final_path_);
+ }
+ temp_id_ = temp_path_.substr(/*pos=*/final_path_.length() + 1, /*count=*/6);
+ if (fchmod(fd_, mode) != 0) {
+ return ErrnoErrorf("Failed to chmod file '{}'", temp_path_);
+ }
+ if (fchown(fd_, fs_permission_.uid, fs_permission_.gid) != 0) {
+ return ErrnoErrorf("Failed to chown file '{}'", temp_path_);
+ }
+ return {};
+}
+
+void NewFile::Unlink() {
+ // This should never fail. We were able to create the file, so we should be able to remove it.
+ UnlinkIfExists(temp_path_);
+}
+
+Result<void> NewFile::CommitAllOrAbandon(const std::vector<NewFile*>& files_to_commit,
+ const std::vector<std::string_view>& files_to_remove) {
+ std::vector<std::pair<std::string_view, std::string>> moved_files;
+
+ auto cleanup = make_scope_guard([&]() {
+ // Clean up new files.
+ for (NewFile* new_file : files_to_commit) {
+ if (new_file->committed_) {
+ UnlinkIfExists(new_file->FinalPath());
+ } else {
+ new_file->Cleanup();
+ }
+ }
+
+ // Move old files back.
+ for (const auto& [original_path, temp_path] : moved_files) {
+ std::error_code ec;
+ std::filesystem::rename(temp_path, original_path, ec);
+ if (ec) {
+ // This should never happen. We were able to move the file from `original_path` to
+ // `temp_path`. We should be able to move it back.
+ LOG(WARNING) << StringPrintf(
+ "Failed to move old file '%s' back from temporary path '%s': %s",
+ std::string(original_path).c_str(),
+ std::string(temp_path).c_str(),
+ ec.message().c_str());
+ }
+ }
+ });
+
+ // Move old files to temporary locations.
+ std::vector<std::string_view> all_files_to_remove;
+ for (NewFile* file : files_to_commit) {
+ all_files_to_remove.push_back(file->FinalPath());
+ }
+ all_files_to_remove.insert(
+ all_files_to_remove.end(), files_to_remove.begin(), files_to_remove.end());
+
+ for (std::string_view original_path : all_files_to_remove) {
+ std::error_code ec;
+ std::filesystem::file_status status = std::filesystem::status(original_path, ec);
+ if (!std::filesystem::status_known(status)) {
+ return Errorf("Failed to get status of old file '{}': {}", original_path, ec.message());
+ }
+ if (std::filesystem::is_directory(status)) {
+ return ErrnoErrorf("Old file '{}' is a directory", original_path);
+ }
+ if (std::filesystem::exists(status)) {
+ std::string temp_path = BuildTempPath(original_path, "XXXXXX");
+ int fd = mkstemps(temp_path.data(), /*suffixlen=*/4);
+ if (fd < 0) {
+ return ErrnoErrorf("Failed to create temporary path for old file '{}'", original_path);
+ }
+ close(fd);
+
+ std::filesystem::rename(original_path, temp_path, ec);
+ if (ec) {
+ UnlinkIfExists(temp_path);
+ return Errorf("Failed to move old file '{}' to temporary path '{}': {}",
+ original_path,
+ temp_path,
+ ec.message());
+ }
+
+ moved_files.push_back({original_path, std::move(temp_path)});
+ }
+ }
+
+ // Commit new files.
+ for (NewFile* file : files_to_commit) {
+ OR_RETURN(file->CommitOrAbandon());
+ }
+
+ cleanup.Disable();
+
+ // Clean up old files.
+ for (const auto& [original_path, temp_path] : moved_files) {
+ // This should never fail. We were able to move the file to `temp_path`. We should be able to
+ // remove it.
+ UnlinkIfExists(temp_path);
+ }
+
+ return {};
+}
+
+std::string NewFile::BuildTempPath(std::string_view final_path, const std::string& id) {
+ return StringPrintf("%s.%s.tmp", std::string(final_path).c_str(), id.c_str());
+}
+
+Result<std::unique_ptr<File>> OpenFileForReading(const std::string& path) {
+ std::unique_ptr<File> file(OS::OpenFileForReading(path.c_str()));
+ if (file == nullptr) {
+ return ErrnoErrorf("Failed to open file '{}'", path);
+ }
+ return file;
+}
+
+mode_t FsPermissionToMode(const FsPermission& fs_permission) {
+ return S_IRUSR | S_IWUSR | S_IRGRP | (fs_permission.isOtherReadable ? S_IROTH : 0) |
+ (fs_permission.isOtherExecutable ? S_IXOTH : 0);
+}
+
+} // namespace artd
+} // namespace art
diff --git a/artd/file_utils.h b/artd/file_utils.h
new file mode 100644
index 0000000..9dc41f4
--- /dev/null
+++ b/artd/file_utils.h
@@ -0,0 +1,128 @@
+/*
+ * 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_FILE_UTILS_H_
+#define ART_ARTD_FILE_UTILS_H_
+
+#include <sys/types.h>
+
+#include <memory>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "aidl/com/android/server/art/FsPermission.h"
+#include "android-base/result.h"
+#include "base/os.h"
+
+namespace art {
+namespace artd {
+
+// A class that creates a new file that will eventually be committed to the given path. The new file
+// is created at a temporary location. It will not overwrite the file at the given path until
+// `CommitOrAbandon` has been called and will be automatically cleaned up on object destruction
+// unless `CommitOrAbandon` has been called.
+// The new file is opened without O_CLOEXEC so that it can be passed to subprocesses.
+class NewFile {
+ public:
+ // Creates a new file at the given path with the given permission.
+ static android::base::Result<std::unique_ptr<NewFile>> Create(
+ const std::string& path, const aidl::com::android::server::art::FsPermission& fs_permission);
+
+ NewFile(const NewFile&) = delete;
+ NewFile& operator=(const NewFile&) = delete;
+ NewFile(NewFile&& other) noexcept
+ : fd_(std::exchange(other.fd_, -1)),
+ final_path_(std::move(other.final_path_)),
+ temp_path_(std::move(other.temp_path_)),
+ temp_id_(std::move(other.temp_id_)),
+ fs_permission_(other.fs_permission_) {}
+
+ // Deletes the file if it is not committed.
+ virtual ~NewFile();
+
+ int Fd() const { return fd_; }
+
+ // The path that the file will eventually be committed to.
+ const std::string& FinalPath() const { return final_path_; }
+
+ // The path to the new file.
+ const std::string& TempPath() const { return temp_path_; }
+
+ // The unique ID of the new file. Can be used by `BuildTempPath` for reconstructing the path to
+ // the file.
+ const std::string& TempId() const { return temp_id_; }
+
+ // Closes the new file, keeps it, moves the file to the final path, and overwrites any existing
+ // file at that path, or abandons the file on failure. The fd will be invalid after this function
+ // is called.
+ android::base::Result<void> CommitOrAbandon();
+
+ // Closes the new file and keeps it at the temporary location. The file will not be automatically
+ // cleaned up on object destruction. The file can be found at `TempPath()` (i.e.,
+ // `BuildTempPath(FinalPath(), TempId())`). The fd will be invalid after this function is called.
+ virtual android::base::Result<void> Keep();
+
+ // Unlinks and closes the new file if it is not committed. The fd will be invalid after this
+ // function is called.
+ void Cleanup();
+
+ // Commits all new files, replacing old files, and removes given files in addition. Or abandons
+ // new files and restores old files at best effort if any error occurs. The fds will be invalid
+ // after this function is called.
+ //
+ // Note: This function is NOT thread-safe. It is intended to be used in single-threaded code or in
+ // cases where some race condition is acceptable.
+ //
+ // Usage:
+ //
+ // Commit `file_1` and `file_2`, and remove the file at "path_3":
+ // CommitAllOrAbandon({file_1, file_2}, {"path_3"});
+ static android::base::Result<void> CommitAllOrAbandon(
+ const std::vector<NewFile*>& files_to_commit,
+ const std::vector<std::string_view>& files_to_remove = {});
+
+ // Returns the path to a temporary file. See `Keep`.
+ static std::string BuildTempPath(std::string_view final_path, const std::string& id);
+
+ private:
+ NewFile(const std::string& path,
+ const aidl::com::android::server::art::FsPermission& fs_permission)
+ : final_path_(path), fs_permission_(fs_permission) {}
+
+ android::base::Result<void> Init();
+
+ // Unlinks the new file. The fd will still be valid after this function is called.
+ void Unlink();
+
+ int fd_ = -1;
+ std::string final_path_;
+ std::string temp_path_;
+ std::string temp_id_;
+ aidl::com::android::server::art::FsPermission fs_permission_;
+ bool committed_ = false;
+};
+
+// Opens a file for reading.
+android::base::Result<std::unique_ptr<File>> OpenFileForReading(const std::string& path);
+
+// Converts FsPermission to Linux access mode.
+mode_t FsPermissionToMode(const aidl::com::android::server::art::FsPermission& fs_permission);
+
+} // namespace artd
+} // namespace art
+
+#endif // ART_ARTD_FILE_UTILS_H_
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