// Copyright 2018 Bloomberg Finance L.P
//
// 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 <buildboxcommon_fileutils.h>

#include <buildboxcommon_direntwrapper.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_permissions.h>
#include <buildboxcommon_stringutils.h>
#include <buildboxcommon_systemutils.h>
#include <buildboxcommon_temporaryfile.h>
#include <buildboxcommon_timeutils.h>

#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <dirent.h>
#include <fcntl.h>
#include <fstream>
#include <libgen.h>
#include <memory>
#include <optional>
#include <stdexcept>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <system_error>
#include <unistd.h>
#include <utility>
#include <vector>

#if __APPLE__
#define st_mtim st_mtimespec
#define st_atim st_atimespec
#endif

constexpr uint32_t OPEN_MAX_SYMLINKS = 64, OPEN_MAX_DEPTH = 64;

namespace buildboxcommon {

bool FileUtils::existsInDir(int dirfd, const char *path)
{
    struct stat statResult{};
    if (fstatat(dirfd, path, &statResult, AT_SYMLINK_NOFOLLOW) == 0) {
        return true;
    }
    if (errno == ENOENT || errno == ENOTDIR) {
        return false;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Error in fstatat for path \"" << path << "\"");
}

bool FileUtils::isRegularFileNoFollow(const char *path)
{
    return isRegularFileNoFollow(AT_FDCWD, path);
}

bool FileUtils::isRegularFileNoFollow(int dirfd, const char *path)
{
    struct stat statResult{};
    if (fstatat(dirfd, path, &statResult, AT_SYMLINK_NOFOLLOW) == 0) {
        return S_ISREG(statResult.st_mode);
    }
    if (errno == ENOENT || errno == ENOTDIR) {
        return false;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Error in fstatat for path \"" << path << "\"");
}

bool FileUtils::isRegularFile(const char *path)
{
    return isRegularFile(AT_FDCWD, path);
}

bool FileUtils::isRegularFile(int dirfd, const char *path)
{
    struct stat statResult{};
    if (fstatat(dirfd, path, &statResult, 0) == 0) {
        return S_ISREG(statResult.st_mode);
    }
    if (errno == ENOENT || errno == ENOTDIR) {
        return false;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Error in fstatat for path \"" << path << "\"");
}

bool FileUtils::isDirectoryNoFollow(const char *path)
{
    struct stat statResult{};
    if (lstat(path, &statResult) == 0) {
        return S_ISDIR(statResult.st_mode);
    }
    if (errno == ENOENT || errno == ENOTDIR) {
        return false;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Error in lstat for path \"" << path << "\"");
}

bool FileUtils::isDirectory(const char *path)
{
    struct stat statResult{};
    if (stat(path, &statResult) == 0) {
        return S_ISDIR(statResult.st_mode);
    }
    if (errno == ENOENT || errno == ENOTDIR) {
        return false;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Error in stat for path \"" << path << "\"");
}

bool FileUtils::isDirectory(int fd)
{
    struct stat statResult{};
    if (fstat(fd, &statResult) == 0) {
        return S_ISDIR(statResult.st_mode);
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Error in fstat for file descriptor: " << fd);
}

bool FileUtils::isSymlink(const char *path)
{
    struct stat statResult{};
    if (lstat(path, &statResult) == 0) {
        return S_ISLNK(statResult.st_mode);
    }
    if (errno == ENOENT || errno == ENOTDIR) {
        return false;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Error in lstat for path \"" << path << "\"");
}

bool FileUtils::directoryIsEmpty(const char *path)
{
    DirentWrapper root(path);

    // DirentWrapper's entry will point to an entry in the directory (not
    // including "." and ".."). If the entry isn't nullptr, then the directory
    // can't be empty, therefore return false.
    return (root.entry() == nullptr);
}

void FileUtils::createDirectory(const char *path, mode_t mode)
{
    createDirectory(AT_FDCWD, path, mode);
}

void FileUtils::createDirectory(int dirfd, const char *path, mode_t mode)
{
    createDirectoriesInPath(dirfd, path, mode);
}

void FileUtils::createDirectoriesInPath(int dirfd, const std::string &path,
                                        const mode_t mode)
{
    // Trim trailing slashes and trailing `.` and `..` components
    std::string trimmed_path = path;
    while (true) {
        const auto slash_pos = trimmed_path.rfind('/');
        if (slash_pos == std::string::npos) {
            break;
        }
        const auto basename = trimmed_path.substr(slash_pos + 1);
        if (basename == "" || basename == "." || basename == "..") {
            trimmed_path = trimmed_path.substr(0, slash_pos);
        }
        else {
            break;
        }
    }
    if (trimmed_path.empty() && !path.empty()) {
        // Path consists solely of `.` and `..` components, nothing to do.
        return;
    }

    // Attempt to create the directory:
    const auto mkdir_status = mkdirat(dirfd, trimmed_path.c_str(), mode);
    const auto mkdir_error = errno;

    const auto log_and_throw = [&trimmed_path](const int errno_value) {
        BUILDBOX_LOG_ERROR("Could not create directory [" + trimmed_path +
                           "]: " + strerror(errno_value))
        throw std::system_error(errno_value, std::system_category());
    };

    if (mkdir_status == 0 || mkdir_error == EEXIST) {
        return; // Directory was successfully created or already exists, done.
    }
    if (mkdir_error != ENOENT) { // Something went wrong, aborting.
        log_and_throw(mkdir_error);
    }

    // `mkdir_error == ENOENT` => Some portion of the path does not exist yet.
    // We'll recursively create the parent directory and try again:
    const std::string parent_path =
        trimmed_path.substr(0, trimmed_path.rfind('/'));
    createDirectoriesInPath(dirfd, parent_path, mode);

    // Now that all the parent directories exist, we create the last directory:
    if (mkdirat(dirfd, trimmed_path.c_str(), mode) != 0) {
        // If the path contains a `..` segment, it's possible that the last
        // directory already exists.
        if (errno != EEXIST) {
            log_and_throw(errno);
        }
    }
}

void FileUtils::deleteDirectory(const char *path)
{
    deleteDirectory(AT_FDCWD, path);
}

void FileUtils::deleteDirectory(int dirfd, const char *path)
{
    deleteRecursively(dirfd, path, true);
}

void FileUtils::clearDirectory(const char *path)
{
    clearDirectory(AT_FDCWD, path);
}

void FileUtils::clearDirectory(int dirfd, const char *path)
{
    deleteRecursively(dirfd, path, false);
}

bool FileUtils::isExecutable(const char *path)
{
    struct stat statResult{};
    if (stat(path, &statResult) == 0) {
        return statResult.st_mode & S_IXUSR;
    }
    return false;
}

bool FileUtils::isExecutable(int fd)
{
    struct stat statResult{};
    if (fstat(fd, &statResult) == 0) {
        return statResult.st_mode & S_IXUSR;
    }
    return false;
}

struct stat FileUtils::getFileStat(int dirfd, const char *path)
{
    struct stat statResult{};
    if (fstatat(dirfd, path, &statResult, 0) != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to get file stats at \"" << path << "\"");
    }
    return statResult;
}

struct stat FileUtils::getFileStat(const char *path)
{
    struct stat statResult{};
    if (stat(path, &statResult) != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to get file stats at \"" << path << "\"");
    }
    return statResult;
}

