/* common/KMExports.cpp
 *
 * Copyright (C) 2024 OpenKylin, Inc
 */

#include "KMExports.h"

#include <glib.h>
#include <sys/vfs.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

#include <filesystem>

#include "KMDirFd.h"
#include "KMFileGuard.h"
#include "KMLogger.h"
#include "KMUtil.h"
#include "kmtranslation.h"

namespace fs = std::filesystem;

/* We don't want to export paths pointing into these, because they are readonly (so we can't create mountpoints there) and don't match what's on the host anyway.
   m_absUsrmergedDirs get the same treatment without having to be listed here. */
std::vector<std::string> KMExports::m_dontExportIn = { "/.kaiming-info", "/app", "/dev", "/etc", "/proc", "/run/flatpak", "/run/host", "/usr" };

std::vector<std::string> KMExports::m_absUsrmergedDirs = { "/bin", "/lib", "/lib32", "/lib64", "/sbin" };

std::vector<LibsNeedEtc> KMExports::m_libsNeedEtc = {
    /* glibc */
    { "ld.so.cache", S_IFREG },
    /* Used for executables and a few libraries on e.g. Debian */
    { "alternatives", S_IFDIR }
};

KMExports::KMExports() { }

KMExports::~KMExports() { }

/* path must be absolute, but this is not checked because assertions are not async-signal-safe. */
int KMExports::openInHostAsyncSignalSafe(const std::string &absPath, int flags)
{
    if (!KMUtil::startsWith(absPath, "/"))
    {
        return false;
    }

    flags |= O_CLOEXEC;

    if (m_hostFd >= 0)
    {
        /* Similar to statInHost, this assumes the mock root doesn't contain symlinks that escape from the mock root. */
        return ::openat(m_hostFd, absPath.substr(1).c_str(), flags);
    }

    return ::openat(AT_FDCWD, absPath.c_str(), flags);
}

/* We use level to avoid infinite recursion.
 *
 * Note that some of the errors produced by this function are "real errors"
 * and should show up as a user-visible warning, but others are relatively
 * uninteresting, and in general none are actually fatal: we prefer to
 * continue with fewer paths exposed rather than failing to run. */
