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

View File

@@ -0,0 +1,86 @@
/**
* @file commands.hpp
* @brief Command handlers for the info module
*
* @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/>.
*/
#pragma once
#include <dpm/core.h>
/**
* @enum Command
* @brief Commands supported by the info module
*/
enum Command {
CMD_UNKNOWN, /**< Unknown or unsupported command */
CMD_HELP, /**< Display help information */
CMD_VERSION, /**< Display core and module versions */
CMD_SYSTEM, /**< Display system information */
CMD_CONFIG, /**< Display configuration information */
};
/**
* @brief Parses a command string into a Command enum value
*
* @param cmd_str The command string to parse (NULL/empty means help)
* @return The corresponding Command enum value
*/
Command parse_command(const char* cmd_str);
/**
* @brief Displays the info module's help text
*
* @param ctx Host core context
* @return 0 on success
*/
int cmd_help(dpm_ctx* ctx);
/**
* @brief Reports the running core version and the info module version
*
* @param ctx Host core context
* @return 0 on success
*/
int cmd_version(dpm_ctx* ctx);
/**
* @brief Reports operating system and architecture information
*
* @param ctx Host core context
* @return 0 on success
*/
int cmd_system(dpm_ctx* ctx);
/**
* @brief Reports core configuration as resolved by the running context
*
* @param ctx Host core context
* @return 0 on success
*/
int cmd_config(dpm_ctx* ctx);
/**
* @brief Reports an unrecognized command
*
* @param ctx Host core context
* @param command The unrecognized command string
* @return 1 to indicate failure
*/
int cmd_unknown(dpm_ctx* ctx, const char* command);

View File

@@ -0,0 +1,147 @@
/**
* @file info.cpp
* @brief The info module: contract symbols and command routing
*
* Bundles with core; used for testing and reporting functionality of
* core. Implements the full DPM module contract: the five reserved
* symbols plus a manifest-declared API table.
*
* @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 "commands.hpp"
#define INFO_MODULE_VERSION "0.1.0"
#define INFO_CORE_MIN "0.1.0"
/* ------------------------------------------------------------------ */
/* info_api_v1 — typed access surface */
/* ------------------------------------------------------------------ */
/**
* @brief Version 1 API table for the info module
*
* Lets consumers exercise core's typed-access path (require → get_api
* → call) end to end, which is part of this module's purpose of
* testing core functionality.
*/
struct info_api_v1_s {
dpm_api_table_header hdr;
const char* (*version)(void); /**< info module version */
const char* (*description)(void); /**< info module description */
};
static const char* api_version(void)
{
return INFO_MODULE_VERSION;
}
static const char* api_description(void)
{
return "Reports and tests DPM core functionality.";
}
extern "C" {
extern const struct info_api_v1_s info_api_v1;
const struct info_api_v1_s info_api_v1 = {
{ DPM_API_TABLE_MAGIC, sizeof(struct info_api_v1_s) },
api_version,
api_description,
};
}
/* ------------------------------------------------------------------ */
/* Reserved contract symbols */
/* ------------------------------------------------------------------ */
/**
* @brief Returns the module's own version
*/
extern "C" const char* dpm_module_version(void)
{
return INFO_MODULE_VERSION;
}
/**
* @brief Returns the module's one-line description
*/
extern "C" const char* dpm_module_description(void)
{
return "Reports and tests DPM core functionality.";
}
/**
* @brief Returns the minimum core version this module supports
*/
extern "C" const char* dpm_module_core_min(void)
{
return INFO_CORE_MIN;
}
/**
* @brief Declares the module's entire functional surface
*/
extern "C" const dpm_manifest* dpm_module_manifest(void)
{
static const dpm_manifest_entry entries[] = {
{ "info", 1, "info_api_v1" },
};
static const dpm_manifest manifest = { 1, entries };
return &manifest;
}
/**
* @brief Generic command entry point
*
* Routes the command to the appropriate handler. NULL or empty
* command behaves as help.
*
* @param ctx Host core context (reaches core services)
* @param command The command string to execute
* @param argc Number of arguments
* @param argv Array of argument strings (argv[0] is the command)
* @return 0 on success, non-zero on failure
*/
extern "C" int dpm_module_execute(dpm_ctx* ctx, const char* command,
int argc, char** argv)
{
(void)argc;
(void)argv;
dpm_log(ctx, DPM_LOG_DEBUG, "Info module execution started.");
Command cmd = parse_command(command);
switch (cmd) {
case CMD_VERSION:
return cmd_version(ctx);
case CMD_SYSTEM:
return cmd_system(ctx);
case CMD_CONFIG:
return cmd_config(ctx);
case CMD_HELP:
return cmd_help(ctx);
case CMD_UNKNOWN:
default:
return cmd_unknown(ctx, command);
}
}