struct stat FileUtils::getFileStat(const int fd)
{
    struct stat statResult{};
    if (fstat(fd, &statResult) != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to get file stats for file descriptor " << fd);
    }
    return statResult;
}

int64_t FileUtils::getFileSize(const char *path)
{
    struct stat statResult = getFileStat(path);
    return statResult.st_size;
}

int64_t FileUtils::getFileSize(int fd)
{
    struct stat statResult = getFileStat(fd);
    return statResult.st_size;
}

mode_t FileUtils::getUnixMode(int fd)
{
    struct stat statResult = getFileStat(fd);
    return statResult.st_mode;
}

mode_t FileUtils::getUnixMode(const char *path)
{
    struct stat statResult = getFileStat(path);
    return statResult.st_mode;
}

std::chrono::system_clock::time_point FileUtils::getFileMtime(const char *path)
{
    struct stat statResult = getFileStat(path);
    return FileUtils::getMtimeTimepoint(statResult);
}

std::chrono::system_clock::time_point FileUtils::getFileMtime(const int fd)
{
    struct stat statResult = getFileStat(fd);
    return FileUtils::getMtimeTimepoint(statResult);
}

std::chrono::system_clock::time_point
FileUtils::getMtimeTimepoint(struct stat &result)
{
    const std::chrono::system_clock::time_point timepoint =
        std::chrono::system_clock::from_time_t(result.st_mtim.tv_sec) +
        std::chrono::microseconds{result.st_mtim.tv_nsec / 1000};
    return timepoint;
}

void FileUtils::setFileMtime(
    const int fd, const std::chrono::system_clock::time_point timepoint)
{
    const struct timespec new_mtime = TimeUtils::make_timespec(timepoint);
    const struct stat stat_result = FileUtils::getFileStat(fd);
    // AIX stat.h defines st_timespec_t as the return of stat().st_atim
    const struct timespec atime = {
        stat_result.st_atim.tv_sec,
        static_cast<long>(stat_result.st_atim.tv_nsec)};
    const struct timespec times[2] = {atime, new_mtime};
    if (futimens(fd, times) == 0) {
        return;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(std::system_error, errno,
                                          std::system_category,
                                          "Failed to set file mtime");
}

void FileUtils::setFileMtime(
    int dirfd, const char *path,
    const std::chrono::system_clock::time_point timepoint)
{
    const struct timespec new_mtime = TimeUtils::make_timespec(timepoint);
    const struct stat stat_result = FileUtils::getFileStat(dirfd, path);
    // AIX stat.h defines st_timespec_t as the return of stat().st_atim
    const struct timespec atime = {
        stat_result.st_atim.tv_sec,
        static_cast<long>(stat_result.st_atim.tv_nsec)};
    const struct timespec times[2] = {atime, new_mtime};
    if (utimensat(dirfd, path, times, 0) == 0) {
        return;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Failed to set file \"" << path << "\" mtime");
}

void FileUtils::setFileMtime(
    const char *path, const std::chrono::system_clock::time_point timepoint)
{
    setFileMtime(AT_FDCWD, path, timepoint);
}

std::vector<struct timespec> makeTimeSpecs(
    const std::optional<std::chrono::system_clock::time_point> &atime,
    const std::optional<std::chrono::system_clock::time_point> &mtime)
{
    std::vector<struct timespec> result(2);

    if (atime.has_value()) {
        result[0] = TimeUtils::make_timespec(*atime);
    }
    else {
        result[0].tv_nsec = UTIME_NOW;
    }
    if (mtime.has_value()) {
        result[1] = TimeUtils::make_timespec(*mtime);
    }
    else {
        result[1].tv_nsec = UTIME_NOW;
    }

    return result;
}

void FileUtils::setFileTimes(
    int dirfd, const char *path,
    const std::optional<std::chrono::system_clock::time_point> &atime,
    const std::optional<std::chrono::system_clock::time_point> &mtime)
{
    const auto timeSpecs = makeTimeSpecs(atime, mtime);
    if (utimensat(dirfd, path, timeSpecs.data(), 0) == 0) {
        return;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Failed to set file \"" << path
                                << "\" atime and mtime using utimensat");
}

void FileUtils::setFileTimes(
    int fd, const std::optional<std::chrono::system_clock::time_point> &atime,
    const std::optional<std::chrono::system_clock::time_point> &mtime)
{
    const auto timeSpecs = makeTimeSpecs(atime, mtime);
    if (futimens(fd, timeSpecs.data()) == 0) {
        return;
    }
    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
        std::system_error, errno, std::system_category,
        "Failed to set fd \"" << fd << "\" atime and mtime using futimens");
}

