Compare commits

...

21 Commits

Author SHA1 Message Date
rakshasa 184a6cb796 Merge branch 'master' into silasmariusz-untrusted-safe-file-allocate 2026-09-30 10:05:01 +02:00
Silas Mariusz 11885cbb59 Mark the system.file.allocate getter untrusted-safe, because d.open reads it. 2026-09-30 10:04:42 +02:00
noctuum dc916278c2 Do not switch an active download to initial_seed
`confirm_finished` did, and the `input_error` reached `main`.
2026-09-29 10:51:58 +02:00
noctuum 366b936ded Check m_entry against size() in set_entry 2026-09-29 10:18:35 +02:00
noctuum 8bcc4c6e92 Read the unpacked object in the raw string case
A list holding one raw string reached this case and threw on `src`.
2026-09-29 10:00:30 +02:00
noctuum 5421a35349 Remove code that has no remaining user
Three declarations had no definition and the rest had no user left.
2026-09-28 18:40:17 +02:00
noctuum d5ce0984fc Reset the freed pointers in XmlRpc::cleanup
`is_valid()` tests `m_env`, so it stayed true after `cleanup()`.
2026-09-28 18:01:05 +02:00
noctuum a89ce3cd7b Check first against last in parse_object
A command ending in `=` reaches it with an empty range.
2026-09-28 17:34:06 +02:00
noctuum d98a441dc4 Sort the transfer chunks by index
The default comparison ordered the pointers, not the chunk indexes.
2026-09-28 17:11:08 +02:00
noctuum fc6e57c3fa Check pthread_sigmask by its return value
`pthread_sigmask` returns the error number and does not set `errno`.
2026-09-28 16:46:02 +02:00
noctuum 8e23ba8b17 Reject an empty argument list in execute_lua
`lua.execute` with no arguments dereferenced `args.begin()`.
2026-09-28 11:36:53 +02:00
noctuum e656801cfa Limit tab completion to text before the cursor
`substr` takes a count, so the text after the cursor joined the prefix.
2026-09-28 11:00:25 +02:00
noctuum 9f6fd7994e Guard control in the main catch handlers
A start without HOME throws before control is constructed.
2026-09-28 10:46:13 +02:00
noctuum bb77aa644c Stop at max_active in scheduler.simple.removed
The limit was checked once, then every download was resumed.
2026-09-28 10:28:46 +02:00
xirvik 83a5e7687b Sync the session directory after committing the renames
Syncing the files alone does not make the new names survive a crash.
2026-09-25 09:31:08 +02:00
Jari Sundell 910b9b35fc Reorder mutex and atomic variable declarations 2026-09-25 08:52:56 +02:00
xirvik e3051df78c Give SessionManager::m_active its own cache line
Prevents false sharing with the adjacent m_mutex under concurrent access.
2026-09-25 08:52:56 +02:00
xirvik 21287a1355 Make SessionManager::m_active atomic
It is written under m_mutex but read under m_pending_builds_mutex in one place.
2026-09-25 08:52:56 +02:00
xirvik 6471dc181e Guard the download against handlers that erase it mid-close
Track a weak_ptr lifetime handle and make erase ignore re-entrant erase.
2026-09-24 10:22:40 +02:00
xirvik a5c39566c8 Write session files with O_NOFOLLOW and mode 0600
Also make directory_entry::is_file() report the real type so symlinks are skipped.
2026-09-24 09:52:03 +02:00
xirvik 019c16a077 rpc: multicall requires methodName as the struct's first member
Faults clearly instead of the confusing positional parse error it replaces.
2026-09-24 09:20:25 +02:00
35 changed files with 652 additions and 466 deletions
-2
View File
@@ -17,7 +17,6 @@ libsub_root_a_SOURCES = \
core/http_queue.h \
core/manager.cc \
core/manager.h \
core/range_map.h \
core/view.cc \
core/view.h \
core/view_manager.cc \
@@ -171,7 +170,6 @@ libsub_root_a_SOURCES = \
utils/functional.h \
utils/gzip.cc \
utils/gzip.h \
utils/list_focus.h \
utils/lockfile.cc \
utils/lockfile.h \
utils/waitpid_queue.cc \
-48
View File
@@ -77,54 +77,6 @@ group_insert(const torrent::Object::list_type& args) {
return name;
}
static const int file_print_use_space = 0x1;
static const int file_print_delim_space = 0x2;
void
file_print_list(torrent::Object::list_const_iterator first, torrent::Object::list_const_iterator last, FILE* output, int flags) {
while (first != last) {
switch (first->type()) {
case torrent::Object::TYPE_STRING:
fprintf(output, (const char*)" %s" + !(flags & file_print_use_space), first->as_string().c_str());
break;
case torrent::Object::TYPE_VALUE:
fprintf(output, (const char*)" %" PRIi64 + !(flags & file_print_use_space), first->as_value());
break;
case torrent::Object::TYPE_LIST:
file_print_list(first->as_list().begin(), first->as_list().end(), output, 0);
break;
case torrent::Object::TYPE_NONE:
break;
default:
throw torrent::input_error("Invalid type.");
}
flags |= (flags & file_print_delim_space) >> 1;
first++;
}
}
torrent::Object
cmd_file_append(const torrent::Object::list_type& args) {
if (args.empty())
throw torrent::input_error("Invalid number of arguments.");
FILE* output = fopen(args.front().as_string().c_str(), "a");
if (output == nullptr)
throw torrent::input_error("Could not append to file '" + args.front().as_string() + "': " + std::strerror(errno));
try {
file_print_list(++args.begin(), args.end(), output, file_print_delim_space);
fprintf(output, "\n");
} catch (...) {
fclose(output);
throw;
}
fclose(output);
return torrent::Object();
}
void
initialize_command_local() {
core::DownloadList* dList = control->core()->download_list();
+5 -1
View File
@@ -28,9 +28,10 @@ cmd_scheduler_simple_removed(core::Download* download) {
control->core()->download_list()->pause(download);
core::View* viewActive = *control->view_manager()->find("active");
unsigned int numActive = viewActive->size_visible();
int64_t maxActive = rpc::call_command("scheduler.max_active", torrent::Object()).as_value();
if ((int64_t)viewActive->size_visible() >= maxActive)
if ((int64_t)numActive >= maxActive)
return torrent::Object();
// The 'started' view contains all the views we may choose amongst.
@@ -41,6 +42,9 @@ cmd_scheduler_simple_removed(core::Download* download) {
continue;
control->core()->download_list()->resume(*itr);
if ((int64_t)++numActive >= maxActive)
break;
}
return torrent::Object();
+1
View File
@@ -167,6 +167,7 @@ initialize_command_system() {
rpc::rpc.mark_safe("system.time");
rpc::rpc.mark_safe("system.time_seconds");
rpc::rpc.mark_safe("system.time_usec");
rpc::rpc.mark_safe("system.file.allocate");
rpc::rpc.mark_safe("system.file.max_size");
rpc::rpc.mark_safe("system.file.split_size");
rpc::rpc.mark_safe("system.file.split_suffix");
+10
View File
@@ -51,6 +51,14 @@ public:
bool is_hash_checking() const { return m_download.is_hash_checking(); }
bool is_hash_failed() const { return m_hashFailed; }
// Expires once the download is erased, even if other owners keep the
// object alive. Take it before triggering events that may erase.
std::weak_ptr<void> lifetime() const { return m_lifetime; }
void release_lifetime() { m_lifetime.reset(); }
bool is_erasing() const { return m_erasing; }
void set_erasing() { m_erasing = true; }
void set_hash_failed(bool v) { m_hashFailed = v; }
download_type* download() { return &m_download; }
@@ -103,6 +111,8 @@ private:
// Store the FileList instance so we can use slots etc on it.
download_type m_download;
bool m_hashFailed{};
bool m_erasing{};
std::shared_ptr<void> m_lifetime{std::make_shared<char>()};
std::string m_message;
uint32_t m_resumeFlags{default_resume_flags};
unsigned int m_group{};
+60 -4
View File
@@ -194,6 +194,12 @@ DownloadList::erase(iterator itr) {
if (itr == end())
throw torrent::internal_error("DownloadList::erase(...) could not find download.");
// An event handler below may erase the same download again.
if ((*itr)->is_erasing())
return std::next(itr);
(*itr)->set_erasing();
lt_log_print_info(torrent::LOG_TORRENT_INFO, (*itr)->info(), "download_list", "Erasing download.");
// Makes sure close doesn't restart hashing of this download.
@@ -207,6 +213,7 @@ DownloadList::erase(iterator itr) {
for (auto v : *control->view_manager())
v->erase(itr->get());
(*itr)->release_lifetime();
torrent::download_remove(*(*itr)->download());
return base_type::erase(itr);
@@ -275,6 +282,7 @@ void
DownloadList::close_directly(Download* download) {
lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Closing download directly.");
auto lifetime = download->lifetime();
bool was_active = download->download()->info()->is_active();
bool was_open = download->download()->info()->is_open();
@@ -283,11 +291,19 @@ DownloadList::close_directly(Download* download) {
if (was_active) {
DL_TRIGGER_EVENT(download, "event.download.paused");
if (lifetime.expired())
return;
update_paused_state(download);
}
if (was_open) {
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
if (lifetime.expired())
return;
DL_TRIGGER_EVENT(download, "event.download.closed");
}
}
@@ -330,8 +346,13 @@ DownloadList::close_throw(Download* download) {
// When pause gets called it will clear the initial hash check state
// and set hash failed. This should ensure hashing doesn't restart
// until resume gets called.
auto lifetime = download->lifetime();
pause(download);
if (lifetime.expired())
return;
// Check for is_open after pause due to hashing.
if (!download->is_open())
return;
@@ -351,6 +372,10 @@ DownloadList::close_throw(Download* download) {
throw torrent::internal_error("DownloadList::close_throw(...) called but we're going into a hashing loop.");
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
if (lifetime.expired())
return;
DL_TRIGGER_EVENT(download, "event.download.closed");
}
@@ -457,6 +482,8 @@ DownloadList::pause(Download* download, int flags) {
lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Pausing download: flags:%0x.", flags);
auto lifetime = download->lifetime();
try {
download->set_resume_flags(Download::default_resume_flags);
@@ -470,6 +497,9 @@ DownloadList::pause(Download* download, int flags) {
rpc::call_command_set_value("d.hashing.set", Download::variable_hashing_stopped, rpc::make_target(download));
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
if (lifetime.expired())
return;
}
if (!download->download()->info()->is_active())
@@ -483,6 +513,9 @@ DownloadList::pause(Download* download, int flags) {
// view.
DL_TRIGGER_EVENT(download, "event.download.paused");
if (lifetime.expired())
return;
update_paused_state(download);
// Save the state after all the slots, etc have been called so we
@@ -563,9 +596,15 @@ DownloadList::hash_done(Download* download) {
rpc::call_command("d.complete.set", (int64_t)download->is_done(), rpc::make_target(download));
torrent::resume_save_progress(*download->download(), download->download()->bencode()->get_key("libtorrent_resume"));
if (rpc::call_command_value("d.state", rpc::make_target(download)) == 1)
if (rpc::call_command_value("d.state", rpc::make_target(download)) == 1) {
auto lifetime = download->lifetime();
resume(download, download->resume_flags());
if (lifetime.expired())
return;
}
break;
case Download::variable_hashing_last:
@@ -602,11 +641,24 @@ DownloadList::hash_queue(Download* download, int type) {
// HACK
if (download->is_open()) {
auto lifetime = download->lifetime();
pause(download, torrent::Download::stop_skip_tracker);
if (lifetime.expired())
return;
download->download()->close();
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
if (lifetime.expired())
return;
DL_TRIGGER_EVENT(download, "event.download.closed");
if (lifetime.expired())
return;
}
torrent::resume_clear_progress(*download->download(), download->download()->bencode()->get_key("libtorrent_resume"));
@@ -657,6 +709,10 @@ DownloadList::confirm_finished(Download* download) {
if (choke_up.is_string_empty()) choke_up = rpc::call_command("protocol.choke_heuristics.up.seed", torrent::Object(), rpc::make_target(download));
if (choke_down.is_string_empty()) choke_down = rpc::call_command("protocol.choke_heuristics.down.seed", torrent::Object(), rpc::make_target(download));
// libtorrent refuses initial seeding on an active download.
if (download->is_active() && conn_current.as_string() == "initial_seed")
conn_current = "seed";
rpc::call_command("d.connection_current.set", conn_current, rpc::make_target(download));
rpc::call_command("d.up.choke_heuristics.set", choke_up, rpc::make_target(download));
rpc::call_command("d.down.choke_heuristics.set", choke_down, rpc::make_target(download));
@@ -682,12 +738,12 @@ DownloadList::confirm_finished(Download* download) {
// up/downloaded baseline.
download->download()->send_completed();
// Save the hash in case the finished event erases it.
torrent::HashString infohash = download->info()->hash();
// The finished event may erase the download.
auto lifetime = download->lifetime();
DL_TRIGGER_EVENT(download, "event.download.finished");
if (find(infohash) == end())
if (lifetime.expired())
return;
// if (download->resume_flags() != Download::default_resume_flags)
-1
View File
@@ -8,7 +8,6 @@
#include <torrent/object.h>
#include "download_list.h"
#include "range_map.h"
namespace torrent {
class Bencode;
-199
View File
@@ -1,199 +0,0 @@
// rTorrent - BitTorrent client
// Copyright (C) 2005-2008, Jari Sundell
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations
// including the two.
//
// You must obey the GNU General Public License in all respects for
// all of the code used other than OpenSSL. If you modify file(s)
// with this exception, you may extend this exception to your version
// of the file(s), but you are not obligated to do so. If you do not
// wish to do so, delete this exception statement from your version.
// If you delete this exception statement from all source files in the
// program, then also delete it here.
//
// Contact: Jari Sundell <sundell.software@gmail.com>
#ifndef RTORRENT_CORE_RANGE_MAP_H
#define RTORRENT_CORE_RANGE_MAP_H
#include <map>
#include <stdexcept>
namespace core {
// Associate values with a range of keys, and retrieve for any key in the range.
// The template arguments have the same semantics as std::map.
// Exception: if set_merge is used, the value type must have a defined operator ==.
template<typename Key, typename T, typename Compare = std::less<Key>,
typename Alloc = std::allocator<std::pair<const Key, T> > >
class RangeMap : private std::map<Key, std::pair<Key, T>, Compare,
typename std::allocator_traits<Alloc>::template rebind_alloc<std::pair<const Key, std::pair<Key, T>>>> {
typedef std::map<Key, std::pair<Key, T>, Compare,
typename std::allocator_traits<Alloc>::template rebind_alloc<std::pair<const Key, std::pair<Key, T>>>> base_type;
//std::allocator_traits<Alloc>::template rebind_alloc<std::pair<const Key, std::pair<Key, T>>>
public:
RangeMap() = default;
RangeMap(const Compare& c) : base_type(c) {}
typedef typename base_type::iterator iterator;
typedef typename base_type::reverse_iterator reverse_iterator;
typedef typename base_type::const_iterator const_iterator;
typedef typename base_type::const_reverse_iterator const_reverse_iterator;
// using typename base_type::const_iterator;
// using typename base_type::const_reverse_iterator;
using base_type::clear;
using base_type::swap;
using base_type::size;
using base_type::empty;
using base_type::begin;
using base_type::end;
using base_type::rbegin;
using base_type::rend;
using base_type::key_comp;
using base_type::value_comp;
// Store a value for the range [begin, end). Returns iterator for the range.
const_iterator set_range(const Key& begin, const Key& end, const T& value);
// Same, but merge adjacent ranges having the same value. Returns iterator for the merged range.
const_iterator set_merge(Key begin, const Key& end, const T& value);
// Find range containing the given key, or end().
const_iterator find(const Key& key) const;
// Retrieve value for key in a range, throw std::out_of_range if range does not exist.
const T& get(const Key& key) const;
// Retrieve value for key in a range, return def if range does not exist.
T get(const Key& key, T def) const;
private:
iterator crop_overlap(const Key& begin, const Key& end);
};
// Semantics of an entry:
// .first End of range (exclusive), map key.
// .second.first Beginning of range.
// .second.second Value.
template<typename Key, typename T, typename C, typename A>
inline typename RangeMap<Key,T,C,A>::iterator
RangeMap<Key,T,C,A>::crop_overlap(const Key& _begin, const Key& _end) {
typename RangeMap::iterator itr = base_type::upper_bound(_begin);
while (itr != end() && key_comp()(itr->second.first, _end)) {
// There's a subrange before the new begin: need new entry (new range end means new key).
if (key_comp()(itr->second.first, _begin))
base_type::insert(itr, typename RangeMap::value_type(_begin, itr->second));
// Old end is within our range: erase entry.
if (!key_comp()(_end, itr->first)) {
base_type::erase(itr++);
// Otherwise simply set the new begin of the old range.
} else {
itr->second.first = _end;
++itr;
}
}
return itr;
}
template<typename Key, typename T, typename C, typename A>
inline typename RangeMap<Key,T,C,A>::const_iterator
RangeMap<Key,T,C,A>::set_merge(Key _begin, const Key& _end, const T& value) {
if (!key_comp()(_begin, _end))
return end();
// Crop overlapping ranges and return iterator to first range after the one we're inserting.
typename RangeMap::iterator itr = crop_overlap(_begin, _end);
// Check if range before new one is adjacent and has same value: if so erase it and use its beginning.
if (itr != begin()) {
typename RangeMap::iterator prev = itr;
if (!key_comp()((--prev)->first, _begin) && prev->second.second == value) {
_begin = prev->second.first;
base_type::erase(prev);
}
}
// Range after new one is adjacent and has same value: set new beginning.
if (itr != end() && !key_comp()(_end, itr->second.first) && itr->second.second == value) {
itr->second.first = _begin;
return itr;
}
// Otherwise, this range isn't mergeable, make new entry.
return base_type::insert(itr, typename RangeMap::value_type(_end, typename RangeMap::mapped_type(_begin, value)));
}
template<typename Key, typename T, typename C, typename A>
inline typename RangeMap<Key,T,C,A>::const_iterator
RangeMap<Key,T,C,A>::set_range(const Key& _begin, const Key& _end, const T& value) {
if (!key_comp()(_begin, _end))
return end();
return base_type::insert(crop_overlap(_begin, _end), typename RangeMap::value_type(_end, typename RangeMap::mapped_type(_begin, value)));
}
template<typename Key, typename T, typename C, typename A>
inline typename RangeMap<Key,T,C,A>::const_iterator
RangeMap<Key,T,C,A>::find(const Key& key) const {
typename RangeMap::const_iterator itr = base_type::upper_bound(key);
if (itr != end() && key_comp()(key, itr->second.first))
itr = end();
return itr;
}
template<typename Key, typename T, typename C, typename A>
inline const T&
RangeMap<Key,T,C,A>::get(const Key& key) const {
typename RangeMap::const_iterator itr = find(key);
if (itr == end())
throw std::out_of_range("RangeMap::get");
return itr->second.second;
}
template<typename Key, typename T, typename C, typename A>
inline T
RangeMap<Key,T,C,A>::get(const Key& key, T def) const {
typename RangeMap::const_iterator itr = find(key);
return (itr == end() ? def : itr->second.second);
}
}
#endif
-6
View File
@@ -16,15 +16,12 @@ namespace utils {
namespace torrent {
class ClientInfo;
class Entry;
}
class Control;
namespace display {
char* print_string(char* first, char* last, char* str);
char* print_hhmmss(char* first, char* last, time_t t);
char* print_hhmmss_local(char* first, char* last, time_t t);
char* print_ddhhmm(char* first, char* last, time_t t);
@@ -42,9 +39,6 @@ char* print_download_percentage_done(char* first, char* last, core::Downlo
char* print_client_version(char* first, char* last, const torrent::ClientInfo& clientInfo);
char* print_entry_tags(char* first, char* last);
char* print_entry_file(char* first, char* last, const torrent::Entry& entry);
char* print_status_throttle_limit(char* first, char* last, bool up, const std::vector<std::string>& throttle_names);
char* print_status_throttle_rate(char* first, char* last, bool up, const std::vector<std::string>& throttle_names, const double& global_rate);
+2 -1
View File
@@ -57,7 +57,8 @@ WindowDownloadChunksSeen::redraw() {
const torrent::TransferList* transfers = m_download->download()->transfer_list();
std::vector<torrent::BlockList*> transferChunks(transfers->begin(), transfers->end());
std::sort(transferChunks.begin(), transferChunks.end());
std::sort(transferChunks.begin(), transferChunks.end(),
[](const auto& left, const auto& right) { return left->index() < right->index(); });
std::vector<torrent::BlockList*>::const_iterator itrTransfer = transferChunks.begin();
-2
View File
@@ -69,8 +69,6 @@ public:
bool pressed(int key);
void ignore(int key) { (*this)[key] = slot_void(); }
private:
bool m_enabled{true};
};
+1 -1
View File
@@ -100,7 +100,7 @@ PathInput::receive_do_complete() {
entry.s_name += '/';
}
range_type r = find_incomplete(dir, str().substr(dirEnd, get_pos()));
range_type r = find_incomplete(dir, str().substr(dirEnd, get_pos() - dirEnd));
if (r.first == r.second)
return; // Show some nice colors here.
+4 -2
View File
@@ -492,7 +492,8 @@ main(int argc, char** argv) {
control->cleanup();
} catch (torrent::internal_error& e) {
control->cleanup_exception();
if (control != nullptr)
control->cleanup_exception();
std::cout << "rtorrent: caught torrent::internal_error: "
<< e.what() << std::endl
@@ -505,7 +506,8 @@ main(int argc, char** argv) {
return -1;
} catch (std::exception& e) {
control->cleanup_exception();
if (control != nullptr)
control->cleanup_exception();
std::cout << "rtorrent: caught" << typeid(e).name() << " : " << e.what() << std::endl;
+4
View File
@@ -394,6 +394,10 @@ execute_lua(LuaEngine* engine, rpc::target_type target_type, torrent::Object con
switch (raw_args.type()) {
case torrent::Object::TYPE_LIST: {
const torrent::Object::list_type& args = raw_args.as_list();
if (args.empty())
throw torrent::input_error("Too few arguments.");
if (flags & LuaEngine::flag_string) {
check_lua_status(l_state, luaL_loadstring(l_state, args.begin()->as_string().c_str()));
} else {
+3 -3
View File
@@ -161,7 +161,7 @@ parse_object(const char* first, const char* last, torrent::Object* dest, bool (*
if (++depth >= max_parse_depth)
throw torrent::input_error("Max parse depth reached.");
if (*first == '{') {
if (first != last && *first == '{') {
*dest = torrent::Object::create_list();
first = parse_list(first + 1, last, dest, &parse_is_delim_block, depth);
first = parse_skip_wspace(first, last);
@@ -171,7 +171,7 @@ parse_object(const char* first, const char* last, torrent::Object* dest, bool (*
return ++first;
} else if (*first == '(') {
} else if (first != last && *first == '(') {
int32_t parentheses = 1;
while (first + 1 != last && *(first + 1) == '(') {
@@ -393,7 +393,7 @@ convert_to_value_nothrow(const torrent::Object& src, int64_t* value, int base, i
== unpacked.as_string().c_str() + unpacked.as_string().size();
case torrent::Object::TYPE_RAW_STRING: {
const torrent::raw_string& str = src.as_raw_string();
const torrent::raw_string& str = unpacked.as_raw_string();
auto buffer = std::make_unique<char[]>(str.size() + 1);
std::memcpy(buffer.get(), str.data(), str.size());
+2
View File
@@ -440,6 +440,8 @@ XmlRpc::cleanup() {
xmlrpc_registry_free((xmlrpc_registry*)m_registry);
xmlrpc_env_clean((xmlrpc_env*)m_env);
delete (xmlrpc_env*)m_env;
m_env = nullptr;
m_registry = nullptr;
}
bool
+9 -4
View File
@@ -326,15 +326,20 @@ process_document(const tinyxml2::XMLDocument* doc, tinyxml2::XMLPrinter* printer
auto& result_list = result.as_list();
auto parent_elements = element_access(doc->RootElement(), {"params", "param", "value", "array", "data"});
for (auto child = parent_elements->FirstChildElement("value"); child; child = child->NextSiblingElement("value")) {
auto sub_method_name = element_access(child, {"struct", "member", "value", "string"})->GetText();
auto method_name_member = element_access(child, {"struct", "member"});
auto member_name = method_name_member->FirstChildElement("name");
if (member_name == nullptr || member_name->GetText() == nullptr ||
std::strncmp(member_name->GetText(), "methodName", sizeof("methodName")) != 0)
throw rpc_error(XMLRPC_PARSE_ERROR, "multicall struct's first member must be methodName");
auto sub_method_name = element_access(method_name_member, {"value", "string"})->GetText();
if (sub_method_name == nullptr)
throw rpc_error(XMLRPC_PARSE_ERROR, "multicall methodName element is empty");
// If sub_params ends up a nullptr at the end of this if-chian,
// execute_command will turn it into an empty list
auto sub_params = element_access(child, {"struct", "member"});
if (sub_params != nullptr)
sub_params = sub_params->NextSiblingElement("member");
auto sub_params = method_name_member->NextSiblingElement("member");
if (sub_params != nullptr)
sub_params = sub_params->FirstChildElement("value");
if (sub_params != nullptr)
+60 -18
View File
@@ -2,6 +2,7 @@
#include "download_storer.h"
#include <cerrno>
#include <fcntl.h>
#include <fstream>
#include <unistd.h>
@@ -16,6 +17,13 @@
#include "core/download.h"
#include "utils/directory.h"
// O_DIRECTORY makes the open refuse anything that is not a directory. A
// platform that does not define it still gets the read-only directory
// handle fsync needs, so fall back to no extra flag.
#ifndef O_DIRECTORY
#define O_DIRECTORY 0
#endif
namespace session {
DownloadStorer::DownloadStorer(core::Download* download)
@@ -116,28 +124,36 @@ is_correct_format(const std::string& f) {
void
save_stream(const std::string& path, bool use_fsyncdisk, const std::stringstream& stream) {
std::fstream output(path.c_str(), std::ios::out | std::ios::trunc);
// Remove any leftover temporary file first so that O_EXCL only ever fails on
// an entry that appeared after the unlink, and O_NOFOLLOW keeps a symlink
// planted in the session directory from redirecting the write.
if (::unlink(path.c_str()) == -1 && errno != ENOENT)
throw torrent::storage_error("failed to remove stale file : " + path);
// TODO: If we cannot open more files, wait for some to finish and try again.
if (!output.is_open())
throw torrent::storage_error("failed to open file for writing : " + path);
output << stream.rdbuf();
if (!output.good())
throw torrent::storage_error("failed to write stream to file : " + path);
// The data only reaches the kernel here, so this is where a full disk is seen.
output.close();
if (!output.good())
throw torrent::storage_error("failed to flush stream to file : " + path);
// Ensure that the new file is actually written to the disk
int fd = ::open(path.c_str(), O_WRONLY);
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
if (fd < 0)
throw torrent::storage_error("failed to open file descriptor for fsync : " + path);
throw torrent::storage_error("failed to open file for writing : " + path);
const auto data = stream.view();
std::size_t remaining = data.size();
const char* cursor = data.data();
while (remaining != 0) {
ssize_t result = ::write(fd, cursor, remaining);
if (result == -1) {
if (errno == EINTR)
continue;
::close(fd);
throw torrent::storage_error("failed to write stream to file : " + path);
}
cursor += result;
remaining -= result;
}
if (use_fsyncdisk) {
#ifdef __APPLE__
@@ -152,10 +168,31 @@ save_stream(const std::string& path, bool use_fsyncdisk, const std::stringstream
}
}
// A full disk may only be seen when the descriptor is closed.
if (::close(fd) == -1)
throw torrent::storage_error("failed to close file descriptor : " + path);
}
void
sync_directory_of(const std::string& path) {
auto separator = path.rfind('/');
auto directory = separator == std::string::npos ? std::string(".") :
separator == 0 ? std::string("/") : path.substr(0, separator);
int fd = ::open(directory.c_str(), O_RDONLY | O_DIRECTORY);
if (fd < 0)
throw torrent::storage_error("failed to open session directory for sync : " + directory);
if (::fsync(fd) == -1) {
::close(fd);
throw torrent::storage_error("failed to sync session directory : " + directory);
}
if (::close(fd) == -1)
throw torrent::storage_error("failed to close session directory : " + directory);
}
} // namespace anonymous
void
@@ -183,6 +220,11 @@ DownloadStorer::save_and_move_streams(const std::string& path, bool use_fsyncdis
if (::rename((rtorrent_path + ".new").c_str(), rtorrent_path.c_str()) == -1)
throw torrent::storage_error("failed to rename rtorrent resume file : " + rtorrent_path);
// Syncing the files themselves does not persist the renames; the directory
// holding them has to be synced for the new names to survive a crash.
if (use_fsyncdisk)
sync_directory_of(path);
}
utils::Directory
-1
View File
@@ -75,7 +75,6 @@ SessionManager::save_resume_download(core::Download* download) {
{
std::unique_lock<std::mutex> lock(m_pending_builds_mutex);
// TODO: This is under the wrong lock.
if (!m_active)
throw torrent::internal_error("SessionManager::save_resume_download() called while not active.");
+7 -2
View File
@@ -1,6 +1,7 @@
#ifndef RTORRENT_SESSION_SESSION_MANAGER_H
#define RTORRENT_SESSION_SESSION_MANAGER_H
#include <atomic>
#include <condition_variable>
#include <deque>
#include <future>
@@ -99,9 +100,13 @@ private:
bool m_use_fsyncdisk{true};
bool m_use_lock{true};
align_cacheline std::mutex m_mutex;
align_cacheline
bool m_active{};
std::atomic<bool> m_active{};
align_cacheline
std::mutex m_mutex;
std::deque<SaveRequest> m_save_requests;
std::atomic<size_t> m_save_request_counter{};
+8 -4
View File
@@ -63,8 +63,10 @@ SignalHandler::set_block(unsigned int signum) {
sigemptyset(&mask);
sigaddset(&mask, signum);
if (pthread_sigmask(SIG_BLOCK, &mask, NULL) == -1)
throw std::logic_error("Could not block signal: " + std::string(std::strerror(errno)));
int result = pthread_sigmask(SIG_BLOCK, &mask, NULL);
if (result != 0)
throw std::logic_error("Could not block signal: " + std::string(std::strerror(result)));
}
void
@@ -77,8 +79,10 @@ SignalHandler::set_unblock(unsigned int signum) {
sigemptyset(&mask);
sigaddset(&mask, signum);
if (pthread_sigmask(SIG_UNBLOCK, &mask, NULL) == -1)
throw std::logic_error("Could not unblock signal: " + std::string(std::strerror(errno)));
int result = pthread_sigmask(SIG_UNBLOCK, &mask, NULL);
if (result != 0)
throw std::logic_error("Could not unblock signal: " + std::string(std::strerror(result)));
}
void
-1
View File
@@ -6,7 +6,6 @@
#include <torrent/peer/peer.h>
#include "display/manager.h"
#include "utils/list_focus.h"
#include "element_base.h"
+1 -1
View File
@@ -179,7 +179,7 @@ ElementMenu::set_entry(size_type idx, bool triggerSlot) {
m_entry = idx;
focus_entry(m_entry);
if (triggerSlot)
if (triggerSlot && m_entry < size())
base_type::operator[](m_entry).m_slotFocus();
m_window->mark_dirty();
+27 -5
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <cstdlib>
#include <dirent.h>
#include <fcntl.h>
#include <functional>
#include <sys/stat.h>
#include <torrent/exceptions.h>
@@ -13,6 +14,24 @@
namespace utils {
namespace {
uint8_t
entry_type_from_mode(mode_t mode) {
if (S_ISREG(mode))
return DT_REG;
if (S_ISDIR(mode))
return DT_DIR;
if (S_ISLNK(mode))
return DT_LNK;
return DT_UNKNOWN;
}
} // namespace
// Keep this?
bool
Directory::is_valid() const {
@@ -38,9 +57,6 @@ Directory::update(int flags) {
return false;
struct dirent* entry;
#ifdef __sun__
struct stat s;
#endif
while ((entry = readdir(d)) != NULL) {
if ((flags & update_hide_dot) && entry->d_name[0] == '.')
@@ -49,16 +65,22 @@ Directory::update(int flags) {
iterator itr = base_type::insert(end(), value_type());
#ifdef __sun__
stat(entry->d_name, &s);
itr->s_fileno = entry->d_ino;
itr->s_reclen = 0;
itr->s_type = s.st_mode;
itr->s_type = DT_UNKNOWN;
#else
itr->s_fileno = entry->d_fileno;
itr->s_reclen = entry->d_reclen;
itr->s_type = entry->d_type;
#endif
if (itr->s_type == DT_UNKNOWN) {
struct stat st;
if (fstatat(dirfd(d), entry->d_name, &st, AT_SYMLINK_NOFOLLOW) == 0)
itr->s_type = entry_type_from_mode(st.st_mode);
}
#ifdef DIRENT_NAMLEN_EXISTS_FOOBAR
itr->s_name = std::string(entry->d_name, entry->d_name + entry->d_namlen);
#else
+2 -2
View File
@@ -2,14 +2,14 @@
#define RTORRENT_UTILS_DIRECTORY_H
#include <cstdint>
#include <dirent.h>
#include <string>
#include <vector>
namespace utils {
struct directory_entry {
// Fix.
bool is_file() const { return true; }
bool is_file() const { return s_type == DT_REG; }
// The name and types should match POSIX.
uint32_t s_fileno;
-158
View File
@@ -1,158 +0,0 @@
// rTorrent - BitTorrent client
// Copyright (C) 2005-2011, Jari Sundell
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations
// including the two.
//
// You must obey the GNU General Public License in all respects for
// all of the code used other than OpenSSL. If you modify file(s)
// with this exception, you may extend this exception to your version
// of the file(s), but you are not obligated to do so. If you do not
// wish to do so, delete this exception statement from your version.
// If you delete this exception statement from all source files in the
// program, then also delete it here.
//
// Contact: Jari Sundell <sundell.software@gmail.com>
#ifndef RTORRENT_UTILS_LIST_FOCUS_H
#define RTORRENT_UTILS_LIST_FOCUS_H
#include <functional>
#include <list>
namespace utils {
// Can't make this class inherit privately due to gcc PR 14258.
template <typename Base>
class ListFocus {
public:
typedef Base base_type;
typedef std::function<void ()> slot_void;
typedef std::list<slot_void> signal_void;
typedef typename base_type::iterator iterator;
typedef typename base_type::const_iterator const_iterator;
typedef typename base_type::reverse_iterator reverse_iterator;
typedef typename base_type::const_reverse_iterator const_reverse_iterator;
typedef typename base_type::value_type value_type;
ListFocus(base_type* b = nullptr) : m_base(b) { if (b) m_focus = b->end(); }
// Convinience functions, would have added more through using, but
// can't.
iterator begin() { return m_base->begin(); }
iterator end() { return m_base->end(); }
reverse_iterator rbegin() { return m_base->rbegin(); }
reverse_iterator rend() { return m_base->rend(); }
// Don't do erase on this object without making sure focus is right.
base_type& base() { return *m_base; }
iterator get_focus() { return m_focus; }
void set_focus(iterator itr);
// These are looping increment/decrements.
iterator inc_focus();
iterator dec_focus();
iterator erase(iterator itr);
void remove(const value_type& v);
// Be careful with copying signals.
signal_void& signal_changed() { return m_signal_changed; }
private:
void emit_changed();
base_type* m_base;
iterator m_focus;
signal_void m_signal_changed;
};
template <typename Base>
void
ListFocus<Base>::set_focus(iterator itr) {
m_focus = itr;
emit_changed();
}
template <typename Base>
typename ListFocus<Base>::iterator
ListFocus<Base>::inc_focus() {
if (m_focus != end())
++m_focus;
else
m_focus = begin();
emit_changed();
return m_focus;
}
template <typename Base>
typename ListFocus<Base>::iterator
ListFocus<Base>::dec_focus() {
if (m_focus != begin())
--m_focus;
else
m_focus = end();
emit_changed();
return m_focus;
}
template <typename Base>
typename ListFocus<Base>::iterator
ListFocus<Base>::erase(iterator itr) {
if (itr == m_focus) {
m_focus = m_base->erase(itr);
emit_changed();
return m_focus;
}
return m_base->erase(itr);
}
template <typename Base>
void
ListFocus<Base>::remove(const value_type& v) {
iterator first = begin();
iterator last = end();
while (first != last)
if (*first == v)
first = erase(first);
else
++first;
}
template <typename Base>
void
ListFocus<Base>::emit_changed() {
for (const auto& itr : m_signal_changed)
itr();
}
}
#endif
+6
View File
@@ -69,6 +69,12 @@ rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \
src/test_command_string.h \
src/test_command_throttle.cc \
src/test_command_throttle.h \
src/test_input_path_input.cc \
src/test_input_path_input.h \
src/test_session_commit.cc \
src/test_session_commit.h \
src/test_session_storer.cc \
src/test_session_storer.h \
src/test_setup.cc \
src/test_setup.h \
src/test_ui_download_list.cc \
+42
View File
@@ -192,12 +192,54 @@ TestXmlrpc::test_response_size_limit() {
CPPUNIT_ASSERT_EQUAL(expected, output);
}
namespace {
const std::string multicall_method_name =
"<member><name>methodName</name><value><string>xmlrpc_reflect</string></value></member>";
const std::string multicall_params =
"<member><name>params</name><value><array><data>"
"<value><string></string></value><value><string>a</string></value>"
"</data></array></value></member>";
std::string
multicall_request(const std::string& members) {
return "<?xml version=\"1.0\"?><methodCall><methodName>system.multicall</methodName>"
"<params><param><value><array><data><value><struct>" + members +
"</struct></value></data></array></value></param></params></methodCall>";
}
}
void
TestXmlrpc::test_multicall_member_order() {
auto call = [this](const std::string& input) {
std::string output;
m_xmlrpc.process(input.c_str(), input.size(), [&output](const char* c, uint32_t l){ output.append(c, l); return true;});
return output;
};
// methodName first is the only order accepted; the positive control proves
// the harness drives the real code path rather than a stub.
std::string ordered = call(multicall_request(multicall_method_name + multicall_params));
CPPUNIT_ASSERT(ordered.find("faultCode") == std::string::npos);
// params before methodName is rejected with a clear top-level fault,
// instead of the "could not find expected element string" of a positional read.
std::string expected_fault =
"<?xml version=\"1.0\"?><methodResponse><fault><value><struct>"
"<member><name>faultCode</name><value><i8>-503</i8></value></member>"
"<member><name>faultString</name><value><string>multicall struct's first member must be methodName</string></value></member>"
"</struct></value></fault></methodResponse>";
CPPUNIT_ASSERT_EQUAL(expected_fault, call(multicall_request(multicall_params + multicall_method_name)));
}
#else
void TestXmlrpc::test_invalid_utf8() {}
void TestXmlrpc::test_basics() {}
void TestXmlrpc::test_size_limit() {}
void TestXmlrpc::test_response_size_limit() {}
void TestXmlrpc::test_multicall_member_order() {}
void TestXmlrpc::setUp() {}
void TestXmlrpc::tearDown() {}
+2
View File
@@ -11,6 +11,7 @@ class TestXmlrpc : public test_fixture {
CPPUNIT_TEST(test_invalid_utf8);
CPPUNIT_TEST(test_size_limit);
CPPUNIT_TEST(test_response_size_limit);
CPPUNIT_TEST(test_multicall_member_order);
CPPUNIT_TEST_SUITE_END();
@@ -24,6 +25,7 @@ public:
void test_invalid_utf8();
void test_size_limit();
void test_response_size_limit();
void test_multicall_member_order();
private:
std::unique_ptr<TestMainThread> m_test_main_thread;
+77
View File
@@ -0,0 +1,77 @@
#include "config.h"
#include "test/src/test_input_path_input.h"
#include <cstdlib>
#include <sys/stat.h>
#include <unistd.h>
#include "input/path_input.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestInputPathInput);
void
TestInputPathInput::setUp() {
test_fixture::setUp();
char temp_dir[] = "/tmp/rtorrent_test_path_input_XXXXXX";
CPPUNIT_ASSERT(mkdtemp(temp_dir) != nullptr);
m_temp_dir = temp_dir;
CPPUNIT_ASSERT_EQUAL(0, mkdir((m_temp_dir + "/alpha").c_str(), 0755));
CPPUNIT_ASSERT_EQUAL(0, mkdir((m_temp_dir + "/alpha/beta").c_str(), 0755));
}
void
TestInputPathInput::tearDown() {
rmdir((m_temp_dir + "/alpha/beta").c_str());
rmdir((m_temp_dir + "/alpha").c_str());
rmdir(m_temp_dir.c_str());
test_fixture::tearDown();
}
// Tab at the end of the line completes the trailing component.
void
TestInputPathInput::test_complete_at_end() {
input::PathInput input;
input.str() = m_temp_dir + "/al";
input.set_pos(input.str().size());
CPPUNIT_ASSERT(input.pressed('\t'));
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/alpha/", input.str());
CPPUNIT_ASSERT_EQUAL(input.str().size(), input.get_pos());
}
// The same completion with the cursor left inside the line. Only the text up to
// the cursor selects the entry, and the rest of the line is discarded.
void
TestInputPathInput::test_complete_mid_line() {
input::PathInput input;
input.str() = m_temp_dir + "/alZZZ";
input.set_pos(m_temp_dir.size() + 3);
CPPUNIT_ASSERT(input.pressed('\t'));
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/alpha/", input.str());
CPPUNIT_ASSERT_EQUAL(input.str().size(), input.get_pos());
}
// A cursor resting on a separator keeps completing the component after it.
void
TestInputPathInput::test_cursor_on_separator() {
input::PathInput input;
input.str() = m_temp_dir + "/alpha/be";
input.set_pos(m_temp_dir.size() + 6);
CPPUNIT_ASSERT(input.pressed('\t'));
CPPUNIT_ASSERT_EQUAL(m_temp_dir + "/alpha/beta/", input.str());
CPPUNIT_ASSERT_EQUAL(input.str().size(), input.get_pos());
}
+24
View File
@@ -0,0 +1,24 @@
#include "test/helpers/test_fixture.h"
#include <string>
class TestInputPathInput : public test_fixture {
CPPUNIT_TEST_SUITE(TestInputPathInput);
CPPUNIT_TEST(test_complete_at_end);
CPPUNIT_TEST(test_complete_mid_line);
CPPUNIT_TEST(test_cursor_on_separator);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void test_complete_at_end();
void test_complete_mid_line();
void test_cursor_on_separator();
private:
std::string m_temp_dir;
};
+94
View File
@@ -0,0 +1,94 @@
#include "config.h"
#include "test/src/test_session_commit.h"
#include <cstdlib>
#include <dirent.h>
#include <fstream>
#include <sstream>
#include <sys/stat.h>
#include <unistd.h>
#include "session/download_storer.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestSessionCommit);
namespace {
const char* entry_name = "0123456789ABCDEF0123456789ABCDEF01234567.torrent";
std::string
read_file(const std::string& path) {
std::ifstream file(path.c_str());
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
void
remove_directory(const std::string& path) {
DIR* d = ::opendir(path.c_str());
if (d == NULL)
return;
struct dirent* entry;
while ((entry = ::readdir(d)) != NULL) {
if (entry->d_name[0] == '.' && (entry->d_name[1] == '\0' || (entry->d_name[1] == '.' && entry->d_name[2] == '\0')))
continue;
::unlink((path + "/" + entry->d_name).c_str());
}
::closedir(d);
::rmdir(path.c_str());
}
} // namespace
void
TestSessionCommit::setUp() {
test_fixture::setUp();
char temp_dir[] = "/tmp/rtorrent_test_commit_XXXXXX";
CPPUNIT_ASSERT(mkdtemp(temp_dir) != nullptr);
m_session_dir = temp_dir;
}
void
TestSessionCommit::tearDown() {
remove_directory(m_session_dir);
test_fixture::tearDown();
}
void
TestSessionCommit::commit_and_verify(bool use_fsyncdisk) {
auto path = m_session_dir + "/" + entry_name;
std::stringstream torrent_stream("torrent-data");
std::stringstream rtorrent_stream("rtorrent-data");
std::stringstream libtorrent_stream("libtorrent-data");
session::DownloadStorer::save_and_move_streams(path, use_fsyncdisk, &torrent_stream, &rtorrent_stream, &libtorrent_stream);
CPPUNIT_ASSERT_EQUAL(std::string("torrent-data"), read_file(path));
CPPUNIT_ASSERT_EQUAL(std::string("rtorrent-data"), read_file(path + ".rtorrent"));
CPPUNIT_ASSERT_EQUAL(std::string("libtorrent-data"), read_file(path + ".libtorrent_resume"));
struct stat st;
CPPUNIT_ASSERT(::stat((path + ".new").c_str(), &st) == -1);
}
void
TestSessionCommit::test_commit_publishes_all_three_files() {
commit_and_verify(false);
}
void
TestSessionCommit::test_commit_with_fsync_publishes_all_three_files() {
commit_and_verify(true);
}
+24
View File
@@ -0,0 +1,24 @@
#include "test/helpers/test_fixture.h"
#include <string>
class TestSessionCommit : public test_fixture {
CPPUNIT_TEST_SUITE(TestSessionCommit);
CPPUNIT_TEST(test_commit_publishes_all_three_files);
CPPUNIT_TEST(test_commit_with_fsync_publishes_all_three_files);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void test_commit_publishes_all_three_files();
void test_commit_with_fsync_publishes_all_three_files();
private:
void commit_and_verify(bool use_fsyncdisk);
std::string m_session_dir;
};
+152
View File
@@ -0,0 +1,152 @@
#include "config.h"
#include "test/src/test_session_storer.h"
#include <cstdlib>
#include <dirent.h>
#include <fstream>
#include <sstream>
#include <sys/stat.h>
#include <unistd.h>
#include "session/download_storer.h"
#include "utils/directory.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestSessionStorer);
namespace {
const char* entry_name = "0123456789ABCDEF0123456789ABCDEF01234567.torrent";
const char* link_name = "FEDCBA9876543210FEDCBA9876543210FEDCBA98.torrent";
void
write_file(const std::string& path, const std::string& content) {
std::ofstream file(path.c_str());
file << content;
file.close();
CPPUNIT_ASSERT(file.good());
}
std::string
read_file(const std::string& path) {
std::ifstream file(path.c_str());
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
void
save_session_files(const std::string& path) {
std::stringstream torrent_stream("torrent-data");
std::stringstream rtorrent_stream("rtorrent-data");
std::stringstream libtorrent_stream("libtorrent-data");
session::DownloadStorer::save_and_move_streams(path, false, &torrent_stream, &rtorrent_stream, &libtorrent_stream);
}
unsigned int
permissions_of(const std::string& path) {
struct stat st;
CPPUNIT_ASSERT_EQUAL(0, ::stat(path.c_str(), &st));
return st.st_mode & 07777;
}
void
remove_directory(const std::string& path) {
DIR* d = ::opendir(path.c_str());
if (d == NULL)
return;
struct dirent* entry;
while ((entry = ::readdir(d)) != NULL) {
if (entry->d_name[0] == '.' && (entry->d_name[1] == '\0' || (entry->d_name[1] == '.' && entry->d_name[2] == '\0')))
continue;
::unlink((path + "/" + entry->d_name).c_str());
}
::closedir(d);
::rmdir(path.c_str());
}
} // namespace
void
TestSessionStorer::setUp() {
test_fixture::setUp();
char temp_dir[] = "/tmp/rtorrent_test_session_XXXXXX";
CPPUNIT_ASSERT(mkdtemp(temp_dir) != nullptr);
m_temp_dir = temp_dir;
m_session_dir = m_temp_dir + "/session";
CPPUNIT_ASSERT_EQUAL(0, ::mkdir(m_session_dir.c_str(), 0755));
}
void
TestSessionStorer::tearDown() {
remove_directory(m_session_dir);
remove_directory(m_temp_dir);
test_fixture::tearDown();
}
// A symlink planted where the next temporary session file will be written must
// not redirect the write to the file it points at.
void
TestSessionStorer::test_temp_file_symlink_is_not_followed() {
auto outside = m_temp_dir + "/outside.txt";
auto path = m_session_dir + "/" + entry_name;
write_file(outside, "original");
CPPUNIT_ASSERT_EQUAL(0, ::symlink(outside.c_str(), (path + ".new").c_str()));
save_session_files(path);
CPPUNIT_ASSERT_EQUAL(std::string("original"), read_file(outside));
CPPUNIT_ASSERT_EQUAL(std::string("torrent-data"), read_file(path));
}
// Session files carry tracker announce urls, so they must not be readable by
// other users regardless of the umask rtorrent was started with.
void
TestSessionStorer::test_saved_files_are_owner_only() {
auto path = m_session_dir + "/" + entry_name;
auto prev_umask = ::umask(0);
save_session_files(path);
::umask(prev_umask);
CPPUNIT_ASSERT_EQUAL(0600u, permissions_of(path));
CPPUNIT_ASSERT_EQUAL(0600u, permissions_of(path + ".rtorrent"));
CPPUNIT_ASSERT_EQUAL(0600u, permissions_of(path + ".libtorrent_resume"));
}
// Entries listed for loading must report their real type so that a symlink in
// the session directory is skipped instead of loaded.
void
TestSessionStorer::test_symlinked_entry_is_not_a_file() {
write_file(m_temp_dir + "/outside.txt", "d0:e");
write_file(m_session_dir + "/" + entry_name, "d0:e");
CPPUNIT_ASSERT_EQUAL(0, ::symlink((m_temp_dir + "/outside.txt").c_str(), (m_session_dir + "/" + link_name).c_str()));
auto entries = session::DownloadStorer::get_formated_entries(m_session_dir + "/");
CPPUNIT_ASSERT_EQUAL(size_t{2}, size_t{entries.size()});
for (const auto& entry : entries) {
if (entry.s_name == entry_name)
CPPUNIT_ASSERT(entry.is_file());
else
CPPUNIT_ASSERT(!entry.is_file());
}
}
+25
View File
@@ -0,0 +1,25 @@
#include "test/helpers/test_fixture.h"
#include <string>
class TestSessionStorer : public test_fixture {
CPPUNIT_TEST_SUITE(TestSessionStorer);
CPPUNIT_TEST(test_temp_file_symlink_is_not_followed);
CPPUNIT_TEST(test_saved_files_are_owner_only);
CPPUNIT_TEST(test_symlinked_entry_is_not_a_file);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void test_temp_file_symlink_is_not_followed();
void test_saved_files_are_owner_only();
void test_symlinked_entry_is_not_a_file();
private:
std::string m_temp_dir;
std::string m_session_dir;
};