Initial commit: DPM core library, CLI, bundled info module, and tests

This commit is contained in:
2026-08-09 18:00:05 -04:00
commit 19cf285cb7
29 changed files with 3722 additions and 0 deletions

370
src/modules.cpp Normal file
View File

@@ -0,0 +1,370 @@
/**
* @file modules.cpp
* @brief Module discovery, load-time validation, routing, enumeration
*
* Implements the load-time enforcement sequence: 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 <chris.punches@silogroup.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "internal/context.hpp"
#include "internal/version.hpp"
#include <algorithm>
#include <dlfcn.h>
#include <filesystem>
#include <new>
#include <set>
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<dpm_module> 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<execute_fn>(resolve(handle, "dpm_module_execute"));
auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version"));
auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description"));
auto core_min_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_core_min"));
auto manifest_f = reinterpret_cast<manifest_fn>(resolve(handle, "dpm_module_manifest"));
/* Step 2: core-minimum 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 core >= ") + core_min +
", running core is " DPM_CORE_VERSION_STR " — update 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<const dpm_api_table_header*>(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<dpm_module>();
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,
const char* min_version)
{
if (!ctx || !name || !*name) {
return nullptr;
}
long parsed[3];
if (min_version && !dpmcore::parse_version(min_version, parsed)) {
dpmcore::set_error(ctx, std::string("malformed minimum version: ") +
min_version);
return nullptr;
}
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) {
dpmcore::set_error(ctx, std::string("module '") + name +
"': " + reason);
return nullptr;
}
mod = loaded.get();
ctx->modules[name] = std::move(loaded);
}
if (min_version &&
dpmcore::compare_versions(mod->version.c_str(), min_version) < 0) {
dpmcore::set_error(ctx, std::string("module '") + name +
"' is version " + mod->version +
", below required minimum " + min_version);
return nullptr;
}
return mod;
}
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<std::string> 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" */