void FileUtils::setFileTimes(
    const char *path,
    const std::optional<std::chrono::system_clock::time_point> &atime,
    const std::optional<std::chrono::system_clock::time_point> &mtime)
{
    setFileTimes(AT_FDCWD, path, atime, mtime);
}

void FileUtils::makeExecutable(const char *path)
{
    // TODO: evaluate using O_NOFOLLOW to avoid folling symlinks
    FileDescriptor fd(open(path, O_RDONLY | O_CLOEXEC));
    if (fd.get() != -1) {
        struct stat statResult{};
        if (fstat(fd.get(), &statResult) == 0) {
            if (fchmod(fd.get(), statResult.st_mode | S_IXUSR | S_IXGRP |
                                     S_IXOTH) != 0) {
                BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                    std::system_error, errno, std::system_category,
                    "Error in chmod for path \"" << path << "\"");
            }
        }
        else {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Error in stat for file path \"" << path << "\"");
        }
    }
    else {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error in opening file path \"" << path << "\"");
    }
}

std::string FileUtils::getFileContents(const char *path)
{
    return getFileContents(AT_FDCWD, path);
}

std::string FileUtils::getFileContents(int dirfd, const char *path)
{
    FileDescriptor fd(openat(dirfd, path, O_RDONLY | O_CLOEXEC));
    if (fd.get() < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to open file \"" << path << "\"");
    }
    return getFileContents(fd.get());
}

std::string FileUtils::getFileContents(int fd)
{
    return getFileContents(fd, 0, std::nullopt);
}

std::string FileUtils::getFileContents(int fd, int64_t offset,
                                       std::optional<int64_t> limit)
{
    struct stat statResult = getFileStat(fd);
    if (!S_ISREG(statResult.st_mode)) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "getFileContents() called on a directory or special file");
    }
    if (offset > statResult.st_size) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "getFileContents() called with offset greater than file size");
    }
    if (limit.has_value() && offset + *limit > statResult.st_size) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "getFileContents() called with limit greater than file size");
    }

    const int64_t end =
        limit.has_value() ? offset + *limit : statResult.st_size;
    std::string buffer(end - offset, '\0');
    auto pos = offset;
    while (pos < end) {
        ssize_t n = pread(fd, &buffer[pos - offset], end - pos, pos);
        if (n < 0) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Failed to read file in getFileContents()");
        }
        else if (n == 0) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error,
                "Unexpected end of file in getFileContents()");
        }
        else {
            pos += n;
        }
    }
    return buffer;
}

void FileUtils::copyFile(const char *src_path, const char *dest_path)
{
    copyFile(AT_FDCWD, src_path, AT_FDCWD, dest_path);
}

void FileUtils::copyFile(int src_dirfd, const char *src_path, int dest_dirfd,
                         const char *dest_path, mode_t mode,
                         struct stat *srcStat, struct stat *destStat)
{
    int err = 0;

    FileDescriptor src(openat(src_dirfd, src_path, O_RDONLY | O_CLOEXEC));
    if (src.get() == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to open src file at " << src_path);
    }
    FileDescriptor dest(
        openat(dest_dirfd, dest_path,
               O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC,
               PERMISSION_RWUSR_RALL));
    if (dest.get() == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to open dest file at " << dest_path);
    }