View File

@@ -0,0 +1,185 @@
/**
* @file commands.cpp
* @brief Implementation of the info module command handlers
*
* Reports on, and thereby exercises, core functionality: version,
* system details, and configuration as resolved by the host context.
*
* @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 "commands.hpp"
#include <cstring>
#include <fstream>
#include <string>
#include <sys/utsname.h>
#define INFO_VERSION "0.1.0"
namespace {
std::string detect_architecture()
{
struct utsname system_info;
if (uname(&system_info) == -1) {
return "Unknown";
}
return system_info.machine;
}
std::string detect_os()
{
struct utsname system_info;
if (uname(&system_info) == -1) {
return "Unknown";
}
std::string os = system_info.sysname;
if (os == "Linux") {
std::ifstream os_release("/etc/os-release");
if (os_release.is_open()) {
std::string line;
std::string distro_name;
std::string distro_version;
while (std::getline(os_release, line)) {
if (line.rfind("NAME=", 0) == 0) {
distro_name = line.substr(5);
if (distro_name.size() >= 2 &&
distro_name.front() == '"' &&
distro_name.back() == '"') {
distro_name =
distro_name.substr(1, distro_name.length() - 2);
}
}
if (line.rfind("VERSION_ID=", 0) == 0) {
distro_version = line.substr(11);
if (distro_version.size() >= 2 &&
distro_version.front() == '"' &&
distro_version.back() == '"') {
distro_version =
distro_version.substr(1,
distro_version.length() - 2);
}
}
}
if (!distro_name.empty()) {
os += " (" + distro_name;
if (!distro_version.empty()) {
os += " " + distro_version;
}
os += ")";
}
}
}
return os;
}
} // namespace
Command parse_command(const char* cmd_str)
{
if (cmd_str == nullptr || std::strlen(cmd_str) == 0) {
return CMD_HELP;
}
if (std::strcmp(cmd_str, "help") == 0) {
return CMD_HELP;
}
if (std::strcmp(cmd_str, "version") == 0) {
return CMD_VERSION;
}
if (std::strcmp(cmd_str, "system") == 0) {
return CMD_SYSTEM;
}
if (std::strcmp(cmd_str, "config") == 0) {
return CMD_CONFIG;
}
return CMD_UNKNOWN;
}
int cmd_help(dpm_ctx* ctx)
{
dpm_log(ctx, DPM_LOG_INFO, "DPM Info Module - Reports and tests DPM core functionality.");
dpm_log(ctx, DPM_LOG_INFO, "");
dpm_log(ctx, DPM_LOG_INFO, "Available commands:");
dpm_log(ctx, DPM_LOG_INFO, "");
dpm_log(ctx, DPM_LOG_INFO, " version - Display core and module version information");
dpm_log(ctx, DPM_LOG_INFO, " system - Display system information");
dpm_log(ctx, DPM_LOG_INFO, " config - Display configuration as resolved by core");
dpm_log(ctx, DPM_LOG_INFO, " help - Display this help message");
dpm_log(ctx, DPM_LOG_INFO, "");
return 0;
}
int cmd_version(dpm_ctx* ctx)
{
std::string core_msg = "libdpm-core Version: ";
core_msg += dpm_core_version();
dpm_log(ctx, DPM_LOG_INFO, core_msg.c_str());
std::string info_msg = "Info Module Version: ";
info_msg += INFO_VERSION;
dpm_log(ctx, DPM_LOG_INFO, info_msg.c_str());
return 0;
}
int cmd_system(dpm_ctx* ctx)
{
dpm_log(ctx, DPM_LOG_INFO, "System Information:");
std::string os_msg = " OS: ";
os_msg += detect_os();
dpm_log(ctx, DPM_LOG_INFO, os_msg.c_str());
std::string arch_msg = " Architecture: ";
arch_msg += detect_architecture();
dpm_log(ctx, DPM_LOG_INFO, arch_msg.c_str());
return 0;
}
int cmd_config(dpm_ctx* ctx)
{
dpm_log(ctx, DPM_LOG_INFO, "Configuration Information:");
const char* configured_path = dpm_config_get(ctx, "core", "modules", "path");
std::string configured_msg = " Configured module path (core.conf): ";
configured_msg += configured_path ? configured_path : "not configured";
dpm_log(ctx, DPM_LOG_INFO, configured_msg.c_str());
const char* active_path = dpm_module_path(ctx);
std::string active_msg = " Active module path: ";
active_msg += active_path ? active_path : "unknown";
dpm_log(ctx, DPM_LOG_INFO, active_msg.c_str());
return 0;
}
int cmd_unknown(dpm_ctx* ctx, const char* command)
{
std::string msg = "Unknown command: ";
msg += (command ? command : "");
dpm_log(ctx, DPM_LOG_WARN, msg.c_str());
dpm_log(ctx, DPM_LOG_WARN, "Run 'dpm info help' for a list of available commands");
return 1;
}

265
src/cli/dpm.cpp Normal file
View File

@@ -0,0 +1,265 @@
/**
* @file dpm.cpp
* @brief The dpm CLI: argument parsing and printing over libdpm-core
*
* Thin client of libdpm-core. Every field of the dpm_open_overrides
* struct is exposed as a flag; subcommands map onto library calls; the
* command surface is exactly the set of loadable modules.
*
* @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 <dpm/core.h>
#include <cstdio>
#include <cstring>
#include <string>
#include <strings.h>
#include <vector>
namespace {
/**
* @brief Prints the CLI usage message
*/
void show_help()
{
std::printf(
"Usage: dpm [options] [module] [module args...]\n"
"\n"
"Options:\n"
" -c, --config-dir PATH Configuration directory (default /etc/dpm/conf.d/)\n"
" -m, --module-path PATH Module directory (overrides configuration)\n"
" -r, --root PATH Target root for package operations (default /)\n"
" -L, --log-level LEVEL FATAL, ERROR, WARN, INFO, or DEBUG\n"
" -l, --list-modules List available modules\n"
" -h, --help Show this help message\n"
"\n"
"For module-specific help, use: dpm <module> help\n");
}
/**
* @brief Parses a log level name
*
* @param name The level name (case-insensitive)
* @return The DPM_LOG_* level, or -1 if unrecognized
*/
int level_from_name(const char* name)
{
if (strcasecmp(name, "FATAL") == 0) { return DPM_LOG_FATAL; }
if (strcasecmp(name, "ERROR") == 0) { return DPM_LOG_ERROR; }
if (strcasecmp(name, "WARN") == 0) { return DPM_LOG_WARN; }
if (strcasecmp(name, "INFO") == 0) { return DPM_LOG_INFO; }
if (strcasecmp(name, "DEBUG") == 0) { return DPM_LOG_DEBUG; }
return -1;
}
/**
* @brief Matches an argument against an option's short and long forms
*
* Recognizes the separate-value forms and the --option=value form.
*
* @param arg The command-line argument to test
* @param short_form The option's short form (e.g. "-c")
* @param long_form The option's long form (e.g. "--config-dir")
* @param inline_value Receives the text after '=' for the inline
* form, or NULL when the value is in the next
* argument
* @return true if the argument is this option
*/
bool option_matches(const char* arg, const char* short_form,
const char* long_form, const char** inline_value)
{
if (std::strcmp(arg, short_form) == 0 ||
std::strcmp(arg, long_form) == 0) {
*inline_value = nullptr;
return true;
}
size_t len = std::strlen(long_form);
if (std::strncmp(arg, long_form, len) == 0 && arg[len] == '=') {
*inline_value = arg + len + 1;
return true;
}
return false;
}
/**
* @brief Prints the table of available modules
*
* @param ctx The core context
* @return 0 on success, 1 on failure
*/
int list_modules(dpm_ctx* ctx)
{
dpm_cursor* cur = dpm_list_modules(ctx);
if (!cur) {
const char* err = dpm_last_error(ctx);
std::fprintf(stderr, "dpm: %s\n", err ? err : "module listing failed");
return 1;
}
std::vector<dpm_module_info> infos;
dpm_module_info info;
while (dpm_cursor_next(cur, &info) == 0) {
infos.push_back(info);
}
dpm_cursor_free(cur);
if (infos.empty()) {
std::printf("No valid modules found in %s\n", dpm_module_path(ctx));
return 0;
}
size_t name_w = std::strlen("MODULE");
size_t version_w = std::strlen("VERSION");
for (const auto& i : infos) {
name_w = std::max(name_w, std::strlen(i.name));
version_w = std::max(version_w, std::strlen(i.version));
}
std::printf("Available DPM modules:\n\n");
std::printf("%-*s %-*s %s\n",
static_cast<int>(name_w), "MODULE",
static_cast<int>(version_w), "VERSION",
"DESCRIPTION");
for (const auto& i : infos) {
std::printf("%-*s %-*s %s\n",
static_cast<int>(name_w), i.name,
static_cast<int>(version_w), i.version,
i.description);
}
std::printf("\nUse 'dpm <module> help' for module-specific help.\n");
return 0;
}
} // namespace
int main(int argc, char** argv)
{
dpm_open_overrides overrides = {nullptr, nullptr, nullptr, -1};
bool list = false;
/* Holders for --opt=value forms. */
std::string config_dir;
std::string module_path;
std::string root;
int i = 1;
for (; i < argc; i++) {
const char* arg = argv[i];
if (arg[0] != '-') {
break;
}
/* Yields the option's value: the inline "=value" text if
present, otherwise the next argument. Prints an error and
returns nullptr when no value exists. */
const char* inline_value = nullptr;
auto take_value = [&]() -> const char* {
if (inline_value) {
return inline_value;
}
if (i + 1 < argc) {
return argv[++i];
}
std::fprintf(stderr, "dpm: option '%s' requires a value\n", arg);
return nullptr;
};
if (option_matches(arg, "-c", "--config-dir", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
config_dir = v;
overrides.config_dir = config_dir.c_str();
} else if (option_matches(arg, "-m", "--module-path", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
module_path = v;
overrides.module_path = module_path.c_str();
} else if (option_matches(arg, "-r", "--root", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
root = v;
overrides.root = root.c_str();
} else if (option_matches(arg, "-L", "--log-level", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
int level = level_from_name(v);
if (level < 0) {
std::fprintf(stderr,
"dpm: unknown log level '%s' (use FATAL, ERROR, "
"WARN, INFO, or DEBUG)\n", v);
return 1;
}
overrides.log_level = level;
} else if (std::strcmp(arg, "-l") == 0 ||
std::strcmp(arg, "--list-modules") == 0) {
list = true;
} else if (std::strcmp(arg, "-h") == 0 ||
std::strcmp(arg, "--help") == 0) {
show_help();
return 0;
} else {
std::fprintf(stderr, "dpm: unknown option '%s'\n\n", arg);
show_help();
return 1;
}
}
dpm_ctx* ctx = dpm_open(&overrides);
if (!ctx) {
std::fprintf(stderr,
"dpm: initialization failed — check that override paths "
"exist and are readable\n");
return 1;
}
if (list) {
int rc = list_modules(ctx);
dpm_close(ctx);
return rc;
}
if (i >= argc) {
show_help();
dpm_close(ctx);
return 0;
}
const char* module_name = argv[i];
char** module_argv = &argv[i + 1];
int module_argc = argc - i - 1;
dpm_module* mod = dpm_require(ctx, module_name, nullptr);
if (!mod) {
const char* err = dpm_last_error(ctx);
std::fprintf(stderr, "dpm: %s\n",
err ? err : "module could not be loaded");
dpm_close(ctx);
return 1;
}
const char* command = module_argc > 0 ? module_argv[0] : nullptr;
int rc = dpm_execute(ctx, mod, command, module_argc, module_argv);
dpm_close(ctx);
return rc;
}

336
src/context.cpp Normal file
View File

@@ -0,0 +1,336 @@
/**
* @file context.cpp
* @brief Context lifecycle, configuration, logging, and services
*
* @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 <cstdio>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <new>
namespace fs = std::filesystem;
#ifndef DPM_CORE_VERSION_STR
#define DPM_CORE_VERSION_STR "0.0.0"
#endif
namespace {
const char* DEFAULT_CONFIG_DIR = "/etc/dpm/conf.d/";
const char* DEFAULT_MODULE_PATH = "/usr/lib/dpm/modules/";
const char* DEFAULT_LOG_FILE = "/var/log/dpm/dpm.log";
std::string trim(const std::string& s)
{
const char* ws = " \t\r\n\f\v";
size_t start = s.find_first_not_of(ws);
if (start == std::string::npos) {
return "";
}
size_t end = s.find_last_not_of(ws);
return s.substr(start, end - start + 1);
}
std::string with_trailing_slash(std::string s)
{
if (!s.empty() && s.back() != '/') {
s += '/';
}
return s;
}
bool parse_bool(const std::string& v, bool fallback)
{
std::string lower;
for (char c : v) {
lower += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (lower == "true" || lower == "yes" || lower == "1" || lower == "on") {
return true;
}
if (lower == "false" || lower == "no" || lower == "0" || lower == "off") {
return false;
}
return fallback;
}
int level_from_string(const std::string& v, int fallback)
{
if (v == "FATAL") { return DPM_LOG_FATAL; }
if (v == "ERROR") { return DPM_LOG_ERROR; }
if (v == "WARN") { return DPM_LOG_WARN; }
if (v == "INFO") { return DPM_LOG_INFO; }
if (v == "DEBUG") { return DPM_LOG_DEBUG; }
return fallback;
}
const char* level_name(int level)
{
switch (level) {
case DPM_LOG_FATAL: return "FATAL";
case DPM_LOG_ERROR: return "ERROR";
case DPM_LOG_WARN: return "WARN";
case DPM_LOG_INFO: return "INFO";
case DPM_LOG_DEBUG: return "DEBUG";
default: return "UNKNOWN";
}
}
void parse_config_file(dpm_ctx* ctx, const fs::path& file,
const std::string& module_name)
{
std::ifstream in(file);
if (!in.is_open()) {
return;
}
std::string line;
std::string section = "main";
while (std::getline(in, line)) {
line = trim(line);
if (line.empty() || line[0] == '#' || line[0] == ';') {
continue;
}
if (line.front() == '[' && line.back() == ']') {
std::string name = trim(line.substr(1, line.length() - 2));
section = name.empty() ? "main" : name;
continue;
}
size_t eq = line.find('=');
if (eq == std::string::npos) {
continue;
}
std::string key = trim(line.substr(0, eq));
std::string value = trim(line.substr(eq + 1));
if (key.empty()) {
continue;
}
ctx->config[module_name][section][key] = value;
}
}
} // namespace
namespace dpmcore {
void set_error(dpm_ctx* ctx, const std::string& msg)
{
if (ctx) {
ctx->last_error = msg;
}
}
void load_config_dir(dpm_ctx* ctx)
{
std::error_code ec;
if (!fs::is_directory(ctx->config_dir, ec)) {
return;
}
for (const auto& entry : fs::directory_iterator(ctx->config_dir, ec)) {
if (ec) {
break;
}
if (!entry.is_regular_file()) {
continue;
}
if (entry.path().extension() != ".conf") {
continue;
}
parse_config_file(ctx, entry.path(), entry.path().stem().string());
}
}
} // namespace dpmcore
extern "C" {
dpm_ctx* dpm_open(const dpm_open_overrides* overrides)
{
/* Validate explicit overrides before allocating anything. */
if (overrides) {
if (overrides->config_dir && *overrides->config_dir) {
std::error_code ec;
if (!fs::is_directory(overrides->config_dir, ec)) {
return nullptr;
}
}
if (overrides->log_level < -1 || overrides->log_level > DPM_LOG_DEBUG) {
return nullptr;
}
}
dpm_ctx* ctx = new (std::nothrow) dpm_ctx;
if (!ctx) {
return nullptr;
}
/* Config directory: override > default. */
ctx->config_dir = DEFAULT_CONFIG_DIR;
if (overrides && overrides->config_dir && *overrides->config_dir) {
ctx->config_dir = with_trailing_slash(overrides->config_dir);
}
dpmcore::load_config_dir(ctx);
/* Logging: config, then override. */
ctx->log_level = DPM_LOG_INFO;
ctx->write_to_log = false;
ctx->log_file = DEFAULT_LOG_FILE;
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_level")) {
ctx->log_level = level_from_string(v, DPM_LOG_INFO);
}
if (const char* v = dpm_config_get(ctx, "core", "logging", "write_to_log")) {
ctx->write_to_log = parse_bool(v, false);
}
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_file")) {
ctx->log_file = v;
}
if (overrides && overrides->log_level >= 0) {
ctx->log_level = overrides->log_level;
}
/* Module path: override > config > default. */
ctx->module_path = DEFAULT_MODULE_PATH;
if (const char* v = dpm_config_get(ctx, "core", "modules", "path")) {
ctx->module_path = with_trailing_slash(v);
}
if (overrides && overrides->module_path && *overrides->module_path) {
ctx->module_path = with_trailing_slash(overrides->module_path);
}
/* Target root: override > default. */
ctx->root = "/";
if (overrides && overrides->root && *overrides->root) {
ctx->root = overrides->root;
}
return ctx;
}
void dpm_close(dpm_ctx* ctx)
{
if (!ctx) {
return;
}
/* dpm_module destructors do not dlclose; do it here so the
registry teardown order is explicit. */
for (auto& [name, mod] : ctx->modules) {
if (mod->handle) {
dpm_internal_unload(mod->handle);
}
}
delete ctx;
}
const char* dpm_core_version(void)
{
return DPM_CORE_VERSION_STR;
}
const char* dpm_config_get(dpm_ctx* ctx, const char* module,
const char* section, const char* key)
{
if (!ctx || !module || !section || !key) {
return nullptr;
}
auto mod_it = ctx->config.find(module);
if (mod_it == ctx->config.end()) {
return nullptr;
}
auto sec_it = mod_it->second.find(section);
if (sec_it == mod_it->second.end()) {
return nullptr;
}
auto key_it = sec_it->second.find(key);
if (key_it == sec_it->second.end()) {
return nullptr;
}
return key_it->second.c_str();
}
void dpm_log(dpm_ctx* ctx, int level, const char* message)
{
if (!ctx || !message) {
return;
}
if (level < DPM_LOG_FATAL || level > DPM_LOG_DEBUG) {
level = DPM_LOG_INFO;
}
if (level > ctx->log_level) {
return;
}
if (level <= DPM_LOG_WARN) {
std::fprintf(stderr, "%s: %s\n", level_name(level), message);
} else {
std::fprintf(stdout, "%s\n", message);
}
if (ctx->write_to_log) {
std::ofstream out(ctx->log_file, std::ios::app);
if (out.is_open()) {
char stamp[32];
std::time_t now = std::time(nullptr);
std::strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S",
std::localtime(&now));
out << stamp << " [" << level_name(level) << "] "
<< message << "\n";
} else {
/* Log-file trouble never fails the operation; disable and
continue on console alone. */
ctx->write_to_log = false;
}
}
}
const char* dpm_module_path(dpm_ctx* ctx)
{
if (!ctx) {
return nullptr;
}
return ctx->module_path.c_str();
}
const char* dpm_last_error(dpm_ctx* ctx)
{
if (!ctx || ctx->last_error.empty()) {
return nullptr;
}
return ctx->last_error.c_str();
}
} /* extern "C" */

