/** * @file context.cpp * @brief Context lifecycle, configuration, and services * * @copyright Copyright (c) 2026 SILO GROUP LLC * @author Chris Punches * * 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 . */ #include "internal/context.hpp" #include "internal/conf.hpp" #include #include namespace fs = std::filesystem; /** * @brief Helpers private to this translation unit * * The built-in defaults and the path normalization applied to the * directories a context resolves. An unnamed namespace gives them * internal linkage, so they are unreachable from the library's other * files and cannot collide with a same-named helper in one of them. */ namespace { /** Configuration directory used when no override is supplied. */ const char* DEFAULT_CONFIG_DIR = "/etc/dpm/conf.d/"; /** Module directory used when neither an override nor configuration names one. */ const char* DEFAULT_MODULE_PATH = "/usr/lib/dpm/modules/"; /** Log file used when configuration enables logging without naming a path. */ const char* DEFAULT_LOG_FILE = "/var/log/dpm/dpm.log"; /** * @brief Appends a trailing slash when one is absent * * Directory paths are stored with a trailing slash so that callers * concatenating a filename onto them produce a valid path without * checking the separator themselves. * * @param s The path to normalize * @return The path, ending in a slash unless it is empty */ std::string with_trailing_slash(std::string s) { if (!s.empty() && s.back() != '/') { s += '/'; } return s; } } // namespace namespace dpm_core { /** * @brief Records a failure reason on the context * * Overwrites whatever reason was recorded previously, so the context * carries the most recent failure and nothing older. */ void set_error(dpm_ctx* ctx, const std::string& msg) { if (ctx) { ctx->last_error = msg; } } } // namespace dpm_core extern "C" { /** * @brief Creates a context * * Reads configuration, resolves the module path and target root, and * initializes logging. Loads no modules. * * Each setting is resolved from the most specific source available. * The module path takes an override first, then the configured * value, then the built-in default; logging reads configuration and * then lets an override replace the level. * * Explicit overrides are validated before anything is allocated, so * a caller that names an unusable path gets NULL rather than a * context that fails later. */ 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, then default. ctx->config_dir = DEFAULT_CONFIG_DIR; if (overrides && overrides->config_dir && *overrides->config_dir) { ctx->config_dir = with_trailing_slash(overrides->config_dir); } // Everything below reads configuration, so it loads first. dpm_core::load_config_dir(ctx); // Logging: defaults, then configuration, then the 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 = dpm_core::log_level_str2enum(v, DPM_LOG_INFO); } if (const char* v = dpm_config_get(ctx, "core", "logging", "write_to_log")) { ctx->write_to_log = dpm_core::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: default, then configuration, then the override. 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, then default. ctx->root = "/"; if (overrides && overrides->root && *overrides->root) { ctx->root = overrides->root; } return ctx; } /** * @brief Releases a context * * Unloads every module the context loaded, then frees the context * itself. Every handle and string the context issued is invalid * afterwards. */ void dpm_close(dpm_ctx* ctx) { if (!ctx) { return; } // A dpm_module destructor does not close its handle, so the // registry is unloaded here and the teardown order stays visible. for (auto& [name, mod] : ctx->modules) { if (mod->handle) { dpm_internal_unload(mod->handle); } } delete ctx; } /** * @brief Reports the module directory this context resolved * * The path was fixed when the context opened, so it reflects the * override, configured value, or default that won at that point. */ const char* dpm_module_path(dpm_ctx* ctx) { if (!ctx) { return nullptr; } return ctx->module_path.c_str(); } /** * @brief Reports the most recent failure recorded on the context * * The reason is replaced by the next failing call, so it describes * the most recent failure and nothing earlier. An empty reason reads * as no failure recorded. */ const char* dpm_last_error(dpm_ctx* ctx) { if (!ctx || ctx->last_error.empty()) { return nullptr; } return ctx->last_error.c_str(); } } /* extern "C" */