Files
dpm-core-ng/src/modules.cpp
Christopher M. Punches c41adc2498 Move version compatibility to the consumer; name libdpm-core explicitly
dpm_require no longer takes a minimum version and applies no version
criterion of its own. A handle now means the module is valid, not that
it suits the caller. dpm_module_info_of is added alongside it, reporting
the name, version, description, and minimum-libdpm-core version read at
load, so a consuming module can judge a dependency's version for itself.
The one rule still enforced is the minimum-version handshake, where
libdpm-core is the host and refuses a module that demands a newer library
than the one running.

dpm_module_info_of joins the version script, so the exported surface is
now fourteen symbols under DPM_CORE_1.0.

Separately, the bare word "core" is gone from prose everywhere. It named
both the command-line tool and the library, so every use forced the
reader to guess which. Text now says "the dpm binary" or "libdpm-core".
Identifiers keep their spelling: libdpm-core, core.h, core.conf, the
"core" configuration namespace, dpm_core_version, core_min, DPM_CORE_1.0,
the dpmcore namespace, test_core, core_api.

Three user-visible strings changed with it: the load-refusal message now
reads "requires libdpm-core >= X, running libdpm-core is Y — update
libdpm-core", and the info module's description and help text name the
library. The test asserting on the refusal text was updated to match.

DESIGN.md's terminology line no longer defines "DPM Core" as the CLI,
which was the source of the ambiguity. OVERVIEW.md is restructured around
the three layers a reader meets DPM at — user, developer, filesystem —
so a code-level symbol never appears without saying whose layer it is.
MODULES.md describes the bundled info module as testing and demonstrating
full DPM system functionality rather than as a reference implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 00:38:21 -04:00

368 lines
11 KiB
C++

/**
* @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 <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: 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<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)
{
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<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" */