    auto stat = FileUtils::getFileStat(src.get());
    if (err == 0) {
        if (srcStat != nullptr) {
            *srcStat = stat;
        }

        if (mode == 0) {
            mode = stat.st_mode;
        }

        bool doCopy = false;
        err = FileUtils::cloneFile(src.get(), dest.get());
        if (err != 0) {
            // On these errors, fall back to IO copy and clear the err
            if (err == EXDEV || err == ENOSYS || err == EOPNOTSUPP) {
                doCopy = true;
                err = 0;
            }
            else {
                BUILDBOX_LOG_ERROR("Failed when calling copy_file_range into "
                                   << dest_path);
            }
        }

        if (doCopy) {
            ssize_t rdsize = 0, wrsize = 0;
            const size_t bufsize = 65536;
            auto buf = std::make_unique<char[]>(bufsize);

            while ((rdsize = read(src.get(), buf.get(), bufsize)) > 0) {
                wrsize = write(dest.get(), buf.get(),
                               static_cast<unsigned long>(rdsize));
                if (wrsize != rdsize) {
                    err = EIO;
                    BUILDBOX_LOG_ERROR("Failed to write to file at "
                                       << dest_path);
                    break;
                }
            }

            if (rdsize == -1) {
                err = errno;
                BUILDBOX_LOG_ERROR("Failed to read file at " << src_path);
            }
        }

        if (fchmod(dest.get(), mode) != 0) {
            err = errno;
            BUILDBOX_LOG_ERROR("Failed to set mode of file at " << dest_path);
        }
    }

    if (err != 0) {
        if (unlink(dest_path) != 0) {
            err = errno;
            BUILDBOX_LOG_ERROR("Failed to remove file at " << dest_path);
        }
        throw std::system_error(err, std::system_category());
    }

    if (destStat != nullptr) {
        *destStat = FileUtils::getFileStat(dest.get());
    }
}

int FileUtils::cloneFile(int srcFd, int dstFd)
{
// static support check
#if defined(__linux__) && defined(SYS_copy_file_range)
    // runtime kernel version check
    if (!PlatformInfo::get().enableCopyFileRange()) {
        // kernel too old
        return ENOSYS;
    }

    auto stat = FileUtils::getFileStat(srcFd);
    auto len = stat.st_size;
    while (len > 0) {
#if (__GLIBC__ < 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ < 30)
        // Instead of calling copy_file_range() directly, invoke it via
        // syscall to ensure
        // compatibility with older OS releases where the glibc version
        // is older than 2.27. Also, avoid the userspace fallback
        // in 2.27-2.29
        auto copied =
            syscall(SYS_copy_file_range, srcFd, NULL, dstFd, NULL, len, 0);
#else
        // glibc version >= 2.30
        auto copied = copy_file_range(srcFd, NULL, dstFd, NULL, len, 0);
#endif
        if (copied == -1) {
            return errno;
        }
        len -= copied;
    }

    return 0;
#else
    // Suppress unused parameter warnings on non-Linux platforms
    (void)srcFd;
    (void)dstFd;
    // not linux or syscall not supported
    return ENOSYS;
#endif
}

void FileUtils::writeFileAtomically(
    const std::string &path, const std::string &data, mode_t mode,
    const std::string &intermediate_directory, const std::string &prefix,
    const std::optional<std::chrono::time_point<std::chrono::system_clock>>
        mtime)
{
    std::string temporary_directory;
    if (!intermediate_directory.empty()) {
        temporary_directory = intermediate_directory;
    }
    else {
        // If no intermediate directory is specified, we use the parent
        // directory in `path`.

        // `dirname()` modifies its input, so we give it a copy.
        std::vector<char> output_path(path.cbegin(), path.cend());
        output_path.push_back('\0');
        const char *parent_directory = dirname(&output_path[0]);
        if (parent_directory == nullptr) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Could not determine intermediate "
                    << "directory with `dirname(3)` for atomic write to path "
                       "\""
                    << path << "\"");
        }
        temporary_directory = std::string(parent_directory);
    }

    std::unique_ptr<buildboxcommon::TemporaryFile> temp_file = nullptr;
    try {
        temp_file = std::make_unique<buildboxcommon::TemporaryFile>(
            temporary_directory.c_str(), prefix.c_str());
    }
    catch (const std::system_error &e) {
        BUILDBOX_LOG_ERROR("Error creating intermediate file in " +
                           temporary_directory + " for atomic write to " +
                           path + ": " + std::string(e.what()));
        throw e;
    }

    // `temp_file`'s destructor will `unlink()` the created file, removing
    // it from the temporary directory.

    const std::string temp_filename = temp_file->name();

    // Writing the data to it:
    std::ofstream file(temp_filename, std::fstream::binary);
    file << data;
    file.close();

    if (!file.good()) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed writing to temporary file \"" << temp_filename << "\"");
    }

    // Update the file permissions based on the given mode
    if (fchmod(temp_file->fd(), mode) != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to update mode of \"" << temp_filename << "\" to \""
                                          << std::oct << mode << "\"");
    }

    // Atomically rename temporary file to the final path
    if (rename(temp_filename.c_str(), path.c_str()) != 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Could not atomically rename temporary file \""
                << temp_filename << "\" to \"" << path << "\"");
    }

    // Update the mtime if necessary
    if (mtime.has_value()) {
        setFileTimes(path.c_str(), std::nullopt, mtime);
    }
}

