This commit is contained in:
atagen 2026-02-25 13:16:02 +11:00
commit 807c3b0094
11 changed files with 533 additions and 0 deletions

99
src/config.cc Normal file
View file

@ -0,0 +1,99 @@
#include "config.hh"
#include <cstdlib>
#include <fstream>
#include <nlohmann/json.hpp>
namespace nix::shorturl {
std::filesystem::path Config::defaultConfigPath()
{
if (auto env = std::getenv("NIX_SHORTURL_CONFIG"))
return env;
if (auto xdg = std::getenv("XDG_CONFIG_HOME"))
return std::filesystem::path(xdg) / "nix" / "shorturl.json";
if (auto home = std::getenv("HOME"))
return std::filesystem::path(home) / ".config" / "nix" / "shorturl.json";
return {};
}
std::optional<Config> Config::load(const std::filesystem::path & path)
{
if (path.empty() || !std::filesystem::exists(path))
return std::nullopt;
std::ifstream file(path);
if (!file.is_open())
return std::nullopt;
nlohmann::json json;
try {
file >> json;
} catch (const nlohmann::json::parse_error &) {
return std::nullopt;
}
Config config;
if (!json.contains("schemes") || !json["schemes"].is_object())
return std::nullopt;
for (auto & [name, value] : json["schemes"].items()) {
if (!value.is_object() || !value.contains("template") || !value["template"].is_string())
continue;
config.schemes[name] = SchemeConfig{value["template"].get<std::string>()};
}
return config;
}
std::optional<Config> Config::load()
{
return load(defaultConfigPath());
}
bool Config::hasScheme(const std::string & scheme) const
{
return schemes.count(scheme) > 0;
}
std::optional<std::string> Config::expand(
const std::string & scheme,
const std::string & path,
const std::map<std::string, std::string, std::less<>> & query,
const std::string & fragment) const
{
auto it = schemes.find(scheme);
if (it == schemes.end())
return std::nullopt;
std::string result = it->second.templateStr;
// Replace {path} with the actual path
std::string placeholder = "{path}";
auto pos = result.find(placeholder);
if (pos != std::string::npos)
result.replace(pos, placeholder.size(), path);
// Append query parameters
if (!query.empty()) {
bool first = (result.find('?') == std::string::npos);
for (auto & [k, v] : query) {
result += first ? '?' : '&';
result += k + "=" + v;
first = false;
}
}
// Append fragment
if (!fragment.empty()) {
result += "#" + fragment;
}
return result;
}
} // namespace nix::shorturl

35
src/config.hh Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include <filesystem>
#include <functional>
#include <map>
#include <optional>
#include <string>
namespace nix::shorturl {
struct SchemeConfig {
std::string templateStr;
};
struct Config {
std::map<std::string, SchemeConfig> schemes;
static std::filesystem::path defaultConfigPath();
static std::optional<Config> load(const std::filesystem::path & path);
static std::optional<Config> load();
/**
* Expand a short URL scheme + path into a full URL string.
* Query parameters and fragment are appended to the expanded URL.
*/
std::optional<std::string> expand(
const std::string & scheme,
const std::string & path,
const std::map<std::string, std::string, std::less<>> & query = {},
const std::string & fragment = {}) const;
bool hasScheme(const std::string & scheme) const;
};
} // namespace nix::shorturl

8
src/plugin.cc Normal file
View file

@ -0,0 +1,8 @@
#include "shorturl-scheme.hh"
#include <nix/util/types.hh>
static auto reg = nix::OnStartup([] {
nix::fetchers::registerInputScheme(
std::make_shared<nix::shorturl::ShortUrlInputScheme>());
});

97
src/shorturl-scheme.cc Normal file
View file

