/* app/builder/KMBuildFinish.cpp
 *
 * Copyright (C) 2024 OpenKylin, Inc
 */

#include "KMBuildFinish.h"

#include <gio/gio.h>
#include <filesystem>

#include "common/KMBuildinsUtils.h"
#include "common/KMContext.h"
#include "common/KMException.h"
#include "common/KMLogger.h"
#include "common/KMInfoJson.h"
#include "common/KMOptions.h"
#include "common/KMUtil.h"
#include "common/kmtranslation.h"

namespace fs = std::filesystem;

class KMBuildFinish::Options : public KMOptions
{
public:
    Options() = default;
    virtual ~Options() = default;

protected:
    void preParseHook() override;
    void postParseHook() override;

private:
    void addOptions();

public:
    // application options
    bool m_help = false;

    // TODO ：下面基本全部废弃，后面确定后再删除
    bool m_noexports = false;
    bool m_noInheritPermissions = false;
    std::string m_command;
    std::string m_requireVersion;
    std::vector<std::string> m_extraDatas;
    std::vector<std::string> m_extensions;
    std::vector<std::string> m_removeExtensions;
    int m_extensionPriority = -1;
    std::vector<std::string> m_metadatas;

    // context options
    std::vector<std::string> m_shares;
    std::vector<std::string> m_unshares;
    std::vector<std::string> m_sockets;
    std::vector<std::string> m_nosockets;
    std::vector<std::string> m_devices;
    std::vector<std::string> m_nodevices;
    std::vector<std::string> m_allows;
    std::vector<std::string> m_disallows;
    std::vector<std::string> m_filesystems;
    std::vector<std::string> m_nofilesystems;
    std::vector<std::string> m_envs;
    std::string m_envFD;
    std::vector<std::string> m_unsetenvs;
    std::vector<std::string> m_ownNames;
    std::vector<std::string> m_talkNames;
    std::vector<std::string> m_notalkNames;
    std::vector<std::string> m_systemOwnNames;
    std::vector<std::string> m_systemTalkNames;
    std::vector<std::string> m_systemNoTalkNames;
    std::vector<std::string> m_addPolicys;
    std::vector<std::string> m_removePolicys;
    std::vector<std::string> m_persists;

    // position options
    std::string m_directory;
};

void KMBuildFinish::Options::preParseHook()
{
    addOptions();
}

void KMBuildFinish::Options::postParseHook()
{
    if (m_help)
    {
        showUsage();
        exit(EXIT_SUCCESS);
    }

    if (m_directory.empty())
    {
        KMError(_("DIRECTORY must be specified"));
        showUsage();
        exit(EXIT_FAILURE);
    }
}

