Files
cargoxx/src/resolver/fuzzy_listing.cpp

194 lines
6.1 KiB
C++

module;
#include <json.hpp>
module cargoxx.resolver;
import std;
import cargoxx.exec;
import cargoxx.util;
namespace cargoxx::resolver {
namespace fs = std::filesystem;
namespace {
auto network_error(std::string msg) -> util::Error {
return util::Error{util::ErrorCode::ResolutionNetworkError, std::move(msg),
"", std::nullopt, std::nullopt};
}
auto fetch_tree_paths(const std::string& url) -> util::Result<std::vector<std::string>> {
auto r = exec::run("curl", {"-fsSL", "--max-time", "20", url},
exec::ExecOptions{
.cwd = {},
.env_overrides = {},
.timeout = std::chrono::seconds{30},
.inherit_stdio = false,
});
if (!r) {
return std::unexpected(r.error());
}
if (r->exit_code != 0) {
return std::unexpected(network_error(std::format(
"curl failed (exit {}): {}", r->exit_code, r->stderr_text)));
}
nlohmann::json j;
try {
j = nlohmann::json::parse(r->stdout_text);
} catch (const nlohmann::json::parse_error& e) {
return std::unexpected(
network_error(std::format("tree listing not valid JSON: {}", e.what())));
}
if (!j.contains("tree") || !j["tree"].is_array()) {
return std::unexpected(network_error("tree listing missing 'tree' array"));
}
std::vector<std::string> out;
for (const auto& entry : j["tree"]) {
if (entry.contains("path") && entry["path"].is_string()) {
out.push_back(entry["path"].get<std::string>());
}
}
return out;
}
auto cache_root() -> fs::path {
if (auto* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
return fs::path{xdg} / "cargoxx";
}
if (auto* home = std::getenv("HOME"); home && *home) {
return fs::path{home} / ".cache" / "cargoxx";
}
return fs::temp_directory_path() / "cargoxx";
}
constexpr auto INDEX_TTL = std::chrono::hours{24};
auto load_or_fetch(const std::string& cache_key, const std::string& url)
-> util::Result<std::vector<std::string>> {
auto path = cache_root() / std::format("{}-index.txt", cache_key);
std::error_code ec;
if (fs::exists(path, ec) && !ec) {
auto age = std::chrono::system_clock::now() -
std::chrono::file_clock::to_sys(fs::last_write_time(path));
if (age < INDEX_TTL) {
std::ifstream in{path};
if (in) {
std::vector<std::string> out;
std::string line;
while (std::getline(in, line)) {
if (!line.empty()) {
out.push_back(std::move(line));
}
}
if (!out.empty()) {
return out;
}
}
}
}
auto fresh = fetch_tree_paths(url);
if (!fresh) {
return std::unexpected(fresh.error());
}
fs::create_directories(path.parent_path(), ec);
if (std::ofstream out{path}; out) {
for (const auto& p : *fresh) {
out << p << '\n';
}
}
return fresh;
}
// Levenshtein top-k filter with a max-distance gate of ⌈len/4⌉ (min 1).
auto top_fuzzy(std::string_view query, const std::vector<std::string>& corpus,
std::size_t k) -> std::vector<std::string> {
const std::size_t cap = std::max<std::size_t>(1, (query.size() + 3) / 4);
struct Scored {
std::size_t dist;
std::string name;
};
std::vector<Scored> scored;
scored.reserve(corpus.size());
for (const auto& c : corpus) {
auto d = util::levenshtein(query, c);
if (d <= cap) {
scored.push_back({d, c});
}
}
std::ranges::sort(scored, [](const auto& a, const auto& b) {
if (a.dist != b.dist) {
return a.dist < b.dist;
}
return a.name < b.name;
});
std::vector<std::string> out;
for (std::size_t i = 0; i < std::min(k, scored.size()); ++i) {
out.push_back(std::move(scored[i].name));
}
return out;
}
constexpr auto FUZZY_K = std::size_t{3};
} // namespace
auto conan_probe_fuzzy(const std::string& name) -> util::Result<ConanRecipe> {
if (auto exact = conan_probe(name); exact) {
return exact;
}
auto index = load_or_fetch(
"conan",
"https://api.github.com/repos/conan-io/conan-center-index/git/trees/master:recipes");
if (!index) {
return std::unexpected(util::Error{
util::ErrorCode::ResolutionUnknownPackage,
std::format("no Conan recipe for '{}' and index fetch failed", name),
"", std::nullopt, std::nullopt,
});
}
auto candidates = top_fuzzy(name, *index, FUZZY_K);
for (const auto& cand : candidates) {
if (auto r = conan_probe(cand); r) {
return r;
}
}
return std::unexpected(util::Error{
util::ErrorCode::ResolutionUnknownPackage,
std::format("no Conan recipe matches '{}' (tried exact + fuzzy top-{})",
name, FUZZY_K),
"", std::nullopt, std::nullopt,
});
}
auto vcpkg_probe_fuzzy(const std::string& name) -> util::Result<VcpkgRecipe> {
if (auto exact = vcpkg_probe(name); exact) {
return exact;
}
auto index = load_or_fetch(
"vcpkg",
"https://api.github.com/repos/microsoft/vcpkg/git/trees/master:ports");
if (!index) {
return std::unexpected(util::Error{
util::ErrorCode::ResolutionUnknownPackage,
std::format("no vcpkg port for '{}' and index fetch failed", name),
"", std::nullopt, std::nullopt,
});
}
auto candidates = top_fuzzy(name, *index, FUZZY_K);
for (const auto& cand : candidates) {
if (auto r = vcpkg_probe(cand); r) {
return r;
}
}
return std::unexpected(util::Error{
util::ErrorCode::ResolutionUnknownPackage,
std::format("no vcpkg port matches '{}' (tried exact + fuzzy top-{})",
name, FUZZY_K),
"", std::nullopt, std::nullopt,
});
}
} // namespace cargoxx::resolver