#include "util.h" #include #include #include #include namespace util { namespace { constexpr int MIN_OID_LEN = 4; constexpr int MAX_OID_LEN = 40; } // namespace bool is_hex_oid(const std::string &s) { if (s.length() < MIN_OID_LEN || s.length() > MAX_OID_LEN) return false; for (char c : s) { const bool is_digit = c >= '0' && c <= '9'; const bool is_lower_hex = c >= 'a' && c <= 'f'; if (!is_digit && !is_lower_hex) return false; } return true; } std::string human_size(uint64_t bytes) { constexpr uint64_t unit = 1024; const std::array suffixes{"B", "KiB", "MiB", "GiB", "TiB"}; if (bytes < unit) return std::to_string(bytes) + " B"; double value = static_cast(bytes); size_t i = 0; while (value >= unit && i + 1 < suffixes.size()) { value /= unit; ++i; } std::array buf{}; std::snprintf(buf.data(), buf.size(), "%.1f %s", value, suffixes[i]); return std::string(buf.data()); } std::string format_time(std::time_t t) { std::tm tm{}; gmtime_r(&t, &tm); std::array buf{}; std::strftime(buf.data(), buf.size(), "%Y-%m-%d %H:%M", &tm); return std::string(buf.data()); } std::string basename(const std::string &path) { const size_t pos = path.find_last_of('/'); if (pos == std::string::npos) return path; return path.substr(pos + 1); } std::string parent_path(const std::string &path) { const size_t pos = path.find_last_of('/'); if (pos == std::string::npos) return ""; return path.substr(0, pos); } std::string trim_slashes(const std::string &path) { size_t start = 0; size_t end = path.length(); while (start < end && path[start] == '/') ++start; while (end > start && path[end - 1] == '/') --end; return path.substr(start, end - start); } std::string sanitize_filename(const std::string &name) { std::string out = name; for (char &c : out) { const bool is_digit = c >= '0' && c <= '9'; const bool is_upper = c >= 'A' && c <= 'Z'; const bool is_lower = c >= 'a' && c <= 'z'; const bool is_safe_punct = c == '.' || c == '_' || c == '-'; if (!is_digit && !is_upper && !is_lower && !is_safe_punct) c = '_'; } return out; } bool is_markdown_path(const std::string &path) { const size_t dot = path.find_last_of('.'); if (dot == std::string::npos) return false; std::string ext = path.substr(dot + 1); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); return ext == "md" || ext == "markdown"; } std::string image_mime_type(const std::string &path) { const size_t dot = path.find_last_of('.'); if (dot == std::string::npos) return ""; std::string ext = path.substr(dot + 1); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); if (ext == "png") return "image/png"; if (ext == "jpg" || ext == "jpeg") return "image/jpeg"; if (ext == "gif") return "image/gif"; if (ext == "webp") return "image/webp"; if (ext == "svg") return "image/svg+xml"; if (ext == "bmp") return "image/bmp"; if (ext == "ico") return "image/x-icon"; if (ext == "avif") return "image/avif"; return ""; } namespace { // Splits a remote URL into host and path ("owner/repo[.git]"). Returns false // when the URL has no recognisable host/path split. bool split_remote(const std::string &url, std::string &host, std::string &path) { const std::string scheme_sep = "://"; const size_t scheme = url.find(scheme_sep); if (scheme != std::string::npos) { // scheme://[user@]host[:port]/owner/repo std::string rest = url.substr(scheme + scheme_sep.length()); const size_t at = rest.find('@'); if (at != std::string::npos) rest = rest.substr(at + 1); const size_t slash = rest.find('/'); if (slash == std::string::npos) return false; host = rest.substr(0, slash); path = rest.substr(slash + 1); const size_t port = host.find(':'); if (port != std::string::npos) host = host.substr(0, port); return true; } // scp-like: [user@]host:owner/repo const size_t colon = url.find(':'); if (colon == std::string::npos) return false; std::string host_part = url.substr(0, colon); path = url.substr(colon + 1); const size_t at = host_part.find('@'); host = at == std::string::npos ? host_part : host_part.substr(at + 1); return true; } bool is_github_host(std::string host, const std::vector &enterprise_hosts) { std::transform(host.begin(), host.end(), host.begin(), ::tolower); if (host == "github.com") return true; for (const std::string &configured : enterprise_hosts) { std::string h = configured; std::transform(h.begin(), h.end(), h.begin(), ::tolower); if (host == h) return true; } return false; } } // namespace GitHubClone github_clone_urls(const std::string &remote_url, const std::vector &enterprise_hosts) { GitHubClone result; std::string host; std::string path; if (!split_remote(remote_url, host, path)) return result; if (!is_github_host(host, enterprise_hosts)) return result; while (path.length() > 0 && path.front() == '/') path.erase(path.begin()); while (path.length() > 0 && path.back() == '/') path.pop_back(); const std::string git_suffix = ".git"; if (path.length() >= git_suffix.length() && path.compare(path.length() - git_suffix.length(), git_suffix.length(), git_suffix) == 0) path = path.substr(0, path.length() - git_suffix.length()); if (path.empty()) return result; result.ssh = "git@" + host + ":" + path + ".git"; result.https = "https://" + host + "/" + path + ".git"; return result; } bool show_emails() { static const bool value = [] { const char *flag = std::getenv("SHOW_EMAILS"); return flag && std::string(flag) == "1"; }(); return value; } } // namespace util