void FileUtils::copyRecursively(int srcfd, int destfd)
{
    DirentWrapper srcdir(srcfd, ".");

    while (srcdir.entry() != nullptr) {
        std::string filename =
            FileUtils::pathBasename(srcdir.currentEntryPath());
        if (srcdir.currentEntryIsDirectory()) {
            auto nextSrcDir = srcdir.nextDir();
            FileDescriptor nextTargetFd(openBeneath(
                destfd, filename, O_DIRECTORY | O_RDONLY | O_CLOEXEC, true));
            copyRecursively(nextSrcDir.fd(), nextTargetFd.get());
        }
        else if (srcdir.currentEntryIsFile()) {
            copyFile(srcdir.fd(), filename.c_str(), destfd, filename.c_str());
        }
        else if (srcdir.currentEntryIsSymlink()) {
            std::string target(PATH_MAX, '\0');
            ssize_t target_len = readlinkat(srcdir.fd(), filename.c_str(),
                                            &target[0], target.size() - 1);
            if (target_len == -1) {
                BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                    std::system_error, errno, std::system_category,
                    "Error reading symlink at \"" << srcdir.currentEntryPath()
                                                  << "\"");
            }
            target.resize(target_len);
            if (symlinkat(target.c_str(), destfd, filename.c_str()) == -1) {
                BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                    std::system_error, errno, std::system_category,
                    "Error copying symlink at \"" << srcdir.currentEntryPath()
                                                  << "\"");
            }
        }
        else {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::invalid_argument,
                "Non-copyable file in source: " << srcdir.currentEntryPath());
        }
        srcdir.next();
    }
}

void FileUtils::deleteRecursively(int dirfd, const char *path,
                                  const bool delete_root_directory)
{
    DirectoryTraversalFnPtr chmod_func = [](const char *dir_path, int fd) {
        struct stat statResult{};
        if (fstatat(fd, dir_path, &statResult, 0) == 0 &&
            (statResult.st_mode & S_IWUSR) != S_IWUSR) {

            //
            // There is nothing that can be done if fchmodat fails,
            // so just ignore the return value. The actual error will
            // be thrown from file deletion loop.
            //

            // fchmodat is declared as __attribute__((unused_result)__ in some
            // glibc versions, and it can't be suppressed in GCC by just
            // casting the result to void.
            // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66425
            auto ret = fchmodat(fd, dir_path, statResult.st_mode | S_IWUSR, 0);
            (void)ret;
        }
    };

    DirectoryTraversalFnPtr rmdir_func = [dirfd](const char *dir_path,
                                                 int fd) {
        std::string dir_basename(dir_path);
        if (fd != -1) {
            dir_basename = FileUtils::pathBasename(dir_path);
            if (dir_basename.empty()) {
                return;
            }
        }
        else {
            // The root directory has no parent directory file descriptor,
            // but it may still be relative to the specified dirfd.
            fd = dirfd;
        }
        // unlinkat will disregard the file descriptor and call
        // rmdir/unlink on the path depending on the entity
        // type(file/directory).
        //
        // For deletion using the file descriptor, the path must be
        // relative to the directory the file descriptor points to.
        if (unlinkat(fd, dir_basename.c_str(), AT_REMOVEDIR) == -1) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Error removing directory \"" << dir_path << "\"");
        }
    };

    DirectoryTraversalFnPtr unlink_func = [](const char *file_path = nullptr,
                                             int fd = 0) {
        if (unlinkat(fd, file_path, 0) == -1) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Error removing file \"" << file_path << "\"");
        }
    };

    // Do a first pass to make all directories owner-writable
    // to ensure any files within can be removed
    {
        DirentWrapper root(dirfd, path);
        fileDescriptorTraverseAndApply(&root, chmod_func, nullptr,
                                       delete_root_directory, false);
    }

    struct stat rootDirStatResult{};
    bool needs_write_perm = false;
    if (!delete_root_directory) {
        if (fstatat(dirfd, path, &rootDirStatResult, 0) == -1) {
            BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                std::system_error, errno, std::system_category,
                "Unable to fstatat root directory \"" << path << "\"");
        }
        needs_write_perm = (rootDirStatResult.st_mode & S_IWUSR) != S_IWUSR;
        if (needs_write_perm) {
            // Ensure the root directory has write permissions to ensure
            // everything inside can be removed. We restore it after running
            // the deletion below
            if (fchmodat(dirfd, path, rootDirStatResult.st_mode | S_IWUSR,
                         0) != 0) {
                BUILDBOX_LOG_WARNING(
                    "Failed to add write permission to root directory \""
                    << path << "\" before clearing its contents");
            }
        }
    }
    DirentWrapper root(dirfd, path);
    try {
        fileDescriptorTraverseAndApply(&root, rmdir_func, unlink_func,
                                       delete_root_directory, true);
    }
    catch (...) {
        if (needs_write_perm) {
            // Restore original permissions on root directory
            if (fchmodat(dirfd, path, rootDirStatResult.st_mode, 0) != 0) {
                BUILDBOX_LOG_WARNING(
                    "Failed to restore permissions for root directory \""
                    << path << "\" after clearing its contents");
            }
        }
        throw;
    }
    if (needs_write_perm) {
        // Restore original permissions on root directory
        if (fchmodat(dirfd, path, rootDirStatResult.st_mode, 0) != 0) {
            BUILDBOX_LOG_WARNING(
                "Failed to restore permissions for root directory \""
                << path << "\" after clearing its contents");
        }
    }
}

