365 lines · 12.0 KB · C++
RawDownload
#include "config.h" #include <algorithm>#include <cctype>#include <iterator>#include <set>#include <string>#include <utility>#include <vector>#include <torrent/exceptions.h>#include <torrent/object.h> #include "rpc/parse.h"#include "rpc/rpc_manager.h" #include "globals.h"#include "control.h"#include "command_helpers.h" namespace { constexpr int64_t max_string_pad_size = 1 << 14; const std::string whitespace_characters = " \t\n\r\f\v"; // The byte offset of every utf-8 character in 'text', terminated by the offset// past the last character. Bytes that are not valid utf-8 lead bytes are// treated as single characters.std::vector<size_t>utf8_offsets(const std::string& text) {  std::vector<size_t> offsets;   for (size_t i = 0; i < text.size(); i++)    if (i == 0 || (static_cast<unsigned char>(text[i]) & 0xC0) != 0x80)      offsets.push_back(i);   offsets.push_back(text.size());  return offsets;} int64_tutf8_length(const std::string& text) {  int64_t result = 0;   for (size_t i = 0; i < text.size(); i++)    if (i == 0 || (static_cast<unsigned char>(text[i]) & 0xC0) != 0x80)      result++;   return result;} std::stringutf8_substr(const std::string& text, const std::vector<size_t>& offsets, size_t first, size_t last) {  return text.substr(offsets[first], offsets[last] - offsets[first]);} std::set<std::string>utf8_character_set(const std::string& text) {  auto offsets = utf8_offsets(text);  std::set<std::string> result;   for (size_t i = 0; i + 1 < offsets.size(); i++)    result.insert(utf8_substr(text, offsets, i, i + 1));   return result;} std::stringascii_lowercase(std::string text) {  std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return std::tolower(c); });  return text;} // A 'max_count' of zero means the command accepts any number of arguments.voidverify_argument_count(const char* name, const torrent::Object::list_type& args, size_t min_count, size_t max_count) {  if (args.size() < min_count || (max_count != 0 && args.size() > max_count))    throw torrent::input_error(std::string(name) + ": invalid number of arguments.");} const torrent::Object&argument_at(const torrent::Object::list_type& args, size_t index) {  return *std::next(args.begin(), index);} voidflatten_argument(const torrent::Object& arg, std::vector<std::string>* dest) {  if (!arg.is_list()) {    dest->push_back(rpc::convert_to_string(arg));    return;  }   for (const auto& child : arg.as_list())    flatten_argument(child, dest);} // The {old, new} pairs shared by 'string.map' and 'string.replace'.std::pair<std::string, std::string>argument_to_pair(const char* name, const torrent::Object& arg) {  if (!arg.is_list() || arg.as_list().size() != 2)    throw torrent::input_error(std::string(name) + ": arguments after the text must be {old, new} pairs.");   return {rpc::convert_to_string(arg.as_list().front()), rpc::convert_to_string(arg.as_list().back())};} // Compares the first argument against every remaining argument, returning true// as soon as one of them matches.torrent::Objectapply_string_predicate(const char* name, const torrent::Object::list_type& args, bool (*predicate)(const std::string&, const std::string&)) {  verify_argument_count(name, args, 2, 0);   auto text = rpc::convert_to_string(args.front());   for (auto itr = std::next(args.begin()); itr != args.end(); itr++)    if (predicate(text, rpc::convert_to_string(*itr)))      return int64_t(1);   return int64_t(0);} booltext_equals(const std::string& text, const std::string& other) {  return text == other;} booltext_starts_with(const std::string& text, const std::string& prefix) {  return text.size() >= prefix.size() && text.compare(0, prefix.size(), prefix) == 0;} booltext_ends_with(const std::string& text, const std::string& tail) {  return text.size() >= tail.size() && text.compare(text.size() - tail.size(), tail.size(), tail) == 0;} booltext_contains(const std::string& text, const std::string& needle) {  return text.find(needle) != std::string::npos;} booltext_contains_i(const std::string& text, const std::string& needle) {  return ascii_lowercase(text).find(ascii_lowercase(needle)) != std::string::npos;} torrent::Objectapply_string_length(const torrent::Object::list_type& args) {  verify_argument_count("string.length", args, 1, 1);   return utf8_length(rpc::convert_to_string(args.front()));} torrent::Objectapply_string_substr(const torrent::Object::list_type& args) {  verify_argument_count("string.substr", args, 1, 4);   auto text        = rpc::convert_to_string(args.front());  auto offsets     = utf8_offsets(text);  auto text_length = static_cast<int64_t>(offsets.size() - 1);   auto position = args.size() > 1 ? rpc::convert_to_value(argument_at(args, 1)) : 0;  auto fallback = args.size() > 3 ? rpc::convert_to_string(argument_at(args, 3)) : std::string();   // Negative positions are relative to the end of the string.  if (position < 0)    position += text_length;   if (position < 0 || position > text_length)    return fallback;   auto last = text_length;   if (args.size() > 2) {    auto count = rpc::convert_to_value(argument_at(args, 2));     if (count < 0)      throw torrent::input_error("string.substr: the character count cannot be negative.");     last = std::min(text_length, position + std::min(count, text_length));  }   return utf8_substr(text, offsets, position, last);} torrent::Objectapply_string_split(const torrent::Object::list_type& args) {  verify_argument_count("string.split", args, 2, 2);   auto text   = rpc::convert_to_string(args.front());  auto delim  = rpc::convert_to_string(args.back());  auto result = torrent::Object::create_list();   // An empty delimiter splits the text into its utf-8 characters.  if (delim.empty()) {    auto offsets = utf8_offsets(text);     for (size_t i = 0; i + 1 < offsets.size(); i++)      result.as_list().push_back(utf8_substr(text, offsets, i, i + 1));     return result;  }   size_t first = 0;   while (true) {    auto pos = text.find(delim, first);     if (pos == std::string::npos) {      result.as_list().push_back(text.substr(first));      return result;    }     result.as_list().push_back(text.substr(first, pos - first));    first = pos + delim.size();  }} torrent::Objectapply_string_join(const torrent::Object::list_type& args) {  verify_argument_count("string.join", args, 1, 0);   auto delim = rpc::convert_to_string(args.front());   std::vector<std::string> parts;   for (auto itr = std::next(args.begin()); itr != args.end(); itr++)    flatten_argument(*itr, &parts);   std::string result;   for (auto itr = parts.begin(); itr != parts.end(); itr++) {    if (itr != parts.begin())      result += delim;     result += *itr;  }   return result;} torrent::Objectapply_string_pad(const char* name, const torrent::Object::list_type& args, bool pad_start) {  verify_argument_count(name, args, 2, 3);   auto text     = rpc::convert_to_string(args.front());  auto pad_size = rpc::convert_to_value(argument_at(args, 1));  auto padding  = args.size() > 2 ? rpc::convert_to_string(argument_at(args, 2)) : std::string(" ");   auto text_length = utf8_length(text);   if (pad_size <= text_length || padding.empty())    return text;   if (pad_size - text_length > max_string_pad_size)    throw torrent::input_error(std::string(name) + ": padding is too large.");   auto padding_offsets = utf8_offsets(padding);  auto padding_length  = static_cast<int64_t>(padding_offsets.size() - 1);   std::string result;  result.reserve(pad_size - text_length);   for (int64_t i = 0; i < pad_size - text_length; i++) {    auto index = static_cast<size_t>(i % padding_length);    result += utf8_substr(padding, padding_offsets, index, index + 1);  }   return pad_start ? result + text : text + result;} torrent::Objectapply_string_strip(const char* name, const torrent::Object::list_type& args, bool strip_start, bool strip_end) {  verify_argument_count(name, args, 1, 0);   auto text = rpc::convert_to_string(args.front());   std::string strippable;   for (auto itr = std::next(args.begin()); itr != args.end(); itr++)    strippable += rpc::convert_to_string(*itr);   if (args.size() == 1)    strippable = whitespace_characters;   auto characters = utf8_character_set(strippable);  auto offsets    = utf8_offsets(text);   size_t first = 0;  size_t last  = offsets.size() - 1;   while (strip_start && first < last && characters.count(utf8_substr(text, offsets, first, first + 1)) != 0)    first++;   while (strip_end && last > first && characters.count(utf8_substr(text, offsets, last - 1, last)) != 0)    last--;   return utf8_substr(text, offsets, first, last);} torrent::Objectapply_string_map(const torrent::Object::list_type& args) {  verify_argument_count("string.map", args, 2, 0);   auto text = rpc::convert_to_string(args.front());   for (auto itr = std::next(args.begin()); itr != args.end(); itr++) {    auto pair = argument_to_pair("string.map", *itr);     if (text == pair.first)      return pair.second;  }   return text;} torrent::Objectapply_string_replace(const torrent::Object::list_type& args) {  verify_argument_count("string.replace", args, 2, 0);   auto text = rpc::convert_to_string(args.front());   for (auto itr = std::next(args.begin()); itr != args.end(); itr++) {    auto pair = argument_to_pair("string.replace", *itr);     if (pair.first.empty())      throw torrent::input_error("string.replace: the replaced text cannot be empty.");     for (auto pos = text.find(pair.first); pos != std::string::npos; pos = text.find(pair.first, pos + pair.second.size()))      text.replace(pos, pair.first.size(), pair.second);  }   return text;} } voidinitialize_command_string() {  // clang-format off  CMD2_ANY_LIST("string.length",        [](auto, const auto& args) { return apply_string_length(args); });  CMD2_ANY_LIST("string.substr",     [](auto, const auto& args) { return apply_string_substr(args); });  CMD2_ANY_LIST("string.split",      [](auto, const auto& args) { return apply_string_split(args); });  CMD2_ANY_LIST("string.join",       [](auto, const auto& args) { return apply_string_join(args); });  CMD2_ANY_LIST("string.map",        [](auto, const auto& args) { return apply_string_map(args); });  CMD2_ANY_LIST("string.replace",    [](auto, const auto& args) { return apply_string_replace(args); });   CMD2_ANY_LIST("string.equals",     [](auto, const auto& args) { return apply_string_predicate("string.equals", args, &text_equals); });  CMD2_ANY_LIST("string.starts_with", [](auto, const auto& args) { return apply_string_predicate("string.starts_with", args, &text_starts_with); });  CMD2_ANY_LIST("string.ends_with",   [](auto, const auto& args) { return apply_string_predicate("string.ends_with", args, &text_ends_with); });  CMD2_ANY_LIST("string.contains",   [](auto, const auto& args) { return apply_string_predicate("string.contains", args, &text_contains); });  CMD2_ANY_LIST("string.contains_i", [](auto, const auto& args) { return apply_string_predicate("string.contains_i", args, &text_contains_i); });   CMD2_ANY_LIST("string.lpad",       [](auto, const auto& args) { return apply_string_pad("string.lpad", args, true); });  CMD2_ANY_LIST("string.rpad",       [](auto, const auto& args) { return apply_string_pad("string.rpad", args, false); });   CMD2_ANY_LIST("string.strip",      [](auto, const auto& args) { return apply_string_strip("string.strip", args, true, true); });  CMD2_ANY_LIST("string.lstrip",     [](auto, const auto& args) { return apply_string_strip("string.lstrip", args, true, false); });  CMD2_ANY_LIST("string.rstrip",     [](auto, const auto& args) { return apply_string_strip("string.rstrip", args, false, true); });  // clang-format on   for (const auto name : {"string.length", "string.substr", "string.split", "string.join", "string.map", "string.replace",                          "string.equals", "string.starts_with", "string.ends_with", "string.contains", "string.contains_i",                          "string.lpad", "string.rpad", "string.strip", "string.lstrip", "string.rstrip"})    rpc::rpc.mark_safe(name);}