// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include #include module Crafter.Review:Text_impl; import :Text; import :Unicode; import :Json; import std; namespace Crafter::Review::Dyn { namespace { template bool InRuns(const Table& table, char32_t c) { const auto found = std::ranges::upper_bound(table, c, {}, [](const Unicode::Run& run) { return run.first; }); if (found == table.begin()) return false; return c <= std::prev(found)->last; } template std::string_view Mapped(const Table& table, char32_t c) { const auto found = std::ranges::lower_bound(table, c, {}, [](const Unicode::Mapping& mapping) { return mapping.from; }); if (found == table.end() || found->from != c) return {}; return found->to; } void AppendUtf8(std::string& out, char32_t c) { if (c < 0x80) { out.push_back(static_cast(c)); } else if (c < 0x800) { out.push_back(static_cast(0xC0 | (c >> 6))); out.push_back(static_cast(0x80 | (c & 0x3F))); } else if (c < 0x10000) { out.push_back(static_cast(0xE0 | (c >> 12))); out.push_back(static_cast(0x80 | ((c >> 6) & 0x3F))); out.push_back(static_cast(0x80 | (c & 0x3F))); } else { out.push_back(static_cast(0xF0 | (c >> 18))); out.push_back(static_cast(0x80 | ((c >> 12) & 0x3F))); out.push_back(static_cast(0x80 | ((c >> 6) & 0x3F))); out.push_back(static_cast(0x80 | (c & 0x3F))); } } // One code point from `bytes` at `at`: the code point and the bytes // it took, or U+FFFD and the length of the maximal subpart of an // ill-formed sequence, which one U+FFFD replaces. std::pair Next(std::string_view bytes, std::size_t at) { const auto lead = static_cast(bytes[at]); if (lead < 0x80) return {lead, 1}; std::size_t need = 0; char32_t value = 0; std::uint8_t low = 0x80; std::uint8_t high = 0xBF; if (lead >= 0xC2 && lead <= 0xDF) { need = 1; value = lead & 0x1F; } else if (lead >= 0xE0 && lead <= 0xEF) { need = 2; value = lead & 0x0F; if (lead == 0xE0) low = 0xA0; if (lead == 0xED) high = 0x9F; } else if (lead >= 0xF0 && lead <= 0xF4) { need = 3; value = lead & 0x07; if (lead == 0xF0) low = 0x90; if (lead == 0xF4) high = 0x8F; } else { return {0xFFFD, 1}; } std::size_t used = 1; for (std::size_t i = 0; i < need; ++i) { if (at + used >= bytes.size()) return {0xFFFD, used}; const auto next = static_cast(bytes[at + used]); if (next < low || next > high) return {0xFFFD, used}; value = (value << 6) | (next & 0x3F); ++used; low = 0x80; high = 0xBF; } return {value, used}; } bool IsAsciiDigit(char32_t c) { return c >= '0' && c <= '9'; } // ParseInt()'s and ParseFloat()'s first step: Unicode whitespace // stripped, then every decimal digit made ASCII and every space a plain // one. std::optional AsciiNumber(std::string_view text) { std::u32string decoded = Decode(Strip(text)); std::string out; for (const char32_t c : decoded) { if (c < 0x80) { out.push_back(static_cast(c)); } else if (const std::int32_t digit = DecimalValue(c); digit >= 0) { out.push_back(static_cast('0' + digit)); } else if (IsSpace(c)) { out.push_back(' '); } else { return std::nullopt; } } return out; } // A run of digits with single underscores between them, as ParseInt() // and ParseFloat() take them; the digits alone. std::optional Digits(std::string_view text) { if (text.empty() || text.front() == '_' || text.back() == '_') return std::nullopt; std::string digits; char previous = '\0'; for (const char c : text) { if (c == '_') { if (previous == '_') return std::nullopt; } else if (c >= '0' && c <= '9') { digits.push_back(c); } else { return std::nullopt; } previous = c; } return digits; } std::string ToLowerAscii(std::string_view text) { std::string out(text); for (char& c : out) { if (c >= 'A' && c <= 'Z') c = static_cast(c - 'A' + 'a'); } return out; } // NormPath() on a string that is known to be a path. std::vector PathComponents(std::string_view path) { std::vector out; for (const std::string& piece : Split(path, "/")) { if (!piece.empty()) out.push_back(piece); } return out; } } std::u32string Decode(std::string_view utf8) { std::u32string out; out.reserve(utf8.size()); for (std::size_t at = 0; at < utf8.size();) { const auto [c, used] = Next(utf8, at); out.push_back(c); at += used; } return out; } std::string Encode(std::u32string_view text) { std::string out; out.reserve(text.size()); for (const char32_t c : text) AppendUtf8(out, c); return out; } std::string Encode(char32_t c) { std::string out; AppendUtf8(out, c); return out; } bool IsUtf8(std::string_view bytes) { for (std::size_t at = 0; at < bytes.size();) { const auto [c, used] = Next(bytes, at); if (c == 0xFFFD) { // A real U+FFFD in the input is three valid bytes. if (used != 3 || bytes.substr(at, 3) != "\xEF\xBF\xBD") return false; } at += used; } return true; } std::string DecodeReplace(std::string_view bytes) { if (IsUtf8(bytes)) return std::string(bytes); return Encode(Decode(bytes)); } std::size_t Length(std::string_view text) { std::size_t count = 0; for (std::size_t at = 0; at < text.size(); ++count) at += Next(text, at).second; return count; } std::string Slice(std::string_view text, std::int64_t start, std::optional end) { const std::u32string decoded = Decode(text); const auto size = static_cast(decoded.size()); auto clip = [size](std::int64_t index) { if (index < 0) index += size; return std::clamp(index, 0, size); }; const std::int64_t from = clip(start); const std::int64_t to = end ? clip(*end) : size; if (to <= from) return {}; return Encode(std::u32string_view(decoded).substr(static_cast(from), static_cast(to - from))); } bool IsSpace(char32_t c) { return std::ranges::binary_search(Unicode::Space, c); } bool IsLineBreak(char32_t c) { return std::ranges::binary_search(Unicode::LineBreak, c); } bool IsDecimal(char32_t c) { return DecimalValue(c) >= 0; } std::int32_t DecimalValue(char32_t c) { if (IsAsciiDigit(c)) return static_cast(c - '0'); const auto found = std::ranges::upper_bound(Unicode::Decimal, c, {}, [](const Unicode::Run& run) { return run.first; }); if (found == Unicode::Decimal.begin()) return -1; const Unicode::Run& run = *std::prev(found); if (c > run.last) return -1; return static_cast((c - run.first) % 10); } bool IsPrintable(char32_t c) { return InRuns(Unicode::Printable, c); } char IgnoreCaseLetter(char32_t c) { if (c >= 'a' && c <= 'z') return static_cast(c); if (c >= 'A' && c <= 'Z') return static_cast(c - 'A' + 'a'); const auto found = std::ranges::lower_bound(Unicode::IgnoreCase, c, {}, [](const Unicode::Fold& fold) { return fold.from; }); if (found == Unicode::IgnoreCase.end() || found->from != c) return '\0'; return found->letter; } std::string LStrip(std::string_view text) { std::size_t at = 0; while (at < text.size()) { const auto [c, used] = Next(text, at); if (!IsSpace(c)) break; at += used; } return std::string(text.substr(at)); } std::string RStrip(std::string_view text) { const std::u32string decoded = Decode(text); std::size_t end = decoded.size(); while (end > 0 && IsSpace(decoded[end - 1])) --end; if (end == decoded.size()) return std::string(text); return Encode(std::u32string_view(decoded).substr(0, end)); } std::string Strip(std::string_view text) { return RStrip(LStrip(text)); } std::string LStrip(std::string_view text, std::string_view chars) { const std::u32string set = Decode(chars); const std::u32string decoded = Decode(text); std::size_t start = 0; while (start < decoded.size() && set.contains(decoded[start])) ++start; return Encode(std::u32string_view(decoded).substr(start)); } std::string RStrip(std::string_view text, std::string_view chars) { const std::u32string set = Decode(chars); const std::u32string decoded = Decode(text); std::size_t end = decoded.size(); while (end > 0 && set.contains(decoded[end - 1])) --end; return Encode(std::u32string_view(decoded).substr(0, end)); } std::string Strip(std::string_view text, std::string_view chars) { return RStrip(LStrip(text, chars), chars); } std::vector Split(std::string_view text) { std::vector out; std::string current; bool inWord = false; for (std::size_t at = 0; at < text.size();) { const auto [c, used] = Next(text, at); if (IsSpace(c)) { if (inWord) out.push_back(std::move(current)); current.clear(); inWord = false; } else { current.append(text.substr(at, used)); inWord = true; } at += used; } if (inWord) out.push_back(std::move(current)); return out; } std::vector Split(std::string_view text, std::string_view separator, std::int64_t maxSplit) { if (separator.empty()) throw std::invalid_argument("empty separator"); std::vector out; std::size_t start = 0; while (maxSplit < 0 || static_cast(out.size()) < maxSplit) { const std::size_t found = text.find(separator, start); if (found == std::string_view::npos) break; out.emplace_back(text.substr(start, found - start)); start = found + separator.size(); } out.emplace_back(text.substr(start)); return out; } std::vector RSplit(std::string_view text, std::string_view separator, std::int64_t maxSplit) { if (separator.empty()) throw std::invalid_argument("empty separator"); std::vector out; std::size_t end = text.size(); while (maxSplit < 0 || static_cast(out.size()) < maxSplit) { if (end < separator.size()) break; const std::size_t found = text.rfind(separator, end - separator.size()); if (found == std::string_view::npos) break; out.emplace_back(text.substr(found + separator.size(), end - found - separator.size())); end = found; } out.emplace_back(text.substr(0, end)); std::ranges::reverse(out); return out; } std::vector SplitLines(std::string_view text, bool keepEnds) { std::vector out; std::size_t start = 0; std::size_t at = 0; while (at < text.size()) { const auto [c, used] = Next(text, at); if (!IsLineBreak(c)) { at += used; continue; } std::size_t end = at + used; if (c == '\r' && end < text.size() && text[end] == '\n') ++end; out.emplace_back(text.substr(start, (keepEnds ? end : at) - start)); start = end; at = end; } if (start < text.size()) out.emplace_back(text.substr(start)); return out; } std::string Lower(std::string_view text) { std::string out; out.reserve(text.size()); for (std::size_t at = 0; at < text.size();) { const auto [c, used] = Next(text, at); if (c < 0x80) { out.push_back(static_cast(c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c)); } else if (const std::string_view to = Mapped(Unicode::Lower, c); !to.empty()) { out.append(to); } else { AppendUtf8(out, c); } at += used; } return out; } std::string Upper(std::string_view text) { std::string out; out.reserve(text.size()); for (std::size_t at = 0; at < text.size();) { const auto [c, used] = Next(text, at); if (c < 0x80) { out.push_back(static_cast(c >= 'a' && c <= 'z' ? c - 'a' + 'A' : c)); } else if (const std::string_view to = Mapped(Unicode::Upper, c); !to.empty()) { out.append(to); } else { AppendUtf8(out, c); } at += used; } return out; } std::size_t Count(std::string_view text, std::string_view sub) { if (sub.empty()) return Length(text) + 1; std::size_t count = 0; for (std::size_t at = text.find(sub); at != std::string_view::npos; at = text.find(sub, at + sub.size())) ++count; return count; } std::string Replace(std::string_view text, std::string_view from, std::string_view to) { if (from.empty()) return std::string(text); std::string out; std::size_t start = 0; for (std::size_t at = text.find(from); at != std::string_view::npos; at = text.find(from, start)) { out.append(text.substr(start, at - start)); out.append(to); start = at + from.size(); } out.append(text.substr(start)); return out; } std::string Join(std::span parts, std::string_view separator) { std::string out; for (std::size_t i = 0; i < parts.size(); ++i) { if (i > 0) out.append(separator); out.append(parts[i]); } return out; } std::string ReadText(const std::filesystem::path& path) { std::ifstream in(path, std::ios::binary); if (!in) throw std::runtime_error(std::format("[Errno 2] No such file or directory: {}", Repr(std::string_view(path.string())))); const std::string bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); for (std::size_t at = 0; at < bytes.size();) { const auto [c, used] = Next(bytes, at); if (c == 0xFFFD && (used != 3 || std::string_view(bytes).substr(at, 3) != "\xEF\xBF\xBD")) { throw std::runtime_error(std::format("'utf-8' codec can't decode byte 0x{:02x} in position {}: invalid utf-8 ({})", static_cast(bytes[at]), at, path.string())); } at += used; } return UniversalNewlines(bytes); } std::string UniversalNewlines(std::string_view text) { if (!text.contains('\r')) return std::string(text); std::string out; out.reserve(text.size()); for (std::size_t i = 0; i < text.size(); ++i) { if (text[i] == '\r') { out.push_back('\n'); if (i + 1 < text.size() && text[i + 1] == '\n') ++i; } else { out.push_back(text[i]); } } return out; } std::string FloatRepr(double value) { if (std::isnan(value)) return "nan"; if (std::isinf(value)) return value > 0 ? "inf" : "-inf"; // The shortest round-trip digits, in to_chars' scientific form, then // laid out: fixed from 1e-4 up to 1e16, with an exponent otherwise. std::array buffer {}; const auto [end, error] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value, std::chars_format::scientific); const std::string_view scientific(buffer.data(), end); const bool negative = scientific.starts_with('-'); const std::string_view body = negative ? scientific.substr(1) : scientific; const std::size_t e = body.find('e'); std::string digits; for (const char c : body.substr(0, e)) { if (c != '.') digits.push_back(c); } std::int32_t exponent = 0; std::from_chars(body.data() + e + 1 + (body[e + 1] == '+' ? 1 : 0), body.data() + body.size(), exponent); while (digits.size() > 1 && digits.back() == '0') digits.pop_back(); const std::int32_t decimalPoint = exponent + 1; std::string out = negative ? "-" : ""; if (decimalPoint <= -4 || decimalPoint > 16) { out += digits.substr(0, 1); if (digits.size() > 1) out += std::format(".{}", digits.substr(1)); const std::int32_t shown = decimalPoint - 1; out += std::format("e{}{:02}", shown < 0 ? '-' : '+', shown < 0 ? -shown : shown); return out; } if (decimalPoint <= 0) { out += std::format("0.{}{}", std::string(static_cast(-decimalPoint), '0'), digits); } else if (static_cast(decimalPoint) >= digits.size()) { out += std::format("{}{}.0", digits, std::string(static_cast(decimalPoint) - digits.size(), '0')); } else { out += std::format("{}.{}", digits.substr(0, static_cast(decimalPoint)), digits.substr(static_cast(decimalPoint))); } return out; } double Round(double value, std::int32_t digits) { if (!std::isfinite(value)) return value; // to_chars with a precision rounds the exact binary value correctly, // half to even on an exact tie. std::array buffer {}; const auto [end, error] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value, std::chars_format::fixed, digits); if (error != std::errc()) return value; double rounded = value; std::from_chars(buffer.data(), end, rounded); return rounded; } std::int64_t RoundToInt(double value) { return static_cast(std::nearbyint(value)); } std::optional ParseInt(std::string_view text) { const std::optional ascii = AsciiNumber(text); if (!ascii || ascii->empty()) return std::nullopt; std::string_view body = *ascii; bool negative = false; if (body.front() == '+' || body.front() == '-') { negative = body.front() == '-'; body.remove_prefix(1); } const std::optional digits = Digits(body); if (!digits) return std::nullopt; std::int64_t value = 0; for (const char c : *digits) { if (value > (std::numeric_limits::max() - (c - '0')) / 10) return std::nullopt; value = value * 10 + (c - '0'); } return negative ? -value : value; } std::optional ParseFloat(std::string_view text) { const std::optional ascii = AsciiNumber(text); if (!ascii || ascii->empty()) return std::nullopt; std::string_view body = *ascii; bool negative = false; if (body.front() == '+' || body.front() == '-') { negative = body.front() == '-'; body.remove_prefix(1); } const std::string word = ToLowerAscii(body); if (word == "inf" || word == "infinity") return negative ? -std::numeric_limits::infinity() : std::numeric_limits::infinity(); if (word == "nan") return std::numeric_limits::quiet_NaN(); // digits [. digits] [e [sign] digits], one of the two digit runs // present, underscores only between digits. const std::size_t exponentAt = body.find_first_of("eE"); const std::string_view mantissa = body.substr(0, exponentAt); const std::size_t dot = mantissa.find('.'); const std::string_view whole = mantissa.substr(0, dot); const std::string_view fraction = dot == std::string_view::npos ? std::string_view() : mantissa.substr(dot + 1); if (whole.empty() && fraction.empty()) return std::nullopt; std::string normal = negative ? "-" : ""; if (!whole.empty()) { const std::optional digits = Digits(whole); if (!digits) return std::nullopt; normal += *digits; } if (dot != std::string_view::npos) { normal += '.'; if (!fraction.empty()) { const std::optional digits = Digits(fraction); if (!digits) return std::nullopt; normal += *digits; } } if (exponentAt != std::string_view::npos) { std::string_view exponent = body.substr(exponentAt + 1); normal += 'e'; if (!exponent.empty() && (exponent.front() == '+' || exponent.front() == '-')) { normal += exponent.front(); exponent.remove_prefix(1); } const std::optional digits = Digits(exponent); if (!digits) return std::nullopt; normal += *digits; } // strtod, not from_chars: an exponent out of range is inf or 0.0, not // an error. The process runs in the "C" locale. return std::strtod(normal.c_str(), nullptr); } std::string IntError(std::string_view text) { return std::format("invalid literal for int() with base 10: {}", Repr(text)); } std::string FloatError(std::string_view text) { return std::format("could not convert string to float: {}", Repr(text)); } std::string Repr(std::string_view text) { const char quote = text.contains('\'') && !text.contains('"') ? '"' : '\''; std::string out(1, quote); for (std::size_t at = 0; at < text.size();) { const auto [c, used] = Next(text, at); if (c == static_cast(quote) || c == '\\') { out.push_back('\\'); out.push_back(static_cast(c)); } else if (c == '\t') { out += "\\t"; } else if (c == '\n') { out += "\\n"; } else if (c == '\r') { out += "\\r"; } else if (c < 0x20 || c == 0x7F) { out += std::format("\\x{:02x}", static_cast(c)); } else if (c < 0x7F) { out.push_back(static_cast(c)); } else if (IsPrintable(c)) { AppendUtf8(out, c); } else if (c <= 0xFF) { out += std::format("\\x{:02x}", static_cast(c)); } else if (c <= 0xFFFF) { out += std::format("\\u{:04x}", static_cast(c)); } else { out += std::format("\\U{:08x}", static_cast(c)); } at += used; } out.push_back(quote); return out; } std::string ReprValue(const Json& value) { if (value.is_null()) return "None"; if (value.is_boolean()) return value.get() ? "True" : "False"; if (value.is_number_float()) return FloatRepr(value.get()); if (value.is_number_unsigned()) return std::to_string(value.get()); if (value.is_number_integer()) return std::to_string(value.get()); if (value.is_string()) return Repr(std::string_view(value.get_ref())); if (value.is_array()) { std::string out = "["; bool first = true; for (const Json& item : value) { if (!first) out += ", "; first = false; out += ReprValue(item); } return std::format("{}]", out); } std::string out = "{"; bool first = true; for (auto it = value.begin(); it != value.end(); ++it) { if (!first) out += ", "; first = false; out += std::format("{}: {}", Repr(std::string_view(it.key())), ReprValue(it.value())); } return std::format("{}}}", out); } namespace { // One element of a translated shell pattern. struct GlobToken { enum class Kind { Star, Any, Literal, Class, Never }; Kind kind; char32_t literal = 0; bool negate = false; std::vector> ranges; }; // A bracket expression, from the text between [ and ] — escapes and // all — read as a regular expression reads a character class. GlobToken ClassFrom(const std::u32string& content) { GlobToken token { .kind = GlobToken::Kind::Class }; std::u32string_view rest(content); if (rest.starts_with(U'!')) { token.negate = true; rest.remove_prefix(1); } auto item = [&rest]() { char32_t c = rest.front(); rest.remove_prefix(1); if (c == U'\\' && !rest.empty()) { c = rest.front(); rest.remove_prefix(1); } return c; }; while (!rest.empty()) { const char32_t first = item(); if (rest.size() >= 2 && rest.front() == U'-') { rest.remove_prefix(1); const char32_t last = item(); token.ranges.emplace_back(first, last); } else { token.ranges.emplace_back(first, first); } } return token; } std::u32string EscapeClassChunk(std::u32string_view chunk, bool hyphens) { std::u32string out; for (const char32_t c : chunk) { if (c == U'\\' || (hyphens && c == U'-')) out.push_back(U'\\'); out.push_back(c); } return out; } // A shell pattern, translated token by token. std::vector Translate(const std::u32string& pattern) { std::vector tokens; const std::size_t n = pattern.size(); std::size_t i = 0; while (i < n) { const char32_t c = pattern[i++]; if (c == U'*') { if (tokens.empty() || tokens.back().kind != GlobToken::Kind::Star) tokens.push_back({ .kind = GlobToken::Kind::Star }); while (i < n && pattern[i] == U'*') ++i; } else if (c == U'?') { tokens.push_back({ .kind = GlobToken::Kind::Any }); } else if (c == U'[') { std::size_t j = i; if (j < n && pattern[j] == U'!') ++j; if (j < n && pattern[j] == U']') ++j; while (j < n && pattern[j] != U']') ++j; if (j >= n) { tokens.push_back({ .kind = GlobToken::Kind::Literal, .literal = U'[' }); continue; } std::u32string stuff = pattern.substr(i, j - i); if (!stuff.contains(U'-')) { stuff = EscapeClassChunk(stuff, false); } else { std::vector chunks; std::size_t start = i; std::size_t k = pattern[i] == U'!' ? i + 2 : i + 1; while (true) { const std::size_t found = k < j ? pattern.find(U'-', k) : std::u32string::npos; if (found == std::u32string::npos || found >= j) break; chunks.push_back(pattern.substr(start, found - start)); start = found + 1; k = found + 3; } const std::u32string chunk = pattern.substr(start, j - start); if (!chunk.empty()) { chunks.push_back(chunk); } else { chunks.back() += U'-'; } // A reversed range is dropped: it matches nothing. for (std::size_t m = chunks.size() - 1; m > 0; --m) { if (chunks[m - 1].back() > chunks[m].front()) { chunks[m - 1] = chunks[m - 1].substr(0, chunks[m - 1].size() - 1) + chunks[m].substr(1); chunks.erase(chunks.begin() + static_cast(m)); } } stuff.clear(); for (std::size_t m = 0; m < chunks.size(); ++m) { if (m > 0) stuff += U'-'; stuff += EscapeClassChunk(chunks[m], true); } } i = j + 1; if (stuff.empty()) { tokens.push_back({ .kind = GlobToken::Kind::Never }); } else if (stuff == U"!") { tokens.push_back({ .kind = GlobToken::Kind::Any }); } else { tokens.push_back(ClassFrom(stuff)); } } else { tokens.push_back({ .kind = GlobToken::Kind::Literal, .literal = c }); } } return tokens; } bool Matches(const GlobToken& token, char32_t c) { switch (token.kind) { case GlobToken::Kind::Any: return true; case GlobToken::Kind::Literal: return token.literal == c; case GlobToken::Kind::Class: { const bool in = std::ranges::any_of(token.ranges, [c](const auto& range) { return c >= range.first && c <= range.second; }); return in != token.negate; } default: return false; } } } bool FnMatch(std::string_view name, std::string_view pattern) { const std::vector tokens = Translate(Decode(pattern)); const std::u32string text = Decode(name); // The classic two-pointer match: every token but * consumes exactly // one character, so backtracking only ever returns to the last star. std::size_t t = 0; std::size_t s = 0; std::optional starToken; std::size_t starText = 0; while (s < text.size()) { if (t < tokens.size() && tokens[t].kind == GlobToken::Kind::Star) { starToken = t++; starText = s; } else if (t < tokens.size() && Matches(tokens[t], text[s])) { ++t; ++s; } else if (starToken) { t = *starToken + 1; s = ++starText; } else { return false; } } while (t < tokens.size() && tokens[t].kind == GlobToken::Kind::Star) ++t; return t == tokens.size(); } std::optional SplitUrl::Hostname() const { std::string hostinfo = netloc.substr(netloc.rfind('@') == std::string::npos ? 0 : netloc.rfind('@') + 1); std::string hostname; if (const std::size_t open = hostinfo.find('['); open != std::string::npos) { const std::string bracketed = hostinfo.substr(open + 1); hostname = bracketed.substr(0, bracketed.find(']')); } else { hostname = hostinfo.substr(0, hostinfo.find(':')); } if (hostname.empty()) return std::nullopt; // A scoped IPv6 address's zone is left as written. const std::size_t percent = hostname.find('%'); if (percent == std::string::npos) return Lower(hostname); return Lower(hostname.substr(0, percent)) + hostname.substr(percent); } std::optional SplitUrl::Username() const { const std::size_t at = netloc.rfind('@'); if (at == std::string::npos) return std::nullopt; const std::string userinfo = netloc.substr(0, at); return userinfo.substr(0, userinfo.find(':')); } std::optional SplitUrl::Password() const { const std::size_t at = netloc.rfind('@'); if (at == std::string::npos) return std::nullopt; const std::string userinfo = netloc.substr(0, at); const std::size_t colon = userinfo.find(':'); if (colon == std::string::npos) return std::nullopt; return userinfo.substr(colon + 1); } std::optional SplitUrl::Port() const { std::string hostinfo = netloc.substr(netloc.rfind('@') == std::string::npos ? 0 : netloc.rfind('@') + 1); std::string port; if (const std::size_t open = hostinfo.find('['); open != std::string::npos) { const std::string bracketed = hostinfo.substr(open + 1); const std::size_t close = bracketed.find(']'); if (close != std::string::npos) { const std::string after = bracketed.substr(close + 1); if (const std::size_t colon = after.find(':'); colon != std::string::npos) port = after.substr(colon + 1); } } else if (const std::size_t colon = hostinfo.find(':'); colon != std::string::npos) { port = hostinfo.substr(colon + 1); } if (port.empty()) return std::nullopt; if (!std::ranges::all_of(port, [](char c) { return c >= '0' && c <= '9'; })) { throw std::invalid_argument(std::format("Port could not be cast to integer value as {}", Repr(port))); } std::int64_t value = 0; for (const char c : port) { value = value * 10 + (c - '0'); if (value > 65535) break; } if (value > 65535) throw std::invalid_argument("Port out of range 0-65535"); return static_cast(value); } namespace { // ipaddress.ip_address's verdict on what sits between brackets, as // far as urlsplit needs one: an IPv4 address is refused, anything that // is not an IPv6 address is invalid. void CheckBracketedHost(const std::string& host) { if (host.starts_with('v')) { // \Av[a-fA-F0-9]+\..+\z std::size_t i = 1; while (i < host.size() && std::isxdigit(static_cast(host[i]))) ++i; if (i == 1 || i >= host.size() || host[i] != '.' || i + 1 >= host.size()) { throw std::invalid_argument("IPvFuture address is invalid"); } return; } std::array address {}; std::string withoutZone = host.substr(0, host.find('%')); if (inet_pton(AF_INET, withoutZone.c_str(), address.data()) == 1) { throw std::invalid_argument("An IPv4 address cannot be in brackets"); } if (inet_pton(AF_INET6, withoutZone.c_str(), address.data()) != 1) { throw std::invalid_argument(std::format("{} does not appear to be an IPv4 or IPv6 address", Repr(host))); } } } SplitUrl UrlSplit(std::string_view input) { std::string url; for (const char c : input) { if (c != '\t' && c != '\r' && c != '\n') url.push_back(c); } std::size_t lead = 0; while (lead < url.size() && static_cast(url[lead]) <= 0x20) ++lead; url.erase(0, lead); SplitUrl parts; const std::size_t colon = url.find(':'); auto isAlpha = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); }; auto isSchemeChar = [&](char c) { return isAlpha(c) || (c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.'; }; if (colon != std::string::npos && colon > 0 && isAlpha(url[0]) && std::ranges::all_of(std::string_view(url).substr(0, colon), isSchemeChar)) { parts.scheme = ToLowerAscii(std::string_view(url).substr(0, colon)); url.erase(0, colon + 1); } if (url.starts_with("//")) { std::size_t end = url.find_first_of("/?#", 2); if (end == std::string::npos) end = url.size(); parts.netloc = url.substr(2, end - 2); url.erase(0, end); const bool open = parts.netloc.contains('['); const bool close = parts.netloc.contains(']'); if (open != close) throw std::invalid_argument("Invalid IPv6 URL"); if (open && close) { // _check_bracketed_netloc: the brackets may sit in the user // part, and then the host is checked as an address all the same. const std::string hostAndPort = parts.netloc.substr(parts.netloc.rfind('@') == std::string::npos ? 0 : parts.netloc.rfind('@') + 1); const std::size_t bracket = hostAndPort.find('['); if (bracket == std::string::npos) { CheckBracketedHost(hostAndPort.substr(0, hostAndPort.find(':'))); } else { if (bracket != 0) throw std::invalid_argument("Invalid IPv6 URL"); const std::string bracketed = hostAndPort.substr(1); const std::size_t closing = bracketed.find(']'); const std::string after = closing == std::string::npos ? std::string() : bracketed.substr(closing + 1); if (!after.empty() && !after.starts_with(':')) throw std::invalid_argument("Invalid IPv6 URL"); CheckBracketedHost(bracketed.substr(0, closing)); } } } if (const std::size_t hash = url.find('#'); hash != std::string::npos) { parts.fragment = url.substr(hash + 1); url.erase(hash); } if (const std::size_t question = url.find('?'); question != std::string::npos) { parts.query = url.substr(question + 1); url.erase(question); } parts.path = std::move(url); return parts; } std::string Unquote(std::string_view text) { if (!text.contains('%')) return std::string(text); // Percent sequences become bytes; runs of bytes are decoded as UTF-8 // with bad bytes replaced; everything else is kept as it is. std::string out; std::string bytes; auto hexValue = [](char c) -> std::int32_t { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; }; for (std::size_t i = 0; i < text.size();) { if (text[i] == '%' && i + 2 < text.size() && hexValue(text[i + 1]) >= 0 && hexValue(text[i + 2]) >= 0) { bytes.push_back(static_cast(hexValue(text[i + 1]) * 16 + hexValue(text[i + 2]))); i += 3; continue; } if (!bytes.empty()) { out += DecodeReplace(bytes); bytes.clear(); } out.push_back(text[i]); ++i; } if (!bytes.empty()) out += DecodeReplace(bytes); return out; } NaturalKey Natural(std::string_view name) { NaturalKey key; std::string text; std::int64_t number = 0; bool inDigits = false; for (std::size_t at = 0; at < name.size();) { const auto [c, used] = Next(name, at); const std::int32_t digit = DecimalValue(c); if (digit >= 0) { if (!inDigits) { key.parts.emplace_back(text); text.clear(); number = 0; inDigits = true; } number = number * 10 + digit; } else { if (inDigits) { key.parts.emplace_back(number); inDigits = false; } text.append(name.substr(at, used)); } at += used; } if (inDigits) { key.parts.emplace_back(number); text.clear(); } key.parts.emplace_back(text); return key; } std::string NormPath(std::string_view path) { if (path.empty()) return "."; std::size_t leading = 0; while (leading < path.size() && path[leading] == '/') ++leading; // POSIX gives exactly two leading slashes a meaning of their own. const std::string prefix = leading == 0 ? "" : (leading == 2 ? "//" : "/"); std::vector kept; for (const std::string& component : PathComponents(path)) { if (component == ".") continue; if (component == "..") { if (!kept.empty() && kept.back() != "..") { kept.pop_back(); } else if (prefix.empty()) { kept.push_back(component); } continue; } kept.push_back(component); } std::string out = prefix + Join(kept, "/"); return out.empty() ? std::string(".") : out; } std::string AbsPath(std::string_view path) { if (path.starts_with('/')) return NormPath(path); return NormPath(JoinPath(std::filesystem::current_path().string(), path)); } std::string RelPath(std::string_view path, std::string_view start) { if (path.empty()) throw std::invalid_argument("no path specified"); const std::vector from = PathComponents(AbsPath(start.empty() ? "." : start)); const std::vector to = PathComponents(AbsPath(path)); std::size_t common = 0; while (common < from.size() && common < to.size() && from[common] == to[common]) ++common; std::vector relative(from.size() - common, ".."); relative.insert(relative.end(), to.begin() + static_cast(common), to.end()); if (relative.empty()) return "."; return Join(relative, "/"); } std::string BaseName(std::string_view path) { return std::string(path.substr(path.rfind('/') == std::string_view::npos ? 0 : path.rfind('/') + 1)); } std::string DirName(std::string_view path) { const std::size_t slash = path.rfind('/'); if (slash == std::string_view::npos) return ""; std::string head(path.substr(0, slash + 1)); if (!std::ranges::all_of(head, [](char c) { return c == '/'; })) { while (!head.empty() && head.back() == '/') head.pop_back(); } return head; } std::string JoinPath(std::string_view a, std::string_view b) { if (b.starts_with('/')) return std::string(b); if (a.empty() || a.ends_with('/')) return std::string(a) + std::string(b); return std::format("{}/{}", std::string(a), std::string(b)); } std::string ErrnoText(std::int32_t code) { return std::format("[Errno {}] {}", code, std::strerror(code)); } }