void FileUtils::fileDescriptorTraverseAndApply(
    DirentWrapper *dir, const DirectoryTraversalFnPtr &dir_func,
    const DirectoryTraversalFnPtr &file_func, bool apply_to_root,
    bool pass_parent_fd)
{
    while (dir->entry() != nullptr) {
        if (dir->currentEntryIsDirectory()) {
            auto nextDir = dir->nextDir();
            fileDescriptorTraverseAndApply(&nextDir, dir_func, file_func, true,
                                           pass_parent_fd);
        }
        else {
            if (file_func != nullptr) {
                file_func(dir->entry()->d_name, dir->fd());
            }
        }
        dir->next();
    }
    if (apply_to_root && dir_func != nullptr) {
        if (pass_parent_fd) {
            dir_func(dir->path().c_str(), dir->pfd());
        }
        else {
            dir_func(dir->path().c_str(), dir->fd());
        }
    }
}

std::string FileUtils::makePathAbsolute(const std::string &path,
                                        const std::string &cwd)
{
    if (cwd.empty() || cwd.front() != '/') {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "cwd must be an absolute path: ["
                                           << cwd << "]");
    }

    std::string full_path;
    if (path.front() != '/') {
        full_path = cwd + '/' + path;
    }
    else {
        full_path = path;
    }
    std::string normalized_path = FileUtils::normalizePath(full_path.c_str());

    // normalize_path removes trailing slashes, so let's preserve them here
    if (path.back() == '/' && normalized_path.back() != '/') {
        normalized_path.push_back('/');
    }
    return normalized_path;
}

std::string FileUtils::joinPathSegments(const std::string &firstSegment,
                                        const std::string &secondSegment,
                                        const bool forceSecondSegmentRelative)
{
    if (firstSegment.empty() || secondSegment.empty()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Both path segments must be non-empty."
                                           << " firstSegment=[" << firstSegment
                                           << "], secondSegment=["
                                           << secondSegment << "]");
    }

    const std::string firstSegmentNormalized =
        FileUtils::normalizePath(firstSegment.c_str());

    std::string secondSegmentNormalized =
        FileUtils::normalizePath(secondSegment.c_str());

    if (!forceSecondSegmentRelative &&
        secondSegmentNormalized.front() == '/') {
        return secondSegmentNormalized;
    }
    else {
        // Now all we have to do is concatenate the paths with a '/' between
        // them and call normalizePath() to evaluate any remaining `..` and
        // make sure we remove any potential double '//' (e.g.
        // `joinPathSegments('a/','/b', true) -> '/a/b'`)
        const std::string combined_path =
            firstSegmentNormalized + '/' + secondSegmentNormalized;
        return FileUtils::normalizePath(combined_path.c_str());
    }
}

std::string
FileUtils::joinPathSegmentsNoEscape(const std::string &basedir,
                                    const std::string &pathWithinBasedir,
                                    const bool forceRelativePathWithinBaseDir)
{
    const std::string normalizedBaseDir =
        FileUtils::normalizePath(basedir.c_str());

    const std::string normalizedPathWithinBaseDir =
        FileUtils::normalizePath(pathWithinBasedir.c_str());

    // Join paths using`joinPathSegments`
    std::string joinedPath = FileUtils::joinPathSegments(
        basedir, normalizedPathWithinBaseDir, forceRelativePathWithinBaseDir);

    // Let's by default assume that the path escapes to reduce potential missed
    // cases in which we may incorrectly assume it doesn't escape, and check
    // for the known cases we are certain it doesn't escape
    bool escapes = true;

    // Do not allow any escaping.
    if (joinedPath == normalizedBaseDir) {
        // Normalized path is the base directory
        escapes = false;
    }
    else if (normalizedBaseDir == "/" || normalizedBaseDir == "" ||
             normalizedBaseDir == ".") {
        // Normalized baseDir is `/` or `` or `.` (root or relative to cwd)

        // In these cases not having `..` in the normalizedPathWithinBaseDir is
        // enough to make sure it doesn't escape
        if (normalizedPathWithinBaseDir != ".." &&
            normalizedPathWithinBaseDir.substr(0, 3) != "../") {
            escapes = false;
        }
    }
    else if (joinedPath.find(normalizedBaseDir) == 0) {
        // Normalized path is within base directory

        // Make sure that this has a '/' as well after the `normalizedBaseDir`
        // Detects cases like:
        //       `joinPathSegmentsNoEscape('/base/dir', '../dir2')`
        // which would result in '/base/dir2', matching '/base/dir'
        // prefix but actually escaping `/base/dir`
        if (joinedPath.size() >=
            normalizedBaseDir.size()) { // sanity check, should always be the
                                        // case if we get here
            if (joinedPath.at(normalizedBaseDir.size()) == '/') {
                escapes = false;
            }
        }
    }

    // At this point the checks we made above should inform us whether this
    // joined path is escaping
    if (escapes) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Detected escaping path while joining "
                << "basedir=[" << basedir << "] and "
                << "pathWithinBasedir=[" << pathWithinBasedir << "]."
                << " Resulting escaping path=[" << joinedPath << "].");
    }
    return joinedPath;
}

bool FileUtils::containsDotDot(const char *path)
{
    std::string_view pathView(path);
    while (!pathView.empty()) {
        size_t slashPos = pathView.find('/');
        std::string_view segment;
        if (slashPos == std::string_view::npos) {
            segment = pathView;
        }
        else {
            segment = pathView.substr(0, slashPos);
        }
        if (segment == "..") {
            return true;
        }
        if (slashPos == std::string_view::npos) {
            break;
        }
        else {
            pathView.remove_prefix(slashPos + 1);
        }
    }
    return false;
}

