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

#include "KMBuildExport.h"

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

#include "config.h"
#include "common/KMBuildinsUtils.h"
#include "common/KMException.h"
#include "common/KMInputStream.h"
#include "common/KMLogger.h"
#include "common/KMMetaData.h"
#include "common/KMOptions.h"
#include "common/KMOstreeCore.h"
#include "common/KMOstreeRepoCommitModifier.h"
#include "common/KMUtil.h"
#include "common/KMVariantDict.h"
#include "common/kmtranslation.h"
#include "common/parsedatetime.h"

namespace fs = std::filesystem;

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

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

private:
    void addOptions();
    void checkOptions(const opt::variables_map &varsMap);

public:
    // application options
    bool m_help = false;
    bool m_isRuntime = false;
    bool m_noUpdateSummary = false;
    bool m_disableFsync = false;
    bool m_disableSandbox = false;
    bool m_noSummaryIndex = false;
    std::string m_subject;
    std::string m_body;
    std::string m_arch;
    std::string m_files;
    std::string m_metadata;
    std::vector<std::string> m_gpgKeyIds;
    std::vector<std::string> m_excludes;
    std::vector<std::string> m_includes;
    std::string m_gpgHomedir;
    std::vector<std::string> m_subsets;
    std::string m_endoflife;
    std::string m_endoflifeRebase;
    int m_tokenType = -1;
    std::string m_timestamp;
    std::string m_collectionId;

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

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

void KMBuildExport::Options::postParseHook()
{
    checkOptions(m_varsMap);

    if (m_help)
    {
        showUsage();
        exit(EXIT_SUCCESS);
    }

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

    if (m_branch.empty())
    {
        m_branch = KMUtil::kmGetDefaultChannel();
    }
}

void KMBuildExport::Options::addOptions()
{
    m_optionsDescription = std::make_unique<opt::options_description>(_("\nUsage:\n \tkaiming build-export [OPTION…] LOCATION DIRECTORY [BRANCH] - Create a repository from a build directory "
                                                                        "\n\nOptions"));

    opt::options_description helpOptions(_("Help Options"));
    helpOptions.add_options()("help,h", _("Show help options"));
    opt::options_description appOptions(_("Application Options"));
    appOptions.add_options()("subject", opt::value<std::string>(&m_subject), _("One line subject, --subject=SUBJECT"))
                            ("body,b", opt::value<std::string>(&m_body), _("Full description, --body=BODY"))
                            ("arch", opt::value<std::string>(&m_arch), _("Architecture to export for (must be host compatible)"))
                            ("runtime,r", _("Commit runtime (/usr), not /app"))
                            // ("update-appstream", _("Update the appstream branch"))
                            ("no-update-summary", _("Don't update the summary"))
                            ("files", opt::value<std::string>(&m_files), _("Use alternative directory for the files, --files=SUBDIR"))
                            ("metadata", opt::value<std::string>(&m_metadata), _("Use alternative file for the metadata, --metadata=FILE"))
                            ("gpg-sign", opt::value<std::vector<std::string>>(&m_gpgKeyIds), _("GPG Key ID to sign the commit with, --gpg-sign=KEY-ID"))
                            ("exclude", opt::value<std::vector<std::string>>(&m_excludes), _("Files to exclude, --exclude=PATTERN"))
                            ("include", opt::value<std::vector<std::string>>(&m_includes), _("Excluded files to include, --include=PATTERN"))
                            ("gpg-homedir", opt::value<std::string>(&m_gpgHomedir), _("GPG Homedir to use when looking for keyrings, --gpg-homedir=HOMEDIR"))
                            ("subset", opt::value<std::vector<std::string>>(&m_subsets), _("Add to a named subset"))
                            ("end-of-life", opt::value<std::string>(&m_endoflife), _("Mark build as end-of-life, --end-of-life=REASON"))
                            ("end-of-life-rebase", opt::value<std::string>(&m_endoflifeRebase), _("Mark build as end-of-life, to be replaced with the given ID, --end-of-life-rebase=ID"))
                            ("token-type", opt::value<int>(&m_tokenType), _("Set type of token needed to install this commit, --token-type=VAL"))
                            ("timestamp", opt::value<std::string>(&m_timestamp), _("Override the timestamp of the commit"))
                            ("collection-id", opt::value<std::string>(&m_collectionId), _("Collection ID"))
                            ("disable-fsync", _("Do not invoke fsync()"))
                            ("disable-sandbox", _("Do not sandbox icon validator"))
                            ("no-summary-index", _("Don't generate a summary index"))
                            ;

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

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

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

void KMBuildExport::Options::checkOptions(const opt::variables_map &varsMap)
{
    checkOption(varsMap, "help", "h", m_help);
    checkOption(varsMap, "runtime", "r", m_isRuntime);
    checkOption(varsMap, "no-update-summary", nullptr, m_noUpdateSummary);
    checkOption(varsMap, "disable-fsync", nullptr, m_disableFsync);
    checkOption(varsMap, "disable-sandbox", nullptr, m_disableSandbox);
    checkOption(varsMap, "no-summary-index", nullptr, m_noSummaryIndex);
}

class KMBuildExport::Private
{
public:
    std::unique_ptr<Options> m_kmOptions;
    std::string m_baseDir;
    std::string m_filesDir;
    std::string m_usrDir;
    std::string m_metadataFile;
    std::string m_exportDir;

    std::shared_ptr<KMMetaData> m_kmmetadata;
    std::string m_id;
    std::string m_arch;
    bool m_isRuntime = false;
    bool m_isExtension = false;

    KMVariantDict m_dict;
    std::shared_ptr<KMOstreeRepo> m_repo;
    std::shared_ptr<KMOstreeMutableTree> m_mutableTree;
};

REGISTER_SUBCOMMAND_DYNCREATE(build-export, KMBuildExport)

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

    enableVfs();
}

