#include "gemini_url.h" #include #include #include #include #include "storage_paths.h" namespace roro::gemini { namespace { // RFC 3986, appendix B: ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? struct Parts { std::string scheme, authority, path, query, fragment; bool hasScheme = false, hasAuthority = false, hasQuery = false, hasFragment = false; }; Parts split(const std::string& s) { Parts p; size_t i = 0; size_t colon = s.find(':'); size_t stop = s.find_first_of("/?#"); if (colon != std::string::npos && colon > 0 && (stop == std::string::npos || colon < stop)) { p.scheme = s.substr(0, colon); p.hasScheme = true; i = colon + 1; } if (s.compare(i, 2, "//") == 0) { size_t end = s.find_first_of("/?#", i + 2); if (end == std::string::npos) end = s.size(); p.authority = s.substr(i + 2, end - i - 2); p.hasAuthority = true; i = end; } size_t end = s.find_first_of("?#", i); if (end == std::string::npos) end = s.size(); p.path = s.substr(i, end - i); i = end; if (i < s.size() && s[i] == '?') { end = s.find('#', i); if (end == std::string::npos) end = s.size(); p.query = s.substr(i + 1, end - i - 1); p.hasQuery = true; i = end; } if (i < s.size() && s[i] == '#') { p.fragment = s.substr(i + 1); p.hasFragment = true; } return p; } std::string lower(std::string s) { for (auto& c : s) c = static_cast(std::tolower(static_cast(c))); return s; } // RFC 3986, section 5.2.4. std::string removeDotSegments(std::string in) { std::string out; while (!in.empty()) { if (in.compare(0, 3, "../") == 0) in.erase(0, 3); else if (in.compare(0, 2, "./") == 0) in.erase(0, 2); else if (in.compare(0, 3, "/./") == 0) in.replace(0, 3, "/"); else if (in == "/.") in = "/"; else if (in.compare(0, 4, "/../") == 0 || in == "/..") { in = in == "/.." ? "/" : in.substr(3); size_t slash = out.rfind('/'); out.erase(slash == std::string::npos ? 0 : slash); } else if (in == "." || in == "..") in.clear(); else { size_t next = in.find('/', in[0] == '/' ? 1 : 0); if (next == std::string::npos) next = in.size(); out += in.substr(0, next); in.erase(0, next); } } return out; } // RFC 3986, section 5.2.3. std::string merge(const Parts& base, const std::string& refPath) { if (base.hasAuthority && base.path.empty()) return "/" + refPath; size_t slash = base.path.rfind('/'); return slash == std::string::npos ? refPath : base.path.substr(0, slash + 1) + refPath; } // RFC 3986, section 5.3. std::string recompose(const Parts& p) { std::string out; if (p.hasScheme) out += p.scheme + ":"; if (p.hasAuthority) out += "//" + p.authority; out += p.path; if (p.hasQuery) out += "?" + p.query; if (p.hasFragment) out += "#" + p.fragment; return out; } } // namespace bool parseUrl(const std::string& text, Url& out) { Parts p = split(text); if (!p.hasScheme || !p.hasAuthority) return false; out = Url(); out.scheme = lower(p.scheme); out.authority = p.authority; std::string hostPort = p.authority.substr(p.authority.find('@') == std::string::npos ? 0 : p.authority.find('@') + 1); size_t colon = hostPort.rfind(':'); if (colon != std::string::npos && hostPort.find(']', colon) == std::string::npos) { out.port = std::atoi(hostPort.c_str() + colon + 1); hostPort = hostPort.substr(0, colon); } out.host = lower(hostPort); if (out.host.empty()) return false; out.path = p.path; out.query = p.query; out.fragment = p.fragment; out.hasAuthority = true; out.hasQuery = p.hasQuery; out.hasFragment = p.hasFragment; return true; } bool isGemini(const std::string& url) { Parts p = split(url); return p.hasScheme && lower(p.scheme) == "gemini"; } std::string resolve(const std::string& baseText, const std::string& refText) { Parts base = split(baseText), r = split(refText), t; if (r.hasScheme) { t = r; t.path = removeDotSegments(r.path); } else { if (r.hasAuthority) { t.authority = r.authority; t.hasAuthority = true; t.path = removeDotSegments(r.path); t.query = r.query; t.hasQuery = r.hasQuery; } else { if (r.path.empty()) { t.path = base.path; t.query = r.hasQuery ? r.query : base.query; t.hasQuery = r.hasQuery || base.hasQuery; } else { t.path = removeDotSegments(r.path[0] == '/' ? r.path : merge(base, r.path)); t.query = r.query; t.hasQuery = r.hasQuery; } t.authority = base.authority; t.hasAuthority = base.hasAuthority; } t.scheme = base.scheme; t.hasScheme = base.hasScheme; } t.fragment = r.fragment; t.hasFragment = r.hasFragment; return recompose(t); } std::string requestUrl(const std::string& url) { Url u; if (!parseUrl(url, u)) return url; std::string out = u.scheme + "://" + u.host; if (u.port > 0 && u.port != 1965) out += ":" + std::to_string(u.port); out += u.path.empty() ? "/" : u.path; if (u.hasQuery) out += "?" + u.query; return out; } std::string encodeQuery(const std::string& text) { std::string out; for (unsigned char c : text) { if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') out += static_cast(c); else { char buf[4]; std::snprintf(buf, sizeof buf, "%%%02X", c); out += buf; } } return out; } std::string savedPath(const std::string& url) { Url u; if (!parseUrl(url, u)) return "/gemini/saved/unknown.gmi"; std::string out = "/gemini/saved/" + storage::sanitize(u.host + (u.port > 0 && u.port != 1965 ? "_" + std::to_string(u.port) : "")); std::string path = u.path.empty() ? "/" : u.path; std::vector parts; size_t start = 1; while (start <= path.size()) { size_t slash = path.find('/', start); if (slash == std::string::npos) slash = path.size(); parts.push_back(path.substr(start, slash - start)); start = slash + 1; } if (parts.empty() || parts.back().empty()) { if (!parts.empty()) parts.pop_back(); parts.push_back("index"); } for (size_t i = 0; i < parts.size(); i++) { std::string name = parts[i]; if (i + 1 == parts.size()) { if (u.hasQuery) name += "~" + u.query; if (name.size() < 4 || name.compare(name.size() - 4, 4, ".gmi") != 0) name += ".gmi"; } out += "/" + storage::sanitize(name); } return out; } } // namespace roro::gemini