bool KMExports::exportsPathExpose(KMFilesystemMode mode, const std::string &path, int level, std::string &errMsg)
{
    if (!isExportMode(mode))
    {
        return false;
    }

    if (level > 40) /* 40 is the current kernel ELOOP check */
    {
        errMsg = strerror(ELOOP);
        return false;
    }

    if (!fs::path(path).is_absolute())
    {
        errMsg = _("An absolute path is required");
        return false;
    }

    /* Check if it exists at all */
    int openPathFd = openInHostAsyncSignalSafe(path, O_PATH | O_NOFOLLOW);
    KMFileGuard fileGuard(openPathFd);

    if (openPathFd == -1)
    {
        int savedErrno = errno;

        /* Intentionally using G_IO_ERROR_NOT_FOUND even if errno is something different, so callers can suppress the warning in this
         * relatively likely and uninteresting case: we don't particularly care whether this is happening as a result of ENOENT or EACCES
         * or any other reason. */
        errMsg = _("Unable to open path ") + path + " : " + ::strerror(savedErrno);
        return false;
    }

    struct stat st;
    if (::fstat(openPathFd, &st) != 0)
    {
        errMsg = _("Unable to get file type of ") + path + " : " + ::strerror(errno);
        return false;
    }

    /* Don't expose weird things */
    if (!(S_ISDIR(st.st_mode) || S_ISREG(st.st_mode) || S_ISLNK(st.st_mode) || S_ISSOCK(st.st_mode)))
    {
        errMsg = path + _(" has unsupported file type");
        return false;
    }

    /* O_PATH + fstatfs is the magic that we need to statfs without automounting the target */
    struct statfs stfs;
    if (::fstatfs(openPathFd, &stfs) != 0)
    {
        errMsg = _("Unable to get filesystem information for ") + path + " : " + ::strerror(errno);
        return false;
    }

    // TODO : 暂不提供mock单元测试功能
    // if (stfs.f_type == AUTOFS_SUPER_MAGIC ||
    //     (G_UNLIKELY (exports->test_flags & KAIMING_EXPORTS_TEST_FLAGS_AUTOFS) &&
    //     S_ISDIR (st.st_mode)))
    // {
    //     if (!check_if_autofs_works (exports, path))
    //     {
    //         errMsg = _("Ignoring blocking autofs path  ") + path;
    //         return false;
    //     }
    // }

    /* Syntactic canonicalization only, no need to use host_fd */
    std::string canonical = KMUtil::canonicalizeFilename(path);

    for (auto &dont : m_dontExportIn)
    {
        /* Don't expose files in non-mounted dirs like /app or /usr, as they are not the same as on the host, and we generally can't create the parents for them anyway */
        if (KMUtil::startsWith(canonical, dont))
        {
            errMsg = path + _(" is reserved by kaiming");
            return false;
        }

        /* Also don't expose directories that are a parent of a directory that is "owned" by the sandboxing framework. For example, because
         * kaiming controls /run/host and /run/kaiming, we cannot allow --filesystem=/run, which would prevent us from creating the
         * contents of /run/host and /run/kaiming. */
        if (KMUtil::startsWith(dont, canonical))
        {
            errMsg = path + _(" is reserved by kaiming");
            return false;
        }
    }

    for (auto &bsUsrmergedDir : m_absUsrmergedDirs)
    {
        /* Same as /usr, but for the directories that get merged into /usr. Keep the translatable string here the same as the one above */
        if (KMUtil::startsWith(canonical, bsUsrmergedDir))
        {
            errMsg = bsUsrmergedDir + _(" is reserved by kaiming");
            return false;
        }
    }

    /* Handle any symlinks prior to the target itself. This includes path itself, because we expose the target of the symlink. */
    const char *slash = canonical.c_str();
    const char *start = slash;
    std::string tmpPath = canonical;
    do
    {
        slash = strchr(slash + 1, '/');
        if (slash)
        {
            tmpPath = canonical.substr(0, slash - start);
        }

        if (!fs::is_symlink(tmpPath))
        {
            KMDebug(tmpPath + " is not a symlink");
        }
        else if (neverExportAsSymlink(tmpPath))
        {
            KMDebug(tmpPath + " is a symlink, but we avoid exporting it as such");
        }
        else
        {
            std::string resolved = resolveLinkInHost(tmpPath);

            if (resolved.empty())
            {
                errMsg = _("Unable to resolve symbolic link: ") + tmpPath;
                return false;
            }
            else
            {
                KMDebug(tmpPath + " is a symlink, resolved to " + resolved);

                std::string newTarget;
                if (slash)
                    newTarget = KMUtil::buildFilename(resolved, slash + 1);
                else
                    newTarget = resolved;

                KMDebug("Trying to export the target instead: " + newTarget);

                if (exportsPathExpose(mode, newTarget, level + 1, errMsg))
                {
                    doExportPath(tmpPath, FAKE_MODE_SYMLINK);
                    return true;
                }

                KMDebug("Could not export target " + newTarget + ", so ignoring " + tmpPath);
                return false;
            }
        }

    } while (slash != nullptr);

    doExportPath(path, mode);

    return true;
}

bool KMExports::isExportMode(int mode)
{
    return ((mode >= KAIMING_FILESYSTEM_MODE_NONE && mode <= KAIMING_FILESYSTEM_MODE_LAST) || mode == FAKE_MODE_DIR || mode == FAKE_MODE_SYMLINK);
}

bool KMExports::neverExportAsSymlink(const std::string &path)
{
    /* Don't export {/var,}/tmp as a symlink even if it is on the host, because that will fail with the pre-existing directory we created for it,
       and anyway, it being a symlink is not useful in the sandbox */
    if (path == "/tmp" || path == "/var/tmp")
    {
        return true;
    }

    return false;
}

std::string KMExports::resolveLinkInHost(const std::string &absPath) const
{
    if (absPath.at(0) != '/')
    {
        return "";
    }

    if (m_hostFd > 0)
    {
        std::string fdPath = "/proc/self/fd/" + std::to_string(m_hostFd) + "/";
        std::string realPath = fdPath + absPath.substr(1);
        std::string resolved = KMUtil::realpath(realPath);

        if (resolved.empty())
        {
            return "";
        }

        if (!KMUtil::startsWith(resolved, fdPath))
        {
            KMError("Symbolic link escapes from mock root");
            return "";
        }

        return resolved.substr(fdPath.length() - 1);
    }

    return KMUtil::realpath(absPath);
}