KMBuildExport::~KMBuildExport() { }

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

    init(argc, argv);

    int ret = run();

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

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

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

    std::string errMsg;
    if (!d->m_kmOptions->m_collectionId.empty() && KMOstreeCore::validateCollectionId(d->m_kmOptions->m_collectionId, errMsg))
    {
        throw KMException(d->m_kmOptions->m_collectionId + _(" is not a valid collection ID: ") + errMsg);
    }

    if (!KMBuildinsUtils::isValidBranch(d->m_kmOptions->m_branch, errMsg))
    {
        throw KMException(d->m_kmOptions->m_branch + _(" is not a valid branch name: ") + errMsg);
    }

    d->m_baseDir = d->m_kmOptions->m_directory;
    if (!d->m_kmOptions->m_files.empty())
    {
        d->m_filesDir = KMUtil::buildFilename(d->m_baseDir, d->m_kmOptions->m_files);
        d->m_usrDir = d->m_filesDir;
    }
    else
    {
        d->m_filesDir = KMUtil::buildFilename(d->m_baseDir, "files");
        d->m_usrDir = KMUtil::buildFilename(d->m_baseDir, "usr");
    }

    if (!d->m_kmOptions->m_metadata.empty())
    {
        d->m_metadataFile = KMUtil::buildFilename(d->m_baseDir, d->m_kmOptions->m_metadata);
    }
    else
    {
        d->m_metadataFile = KMUtil::buildFilename(d->m_baseDir, "metadata");
    }

    d->m_exportDir = KMUtil::buildFilename(d->m_baseDir, "export");

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

    d->m_kmmetadata = std::make_shared<KMMetaData>();
    if (!d->m_kmmetadata->load(d->m_metadataFile))
    {
        throw KMException(_("Cann't load metadata ") + d->m_metadataFile);
    }

    d->m_id = d->m_kmmetadata->getStringValue(METADATA_GROUP_APPLICATION, METADATA_KEY_NAME);
    if (d->m_id.empty())
    {
        d->m_id = d->m_kmmetadata->getStringValue(METADATA_GROUP_RUNTIME, METADATA_KEY_NAME);
        if (d->m_id.empty())
        {
            throw KMException(_("No name specified in the metadata"));
        }

        d->m_isRuntime = true;

        if (d->m_kmmetadata->groupExists(METADATA_GROUP_EXTENSION_OF))
        {
            d->m_isExtension = true;
        }
    }

    if (!(d->m_kmOptions->m_isRuntime || d->m_isRuntime) && !fs::exists(d->m_exportDir))
    {
        throw KMException(d->m_baseDir + _(" directory not finalized, use kaiming build-finish"));
    }

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

typedef struct
{
    std::vector<std::string> excludes;
    std::vector<std::string> includes;
} FilterData;

static bool matches_patterns(const std::vector<std::string> &patterns, const char *path)
{
    if (patterns.empty())
    {
        return false;
    }

    for (const std::string &pattern : patterns)
    {
        if (KMBuildinsUtils::pathMatchPrefix(pattern.c_str(), path) != nullptr)
        {
            return true;
        }
    }

    return false;
}