void KMBuildFinish::Options::addOptions()
{
    m_optionsDescription = std::make_unique<opt::options_description>(_("\nUsage:\n \tkaiming build-finish [OPTION…] DIRECTORY - Finalize a build directory "
                                                                        "\nExample:\n \tkaiming build-finish /home/kylin/workspace/qtdemo/build"
                                                                        "\n\nOptions"));

    opt::options_description helpOptions(_("Help Options"));
    helpOptions.add_options()("help,h", opt::bool_switch(&m_help), _("Show help options"));
    // opt::options_description appOptions(_("Application Options"));
    // appOptions.add_options()("command", opt::value<std::string>(&m_command), _("Command to set, --command=COMMAND"))
    //                         ("require-version", opt::value<std::string>(&m_requireVersion), _("Flatpak version to require, like MAJOR.MINOR.MICRO"))
    //                         ("no-exports", _("Don't process exports"))
    //                         ("extra-data", opt::value<std::vector<std::string>>(&m_extraDatas), _("Extra data info"))
    //                         ("extension", opt::value<std::vector<std::string>>(&m_extensions), _("Add extension point info, like NAME=VARIABLE[=VALUE]"))
    //                         ("remove-extension", opt::value<std::vector<std::string>>(&m_removeExtensions), _("Remove extension point info"))
    //                         ("extension-priority", opt::value<int>(&m_extensionPriority), _("Set extension priority (only for extensions)"))
    //                         ("metadata", opt::value<std::vector<std::string>>(&m_metadatas), _("Set generic metadata option, like GROUP=KEY[=VALUE]"))
    //                         ("no-inherit-permissions", _("Don't inherit permissions from runtime"));

    // opt::options_description contextOptions(_("Context Options"));
    // contextOptions.add_options()("share", opt::value<std::vector<std::string>>(&m_shares), _("Share with host, --share=SHARE"))
    //                             ("unshare", opt::value<std::vector<std::string>>(&m_unshares), _("Unshare with host, --unshare=SHARE"))
    //                             ("socket", opt::value<std::vector<std::string>>(&m_sockets), _("Expose socket to app, --socket=SOCKET"))
    //                             ("nosocket", opt::value<std::vector<std::string>>(&m_nosockets), _("Don't expose socket to app, --nosocket=SOCKET"))
    //                             ("device", opt::value<std::vector<std::string>>(&m_devices), _("Expose device to app, --device=DEVICE"))
    //                             ("nodevice", opt::value<std::vector<std::string>>(&m_nodevices), _("Don't expose device to app, --nodevice=DEVICE"))
    //                             ("allow", opt::value<std::vector<std::string>>(&m_allows), _("Allow feature, --allow=FEATURE"))
    //                             ("disallow", opt::value<std::vector<std::string>>(&m_disallows), _("Don't allow feature, -disallow=FEATURE"))
    //                             ("filesystem", opt::value<std::vector<std::string>>(&m_filesystems), _("Expose filesystem to app (:ro for read-only), --filesystem=FILESYSTEM[:ro]"))
    //                             ("nofilesystem", opt::value<std::vector<std::string>>(&m_nofilesystems), _("Don't expose filesystem to app, --nofilesystem=FILESYSTEM"))
    //                             ("env", opt::value<std::vector<std::string>>(&m_envs), _("Set environment variable, --env=VAR=VALUE"))
    //                             ("env-fd", opt::value<std::string>(&m_envFD), _("Read environment variables in env -0 format from FD, --env-fd=FD"))
    //                             ("unset-env", opt::value<std::vector<std::string>>(&m_unsetenvs), _("Remove variable from environment, --unset-env=VAR"))
    //                             ("own-name", opt::value<std::vector<std::string>>(&m_ownNames), _("Allow app to own name on the session bus, --own-name=DBUS_NAME"))
    //                             ("talk-name", opt::value<std::vector<std::string>>(&m_talkNames), _("Allow app to talk to name on the session bus, --talk-name=DBUS_NAME"))
    //                             ("no-talk-name", opt::value<std::vector<std::string>>(&m_notalkNames), _("Don't allow app to talk to name on the session bus, --no-talk-name=DBUS_NAME"))
    //                             ("system-own-name", opt::value<std::vector<std::string>>(&m_systemOwnNames), _("Allow app to own name on the system bus, --system-own-name=DBUS_NAME"))
    //                             ("system-talk-name", opt::value<std::vector<std::string>>(&m_systemTalkNames), _("Allow app to talk to name on the system bus, --system-talk-name=DBUS_NAME"))
    //                             ("system-no-talk-name", opt::value<std::vector<std::string>>(&m_systemNoTalkNames), _("Don't allow app to talk to name on the system bus, --system-no-talk-name=DBUS_NAME"))
    //                             ("add-policy", opt::value<std::vector<std::string>>(&m_addPolicys), _("Add generic policy option, --add-policy=SUBSYSTEM.KEY=VALUE"))
    //                             ("remove-policy", opt::value<std::vector<std::string>>(&m_removePolicys), _("Remove generic policy option, --remove-policy=SUBSYSTEM.KEY=VALUE"))
    //                             ("persist", opt::value<std::vector<std::string>>(&m_persists), _("Persist home directory subpath, --persist=FILENAME"));

    opt::options_description positionalOptions(_("Positional Options"));
    positionalOptions.add_options()("DIRECTORY", opt::value<std::string>(&m_directory), _("The build dir"));

    // m_optionsDescription->add(helpOptions).add(appOptions).add(contextOptions).add(positionalOptions);
    m_optionsDescription->add(helpOptions).add(positionalOptions);

    m_optionsPositionalDescription->add("DIRECTORY", 1);
}

