Store the version in version.cpp and group the library under src/core
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.
This commit is contained in:
265
src/core/modules.cpp
Normal file
265
src/core/modules.cpp
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @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" */
|
||||
Reference in New Issue
Block a user