void KMExports::doExportPath(const std::string &path, unsigned int mode)
{
    if (!isExportMode(mode))
    {
        return;
    }

    ExportedPath ep;
    ep.path = path;

    if (m_hash.end() != m_hash.find(path))
    {
        ExportedPath oldExportPath = m_hash[path];

        if (oldExportPath.mode < mode)
        {
            ep.mode = mode;
        }
        else
        {
            ep.mode = oldExportPath.mode;
        }
    }
    else
    {
        ep.mode = mode;
    }

    m_hash[path] = ep;
}

void KMExports::addHostOsExpose(KMFilesystemMode mode)
{
    if (mode <= KAIMING_FILESYSTEM_MODE_NONE || mode > KAIMING_FILESYSTEM_MODE_LAST)
    {
        return;
    }

    m_hostOs = mode;
}

void KMExports::addHostEtcExpose(KMFilesystemMode mode)
{
    if (mode <= KAIMING_FILESYSTEM_MODE_NONE || mode > KAIMING_FILESYSTEM_MODE_LAST)
    {
        return;
    }

    m_hostEtc = mode;
}

bool KMExports::addPathTmpfs(const std::string &path, std::string &errMsg)
{
    return exportsPathExpose((KMFilesystemMode)FAKE_MODE_TMPFS, path, 0, errMsg);
}

bool KMExports::addPathDir(const std::string &path, std::string &errMsg)
{
    return exportsPathExpose((KMFilesystemMode)FAKE_MODE_DIR, path, 0, errMsg);
}

bool KMExports::addPathExpose(KMFilesystemMode mode, const std::string &path, std::string &errMsg)
{
    if (mode <= KAIMING_FILESYSTEM_MODE_NONE || mode > KAIMING_FILESYSTEM_MODE_LAST)
    {
        return false;
    }

    return exportsPathExpose(mode, path, 0, errMsg);
}

bool KMExports::addPathExposeOrHide(KMFilesystemMode mode, const std::string &path, std::string &errMsg)
{
    if (mode < KAIMING_FILESYSTEM_MODE_NONE || mode > KAIMING_FILESYSTEM_MODE_LAST)
    {
        return false;
    }

    if (mode == KAIMING_FILESYSTEM_MODE_NONE)
    {
        return addPathTmpfs(path, errMsg);
    }
    else
    {
        return addPathExpose(mode, path, errMsg);
    }
}

bool KMExports::pathIsVisible(const std::string &path) const
{
    return pathGetMode(path) > KAIMING_FILESYSTEM_MODE_NONE;
}

bool KMExports::pathParentIsMapped(const std::vector<std::string> &keys, const std::string &path) const
{
    bool isMapped = false;

    /* The keys are sorted so shorter (i.e. parents) are first */
    for (auto &key : keys)
    {
        const std::string &mountedPath = key;
        const ExportedPath &ep = m_hash.at(key);
        assert(isExportMode(ep.mode));

        if (KMUtil::startsWith(path, mountedPath) && path != mountedPath)
        {
            /* FAKE_MODE_DIR has same mapped value as parent */
            if (ep.mode == FAKE_MODE_DIR)
            {
                continue;
            }

            isMapped = ep.mode != FAKE_MODE_TMPFS;
        }
    }

    return isMapped;
}

bool KMExports::pathIsMapped(const std::vector<std::string> &keys, const std::string &path, bool &isReadonlyOut) const
{
    bool isMapped = false;
    bool isReadonly = false;

    /* The keys are sorted so shorter (i.e. parents) are first */
    for (auto &key : keys)
    {
        const std::string &mountedPath = key;
        const ExportedPath &ep = m_hash.at(key);
        assert(isExportMode(ep.mode));

        if (KMUtil::startsWith(path, mountedPath))
        {
            /* FAKE_MODE_DIR has same mapped value as parent */
            if (ep.mode == FAKE_MODE_DIR)
            {
                continue;
            }

            if (ep.mode == FAKE_MODE_SYMLINK)
            {
                isMapped = path == mountedPath;
            }
            else
            {
                isMapped = ep.mode != FAKE_MODE_TMPFS;
            }

            if (isMapped)
            {
                isReadonly = ep.mode == KAIMING_FILESYSTEM_MODE_READ_ONLY;
            }
            else
            {
                isReadonly = false;
            }
        }
    }

    isReadonlyOut = isReadonly;
    return isMapped;
}