class KMBuildFinish::Private
{
public:
    std::unique_ptr<Options> m_kmOptions;
    std::shared_ptr<KMContext> m_argContext;

    std::string m_baseDir;
    std::string m_filesDir;
    std::string m_exportDir;
    std::string m_metadataFile;

    std::shared_ptr<KMInfoJson> m_infoJson;
    std::string m_id;
};

REGISTER_SUBCOMMAND_DYNCREATE(build-finish, KMBuildFinish)

KMBuildFinish::KMBuildFinish()
    : d(std::make_unique<Private>())
{
    d->m_kmOptions = std::make_unique<Options>();

    enableVfs();
}

KMBuildFinish::~KMBuildFinish() { }

int KMBuildFinish::despose(int argc, char **argv)
{
    KMTrace("KMBuildFinish::despose invoke begin");

    init(argc, argv);

    int ret = run();

    KMTrace("KMBuildFinish::despose invoke end");
    return ret;
}

void KMBuildFinish::init(int argc, char **argv)
{
    KMTrace("KMBuildFinish::init invoke begin");

    d->m_kmOptions->parseCommandLine(argc, argv);
    parseOptionsToContext();

    d->m_baseDir = d->m_kmOptions->m_directory;
    d->m_filesDir = KMUtil::buildFilename(d->m_baseDir, "files");
    d->m_exportDir = KMUtil::buildFilename(d->m_baseDir, "entries");
    d->m_metadataFile = KMUtil::buildFilename(d->m_baseDir, "info.json");

    if (!fs::exists(d->m_filesDir) || !fs::exists(d->m_metadataFile))
    {
        throw KMException(d->m_baseDir + _(" directory not initialized"));
    }

    d->m_infoJson = std::make_shared<KMInfoJson>();
    d->m_infoJson->loadFile(d->m_metadataFile);

    d->m_id = d->m_infoJson->id;
    if (d->m_id.empty())
    {
        throw KMException(_("No name specified in the metadata"));
    }

    KMTrace("KMBuildFinish::init invoke end");
}

void KMBuildFinish::parseOptionsToContext()
{
    KMTrace("KMBuildFinish::parseOptionsToContext invoke begin");

    d->m_argContext = std::make_shared<KMContext>();

    // TODO : 权限设置部分暂时去掉
    // setOptionSharesContext();
    // setOptionSocketsContext();
    // setOptionDevicesContext();
    // setOptionFeaturesContext();
    // setOptionFilesystemContext();
    // setOptionEnvContext();
    // setOptionOwnNameContext();
    // setOptionTalkNameContext();
    // setOptionSystemDbusContext();
    // setOptionGenericPolicyContext();
    // setOptionPersistContext();

    KMTrace("KMBuildFinish::parseOptionsToContext invoke end");
}

void KMBuildFinish::setOptionSharesContext()
{
    for (auto &share : d->m_kmOptions->m_shares)
    {
        if (!d->m_argContext->addShares(share))
        {
            throw KMException(_("Wrong option --share=") + share);
        }
    }

    for (auto &unshare : d->m_kmOptions->m_unshares)
    {
        if (!d->m_argContext->removeShares(unshare))
        {
            throw KMException(_("Wrong option --unshare=") + unshare);
        }
    }
}

void KMBuildFinish::setOptionSocketsContext()
{
    for (auto &socket : d->m_kmOptions->m_sockets)
    {
        if (!d->m_argContext->addSockets(socket))
        {
            throw KMException(_("Wrong option --socket=") + socket);
        }
    }

    for (auto &nosocket : d->m_kmOptions->m_nosockets)
    {
        if (!d->m_argContext->removeSockets(nosocket))
        {
            throw KMException(_("Wrong option --nosocket=") + nosocket);
        }
    }
}

void KMBuildFinish::setOptionDevicesContext()
{
    for (auto &device : d->m_kmOptions->m_devices)
    {
        if (!d->m_argContext->addDevices(device))
        {
            throw KMException(_("Wrong  option --device=") + device);
        }
    }

    for (auto &nodevice : d->m_kmOptions->m_nodevices)
    {
        if (!d->m_argContext->removeDevices(nodevice))
        {
            throw KMException(_("Wrong option --nodevice=") + nodevice);
        }
    }
}

