/** * @file modules.cpp * @brief Module discovery, load-time validation, routing, enumeration * * Implements the load-time enforcement sequence: libdpm-core is the sole * authority on module validity. A module is either fully valid or not * loaded — no partial states. * * @copyright Copyright (c) 2026 SILO GROUP LLC * @author Chris Punches * * Part of the Dark Horse Linux Package Manager (DPM) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include "internal/context.hpp" #include "internal/version.hpp" #include #include #include #include #include namespace fs = std::filesystem; #ifndef DPM_CORE_VERSION_STR #define DPM_CORE_VERSION_STR "0.0.0" #endif /* dlclose wrapper referenced from context.cpp so handle release stays in one translation unit with the loader. */ void dpm_internal_unload(void* handle) { if (handle) { dlclose(handle); } } namespace { using execute_fn = int (*)(dpm_ctx*, const char*, int, char**); using string_fn = const char* (*)(void); using manifest_fn = const dpm_manifest* (*)(void); /** dlsym with dlerror() discipline; returns nullptr on any error. */ void* resolve(void* handle, const char* symbol) { dlerror(); void* addr = dlsym(handle, symbol); if (dlerror() != nullptr) { return nullptr; } return addr; } } // namespace namespace dpmcore { std::unique_ptr validate_and_load(dpm_ctx* ctx, const std::string& name, std::string& reason) { std::string so_path = ctx->module_path + name + ".so"; std::error_code ec; if (!fs::exists(so_path, ec)) { reason = "not found at " + so_path; return nullptr; } void* handle = dlopen(so_path.c_str(), RTLD_NOW | RTLD_LOCAL); if (!handle) { const char* err = dlerror(); reason = std::string("dlopen failed: ") + (err ? err : "unknown"); return nullptr; } /* Step 1: resolve all reserved contract symbols. */ static const char* required[] = { "dpm_module_execute", "dpm_module_version", "dpm_module_description", "dpm_module_core_min", "dpm_module_manifest", }; std::string missing; for (const char* sym : required) { if (!resolve(handle, sym)) { if (!missing.empty()) { missing += ", "; } missing += sym; } } if (!missing.empty()) { reason = "missing required contract symbols: " + missing; dlclose(handle); return nullptr; } auto exec_f = reinterpret_cast(resolve(handle, "dpm_module_execute")); auto version_f = reinterpret_cast(resolve(handle, "dpm_module_version")); auto desc_f = reinterpret_cast(resolve(handle, "dpm_module_description")); auto core_min_f = reinterpret_cast(resolve(handle, "dpm_module_core_min")); auto manifest_f = reinterpret_cast(resolve(handle, "dpm_module_manifest")); /* Step 2: minimum-version handshake. */ const char* core_min = core_min_f(); long parsed[3]; if (!core_min || !parse_version(core_min, parsed)) { reason = "dpm_module_core_min() returned a malformed version"; dlclose(handle); return nullptr; } if (compare_versions(core_min, DPM_CORE_VERSION_STR) > 0) { reason = std::string("requires libdpm-core >= ") + core_min + ", running libdpm-core is " DPM_CORE_VERSION_STR " — update libdpm-core"; dlclose(handle); return nullptr; } /* Step 3: probe the cheap calls. */ const char* version = version_f(); if (!version || !parse_version(version, parsed)) { reason = "dpm_module_version() returned a malformed version"; dlclose(handle); return nullptr; } const char* description = desc_f(); if (!description || !*description) { reason = "dpm_module_description() returned nothing"; dlclose(handle); return nullptr; } /* Step 4: cross-check the manifest. */ const dpm_manifest* manifest = manifest_f(); if (!manifest) { reason = "dpm_module_manifest() returned NULL"; dlclose(handle); return nullptr; } if (manifest->count > 0 && !manifest->entries) { reason = "manifest declares entries but the entry table is NULL"; dlclose(handle); return nullptr; } for (uint32_t i = 0; i < manifest->count; i++) { const dpm_manifest_entry& entry = manifest->entries[i]; if (!entry.api_name || !*entry.api_name || !entry.symbol || !*entry.symbol || entry.table_version < 1) { reason = "manifest entry " + std::to_string(i) + " is malformed"; dlclose(handle); return nullptr; } void* table = resolve(handle, entry.symbol); if (!table) { reason = std::string("manifest declares API '") + entry.api_name + "' v" + std::to_string(entry.table_version) + "' at symbol '" + entry.symbol + "' but the symbol does not resolve"; dlclose(handle); return nullptr; } /* Step 5: table sanity. */ auto* header = static_cast(table); if (header->magic != DPM_API_TABLE_MAGIC) { reason = std::string("API table '") + entry.symbol + "' has a bad magic constant"; dlclose(handle); return nullptr; } if (header->size < sizeof(dpm_api_table_header)) { reason = std::string("API table '") + entry.symbol + "' reports an impossible size"; dlclose(handle); return nullptr; } } auto mod = std::make_unique(); mod->name = name; mod->handle = handle; mod->version = version; mod->description = description; mod->core_min = core_min; mod->manifest = manifest; mod->execute = exec_f; return mod; } } // namespace dpmcore extern "C" { dpm_module* dpm_require(dpm_ctx* ctx, const char* name) { if (!ctx || !name || !*name) { return nullptr; } auto it = ctx->modules.find(name); if (it != ctx->modules.end()) { return it->second.get(); } std::string reason; auto loaded = dpmcore::validate_and_load(ctx, name, reason); if (!loaded) { dpmcore::set_error(ctx, std::string("module '") + name + "': " + reason); return nullptr; } dpm_module* mod = loaded.get(); ctx->modules[name] = std::move(loaded); return mod; } int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out) { if (!ctx || !mod || !out) { return 1; } out->name = mod->name.c_str(); out->version = mod->version.c_str(); out->description = mod->description.c_str(); out->core_min = mod->core_min.c_str(); return 0; } const void* dpm_get_api(dpm_ctx* ctx, dpm_module* mod, const char* api_name, int table_version) { if (!ctx || !mod || !api_name) { return nullptr; } for (uint32_t i = 0; i < mod->manifest->count; i++) { const dpm_manifest_entry& entry = mod->manifest->entries[i]; if (entry.table_version == table_version && std::string(entry.api_name) == api_name) { dlerror(); void* table = dlsym(mod->handle, entry.symbol); if (dlerror() != nullptr || !table) { dpmcore::set_error(ctx, std::string("API table symbol '") + entry.symbol + "' vanished after load"); return nullptr; } return table; } } dpmcore::set_error(ctx, std::string("module '") + mod->name + "' does not provide API '" + api_name + "' v" + std::to_string(table_version)); return nullptr; } int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv) { if (!ctx || !mod || !mod->execute) { return 1; } return mod->execute(ctx, command, argc, argv); } dpm_cursor* dpm_list_modules(dpm_ctx* ctx) { if (!ctx) { return nullptr; } std::error_code ec; if (!fs::is_directory(ctx->module_path, ec)) { dpmcore::set_error(ctx, "module path is not a readable directory: " + ctx->module_path); return nullptr; } std::set names; for (const auto& entry : fs::directory_iterator(ctx->module_path, ec)) { if (ec) { break; } if (!entry.is_regular_file() && !entry.is_symlink()) { continue; } if (entry.path().extension() != ".so") { continue; } names.insert(entry.path().stem().string()); } auto* cur = new (std::nothrow) dpm_cursor; if (!cur) { return nullptr; } for (const std::string& name : names) { dpm_module* mod = nullptr; auto it = ctx->modules.find(name); if (it != ctx->modules.end()) { mod = it->second.get(); } else { std::string reason; auto loaded = dpmcore::validate_and_load(ctx, name, reason); if (!loaded) { dpm_log(ctx, DPM_LOG_WARN, ("module '" + name + "' failed validation: " + reason).c_str()); continue; } mod = loaded.get(); ctx->modules[name] = std::move(loaded); } dpm_module_info info; info.name = mod->name.c_str(); info.version = mod->version.c_str(); info.description = mod->description.c_str(); info.core_min = mod->core_min.c_str(); cur->infos.push_back(info); } return cur; } int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out) { if (!cur || !out || cur->idx >= cur->infos.size()) { return 1; } *out = cur->infos[cur->idx]; cur->idx++; return 0; } void dpm_cursor_free(dpm_cursor* cur) { delete cur; } } /* extern "C" */