blob: 52cd9c0d65bc687c0bf756dd96a783f50282effb [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/storage/fshost/copier.h"
#include <dirent.h>
#include <fcntl.h>
#include <lib/syslog/cpp/macros.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <zircon/errors.h>
#include <filesystem>
#include <memory>
#include <string>
#include <variant>
#include <vector>
#include <fbl/unique_fd.h>
#include "src/lib/files/directory.h"
#include "src/lib/files/file.h"
namespace fshost {
namespace {
struct DirCloser {
void operator()(DIR* dir) { closedir(dir); }
};
// RAII wrapper around a DIR* that close the DIR when it goes out of scope.
using UniqueDir = std::unique_ptr<DIR, DirCloser>;
UniqueDir OpenDir(fbl::unique_fd fd) {
UniqueDir dir(fdopendir(fd.get()));
if (dir) {
// DIR only takes ownership of the file descriptor on success.
fd.release();
}
return dir;
}
bool IsPathExcluded(const std::vector<std::filesystem::path>& excluded_paths,
const std::filesystem::path& path) {
for (const auto& exclusion : excluded_paths) {
if (exclusion.empty()) {
// Skip the empty path. It would cause all files to be excluded which is probably not what the
// caller wanted.
continue;
}
auto exclusion_it = exclusion.begin();
auto path_it = path.begin();
while (exclusion_it != exclusion.end() && path_it != path.end()) {
if (*exclusion_it != *path_it) {
break;
}
++exclusion_it;
++path_it;
}
if (exclusion_it == exclusion.end()) {
return true;
}
}
return false;
}
Copier::DirectoryEntry* GetEntry(Copier::DirectoryEntries& entries, const std::string& name) {
for (auto& entry : entries) {
if (std::visit([&name](auto& entry) { return entry.name == name; }, entry)) {
return &entry;
}
}
return nullptr;
}
} // namespace
zx::status<Copier> Copier::Read(fbl::unique_fd root_fd,
const std::vector<std::filesystem::path>& excluded_paths) {
struct PendingRead {
UniqueDir dir;
DirectoryEntries* entries;
// The path relative to |root_fd|.
std::filesystem::path path;
};
std::vector<PendingRead> pending;
Copier copier;
{
UniqueDir dir = OpenDir(std::move(root_fd));
if (!dir)
return zx::error(ZX_ERR_BAD_STATE);
pending.push_back({
.dir = std::move(dir),
.entries = &copier.entries_,
.path = "",
});
}
while (!pending.empty()) {
PendingRead current = std::move(pending.back());
pending.pop_back();
struct dirent* entry;
while ((entry = readdir(current.dir.get())) != nullptr) {
std::string name(entry->d_name);
std::filesystem::path path = current.path / name;
if (IsPathExcluded(excluded_paths, path))
continue;
fbl::unique_fd fd(openat(dirfd(current.dir.get()), name.c_str(), O_RDONLY));
if (!fd)
return zx::error(ZX_ERR_BAD_STATE);
switch (entry->d_type) {
case DT_REG: {
struct stat stat_buf;
if (fstat(fd.get(), &stat_buf) != 0) {
return zx::error(ZX_ERR_BAD_STATE);
}
std::string buf;
buf.reserve(stat_buf.st_size);
if (!files::ReadFileDescriptorToString(fd.get(), &buf)) {
return zx::error(ZX_ERR_BAD_STATE);
}
current.entries->push_back(File{std::move(name), std::move(buf)});
break;
}
case DT_DIR: {
if (name == "." || name == "..")
continue;
UniqueDir child_dir = OpenDir(std::move(fd));
if (!child_dir)
return zx::error(ZX_ERR_BAD_STATE);
current.entries->push_back(Directory{std::move(name), {}});
pending.push_back({
.dir = std::move(child_dir),
.entries = &std::get<Directory>(current.entries->back()).entries,
.path = std::move(path),
});
break;
}
}
}
}
return zx::ok(std::move(copier));
}
zx_status_t Copier::Write(fbl::unique_fd root_fd) const {
std::vector<std::pair<fbl::unique_fd, const DirectoryEntries*>> pending;
pending.emplace_back(std::move(root_fd), &entries_);
while (!pending.empty()) {
fbl::unique_fd fd = std::move(pending.back().first);
const DirectoryEntries* entries = pending.back().second;
pending.pop_back();
// Fail to compile if extra types are added.
static_assert(std::variant_size_v<DirectoryEntry> == 2);
for (const auto& entry : *entries) {
if (std::holds_alternative<File>(entry)) {
const File& file = std::get<File>(entry);
if (!files::WriteFileAt(fd.get(), file.name, file.contents.data(),
static_cast<ssize_t>(file.contents.size()))) {
FX_LOGS(ERROR) << "Unable to write to " << file.name;
return ZX_ERR_BAD_STATE;
}
} else if (std::holds_alternative<Directory>(entry)) {
const Directory& directory = std::get<Directory>(entry);
if (!files::CreateDirectoryAt(fd.get(), directory.name)) {
FX_LOGS(ERROR) << "Unable to make directory " << directory.name;
return ZX_ERR_BAD_STATE;
}
fbl::unique_fd child_fd(openat(fd.get(), directory.name.c_str(), O_RDONLY));
if (!child_fd) {
FX_LOGS(ERROR) << "Unable to open directory " << directory.name;
return ZX_ERR_BAD_STATE;
}
pending.emplace_back(std::move(child_fd), &directory.entries);
}
}
}
return ZX_OK;
}
zx::status<> Copier::InsertFile(const std::filesystem::path& path, std::string contents) {
if (path.filename().empty() || path.is_absolute()) {
// |path| was either empty, ended with '/', or started with '/'.
return zx::error(ZX_ERR_INVALID_ARGS);
}
DirectoryEntries* entries = &entries_;
for (const auto& parent : path.parent_path()) {
DirectoryEntry* entry = GetEntry(*entries, parent);
if (entry == nullptr) {
entries->push_back(Directory{parent, {}});
entries = &std::get<Directory>(entries->back()).entries;
} else if (Directory* child_dir = std::get_if<Directory>(entry); child_dir != nullptr) {
entries = &child_dir->entries;
} else {
// A file exists where a directory needed to be created.
return zx::error(ZX_ERR_BAD_STATE);
}
}
if (GetEntry(*entries, path.filename()) != nullptr) {
// The file already exists.
return zx::error(ZX_ERR_ALREADY_EXISTS);
}
entries->push_back(File{path.filename(), std::move(contents)});
return zx::ok();
}
} // namespace fshost