void KMBuildFinish::setOptionFeaturesContext()
{
    for (auto &allow : d->m_kmOptions->m_allows)
    {
        if (!d->m_argContext->addFeatures(allow))
        {
            throw KMException(_("Wrong option --allow=") + allow);
        }
    }

    for (auto &disallow : d->m_kmOptions->m_disallows)
    {
        if (!d->m_argContext->removeFeatures(disallow))
        {
            throw KMException(_("Wrong option --disallow=") + disallow);
        }
    }
}

void KMBuildFinish::setOptionFilesystemContext()
{
    std::string filesystem_out;
    KMFilesystemMode mode = KMFilesystemMode::KAIMING_FILESYSTEM_MODE_NONE;

    for (auto &filesystem : d->m_kmOptions->m_filesystems)
    {
        if (!KMContext::parseFilesystem(filesystem, false, filesystem_out, mode))
        {
            throw KMException(_("Wrong option --filesystem=") + filesystem);
        }

        d->m_argContext->takeFilesystem(filesystem_out, mode);
    }

    for (auto &nofilesystem : d->m_kmOptions->m_nofilesystems)
    {
        if (!KMContext::parseFilesystem(nofilesystem, true, filesystem_out, mode))
        {
            throw KMException(_("Wrong option --nofilesystem=") + nofilesystem);
        }

        d->m_argContext->takeFilesystem(filesystem_out, KAIMING_FILESYSTEM_MODE_NONE);
    }
}

void KMBuildFinish::parseEnvFd()
{
    if (d->m_kmOptions->m_envFD.empty())
    {
        return;
    }

    unsigned long long fd = std::stoull(d->m_kmOptions->m_envFD);
    if (fd > INT_MAX)
    {
        throw KMException(_("Not a valid file descriptor: ") + d->m_kmOptions->m_envFD);
    }

    ssize_t filesize = lseek(fd, 0L, SEEK_END);
    lseek(fd, 0L, SEEK_SET);

    char *buffer = new char[filesize + 1];
    std::shared_ptr<char> data(buffer, [fd](auto p) {
        delete[] p;

        if (fd >= 3)
            close(fd);
    });
    ssize_t bytesRead = read(fd, buffer, filesize);
    if (bytesRead < 0)
    {
        throw KMException(_("Can not read file descriptor: ") + d->m_kmOptions->m_envFD);
    }

    parseEnvBlock(buffer, bytesRead);
}

void KMBuildFinish::parseEnvBlock(const char *data, std::size_t length)
{
    if (data == nullptr)
    {
        return;
    }

    const char *p = data;
    std::size_t remaining = length;

    /* data might not be \0-terminated */
    while (remaining > 0)
    {
        std::size_t len = strnlen(p, remaining);
        assert(len <= remaining);

        std::string item(p);
        std::string::size_type pos = item.find("=");
        if (std::string::npos == pos)
        {
            throw KMException(_("Environment variable must be given in the form VARIABLE=VALUE, not ") + item);
        }

        std::string var = item.substr(0, pos);
        std::string value = item.substr(pos + 1);
        d->m_argContext->addEnvVar(var, value);

        p += len;
        remaining -= len;

        if (remaining > 0)
        {
            assert(*p == '\0');
            p += 1;
            remaining -= 1;
        }
    }
}

void KMBuildFinish::setOptionEnvContext()
{
    parseEnvFd();

    for (const std::string &env : d->m_kmOptions->m_envs)
    {
        std::string::size_type pos = env.find("=");
        if (std::string::npos == pos)
        {
            throw KMException(_("Wrong option --env=") + env);
        }

        std::string var = env.substr(0, pos);
        std::string value = env.substr(pos + 1);
        d->m_argContext->addEnvVar(var, value);
    }

    for (const auto &unenv : d->m_kmOptions->m_unsetenvs)
    {
        if (KMUtil::contains(unenv, "="))
        {
            throw KMException(_("Wrong option --unset-env=") + unenv + _(". Environment variable name must not contain '='"));
        }

        d->m_argContext->addEnvVar(unenv, "");
    }
}