static OstreeRepoCommitFilterResult commitFilter(OstreeRepo *repo, const char *path, GFileInfo *fileInfo, FilterData *filterData)
{
    guint mode;

    g_file_info_set_attribute_uint32(fileInfo, "unix::uid", 0);
    g_file_info_set_attribute_uint32(fileInfo, "unix::gid", 0);

    /*
        在开明中，没有真正的理由让文件根据组或用户有不同的权限。实际上，所有的东西都是只读的。
        除了用户之外，任何人都可以写东西，这只会给系统安装案例带来风险。因此，我们将模式规范化为只能由用户写入，
        所有人都可读，并且对于用户可以执行的目录和文件，所有人都可执行。
    */
    mode = g_file_info_get_attribute_uint32(fileInfo, "unix::mode");
    if (g_file_info_get_file_type(fileInfo) == G_FILE_TYPE_DIRECTORY)
    {
        mode = 0755 | S_IFDIR;
    }
    else if (g_file_info_get_file_type(fileInfo) == G_FILE_TYPE_REGULAR)
    {
        /* 如果使用可以执行，则使所有人都可以执行 */
        if (mode & S_IXUSR)
        {
            mode = 0755 | S_IFREG;
        }
        else /* 否则无可执行 */
        {
            mode = 0644 | S_IFREG;
        }
    }
    g_file_info_set_attribute_uint32(fileInfo, "unix::mode", mode);

    if (matches_patterns(filterData->excludes, path) && !matches_patterns(filterData->includes, path))
    {
        KMDebug(std::string("Excluding ") + path);
        return OSTREE_REPO_COMMIT_FILTER_SKIP;
    }

    return OSTREE_REPO_COMMIT_FILTER_ALLOW;
}

