static void PluginLoadGenericSingle(
    _Inout_ PPLUGIN_HEAD plugin
    ) {
    TCHAR path[MAX_PATH] = { 0 };
    size_t size = 0;

    size = _stprintf_s(path, _countof(path), "%s/%s.%s", plugin->type->directory, PLUGIN_GET_NAME(plugin), PLUGINS_FILE_EXTENSION);
    if (size == -1) {
        FATAL(_T("Cannot construct path for module <%s> (directory is <%s>)"), PLUGIN_GET_NAME(plugin), plugin->type->directory);
    }

    plugin->module = LoadLibrary(path);
    if (!plugin->module) {
        FATAL(_T("Cannot open module for plugin <%s> : <%u> (path is <%s>)"), PLUGIN_GET_NAME(plugin), GetLastError(), path);
    }
    else {
        LOG(Dbg, _T("Loaded module for plugin <%s> : <%#08x>"), PLUGIN_GET_NAME(plugin), plugin->module);
    }

    PLUGIN_RESOLVE_FN(plugin, PLUGIN_PFN_INITIALIZE, Initialize, PLUGIN_GENERIC_INITIALIZE, FALSE);
    PLUGIN_RESOLVE_FN(plugin, PLUGIN_PFN_FINALIZE, Finalize, PLUGIN_GENERIC_FINALIZE, FALSE);
    PLUGIN_RESOLVE_FN(plugin, PLUGIN_PFN_SIMPLE, Help, PLUGIN_GENERIC_HELP, TRUE);
#pragma warning(disable:4054)
    PLUGIN_RESOLVE_VAR(plugin, LPTSTR, description, PLUGIN_GENERIC_DESCRIPTION, FALSE);
    PLUGIN_RESOLVE_VAR(plugin, LPTSTR, keyword, PLUGIN_GENERIC_KEYWORD, TRUE);
#pragma warning(default:4054)

    PluginLoadRequirements(plugin);
}
BOOL PluginUnload(
    _Inout_ PPLUGIN_HEAD plugin
    ) {
    LOG(Info, SUB_LOG(_T("Unloading plugin <%s>")), PLUGIN_GET_NAME(plugin));

    BOOL bResult = FreeLibrary(plugin->module);
    if (!bResult) {
        LOG(Warn, SUB_LOG(_T("Cannot close plugin <%s> : <%u>")), PLUGIN_GET_NAME(plugin), GetLastError());
    }
    return bResult;
}
static DWORD PluginLoadGenericMultiple(
    _Inout_ LPTSTR names,
    _Inout_ PBYTE pluginsArray,
    _In_ DWORD max,
    _In_ PLUGIN_TYPE const * const type
    ) {
    DWORD count = 0;
    DWORD i = 0;
    LPTSTR pluginName = NULL;
    LPTSTR ctx = NULL;
    PPLUGIN_HEAD plugin = NULL;

    while (StrNextToken(names, _T(","), &ctx, &pluginName)) {
        LOG(Info, SUB_LOG(_T("Loading %s <%s>")), type->name, pluginName);

        if (count >= max) {
            FATAL(_T("Cannot load more than <%u> %ss"), max, type->name);
        }

        for (i = 0; i < count; i++) {
            if (STR_EQ(pluginName, PLUGIN_GET_NAME((PPLUGIN_HEAD)(pluginsArray + (i * type->size))))) {
                FATAL(_T("%s <%s> is already loaded"), type->name, pluginName);
            }
        }

        plugin = (PPLUGIN_HEAD)(pluginsArray + (count * type->size));
        PLUGIN_SET_NAME(plugin, pluginName);
        PLUGIN_SET_TYPE(plugin, type);
        PluginLoadGenericSingle(plugin);

        count++;
    }

    return count;
}
BOOL PluginFinalize(
    _Inout_ PPLUGIN_HEAD plugin
    ) {
    LOG(Info, SUB_LOG(_T("Finalizing plugin <%s>")), PLUGIN_GET_NAME(plugin));

    if (plugin->functions.Finalize) {
        BOOL bResult = plugin->functions.Finalize(&gc_PluginApiTable);
        if (!bResult){
            LOG(Err, SUB_LOG(_T("Cannot finalize plugin <%s>")), PLUGIN_GET_NAME(plugin));
        }
        return bResult;
    }

    LOG(Dbg, SUB_LOG(_T("Plugin <%s> does not have an finalizer")), PLUGIN_GET_NAME(plugin));
    return TRUE;
}
BOOL PluginHelp(
    _Inout_ PPLUGIN_HEAD plugin
    ) {
    LOG(Bypass, _T("------------------------------"));
    LOG(Bypass, _T("Help for %s <%s> :"), plugin->type->name, PLUGIN_GET_NAME(plugin));
    LOG(Bypass, SUB_LOG(_T("Description : %s")), PLUGIN_GET_DESCRIPTION(plugin));
    plugin->functions.Help(&gc_PluginApiTable);
    LOG(Bypass, EMPTY_STR);

    return TRUE;
}
/* --- PRIVATE FUNCTIONS ---------------------------------------------------- */
static FARPROC PluginResolve(
    _Inout_ PPLUGIN_HEAD plugin,
    _In_ LPCSTR procName,
    _In_ BOOL fatal
    ) {

    FARPROC proc = GetProcAddress(plugin->module, procName);
    if (!proc && fatal) {
        FATAL(_T("Failed to resolve required symbol <%s> for plugin <%s>"), procName, PLUGIN_GET_NAME(plugin));
    }
    else {
        LOG(Dbg, _T("Symbol <%s> for plugin <%s> : <%p>"), procName, PLUGIN_GET_NAME(plugin), proc);
    }
    return proc;
}
static void ParseOptions(
    _In_ PACE_FILTER_OPTIONS opt,
    _In_ int argc,
    _In_ TCHAR * argv[]
    ) {

    const static struct option long_options[] = {
        { _T("list-plugins"), no_argument, 0, _T('L') },
        { _T("list-importers"), no_argument, 0, _T('I') },
        { _T("list-filters"), no_argument, 0, _T('F') },
        { _T("list-writers"), no_argument, 0, _T('W') },

        { _T("importers"), required_argument, 0, _T('i') },
        { _T("filters"), required_argument, 0, _T('f') },
        { _T("writers"), required_argument, 0, _T('w') },
        { _T("invert-filters"), required_argument, 0, _T('r') },

        { _T("progression"), required_argument, 0, _T('p') },
        { _T("logfile"), required_argument, 0, _T('l') },
        { _T("dbglvl"), required_argument, 0, _T('d') },
        { _T("loglvl"), required_argument, 0, _T('d') },
        { _T("help"), no_argument, 0, _T('h') },
        { _T("usage"), no_argument, 0, _T('h') },

        { 0, 0, 0, 0 }
    };

    int curropt = 0;
    BOOL bExitAfterOpt = FALSE;
    LPTSTR filter = NULL;
    LPTSTR ctx = NULL;

    //
    // Default options
    //
    SetLogLevel(DEFAULT_OPT_DEBUGLEVEL);
    opt->misc.progression = DEFAULT_OPT_PROGRESSION;

    //
    // Parsing
    //
    while ((curropt = getopt_long_only(argc, argv, EMPTY_STR, long_options, NULL)) != -1) {
        switch (curropt) {
            //
            // List
            //
        case _T('L'):
            SetLogLevel(_T("NONE"));
            ListImporters();
            ListFilters();
            ListWriters();
            ExitProcess(EXIT_SUCCESS);
            break;

        case _T('I'):
            SetLogLevel(_T("NONE"));
            ListImporters();
            bExitAfterOpt = TRUE;
            break;

        case _T('F'):
            SetLogLevel(_T("NONE"));
            ListFilters();
            bExitAfterOpt = TRUE;
            break;

        case _T('W'):
            SetLogLevel(_T("NONE"));
            ListWriters();
            bExitAfterOpt = TRUE;
            break;

            //
            // Plugins
            //
        case _T('i'):
            opt->names.importers = optarg;
            opt->plugins.numberOfImporters = PluginLoadImporters(opt->names.importers, opt->plugins.importers, PLUGIN_MAX_IMPORTERS);
            break;

        case _T('f'):
            opt->names.filters = optarg;
            opt->plugins.numberOfFilters = PluginLoadFilters(opt->names.filters, opt->plugins.filters, PLUGIN_MAX_FILTERS);
            break;

        case _T('w'):
            opt->names.writers = optarg;
            opt->plugins.numberOfWriters = PluginLoadWriters(opt->names.writers, opt->plugins.writers, PLUGIN_MAX_WRITERS);
            break;

        case _T('r'):
            opt->names.inverted = optarg;

            DWORD i = 0;
            filter = NULL;
            ctx = NULL;

            // TODO : invert after filters (numOfF == 0)

            while (StrNextToken(opt->names.inverted, ",", &ctx, &filter)) {
                for (i = 0; i < PLUGIN_MAX_FILTERS; i++) {
                    if (PLUGIN_IS_LOADED(&opt->plugins.filters[i])) {
                        if (STR_EQ(filter, PLUGIN_GET_NAME(&opt->plugins.filters[i]))) {
                            LOG(Info, SUB_LOG(_T("Inverting filter <%s>")), filter);
                            opt->plugins.filters[i].inverted = TRUE;
                            break;
                        }
                    }
                }
                if (i == PLUGIN_MAX_FILTERS) {
                    FATAL(_T("Trying to invert a non-loaded filter : <%s>"), filter);
                }
            }
            break;

            //
            // Misc
            //
        case _T('p'):
            opt->misc.progression = _tstoi(optarg);
            LOG(Info, SUB_LOG(_T("Printing progression every <%u> ACE")), opt->misc.progression);
            break;

        case _T('l'):
            opt->misc.logfile = optarg;
            SetLogFile(opt->misc.logfile);
            break;

        case _T('d'):
            opt->misc.loglvl = optarg;
            SetLogLevel(opt->misc.loglvl);
            break;

        case _T('h'):
            opt->misc.showHelp = TRUE;
            break;

        default:
            FATAL(_T("Unknown option"));
            break;
        }
    }


    LPTSTR optname = NULL;
    LPTSTR optval = NULL;
    int i = 0;

    for (i = optind; i < argc; i++) {
        ctx = NULL;

        StrNextToken(argv[i], "=", &ctx, &optname);
        optval = ctx; //We do not use StrNextToken here because optval can contain an equal sign.

        if (!optname || !optval) {
            FATAL(_T("Cannot parse plugin option <%s> (must be in format name=value)"), argv[i]);
        }
        else {
            LOG(Dbg, _T("Adding plugin option <%s : %s>"), optname, optval);
            AddStrPair(&gs_PluginOptions.end, optname, optval);
            if (!gs_PluginOptions.head) {
                gs_PluginOptions.head = gs_PluginOptions.end;
            }
        }
    }

    if (bExitAfterOpt) {
        ExitProcess(EXIT_SUCCESS);
    }
}
int main(
    _In_ int argc,
    _In_ TCHAR * argv[]
    ) {
    DWORD i = 0;

    LOG(Succ, _T("Starting"));

    //
    // Options parsing
    //
    LOG(Succ, _T("Parsing options"));
    ACE_FILTER_OPTIONS options = { 0 };
    DWORD importerAce = PLUGIN_MAX_IMPORTERS;
    DWORD importerObj = PLUGIN_MAX_IMPORTERS;
    DWORD importerSch = PLUGIN_MAX_IMPORTERS;

    if (argc > 1) {
        ParseOptions(&options, argc, argv);
    }
    else {
        options.misc.showHelp = TRUE;
    }

    if (options.misc.showHelp) {
        Usage(argv[0], NULL);
        ForeachLoadedPlugins(&options, PluginHelp);
        ExitProcess(EXIT_FAILURE);
    }


    //
    // Plugins verifications
    //
    LOG(Succ, "Verifying and choosing plugins");

    if (options.plugins.numberOfImporters == 0){
        FATAL(_T("No importer has been loaded"));
    }

    if (options.plugins.numberOfWriters == 0){
        FATAL(_T("No writer has been loaded"));
    }

    for (i = 0; i < options.plugins.numberOfImporters; i++) {
        if (!PLUGIN_IS_LOADED(&options.plugins.importers[i])) {
            FATAL(_T("Importer <%u> is registered, but not loaded"), i);
        }

        if (importerAce == PLUGIN_MAX_IMPORTERS && options.plugins.importers[i].functions.GetNextAce) {
            LOG(Info, SUB_LOG(_T("Using <%s> to import ACE")), PLUGIN_GET_NAME(&options.plugins.importers[i]));
            importerAce = i;
        }

        if (importerObj == PLUGIN_MAX_IMPORTERS && options.plugins.importers[i].functions.GetNextSchema) {
            LOG(Info, SUB_LOG(_T("Using <%s> to import objects")), PLUGIN_GET_NAME(&options.plugins.importers[i]));
            importerObj = i;
        }

        if (importerSch == PLUGIN_MAX_IMPORTERS && options.plugins.importers[i].functions.GetNextObject) {
            LOG(Info, SUB_LOG(_T("Using <%s> to import schema")), PLUGIN_GET_NAME(&options.plugins.importers[i]));
            importerSch = i;
        }
    }

	if (importerAce == PLUGIN_MAX_IMPORTERS){
		FATAL(_T("ACE importer is missing"));
	}
	if (importerObj == PLUGIN_MAX_IMPORTERS){
		FATAL(_T("Obj importer is missing"));
	}
	if (importerSch == PLUGIN_MAX_IMPORTERS){
		FATAL(_T("Sch importer is missing"));
	}

    // We allow no filter to be loaded, to permit ACE format conversion (direct link importer->writer)
    if (options.plugins.numberOfFilters > 0) {
        for (i = 0; i < options.plugins.numberOfFilters; i++) {
            if (!PLUGIN_IS_LOADED(&options.plugins.filters[i])) {
                FATAL(_T("Filter <%u> is registered, but not loaded"), i);
            }
        }
    }

    for (i = 0; i < options.plugins.numberOfWriters; i++) {
        if (!PLUGIN_IS_LOADED(&options.plugins.writers[i])) {
            FATAL(_T("Writer <%u> is registered, but not loaded"), i);
        }
    }


    //
    // Initializing plugins
    //
    LOG(Succ, _T("Initializing plugins"));
    ForeachLoadedPlugins(&options, PluginInitialize);


    //
    // Constructing caches
    //
    LOG(Succ, _T("Constructing caches"));

    CachesInitialize();

    if (PluginsRequires(&options, OPT_REQ_SID_RESOLUTION) || PluginsRequires(&options, OPT_REQ_DN_RESOLUTION)) {
        LOG(Info, SUB_LOG(_T("Plugins require SID or DN resolution, constructing object cache")));
        ConstructObjectCache(&options.plugins.importers[importerObj]);
        LOG(Info, SUB_LOG(_T("Object cache count : <by-sid:%u> <by-dn:%u>")), CacheObjectBySidCount(), CacheObjectByDnCount());
    }

	if (PluginsRequires(&options, OPT_REQ_GUID_RESOLUTION) || PluginsRequires(&options, OPT_REQ_CLASSID_RESOLUTION) || PluginsRequires(&options, OPT_REQ_DISPLAYNAME_RESOLUTION)) {
        LOG(Info, SUB_LOG(_T("Plugins require GUID or CLASSID or DisplayName resolution, constructing schema cache")));
        ConstructSchemaCache(&options.plugins.importers[importerSch]);
        LOG(Info, SUB_LOG(_T("Schema cache count : <by-guid:%u> <by-classid:%u> <by-displayname:%u>")), CacheSchemaByGuidCount(), CacheSchemaByClassidCount(), CacheSchemaByDisplayNameCount());
    }

    if (PluginsRequires(&options, OPT_REQ_ADMINSDHOLDER_SD)) {
        LOG(Info, SUB_LOG(_T("Plugins require AdminSdHolder security descriptor, constructing it")));
        ConstructAdminSdHolderSD(&options.plugins.importers[importerAce]);
    }


    //
    // Main Loop : process and filter ACEs
    //
    LOG(Succ, _T("Starting ACE filtering"));

    IMPORTED_ACE ace = { 0 };
    BOOL passedFilters = TRUE;
    BOOL filterRet = FALSE;

    // Stats variables
    DWORD aceCount = 0;
    DWORD keptAceCount = 0;
    ULONGLONG timeStart = 0;

    timeStart = GetTickCount64();

    options.plugins.importers[importerAce].functions.ResetReading(&gc_PluginApiTable, ImporterAce);
    while (options.plugins.importers[importerAce].functions.GetNextAce(&gc_PluginApiTable, &ace)) {
        passedFilters = TRUE;
        ace.computed.number = ++aceCount;

        for (i = 0; i < options.plugins.numberOfFilters; i++) {

            filterRet = options.plugins.filters[i].functions.FilterAce(&gc_PluginApiTable, &ace);
            passedFilters &= options.plugins.filters[i].inverted ? !filterRet : filterRet;

            if (!passedFilters) {
                LOG(All, _T("Ace <%u> was filtered by <%s>"), aceCount, PLUGIN_GET_NAME(&options.plugins.filters[i]));
                break;
            }
        }

        if (passedFilters) {
            keptAceCount++;
            LOG(All, _T("Ace <%u> passed all filters"), aceCount);
            for (i = 0; i < options.plugins.numberOfWriters; i++) {
                options.plugins.writers[i].functions.WriteAce(&gc_PluginApiTable, &ace);
            }
        }

        if (aceCount % options.misc.progression == 0) {
            LOG(Info, SUB_LOG(_T("Count : %u")), aceCount);
        }

        FreeCheckX(ace.imported.objectDn); // TODO : possible leak, since this is importer-dependant. Call importer's destroyer instead.
        LocalFreeCheckX(ace.imported.raw);
        ZeroMemory(&ace, sizeof(IMPORTED_ACE));
    }

    //
    // Stats
    //
    LOG(Succ, _T("Done. ACE Statistics :"));
    LOG(Succ, SUB_LOG(_T("<total    : %u>")), aceCount);
    LOG(Succ, SUB_LOG(_T("<filtered : %06.2f%%  %u>")), PERCENT((aceCount - keptAceCount), aceCount), (aceCount - keptAceCount));
    LOG(Succ, SUB_LOG(_T("<kept     : %06.2f%%  %u>")), PERCENT(keptAceCount, aceCount), keptAceCount);
    LOG(Succ, SUB_LOG(_T("<time     : %.3fs>")), TIME_DIFF_SEC(timeStart, GetTickCount64()));


    //
    // Finalizing and unloading plugins
    //
    LOG(Succ, _T("Finalizing and unloading plugins"));
    ForeachLoadedPlugins(&options, PluginFinalize);
    ForeachLoadedPlugins(&options, PluginUnload);

    CachesDestroy();
    DestroyStrPairList(gs_PluginOptions.head);

    //
    // End
    //
    LOG(Succ, _T("Done"));
    return EXIT_SUCCESS;
}
static void ConstructSchemaCache(
    _In_ PPLUGIN_IMPORTER importer
    ) {
    IMPORTED_SCHEMA sch = { 0 };
    DWORD schCount = 0;

    if (!importer->functions.GetNextSchema) {
        FATAL(_T("Schema cache is required but importer <%s> does allows Schema importation"), PLUGIN_GET_NAME(importer));
    }

    importer->functions.ResetReading(&gc_PluginApiTable, ImporterSchema);
    while (importer->functions.GetNextSchema(&gc_PluginApiTable, &sch)) {
        sch.computed.number = ++schCount;
        CacheInsertSchema(&sch);
        FreeCheckX(sch.imported.dn); // TODO : possible leak, since this is importer-dependant. Call importer's destroyer instead.
        FreeCheckX(sch.imported.defaultSecurityDescriptor);
        ZeroMemory(&sch, sizeof(IMPORTED_SCHEMA));
    }

    CacheActivateSchemaCache();
}
static void ConstructObjectCache(
    _In_ PPLUGIN_IMPORTER importer
    ) {
    IMPORTED_OBJECT obj = { 0 };
    DWORD objCount = 0;

    if (!importer->functions.GetNextObject) {
        FATAL(_T("Object cache is required but importer <%s> does allows Object importation"), PLUGIN_GET_NAME(importer));
    }

    importer->functions.ResetReading(&gc_PluginApiTable, ImporterObject);
    while (importer->functions.GetNextObject(&gc_PluginApiTable, &obj)) {
        obj.computed.number = ++objCount;
        CacheInsertObject(&obj);
        FreeCheckX(obj.imported.dn); // TODO : possible leak, since this is importer-dependant. Call importer's destroyer instead.
        ZeroMemory(&obj, sizeof(IMPORTED_OBJECT));
    }

    CacheActivateObjectCache();
}