bool KMBuildFinish::verifyDbusName(const std::string &dbusName)
{
    std::string namePart(dbusName);
    if (KMUtil::endsWith(dbusName, ".*"))
    {
        namePart = dbusName.substr(0, dbusName.length() - 2);
    }

    if (g_dbus_is_name(namePart.c_str()) && !g_dbus_is_unique_name(namePart.c_str()))
    {
        return true;
    }

    return false;
}

void KMBuildFinish::setOptionOwnNameContext()
{
    for (auto &ownName : d->m_kmOptions->m_ownNames)
    {
        if (!verifyDbusName(ownName))
        {
            throw KMException(_("Invalid dbus name ") + ownName);
        }

        d->m_argContext->setSessionBusPolicy(ownName, KAIMING_POLICY_OWN);
    }
}

void KMBuildFinish::setOptionTalkNameContext()
{
    for (auto &talkName : d->m_kmOptions->m_talkNames)
    {
        if (!verifyDbusName(talkName))
        {
            throw KMException(_("Invalid dbus name ") + talkName);
        }

        d->m_argContext->setSessionBusPolicy(talkName, KAIMING_POLICY_TALK);
    }

    for (auto &notalkName : d->m_kmOptions->m_notalkNames)
    {
        if (!verifyDbusName(notalkName))
        {
            throw KMException(_("Invalid dbus name ") + notalkName);
        }

        d->m_argContext->setSessionBusPolicy(notalkName, KAIMING_POLICY_NONE);
    }
}

void KMBuildFinish::setOptionSystemDbusContext()
{
    for (auto &ownName : d->m_kmOptions->m_systemOwnNames)
    {
        if (!verifyDbusName(ownName))
        {
            throw KMException(_("Invalid dbus name ") + ownName);
        }

        d->m_argContext->setSystemBusPolicy(ownName, KAIMING_POLICY_OWN);
    }

    for (auto &talkName : d->m_kmOptions->m_systemTalkNames)
    {
        if (!verifyDbusName(talkName))
        {
            throw KMException(_("Invalid dbus name ") + talkName);
        }

        d->m_argContext->setSystemBusPolicy(talkName, KAIMING_POLICY_TALK);
    }

    for (auto &notalkName : d->m_kmOptions->m_systemNoTalkNames)
    {
        if (!verifyDbusName(notalkName))
        {
            throw KMException(_("Invalid dbus name ") + notalkName);
        }

        d->m_argContext->setSystemBusPolicy(notalkName, KAIMING_POLICY_NONE);
    }
}

void KMBuildFinish::verifyPolicy(const std::string &policy, std::string &key, std::string &value)
{
    std::vector<std::string> policyPair = KMUtil::splitString(policy, "=");
    if (policyPair.size() != 2)
    {
        throw KMException(_("--add-policy or --remove-policy arguments must be in the form SUBSYSTEM.KEY=VALUE"));
    }

    key = policyPair.at(0);
    std::vector<std::string> keyPair = KMUtil::splitString(key, ".");
    if (keyPair.size() != 2)
    {
        throw KMException(_("--add-policy or --remove-policy arguments must be in the form SUBSYSTEM.KEY=VALUE"));
    }

    value = policyPair.at(1);
    if (KMUtil::startsWith(value, "!"))
    {
        throw KMException(_("--add-policy or --remove-policy values can't start with \"!\""));
    }
}

void KMBuildFinish::setOptionGenericPolicyContext()
{
    for (auto &policy : d->m_kmOptions->m_addPolicys)
    {
        std::string key, value;
        verifyPolicy(policy, key, value);
        d->m_argContext->applyGenericPolicy(key, value);
    }

    for (auto &rmpolicy : d->m_kmOptions->m_removePolicys)
    {
        std::string key, value;
        verifyPolicy(rmpolicy, key, value);
        value = std::string("!") + value;
        d->m_argContext->applyGenericPolicy(key, value);
    }
}

void KMBuildFinish::setOptionPersistContext()
{
    for (auto &persist : d->m_kmOptions->m_persists)
    {
        d->m_argContext->addPersistent(persist, 1);
    }
}