static bool getSubsets(std::vector<std::string> &subsets, KMVariant &out)
{
    if (subsets.empty())
    {
        return false;
    }

    KMVariantBuilder builder("as");
    bool found = false;
    for (const std::string &subset : subsets)
    {
        if (subset.empty())
        {
            continue;
        }

        found = true;
        builder.add("s", subset);
    }

    if (!found)
    {
        return false;
    }

    out = builder.end();

    return true;
}

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

    collectExtraData();

    if (!(d->m_kmOptions->m_isRuntime || d->m_isRuntime))
    {
        validateExports();
    }

    metadataGetArch();

    std::string subject;
    if (d->m_kmOptions->m_subject.empty())
    {
        subject = "Export " + d->m_id;
    }
    else
    {
        subject = d->m_kmOptions->m_subject;
    }

    std::string body;
    if (d->m_kmOptions->m_body.empty())
    {
        body = "Name: " + d->m_id + "\n"
             + "Arch: " + d->m_arch + "\n"
             + "Branch: " + d->m_kmOptions->m_branch + "\n"
             + "Built with: " + PACKAGE_STRING + "\n";
    }
    else
    {
        body = d->m_kmOptions->m_body;
    }

    std::string fullBranchString = ((d->m_kmOptions->m_isRuntime || d->m_isRuntime) ? "runtime/" : "app/") + d->m_id + "/" + d->m_arch + "/" + d->m_kmOptions->m_branch;

    std::string repofile = d->m_kmOptions->m_location;
    d->m_repo = std::make_shared<KMOstreeRepo>(repofile);

    std::string parent;
    std::string errMsg;
    if (fs::exists(repofile) && !KMUtil::isEmptyDirectory(repofile))
    {
        if (!d->m_repo->open())
        {
            throw KMException(d->m_repo->errorMessage());
        }

        std::string repo_collection_id = d->m_repo->getCollectionId();
        if (!KMBuildinsUtils::repoResolveRev(d->m_repo.get(), repo_collection_id, "", fullBranchString, true, parent, errMsg))
        {
            throw KMException(errMsg);
        }

        if (!d->m_kmOptions->m_collectionId.empty() && repo_collection_id != d->m_kmOptions->m_collectionId)
        {
            throw KMException("Specified collection ID ‘" + d->m_kmOptions->m_collectionId + "’ doesn’t match collection ID in repository configuration ‘" + repo_collection_id + "’.");
        }
    }
    else
    {
        if (!d->m_kmOptions->m_collectionId.empty() && !d->m_repo->setCollectionId(d->m_kmOptions->m_collectionId))
        {
            throw KMException(d->m_repo->errorMessage());
        }

        if (!d->m_repo->create(OSTREE_REPO_MODE_ARCHIVE_Z2))
        {
            throw KMException(d->m_repo->errorMessage());
        }
    }

    /* 获取我们将用于提交的规范集合ID。如果现有的repo没有，并且在命令行上没有指定，则这可能为NULL。 */
    std::string collectionId = d->m_repo->getCollectionId();

    if (!d->m_repo->prepareTransaction(nullptr))
    {
        throw KMException(d->m_repo->errorMessage());
    }

    /*  只有当目标是“裸”rep时，这才有用，但在提交到缓存repo时，这种情况会发生在ak生成器中。对于其他repo，这是一个禁止操作 */
    if (!d->m_repo->scanHardlinks())
    {
        throw KMException(d->m_repo->errorMessage());
    }

    d->m_mutableTree = std::make_shared<KMOstreeMutableTree>();
    if (!KMBuildinsUtils::mtreeEnsureDirMetadata(d->m_repo.get(), d->m_mutableTree.get(), errMsg))
    {
        throw KMException(errMsg);
    }

    KMOstreeMutableTree files_mtree;
    if (!d->m_mutableTree->ensureDir("files", files_mtree))
    {
        throw KMException(d->m_mutableTree->errorMessage());
    }

    FilterData filterData;
    KMOstreeRepoCommitModifier modifier(OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS, (OstreeRepoCommitFilter)commitFilter, &filterData, NULL);

    if (d->m_isExtension)
    {
        filterData.excludes = d->m_kmOptions->m_excludes;
        filterData.includes = d->m_kmOptions->m_includes;
        if (!d->m_repo->writeDirectoryToMtree(d->m_filesDir, files_mtree, modifier))
        {
            throw KMException(d->m_repo->errorMessage());
        }
        filterData.excludes.clear();
        filterData.includes.clear();
    }
    else if (d->m_kmOptions->m_isRuntime || d->m_isRuntime)
    {
        filterData.excludes = d->m_kmOptions->m_excludes;
        filterData.includes = d->m_kmOptions->m_includes;
        if (!d->m_repo->writeDirectoryToMtree(d->m_usrDir, files_mtree, modifier))
        {
            throw KMException(d->m_repo->errorMessage());
        }
        filterData.excludes.clear();
        filterData.includes.clear();
    }
    else
    {
        filterData.excludes = d->m_kmOptions->m_excludes;
        filterData.includes = d->m_kmOptions->m_includes;
        if (!d->m_repo->writeDirectoryToMtree(d->m_filesDir, files_mtree, modifier))
        {
            throw KMException(d->m_repo->errorMessage());
        }
        filterData.excludes.clear();
        filterData.includes.clear();

        KMOstreeMutableTree exportMtree;
        if (!d->m_mutableTree->ensureDir("export", exportMtree))
        {
            throw KMException(d->m_mutableTree->errorMessage());
        }

        if (!d->m_repo->writeDirectoryToMtree(d->m_exportDir, exportMtree, modifier))
        {
            throw KMException(d->m_repo->errorMessage());
        }
    }

    KMFile metadataFile(d->m_metadataFile);
    addFileToMtree(metadataFile, "metadata");

    KMFile root;
    if (!d->m_repo->writeMtree(*d->m_mutableTree, root))
    {
        throw KMException(d->m_repo->errorMessage());
    }

    std::uint64_t installedSize = 0, downloadSize = 0;
    if (!KMBuildinsUtils::repoCollectSizes(*d->m_repo, root, nullptr, installedSize, downloadSize, errMsg))
    {
        throw KMException(errMsg);
    }

    /* 绑定信息。xa.ref不赞成使用OSTree密钥，但为了向后兼容，请保留它。*/
    d->m_dict.insert("ostree.collection-binding", collectionId);
    const char *fullBranch = fullBranchString.c_str();
    d->m_dict.insert("ostree.ref-binding", g_variant_new_strv((const gchar *const *)(&fullBranch), 1));
    d->m_dict.insert("xa.ref", fullBranchString);

    std::string metadataContents;
    if (!metadataFile.loadContents(metadataContents))
    {
        throw KMException(metadataFile.errorMessage());
    }
    d->m_dict.insert("xa.metadata", metadataContents);
    d->m_dict.insert("xa.installed-size", GUINT64_TO_BE(installedSize));
    d->m_dict.insert("xa.download-size", GUINT64_TO_BE(downloadSize));

    if (!d->m_kmOptions->m_endoflife.empty())
    {
        d->m_dict.insert(OSTREE_COMMIT_META_KEY_ENDOFLIFE, d->m_kmOptions->m_endoflife);
    }

    if (!d->m_kmOptions->m_endoflifeRebase.empty())
    {
        std::vector<std::string> fullRefParts = KMUtil::splitString(fullBranchString, "/");
        std::string rebasedRef = KMUtil::buildFilename(fullRefParts.at(0), d->m_kmOptions->m_endoflifeRebase, fullRefParts.at(2), fullRefParts.at(3));
        if (!KMBuildinsUtils::isValidAppName(d->m_kmOptions->m_endoflifeRebase, errMsg))
        {
            throw KMException("Invalid name in --end-of-life-rebase");
        }
        d->m_dict.insert(OSTREE_COMMIT_META_KEY_ENDOFLIFE_REBASE, rebasedRef);
    }

    if (d->m_kmOptions->m_tokenType >= 0)
    {
        d->m_dict.insert("xa.token-type", d->m_kmOptions->m_tokenType);
    }

    KMVariant subsetsVariant;
    if (getSubsets(d->m_kmOptions->m_subsets, subsetsVariant))
    {
        d->m_dict.insert("xa.subsets", subsetsVariant);
    }

    KMVariant metadataDict;
    metadataDict = d->m_dict.end();

    /* 时间戳用于提交元数据和AppStream数据 */
    struct timespec ts;
    std::string commitChecksum;
    if (!d->m_kmOptions->m_timestamp.empty())
    {
        if (!parse_datetime(&ts, d->m_kmOptions->m_timestamp.c_str(), nullptr))
        {
            throw KMException(_("Could not parse timestamp : ") + d->m_kmOptions->m_timestamp);
        }

        if (!d->m_repo->writeCommitWithTime(parent, subject.c_str(), body.c_str(), metadataDict, root, ts.tv_sec, commitChecksum))
        {
            throw KMException(d->m_repo->errorMessage());
        }
    }
    else
    {
        if (!d->m_repo->writeCommit(parent, subject.c_str(), body.c_str(), metadataDict, root, commitChecksum))
        {
            throw KMException(d->m_repo->errorMessage());
        }
    }

    if (!d->m_kmOptions->m_gpgKeyIds.empty())
    {
        for (const std::string keyId : d->m_kmOptions->m_gpgKeyIds)
        {
            if (!d->m_repo->signCommit(commitChecksum.c_str(), keyId.c_str(), d->m_kmOptions->m_gpgHomedir.empty() ? nullptr : d->m_kmOptions->m_gpgHomedir.c_str()))
            {
                throw KMException(d->m_repo->errorMessage());
            }
        }
    }

    if (!collectionId.empty())
    {
        OstreeCollectionRef ref = { (char *)collectionId.c_str(), (char *)fullBranchString.c_str() };
        d->m_repo->transactionSetCollectionRef(&ref, commitChecksum.empty() ? nullptr : commitChecksum.c_str());
    }
    else
    {
        d->m_repo->transactionSetRef(nullptr, fullBranchString.c_str(), commitChecksum.empty() ? nullptr : commitChecksum.c_str());
    }

    OstreeRepoTransactionStats stats;
    if (!d->m_repo->commitTransaction(&stats))
    {
        throw KMException(d->m_repo->errorMessage());
    }

    // TODO : 暂不支持appstream

    if (!d->m_kmOptions->m_noUpdateSummary)
    {
        KMRepoUpdateFlags flags = KAIMING_REPO_UPDATE_FLAG_NONE;

        if (d->m_kmOptions->m_noSummaryIndex)
        {
            flags = (KMRepoUpdateFlags)(flags | KAIMING_REPO_UPDATE_FLAG_DISABLE_INDEX);
        }

        KMDebug("Updating summary");
        if (!KMBuildinsUtils::repoUpdate(*d->m_repo, flags, d->m_kmOptions->m_gpgKeyIds, d->m_kmOptions->m_gpgHomedir, errMsg))
        {
            throw KMException(errMsg);
        }
    }

    KMInfo(_("Commit: ") + commitChecksum);
    KMInfo(_("Metadata Total: ") + std::to_string(stats.metadata_objects_total));
    KMInfo(_("Metadata Written: ") + std::to_string(stats.metadata_objects_written));
    KMInfo(_("Content Total: ") + std::to_string(stats.content_objects_total));
    KMInfo(_("Content Written: ") + std::to_string(stats.content_objects_written));
    KMInfo(_("Content Bytes Written:"));
    KMInfo(" " + std::to_string(stats.content_bytes_written) + " (" + KMUtil::formatSize(stats.content_bytes_written) + ")");

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

