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.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