void KMBuildFinish::collectExports()
{
    KMTrace("KMBuildFinish::collectExports invoke begin");

    if (!KMUtil::mkpath(d->m_exportDir))
    {
        throw KMException(_("Failed to create export dir"));
    }

    if (d->m_kmOptions->m_noexports)
    {
        KMTrace("KMBuildFinish::collectExports invoke end : noexports");
        return;
    }

    std::vector<std::string> paths = {
        "usr/share/applications",                 /* Copy desktop files */
        "usr/share/mime/packages",                /* Copy MIME Type files */
        "usr/share/icons",                        /* Icons */
        "usr/share/dbus-1/services",              /* D-Bus service files */
        "usr/share/gnome-shell/search-providers", /* Search providers */
        "usr/share/appdata",                      /* Copy appdata/metainfo files (legacy path) */
        "usr/share/metainfo",                     /* Copy appdata/metainfo files */
        "usr/lib/systemd",                        /* .service, .target, .timer, .socket, .conf */
        "etc/systemd",                            /* .service, .target, .timer, .socket, .conf */
    };

    std::string errMsg;
    for (const std::string &path : paths)
    {
        std::set<std::string> allowed_extensions;
        std::set<std::string> allowed_prefixes;
        bool require_exact_match;
        if (!getAllowedExports(path, allowed_extensions, allowed_prefixes, require_exact_match))
        {
            KMInfo("Unexpectedly not allowed to export " + path);
            continue;
        }

        std::string src = KMUtil::buildFilename(d->m_filesDir, path);
        if (fs::exists(src))
        {
            KMDebug("Exporting from " + path);

            std::string dest;
            if (path == "usr/share/appdata")
            {
                dest = KMUtil::buildFilename(d->m_exportDir, "usr/share/metainfo");
            }
            else
            {
                dest = KMUtil::buildFilename(d->m_exportDir, path);
            }

            std::string parentDest = fs::path(dest).parent_path().string();
            KMDebug("Ensuring export/" + path + " parent exists");
            if (!KMUtil::mkpath(parentDest))
            {
                throw KMException(_("Failed to create dir : ") + parentDest);
            }

            KMDebug("Copying from files/" + path);
            if (!copyExports(src, dest, path, allowed_prefixes, allowed_extensions, require_exact_match, errMsg))
            {
                throw KMException(errMsg);
            }
        }
    }

    KMTrace("KMBuildFinish::collectExports invoke end");
}

bool KMBuildFinish::copyExports(const std::string &srcDir, const std::string &destDir, const std::string &srcPrefix, const std::set<std::string> &allowedPrefixes,
                                const std::set<std::string> &allowedExtensions, bool requireExactMatch, std::string &errMsg)
{
    KMTrace("KMBuildFinish::copyExports invoke begin");

    if (!KMUtil::mkpath(destDir))
    {
        errMsg = _("Failed to create path :") + destDir;
        return false;
    }

    if (!exportDir("", srcDir, srcPrefix, "", destDir, allowedPrefixes, allowedExtensions, requireExactMatch, errMsg))
    {
        return false;
    }

    KMTrace("KMBuildFinish::copyExports invoke end");
    return true;
}