void KMBuildExport::collectExtraData()
{
    KMTrace("KMBuildExport::collectExtraData invoke begin");

    std::map<std::string, std::string> extraDatas = d->m_kmmetadata->getGroupData(METADATA_GROUP_EXTRA_DATA);
    if (extraDatas.empty())
    {
        return;
    }

    KMVariantBuilder builder("a(ayttays)");

    std::string suffix, checksumKey, sizeKey, installedSizeKey, nameKey, uri, name, checksum, errMsg;
    for (auto &pair : extraDatas)
    {
        const std::string &key = pair.first;
        if (KMUtil::startsWith(key, METADATA_KEY_EXTRA_DATA_URI))
        {
            suffix = key.substr(3);

            checksumKey = METADATA_KEY_EXTRA_DATA_CHECKSUM + suffix;
            sizeKey = METADATA_KEY_EXTRA_DATA_SIZE + suffix;
            installedSizeKey = METADATA_KEY_EXTRA_DATA_INSTALLED_SIZE + suffix;
            nameKey = METADATA_KEY_EXTRA_DATA_NAME + suffix;

            uri = d->m_kmmetadata->getStringValue(METADATA_GROUP_EXTRA_DATA, key);
            if (uri.empty())
            {
                throw KMException(_("'uri' cann't be empty"));
            }

            if (!KMUtil::startsWith(uri, "http:") && !KMUtil::startsWith(uri, "https:"))
            {
                throw KMException(uri + _(" is invalid uri type, only http/https supported"));
            }

            if (d->m_kmmetadata->keyExists(METADATA_GROUP_EXTRA_DATA, nameKey))
            {
                name = d->m_kmmetadata->getStringValue(METADATA_GROUP_EXTRA_DATA, nameKey);
                if (name.empty())
                {
                    throw KMException(_("'name' cann't be empty"));
                }
            }
            else
            {
                g_autoptr(GFile) file = g_file_new_for_uri(uri.c_str());
                g_autofree char *filename = g_file_get_basename(file);
                if (filename == NULL || *filename == 0)
                {
                    throw KMException(_("Please specify a name explicitly, unable to find basename in ") + uri);
                }

                name = filename;
            }

            if (std::string::npos != name.find_first_of('/'))
            {
                throw KMException(_("No slashes allowed in extra data name"));
            }

            checksum = d->m_kmmetadata->getStringValue(METADATA_GROUP_EXTRA_DATA, checksumKey);
            if (checksum.empty())
            {
                throw KMException(_("'checksum' cann't be empty"));
            }

            if (!KMOstreeCore::validateChecksumString(checksum, errMsg))
            {
                throw KMException(_("Invalid format for sha256 checksum: ") + checksum);
            }

            std::uint64_t size = d->m_kmmetadata->getLongValue(METADATA_GROUP_EXTRA_DATA, sizeKey);
            if (size == 0)
            {
                throw KMException(_("Extra data sizes of zero not supported"));
            }

            std::uint64_t installedSize = d->m_kmmetadata->getLongValue(METADATA_GROUP_EXTRA_DATA, installedSizeKey);

            builder.add("(^aytt@ay&s)", name.c_str(), GUINT64_TO_BE(size), GUINT64_TO_BE(installedSize), KMOstreeCore::checksumToBytes_v(checksum).get());
        }
    }

    d->m_dict.insert("xa.extra-data-sources", builder.end());

    KMTrace("KMBuildExport::collectExtraData invoke end");
}

