diff options
author | 2022-07-04 16:56:12 +0100 | |
---|---|---|
committer | 2022-07-07 20:33:11 +0000 | |
commit | a2ba696d009f9e5755d6769e5ead6e892e303c69 (patch) | |
tree | 0cedad4bdd744e8f67b96d20cd85b6d42c7d2bf0 | |
parent | 99bd8dd2bf8eac1a60bf65e442462753ed7f2147 (diff) |
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
-rw-r--r-- | artd/Android.bp | 2 | ||||
-rw-r--r-- | artd/binder/com/android/server/art/FsPermission.aidl | 38 | ||||
-rw-r--r-- | artd/file_utils.cc | 228 | ||||
-rw-r--r-- | artd/file_utils.h | 128 | ||||
-rw-r--r-- | artd/file_utils_test.cc | 340 |
5 files changed, 736 insertions, 0 deletions
diff --git a/artd/Android.bp b/artd/Android.bp index 69c7706a7a..58680a5388 100644 --- a/artd/Android.bp +++ b/artd/Android.bp @@ -27,6 +27,7 @@ cc_defaults { defaults: ["art_defaults"], srcs: [ "artd.cc", + "file_utils.cc", "path_utils.cc", ], shared_libs: [ @@ -71,6 +72,7 @@ art_cc_defaults { ], 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 0000000000..229f1829bd --- /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 0000000000..67185b86f1 --- /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 0000000000..9dc41f4097 --- /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 0000000000..e6219a4658 --- /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 |