libdpm-core.so's version was reaching the source as a compile-line definition, with a fallback in two translation units that would have let the library build and report 0.0.0 if the build ever stopped supplying it. The version is a literal in src/core/version.cpp, the file named for it, alongside dpm_core_version() which returns it and the parser for the X.Y.Z strings modules report. The library's sources and its version script move to src/core, leaving src/cli, src/bundled-modules, and the documentation source beside it.
266 lines
7.2 KiB
C++
266 lines
7.2 KiB
C++
/**
|
|
* @file modules.cpp
|
|
* @brief Module discovery, load-time validation, routing, enumeration
|
|
*
|
|
* Implements the load-time enforcement sequence: libdpm-core.so is the
|
|
* sole authority on module validity. A module is either fully valid or
|
|
* not loaded — no partial states.
|
|
*
|
|
* Dispatch through dpm_execute is the only path into module code, so a
|
|
* caller never holds compile-time knowledge of what it is calling.
|
|
*
|
|
* @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;
|
|
|
|
/* 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);
|
|
|
|
/** 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 dpm_core {
|
|
|
|
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",
|
|
};
|
|
|
|
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"));
|
|
|
|
/* Step 2: probe the cheap calls. */
|
|
long parsed[3];
|
|
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;
|
|
}
|
|
|
|
auto mod = std::make_unique<dpm_module>();
|
|
mod->name = name;
|
|
mod->handle = handle;
|
|
mod->version = version;
|
|
mod->description = description;
|
|
mod->execute = exec_f;
|
|
return mod;
|
|
}
|
|
|
|
} // namespace dpm_core
|
|
|
|
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 = dpm_core::validate_and_load(ctx, name, reason);
|
|
if (!loaded) {
|
|
dpm_core::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();
|
|
return 0;
|
|
}
|
|
|
|
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)) {
|
|
dpm_core::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 = dpm_core::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();
|
|
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" */
|