void KMBuildExport::validateExports()
{
    KMTrace("KMBuildExport::validateExports invoke begin");

    std::string icon;
    bool activatable = false;
    validateDesktopFile(icon, activatable);

    validateIcon(icon);

    validateExportedIcons();

    validateServiceFile(activatable);

    KMTrace("KMBuildExport::validateExports invoke end");
}

static std::string convertAppAbsolutePath(const std::string &command, const std::string &fileDir)
{
    std::string exec_path;
    if (g_path_is_absolute(command.c_str()))
    {
        if (KMUtil::startsWith(command, "/app/"))
        {
            exec_path = command.substr(5);
        }
        else
        {
            exec_path = command;
        }
    }
    else
    {
        exec_path = "bin/" + command;
    }

    return KMUtil::buildFilename(fileDir, exec_path);
}

void KMBuildExport::validateDesktopFile(std::string &icon, bool &activatable)
{
    KMTrace("KMBuildExport::validateDesktopFile invoke begin");

    std::string desktopPath = "share/applications/" + d->m_id + ".desktop";
    std::string desktopFile = KMUtil::buildFilename(d->m_exportDir, desktopPath);

    if (fs::exists(desktopFile))
    {
        std::string result = KMUtil::execuseCMD("desktop-file-validate", desktopFile);
        if (std::string::npos != result.find("error:"))
        {
            KMError(_("WARNING: Error reading from desktop-file-validate: \n") + result);
        }
        else if (!result.empty())
        {
            KMWarn(result);
        }

        /* Test that references to other files are valid */

        KMKeyFile keyFile;
        if (!keyFile.loadFile(desktopFile))
        {
            throw KMException(_("Failed to load file : ") + desktopFile);
        }

        activatable = keyFile.getBoolValue(G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_DBUS_ACTIVATABLE);

        /* Validate Exec command: Unless we have DBusActivatable=true the key should
         * be present and – if set to a non-empty value – should point to an existing
         * binary.
         *
         * Empty values are allowed, they will result in the default command being
         * run by kaiming when starting the application.
         */
        std::string command = keyFile.getStringValue(G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_EXEC);
        if (command.empty() && activatable == false)
        {
            KMWarn(_("WARNING: Can't find Exec key in ") + desktopFile);
        }
        else if (!command.empty())
        {
            std::vector<std::string> args = KMUtil::splitString(command, " ");
            std::string bin_file = convertAppAbsolutePath(args.at(0), d->m_filesDir);

            if (!fs::exists(bin_file))
            {
                KMWarn(_("WARNING: Binary not found for Exec line in ") + desktopFile + " : " + command);
            }
        }

        icon = keyFile.getStringValue(G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON);
        if (!KMUtil::startsWith(icon, d->m_id))
        {
            KMWarn(_("WARNING: Icon not matching app id in ") + desktopFile + " : " + command);
        }
    }

    KMTrace("KMBuildExport::validateDesktopFile invoke end");
}

