mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 21:40:12 +01:00
This layer allows runtimes to be built on-demand but cached in a consistent and re-usable location on the system. It handles careful filesystem operations to ensure consistency even in the face of multiple versions and build configurations. This addresses a number of TODOs from the initial runtimes building on-demand, and sets the stage to scale up to more runtimes. This doesn't switch on-demand runtimes to be on by default, I wanted to wait and make that change as a separate step. --------- Co-authored-by: Geoff Romer <gromer@google.com>
747 lines
29 KiB
C++
747 lines
29 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "common/filesystem.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <chrono>
|
|
|
|
#include "common/build_data.h"
|
|
#include "llvm/Support/MathExtras.h"
|
|
|
|
namespace Carbon::Filesystem {
|
|
|
|
// Render an error number from `errno` to the provided stream using the richest
|
|
// rendering available on the platform.
|
|
static auto PrintErrorNumber(llvm::raw_ostream& out, int errnum) -> void {
|
|
#if defined(_GNU_SOURCE) && \
|
|
(__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 32))
|
|
// For sufficiently recent glibc versions, use GNU-specific routines to
|
|
// compute the error name and description.
|
|
llvm::StringRef name = strerrordesc_np(errnum);
|
|
llvm::StringRef desc = strerrorname_np(errnum);
|
|
|
|
out << llvm::formatv("{0}: {1}", name, desc);
|
|
#elif defined(__APPLE__) || defined(_GNU_SOURCE) || defined(_POSIX_SOURCE)
|
|
// Broadly portable fallback for Unix-like systems.
|
|
char buffer[4096];
|
|
#ifdef _GNU_SOURCE
|
|
const char* str = strerror_r(errnum, buffer, sizeof(buffer));
|
|
// The GNU version doesn't report a meta-error.
|
|
int meta_error = 0;
|
|
#else
|
|
int meta_error = strerror_r(errnum, buffer, sizeof(buffer));
|
|
const char* str = buffer;
|
|
#endif
|
|
if (meta_error == 0) {
|
|
out << llvm::formatv("errno {0}: {1}", errnum, llvm::StringRef(str));
|
|
} else {
|
|
out << llvm::formatv(
|
|
"error number {0}; encountered meta-error number {1} while rendering "
|
|
"an error message",
|
|
errnum, meta_error);
|
|
}
|
|
#else
|
|
#error TODO: Implement this for other platforms.
|
|
#endif
|
|
}
|
|
|
|
auto FdError::Print(llvm::raw_ostream& out) const -> void {
|
|
// The `format_` member is a `StringLiteral` that is null terminated, so
|
|
// `.data()` is safe here.
|
|
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
|
|
out << llvm::formatv(format_.data(), fd_) << " failed: ";
|
|
PrintErrorNumber(out, unix_errnum());
|
|
}
|
|
|
|
auto PathError::Print(llvm::raw_ostream& out) const -> void {
|
|
// The `format_` member is a `StringLiteral` that is null terminated, so
|
|
// `.data()` is safe here.
|
|
// NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
|
|
out << llvm::formatv(format_.data(), path_,
|
|
dir_fd_ == AT_FDCWD ? std::string("AT_FDCWD")
|
|
: std::to_string(dir_fd_))
|
|
<< " failed: ";
|
|
PrintErrorNumber(out, unix_errnum());
|
|
}
|
|
|
|
auto Internal::FileRefBase::ReadFileToString()
|
|
-> ErrorOr<std::string, FdError> {
|
|
std::string result;
|
|
|
|
// Read a buffer at a time until we reach the end. We use the pipe buffer
|
|
// length as our max buffer size as it is likely to be small but reasonable
|
|
// for the OS, and in the case of pipes the same chunking in which the data
|
|
// will arrive.
|
|
//
|
|
// TODO: Replace this with a smaller buffer and using `resize_and_overwrite`
|
|
// to read into the string in-place for larger strings. Unclear if that will
|
|
// be any faster, but it will be much more friendly to callers with
|
|
// constrained stack sizes and use less memory overall.
|
|
std::byte buffer[PIPE_BUF];
|
|
CARBON_RETURN_IF_ERROR(SeekFromBeginning(0));
|
|
for (;;) {
|
|
auto read_result = ReadToBuffer(buffer);
|
|
if (!read_result.ok()) {
|
|
return std::move(read_result).error();
|
|
}
|
|
if (read_result->empty()) {
|
|
// EOF
|
|
break;
|
|
}
|
|
result.append(reinterpret_cast<const char*>(read_result->data()),
|
|
read_result->size());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
auto Internal::FileRefBase::WriteFileFromString(llvm::StringRef str)
|
|
-> ErrorOr<Success, FdError> {
|
|
CARBON_RETURN_IF_ERROR(SeekFromBeginning(0));
|
|
auto bytes = llvm::ArrayRef<std::byte>(
|
|
reinterpret_cast<const std::byte*>(str.data()), str.size());
|
|
while (!bytes.empty()) {
|
|
auto write_result = WriteFromBuffer(bytes);
|
|
if (!write_result.ok()) {
|
|
return std::move(write_result).error();
|
|
}
|
|
bytes = *write_result;
|
|
}
|
|
CARBON_RETURN_IF_ERROR(Truncate(str.size()));
|
|
return Success();
|
|
}
|
|
|
|
// A macOS specific sleep routine that builds on more standard utilities. This
|
|
// is technically a portable implementation so we always compile it but only use
|
|
// it on macOS where the more efficient direct use of `clock_nanosleep` isn't
|
|
// available.
|
|
[[maybe_unused]]
|
|
static auto SleepMacos(Duration sleep) -> void {
|
|
TimePoint stop = Clock::now() + sleep;
|
|
|
|
timespec sleep_ts = Internal::DurationToTimespec(sleep);
|
|
for (;;) {
|
|
timespec rem_sleep_ts = {};
|
|
int result = nanosleep(&sleep_ts, &rem_sleep_ts);
|
|
if (result == 0) {
|
|
return;
|
|
}
|
|
|
|
// Continue sleeping if we get interrupted by a resumable signal. For
|
|
// everything else report it.
|
|
if (errno != EINTR) {
|
|
int errnum = errno;
|
|
RawStringOstream error_os;
|
|
PrintErrorNumber(error_os, errnum);
|
|
CARBON_FATAL("Unexpected error while sleeping: {0}", error_os.TakeStr());
|
|
}
|
|
|
|
// Update to the remaining sleep time for the next attempt at sleeping.
|
|
sleep_ts = rem_sleep_ts;
|
|
|
|
// Also check if the clock has passed our stop time as a fallback to avoid
|
|
// too much clock skew.
|
|
if (Clock::now() > stop) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static auto Sleep(Duration sleep) -> void {
|
|
// For every platform but macOS we can sleep directly on an absolute time.
|
|
#if __APPLE__
|
|
// On Apple platforms, dispatch to a specialized routine.
|
|
SleepMacos(sleep);
|
|
#else
|
|
|
|
// We use `clock_gettime` instead of the filesystem `Clock` or some other
|
|
// `std::chrono` clock because we want to use the exact same clock that we'll
|
|
// use for sleeping below, and we'll need the time in a `timespec` for that
|
|
// call anyways. We do use a monotonic clock to try and avoid sleeps being
|
|
// interrupted by clock changes.
|
|
timespec ts = {};
|
|
int result = clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
CARBON_CHECK(result == 0, "Error getting the time: {0}", strerror(errno));
|
|
|
|
// Now convert the timespec to a duration that we can safely do arithmetic on.
|
|
// Since the sleep interval is in nanoseconds it is tempting to directly do
|
|
// arithmetic here, but this has a subtle pitfall near the boundary between
|
|
// the nanosecond component and the second component.
|
|
//
|
|
// Note that our `Duration` uses `__int128` to avoid worrying about running
|
|
// out of precision to represent the final deadline.
|
|
Duration stop_time = std::chrono::seconds(ts.tv_sec);
|
|
stop_time += std::chrono::nanoseconds(ts.tv_nsec);
|
|
stop_time += sleep;
|
|
|
|
// Now convert back to timespec.
|
|
ts = Internal::DurationToTimespec(stop_time);
|
|
|
|
do {
|
|
result = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, nullptr);
|
|
|
|
// Continue sleeping if we get interrupted by a resumable signal. Because
|
|
// we're using a monotonic clock and an absolute deadline time we will
|
|
// eventually progress past that deadline.
|
|
} while (result != 0 && (errno == EINTR));
|
|
|
|
if (result != 0) {
|
|
int errnum = errno;
|
|
RawStringOstream error_os;
|
|
PrintErrorNumber(error_os, errnum);
|
|
CARBON_FATAL("Unexpected error while sleeping: {0}", error_os.TakeStr());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
auto Internal::FileRefBase::TryLock(FileLock::Kind kind, Duration deadline,
|
|
Duration poll_interval)
|
|
-> ErrorOr<FileLock, FdError> {
|
|
CARBON_CHECK(poll_interval <= deadline);
|
|
if (deadline != Duration(0) && poll_interval == Duration(0)) {
|
|
// If the caller didn't provide a poll interval but did provide a deadline,
|
|
// pick a poll interval to roughly be 1/1000th of the deadline but at least
|
|
// 1 microsecond. We don't support polling faster than 1 microsecond given
|
|
// how expensive file locking is.
|
|
poll_interval =
|
|
std::max(Duration(std::chrono::microseconds(1)), deadline / 1000);
|
|
}
|
|
if (deadline != Duration(0)) {
|
|
CARBON_CHECK(
|
|
deadline >= std::chrono::microseconds(10),
|
|
"A deadline for a file lock shorter than 10 microseconds is not "
|
|
"supported, callers can implement their own polling logic.");
|
|
CARBON_CHECK(poll_interval >= std::chrono::microseconds(1),
|
|
"Polling for a file lock faster than every microsecond is not "
|
|
"supported, callers can implement their own polling logic.");
|
|
}
|
|
auto stop = Clock::now() + deadline;
|
|
for (;;) {
|
|
int result = flock(fd_, static_cast<int>(kind) | LOCK_NB);
|
|
if (result == 0) {
|
|
return FileLock(fd_);
|
|
}
|
|
|
|
// Return an error if this is something other than blocking for the lock to
|
|
// be available, or we didn't get a deadline for continuing to try and
|
|
// acquire the lock, or we've reached our deadline.
|
|
if (errno != EWOULDBLOCK || deadline == Duration(0) ||
|
|
Clock::now() >= stop) {
|
|
return FdError(errno, "File::TryLock on '{0}'", fd_);
|
|
}
|
|
|
|
// The caller requested attempting to wait up to a deadline to acquire the
|
|
// lock with a specific poll interval. Try to sleep for that poll interval
|
|
// before trying the lock again.
|
|
Sleep(poll_interval);
|
|
}
|
|
}
|
|
|
|
auto DirRef::AppendEntriesIf(
|
|
llvm::SmallVectorImpl<std::filesystem::path>& entries,
|
|
llvm::function_ref<auto(llvm::StringRef name)->bool> predicate)
|
|
-> ErrorOr<Success, FdError> {
|
|
CARBON_ASSIGN_OR_RETURN(Reader reader, Read());
|
|
for (const Entry& entry : reader) {
|
|
llvm::StringRef name = entry.name();
|
|
if (name == "." || name == "..") {
|
|
continue;
|
|
}
|
|
if (predicate && !predicate(name)) {
|
|
continue;
|
|
}
|
|
entries.push_back(name.str());
|
|
}
|
|
return Success();
|
|
}
|
|
|
|
auto DirRef::AppendEntriesIf(
|
|
llvm::SmallVectorImpl<std::filesystem::path>& dir_entries,
|
|
llvm::SmallVectorImpl<std::filesystem::path>& non_dir_entries,
|
|
llvm::function_ref<auto(llvm::StringRef name)->bool> predicate)
|
|
-> ErrorOr<Success, FdError> {
|
|
CARBON_ASSIGN_OR_RETURN(Reader reader, Read());
|
|
for (const Entry& entry : reader) {
|
|
llvm::StringRef name = entry.name();
|
|
if (name == "." || name == "..") {
|
|
continue;
|
|
}
|
|
if (predicate && !predicate(name)) {
|
|
continue;
|
|
}
|
|
std::filesystem::path name_path = name.str();
|
|
if (entry.is_known_dir()) {
|
|
dir_entries.push_back(std::move(name_path));
|
|
continue;
|
|
}
|
|
if (!entry.is_unknown_type()) {
|
|
non_dir_entries.push_back(std::move(name_path));
|
|
continue;
|
|
}
|
|
|
|
auto stat_result = Lstat(name_path);
|
|
if (!stat_result.ok()) {
|
|
return FdError(stat_result.error().unix_errnum(),
|
|
"Dir::AppendEntriesIf on '{0}' failed while stat-ing "
|
|
"entries to determine which are directories",
|
|
dfd_);
|
|
}
|
|
if (stat_result->is_dir()) {
|
|
dir_entries.push_back(std::move(name_path));
|
|
} else {
|
|
non_dir_entries.push_back(std::move(name_path));
|
|
}
|
|
}
|
|
return Success();
|
|
}
|
|
|
|
auto DirRef::OpenDir(const std::filesystem::path& path,
|
|
CreationOptions creation_options, ModeType creation_mode,
|
|
OpenFlags open_flags) -> ErrorOr<Dir, PathError> {
|
|
// If we potentially need to create a directory, we have to do that
|
|
// separately as no systems support `O_CREAT | O_DIRECTORY`, even though
|
|
// that would be (much) nicer.
|
|
|
|
if (creation_options == CreateNew) {
|
|
// If we are required to be the one that created the directory, disable
|
|
// following the last symlink when we open that directory. The last symlink
|
|
// is the only one that matters for security here because it is only valid
|
|
// to create the last component. It is that directory component that we want
|
|
// to ensure has not been replaced with a symlink by an adversarial
|
|
// concurrent process.
|
|
open_flags |= OpenFlags::NoFollow;
|
|
}
|
|
|
|
if (creation_options != OpenExisting) {
|
|
CARBON_CHECK(creation_options != CreateAlways,
|
|
"Invalid `creation_options` value of `CreateAlways`: there is "
|
|
"no support for truncating directories, and so they cannot be "
|
|
"created in an analogous way to files if they already exist.");
|
|
|
|
if (mkdirat(dfd_, path.c_str(), creation_mode) != 0) {
|
|
// Unless the error is just that the path already exists, and that is
|
|
// allowed for the requested creation flags, report any error here as part
|
|
// of opening just like we would if the error originated from `openat`
|
|
// with `O_CREAT`.
|
|
if (creation_options == CreateNew || errno != EEXIST) {
|
|
return PathError(errno,
|
|
"Calling `mkdirat` on '{0}' relative to '{1}' during "
|
|
"DirRef::OpenDir",
|
|
path, dfd_);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Open this path as a directory. Note that this has to succeed, and when we
|
|
// created the directory we require the last component to not be a symlink in
|
|
// case it was _replaced_ with a symlink while running.
|
|
int result_fd =
|
|
openat(dfd_, path.c_str(), static_cast<int>(open_flags) | O_DIRECTORY);
|
|
if (result_fd == -1) {
|
|
// No need for `EINTR` handling here as if this is a FIFO it would be an
|
|
// error with `O_DIRECTORY`.
|
|
return PathError(
|
|
errno,
|
|
"Calling `openat` on '{0}' relative to '{1}' during DirRef::OpenDir",
|
|
path, dfd_);
|
|
}
|
|
Dir result(result_fd);
|
|
|
|
// If we were required to create the directory, we also need to verify that
|
|
// the opened file descriptor continues to have the same permissions and the
|
|
// correct owner as we couldn't do the creation atomically with the open. This
|
|
// defends against an adversarial removal of the created directory and
|
|
// creation of a new directory with the same name but either with wider
|
|
// permissions such as all-write, or with a different owner.
|
|
//
|
|
// We don't defend against replacement with a directory of the same name, same
|
|
// permissions, same owner, but different group. There is no good way to do
|
|
// this defense given the complexity of group assignment, and there appears to
|
|
// be no need. Achieving such a replacement without superuser power would
|
|
// require a parent directory with `setgid` bit, and a group that gives the
|
|
// attacker access -- but such a parent directory would make *any* creation
|
|
// vulnerable without any need for a replacement, so we can't defend against
|
|
// that here. The caller has ample tools to defend against this including
|
|
// taking care with the parent directory and restricting the group permission
|
|
// bits which we *do* verify.
|
|
if (creation_options == CreateNew) {
|
|
auto stat_result = result.Stat();
|
|
if (!stat_result.ok()) {
|
|
// Manually propagate this error so we can attach it back to the opened
|
|
// path and relative directory.
|
|
return PathError(stat_result.error().unix_errnum(),
|
|
"DirRef::Stat after opening '{0}' relative to '{1}'",
|
|
path, dfd_);
|
|
}
|
|
|
|
// Check that the owning UID is the current effective UID.
|
|
if (stat_result->unix_uid() != geteuid()) {
|
|
// Model this as `EPERM`, which is a bit awkward, but should be fine.
|
|
return PathError(EPERM,
|
|
"Unexpected UID change after creating '{0}' relative to "
|
|
"'{1}' during DirRef::OpenDir",
|
|
path, dfd_);
|
|
}
|
|
|
|
// Check that the permissions are a subset of the requested ones. They may
|
|
// have been masked down by `umask`, but if there are *new* permissions,
|
|
// that would be a security issue.
|
|
if ((stat_result->permissions() & creation_mode) !=
|
|
stat_result->permissions()) {
|
|
// Model this with `EPERM` and a custom message.
|
|
return PathError(EPERM,
|
|
"Unexpected permissions after creating '{0}' relative "
|
|
"to '{1}' during DirRef::OpenDir",
|
|
path, dfd_);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
auto DirRef::ReadFileToString(const std::filesystem::path& path)
|
|
-> ErrorOr<std::string, PathError> {
|
|
CARBON_ASSIGN_OR_RETURN(ReadFile f, OpenReadOnly(path));
|
|
auto result = f.ReadFileToString();
|
|
if (result.ok()) {
|
|
return *std::move(result);
|
|
}
|
|
return PathError(result.error().unix_errnum(),
|
|
"Dir::ReadFileToString on '{0}' relative to '{1}'", path,
|
|
dfd_);
|
|
}
|
|
|
|
auto DirRef::WriteFileFromString(const std::filesystem::path& path,
|
|
llvm::StringRef content,
|
|
CreationOptions creation_options)
|
|
-> ErrorOr<Success, PathError> {
|
|
CARBON_ASSIGN_OR_RETURN(WriteFile f, OpenWriteOnly(path, creation_options));
|
|
auto write_result = f.WriteFileFromString(content);
|
|
// Immediately close the file as even if there was a write error we don't want
|
|
// to leave the file open.
|
|
auto close_result = std::move(f).Close();
|
|
|
|
// Now report the first error encountered or return success.
|
|
if (!write_result.ok()) {
|
|
return PathError(
|
|
write_result.error().unix_errnum(),
|
|
"Write error in Dir::WriteFileFromString on '{0}' relative to '{1}'",
|
|
path, dfd_);
|
|
}
|
|
if (!close_result.ok()) {
|
|
return PathError(
|
|
close_result.error().unix_errnum(),
|
|
"Close error in Dir::WriteFileFromString on '{0}' relative to '{1}'",
|
|
path, dfd_);
|
|
}
|
|
return Success();
|
|
}
|
|
|
|
auto DirRef::CreateDirectories(const std::filesystem::path& path,
|
|
ModeType creation_mode)
|
|
-> ErrorOr<Dir, PathError> {
|
|
// Avoid having to handle an empty path by immediately rejecting it as
|
|
// invalid.
|
|
if (path.empty()) {
|
|
return PathError(EINVAL,
|
|
"DirRef::CreateDirectories on '{0}' relative to '{1}'",
|
|
path, dfd_);
|
|
}
|
|
// Try directly opening the directory and use that if successful. This is an
|
|
// important hot path case of users essentially doing an "open-always" form of
|
|
// creating multiple steps of directories.
|
|
auto open_result = OpenDir(path, OpenExisting);
|
|
if (open_result.ok()) {
|
|
return std::move(*open_result);
|
|
} else if (!open_result.error().no_entity()) {
|
|
return std::move(open_result).error();
|
|
}
|
|
|
|
// Walk from the full path towards this directory (or the root) to find the
|
|
// first existing directory. This is faster than walking down as no file
|
|
// descriptors have to be allocated for any intervening directories, etc. We
|
|
// keep the path components that are missing as we pop them off for easy
|
|
// traversal back down.
|
|
std::optional<Dir> work_dir;
|
|
// Paths typically consist of relatively few components
|
|
// and so we can use a bit of stack and avoid allocating and moving the paths
|
|
// in common cases. We use `8` as an arbitrary but likely good for all of the
|
|
// hottest cases.
|
|
llvm::SmallVector<std::filesystem::path, 8> missing_components;
|
|
missing_components.push_back(path.filename());
|
|
for (std::filesystem::path parent_path = path.parent_path();
|
|
!parent_path.empty(); parent_path = parent_path.parent_path()) {
|
|
auto open_result = OpenDir(parent_path, OpenExisting);
|
|
if (open_result.ok()) {
|
|
work_dir = std::move(*open_result);
|
|
break;
|
|
}
|
|
missing_components.push_back(parent_path.filename());
|
|
}
|
|
CARBON_CHECK(!missing_components.empty());
|
|
|
|
// If we haven't yet opened an intermediate directory, start by creating one
|
|
// relative to this directory. We can't do this as part of the loop below as
|
|
// `this` and the newly opened directory have different types.
|
|
if (!work_dir) {
|
|
std::filesystem::path component = missing_components.pop_back_val();
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Dir component_dir,
|
|
OpenDir(component, CreationOptions::OpenAlways, creation_mode));
|
|
// Move this component into our temporary directory slot.
|
|
work_dir = std::move(component_dir);
|
|
}
|
|
|
|
// Now walk through the remaining components opening and creating each
|
|
// relative to the previous.
|
|
while (!missing_components.empty()) {
|
|
std::filesystem::path component = missing_components.pop_back_val();
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Dir component_dir,
|
|
work_dir->OpenDir(component, CreationOptions::OpenAlways,
|
|
creation_mode));
|
|
|
|
// Close the current temporary directory and move the new component
|
|
// directory object into its place.
|
|
work_dir = std::move(component_dir);
|
|
}
|
|
|
|
CARBON_CHECK(work_dir,
|
|
"Should always have created at least one directory for a "
|
|
"non-empty path!");
|
|
return std::move(work_dir).value();
|
|
}
|
|
|
|
auto DirRef::Rmtree(const std::filesystem::path& path)
|
|
-> ErrorOr<Success, PathError> {
|
|
struct DirAndIterator {
|
|
DirRef::Reader dir;
|
|
ssize_t dir_entry_start;
|
|
};
|
|
llvm::SmallVector<DirAndIterator> dir_stack;
|
|
|
|
llvm::SmallVector<std::filesystem::path> dir_entries;
|
|
llvm::SmallVector<std::filesystem::path> unknown_entries;
|
|
|
|
dir_entries.push_back(path);
|
|
for (;;) {
|
|
// When we bottom out, we're removing the initial tree path and doing so
|
|
// relative to `this` directory.
|
|
DirRef current = dir_stack.empty() ? *this : dir_stack.back().dir;
|
|
ssize_t dir_entry_start =
|
|
dir_stack.empty() ? 0 : dir_stack.back().dir_entry_start;
|
|
|
|
// If we've finished all the child directories of the current entry in the
|
|
// stack, pop it off and continue.
|
|
if (dir_entry_start == static_cast<ssize_t>(dir_entries.size())) {
|
|
dir_stack.pop_back();
|
|
continue;
|
|
}
|
|
CARBON_CHECK(dir_entry_start < static_cast<ssize_t>(dir_entries.size()));
|
|
|
|
// Take the last entry under the current directory and try removing it.
|
|
const std::filesystem::path& entry_path = dir_entries.back();
|
|
auto rmdir_result = current.Rmdir(entry_path);
|
|
if (rmdir_result.ok() || rmdir_result.error().no_entity()) {
|
|
// Removed here or elsewhere already, so pop the entry.
|
|
dir_entries.pop_back();
|
|
if (dir_entries.empty()) {
|
|
// The last entry is the input path with an empty stack, so we've
|
|
// finished at this point.
|
|
CARBON_CHECK(dir_stack.empty());
|
|
return Success();
|
|
}
|
|
continue;
|
|
}
|
|
// If we get any error other than not-empty, just return that.
|
|
if (!rmdir_result.error().not_empty()) {
|
|
return std::move(rmdir_result).error();
|
|
}
|
|
|
|
// Recurse into the subdirectory since it isn't empty, opening it, getting a
|
|
// reader, and pushing it onto our stack.
|
|
CARBON_ASSIGN_OR_RETURN(Dir subdir, current.OpenDir(entry_path));
|
|
auto read_result = std::move(subdir).TakeAndRead();
|
|
if (!read_result.ok()) {
|
|
return PathError(
|
|
read_result.error().unix_errnum(),
|
|
"Dir::Read on '{0}' relative to '{1}' during RmdirRecursively",
|
|
entry_path, current.dfd_);
|
|
}
|
|
dir_stack.push_back(
|
|
{*std::move(read_result), static_cast<ssize_t>(dir_entries.size())});
|
|
|
|
// Now read the directory entries. It would be nice to be able to directly
|
|
// remove the files and empty directories as we find them when reading, and
|
|
// the POSIX spec appears to require that to work, but testing shows at
|
|
// least some Linux environments don't work reliably in this case and will
|
|
// fail to visit some entries entirely. As a consequence, we walk the entire
|
|
// directory and collect the entries into data structures before beginning
|
|
// to remove them.
|
|
DirRef::Reader& subdir_reader = dir_stack.back().dir;
|
|
for (const auto& entry : subdir_reader) {
|
|
llvm::StringRef name = entry.name();
|
|
if (name == "." || name == "..") {
|
|
continue;
|
|
}
|
|
if (entry.is_known_dir()) {
|
|
dir_entries.push_back(name.str());
|
|
} else {
|
|
// We end up here for entries known to be regular files, other kinds of
|
|
// non-directory entries, or when the entry kind isn't known.
|
|
//
|
|
// Unless we *know* the entry is a directory, we put it into the unknown
|
|
// entries. For these, we unlink them first in case they are
|
|
// non-directory entries and use the failure of that to move any
|
|
// directories that end up here to the directory entries list.
|
|
unknown_entries.push_back(name.str());
|
|
}
|
|
}
|
|
|
|
// We can immediately try to unlink all the unknown entries, which will
|
|
// include any regular files, and use an error on directories that were
|
|
// unknown above to switch them to the `dir_entries` list.
|
|
while (!unknown_entries.empty()) {
|
|
std::filesystem::path name = unknown_entries.pop_back_val();
|
|
auto unlink_result = subdir_reader.Unlink(name);
|
|
if (unlink_result.ok() || unlink_result.error().no_entity()) {
|
|
continue;
|
|
} else if (!unlink_result.error().is_dir()) {
|
|
return std::move(unlink_result).error();
|
|
}
|
|
dir_entries.push_back(std::move(name));
|
|
}
|
|
|
|
// We'll handle the directory entries we've queued here in the next
|
|
// iteration, removing them or recursing as needed.
|
|
}
|
|
}
|
|
|
|
auto DirRef::ReadlinkSlow(const std::filesystem::path& path)
|
|
-> ErrorOr<std::string, PathError> {
|
|
constexpr ssize_t MinBufferSize =
|
|
#ifdef PATH_MAX
|
|
PATH_MAX
|
|
#else
|
|
1024
|
|
#endif
|
|
;
|
|
// Read directly into a string to avoid allocating two large buffers.
|
|
std::string large_buffer;
|
|
// Stat the symlink to get an initial guess at the size.
|
|
CARBON_ASSIGN_OR_RETURN(FileStatus status, Lstat(path));
|
|
// We try to use the size from the `lstat` unless it is empty, in which case
|
|
// we try to use our minimum buffer size which is `PATH_MAX` or a constant
|
|
// value. We have a fallback to dynamically discover an adequate buffer size
|
|
// below that will handle any inaccuracy.
|
|
ssize_t buffer_size = status.size();
|
|
if (buffer_size == 0) {
|
|
buffer_size = MinBufferSize;
|
|
}
|
|
large_buffer.resize(status.size());
|
|
ssize_t result =
|
|
readlinkat(dfd_, path.c_str(), large_buffer.data(), large_buffer.size());
|
|
if (result == -1) {
|
|
return PathError(errno, "Readlink on '{0}' relative to '{1}'", path, dfd_);
|
|
}
|
|
|
|
// Now the really bad fallback case: if there are racing writes to the
|
|
// symlink, the guessed size may not have been large enough. As a last-ditch
|
|
// effort, begin doubling (from the next power of two >= our min buffer size)
|
|
// the length until it fits. We cap this at 10 MiB to prevent egregious file
|
|
// system contents (or some bug somewhere) from exhausting memory.
|
|
constexpr ssize_t MaxBufferSize = 10 << 20;
|
|
while (result == static_cast<ssize_t>(large_buffer.size())) {
|
|
int64_t next_buffer_size = std::max<ssize_t>(
|
|
MinBufferSize, llvm::NextPowerOf2(large_buffer.size()));
|
|
if (next_buffer_size > MaxBufferSize) {
|
|
return PathError(ENOMEM, "Readlink on '{0}' relative to '{1}'", path,
|
|
dfd_);
|
|
}
|
|
large_buffer.resize(next_buffer_size);
|
|
result = readlinkat(dfd_, path.c_str(), large_buffer.data(),
|
|
large_buffer.size());
|
|
if (result == -1) {
|
|
return PathError(errno, "Readlink on '{0}' relative to '{1}'", path,
|
|
dfd_);
|
|
}
|
|
}
|
|
|
|
// Fix-up the size of the string and return it.
|
|
large_buffer.resize(result);
|
|
return large_buffer;
|
|
}
|
|
|
|
auto MakeTmpDir() -> ErrorOr<RemovingDir, Error> {
|
|
std::filesystem::path tmpdir_path = "/tmp";
|
|
// We use both `TEST_TMPDIR` and `TMPDIR`. The `TEST_TMPDIR` is set by Bazel
|
|
// and preferred to keep tests using the expected output tree rather than
|
|
// the system temporary directory.
|
|
for (const char* tmpdir_env_name : {"TEST_TMPDIR", "TMPDIR"}) {
|
|
const char* tmpdir_env_cstr = getenv(tmpdir_env_name);
|
|
if (tmpdir_env_cstr == nullptr) {
|
|
continue;
|
|
}
|
|
std::filesystem::path tmpdir_env = tmpdir_env_cstr;
|
|
if (!tmpdir_env.is_absolute()) {
|
|
continue;
|
|
}
|
|
tmpdir_path = std::move(tmpdir_env);
|
|
break;
|
|
}
|
|
|
|
std::filesystem::path target = BuildData::BuildTarget.str();
|
|
tmpdir_path /= target.filename();
|
|
return MakeTmpDirWithPrefix(std::move(tmpdir_path));
|
|
}
|
|
|
|
auto MakeTmpDirWithPrefix(std::filesystem::path prefix)
|
|
-> ErrorOr<RemovingDir, Error> {
|
|
std::filesystem::path tmpdir_path = std::move(prefix);
|
|
tmpdir_path += ".XXXXXX";
|
|
|
|
std::string tmpdir_path_buffer = tmpdir_path.native();
|
|
char* result = mkdtemp(tmpdir_path_buffer.data());
|
|
if (result == nullptr) {
|
|
RawStringOstream os;
|
|
os << llvm::formatv("Calling mkdtemp on '{0}' failed: ",
|
|
tmpdir_path.native());
|
|
PrintErrorNumber(os, errno);
|
|
return Error(os.TakeStr());
|
|
}
|
|
CARBON_CHECK(result == tmpdir_path_buffer.data(),
|
|
"`mkdtemp` used a modified path");
|
|
tmpdir_path = std::move(tmpdir_path_buffer);
|
|
|
|
// Because `mkdtemp` doesn't return an open directory atomically, open the
|
|
// created directory and perform safety checks similar to `OpenDir` when
|
|
// creating a new directory.
|
|
CARBON_ASSIGN_OR_RETURN(
|
|
Dir tmp, Cwd().OpenDir(tmpdir_path, OpenExisting, /*creation_mode=*/0,
|
|
OpenFlags::NoFollow));
|
|
// Make sure we try to remove the directory from here on out.
|
|
RemovingDir result_dir(std::move(tmp), tmpdir_path);
|
|
|
|
// It's a bit awkward to report `fstat` errors as `Error`s, but we
|
|
// don't have much choice. The stat failing here would be very weird.
|
|
CARBON_ASSIGN_OR_RETURN(FileStatus stat, result_dir.Stat());
|
|
|
|
// The permissions must be exactly 0700 for a temporary directory, and the UID
|
|
// should be ours.
|
|
if (stat.permissions() != 0700 && stat.unix_uid() != geteuid()) {
|
|
return Error(
|
|
llvm::formatv("Found incorrect permissions or UID on tmpdir '{0}'",
|
|
tmpdir_path.native())
|
|
.str());
|
|
}
|
|
|
|
return result_dir;
|
|
}
|
|
|
|
} // namespace Carbon::Filesystem
|