std::string FileUtils::normalizePath(const char *path)
{
    std::string_view pathView(path);
    std::vector<std::string> segments;

    const bool global = pathView[0] == '/';
    while (!pathView.empty()) {
        size_t slashPos = pathView.find('/');
        std::string segment;
        if (slashPos == std::string_view::npos) {
            segment = std::string(pathView);
        }
        else {
            segment = std::string(pathView.substr(0, slashPos));
        }
        if (segment == ".." && !segments.empty() && segments.back() != "..") {
            segments.pop_back();
        }
        else if (global && segment == ".." && segments.empty()) {
            // dot-dot in the root directory refers to the root directory
            // itself and can thus be dropped.
        }
        else if (segment != "." && segment != "") {
            segments.push_back(segment);
        }
        if (slashPos == std::string_view::npos) {
            break;
        }
        else {
            pathView.remove_prefix(slashPos + 1);
        }
    }

    std::string result(global ? "/" : "");
    if (segments.size() > 0) {
        for (const auto &segment : segments) {
            result += segment + "/";
        }
        result.pop_back();
    }
    else if (!global) {
        // The normalized path for the current directory is `.`,
        // not an empty string.
        result = ".";
    }
    return result;
}

std::string FileUtils::pathBasename(const std::string &path)
{
    std::string basename(path);

    // Check for root or empty
    if (basename.empty() || basename.size() == 1) {
        return "";
    }

    // Remove trailing slashes.
    StringUtils::rtrim(&basename, [](const char c) { return c == '/'; });

    auto pos = basename.rfind('/');

    if (pos == std::string::npos) {
        return basename;
    }
    else {
        return basename.substr(pos + 1);
    }
}

std::string FileUtils::pathBasename(const char *path)
{
    std::string basename(path);
    return FileUtils::pathBasename(basename);
}

// NOTE: Linux 5.6+ supports this in the kernel with openat2() and
// RESOLVE_BENEATH / RESOLVE_IN_ROOT. This userspace implementation is less
// efficient but also works on older Linux and non-Linux systems.
int FileUtils::openInternal(int dirfd, const std::string &orig_path, int flags,
                            bool dir_is_root, bool createDirectories)
{
    std::vector<FileDescriptor> dirfds;
    dirfds.emplace_back(dirfd, false);

    uint32_t symlinks = 0;
    std::string path = orig_path;

    while (true) {
        if (path.empty()) {
            errno = ENOENT;
            return -1;
        }

        const auto slash_pos = path.find('/');
        const bool last = slash_pos == std::string::npos;
        const std::string segment = last ? path : path.substr(0, slash_pos);

        if (slash_pos == 0) {
            if (!dir_is_root) {
                // Match Linux RESOLVE_BENEATH
                errno = EXDEV;
                return -1;
            }

            // Absolute path, resolve it in specified root instead of process
            // root. Drop all dirfds except for the root dirfd.
            dirfds.erase(dirfds.begin() + 1, dirfds.end());
        }
        else if (segment == "." || segment == "..") {
            // `.` never has any effect.
            // `..` has no effect when the current directory is the root
            // `(dirfds.size() == 1)` as `/..` is equivalent to `/`.
            if (segment == "..") {
                if (dirfds.size() > 1) {
                    dirfds.pop_back();
                }
                else if (!dir_is_root) {
                    // Match Linux RESOLVE_BENEATH
                    errno = EXDEV;
                    return -1;
                }
            }

            if (last) {
                // This was the final path segment, return current directory.
                return openat(dirfds.back().get(), ".", flags);
            }
        }
        else {
            // First check whether path segment is a symlink
            std::string target(PATH_MAX, '\0');
            ssize_t target_len =
                readlinkat(dirfds.back().get(), segment.c_str(), &target[0],
                           target.size() - 1);
            if (target_len > 0) {
                // Path segment is symlink
                target.resize(target_len);

                if (last && (flags & O_NOFOLLOW)) {
                    // Don't follow symlink as caller specified O_NOFOLLOW.
                    errno = ELOOP;
                    return -1;
                }

                if (++symlinks > OPEN_MAX_SYMLINKS) {
                    // Too many symbolic links, possible loop
                    errno = ELOOP;
                    return -1;
                }

                if (target == "") {
                    errno = ENOENT;
                    return -1;
                }

                // Continue path resolution with symlink target
                if (last) {
                    path = target;
                }
                else {
                    path = target + path.substr(slash_pos);
                }
                continue;
            }

            // Create the next segment as a directory if it is missing
            if (createDirectories && (!last || (flags & O_DIRECTORY)) &&
                !existsInDir(dirfds.back().get(), segment.c_str())) {
                createDirectory(dirfds.back().get(), segment.c_str());
            }

            // Use O_NOFOLLOW to avoid TOCTOU race condition if non-symlink
            // is replaced with symlink between `readlinkat` and `openat`.
            int fd = openat(dirfds.back().get(), segment.c_str(),
                            (last ? flags : O_RDONLY | O_DIRECTORY) |
                                O_NOFOLLOW | O_CLOEXEC);
            if (fd < 0) {
                // Failed to open path segment, propagate error
                return -1;
            }

            if (last) {
                // Successfully opened final path segment
                return fd;
            }
            else {
                // Successfully opened inner directory segment
                dirfds.emplace_back(fd, true);

                if (dirfds.size() > OPEN_MAX_DEPTH) {
                    // Restrict maximum depth of path to open to avoid an
                    // excessive number of open directory file descriptors.
                    errno = ELOOP;
                    return -1;
                }
            }
        }

        // Skip processed path segment
        path = path.substr(slash_pos + 1);

        // Multiple consecutive path separators are equivalent to a single one
        while (!path.empty() && path.front() == '/') {
            path = path.substr(1);
        }

        if (path.empty()) {
            // Path had a trailing slash, set path to `.` to return the
            // current directory.
            path = ".";
        }
    }
}