void KMBuildExport::validateIcon(const std::string &icon)
{
    KMTrace("KMBuildExport::validateIcon invoke begin");

    if (!icon.empty())
    {
        std::string icondir = KMUtil::buildFilename(d->m_exportDir, "share/icons/hicolor");
        std::string png = icon + ".png";
        std::string svg = icon + ".svg";

        if (!KMUtil::findFileInTree(icondir, png) && !KMUtil::findFileInTree(icondir, svg))
        {
            KMWarn(_("WARNING: Icon referenced in desktop file but not exported: ") + icon);
        }
    }

    KMTrace("KMBuildExport::validateIcon invoke end");
}

typedef bool (*VisitFileFunc)(const std::string &file, std::string &errMsg);

static bool validateIconFile(const std::string &file, std::string &errMsg)
{
    // TODO : 暂未实现校验icon文件的小工具——kaiming-validate-icon
    return true;
}

static bool visitFilesInTree(const std::string &baseDir, VisitFileFunc visitFile, std::string &errMsg)
{
    fs::path basePath(baseDir);
    if (!fs::exists(basePath))
    {
        return true;
    }

    for (auto const &dirEntry : fs::directory_iterator{ basePath })
    {
        fs::path p = dirEntry.path();
        if (fs::is_regular_file(p))
        {
            if (!visitFile(p.string(), errMsg))
            {
                return false;
            }
        }
        else if (fs::is_directory(p))
        {
            if (!visitFilesInTree(p.string(), visitFile, errMsg))
            {
                return false;
            }
        }
    }

    return true;
}

void KMBuildExport::validateExportedIcons()
{
    KMTrace("KMBuildExport::validateExportedIcons invoke begin");

    std::string icondir = KMUtil::buildFilename(d->m_exportDir, "share/icons/hicolor");

    std::string errMsg;
    visitFilesInTree(icondir, validateIconFile, errMsg);

    KMTrace("KMBuildExport::validateExportedIcons invoke end");
}