@ -0,0 +1,97 @@
#include "shorturl-scheme.hh"
#include <nix/fetchers/attrs.hh>
#include <nix/util/url.hh>
namespace nix::shorturl {
ShortUrlInputScheme::ShortUrlInputScheme()
{
if (auto cfg = Config::load())
config = std::move(*cfg);
}
std::string_view ShortUrlInputScheme::schemeName() const
{
return "shorturl";
}
std::optional<nix::fetchers::Input>
ShortUrlInputScheme::inputFromURL(
const nix::fetchers::Settings & settings,
const ParsedURL & url,
bool requireTree) const
{
if (!config.hasScheme(url.scheme))
return std::nullopt;
auto expanded = config.expand(url.scheme, url.path, url.query, url.fragment);
if (!expanded)
return std::nullopt;
// Parse expanded URL and check it doesn't resolve to another shorturl scheme
auto parsedExpanded = nix::parseURL(*expanded);
if (config.hasScheme(parsedExpanded.scheme))
throw nix::Error("shorturl scheme '%s' expands to another shorturl scheme '%s' (circular reference)",
url.scheme, parsedExpanded.scheme);
// Delegate to the real scheme via Input::fromURL
return nix::fetchers::Input::fromURL(settings, *expanded, requireTree);
}
std::optional<nix::fetchers::Input>
ShortUrlInputScheme::inputFromAttrs(
const nix::fetchers::Settings & /* settings */,
const nix::fetchers::Attrs & /* attrs */) const
{
// Lock files store the real type (e.g., "github"), not "shorturl".
// We never match from attrs — this ensures lock files work without the plugin.
return std::nullopt;
}
StringSet ShortUrlInputScheme::allowedAttrs() const
{
return {};
}
std::pair<nix::ref<SourceAccessor>, nix::fetchers::Input>
ShortUrlInputScheme::getAccessor(nix::ref<Store> store, const nix::fetchers::Input & input) const
{
// Reconstruct the real Input from the stored attrs (which have the real type).
// Call the real scheme's getAccessor directly to avoid double fingerprint assignment
// in Input::getAccessorUnchecked.
auto attrs = input.toAttrs();
auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs));
return realInput.scheme->getAccessor(store, realInput);
}
bool ShortUrlInputScheme::isLocked(const nix::fetchers::Input & input) const
{
auto attrs = input.toAttrs();
auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs));
return realInput.isLocked();
}
std::optional<std::string>
ShortUrlInputScheme::getFingerprint(nix::ref<Store> store, const nix::fetchers::Input & input) const
{
auto attrs = input.toAttrs();
auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs));
return realInput.getFingerprint(store);
}
ParsedURL ShortUrlInputScheme::toURL(const nix::fetchers::Input & input) const
{
auto attrs = input.toAttrs();
auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs));
return realInput.toURL();
}
bool ShortUrlInputScheme::isDirect(const nix::fetchers::Input & input) const
{
auto attrs = input.toAttrs();
auto realInput = nix::fetchers::Input::fromAttrs(*input.settings, std::move(attrs));
return realInput.isDirect();
}
} // namespace nix::shorturl

43
src/shorturl-scheme.hh Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "config.hh"
#include <nix/fetchers/fetchers.hh>
namespace nix::shorturl {
struct ShortUrlInputScheme : nix::fetchers::InputScheme
{
Config config;
ShortUrlInputScheme();
std::string_view schemeName() const override;
std::optional<nix::fetchers::Input>
inputFromURL(
const nix::fetchers::Settings & settings,
const ParsedURL & url,
bool requireTree) const override;
std::optional<nix::fetchers::Input>
inputFromAttrs(
const nix::fetchers::Settings & settings,
const nix::fetchers::Attrs & attrs) const override;
StringSet allowedAttrs() const override;
std::pair<nix::ref<SourceAccessor>, nix::fetchers::Input>
getAccessor(nix::ref<Store> store, const nix::fetchers::Input & input) const override;
bool isLocked(const nix::fetchers::Input & input) const override;
std::optional<std::string>
getFingerprint(nix::ref<Store> store, const nix::fetchers::Input & input) const override;
ParsedURL toURL(const nix::fetchers::Input & input) const override;
bool isDirect(const nix::fetchers::Input & input) const override;
};
} // namespace nix::shorturl