18
src/libdpm-core.map Normal file
View File

@@ -0,0 +1,18 @@
DPM_CORE_1.0 {
global:
dpm_open;
dpm_close;
dpm_require;
dpm_get_api;
dpm_execute;
dpm_list_modules;
dpm_cursor_next;
dpm_cursor_free;
dpm_core_version;
dpm_config_get;
dpm_log;
dpm_module_path;
dpm_last_error;
local:
*;
};

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" */

83
src/version.cpp Normal file
View File

@@ -0,0 +1,83 @@
/**
* @file version.cpp
* @brief X.Y.Z version parsing and comparison
*
* @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/version.hpp"
#include <cctype>
#include <cstdlib>
namespace dpmcore {
bool parse_version(const char* s, long out[3])
{
if (!s || !*s) {
return false;
}
const char* p = s;
for (int i = 0; i < 3; i++) {
if (!std::isdigit(static_cast<unsigned char>(*p))) {
return false;
}
char* end = nullptr;
out[i] = std::strtol(p, &end, 10);
if (out[i] < 0) {
return false;
}
if (i < 2) {
if (*end != '.') {
return false;
}
p = end + 1;
} else {
if (*end != '\0') {
return false;
}
}
}
return true;
}
int compare_versions(const char* a, const char* b)
{
long va[3] = {0, 0, 0};
long vb[3] = {0, 0, 0};
parse_version(a, va);
parse_version(b, vb);
for (int i = 0; i < 3; i++) {
if (va[i] < vb[i]) {
return -1;
}
if (va[i] > vb[i]) {
return 1;
}
}
return 0;
}
} // namespace dpmcore