void KMBuildExport::validateServiceFile(bool activatable)
{
    KMTrace("KMBuildExport::validateServiceFile invoke begin");

    std::string servicePath = "share/dbus-1/services/" + d->m_id + ".service";
    std::string serviceFile = KMUtil::buildFilename(d->m_exportDir, servicePath);

    if (!fs::exists(serviceFile))
    {
        if (activatable)
        {
            throw KMException("Desktop file D-Bus activatable, but service file not exported: " + serviceFile);
        }
        else
        {
            return;
        }
    }

    KMKeyFile keyFile;
    if (!keyFile.loadFile(serviceFile))
    {
        throw KMException("Failed to load file : " + serviceFile);
    }

    std::string name = keyFile.getStringValue("D-BUS Service", "Name");
    if (name.empty())
    {
        throw KMException("Invalid service file : " + serviceFile);
    }

    if (name != d->m_id)
    {
        throw KMException("Name in service file " + serviceFile + " does not match app id: " + d->m_id);
    }

    std::string command = keyFile.getStringValue("D-BUS Service", "Exec");
    if (command.empty())
    {
        throw KMException("Invalid service file : " + serviceFile);
    }

    std::vector<std::string> args = KMUtil::splitString(command, " ");
    std::string binFile = convertAppAbsolutePath(args.at(0), d->m_filesDir);

    if (!fs::exists(binFile))
    {
        KMWarn(_("WARNING: Binary not found for Exec line in ") + serviceFile + " : " + command);
    }

    KMTrace("KMBuildExport::validateServiceFile invoke end");
}

void KMBuildExport::metadataGetArch()
{
    KMTrace("KMBuildExport::metadataGetArch invoke begin");

    if (!d->m_kmOptions->m_arch.empty())
    {
        d->m_arch = d->m_kmOptions->m_arch;
        return;
    }

    std::string runtime = d->m_kmmetadata->getStringValue(METADATA_GROUP_APPLICATION, METADATA_KEY_RUNTIME);

    if (runtime.empty())
    {
        runtime = d->m_kmmetadata->getStringValue(METADATA_GROUP_APPLICATION, METADATA_KEY_SDK);
    }

    if (runtime.empty())
    {
        runtime = d->m_kmmetadata->getStringValue(METADATA_GROUP_RUNTIME, METADATA_KEY_RUNTIME);
    }

    if (runtime.empty())
    {
        runtime = d->m_kmmetadata->getStringValue(METADATA_GROUP_RUNTIME, METADATA_KEY_SDK);
    }

    if (runtime.empty())
    {
        d->m_arch = KMUtil::kmGetArch();

        KMTrace("KMBuildExport::metadataGetArch invoke end");
        return;
    }

    KMRef ref = KMBuildinsUtils::prefToKMRef(KaimingKinds::KAIMING_KINDS_RUNTIME, runtime);
    d->m_arch = ref.arch;

    KMTrace("KMBuildExport::metadataGetArch invoke end");
}

void KMBuildExport::addFileToMtree(KMFile &file, const std::string &name)
{
    KMTrace("KMBuildExport::addFileToMtree invoke begin");

    KMFileInfo fileInfo = file.queryInfo("standard::size");

    if (!fileInfo)
    {
        throw KMException("Failed to query file info : " + file.getPath());
    }

    fileInfo.setName(name);
    fileInfo.setFileType(G_FILE_TYPE_REGULAR);
    fileInfo.setAttribute("unix::uid", 0);
    fileInfo.setAttribute("unix::gid", 0);
    fileInfo.setAttribute("unix::mode", 0100644);

    KMInputStream rawInput(file);
    if (!rawInput)
    {
        throw KMException("Failed to read file : " + file.getPath());
    }

    KMInputStream input;
    std::uint64_t length = 0;
    std::string errMsg;
    if (!KMOstreeCore::rawFileToContentStream(rawInput, fileInfo, KMVariant(), input, length, errMsg))
    {
        throw KMException(errMsg);
    }

    g_autofree guchar *childFileCsum = nullptr;
    if (!d->m_repo->writeContent(nullptr, input, length, &childFileCsum))
    {
        throw KMException(d->m_repo->errorMessage());
    }

    std::string tmpChecksum = KMOstreeCore::checksumFromBytes(childFileCsum);
    if (!d->m_mutableTree->replaceFile(name.c_str(), tmpChecksum.c_str()))
    {
        throw KMException(d->m_mutableTree->errorMessage());
    }

    KMTrace("KMBuildExport::addFileToMtree invoke end");
}
