Portability (Linux distro-agnostic): - Remove hardcoded Clang compiler enforcement; GCC now builds - Add find_package(Threads REQUIRED) for older glibc - Add cmake install() target - FHS 3.0 default root: /kappa -> /usr/local/kappa - fs::path operator/ for all init/bootloader paths (fixes prefix fragility) - Multi-distro zoneinfo search (FHS, NixOS, Guix, alt) - Portable tar extraction (drop GNU-only --no-same-permissions) - Runit enable/disable commands now prefix-aware - --root CLI flag before/after subcommand, lazy directory creation - Shebang constants de-duplicated to types.hpp Correctness (race conditions, UB, corruption): - Fix CWD race in scheduler: per-child chdir() instead of process-global - Fix UB const_cast in exec_cmd/exec_capture: mutable argv buffers - Fix non-atomic installed DB writes: tmp+rename pattern - Fix read_file() no longer calls exit(1), throws instead - Fix silent catch(...) parse errors now print diagnostics - Fix rebuild false positives with config_hash change detection - Fix s6 disable_cmd copy-paste bug (was identical to enable) - Fix runit enable_cmd incomplete, disable_cmd wrong target - Fix dinit env vars: functional env-file + companion .env Quality: - Add -Wall -Wextra -Wpedantic to CMake, fix 2 pre-existing warnings - Move parse_int from error.hpp to parse_util.hpp - Fix hash verification guard checks all three hash types - Check patch return code in fetch.cpp - Add explicit system_dir creation in ensure_directories() - Add resolve to needs_dirs for build_registry() consistency - Update stale /kappa path references in examples - Remove inaccurate -Werror claim in CONTRIBUTING.md - Add build-gcc/ and agent dirs to .gitignore - Suppress clang-tidy portability-avoid-pragma-once - Fix .gitignore /kappa pattern (was matching include/kappa/) - Delete stale vcpkg_installed/ directory 54/54 tests pass. Builds on Clang and GCC with 0 warnings.
239 lines
7.7 KiB
C++
239 lines
7.7 KiB
C++
#include "kappa/fetch/fetch.hpp"
|
|
#include "kappa/paths.hpp"
|
|
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <string>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
namespace kappa::fetch {
|
|
|
|
namespace fs = std::filesystem;
|
|
using namespace std::string_view_literals;
|
|
|
|
static int exec_cmd(const std::vector<std::string>& argv) {
|
|
if (argv.empty()) { return -1; }
|
|
std::vector<std::vector<char>> argv_storage(argv.size());
|
|
std::vector<char*> cargs;
|
|
for (size_t i = 0; i < argv.size(); ++i) {
|
|
argv_storage[i].assign(argv[i].begin(), argv[i].end());
|
|
argv_storage[i].push_back('\0');
|
|
cargs.push_back(argv_storage[i].data());
|
|
}
|
|
cargs.push_back(nullptr);
|
|
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
execvp(cargs[0], cargs.data());
|
|
_exit(127);
|
|
}
|
|
if (pid < 0) { return -1; }
|
|
int status = 0;
|
|
pid_t w;
|
|
do { w = waitpid(pid, &status, 0); } while (w == -1 && errno == EINTR);
|
|
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
|
}
|
|
|
|
static std::string exec_capture(const std::vector<std::string>& argv) {
|
|
int pipefd[2];
|
|
if (pipe(pipefd) != 0) { return ""; }
|
|
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
close(pipefd[0]);
|
|
dup2(pipefd[1], STDOUT_FILENO);
|
|
close(pipefd[1]);
|
|
|
|
std::vector<std::vector<char>> argv_storage(argv.size());
|
|
std::vector<char*> cargs;
|
|
for (size_t i = 0; i < argv.size(); ++i) {
|
|
argv_storage[i].assign(argv[i].begin(), argv[i].end());
|
|
argv_storage[i].push_back('\0');
|
|
cargs.push_back(argv_storage[i].data());
|
|
}
|
|
cargs.push_back(nullptr);
|
|
execvp(cargs[0], cargs.data());
|
|
_exit(127);
|
|
}
|
|
|
|
if (pid < 0) { close(pipefd[0]); close(pipefd[1]); return ""; }
|
|
|
|
close(pipefd[1]);
|
|
std::array<char, 256> buf;
|
|
std::string result;
|
|
ssize_t n;
|
|
while ((n = read(pipefd[0], buf.data(), buf.size() - 1)) > 0) {
|
|
buf[static_cast<std::size_t>(n)] = '\0';
|
|
result += buf.data();
|
|
}
|
|
close(pipefd[0]);
|
|
pid_t w;
|
|
do { w = waitpid(pid, nullptr, 0); } while (w == -1 && errno == EINTR);
|
|
|
|
if (!result.empty() && result.back() == '\n') { result.pop_back(); }
|
|
return result;
|
|
}
|
|
|
|
static std::string interpret_url(const dsl::PackageDef& pkg) {
|
|
auto url = pkg.source;
|
|
for (auto& [from, to] : {
|
|
std::pair{"${name}"sv, std::string_view(pkg.name)},
|
|
std::pair{"${version}"sv, std::string_view(pkg.version)}}) {
|
|
std::size_t pos = 0;
|
|
while ((pos = url.find(from, pos)) != std::string::npos) {
|
|
url.replace(pos, from.size(), to);
|
|
pos += to.size();
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
static bool verify_hash(const fs::path& file, std::string_view algo,
|
|
std::string_view expected) {
|
|
if (expected.empty()) { return true; }
|
|
std::string tool;
|
|
if (algo == "sha256") { tool = "sha256sum"; }
|
|
else if (algo == "sha512") { tool = "sha512sum"; }
|
|
else if (algo == "md5") { tool = "md5sum"; }
|
|
else { return false; }
|
|
|
|
auto output = exec_capture({tool, file.string()});
|
|
if (output.empty()) { return false; }
|
|
|
|
auto space = output.find(' ');
|
|
auto computed = (space != std::string::npos)
|
|
? output.substr(0, space)
|
|
: output;
|
|
return computed == expected;
|
|
}
|
|
|
|
FetchResult fetch(const dsl::PackageDef& pkg) {
|
|
FetchResult result;
|
|
|
|
auto url = interpret_url(pkg);
|
|
if (url.empty()) {
|
|
result.error = "empty source URL";
|
|
return result;
|
|
}
|
|
|
|
auto ext_pos = url.rfind('.');
|
|
auto ext = (ext_pos != std::string::npos)
|
|
? url.substr(ext_pos + 1)
|
|
: std::string{};
|
|
if (ext == "gz" || ext == "xz" || ext == "zst") {
|
|
auto prev = url.rfind('.', ext_pos - 1);
|
|
if (prev != std::string::npos) {
|
|
std::string compound{url.substr(prev + 1,
|
|
ext_pos - prev - 1)};
|
|
ext = compound + "." + ext;
|
|
}
|
|
}
|
|
|
|
auto dest_name = pkg.name + "-" + pkg.version;
|
|
// Sanitize: replace path separators to prevent traversal
|
|
for (auto& c : dest_name) {
|
|
if (c == '/' || c == '\\') c = '_';
|
|
}
|
|
auto cache_path = paths::cache_dir() / (dest_name + "." + ext);
|
|
bool from_cache = false;
|
|
fs::path dest_file;
|
|
if (fs::exists(cache_path)) {
|
|
dest_file = cache_path;
|
|
from_cache = true;
|
|
} else {
|
|
dest_file = fs::path(paths::temp_dir()) / (dest_name + "." + ext);
|
|
}
|
|
result.work_dir = fs::path(paths::temp_dir()) / dest_name;
|
|
|
|
if (ext == "git") {
|
|
int rc = exec_cmd({"git", "clone", url, result.work_dir.string()});
|
|
if (rc != 0) { result.error = "git clone failed"; return result; }
|
|
} else {
|
|
if (!from_cache) {
|
|
fs::create_directories(paths::temp_dir());
|
|
int rc = exec_cmd({"curl", "-L", "-o", dest_file.string(), url});
|
|
if (rc != 0) { result.error = "download failed"; return result; }
|
|
std::error_code ec;
|
|
// Atomic cache write: write to .tmp then rename
|
|
auto cache_tmp = fs::path(cache_path.string() + ".tmp");
|
|
fs::copy(dest_file, cache_tmp, ec);
|
|
if (!ec) {
|
|
fs::rename(cache_tmp, cache_path, ec);
|
|
}
|
|
}
|
|
|
|
bool verified = false;
|
|
for (auto algo : {"sha512", "sha256", "md5"}) {
|
|
std::string_view expected;
|
|
if (std::string_view(algo) == "sha512") { expected = pkg.sha512; }
|
|
else if (std::string_view(algo) == "sha256") { expected = pkg.sha256; }
|
|
else { expected = pkg.md5; }
|
|
|
|
if (!expected.empty()) {
|
|
if (verify_hash(dest_file, algo, expected)) {
|
|
verified = true;
|
|
} else {
|
|
result.error = std::format("{} mismatch", algo);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
if (!verified && (!pkg.sha256.empty() || !pkg.sha512.empty() || !pkg.md5.empty())) {
|
|
result.error = "hash verification failed";
|
|
return result;
|
|
}
|
|
|
|
auto extract_cmd = std::string{"tar"};
|
|
if (ext == "zip") {
|
|
int rc2 = exec_cmd({"unzip", "-o", dest_file.string(),
|
|
"-d", result.work_dir.string()});
|
|
if (rc2 != 0) { result.error = "extraction failed"; return result; }
|
|
} else {
|
|
int rc2 = exec_cmd({"tar", "xf", dest_file.string(),
|
|
"-C", paths::temp_dir().string()});
|
|
if (rc2 != 0) { result.error = "extraction failed"; return result; }
|
|
}
|
|
|
|
if (!from_cache) {
|
|
fs::remove(dest_file);
|
|
}
|
|
}
|
|
|
|
for (auto& patch : pkg.patches) {
|
|
auto patch_path = patch.url;
|
|
auto patch_file = patch_path;
|
|
if (patch_path.starts_with("http")) {
|
|
auto local = fs::path(paths::temp_dir())
|
|
/ fs::path(patch_path).filename();
|
|
int rc = exec_cmd({"curl", "-L", "-o", local.string(),
|
|
patch_path});
|
|
if (rc != 0) { continue; }
|
|
patch_file = local.string();
|
|
}
|
|
|
|
if (!patch.sha256.empty()) {
|
|
if (!verify_hash(patch_file, "sha256", patch.sha256)) {
|
|
result.error = "patch hash mismatch: " + patch.url;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
int prc = exec_cmd({"patch", "-p" + std::to_string(patch.level),
|
|
"-d", result.work_dir.string(), "-i", patch_file});
|
|
if (prc != 0) {
|
|
result.error = "patch failed: " + patch.url;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
} // namespace kappa::fetch
|