Initial commit: DPM core library, CLI, bundled info module, and tests
This commit is contained in:
86
src/bundled-modules/info/include/commands.hpp
Normal file
86
src/bundled-modules/info/include/commands.hpp
Normal 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);
|
||||
147
src/bundled-modules/info/info.cpp
Normal file
147
src/bundled-modules/info/info.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
185
src/bundled-modules/info/src/commands.cpp
Normal file
185
src/bundled-modules/info/src/commands.cpp
Normal 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
265
src/cli/dpm.cpp
Normal 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
336
src/context.cpp
Normal 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
18
src/libdpm-core.map
Normal 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
370
src/modules.cpp
Normal 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
83
src/version.cpp
Normal 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
|
||||
Reference in New Issue
Block a user