bool KMBuildFinish::exportDir(const std::string &srcDir, const std::string &srcName, const std::string &srcRelPath, const std::string &destDir, const std::string &destName,
                              const std::set<std::string> &allowedPrefixes, const std::set<std::string> &allowedExtensions, bool requireExactMatch, std::string &errMsg)
{
    std::string dest = KMUtil::buildFilename(destDir, destName);

    if (!KMUtil::mkpath(dest, 0755))
    {
        errMsg = _("Failed to create path : ") + dest;
        return false;
    }

    std::string src = KMUtil::buildFilename(srcDir, srcName);
    fs::path srcPath(src);
    if (!fs::exists(srcPath))
    {
        return true;
    }

    for (auto const &entry : fs::directory_iterator{ srcPath })
    {
        fs::path filepath = entry.path();
        std::string filename = filepath.filename();

        /* Don't export any hidden files or backups */
        if (KMUtil::startsWith(filename, ".") || KMUtil::endsWith(filename, "~"))
        {
            continue;
        }

        if (fs::is_directory(filepath))
        {
            std::string childRelPath = KMUtil::buildFilename(srcRelPath, filename);
            if (!exportDir(src, filename, childRelPath, dest, filename, allowedPrefixes, allowedExtensions, requireExactMatch, errMsg))
            {
                return false;
            }
        }
        else if (fs::is_regular_file(filepath))
        {
            std::string sourcePrintable = KMUtil::buildFilename(srcRelPath, filename);

            std::string extension;
            for (const std::string &allowedExtension : allowedExtensions)
            {
                if (KMUtil::endsWith(filename, allowedExtension))
                {
                    extension = allowedExtension;
                    break;
                }
            }

            if (extension.empty())
            {
                errMsg = _("Wrong extension, not exporting ") + sourcePrintable;
                continue;
            }

            std::string nameWithoutExtension = filepath.stem().filename();
            if (!nameMatchesOneWildcardPrefix(nameWithoutExtension, allowedPrefixes, requireExactMatch))
            {
                errMsg = _("Non-allowed export filename, not exporting ") + sourcePrintable;
                continue;
            }

            KMDebug(_("Exporting ") + sourcePrintable);

            std::string from = KMUtil::buildFilename(src, filename);
            if (!KMUtil::copyAll(from, dest))
            {
                errMsg = _("Failed to copy file : ") + from;
                return false;
            }
        }
        else
        {
            std::string sourcePrintable = KMUtil::buildFilename(srcRelPath, filename);
            KMDebug("Not exporting non-regular file " + sourcePrintable);
        }
    }

    // 如果拷贝的是空目录，则移除；不导出空目录
    if (KMUtil::isEmptyDirectory(dest))
    {
        KMUtil::removeAll(dest);
    }

    return true;
}

bool KMBuildFinish::nameMatchesOneWildcardPrefix(const std::string &name, const std::set<std::string> &allowedPrefixes, bool requireExactMatch)
{
    std::size_t longestMatchLen = 0;
    const char *start = name.c_str();

    /* Find longest valid match */
    for (const std::string &allowedPrefixe : allowedPrefixes)
    {
        std::size_t prefixLen = allowedPrefixe.length();
        bool hasWildcard = false;

        if (KMUtil::endsWith(allowedPrefixe, ".*"))
        {
            hasWildcard = true;
            prefixLen -= 2;
        }

        if (strncmp(start, allowedPrefixe.c_str(), prefixLen) != 0)
        {
            continue;
        }

        const char *endOfMatch = start + prefixLen;

        if (hasWildcard && endOfMatch[0] == '.' && KMBuildinsUtils::isValidInitialNameCharacter(endOfMatch[1], true))
        {
            endOfMatch += 2;
            while (*endOfMatch != 0 &&
                   (KMBuildinsUtils::isValidNameCharacter(*endOfMatch, true) || (endOfMatch[0] == '.' && KMBuildinsUtils::isValidInitialNameCharacter(endOfMatch[1], true))))
            {
                endOfMatch++;
            }
        }

        std::size_t matchLen = endOfMatch - start;
        if (matchLen > longestMatchLen)
        {
            longestMatchLen = matchLen;
        }
    }

    if (longestMatchLen == 0)
    {
        return false;
    }

    if (requireExactMatch)
    {
        return longestMatchLen == name.length();
    }

    /* non-exact matches can be exact, or can be followed by characters that would make
     * not be part of the last element in the matched prefix, due to being invalid or
     * a new element. As a special case we explicitly disallow dash here, even though
     * it iss typically allowed in the final element of a name, this allows you too sloppily
     * match org.the.App with org.the.App-symbolic[.png] or org.the.App-settings[.desktop].
     */
    const char *remainder = start + longestMatchLen;
    return *remainder == 0 || *remainder == '.' || !KMBuildinsUtils::isValidNameCharacter(*remainder, false);
}

