Add realpath variants of the commands that return paths.

Closes #1670.
This commit is contained in:
kunalkakade
2026-08-07 14:52:12 +05:30
committed by Jari Sundell
parent cfb9e083d2
commit 31196ff5f0
8 changed files with 209 additions and 0 deletions
+17
View File
@@ -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");
+4
View File
@@ -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");
+8
View File
@@ -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");
+30
View File
@@ -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;
}
+2
View File
@@ -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;
+2
View File
@@ -51,6 +51,8 @@ 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 \
+116
View File
@@ -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();
}
+30
View File
@@ -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;
};