mirror of
https://github.com/rakshasa/rtorrent.git
synced 2026-08-08 03:02:30 +00:00
Compare commits
7 Commits
e56bffff4d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ae6bbc394 | |||
| fdfca98f1c | |||
| 31196ff5f0 | |||
| cfb9e083d2 | |||
| 5b4b607c98 | |||
| 87db1cf0f0 | |||
| dd3ddf7c39 |
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# 2. Check for multiple consecutive blank lines
|
||||
# This regex looks for 2 or more empty lines anywhere in the message
|
||||
if echo "$CLEAN_MSG" | grep -pz '(\r?\n){3,}'; then
|
||||
if echo "$CLEAN_MSG" | grep -Pz '(\r?\n){3,}'; then
|
||||
echo "❌ Error: Commit message contains multiple consecutive line breaks."
|
||||
echo " Commit: '$SUBJECT'"
|
||||
FAILED=1
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
m4_pattern_allow([PKG_CHECK_EXISTS])
|
||||
|
||||
AC_INIT([rtorrent],[0.16.19],[sundell.software@gmail.com])
|
||||
AC_INIT([rtorrent],[0.16.20],[sundell.software@gmail.com])
|
||||
|
||||
AC_CONFIG_HEADERS([config.h])
|
||||
AC_CONFIG_MACRO_DIRS([scripts])
|
||||
@@ -14,7 +14,7 @@ AX_CXX_COMPILE_STDCXX(20, noext, mandatory)
|
||||
|
||||
PKG_PROG_PKG_CONFIG
|
||||
|
||||
AC_DEFINE([API_VERSION], [24], [api version])
|
||||
AC_DEFINE([API_VERSION], [25], [api version])
|
||||
|
||||
RAK_CHECK_CFLAGS
|
||||
RAK_CHECK_CXXFLAGS
|
||||
@@ -47,7 +47,7 @@ fi
|
||||
|
||||
PKG_CHECK_MODULES([CPPUNIT], [cppunit],, [no_cppunit="yes"])
|
||||
PKG_CHECK_MODULES([ZLIB], [zlib])
|
||||
PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.19])
|
||||
PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.20])
|
||||
|
||||
AC_LANG_PUSH(C++)
|
||||
TORRENT_WITH_XMLRPC_C
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Resolving paths
|
||||
|
||||
Commands that return a path have `.realpath` variants that resolve it to a
|
||||
canonical one, with symlinks followed and any `.` or `..` components removed.
|
||||
|
||||
The intent is to make paths safer to hand to an external script.
|
||||
A script called through `execute` receives whatever path rtorrent gives it, and
|
||||
many scripts do no sanity checking of their own, so resolving the path before it
|
||||
leaves rtorrent removes a class of surprises: a download directory that is a
|
||||
symlink into somewhere unexpected, or a torrent whose name walks upwards out of
|
||||
the directory it is supposed to live in.
|
||||
|
||||
# Instead of this
|
||||
execute = ~/bin/on-finished, (d.base_path)
|
||||
|
||||
# Pass the resolved path
|
||||
execute = ~/bin/on-finished, (d.base_path.realpath.or_throw)
|
||||
|
||||
## Available variants
|
||||
|
||||
Each command below comes in an `.or_empty` and an `.or_throw` form.
|
||||
|
||||
| Command | Resolves |
|
||||
| --- | --- |
|
||||
| `d.base_path.realpath.*` | `d.base_path` |
|
||||
| `d.directory.realpath.*` | `d.directory` |
|
||||
| `d.tied_to_file.realpath.*` | `d.tied_to_file` |
|
||||
| `d.loaded_file.realpath.*` | `d.loaded_file` |
|
||||
| `f.frozen_path.realpath.*` | `f.frozen_path` |
|
||||
| `session.path.realpath.*` | `session.path` |
|
||||
| `directory.default.realpath.*` | `directory.default` |
|
||||
|
||||
A leading `~` is expanded first, exactly as it is for `execute`, so
|
||||
`~/downloads` resolves the same way it would on the command line.
|
||||
|
||||
## Paths that do not exist
|
||||
|
||||
Resolving requires the path to name an existing file or directory, which is
|
||||
often not the case for a download whose data has not been written yet.
|
||||
The two forms differ only in what they do about it.
|
||||
|
||||
`.or_empty` returns an empty string:
|
||||
|
||||
print = (d.base_path.realpath.or_empty) # ""
|
||||
|
||||
This keeps `d.multicall` usable over a view that mixes started and unstarted
|
||||
downloads, since a single unresolvable path does not abort the whole call.
|
||||
A script receiving one of these paths should still check that it is not empty
|
||||
before acting on it.
|
||||
|
||||
`.or_throw` raises an error instead:
|
||||
|
||||
print = (d.base_path.realpath.or_throw) # Could not resolve path: '...'
|
||||
|
||||
Prefer this one wherever an unresolvable path means the command should not run
|
||||
at all, such as a single `execute` on `event.download.finished`.
|
||||
|
||||
Note that a download only has a file list once it has been opened, so
|
||||
`d.base_path` and `f.frozen_path` are empty at `event.download.inserted` time
|
||||
and their `.realpath` variants resolve nothing.
|
||||
@@ -0,0 +1,139 @@
|
||||
# String functions
|
||||
|
||||
The `string.*` commands manipulate and inspect text from within the
|
||||
configuration file.
|
||||
They are pure functions with no side effects, so they can be nested freely and
|
||||
are safe to call over RPC.
|
||||
|
||||
Every argument is converted to its string representation before use, which means
|
||||
numbers can be passed where text is expected.
|
||||
Commands that count characters, such as `string.length` and `string.substr`, count
|
||||
utf-8 characters rather than bytes.
|
||||
|
||||
## string.length
|
||||
|
||||
# string.length = «text»
|
||||
|
||||
string.length = "héllo" # 5
|
||||
|
||||
Returns the number of utf-8 characters in the text.
|
||||
|
||||
## string.equals
|
||||
|
||||
# string.equals = «text», «other»[, ...]
|
||||
|
||||
string.equals = (d.name), "first.iso", "second.iso"
|
||||
|
||||
Returns `1` if the first argument equals any of the following arguments,
|
||||
otherwise `0`.
|
||||
|
||||
## string.starts_with, string.ends_with
|
||||
|
||||
# string.starts_with = «text», «prefix»[, ...]
|
||||
# string.ends_with = «text», «tail»[, ...]
|
||||
|
||||
string.starts_with = (t.url), "http://", "https://"
|
||||
string.ends_with = (d.name), ".iso"
|
||||
|
||||
Returns `1` if the text begins, or ends, with any of the given prefixes or
|
||||
tails.
|
||||
|
||||
## string.contains, string.contains_i
|
||||
|
||||
# string.contains = «haystack», «needle»[, ...]
|
||||
# string.contains_i = «haystack», «needle»[, ...]
|
||||
|
||||
string.contains = (t.url), "retracker.local"
|
||||
string.contains_i = (t.url), "RETRACKER.local"
|
||||
|
||||
Returns `1` if the haystack contains any of the needles.
|
||||
The `_i` variant compares case-insensitively, and only handles ascii.
|
||||
|
||||
## string.substr
|
||||
|
||||
# string.substr = «text»[, «position»[, «count»[, «default»]]]
|
||||
|
||||
string.substr = "abcdef", 2, 3 # "cde"
|
||||
string.substr = "abcdef", -2 # "ef"
|
||||
string.substr = "abcdef", 10, 1, "?" # "?"
|
||||
|
||||
Extracts a part of the text, starting at `position` and spanning `count`
|
||||
characters.
|
||||
The position defaults to the start of the text, and the count to the rest of it.
|
||||
A negative position is relative to the end of the text.
|
||||
If the position falls outside the text, the default value is returned instead,
|
||||
which is the empty string unless given.
|
||||
|
||||
## string.split
|
||||
|
||||
# string.split = «text», «delimiter»
|
||||
|
||||
string.split = "a.b.c", "." # {"a", "b", "c"}
|
||||
string.split = "abc", "" # {"a", "b", "c"}
|
||||
|
||||
Splits the text into a list, keeping empty fields.
|
||||
An empty delimiter splits the text into its utf-8 characters.
|
||||
|
||||
## string.join
|
||||
|
||||
# string.join = «delimiter»[, «object»[, ...]]
|
||||
|
||||
string.join = "-", (string.split, "a.b.c", ".") # "a-b-c"
|
||||
|
||||
Concatenates the objects, inserting the delimiter between them.
|
||||
Lists are flattened, so the result of `string.split` can be joined back
|
||||
together.
|
||||
|
||||
## string.lpad, string.rpad
|
||||
|
||||
# string.lpad = «text», «length»[, «padding»]
|
||||
# string.rpad = «text», «length»[, «padding»]
|
||||
|
||||
string.lpad = 7, 3, 0 # "007"
|
||||
string.rpad = "a", 3 # "a "
|
||||
|
||||
Pads the text at the start, or the end, until it is `length` characters long.
|
||||
The padding defaults to a single space and is repeated as needed.
|
||||
Text that is already long enough is returned unchanged.
|
||||
|
||||
## string.strip, string.lstrip, string.rstrip
|
||||
|
||||
# string.strip = «text»[, «strippable»[, ...]]
|
||||
# string.lstrip = «text»[, «head»[, ...]]
|
||||
# string.rstrip = «text»[, «tail»[, ...]]
|
||||
|
||||
string.strip = " padded " # "padded"
|
||||
string.strip = "//path//", "/" # "path"
|
||||
|
||||
Removes characters from both ends of the text, or from only the start or the
|
||||
end.
|
||||
The arguments after the text form a set of utf-8 characters to remove.
|
||||
Without them, whitespace is removed.
|
||||
|
||||
## string.map
|
||||
|
||||
# string.map = «text», {«old», «new»}[, ...]
|
||||
|
||||
string.map = (d.state), {0, "stopped"}, {1, "started"}
|
||||
|
||||
Returns the replacement of the first pair whose `old` value equals the whole
|
||||
text.
|
||||
If no pair matches, the text is returned unchanged.
|
||||
|
||||
## string.replace
|
||||
|
||||
# string.replace = «text», {«old», «new»}[, ...]
|
||||
|
||||
string.replace = "a-b-c", {"-", "+"} # "a+b+c"
|
||||
|
||||
Replaces every occurrence of `old` with `new`.
|
||||
The pairs are applied in order, so a later pair operates on the result of the
|
||||
earlier ones.
|
||||
|
||||
## Example: dropping unwanted trackers
|
||||
|
||||
The following disables every tracker that points at `retracker.local` as soon as
|
||||
a download is inserted.
|
||||
|
||||
method.set_key = event.download.inserted, drop_retracker, \
|
||||
((t.multicall, default, "branch=(string.contains,(t.url),retracker.local),((t.disable))"))
|
||||
@@ -194,6 +194,7 @@ libsub_root_a_SOURCES = \
|
||||
command_throttle.cc \
|
||||
command_tracker.cc \
|
||||
command_scheduler.cc \
|
||||
command_string.cc \
|
||||
command_ui.cc \
|
||||
control.cc \
|
||||
control.h \
|
||||
|
||||
@@ -655,6 +655,8 @@ initialize_command_download() {
|
||||
CMD2_DL("d.base_path.as_binary", [](auto* download, auto) { return retrieve_d_base_path(download).object_as_binary(); });
|
||||
CMD2_DL("d.base_path.or_base64", [](auto* download, auto) { return retrieve_d_base_path(download).object_utf8_or_base64(); });
|
||||
CMD2_DL("d.base_path.or_as_binary", [](auto* download, auto) { return retrieve_d_base_path(download).object_utf8_or_as_binary(); });
|
||||
CMD2_DL("d.base_path.realpath.or_empty", [](auto* download, auto) { return resolve_path(retrieve_d_base_path(download).str()); });
|
||||
CMD2_DL("d.base_path.realpath.or_throw", [](auto* download, auto) { return resolve_path_or_throw(retrieve_d_base_path(download).str()); });
|
||||
CMD2_DL("d.base_filename", [](auto* download, auto) { return retrieve_d_base_filename(download).str(); });
|
||||
CMD2_DL("d.base_filename.hex", [](auto* download, auto) { return retrieve_d_base_filename(download).object_hex(); });
|
||||
CMD2_DL("d.base_filename.base64", [](auto* download, auto) { return retrieve_d_base_filename(download).object_base64(); });
|
||||
@@ -771,6 +773,11 @@ initialize_command_download() {
|
||||
CMD2_DL_VAR_STRING_PUBLIC("d.tied_to_file", "rtorrent", "tied_to_file");
|
||||
CMD2_DL_VAR_STRING("d.loaded_file", "rtorrent", "loaded_file");
|
||||
|
||||
CMD2_DL("d.tied_to_file.realpath.or_empty", [](auto* download, auto) { return resolve_path(rpc::convert_to_string(download_get_variable(download, "rtorrent", "tied_to_file"))); });
|
||||
CMD2_DL("d.tied_to_file.realpath.or_throw", [](auto* download, auto) { return resolve_path_or_throw(rpc::convert_to_string(download_get_variable(download, "rtorrent", "tied_to_file"))); });
|
||||
CMD2_DL("d.loaded_file.realpath.or_empty", [](auto* download, auto) { return resolve_path(rpc::convert_to_string(download_get_variable(download, "rtorrent", "loaded_file"))); });
|
||||
CMD2_DL("d.loaded_file.realpath.or_throw", [](auto* download, auto) { return resolve_path_or_throw(rpc::convert_to_string(download_get_variable(download, "rtorrent", "loaded_file"))); });
|
||||
|
||||
// The "state_changed" variable is required to be a valid unix time
|
||||
// value, it indicates the last time the torrent changed its state,
|
||||
// resume/pause.
|
||||
@@ -878,6 +885,8 @@ initialize_command_download() {
|
||||
CMD2_DL_VALUE_V ("d.tracker.send_scrape", [](auto download, uint64_t arg) { download->tracker_controller().scrape_request(arg); });
|
||||
|
||||
CMD2_DL ("d.directory", CMD2_ON_FL(root_dir));
|
||||
CMD2_DL ("d.directory.realpath.or_empty", [](auto* download, auto) { return resolve_path(download->file_list()->root_dir()); });
|
||||
CMD2_DL ("d.directory.realpath.or_throw", [](auto* download, auto) { return resolve_path_or_throw(download->file_list()->root_dir()); });
|
||||
CMD2_DL_STRING_V("d.directory.set", std::bind(&apply_d_directory, std::placeholders::_1, std::placeholders::_2));
|
||||
CMD2_DL ("d.directory_base", CMD2_ON_FL(root_dir));
|
||||
CMD2_DL_STRING_V("d.directory_base.set", std::bind(&core::Download::set_root_directory, std::placeholders::_1, std::placeholders::_2));
|
||||
@@ -912,6 +921,14 @@ initialize_command_download() {
|
||||
rpc::rpc.mark_safe("d.local_id_html");
|
||||
rpc::rpc.mark_safe("d.bitfield");
|
||||
rpc::rpc.mark_safe("d.base_path");
|
||||
rpc::rpc.mark_safe("d.base_path.realpath.or_empty");
|
||||
rpc::rpc.mark_safe("d.base_path.realpath.or_throw");
|
||||
rpc::rpc.mark_safe("d.directory.realpath.or_empty");
|
||||
rpc::rpc.mark_safe("d.directory.realpath.or_throw");
|
||||
rpc::rpc.mark_safe("d.tied_to_file.realpath.or_empty");
|
||||
rpc::rpc.mark_safe("d.tied_to_file.realpath.or_throw");
|
||||
rpc::rpc.mark_safe("d.loaded_file.realpath.or_empty");
|
||||
rpc::rpc.mark_safe("d.loaded_file.realpath.or_throw");
|
||||
rpc::rpc.mark_safe("d.base_path.hex");
|
||||
rpc::rpc.mark_safe("d.base_path.base64");
|
||||
rpc::rpc.mark_safe("d.base_path.base64_as_binary");
|
||||
|
||||
@@ -116,6 +116,8 @@ initialize_command_file() {
|
||||
CMD2_FILE("f.frozen_path.as_binary", [](auto* file, auto) { return file->frozen_path().object_as_binary(); });
|
||||
CMD2_FILE("f.frozen_path.or_base64", [](auto* file, auto) { return file->frozen_path().object_utf8_or_base64(); });
|
||||
CMD2_FILE("f.frozen_path.or_as_binary", [](auto* file, auto) { return file->frozen_path().object_utf8_or_as_binary(); });
|
||||
CMD2_FILE("f.frozen_path.realpath.or_empty", [](auto* file, auto) { return resolve_path(file->frozen_path().str()); });
|
||||
CMD2_FILE("f.frozen_path.realpath.or_throw", [](auto* file, auto) { return resolve_path_or_throw(file->frozen_path().str()); });
|
||||
|
||||
CMD2_FILE("f.match_depth_prev", std::bind(&torrent::File::match_depth_prev, std::placeholders::_1));
|
||||
CMD2_FILE("f.match_depth_next", std::bind(&torrent::File::match_depth_next, std::placeholders::_1));
|
||||
@@ -129,6 +131,8 @@ initialize_command_file() {
|
||||
rpc::rpc.mark_safe("f.path_components");
|
||||
rpc::rpc.mark_safe("f.path_depth");
|
||||
rpc::rpc.mark_safe("f.frozen_path");
|
||||
rpc::rpc.mark_safe("f.frozen_path.realpath.or_empty");
|
||||
rpc::rpc.mark_safe("f.frozen_path.realpath.or_throw");
|
||||
rpc::rpc.mark_safe("f.frozen_path.hex");
|
||||
rpc::rpc.mark_safe("f.frozen_path.base64");
|
||||
rpc::rpc.mark_safe("f.frozen_path.base64_as_binary");
|
||||
|
||||
@@ -19,6 +19,7 @@ void initialize_command_groups();
|
||||
void initialize_command_throttle();
|
||||
void initialize_command_tracker();
|
||||
void initialize_command_scheduler();
|
||||
void initialize_command_string();
|
||||
void initialize_command_ui();
|
||||
|
||||
void
|
||||
@@ -37,4 +38,5 @@ initialize_commands() {
|
||||
initialize_command_throttle();
|
||||
initialize_command_tracker();
|
||||
initialize_command_scheduler();
|
||||
initialize_command_string();
|
||||
}
|
||||
|
||||
@@ -296,9 +296,13 @@ initialize_command_local() {
|
||||
CMD_VAR_BOOL ("pieces.hash.on_completion", true);
|
||||
|
||||
CMD_VAR_STRING ("directory.default", "./");
|
||||
CMD_ANY ("directory.default.realpath.or_empty", [](auto, auto) { return resolve_path(rpc::call_command_string("directory.default")); });
|
||||
CMD_ANY ("directory.default.realpath.or_throw", [](auto, auto) { return resolve_path_or_throw(rpc::call_command_string("directory.default")); });
|
||||
|
||||
CMD_VAR_STRING ("session.name", "");
|
||||
CMD_ANY ("session.path", [](auto, auto) { return session_thread::manager()->path(); });
|
||||
CMD_ANY ("session.path.realpath.or_empty", [](auto, auto) { return resolve_path(session_thread::manager()->path()); });
|
||||
CMD_ANY ("session.path.realpath.or_throw", [](auto, auto) { return resolve_path_or_throw(session_thread::manager()->path()); });
|
||||
CMD_ANY_STRING_V("session.path.set", [](auto, auto& str) { return session_thread::manager()->set_path(str); });
|
||||
CMD_ANY ("session.use_lock", [](auto, auto) { return session_thread::manager()->use_lock(); });
|
||||
CMD_ANY_VALUE_V ("session.use_lock.set", [](auto, auto& value) { return session_thread::manager()->set_use_lock(value); });
|
||||
@@ -372,6 +376,10 @@ initialize_command_local() {
|
||||
|
||||
rpc::rpc.mark_safe("directory.default");
|
||||
rpc::rpc.mark_safe("session.path");
|
||||
rpc::rpc.mark_safe("session.path.realpath.or_empty");
|
||||
rpc::rpc.mark_safe("session.path.realpath.or_throw");
|
||||
rpc::rpc.mark_safe("directory.default.realpath.or_empty");
|
||||
rpc::rpc.mark_safe("directory.default.realpath.or_throw");
|
||||
rpc::rpc.mark_safe("session.use_lock");
|
||||
rpc::rpc.mark_safe("session.on_completion");
|
||||
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
#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 {
|
||||
|
||||
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_t
|
||||
utf8_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::string
|
||||
utf8_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::string
|
||||
ascii_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.
|
||||
void
|
||||
verify_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);
|
||||
}
|
||||
|
||||
void
|
||||
flatten_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::Object
|
||||
apply_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);
|
||||
}
|
||||
|
||||
bool
|
||||
text_equals(const std::string& text, const std::string& other) {
|
||||
return text == other;
|
||||
}
|
||||
|
||||
bool
|
||||
text_starts_with(const std::string& text, const std::string& prefix) {
|
||||
return text.size() >= prefix.size() && text.compare(0, prefix.size(), prefix) == 0;
|
||||
}
|
||||
|
||||
bool
|
||||
text_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;
|
||||
}
|
||||
|
||||
bool
|
||||
text_contains(const std::string& text, const std::string& needle) {
|
||||
return text.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
bool
|
||||
text_contains_i(const std::string& text, const std::string& needle) {
|
||||
return ascii_lowercase(text).find(ascii_lowercase(needle)) != std::string::npos;
|
||||
}
|
||||
|
||||
torrent::Object
|
||||
apply_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::Object
|
||||
apply_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::Object
|
||||
apply_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::Object
|
||||
apply_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::Object
|
||||
apply_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;
|
||||
|
||||
auto padding_offsets = utf8_offsets(padding);
|
||||
auto padding_length = static_cast<int64_t>(padding_offsets.size() - 1);
|
||||
|
||||
std::string result;
|
||||
|
||||
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::Object
|
||||
apply_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::Object
|
||||
apply_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::Object
|
||||
apply_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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
initialize_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);
|
||||
}
|
||||
@@ -345,7 +345,7 @@ print_status_info(char* first, char* last) {
|
||||
if (!torrent::up_throttle_global()->is_throttled()) {
|
||||
first = print_buffer(first, last, "[Throttle off");
|
||||
} else {
|
||||
first = print_buffer(first, last, "[Throttle %3i", torrent::up_throttle_global()->max_rate() / 1024);
|
||||
first = print_buffer(first, last, "[Throttle %3i", (int)(torrent::up_throttle_global()->max_rate() / 1024));
|
||||
|
||||
if (!throttle_up_names.empty())
|
||||
first = print_status_throttle_limit(first, last, true, throttle_up_names);
|
||||
@@ -354,7 +354,7 @@ print_status_info(char* first, char* last) {
|
||||
if (!torrent::down_throttle_global()->is_throttled()) {
|
||||
first = print_buffer(first, last, " / off KB]");
|
||||
} else {
|
||||
first = print_buffer(first, last, " / %3i", torrent::down_throttle_global()->max_rate() / 1024);
|
||||
first = print_buffer(first, last, " / %3i", (int)(torrent::down_throttle_global()->max_rate() / 1024));
|
||||
|
||||
if (!throttle_down_names.empty())
|
||||
first = print_status_throttle_limit(first, last, false, throttle_down_names);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "globals.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <stdlib.h>
|
||||
#include <torrent/exceptions.h>
|
||||
|
||||
rpc::ip_table_list ip_tables;
|
||||
@@ -28,3 +29,32 @@ expand_path(const std::string& path) {
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// Resolves a path to a canonical one with no symlinks or relative components,
|
||||
// so it can safely be handed to an external script. Returns an empty string if
|
||||
// the path does not name an existing file or directory.
|
||||
std::string
|
||||
resolve_path(const std::string& path) {
|
||||
if (path.empty())
|
||||
return std::string();
|
||||
|
||||
char* resolved = ::realpath(expand_path(path).c_str(), nullptr);
|
||||
|
||||
if (resolved == nullptr)
|
||||
return std::string();
|
||||
|
||||
std::string result(resolved);
|
||||
std::free(resolved);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string
|
||||
resolve_path_or_throw(const std::string& path) {
|
||||
auto result = resolve_path(path);
|
||||
|
||||
if (result.empty())
|
||||
throw torrent::input_error("Could not resolve path: '" + path + "'.");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ extern rpc::ip_table_list ip_tables;
|
||||
extern Control* control;
|
||||
|
||||
std::string expand_path(const std::string& path);
|
||||
std::string resolve_path(const std::string& path);
|
||||
std::string resolve_path_or_throw(const std::string& path);
|
||||
|
||||
namespace rpc {
|
||||
class SCgi;
|
||||
|
||||
@@ -451,6 +451,11 @@ XmlRpc::process(const char* inBuffer, uint32_t length, slot_write slotWrite) {
|
||||
if (local_env.fault_occurred && local_env.fault_code == XMLRPC_INTERNAL_ERROR)
|
||||
throw torrent::internal_error("Internal error in XMLRPC.");
|
||||
|
||||
if (memblock == nullptr) {
|
||||
xmlrpc_env_clean(&local_env);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = slotWrite((const char*)xmlrpc_mem_block_contents(memblock),
|
||||
xmlrpc_mem_block_size(memblock));
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ public:
|
||||
|
||||
void wait_for(uint32_t max_remaining);
|
||||
|
||||
// TODO: Add a signal handler to tell the worker thread to wake up. Use a counter to batch wakeups.
|
||||
|
||||
private:
|
||||
WaitpidQueue(const WaitpidQueue&) = delete;
|
||||
WaitpidQueue& operator=(const WaitpidQueue&) = delete;
|
||||
|
||||
@@ -51,6 +51,10 @@ rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \
|
||||
src/test_command_dynamic.h \
|
||||
src/test_command_local.cc \
|
||||
src/test_command_local.h \
|
||||
src/test_command_path.cc \
|
||||
src/test_command_path.h \
|
||||
src/test_command_string.cc \
|
||||
src/test_command_string.h \
|
||||
src/test_watch_ready_queue.cc \
|
||||
src/test_watch_ready_queue.h
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "config.h"
|
||||
|
||||
#include "test/src/test_command_path.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <torrent/exceptions.h>
|
||||
#include <torrent/torrent.h>
|
||||
|
||||
#include "control.h"
|
||||
#include "globals.h"
|
||||
#include "rpc/parse_commands.h"
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(TestCommandPath);
|
||||
|
||||
void initialize_command_local();
|
||||
|
||||
void
|
||||
TestCommandPath::setUp() {
|
||||
char temp_dir[] = "/tmp/rtorrent_test_path_XXXXXX";
|
||||
|
||||
CPPUNIT_ASSERT(mkdtemp(temp_dir) != nullptr);
|
||||
|
||||
// The temporary directory itself may sit behind a symlink, as /tmp does on
|
||||
// macOS, so resolve it up front to keep the expected values exact.
|
||||
m_temp_dir = resolve_path(temp_dir);
|
||||
|
||||
CPPUNIT_ASSERT(!m_temp_dir.empty());
|
||||
CPPUNIT_ASSERT_EQUAL(0, mkdir((m_temp_dir + "/data").c_str(), 0755));
|
||||
CPPUNIT_ASSERT_EQUAL(0, symlink((m_temp_dir + "/data").c_str(), (m_temp_dir + "/link").c_str()));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandPath::tearDown() {
|
||||
unlink((m_temp_dir + "/link").c_str());
|
||||
rmdir((m_temp_dir + "/data").c_str());
|
||||
rmdir(m_temp_dir.c_str());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandPath::test_resolves_symlink() {
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data", resolve_path(m_temp_dir + "/link"));
|
||||
|
||||
// A path that lies below a symlinked directory is resolved as well.
|
||||
CPPUNIT_ASSERT_EQUAL(0, mkdir((m_temp_dir + "/data/below").c_str(), 0755));
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data/below", resolve_path(m_temp_dir + "/link/below"));
|
||||
rmdir((m_temp_dir + "/data/below").c_str());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandPath::test_removes_relative_components() {
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir, resolve_path(m_temp_dir + "/data/.."));
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data", resolve_path(m_temp_dir + "/./data"));
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data", resolve_path(m_temp_dir + "/data/"));
|
||||
|
||||
// Trailing slashes and duplicated separators collapse.
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data", resolve_path(m_temp_dir + "//data//"));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandPath::test_expands_tilde() {
|
||||
const char* home = std::getenv("HOME");
|
||||
|
||||
if (home == nullptr || *home == '\0')
|
||||
return;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(resolve_path(home), resolve_path("~"));
|
||||
CPPUNIT_ASSERT_THROW(resolve_path("~root/somewhere"), torrent::input_error);
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandPath::test_missing_path_throws() {
|
||||
CPPUNIT_ASSERT_THROW(resolve_path_or_throw(""), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(resolve_path_or_throw(m_temp_dir + "/does_not_exist"), torrent::input_error);
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data", resolve_path_or_throw(m_temp_dir + "/link"));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandPath::test_missing_path_is_empty() {
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(), resolve_path(""));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(), resolve_path(m_temp_dir + "/does_not_exist"));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(), resolve_path(m_temp_dir + "/does_not_exist/below"));
|
||||
|
||||
// A dangling symlink does not name an existing path either.
|
||||
CPPUNIT_ASSERT_EQUAL(0, symlink((m_temp_dir + "/gone").c_str(), (m_temp_dir + "/dangling").c_str()));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(), resolve_path(m_temp_dir + "/dangling"));
|
||||
unlink((m_temp_dir + "/dangling").c_str());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandPath::test_commands() {
|
||||
torrent::initialize_main_thread();
|
||||
torrent::initialize();
|
||||
|
||||
if (control == nullptr)
|
||||
control = new Control;
|
||||
|
||||
if (!rpc::commands.has("directory.default.realpath.or_empty"))
|
||||
initialize_command_local();
|
||||
|
||||
rpc::commands.call_command("directory.default.set", m_temp_dir + "/link");
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/link", rpc::commands.call_command("directory.default", torrent::Object()).as_string());
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data", rpc::commands.call_command("directory.default.realpath.or_empty", torrent::Object()).as_string());
|
||||
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/data", rpc::commands.call_command("directory.default.realpath.or_throw", torrent::Object()).as_string());
|
||||
|
||||
// A directory that has not been created yet resolves to nothing, and the
|
||||
// or_throw variant reports it instead.
|
||||
rpc::commands.call_command("directory.default.set", m_temp_dir + "/missing");
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(), rpc::commands.call_command("directory.default.realpath.or_empty", torrent::Object()).as_string());
|
||||
CPPUNIT_ASSERT_THROW(rpc::commands.call_command("directory.default.realpath.or_throw", torrent::Object()), torrent::input_error);
|
||||
|
||||
torrent::cleanup();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "test/helpers/test_fixture.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class TestCommandPath : public test_fixture {
|
||||
CPPUNIT_TEST_SUITE(TestCommandPath);
|
||||
|
||||
CPPUNIT_TEST(test_resolves_symlink);
|
||||
CPPUNIT_TEST(test_removes_relative_components);
|
||||
CPPUNIT_TEST(test_expands_tilde);
|
||||
CPPUNIT_TEST(test_missing_path_is_empty);
|
||||
CPPUNIT_TEST(test_missing_path_throws);
|
||||
CPPUNIT_TEST(test_commands);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void setUp();
|
||||
void tearDown();
|
||||
|
||||
void test_resolves_symlink();
|
||||
void test_removes_relative_components();
|
||||
void test_expands_tilde();
|
||||
void test_missing_path_is_empty();
|
||||
void test_missing_path_throws();
|
||||
void test_commands();
|
||||
|
||||
private:
|
||||
std::string m_temp_dir;
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "config.h"
|
||||
|
||||
#include "test/src/test_command_string.h"
|
||||
|
||||
#include "rpc/parse_commands.h"
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(TestCommandString);
|
||||
|
||||
void initialize_command_string();
|
||||
|
||||
namespace {
|
||||
|
||||
torrent::Object
|
||||
args(std::initializer_list<torrent::Object> objects) {
|
||||
auto result = torrent::Object::create_list();
|
||||
|
||||
for (const auto& object : objects)
|
||||
result.as_list().push_back(object);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string
|
||||
call_string(const char* key, std::initializer_list<torrent::Object> objects) {
|
||||
return rpc::commands.call_command(key, args(objects)).as_string();
|
||||
}
|
||||
|
||||
int64_t
|
||||
call_value(const char* key, std::initializer_list<torrent::Object> objects) {
|
||||
return rpc::commands.call_command(key, args(objects)).as_value();
|
||||
}
|
||||
|
||||
torrent::Object::list_type
|
||||
call_list(const char* key, std::initializer_list<torrent::Object> objects) {
|
||||
return rpc::commands.call_command(key, args(objects)).as_list();
|
||||
}
|
||||
|
||||
// Runs a command the way a line in the configuration file would.
|
||||
torrent::Object
|
||||
parse(const char* command) {
|
||||
return rpc::parse_command_single(rpc::make_target(), command);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::setUp() {
|
||||
if (!rpc::commands.has("string.length"))
|
||||
initialize_command_string();
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::tearDown() {
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_length() {
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.length", {""}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(3), call_value("string.length", {"abc"}));
|
||||
|
||||
// The length is counted in utf-8 characters, not bytes.
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(5), call_value("string.length", {"héllo"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(3), call_value("string.length", {"日本語"}));
|
||||
|
||||
// Values are converted to their string representation.
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(4), call_value("string.length", {int64_t(1234)}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_equals() {
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.equals", {"abc", "abc"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.equals", {"abc", "abd"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.equals", {"abc", "ab"}));
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.equals", {"abc", "x", "abc"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.equals", {"abc", "x", "y"}));
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.equals", {int64_t(42), "42"}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_starts_with() {
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.starts_with", {"abcdef", "abc"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.starts_with", {"abcdef", ""}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.starts_with", {"abcdef", "bcd"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.starts_with", {"ab", "abc"}));
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.starts_with", {"abcdef", "x", "ab"}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_ends_with() {
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.ends_with", {"abcdef", "def"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.ends_with", {"abcdef", ""}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.ends_with", {"abcdef", "cde"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.ends_with", {"ef", "def"}));
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.ends_with", {"a.torrent", ".rar", ".torrent"}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_contains() {
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.contains", {"abcdef", "cde"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.contains", {"abcdef", "ace"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.contains", {"abcdef", "x", "bcd"}));
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.contains", {"retracker.local", "RETRACKER"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.contains_i", {"retracker.local", "RETRACKER"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), call_value("string.contains_i", {"RETRACKER.LOCAL", "retracker"}));
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(0), call_value("string.contains_i", {"abcdef", "xyz"}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_substr() {
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("abcdef"), call_string("string.substr", {"abcdef"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("cdef"), call_string("string.substr", {"abcdef", int64_t(2)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("cde"), call_string("string.substr", {"abcdef", int64_t(2), int64_t(3)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("cdef"), call_string("string.substr", {"abcdef", int64_t(2), int64_t(100)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(""), call_string("string.substr", {"abcdef", int64_t(2), int64_t(0)}));
|
||||
|
||||
// Negative positions are relative to the end of the string.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("ef"), call_string("string.substr", {"abcdef", int64_t(-2)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("e"), call_string("string.substr", {"abcdef", int64_t(-2), int64_t(1)}));
|
||||
|
||||
// Out-of-bounds positions return the default value.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(""), call_string("string.substr", {"abcdef", int64_t(10)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("n/a"), call_string("string.substr", {"abcdef", int64_t(10), int64_t(1), "n/a"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("n/a"), call_string("string.substr", {"abcdef", int64_t(-10), int64_t(1), "n/a"}));
|
||||
|
||||
// Positions and counts are in utf-8 characters, not bytes.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("本"), call_string("string.substr", {"日本語", int64_t(1), int64_t(1)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("本語"), call_string("string.substr", {"日本語", int64_t(-2)}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_split() {
|
||||
auto parts = call_list("string.split", {"a,b,c", ","});
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(3), parts.size());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a"), parts.front().as_string());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("c"), parts.back().as_string());
|
||||
|
||||
// Empty fields are preserved.
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(3), call_list("string.split", {"a,,b", ","}).size());
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(1), call_list("string.split", {"abc", ","}).size());
|
||||
|
||||
// A multi-character delimiter is matched as a whole.
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(2), call_list("string.split", {"a::b", "::"}).size());
|
||||
|
||||
// An empty delimiter splits the text into its utf-8 characters.
|
||||
auto characters = call_list("string.split", {"日本語", ""});
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(size_t(3), characters.size());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("日"), characters.front().as_string());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_join() {
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a, b"), call_string("string.join", {", ", "a", "b"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("ab"), call_string("string.join", {"", "a", "b"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(""), call_string("string.join", {", "}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a"), call_string("string.join", {", ", "a"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("1-2"), call_string("string.join", {"-", int64_t(1), int64_t(2)}));
|
||||
|
||||
// Lists are flattened, so the output of string.split can be joined again.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a-b-c"), call_string("string.join", {"-", args({"a", "b"}), "c"}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_pad() {
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(" 7"), call_string("string.lpad", {"7", int64_t(3)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("7 "), call_string("string.rpad", {"7", int64_t(3)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("007"), call_string("string.lpad", {"7", int64_t(3), "0"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("700"), call_string("string.rpad", {"7", int64_t(3), "0"}));
|
||||
|
||||
// Text that is already long enough is returned unchanged.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("abcd"), call_string("string.lpad", {"abcd", int64_t(2)}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("abcd"), call_string("string.rpad", {"abcd", int64_t(4)}));
|
||||
|
||||
// Multi-character padding is repeated, and an empty padding is a no-op.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("axyx"), call_string("string.rpad", {"a", int64_t(4), "xy"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a"), call_string("string.rpad", {"a", int64_t(4), ""}));
|
||||
|
||||
// Padding is counted in utf-8 characters, not bytes.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("00日"), call_string("string.lpad", {"日", int64_t(3), "0"}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_strip() {
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a b"), call_string("string.strip", {" a b "}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a"), call_string("string.strip", {"\t\n a \r\n"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a "), call_string("string.lstrip", {" a "}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(" a"), call_string("string.rstrip", {" a "}));
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a"), call_string("string.strip", {"xxaxx", "x"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a"), call_string("string.strip", {"/x/a/x/", "/", "x"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(""), call_string("string.strip", {"aaa", "a"}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string(" a "), call_string("string.strip", {" a ", ""}));
|
||||
|
||||
// The strippable argument is a set of utf-8 characters.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a"), call_string("string.strip", {"日a日", "日"}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_map() {
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("b"), call_string("string.map", {"a", args({"a", "b"})}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("c"), call_string("string.map", {"c", args({"a", "b"})}));
|
||||
|
||||
// Only whole-string matches are replaced.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("ab"), call_string("string.map", {"ab", args({"a", "b"})}));
|
||||
|
||||
// The first matching pair wins.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("y"), call_string("string.map", {"x", args({"a", "b"}), args({"x", "y"}), args({"x", "z"})}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_replace() {
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a+b+c"), call_string("string.replace", {"a-b-c", args({"-", "+"})}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("abc"), call_string("string.replace", {"a-b-c", args({"-", ""})}));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a-b-c"), call_string("string.replace", {"a-b-c", args({"x", "y"})}));
|
||||
|
||||
// Pairs are applied in order, left to right.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("xby"), call_string("string.replace", {"abc", args({"a", "x"}), args({"c", "y"})}));
|
||||
|
||||
// A replacement that contains the replaced text terminates.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("aaaa"), call_string("string.replace", {"aa", args({"a", "aa"})}));
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_invalid_arguments() {
|
||||
CPPUNIT_ASSERT_THROW(rpc::commands.call_command("string.length", torrent::Object()), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(call_string("string.length", {"a", "b"}), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(call_value("string.equals", {"a"}), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(call_value("string.contains", {"a"}), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(call_list("string.split", {"a"}), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(call_string("string.lpad", {"a"}), torrent::input_error);
|
||||
|
||||
// The character count of string.substr cannot be negative.
|
||||
CPPUNIT_ASSERT_THROW(call_string("string.substr", {"abc", int64_t(0), int64_t(-1)}), torrent::input_error);
|
||||
|
||||
// Both string.map and string.replace require {old, new} pairs.
|
||||
CPPUNIT_ASSERT_THROW(call_string("string.map", {"a", "b"}), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(call_string("string.replace", {"a", args({"a", "b", "c"})}), torrent::input_error);
|
||||
CPPUNIT_ASSERT_THROW(call_string("string.replace", {"a", args({"", "b"})}), torrent::input_error);
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandString::test_config_syntax() {
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(3), parse("string.length=abc").as_value());
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), parse("string.contains=retracker.local,retracker").as_value());
|
||||
CPPUNIT_ASSERT_EQUAL(int64_t(1), parse("string.starts_with=udp://tracker.example.com,http://,udp://").as_value());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("cde"), parse("string.substr=abcdef,2,3").as_string());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("padded"), parse("string.strip=\" padded \"").as_string());
|
||||
|
||||
// The {old, new} pairs are written as a block in the configuration file.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a+b+c"), parse("string.replace=a-b-c,{-,+}").as_string());
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("y"), parse("string.map=x,{a,b},{x,y}").as_string());
|
||||
|
||||
// Commands nest, so a split can be joined back together.
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("a-b-c"), parse("string.join=-,(string.split,a.b.c,.)").as_string());
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "test/helpers/test_fixture.h"
|
||||
|
||||
class TestCommandString : public test_fixture {
|
||||
CPPUNIT_TEST_SUITE(TestCommandString);
|
||||
|
||||
CPPUNIT_TEST(test_length);
|
||||
CPPUNIT_TEST(test_equals);
|
||||
CPPUNIT_TEST(test_starts_with);
|
||||
CPPUNIT_TEST(test_ends_with);
|
||||
CPPUNIT_TEST(test_contains);
|
||||
CPPUNIT_TEST(test_substr);
|
||||
CPPUNIT_TEST(test_split);
|
||||
CPPUNIT_TEST(test_join);
|
||||
CPPUNIT_TEST(test_pad);
|
||||
CPPUNIT_TEST(test_strip);
|
||||
CPPUNIT_TEST(test_map);
|
||||
CPPUNIT_TEST(test_replace);
|
||||
CPPUNIT_TEST(test_invalid_arguments);
|
||||
CPPUNIT_TEST(test_config_syntax);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void setUp();
|
||||
void tearDown();
|
||||
|
||||
void test_length();
|
||||
void test_equals();
|
||||
void test_starts_with();
|
||||
void test_ends_with();
|
||||
void test_contains();
|
||||
void test_substr();
|
||||
void test_split();
|
||||
void test_join();
|
||||
void test_pad();
|
||||
void test_strip();
|
||||
void test_map();
|
||||
void test_replace();
|
||||
void test_invalid_arguments();
|
||||
void test_config_syntax();
|
||||
};
|
||||
Reference in New Issue
Block a user