bool KMBuildFinish::getAllowedExports(const std::string &path, std::set<std::string> &allowedExtensions, std::set<std::string> &allowedPrefixes, bool &requireExactMatch)
{
    requireExactMatch = false;

    allowedPrefixes.insert(d->m_id + ".*");

    if (path == "usr/share/applications")
    {
        allowedExtensions.insert(".desktop");
    }
    else if (KMUtil::startsWith(path, "usr/share/icons"))
    {
        allowedExtensions.insert(".svgz");
        allowedExtensions.insert(".png");
        allowedExtensions.insert(".svg");
        allowedExtensions.insert(".ico");
    }
    else if (path == "usr/share/dbus-1/services")
    {
        allowedExtensions.insert(".service");

        std::vector<std::string> owned_dbus_names = d->m_argContext->getSessionBusPolicyAllowedOwnNames();
        for (auto &name : owned_dbus_names)
        {
            allowedPrefixes.insert(name);
        }

        /* We need an exact match with no extra garbage, because the filename refers to busnames and we can *only* match exactly these */
        requireExactMatch = true;
    }
    else if (path == "usr/share/gnome-shell/search-providers")
    {
        allowedExtensions.insert(".ini");
    }
    else if (path == "usr/share/mime/packages")
    {
        allowedExtensions.insert(".xml");
    }
    else if (path == "usr/share/metainfo" || path == "usr/share/appdata")
    {
        allowedExtensions.insert(".xml");
    }
    else if (path == "usr/lib/systemd" || path == "etc/systemd")
    {
        allowedExtensions.insert(".service");
        allowedExtensions.insert(".target");
        allowedExtensions.insert(".timer");
        allowedExtensions.insert(".socket");
        allowedExtensions.insert(".conf");
    }
    else
    {
        return false;
    }

    return true;
}

int KMBuildFinish::run()
{
    KMTrace("KMBuildFinish::run invoke begin");

    if (fs::exists(d->m_exportDir))
        throw KMException(_("Build directory already finalized : ") + d->m_kmOptions->m_directory);

    KMDebug("Collecting exports");
    collectExports();

    KMDebug("Updating info.json");
    updateMetadata();
    KMInfo(_("Please review the exported files and the info.json"));

    KMTrace("KMBuildFinish::run invoke end");
    return EXIT_SUCCESS;
}

std::string KMBuildFinish::findCommandInPath(const std::string &binDir)
{
    std::string command = "";
    if (fs::exists(binDir) && fs::is_directory(binDir))
    {
        for (auto const &entry : fs::directory_iterator{ binDir })
        {
            if (!command.empty())
            {
                KMWarn(_("More than one executable found"));
                break;
            }

            command = entry.path().filename();
        }
    }

    return command;
}

void KMBuildFinish::updateMetadata()
{
    KMTrace("KMBuildFinish::updateMetadata invoke begin");

    if (d->m_infoJson->commands.empty() && d->m_infoJson->kind == BASE_TYPE_APP)
    {
        KMDebug("Looking for executables");

        std::string binDir = KMUtil::buildFilename(d->m_baseDir, "files/usr/local/bin");
        std::string command = KMBuildFinish::findCommandInPath(binDir);
        if (command.empty())
        {
            binDir = KMUtil::buildFilename(d->m_baseDir, "files/usr/bin");
            command = KMBuildFinish::findCommandInPath(binDir);
        }
        if (command.empty())
        {
            binDir = KMUtil::buildFilename(d->m_baseDir, "files/bin");
            command = KMBuildFinish::findCommandInPath(binDir);
        }

        if (command.empty())
        {
            throw KMException("No executable found, can't set 'command' into info.json");
        }
        else
        {
            KMInfo(_("Using command : ") + command);
            d->m_infoJson->commands.push_back(command);
        }
    }

    d->m_infoJson->size = KMUtil::getDirectorySize(fs::path(d->m_baseDir));
    d->m_infoJson->saveFile(d->m_metadataFile);

    // 更新更精确
    d->m_infoJson->loadFile(d->m_metadataFile);
    d->m_infoJson->size = KMUtil::getDirectorySize(fs::path(d->m_baseDir));
    d->m_infoJson->saveFile(d->m_metadataFile);

    KMTrace("KMBuildFinish::updateMetadata invoke end");
}