int FileUtils::openBeneath(int dirfd, const std::string &path, int flags,
                           bool createDirectories)
{
#if defined(__linux__)
    if (!createDirectories && PlatformInfo::get().enableOpenat2()) {
        // Use openat2() where available as it's much more efficient
        int ret =
            openat2(dirfd, path.c_str(), flags, 0, OPENAT2_RESOLVE_BENEATH);
        if (ret >= 0 || errno != EAGAIN) {
            return ret;
        }
        // If openat2() fails with EAGAIN despite the retry loop, fall back to
        // the userspace implementation.
    }
#endif

    return openInternal(dirfd, path, flags, /* dir_is_root */ false,
                        createDirectories);
}

int FileUtils::openInRoot(int dirfd, const std::string &path, int flags,
                          bool createDirectories)
{
#if defined(__linux__)
    if (!createDirectories && PlatformInfo::get().enableOpenat2()) {
        // Use openat2() where available as it's much more efficient
        int ret =
            openat2(dirfd, path.c_str(), flags, 0, OPENAT2_RESOLVE_IN_ROOT);
        if (ret >= 0 || errno != EAGAIN) {
            return ret;
        }
        // If openat2() fails with EAGAIN despite the retry loop, fall back to
        // the userspace implementation.
    }
#endif

    return openInternal(dirfd, path, flags, /* dir_is_root */ true,
                        createDirectories);
}

int FileUtils::openBeneath(const std::string &dir, const std::string &path,
                           int flags, bool createDirectories)
{
    int dirfd = open(dir.empty() ? "/" : dir.c_str(),
                     O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (dirfd < 0) {
        return -1;
    }

    FileDescriptor dirfd_object(dirfd, true);
    return openBeneath(dirfd, path, flags, createDirectories);
}

int FileUtils::openInRoot(const std::string &dir, const std::string &path,
                          int flags, bool createDirectories)
{
    int dirfd = open(dir.empty() ? "/" : dir.c_str(),
                     O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (dirfd < 0) {
        return -1;
    }

    FileDescriptor dirfd_object(dirfd, true);
    return openInRoot(dirfd, path, flags, createDirectories);
}

#if defined(__linux__)
int FileUtils::openat2(int dirfd, const char *path, uint64_t flags,
                       mode_t mode, uint64_t resolve)
{
#if defined(SYS_openat2)
    constexpr long syscallNr = SYS_openat2;
#else
    constexpr long syscallNr = 437;
#endif

    struct /* open_how */ {
        uint64_t flags;
        uint64_t mode;
        uint64_t resolve;
    } how = {.flags = flags, .mode = mode, .resolve = resolve};

    // openat2() can fail with -EAGAIN if there was a racing rename or mount
    // *anywhere on the system*. Retry up to 128 times, based on extensive
    // tests done by the author of openat2(), see libpathrs.
    constexpr int MAX_RETRIES = 128;
    for (int i = 0; i <= MAX_RETRIES; i++) {
        int ret = (int)syscall(syscallNr, dirfd, path, &how, sizeof(how));
        if (ret >= 0 || errno != EAGAIN) {
            return ret;
        }
    }
    return -1;
}
#endif

LockFile::LockFile(int dirfd, std::string pathname)
    : d_dirfd(fcntl(dirfd, F_DUPFD_CLOEXEC, 0)),
      d_pathname(std::move(pathname))
{
    d_lockfd = buildboxcommon::FileDescriptor(
        openat(d_dirfd.get(), d_pathname.c_str(), O_CREAT | O_RDWR | O_CLOEXEC,
               PERMISSION_RWUSR));
    if (d_lockfd.get() < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening lock file \"" << d_pathname << "\"");
    }

    if (lockf(d_lockfd.get(), F_LOCK, 0) < 0) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to lock file \"" << d_pathname << "\"");
    }
}

LockFile::~LockFile()
{
    if (d_dirfd.get() >= 0) {
        unlinkat(d_dirfd.get(), d_pathname.c_str(), 0);
    }
}

void FileUtils::writeDigestToFile(const Digest &digest,
                                  const std::string &path)
{
    std::ofstream file(path);
    if (!file.is_open()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error, "Failed to open file for writing: " << path);
    }
    file << digest.hash() << "/" << digest.size_bytes();
    if (!file.good()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Failed to write to file: " << path);
    }
}

} // namespace buildboxcommon
