#include "kappa/fetch/fetch.hpp" #include "kappa/paths.hpp" #include #include #include #include #include #include #include #include #include #include namespace kappa::fetch { namespace fs = std::filesystem; using namespace std::string_view_literals; static int exec_cmd(const std::vector& argv) { if (argv.empty()) { return -1; } std::vector> argv_storage(argv.size()); std::vector 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& 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> argv_storage(argv.size()); std::vector 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 buf; std::string result; ssize_t n; while ((n = read(pipefd[0], buf.data(), buf.size() - 1)) > 0) { buf[static_cast(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