bool KMExports::statInHost(const std::string &absPath, struct stat *buf, int flags) const
{
    if (absPath.at(0) != '/')
    {
        return false;
    }

    if (m_hostFd >= 0)
    {
        /* If abs_path is "/usr", then stat "usr" relative to host_fd. As a special case, if abs_path is "/", stat host_fd itself, due to the use of AT_EMPTY_PATH.
         *
         * This won't work if ${host_fd}/${abs_path} contains symlinks that are absolute or otherwise escape from the mock root, so be careful not to do that in unit tests.
         */
        return KMDirFd::fstatat(m_hostFd, absPath.substr(1), buf, AT_EMPTY_PATH | flags);
    }

    return KMDirFd::fstatat(AT_FDCWD, absPath, buf, flags);
}

bool KMExports::pathIsDir(const std::string &absPath) const
{
    struct stat s;
    if (!statInHost(absPath, &s, AT_SYMLINK_NOFOLLOW))
    {
        return false;
    }

    return S_ISDIR(s.st_mode);
}

KMFilesystemMode KMExports::pathGetMode(const std::string &path) const
{
    std::vector<std::string> keys;
    for (auto &pair : m_hash)
    {
        keys.push_back(pair.first);
    }
    std::sort(keys.begin(), keys.end());

    /* Syntactic canonicalization only, no need to use host_fd */
    std::string canonical = KMUtil::canonicalizeFilename(path);

    std::vector<std::string> parts = KMUtil::splitString(canonical.substr(1), "/");

    /* A path is visible in the sandbox if no parent path element that is mapped in the sandbox is a symlink, and the final element is mapped.
     * If any parent is a symlink we resolve that and continue with that instead. */
    std::string pathBuilder;
    bool isReadonly = false;
    size_t size = parts.size();
    int count = 0;
    for (auto &part : parts)
    {
        ++count;
        pathBuilder.append("/");
        pathBuilder.append(part);

        if (pathIsMapped(keys, pathBuilder, isReadonly))
        {
            struct stat st;
            if (!statInHost(pathBuilder, &st, AT_SYMLINK_NOFOLLOW))
            {
                if (count == size && !isReadonly)
                {
                    /* Last element was mapped but isn't there, this is OK (used for the save case) if we the parent is mapped and writable, as the app can then create the file here. */
                    break;
                }

                return KAIMING_FILESYSTEM_MODE_NONE;
            }

            if (S_ISLNK(st.st_mode))
            {
                std::string resolved = resolveLinkInHost(pathBuilder);

                if (resolved.empty())
                {
                    return KAIMING_FILESYSTEM_MODE_NONE;
                }

                std::string path2_builder = resolved;
                for (int i = count; i < size; ++i)
                {
                    path2_builder.append("/");
                    path2_builder.append(parts.at(i));
                }

                return pathGetMode(path2_builder);
            }
        }
        else if (count == size)
        {
            return KAIMING_FILESYSTEM_MODE_NONE; /* Last part was not mapped */
        }
    }

    if (isReadonly)
    {
        return KAIMING_FILESYSTEM_MODE_READ_ONLY;
    }

    return KAIMING_FILESYSTEM_MODE_READ_WRITE;
}

std::string KMExports::readlinkInHost(const std::string &absPath) const
{
    if (absPath.at(0) != '/')
    {
        return "";
    }

    /* Similar to statInHost, this assumes the mock root doesn't contain symlinks that escape from the mock root. */
    if (m_hostFd >= 0)
    {
        return KMDirFd::readlinkat(m_hostFd, absPath.substr(1));
    }

    return KMDirFd::readlinkat(AT_FDCWD, absPath);
}
