699 lines
20 KiB
C++
699 lines
20 KiB
C++
#include "utils.hpp"
|
|
#include "httplib.hpp"
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <memory>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <fstream>
|
|
#include <cstdlib>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <filesystem>
|
|
#include <regex>
|
|
#include <random>
|
|
#include <sys/ioctl.h>
|
|
#include <unistd.h>
|
|
#include <cctype>
|
|
#include <sstream>
|
|
|
|
namespace dropshell {
|
|
|
|
std::string magic_string() {
|
|
return "-_-";
|
|
}
|
|
|
|
bool has_magic_string(std::string name)
|
|
{
|
|
return name.find(magic_string()) != std::string::npos;
|
|
}
|
|
|
|
|
|
void maketitle(const std::string& title, sColour colour) {
|
|
colourstream(colour) << std::string(title.length() + 4, '-') << std::endl;
|
|
colourstream(colour) << "| " << title << " |" << std::endl;
|
|
colourstream(colour) << std::string(title.length() + 4, '-') << std::endl;
|
|
}
|
|
|
|
bool replace_line_in_file(const std::string& file_path, const std::string& search_string, const std::string& replacement_line) {
|
|
std::ifstream input_file(file_path);
|
|
std::vector<std::string> file_lines;
|
|
std::string line;
|
|
|
|
if (!input_file.is_open()) {
|
|
error << "Unable to open file: " << file_path << std::endl;
|
|
return false;
|
|
}
|
|
|
|
while (std::getline(input_file, line)) {
|
|
if (line.find(search_string) != std::string::npos)
|
|
file_lines.push_back(replacement_line);
|
|
else
|
|
file_lines.push_back(line);
|
|
}
|
|
input_file.close();
|
|
|
|
std::ofstream output_file(file_path);
|
|
if (!output_file.is_open())
|
|
{
|
|
error << "Unable to open file: " << file_path << std::endl;
|
|
return false;
|
|
}
|
|
for (const auto& modified_line : file_lines)
|
|
output_file << modified_line << "\n";
|
|
output_file.close();
|
|
return true;
|
|
}
|
|
|
|
std::string trim(std::string str) {
|
|
// Trim leading whitespace
|
|
str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) {
|
|
return !std::isspace(ch);
|
|
}));
|
|
|
|
// Trim trailing whitespace
|
|
str.erase(std::find_if(str.rbegin(), str.rend(), [](unsigned char ch) {
|
|
return !std::isspace(ch);
|
|
}).base(), str.end());
|
|
|
|
return str;
|
|
}
|
|
|
|
std::string dequote(std::string str)
|
|
{
|
|
if (str.length() < 2)
|
|
return str;
|
|
if (str.front() == '"' && str.back() == '"') {
|
|
return str.substr(1, str.length() - 2);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
std::string quote(std::string str)
|
|
{
|
|
return "\""+str+"\"";
|
|
}
|
|
|
|
std::string halfquote(std::string str)
|
|
{
|
|
return "'" + str + "'";
|
|
}
|
|
|
|
std::string escapequotes(std::string str)
|
|
{
|
|
return std::regex_replace(str, std::regex("\""), "\\\"");
|
|
}
|
|
|
|
std::string multi2string(std::vector<std::string> values)
|
|
{
|
|
std::string result;
|
|
for (const auto& value : values) {
|
|
// remove any " contained in the string value, if present
|
|
result += dequote(trim(value)) + ",";
|
|
}
|
|
if (!result.empty())
|
|
result.pop_back(); // Remove the last comma
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::string> string2multi(std::string values)
|
|
{
|
|
std::vector<std::string> result;
|
|
|
|
values = dequote(trim(values));
|
|
|
|
// Return values separated by commas, but ignore commas within quotes
|
|
bool inside_quotes = false;
|
|
std::string current_item;
|
|
|
|
for (char c : values) {
|
|
if (c == '"') {
|
|
inside_quotes = !inside_quotes;
|
|
} else if (c == ',' && !inside_quotes) {
|
|
if (!current_item.empty()) {
|
|
std::string final = dequote(trim(current_item));
|
|
if (!final.empty())
|
|
result.push_back(final);
|
|
current_item.clear();
|
|
}
|
|
} else {
|
|
current_item += c;
|
|
}
|
|
}
|
|
|
|
// Add the last item if not empty
|
|
if (!current_item.empty()) {
|
|
std::string final = dequote(trim(current_item));
|
|
if (!final.empty())
|
|
result.push_back(final);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int str2int(const std::string &str)
|
|
{
|
|
try {
|
|
return std::stoi(str);
|
|
} catch (const std::exception& e) {
|
|
error << "Invalid integer string: [" << str << "]" << std::endl;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
void recursive_copy(const std::string & source, const std::string & destination) {
|
|
try {
|
|
if (std::filesystem::is_directory(source)) {
|
|
if (!std::filesystem::exists(destination)) {
|
|
std::filesystem::create_directory(destination);
|
|
}
|
|
for (const auto& entry : std::filesystem::directory_iterator(source)) {
|
|
recursive_copy(entry.path(), destination / entry.path().filename());
|
|
}
|
|
} else if (std::filesystem::is_regular_file(source)) {
|
|
std::filesystem::copy(source, destination, std::filesystem::copy_options::overwrite_existing);
|
|
}
|
|
} catch (const std::filesystem::filesystem_error& ex) {
|
|
std::cerr << "Error copying " << source << " to " << destination << ": " << ex.what() << std::endl;
|
|
}
|
|
}
|
|
|
|
void ensure_directories_exist(std::vector<std::string> directories)
|
|
{
|
|
for (const auto& directory : directories) {
|
|
if (!std::filesystem::exists(directory)) {
|
|
std::filesystem::create_directories(directory);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//https://www.geeksforgeeks.org/kmp-algorithm-for-pattern-searching/
|
|
void constructLps(const std::string &pat, std::vector<int> &lps) {
|
|
|
|
// len stores the length of longest prefix which
|
|
// is also a suffix for the previous index
|
|
int len = 0;
|
|
|
|
// lps[0] is always 0
|
|
lps[0] = 0;
|
|
|
|
int i = 1;
|
|
while (i < pat.length()) {
|
|
|
|
// If characters match, increment the size of lps
|
|
if (pat[i] == pat[len]) {
|
|
len++;
|
|
lps[i] = len;
|
|
i++;
|
|
}
|
|
|
|
// If there is a mismatch
|
|
else {
|
|
if (len != 0) {
|
|
|
|
// Update len to the previous lps value
|
|
// to avoid reduntant comparisons
|
|
len = lps[len - 1];
|
|
}
|
|
else {
|
|
|
|
// If no matching prefix found, set lps[i] to 0
|
|
lps[i] = 0;
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<int> search(const std::string &pat, const std::string &txt) {
|
|
int n = txt.length();
|
|
int m = pat.length();
|
|
|
|
std::vector<int> lps(m);
|
|
std::vector<int> res;
|
|
|
|
constructLps(pat, lps);
|
|
|
|
// Pointers i and j, for traversing
|
|
// the text and pattern
|
|
int i = 0;
|
|
int j = 0;
|
|
|
|
while (i < n) {
|
|
|
|
// If characters match, move both pointers forward
|
|
if (txt[i] == pat[j]) {
|
|
i++;
|
|
j++;
|
|
|
|
// If the entire pattern is matched
|
|
// store the start index in result
|
|
if (j == m) {
|
|
res.push_back(i - j);
|
|
|
|
// Use LPS of previous index to
|
|
// skip unnecessary comparisons
|
|
j = lps[j - 1];
|
|
}
|
|
}
|
|
|
|
// If there is a mismatch
|
|
else {
|
|
|
|
// Use lps value of previous index
|
|
// to avoid redundant comparisons
|
|
if (j != 0)
|
|
j = lps[j - 1];
|
|
else
|
|
i++;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
int count_substring(const std::string &substring, const std::string &text) {
|
|
std::vector<int> positions = search(substring, text);
|
|
return positions.size();
|
|
}
|
|
|
|
std::vector<std::string> split(const std::string& str, const std::string& delimiter) {
|
|
std::vector<std::string> tokens;
|
|
size_t start = 0;
|
|
size_t end = 0;
|
|
|
|
while ((end = str.find(delimiter, start)) != std::string::npos) {
|
|
tokens.push_back(str.substr(start, end - start));
|
|
start = end + delimiter.length();
|
|
}
|
|
|
|
// Add the last token
|
|
tokens.push_back(str.substr(start));
|
|
|
|
return tokens;
|
|
}
|
|
|
|
std::string random_alphanumeric_string(int length)
|
|
{
|
|
static std::mt19937 generator(std::random_device{}());
|
|
static const std::string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
std::uniform_int_distribution<> distribution(0, chars.size() - 1);
|
|
std::string random_string;
|
|
for (int i = 0; i < length; ++i) {
|
|
random_string += chars[distribution(generator)];
|
|
}
|
|
|
|
return random_string;
|
|
}
|
|
|
|
std::string requote(std::string str) {
|
|
return quote(trim(dequote(trim(str))));
|
|
}
|
|
|
|
|
|
int return_die(const std::string & msg) {
|
|
error << "Fatal error:" << std::endl;
|
|
error << msg << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
std::string safearg(const std::vector<std::string> & args, int index)
|
|
{
|
|
if (index<0 || index >= args.size()) return "";
|
|
return args[index];
|
|
}
|
|
|
|
std::string safearg(int argc, char *argv[], int index)
|
|
{
|
|
if (index<0 || index >= argc) return "";
|
|
return argv[index];
|
|
}
|
|
|
|
std::string left_align(const std::string & str, int width) {
|
|
if (static_cast<int>(str.size()) >= width)
|
|
return str;
|
|
return str + std::string(width - str.size(), ' ');
|
|
}
|
|
|
|
std::string right_align(const std::string & str, int width) {
|
|
if (static_cast<int>(str.size()) >= width)
|
|
return str;
|
|
return std::string(width - str.size(), ' ') + str;
|
|
}
|
|
|
|
std::string center_align(const std::string & str, int width) {
|
|
int pad = width - static_cast<int>(str.size());
|
|
if (pad <= 0)
|
|
return str;
|
|
int pad_left = pad / 2;
|
|
int pad_right = pad - pad_left;
|
|
return std::string(pad_left, ' ') + str + std::string(pad_right, ' ');
|
|
}
|
|
|
|
|
|
std::string replace_with_environment_variables_like_bash(std::string str) {
|
|
// Combined regex pattern for both ${var} and $var formats
|
|
std::regex var_pattern("\\$(?:\\{([^}]+)\\}|([a-zA-Z0-9_]+))");
|
|
std::string result = str;
|
|
std::smatch match;
|
|
|
|
while (std::regex_search(result, match, var_pattern)) {
|
|
// match[1] will contain capture from ${var} format
|
|
// match[2] will contain capture from $var format
|
|
std::string var_name = match[1].matched ? match[1].str() : match[2].str();
|
|
|
|
// Get value from system environment variables
|
|
const char* env_value = std::getenv(var_name.c_str());
|
|
std::string value = env_value ? env_value : "";
|
|
|
|
result = result.replace(match.position(), match.length(), value);
|
|
}
|
|
|
|
// dequote the result
|
|
return result;
|
|
}
|
|
|
|
|
|
std::string substitute_provided_key_value_pairs(std::string str, const ordered_env_vars& env_vars)
|
|
{
|
|
// Combined regex pattern for both ${var} and $var formats
|
|
std::regex var_pattern("\\$(?:\\{([^}]+)\\}|([a-zA-Z0-9_]+))");
|
|
std::string result = str;
|
|
std::smatch match;
|
|
|
|
while (std::regex_search(result, match, var_pattern)) {
|
|
// match[1] will contain capture from ${var} format
|
|
// match[2] will contain capture from $var format
|
|
std::string var_name = match[1].matched ? match[1].str() : match[2].str();
|
|
|
|
// Get value from environment variables
|
|
std::string value = get_var(env_vars, var_name);
|
|
|
|
result = result.replace(match.position(), match.length(), value);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int get_console_width()
|
|
{
|
|
struct winsize w;
|
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
|
|
return w.ws_col;
|
|
}
|
|
// Fallback to a reasonable default if we can't get the width
|
|
return 80;
|
|
}
|
|
|
|
std::string remove_return(std::string str)
|
|
{
|
|
str.erase(std::remove(str.begin(), str.end(), '\n'), str.end());
|
|
return str;
|
|
}
|
|
|
|
std::string get_line_wrap(std::string &src, int maxchars)
|
|
{
|
|
if (src.empty())
|
|
return "";
|
|
|
|
if (src.length() <= maxchars)
|
|
{
|
|
std::string out = src;
|
|
src.erase();
|
|
return remove_return(out) + '\n';
|
|
}
|
|
|
|
// find last whitespace up to but not more than maxchars
|
|
size_t grab_to=maxchars;
|
|
size_t lastreturn = src.rfind('\n', maxchars);
|
|
size_t lastspace = src.rfind(' ', maxchars);
|
|
if (lastreturn != std::string::npos)
|
|
grab_to = lastreturn;
|
|
else if (lastspace != std::string::npos)
|
|
grab_to = lastspace;
|
|
|
|
std::string out = src.substr(0, grab_to);
|
|
src = src.substr(grab_to + 1);
|
|
return remove_return(out) + '\n';
|
|
}
|
|
|
|
std::string tolower(const std::string& str) {
|
|
if (str.empty()) return str;
|
|
|
|
std::string result;
|
|
result.reserve(str.size()); // Pre-allocate space for efficiency
|
|
|
|
for (unsigned char c : str) {
|
|
result.push_back(std::tolower(c));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Common utility function to make HTTP requests
|
|
struct HttpResult {
|
|
bool success;
|
|
int status;
|
|
std::string body;
|
|
std::string error;
|
|
};
|
|
|
|
HttpResult make_http_request(const std::string& url) {
|
|
try {
|
|
// Parse the URL to get host and path
|
|
std::string host;
|
|
std::string path;
|
|
size_t protocol_end = url.find("://");
|
|
if (protocol_end != std::string::npos) {
|
|
size_t host_start = protocol_end + 3;
|
|
size_t path_start = url.find('/', host_start);
|
|
if (path_start != std::string::npos) {
|
|
host = url.substr(host_start, path_start - host_start);
|
|
path = url.substr(path_start);
|
|
} else {
|
|
host = url.substr(host_start);
|
|
path = "/";
|
|
}
|
|
} else {
|
|
return {false, 0, "", "Invalid URL format"};
|
|
}
|
|
|
|
// Create HTTP or HTTPS client based on protocol
|
|
std::string protocol = url.substr(0, protocol_end);
|
|
std::unique_ptr<httplib::Client> cli;
|
|
|
|
if (protocol == "https") {
|
|
// For HTTPS, include the full scheme and host
|
|
cli = std::make_unique<httplib::Client>("https://" + host);
|
|
} else if (protocol == "http") {
|
|
cli = std::make_unique<httplib::Client>("http://" + host);
|
|
} else {
|
|
return {false, 0, "", "Unsupported protocol: " + protocol};
|
|
}
|
|
|
|
cli->set_connection_timeout(10); // 10 second timeout
|
|
cli->set_read_timeout(30); // 30 second read timeout
|
|
|
|
// Make GET request
|
|
auto res = cli->Get(path);
|
|
if (!res) {
|
|
return {false, 0, "", "Failed to connect to server"};
|
|
}
|
|
if (res->status != 200) {
|
|
return {false, res->status, "", "HTTP request failed with status " + std::to_string(res->status)};
|
|
}
|
|
|
|
return {true, res->status, res->body, ""};
|
|
} catch (const std::exception& e) {
|
|
return {false, 0, "", std::string("Exception: ") + e.what()};
|
|
}
|
|
}
|
|
|
|
bool download_file(const std::string &url, const std::string &destination) {
|
|
// For HTTPS URLs, use curl as it handles SSL properly in static builds
|
|
if (url.substr(0, 8) == "https://") {
|
|
std::string cmd = "curl -fsSL " + quote(url) + " -o " + quote(destination) + " 2>/dev/null";
|
|
int result = system(cmd.c_str());
|
|
if (result != 0) {
|
|
warning << "Failed to download file from URL: " << url << std::endl;
|
|
return false;
|
|
}
|
|
// Check if file was actually created
|
|
std::ifstream check_file(destination);
|
|
return check_file.good();
|
|
}
|
|
|
|
// For HTTP URLs, use the built-in httplib
|
|
auto result = make_http_request(url);
|
|
if (!result.success) {
|
|
warning << "Failed to download file from URL: " << url << std::endl;
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
std::ofstream out_file(destination, std::ios::binary);
|
|
if (!out_file) {
|
|
warning << "Failed to open file for writing: " << destination << std::endl;
|
|
return false;
|
|
}
|
|
out_file.write(result.body.c_str(), result.body.size());
|
|
out_file.close();
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
warning << "Failed to download file from URL: " << url << std::endl;
|
|
warning << "Exception: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
nlohmann::json get_json_from_url(const std::string &url) {
|
|
auto result = make_http_request(url);
|
|
if (!result.success) {
|
|
warning << "Failed to get JSON from URL: " << url << std::endl;
|
|
return nlohmann::json();
|
|
}
|
|
|
|
try {
|
|
return nlohmann::json::parse(result.body);
|
|
} catch (const nlohmann::json::parse_error& e) {
|
|
warning << "Failed to parse JSON from URL: " << url << std::endl;
|
|
warning << "JSON: " << result.body << std::endl;
|
|
return nlohmann::json();
|
|
}
|
|
}
|
|
|
|
std::string get_string_from_url(const std::string &url) {
|
|
auto result = make_http_request(url);
|
|
if (!result.success) {
|
|
warning << "Failed to get string from URL: " << url << std::endl;
|
|
return std::string();
|
|
}
|
|
return result.body;
|
|
}
|
|
|
|
bool match_line(const std::string &line, const std::string &pattern) {
|
|
return trim(line) == trim(pattern);
|
|
}
|
|
|
|
// replace or append a block of text to a file, matching first and last lines if replacing.
|
|
// edits file in-place.
|
|
bool file_replace_or_add_segment(std::string filepath, std::string segment)
|
|
{
|
|
// Create a backup of the original file
|
|
std::string backup_path = filepath + ".bak";
|
|
try {
|
|
std::filesystem::copy_file(filepath, backup_path, std::filesystem::copy_options::overwrite_existing);
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Error creating backup file: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Handle empty segment
|
|
if (segment.empty()) {
|
|
error << "Empty segment provided" << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// split the segment into lines
|
|
std::vector<std::string> segment_lines = split(segment, "\n");
|
|
// remove empty lines
|
|
segment_lines.erase(std::remove_if(segment_lines.begin(), segment_lines.end(), [](const std::string& line) {
|
|
return trim(line).empty();
|
|
}), segment_lines.end());
|
|
// remove any lines that are just whitespace
|
|
segment_lines.erase(std::remove_if(segment_lines.begin(), segment_lines.end(), [](const std::string& line) { return trim(line).empty(); }), segment_lines.end());
|
|
|
|
// check that the segment has at least two lines
|
|
if (segment_lines.size() < 2) {
|
|
error << "Segment must contain at least two non-empty lines" << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// Read the entire file into memory
|
|
std::ifstream input_file(filepath);
|
|
if (!input_file.is_open()) {
|
|
error << "Unable to open file: " << filepath << std::endl;
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::string> file_lines;
|
|
std::string line;
|
|
while (std::getline(input_file, line)) {
|
|
file_lines.push_back(line);
|
|
}
|
|
input_file.close();
|
|
|
|
// Store original file size for verification
|
|
size_t original_size = file_lines.size();
|
|
if (original_size == 0) {
|
|
warning << "File is empty" << std::endl;
|
|
}
|
|
|
|
// Try to find the matching block
|
|
bool found_match = false;
|
|
for (size_t i = 0; i < file_lines.size(); i++) {
|
|
if (match_line(file_lines[i], segment_lines[0])) {
|
|
// Found potential start, look for end
|
|
for (size_t j = i + 1; j < file_lines.size(); j++) {
|
|
if (match_line(file_lines[j], segment_lines[segment_lines.size() - 1])) {
|
|
// Found matching block, replace it
|
|
file_lines.erase(file_lines.begin() + i, file_lines.begin() + j + 1);
|
|
file_lines.insert(file_lines.begin() + i, segment_lines.begin(), segment_lines.end());
|
|
|
|
found_match = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found_match) break;
|
|
}
|
|
}
|
|
|
|
// If no match found, append the segment
|
|
if (!found_match) {
|
|
file_lines.insert(file_lines.end(), segment_lines.begin(), segment_lines.end());
|
|
}
|
|
|
|
// Write back to file
|
|
std::ofstream output_file(filepath);
|
|
if (!output_file.is_open()) {
|
|
error << "Unable to open file for writing: " << filepath << std::endl;
|
|
return false;
|
|
}
|
|
|
|
for (const auto& line : file_lines) {
|
|
output_file << line << "\n";
|
|
}
|
|
output_file.close();
|
|
|
|
// If everything succeeded, remove the backup
|
|
try {
|
|
std::filesystem::remove(backup_path);
|
|
} catch (const std::exception& e) {
|
|
warning << "Could not remove backup file: " << e.what() << std::endl;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool legal_service_name(const std::string &service_name) {
|
|
static bool initialized = false;
|
|
static bool legal_chars[256] = {false}; // Initialize all to false
|
|
|
|
// One-time initialization
|
|
if (!initialized) {
|
|
// Set true for valid characters
|
|
for (unsigned char c : "0123456789"
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
"._-") {
|
|
legal_chars[c] = true;
|
|
}
|
|
initialized = true;
|
|
}
|
|
|
|
return std::all_of(service_name.begin(), service_name.end(),
|
|
[](unsigned char c) { return legal_chars[c]; });
|
|
}
|
|
|
|
} // namespace dropshell
|