refactor: tr_metainfo_builder() (#3565)

This commit is contained in:
Charles Kerr
2022-08-02 14:46:08 -05:00
committed by GitHub
parent 5eb7f75010
commit bf8f72e61f
12 changed files with 994 additions and 1322 deletions

View File

@@ -3,10 +3,11 @@
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <iostream>
#include <list>
#include <chrono>
#include <future>
#include <memory>
#include <string>
#include <string_view>
#include <glibmm.h>
#include <glibmm/i18n.h>
@@ -14,6 +15,8 @@
#include <fmt/core.h>
#include <libtransmission/transmission.h>
#include <libtransmission/error.h>
#include <libtransmission/makemeta.h>
#include <libtransmission/utils.h> /* tr_formatter_mem_B() */
@@ -23,6 +26,8 @@
#include "Session.h"
#include "Utils.h"
using namespace std::literals;
namespace
{
@@ -34,12 +39,18 @@ public:
MakeProgressDialog(
Gtk::Window& parent,
tr_metainfo_builder& builder,
std::string const& target,
std::future<tr_error*> future,
std::string_view target,
Glib::RefPtr<Session> const& core);
~MakeProgressDialog() override;
TR_DISABLE_COPY_MOVE(MakeProgressDialog)
[[nodiscard]] bool success() const
{
return success_;
}
private:
bool onProgressDialogRefresh();
void onProgressDialogResponse(int response);
@@ -48,8 +59,10 @@ private:
private:
tr_metainfo_builder& builder_;
std::future<tr_error*> future_;
std::string const target_;
Glib::RefPtr<Session> const core_;
bool success_ = false;
sigc::connection progress_tag_;
Gtk::Label* progress_label_ = nullptr;
@@ -80,9 +93,9 @@ private:
void updatePiecesLabel();
void setFilename(std::string const& filename);
void setFilename(std::string_view filename);
void makeProgressDialog(std::string const& target);
void makeProgressDialog(std::string_view target, std::future<tr_error*> future);
void configurePieceSizeScale();
void onPieceSizeUpdated();
@@ -104,57 +117,66 @@ private:
Gtk::Entry* source_entry_ = nullptr;
std::unique_ptr<MakeProgressDialog> progress_dialog_;
Glib::RefPtr<Gtk::TextBuffer> announce_text_buffer_;
std::unique_ptr<tr_metainfo_builder, void (*)(tr_metainfo_builder*)> builder_ = { nullptr, nullptr };
std::optional<tr_metainfo_builder> builder_;
};
bool MakeProgressDialog::onProgressDialogRefresh()
{
Glib::ustring str;
double const fraction = builder_.pieceCount != 0 ? (double)builder_.pieceIndex / builder_.pieceCount : 0;
auto const base = Glib::path_get_basename(builder_.top);
auto const is_done = !future_.valid() || future_.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
/* progress label */
if (!builder_.isDone)
if (is_done)
{
progress_tag_.disconnect();
}
// progress value
auto percent_done = 1.0;
auto piece_index = tr_piece_index_t{};
if (!is_done)
{
auto const [current, total] = builder_.checksumStatus();
percent_done = static_cast<double>(current) / total;
piece_index = current;
}
// progress text
auto str = std::string{};
auto success = false;
auto const base = Glib::path_get_basename(builder_.top());
if (!is_done)
{
str = fmt::format(_("Creating '{path}'"), fmt::arg("path", base));
}
else if (builder_.result == TrMakemetaResult::OK)
{
str = fmt::format(_("Created '{path}'"), fmt::arg("path", base));
}
else if (builder_.result == TrMakemetaResult::CANCELLED)
{
str = _("Cancelled");
}
else if (builder_.result == TrMakemetaResult::ERR_URL)
{
str = fmt::format(_("Unsupported URL: '{url}'"), fmt::arg("url", builder_.errfile));
}
else if (builder_.result == TrMakemetaResult::ERR_IO_READ)
{
str = fmt::format(
_("Couldn't read '{path}': {error} ({error_code})"),
fmt::arg("path", builder_.errfile),
fmt::arg("error", Glib::strerror(builder_.my_errno)),
fmt::arg("error_code", builder_.my_errno));
}
else if (builder_.result == TrMakemetaResult::ERR_IO_WRITE)
{
str = fmt::format(
_("Couldn't save '{path}': {error} ({error_code})"),
fmt::arg("path", builder_.errfile),
fmt::arg("error", Glib::strerror(builder_.my_errno)),
fmt::arg("error_code", builder_.my_errno));
}
else
{
g_assert_not_reached();
tr_error* error = future_.get();
if (error == nullptr)
{
builder_.save(target_, &error);
}
if (error == nullptr)
{
str = fmt::format(_("Created '{path}'"), fmt::arg("path", base));
success = true;
}
else
{
str = fmt::format(
_("Couldn't create '{path}': {error} ({error_code})"),
fmt::arg("path", base),
fmt::arg("error", error->message),
fmt::arg("error_code", error->code));
tr_error_free(error);
}
}
gtr_label_set_text(*progress_label_, str);
/* progress bar */
if (builder_.pieceIndex == 0)
if (piece_index == 0)
{
str.clear();
}
@@ -163,17 +185,18 @@ bool MakeProgressDialog::onProgressDialogRefresh()
/* how much data we've scanned through to generate checksums */
str = fmt::format(
_("Scanned {file_size}"),
fmt::arg("file_size", tr_strlsize((uint64_t)builder_.pieceIndex * (uint64_t)builder_.pieceSize)));
fmt::arg("file_size", tr_strlsize(static_cast<uint64_t>(piece_index) * builder_.pieceSize())));
}
progress_bar_->set_fraction(fraction);
progress_bar_->set_fraction(percent_done);
progress_bar_->set_text(str);
/* buttons */
set_response_sensitive(Gtk::RESPONSE_CANCEL, !builder_.isDone);
set_response_sensitive(Gtk::RESPONSE_CLOSE, builder_.isDone);
set_response_sensitive(Gtk::RESPONSE_ACCEPT, builder_.isDone && builder_.result == TrMakemetaResult::OK);
set_response_sensitive(Gtk::RESPONSE_CANCEL, !is_done);
set_response_sensitive(Gtk::RESPONSE_CLOSE, is_done);
set_response_sensitive(Gtk::RESPONSE_ACCEPT, is_done && success);
success_ = success;
return true;
}
@@ -186,7 +209,7 @@ void MakeProgressDialog::addTorrent()
{
tr_ctor* ctor = tr_ctorNew(core_->get_session());
tr_ctorSetMetainfoFromFile(ctor, target_.c_str(), nullptr);
tr_ctorSetDownloadDir(ctor, TR_FORCE, Glib::path_get_dirname(builder_.top).c_str());
tr_ctorSetDownloadDir(ctor, TR_FORCE, Glib::path_get_dirname(builder_.top()).c_str());
core_->add_ctor(ctor);
}
@@ -195,7 +218,7 @@ void MakeProgressDialog::onProgressDialogResponse(int response)
switch (response)
{
case Gtk::RESPONSE_CANCEL:
builder_.abortFlag = true;
builder_.cancelChecksums();
hide();
break;
@@ -215,12 +238,14 @@ void MakeProgressDialog::onProgressDialogResponse(int response)
MakeProgressDialog::MakeProgressDialog(
Gtk::Window& parent,
tr_metainfo_builder& builder,
std::string const& target,
std::future<tr_error*> future,
std::string_view target,
Glib::RefPtr<Session> const& core)
: Gtk::Dialog(_("New Torrent"), parent, true)
, builder_(builder)
, target_(target)
, core_(core)
, builder_{ builder }
, future_{ std::move(future) }
, target_{ target }
, core_{ core }
{
add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
add_button(_("_Close"), Gtk::RESPONSE_CLOSE);
@@ -250,15 +275,15 @@ MakeProgressDialog::MakeProgressDialog(
gtr_dialog_set_content(*this, *fr);
}
void MakeDialog::Impl::makeProgressDialog(std::string const& target)
void MakeDialog::Impl::makeProgressDialog(std::string_view target, std::future<tr_error*> future)
{
progress_dialog_ = std::make_unique<MakeProgressDialog>(dialog_, *builder_, target, core_);
progress_dialog_ = std::make_unique<MakeProgressDialog>(dialog_, *builder_, std::move(future), target, core_);
progress_dialog_->signal_hide().connect(
[this]()
{
auto const success = progress_dialog_->success();
progress_dialog_.reset();
if (builder_->result == TrMakemetaResult::OK)
if (success)
{
dialog_.hide();
}
@@ -270,54 +295,34 @@ void MakeDialog::Impl::onResponse(int response)
{
if (response == Gtk::RESPONSE_ACCEPT)
{
if (builder_ != nullptr)
if (builder_)
{
auto const comment = comment_entry_->get_text();
bool const isPrivate = private_check_->get_active();
bool const useComment = comment_check_->get_active();
bool const useSource = source_check_->get_active();
auto const source = source_entry_->get_text();
/* destination file */
// destination file
auto const dir = destination_chooser_->get_filename();
auto const base = Glib::path_get_basename(builder_->top);
auto const target = gtr_sprintf("%s/%s.torrent", dir, base);
auto const base = Glib::path_get_basename(builder_->top());
auto const target = fmt::format("{:s}/{:s}.torrent", dir, base);
/* build the array of trackers */
auto const tracker_text = announce_text_buffer_->get_text(false);
std::istringstream tracker_strings(tracker_text);
// build the announce list
auto trackers = tr_announce_list{};
trackers.parse(announce_text_buffer_->get_text(false).raw());
builder_->setAnnounceList(std::move(trackers));
std::vector<tr_tracker_info> trackers;
std::list<std::string> announce_urls;
int tier = 0;
std::string str;
while (std::getline(tracker_strings, str))
// comment
if (comment_check_->get_active())
{
if (str.empty())
{
++tier;
}
else
{
announce_urls.push_front(str);
trackers.push_back(tr_tracker_info{ tier, announce_urls.front().data() });
}
builder_->setComment(comment_entry_->get_text().raw());
}
/* build the .torrent */
makeProgressDialog(target);
tr_makeMetaInfo(
builder_.get(),
target.c_str(),
trackers.data(),
trackers.size(),
nullptr,
0,
useComment ? comment.c_str() : nullptr,
isPrivate,
false,
useSource ? source.c_str() : nullptr);
// source
if (source_check_->get_active())
{
builder_->setSource(source_entry_->get_text().raw());
}
builder_->setPrivate(private_check_->get_active());
// build the .torrent
makeProgressDialog(target, builder_->makeChecksums());
}
}
else if (response == Gtk::RESPONSE_CLOSE)
@@ -342,11 +347,11 @@ void onSourceToggled(Gtk::ToggleButton* tb, Gtk::Widget* widget)
void MakeDialog::Impl::updatePiecesLabel()
{
char const* filename = builder_ != nullptr ? builder_->top : nullptr;
auto const filename = builder_ ? builder_->top() : ""sv;
auto gstr = Glib::ustring{ "<i>" };
if (filename == nullptr)
if (std::empty(filename))
{
gstr += _("No source selected");
piece_size_scale_->set_visible(false);
@@ -354,17 +359,17 @@ void MakeDialog::Impl::updatePiecesLabel()
else
{
gstr += fmt::format(
ngettext("{total_size} in {file_count:L} file", "{total_size} in {file_count:L} files", builder_->fileCount),
fmt::arg("total_size", tr_strlsize(builder_->totalSize)),
fmt::arg("file_count", builder_->fileCount));
ngettext("{total_size} in {file_count:L} file", "{total_size} in {file_count:L} files", builder_->fileCount()),
fmt::arg("total_size", tr_strlsize(builder_->totalSize())),
fmt::arg("file_count", builder_->fileCount()));
gstr += ' ';
gstr += fmt::format(
ngettext(
"({piece_count} BitTorrent piece @ {piece_size})",
"({piece_count} BitTorrent pieces @ {piece_size})",
builder_->pieceCount),
fmt::arg("piece_count", builder_->pieceCount),
fmt::arg("piece_size", tr_formatter_mem_B(builder_->pieceSize)));
builder_->pieceCount()),
fmt::arg("piece_count", builder_->pieceCount()),
fmt::arg("piece_size", tr_formatter_mem_B(builder_->pieceSize())));
}
gstr += "</i>";
@@ -374,18 +379,18 @@ void MakeDialog::Impl::updatePiecesLabel()
void MakeDialog::Impl::configurePieceSizeScale()
{
// the below lower & upper bounds would allow piece size selection between approx 1KiB - 16MiB
auto adjustment = Gtk::Adjustment::create(log2(builder_->pieceSize), 10, 24, 1.0, 1.0);
auto adjustment = Gtk::Adjustment::create(log2(builder_->pieceSize()), 10, 24, 1.0, 1.0);
piece_size_scale_->set_adjustment(adjustment);
piece_size_scale_->set_visible(true);
}
void MakeDialog::Impl::setFilename(std::string const& filename)
void MakeDialog::Impl::setFilename(std::string_view filename)
{
builder_.reset();
if (!filename.empty())
{
builder_ = { tr_metaInfoBuilderCreate(filename.c_str()), &tr_metaInfoBuilderFree };
builder_.emplace(filename);
configurePieceSizeScale();
}
@@ -555,10 +560,9 @@ MakeDialog::Impl::Impl(MakeDialog& dialog, Glib::RefPtr<Session> const& core)
void MakeDialog::Impl::onPieceSizeUpdated()
{
if (builder_ != nullptr)
if (builder_)
{
auto new_size = static_cast<int>(pow(2, piece_size_scale_->get_value()));
tr_metaInfoBuilderSetPieceSize(builder_.get(), new_size);
builder_->setPieceSize(static_cast<uint32_t>(std::pow(2, piece_size_scale_->get_value())));
updatePiecesLabel();
}
}

View File

@@ -3,18 +3,13 @@
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstring> /* strcmp, strlen */
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include <event2/util.h> /* evutil_ascii_strcasecmp() */
#include <fmt/format.h>
#include "transmission.h"
@@ -24,60 +19,90 @@
#include "file.h"
#include "log.h"
#include "makemeta.h"
#include "session.h"
#include "session.h" // TR_NAME
#include "tr-assert.h"
#include "utils.h" /* buildpath */
#include "utils.h"
#include "variant.h"
#include "version.h"
#include "web-utils.h"
using namespace std::literals;
/****
*****
****/
struct FileList
namespace
{
uint64_t size;
char* filename;
struct FileList* next;
};
static struct FileList* getFiles(std::string_view dir, std::string_view base, struct FileList* list)
namespace find_files_helpers
{
if (std::empty(dir) || std::empty(base))
struct TorrentFile
{
TorrentFile(std::string_view subpath, size_t size)
: subpath_{ subpath }
, lowercase_{ tr_strlower(subpath) }
, size_{ size }
{
return nullptr;
}
auto buf = tr_pathbuf{ dir, '/', base };
tr_sys_path_native_separators(std::data(buf));
[[nodiscard]] auto operator<(TorrentFile const& that) const noexcept
{
return lowercase_ < that.lowercase_;
}
std::string subpath_;
std::string lowercase_;
uint64_t size_ = 0;
};
void walkTree(std::string_view const top, std::string_view const subpath, std::set<TorrentFile>& files)
{
TR_ASSERT(!std::empty(top));
TR_ASSERT(!std::empty(subpath));
if (std::empty(top) || std::empty(subpath))
{
return;
}
auto path = tr_pathbuf{ top, '/', subpath };
tr_sys_path_native_separators(std::data(path));
tr_error* error = nullptr;
auto const info = tr_sys_path_get_info(buf, 0, &error);
auto const info = tr_sys_path_get_info(path, 0, &error);
if (!info)
{
tr_logAddWarn(fmt::format(
_("Skipping '{path}': {error} ({error_code})"),
fmt::arg("path", buf),
fmt::arg("path", path),
fmt::arg("error", error->message),
fmt::arg("error_code", error->code)));
tr_error_free(error);
return list;
return;
}
switch (info->type)
{
case TR_SYS_PATH_IS_DIRECTORY:
if (auto const odir = tr_sys_dir_open(buf.c_str()); odir != TR_BAD_SYS_DIR)
if (tr_sys_dir_t odir = tr_sys_dir_open(path.c_str()); odir != TR_BAD_SYS_DIR)
{
char const* name = nullptr;
while ((name = tr_sys_dir_read_name(odir)) != nullptr)
for (;;)
{
if (name[0] != '.') /* skip dotfiles */
char const* const name = tr_sys_dir_read_name(odir);
if (name == nullptr)
{
list = getFiles(buf.c_str(), name, list);
break;
}
if (name[0] == '.') // skip dotfiles
{
continue;
}
if (!std::empty(subpath))
{
walkTree(top, tr_pathbuf{ subpath, '/', name }, files);
}
else
{
walkTree(top, name, files);
}
}
@@ -86,23 +111,40 @@ static struct FileList* getFiles(std::string_view dir, std::string_view base, st
break;
case TR_SYS_PATH_IS_FILE:
{
auto* const node = tr_new0(FileList, 1);
node->size = info->size;
node->filename = tr_strvDup(buf);
node->next = list;
list = node;
}
files.emplace(subpath, info->size);
break;
default:
break;
}
return list;
}
static uint32_t bestPieceSize(uint64_t totalSize)
} // namespace find_files_helpers
tr_torrent_files findFiles(std::string_view const top, std::string_view const subpath)
{
using namespace find_files_helpers;
auto tmp = std::set<TorrentFile>{};
walkTree(top, subpath, tmp);
auto files = tr_torrent_files{};
for (auto const& file : tmp)
{
files.add(file.subpath_, file.size_);
}
return files;
}
} // namespace
tr_metainfo_builder::tr_metainfo_builder(std::string_view filename)
: top_{ filename }
{
files_ = findFiles(tr_sys_path_dirname(filename), tr_sys_path_basename(filename));
block_info_ = tr_block_info{ files_.totalSize(), defaultPieceSize(files_.totalSize()) };
}
uint32_t tr_metainfo_builder::defaultPieceSize(uint64_t totalSize)
{
uint32_t const KiB = 1024;
uint32_t const MiB = 1048576;
@@ -141,498 +183,249 @@ static uint32_t bestPieceSize(uint64_t totalSize)
return 32 * KiB; /* less than 50 meg */
}
tr_metainfo_builder* tr_metaInfoBuilderCreate(char const* topFileArg)
bool tr_metainfo_builder::isLegalPieceSize(uint32_t x)
{
char* const real_top = tr_sys_path_resolve(topFileArg);
if (real_top == nullptr)
{
/* TODO: Better error reporting */
return nullptr;
}
auto* const ret = tr_new0(tr_metainfo_builder, 1);
ret->top = real_top;
auto const info = tr_sys_path_get_info(ret->top);
ret->isFolder = info && info->isFolder();
/* build a list of files containing top file and,
if it's a directory, all of its children */
auto* files = getFiles(tr_sys_path_dirname(ret->top), tr_sys_path_basename(ret->top), nullptr);
for (auto* walk = files; walk != nullptr; walk = walk->next)
{
++ret->fileCount;
}
ret->files = tr_new0(tr_metainfo_builder_file, ret->fileCount);
int i = 0;
auto const offset = strlen(ret->top);
while (files != nullptr)
{
auto* const tmp = files;
files = files->next;
auto* const file = &ret->files[i++];
file->filename = tmp->filename;
file->size = tmp->size;
file->is_portable = tr_torrent_files::isSubpathPortable(file->filename + offset);
ret->totalSize += tmp->size;
tr_free(tmp);
}
std::sort(
ret->files,
ret->files + ret->fileCount,
[](auto const& a, auto const& b) { return evutil_ascii_strcasecmp(a.filename, b.filename) < 0; });
tr_metaInfoBuilderSetPieceSize(ret, bestPieceSize(ret->totalSize));
return ret;
// It must be a power of two and at least 16KiB
static auto constexpr MinSize = uint32_t{ 1024U * 16U };
auto const is_power_of_two = (x & (x - 1)) == 0;
return x >= MinSize && is_power_of_two;
}
static bool isValidPieceSize(uint32_t n)
bool tr_metainfo_builder::setPieceSize(uint32_t piece_size) noexcept
{
bool const isPowerOfTwo = n != 0 && (n & (n - 1)) == 0;
return isPowerOfTwo;
}
bool tr_metaInfoBuilderSetPieceSize(tr_metainfo_builder* b, uint32_t bytes)
{
if (!isValidPieceSize(bytes))
if (!isLegalPieceSize(piece_size))
{
tr_logAddWarn(fmt::format(
_("Couldn't use invalid piece size {expected_size}; using {actual_size} instead"),
fmt::arg("expected_size", tr_formatter_mem_B(bytes)),
fmt::arg("actual_size", tr_formatter_mem_B(b->pieceSize))));
return false;
}
b->pieceSize = bytes;
b->pieceCount = (int)(b->totalSize / b->pieceSize);
if (b->totalSize % b->pieceSize != 0)
{
++b->pieceCount;
}
block_info_ = tr_block_info{ files_.totalSize(), piece_size };
return true;
}
void tr_metaInfoBuilderFree(tr_metainfo_builder* builder)
bool tr_metainfo_builder::blockingMakeChecksums(tr_error** error)
{
if (builder != nullptr)
checksum_piece_ = 0;
cancel_ = false;
if (totalSize() == 0U)
{
for (uint32_t i = 0; i < builder->fileCount; ++i)
{
tr_free(builder->files[i].filename);
}
tr_free(builder->files);
tr_free(builder->top);
tr_free(builder->comment);
tr_free(builder->source);
for (int i = 0; i < builder->trackerCount; ++i)
{
tr_free(builder->trackers[i].announce);
}
for (int i = 0; i < builder->webseedCount; ++i)
{
tr_free(builder->webseeds[i]);
}
tr_free(builder->trackers);
tr_free(builder->webseeds);
tr_free(builder->outputFile);
tr_free(builder);
}
}
/****
*****
****/
static std::vector<std::byte> getHashInfo(tr_metainfo_builder* b)
{
auto ret = std::vector<std::byte>(std::size(tr_sha1_digest_t{}) * b->pieceCount);
if (b->totalSize == 0)
{
return {};
tr_error_set(error, ENOENT, tr_strerror(ENOENT));
return false;
}
b->pieceIndex = 0;
uint64_t totalRemain = b->totalSize;
uint32_t fileIndex = 0;
auto* walk = std::data(ret);
auto buf = std::vector<char>(b->pieceSize);
uint64_t off = 0;
tr_error* error = nullptr;
auto hashes = std::vector<std::byte>(std::size(tr_sha1_digest_t{}) * pieceCount());
auto* walk = std::data(hashes);
auto sha = tr_sha1::create();
tr_sys_file_t fd = tr_sys_file_open(b->files[fileIndex].filename, TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL, 0, &error);
auto file_index = tr_file_index_t{ 0U };
auto piece_index = tr_piece_index_t{ 0U };
auto total_remain = totalSize();
auto off = uint64_t{ 0U };
auto buf = std::vector<char>(pieceSize());
auto const parent = tr_sys_path_dirname(top_);
auto fd = tr_sys_file_open(
tr_pathbuf{ parent, '/', path(file_index) },
TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL,
0,
error);
if (fd == TR_BAD_SYS_FILE)
{
b->my_errno = error->code;
tr_strlcpy(b->errfile, b->files[fileIndex].filename, sizeof(b->errfile));
b->result = TrMakemetaResult::ERR_IO_READ;
tr_error_free(error);
return {};
return false;
}
while (totalRemain != 0)
while (!cancel_ && (total_remain > 0U))
{
TR_ASSERT(b->pieceIndex < b->pieceCount);
checksum_piece_ = piece_index;
uint32_t const this_piece_size = std::min(uint64_t{ b->pieceSize }, totalRemain);
buf.resize(this_piece_size);
TR_ASSERT(piece_index < pieceCount());
uint32_t const piece_size = block_info_.pieceSize(piece_index);
buf.resize(piece_size);
auto* bufptr = std::data(buf);
uint64_t leftInPiece = this_piece_size;
while (leftInPiece != 0)
auto left_in_piece = piece_size;
while (left_in_piece > 0U)
{
uint64_t const n_this_pass = std::min(b->files[fileIndex].size - off, leftInPiece);
uint64_t n_read = 0;
(void)tr_sys_file_read(fd, bufptr, n_this_pass, &n_read);
auto const n_this_pass = std::min(fileSize(file_index) - off, uint64_t{ left_in_piece });
auto n_read = uint64_t{};
(void)tr_sys_file_read(fd, bufptr, n_this_pass, &n_read, error);
bufptr += n_read;
off += n_read;
leftInPiece -= n_read;
left_in_piece -= n_read;
if (off == b->files[fileIndex].size)
if (off == fileSize(file_index))
{
off = 0;
tr_sys_file_close(fd);
fd = TR_BAD_SYS_FILE;
if (++fileIndex < b->fileCount)
if (++file_index < fileCount())
{
fd = tr_sys_file_open(b->files[fileIndex].filename, TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL, 0, &error);
fd = tr_sys_file_open(
tr_pathbuf{ parent, '/', path(file_index) },
TR_SYS_FILE_READ | TR_SYS_FILE_SEQUENTIAL,
0,
error);
if (fd == TR_BAD_SYS_FILE)
{
b->my_errno = error->code;
tr_strlcpy(b->errfile, b->files[fileIndex].filename, sizeof(b->errfile));
b->result = TrMakemetaResult::ERR_IO_READ;
tr_error_free(error);
return {};
return false;
}
}
}
}
TR_ASSERT(bufptr - std::data(buf) == (int)this_piece_size);
TR_ASSERT(leftInPiece == 0);
auto const digest = tr_sha1::digest(buf);
TR_ASSERT(bufptr - std::data(buf) == (int)piece_size);
TR_ASSERT(left_in_piece == 0);
sha->add(std::data(buf), std::size(buf));
auto const digest = sha->final();
walk = std::copy(std::begin(digest), std::end(digest), walk);
sha->clear();
if (b->abortFlag)
{
b->result = TrMakemetaResult::CANCELLED;
break;
}
totalRemain -= this_piece_size;
++b->pieceIndex;
total_remain -= piece_size;
++piece_index;
}
TR_ASSERT(b->abortFlag || size_t(walk - std::data(ret)) == std::size(ret));
TR_ASSERT(b->abortFlag || !totalRemain);
TR_ASSERT(cancel_ || size_t(walk - std::data(hashes)) == std::size(hashes));
TR_ASSERT(cancel_ || total_remain == 0U);
if (fd != TR_BAD_SYS_FILE)
{
tr_sys_file_close(fd);
}
return ret;
}
static void getFileInfo(
char const* topFile,
tr_metainfo_builder_file const* file,
tr_variant* uninitialized_length,
tr_variant* uninitialized_path)
{
/* get the file size */
tr_variantInitInt(uninitialized_length, file->size);
/* how much of file->filename to walk past */
size_t offset = strlen(topFile);
if (offset > 0 && topFile[offset - 1] != TR_PATH_DELIMITER)
if (cancel_)
{
++offset; /* +1 for the path delimiter */
tr_error_set(error, ECANCELED, tr_strerror(ECANCELED));
return false;
}
/* build the path list */
tr_variantInitList(uninitialized_path, 0);
auto filename = std::string_view{ file->filename };
if (std::size(filename) > offset)
{
filename.remove_prefix(offset);
auto token = std::string_view{};
while (tr_strvSep(&filename, &token, TR_PATH_DELIMITER))
{
tr_variantListAddStr(uninitialized_path, token);
}
}
piece_hashes_ = std::move(hashes);
return true;
}
static void makeInfoDict(tr_variant* dict, tr_metainfo_builder* builder)
std::string tr_metainfo_builder::benc(tr_error** error) const
{
tr_variantDictReserve(dict, 5);
TR_ASSERT_MSG(!std::empty(piece_hashes_), "did you forget to call makeChecksums() first?");
if (builder->isFolder) /* root node is a directory */
auto const anonymize = this->anonymize();
auto const& comment = this->comment();
auto const& source = this->source();
auto const& webseeds = this->webseeds();
if (totalSize() == 0)
{
tr_variant* list = tr_variantDictAddList(dict, TR_KEY_files, builder->fileCount);
tr_error_set(error, ENOENT, tr_strerror(ENOENT));
return {};
}
for (uint32_t i = 0; i < builder->fileCount; ++i)
auto top = tr_variant{};
tr_variantInitDict(&top, 8);
// add the announce-list trackers
if (!std::empty(announceList()))
{
auto* const announce_list = tr_variantDictAddList(&top, TR_KEY_announce_list, 0);
tr_variant* tier_list = nullptr;
auto prev_tier = std::optional<tr_tracker_tier_t>{};
for (auto const& tracker : announceList())
{
tr_variant* d = tr_variantListAddDict(list, 2);
tr_variant* length = tr_variantDictAdd(d, TR_KEY_length);
tr_variant* pathVal = tr_variantDictAdd(d, TR_KEY_path);
getFileInfo(builder->top, &builder->files[i], length, pathVal);
if (!prev_tier || *prev_tier != tracker.tier)
{
tier_list = nullptr;
}
if (tier_list == nullptr)
{
prev_tier = tracker.tier;
tier_list = tr_variantListAddList(announce_list, 0);
}
tr_variantListAddStr(tier_list, tracker.announce);
}
}
// add the webseeds
if (!std::empty(webseeds))
{
auto* const url_list = tr_variantDictAddList(&top, TR_KEY_url_list, std::size(webseeds));
for (auto const& webseed : webseeds)
{
tr_variantListAddStr(url_list, webseed);
}
}
// add the comment
if (!std::empty(comment))
{
tr_variantDictAddStr(&top, TR_KEY_comment, comment);
}
// maybe add some optional metainfo
if (!anonymize)
{
tr_variantDictAddStrView(&top, TR_KEY_created_by, TR_NAME "/" LONG_VERSION_STRING);
tr_variantDictAddInt(&top, TR_KEY_creation_date, time(nullptr));
}
tr_variantDictAddStrView(&top, TR_KEY_encoding, "UTF-8");
if (is_private_)
{
tr_variantDictAddInt(&top, TR_KEY_private, 1);
}
if (!std::empty(source))
{
tr_variantDictAddStr(&top, TR_KEY_source, source_);
}
auto* const info_dict = tr_variantDictAddDict(&top, TR_KEY_info, 5);
auto const base = tr_sys_path_basename(top_);
// "There is also a key `length` or a key `files`, but not both or neither.
// If length is present then the download represents a single file,
// otherwise it represents a set of files which go in a directory structure."
if (fileCount() == 1U && !tr_strvContains(path(0), '/'))
{
tr_variantDictAddInt(info_dict, TR_KEY_length, fileSize(0));
}
else
{
tr_variantDictAddInt(dict, TR_KEY_length, builder->files[0].size);
}
auto const n_files = fileCount();
auto* const file_list = tr_variantDictAddList(info_dict, TR_KEY_files, n_files);
if (auto const base = tr_sys_path_basename(builder->top); !std::empty(base))
{
tr_variantDictAddStr(dict, TR_KEY_name, base);
}
tr_variantDictAddInt(dict, TR_KEY_piece_length, builder->pieceSize);
if (auto const piece_hashes = getHashInfo(builder); !std::empty(piece_hashes))
{
tr_variantDictAddRaw(dict, TR_KEY_pieces, std::data(piece_hashes), std::size(piece_hashes));
}
if (builder->isPrivate)
{
tr_variantDictAddInt(dict, TR_KEY_private, 1);
}
if (builder->source != nullptr)
{
tr_variantDictAddStr(dict, TR_KEY_source, builder->source);
}
}
static void tr_realMakeMetaInfo(tr_metainfo_builder* builder)
{
tr_variant top;
for (int i = 0; i < builder->trackerCount && builder->result == TrMakemetaResult::OK; ++i)
{
if (!tr_urlIsValidTracker(builder->trackers[i].announce))
for (tr_file_index_t i = 0; i < n_files; ++i)
{
tr_strlcpy(builder->errfile, builder->trackers[i].announce, sizeof(builder->errfile));
builder->result = TrMakemetaResult::ERR_URL;
}
}
auto* const file_dict = tr_variantListAddDict(file_list, 2);
tr_variantDictAddInt(file_dict, TR_KEY_length, fileSize(i));
for (int i = 0; i < builder->webseedCount && builder->result == TrMakemetaResult::OK; ++i)
{
if (!tr_urlIsValid(builder->webseeds[i]))
{
tr_strlcpy(builder->errfile, builder->webseeds[i], sizeof(builder->errfile));
builder->result = TrMakemetaResult::ERR_URL;
}
}
tr_variantInitDict(&top, 6);
if (builder->fileCount == 0 || builder->totalSize == 0 || builder->pieceSize == 0 || builder->pieceCount == 0)
{
builder->errfile[0] = '\0';
builder->my_errno = ENOENT;
builder->result = TrMakemetaResult::ERR_IO_READ;
builder->isDone = true;
}
if (builder->result == TrMakemetaResult::OK && builder->trackerCount != 0)
{
int prevTier = -1;
tr_variant* tier = nullptr;
if (builder->trackerCount > 1)
{
tr_variant* annList = tr_variantDictAddList(&top, TR_KEY_announce_list, 0);
for (int i = 0; i < builder->trackerCount; ++i)
auto subpath = std::string_view{ path(i) };
if (!std::empty(base))
{
if (prevTier != builder->trackers[i].tier)
{
prevTier = builder->trackers[i].tier;
tier = tr_variantListAddList(annList, 0);
}
subpath.remove_prefix(std::size(base) + std::size("/"sv));
}
tr_variantListAddStr(tier, builder->trackers[i].announce);
auto* const path_list = tr_variantDictAddList(file_dict, TR_KEY_path, 0);
auto token = std::string_view{};
while (tr_strvSep(&subpath, &token, '/'))
{
tr_variantListAddStr(path_list, token);
}
}
tr_variantDictAddStr(&top, TR_KEY_announce, builder->trackers[0].announce);
}
if (builder->result == TrMakemetaResult::OK && builder->webseedCount > 0)
if (!std::empty(base))
{
tr_variant* url_list = tr_variantDictAddList(&top, TR_KEY_url_list, builder->webseedCount);
for (int i = 0; i < builder->webseedCount; ++i)
{
tr_variantListAddStr(url_list, builder->webseeds[i]);
}
tr_variantDictAddStr(info_dict, TR_KEY_name, base);
}
if (builder->result == TrMakemetaResult::OK && !builder->abortFlag)
{
if (!tr_str_is_empty(builder->comment))
{
tr_variantDictAddStr(&top, TR_KEY_comment, builder->comment);
}
if (!builder->anonymize)
{
tr_variantDictAddStrView(&top, TR_KEY_created_by, TR_NAME "/" LONG_VERSION_STRING);
tr_variantDictAddInt(&top, TR_KEY_creation_date, time(nullptr));
}
tr_variantDictAddStrView(&top, TR_KEY_encoding, "UTF-8");
makeInfoDict(tr_variantDictAddDict(&top, TR_KEY_info, 666), builder);
}
/* save the file */
if ((builder->result == TrMakemetaResult::OK) && (!builder->abortFlag) &&
(tr_variantToFile(&top, TR_VARIANT_FMT_BENC, builder->outputFile) != 0))
{
builder->my_errno = errno;
tr_strlcpy(builder->errfile, builder->outputFile, sizeof(builder->errfile));
builder->result = TrMakemetaResult::ERR_IO_WRITE;
}
/* cleanup */
tr_variantDictAddInt(info_dict, TR_KEY_piece_length, pieceSize());
tr_variantDictAddRaw(info_dict, TR_KEY_pieces, std::data(piece_hashes_), std::size(piece_hashes_));
auto ret = tr_variantToStr(&top, TR_VARIANT_FMT_BENC);
tr_variantFree(&top);
if (builder->abortFlag)
{
builder->result = TrMakemetaResult::CANCELLED;
}
builder->isDone = true;
}
/***
****
**** A threaded builder queue
****
***/
static tr_metainfo_builder* queue = nullptr;
static std::optional<std::thread::id> worker_thread_id;
static std::recursive_mutex queue_mutex_;
static void makeMetaWorkerFunc()
{
for (;;)
{
tr_metainfo_builder* builder = nullptr;
/* find the next builder to process */
queue_mutex_.lock();
if (queue != nullptr)
{
builder = queue;
queue = queue->nextBuilder;
}
queue_mutex_.unlock();
/* if no builders, this worker thread is done */
if (builder == nullptr)
{
break;
}
tr_realMakeMetaInfo(builder);
}
worker_thread_id.reset();
}
void tr_makeMetaInfo(
tr_metainfo_builder* builder,
char const* outputFile,
tr_tracker_info const* trackers,
int trackerCount,
char const** webseeds,
int webseedCount,
char const* comment,
bool isPrivate,
bool anonymize,
char const* source)
{
/* free any variables from a previous run */
for (int i = 0; i < builder->trackerCount; ++i)
{
tr_free(builder->trackers[i].announce);
}
tr_free(builder->trackers);
tr_free(builder->comment);
tr_free(builder->source);
tr_free(builder->outputFile);
/* initialize the builder variables */
builder->abortFlag = false;
builder->result = TrMakemetaResult::OK;
builder->isDone = false;
builder->pieceIndex = 0;
builder->trackerCount = trackerCount;
builder->trackers = tr_new0(tr_tracker_info, builder->trackerCount);
for (int i = 0; i < builder->trackerCount; ++i)
{
builder->trackers[i].tier = trackers[i].tier;
builder->trackers[i].announce = tr_strdup(trackers[i].announce);
}
builder->webseedCount = webseedCount;
builder->webseeds = tr_new0(char*, webseedCount);
for (int i = 0; i < webseedCount; ++i)
{
builder->webseeds[i] = tr_strdup(webseeds[i]);
}
builder->comment = tr_strdup(comment);
builder->isPrivate = isPrivate;
builder->anonymize = anonymize;
builder->source = tr_strdup(source);
builder->outputFile = !tr_str_is_empty(outputFile) ? tr_strdup(outputFile) :
tr_strvDup(fmt::format(FMT_STRING("{:s}.torrent"), builder->top));
/* enqueue the builder */
auto const lock = std::lock_guard(queue_mutex_);
builder->nextBuilder = queue;
queue = builder;
if (!worker_thread_id)
{
auto thread = std::thread(makeMetaWorkerFunc);
worker_thread_id = thread.get_id();
thread.detach();
}
return ret;
}

View File

@@ -5,129 +5,200 @@
#pragma once
#include <cstdint> // uint32_t, uint64_t
#include <algorithm> // std::move
#include <cstddef> // std::byte
#include <future>
#include <string>
#include <string_view>
#include <utility> // std::pair
#include <vector>
#include "transmission.h"
struct tr_metainfo_builder_file
#include "announce-list.h"
#include "block-info.h"
#include "file.h"
#include "torrent-files.h"
class tr_metainfo_builder
{
char* filename;
uint64_t size;
bool is_portable;
public:
tr_metainfo_builder(std::string_view single_file_or_parent_directory);
tr_metainfo_builder(tr_metainfo_builder&&) = delete;
tr_metainfo_builder(tr_metainfo_builder const&) = delete;
tr_metainfo_builder& operator=(tr_metainfo_builder&&) = delete;
tr_metainfo_builder& operator=(tr_metainfo_builder const&) = delete;
// Generate piece checksums asynchronously.
// - This must be done before calling `benc()` or `save()`.
// - Runs in a worker thread because it can be time-consuming.
// - Can be cancelled with `cancelChecksums()` and polled with `checksumStatus()`
// - Resolves with a `tr_error*` which is set on failure or nullptr on success.
std::future<tr_error*> makeChecksums()
{
return std::async(
std::launch::async,
[this]()
{
tr_error* error = nullptr;
blockingMakeChecksums(&error);
return error;
});
}
// Returns the status of a `makeChecksums()` call:
// The current piece being tested and the total number of pieces in the torrent.
[[nodiscard]] constexpr std::pair<tr_piece_index_t, tr_piece_index_t> checksumStatus() const noexcept
{
return std::make_pair(checksum_piece_, block_info_.pieceCount());
}
// Tell the `makeChecksums()` worker thread to cleanly exit ASAP.
constexpr void cancelChecksums() noexcept
{
cancel_ = true;
}
// generate the metainfo
std::string benc(tr_error** error = nullptr) const;
// generate the metainfo and save it to a torrent file
bool save(std::string_view filename, tr_error** error = nullptr) const
{
return tr_saveFile(filename, benc(error), error);
}
/// setters
void setAnnounceList(tr_announce_list announce)
{
announce_ = std::move(announce);
}
// whether or not to include User-Agent and creation time
constexpr void setAnonymize(bool anonymize) noexcept
{
anonymize_ = anonymize;
}
void setComment(std::string_view comment)
{
comment_ = comment;
}
bool setPieceSize(uint32_t piece_size) noexcept;
constexpr void setPrivate(bool is_private) noexcept
{
is_private_ = is_private;
}
void setSource(std::string_view source)
{
source_ = source;
}
void setWebseeds(std::vector<std::string> webseeds)
{
webseeds_ = std::move(webseeds);
}
/// getters
[[nodiscard]] constexpr auto const& announceList() const noexcept
{
return announce_;
}
[[nodiscard]] constexpr auto const& anonymize() const noexcept
{
return anonymize_;
}
[[nodiscard]] constexpr auto const& comment() const noexcept
{
return comment_;
}
[[nodiscard]] auto fileCount() const noexcept
{
return files_.fileCount();
}
[[nodiscard]] auto fileSize(tr_file_index_t i) const noexcept
{
return files_.fileSize(i);
}
[[nodiscard]] constexpr auto const& isPrivate() const noexcept
{
return is_private_;
}
[[nodiscard]] auto name() const noexcept
{
return tr_sys_path_basename(top_);
}
[[nodiscard]] auto const& path(tr_file_index_t i) const noexcept
{
return files_.path(i);
}
[[nodiscard]] constexpr auto pieceSize() const noexcept
{
return block_info_.pieceSize();
}
[[nodiscard]] constexpr auto pieceCount() const noexcept
{
return block_info_.pieceCount();
}
[[nodiscard]] constexpr auto const& source() const noexcept
{
return source_;
}
[[nodiscard]] constexpr auto const& top() const noexcept
{
return top_;
}
[[nodiscard]] constexpr auto totalSize() const noexcept
{
return files_.totalSize();
}
[[nodiscard]] constexpr auto const& webseeds() const noexcept
{
return webseeds_;
}
///
[[nodiscard]] static uint32_t defaultPieceSize(uint64_t total_size);
// must be a power of two and >= 16 KiB
[[nodiscard]] static bool isLegalPieceSize(uint32_t x);
private:
bool blockingMakeChecksums(tr_error** error = nullptr);
std::string top_;
tr_torrent_files files_;
tr_announce_list announce_;
tr_block_info block_info_;
std::vector<std::byte> piece_hashes_;
std::vector<std::string> webseeds_;
std::string comment_;
std::string source_;
tr_piece_index_t checksum_piece_ = 0;
bool is_private_ = false;
bool anonymize_ = false;
bool cancel_ = false;
};
enum class TrMakemetaResult
{
OK,
CANCELLED,
ERR_URL, // invalid announce URL
ERR_IO_READ, // see builder.errfile, builder.my_errno
ERR_IO_WRITE // see builder.errfile, builder.my_errno
};
struct tr_tracker_info
{
int tier;
char* announce;
};
struct tr_metainfo_builder
{
/**
*** These are set by tr_makeMetaInfoBuilderCreate()
*** and cleaned up by tr_metaInfoBuilderFree()
**/
char* top;
tr_metainfo_builder_file* files;
uint64_t totalSize;
uint32_t fileCount;
uint32_t pieceSize;
uint32_t pieceCount;
bool isFolder;
/**
*** These are set inside tr_makeMetaInfo()
*** by copying the arguments passed to it,
*** and cleaned up by tr_metaInfoBuilderFree()
**/
tr_tracker_info* trackers;
int trackerCount;
char** webseeds;
int webseedCount;
char* comment;
char* outputFile;
bool isPrivate;
char* source;
bool anonymize;
/**
*** These are set inside tr_makeMetaInfo() so the client
*** can poll periodically to see what the status is.
*** The client can also set abortFlag to nonzero to
*** tell tr_makeMetaInfo() to abort and clean up after itself.
**/
uint32_t pieceIndex;
bool abortFlag;
bool isDone;
TrMakemetaResult result;
/* file in use when result was set to _IO_READ or _IO_WRITE,
* or the URL in use when the result was set to _URL */
char errfile[2048];
/* errno encountered when result was set to _IO_READ or _IO_WRITE */
int my_errno;
/**
*** This is an implementation detail.
*** The client should never use these fields.
**/
struct tr_metainfo_builder* nextBuilder;
};
tr_metainfo_builder* tr_metaInfoBuilderCreate(char const* topFile);
/**
* Call this before tr_makeMetaInfo() to override the builder.pieceSize
* and builder.pieceCount values that were set by tr_metainfoBuilderCreate()
*
* @return false if the piece size isn't valid; eg, isn't a power of two.
*/
bool tr_metaInfoBuilderSetPieceSize(tr_metainfo_builder* builder, uint32_t bytes);
void tr_metaInfoBuilderFree(tr_metainfo_builder*);
/**
* @brief create a new torrent file
*
* This is actually done in a worker thread, not the main thread!
* Otherwise the client's interface would lock up while this runs.
*
* It is the caller's responsibility to poll builder->isDone
* from time to time! When the worker thread sets that flag,
* the caller must pass the builder to tr_metaInfoBuilderFree().
*
* @param outputFile if nullptr, builder->top + ".torrent" will be used.
* @param trackers An array of trackers, sorted by tier from first to last.
* NOTE: only the `tier' and `announce' fields are used.
*
* @param trackerCount size of the `trackers' array
*/
void tr_makeMetaInfo(
tr_metainfo_builder* builder,
char const* output_file,
tr_tracker_info const* trackers,
int n_trackers,
char const** webseeds,
int n_webseeds,
char const* comment,
bool is_private,
bool anonymize,
char const* source);

View File

@@ -2379,7 +2379,7 @@ static void loadBlocklists(tr_session* session)
}
}
else if (auto const pathinfo = tr_sys_path_get_info(path);
path && pathinfo->last_modified_at >= bininfo->last_modified_at)
pathinfo && pathinfo->last_modified_at >= bininfo->last_modified_at)
{
// update it
auto const old = tr_pathbuf{ binname, ".old"sv };

View File

@@ -147,8 +147,6 @@ public:
return date_created_;
}
[[nodiscard]] std::string benc() const;
[[nodiscard]] constexpr auto infoDictSize() const noexcept
{
return info_dict_size_;

View File

@@ -2,11 +2,17 @@
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#include <chrono>
#include <cmath>
#include <future>
#include <optional>
#include <libtransmission/transmission.h>
#include <libtransmission/error.h>
#include <libtransmission/makemeta.h>
#include <libtransmission/utils.h>
#include <libtransmission/web-utils.h> // tr_urlIsValidTracker()
#include <cmath>
#import "CreatorWindowController.h"
#import "Controller.h"
@@ -33,9 +39,10 @@
@property(nonatomic) IBOutlet NSView* fProgressView;
@property(nonatomic) IBOutlet NSProgressIndicator* fProgressIndicator;
@property(nonatomic, readonly) tr_metainfo_builder* fInfo;
@property(nonatomic, readonly) std::shared_ptr<tr_metainfo_builder> fBuilder;
@property(nonatomic, readonly) NSURL* fPath;
@property(nonatomic) NSURL* fLocation;
@property(nonatomic) std::shared_future<tr_error*> fFuture;
@property(nonatomic) NSURL* fLocation; // path to new torrent file
@property(nonatomic) NSMutableArray<NSString*>* fTrackers;
@property(nonatomic) NSTimer* fTimer;
@@ -89,9 +96,9 @@ NSMutableSet* creatorWindowControllerSet = nil;
_fStarted = NO;
_fPath = path;
_fInfo = tr_metaInfoBuilderCreate(_fPath.path.UTF8String);
_fBuilder = std::make_shared<tr_metainfo_builder>(_fPath.path.UTF8String);
if (_fInfo->fileCount == 0)
if (_fBuilder->fileCount() == 0U)
{
NSAlert* alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> no files -> button")];
@@ -105,7 +112,7 @@ NSMutableSet* creatorWindowControllerSet = nil;
return nil;
}
if (_fInfo->totalSize == 0)
if (_fBuilder->totalSize() == 0U)
{
NSAlert* alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> zero size -> button")];
@@ -166,32 +173,26 @@ NSMutableSet* creatorWindowControllerSet = nil;
self.fNameField.stringValue = name;
self.fNameField.toolTip = self.fPath.path;
BOOL const multifile = self.fInfo->isFolder;
auto const is_folder = self.fBuilder->fileCount() > 1 || tr_strvContains(self.fBuilder->path(0), '/');
NSImage* icon = [NSWorkspace.sharedWorkspace
iconForFileType:multifile ? NSFileTypeForHFSTypeCode(kGenericFolderIcon) : self.fPath.pathExtension];
iconForFileType:is_folder ? NSFileTypeForHFSTypeCode(kGenericFolderIcon) : self.fPath.pathExtension];
icon.size = self.fIconView.frame.size;
self.fIconView.image = icon;
NSString* statusString = [NSString stringForFileSize:self.fInfo->totalSize];
if (multifile)
NSString* status_string = [NSString stringForFileSize:self.fBuilder->totalSize()];
if (is_folder)
{
NSString* fileString;
NSUInteger count = self.fInfo->fileCount;
if (count != 1)
{
fileString = [NSString stringWithFormat:NSLocalizedString(@"%lu files", "Create torrent -> info"), count];
}
else
{
fileString = NSLocalizedString(@"1 file", "Create torrent -> info");
}
statusString = [NSString stringWithFormat:@"%@, %@", fileString, statusString];
NSUInteger const count = self.fBuilder->fileCount();
NSString* const fileString = count != 1 ?
[NSString stringWithFormat:NSLocalizedString(@"%lu files", "Create torrent -> info"), count] :
NSLocalizedString(@"1 file", "Create torrent -> info");
status_string = [NSString stringWithFormat:@"%@, %@", fileString, status_string];
}
self.fStatusField.stringValue = statusString;
self.fStatusField.stringValue = status_string;
[self updatePiecesField];
self.fPieceSizeStepper.intValue = (int)log2((double)self.fInfo->pieceSize);
self.fPieceSizeStepper.intValue = static_cast<int>(log2(self.fBuilder->pieceSize()));
self.fLocation = [[self.fDefaults URLForKey:@"CreatorLocationURL"]
URLByAppendingPathComponent:[name stringByAppendingPathExtension:@"torrent"]];
@@ -225,10 +226,7 @@ NSMutableSet* creatorWindowControllerSet = nil;
- (void)dealloc
{
if (_fInfo)
{
tr_metaInfoBuilderFree(_fInfo);
}
_fBuilder.reset();
[_fTimer invalidate];
}
@@ -359,14 +357,15 @@ NSMutableSet* creatorWindowControllerSet = nil;
- (IBAction)cancelCreateProgress:(id)sender
{
self.fInfo->abortFlag = 1;
self.fBuilder->cancelChecksums();
[self.fTimer fire];
}
- (IBAction)incrementOrDecrementPieceSize:(id)sender
{
uint32_t pieceSize = (uint32_t)pow(2.0, [sender intValue]);
if (tr_metaInfoBuilderSetPieceSize(self.fInfo, pieceSize))
auto const piece_size = static_cast<uint32_t>(pow(2.0, [sender intValue]));
if (self.fBuilder->setPieceSize(piece_size))
{
[self updatePiecesField];
}
@@ -517,16 +516,19 @@ NSMutableSet* creatorWindowControllerSet = nil;
- (void)updatePiecesField
{
if (self.fInfo->pieceCount == 1)
auto const piece_size = self.fBuilder->pieceSize();
auto const piece_count = self.fBuilder->pieceCount();
if (piece_count == 1U)
{
self.fPiecesField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"1 piece, %@", "Create torrent -> info"),
[NSString stringForFileSize:self.fInfo->pieceSize]];
[NSString stringForFileSize:piece_size]];
}
else
{
self.fPiecesField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"%u pieces, %@ each", "Create torrent -> info"),
self.fInfo->pieceCount,
[NSString stringForFileSize:self.fInfo->pieceSize]];
piece_count,
[NSString stringForFileSize:piece_size]];
}
}
@@ -596,14 +598,13 @@ NSMutableSet* creatorWindowControllerSet = nil;
return;
}
//parse non-empty tracker strings
tr_tracker_info* trackerInfo = tr_new0(tr_tracker_info, self.fTrackers.count);
for (NSUInteger i = 0; i < self.fTrackers.count; i++)
// trackers
auto trackers = tr_announce_list{};
for (NSUInteger i = 0; i < self.fTrackers.count; ++i)
{
trackerInfo[i].announce = (char*)(self.fTrackers[i]).UTF8String;
trackerInfo[i].tier = i;
trackers.add((char*)(self.fTrackers[i]).UTF8String, trackers.nextTier());
}
self.fBuilder->setAnnounceList(std::move(trackers));
//store values
[self.fDefaults setObject:self.fTrackers forKey:@"CreatorTrackers"];
@@ -617,88 +618,24 @@ NSMutableSet* creatorWindowControllerSet = nil;
self.window.restorable = NO;
[NSNotificationCenter.defaultCenter postNotificationName:@"BeginCreateTorrentFile" object:self.fLocation userInfo:nil];
tr_makeMetaInfo(
self.fInfo,
self.fLocation.path.UTF8String,
trackerInfo,
self.fTrackers.count,
nullptr,
0,
self.fCommentView.string.UTF8String,
self.fPrivateCheck.state == NSControlStateValueOn,
NO,
self.fSource.stringValue.UTF8String);
tr_free(trackerInfo);
self.fBuilder->setComment(self.fCommentView.string.UTF8String);
self.fBuilder->setPrivate(self.fPrivateCheck.state == NSControlStateValueOn);
self.fBuilder->setSource(self.fSource.stringValue.UTF8String);
self.fFuture = self.fBuilder->makeChecksums();
self.fTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(checkProgress) userInfo:nil
repeats:YES];
}
- (void)checkProgress
{
if (self.fInfo->isDone)
auto const is_done = !self.fFuture.valid() || self.fFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
if (!is_done)
{
[self.fTimer invalidate];
self.fTimer = nil;
NSAlert* alert;
switch (self.fInfo->result)
{
case TrMakemetaResult::OK:
if (self.fOpenWhenCreated)
{
NSDictionary* dict = @{
@"File" : self.fLocation.path,
@"Path" : self.fPath.URLByDeletingLastPathComponent.path
};
[NSNotificationCenter.defaultCenter postNotificationName:@"OpenCreatedTorrentFile" object:self userInfo:dict];
}
[self.window close];
break;
case TrMakemetaResult::CANCELLED:
[self.window close];
break;
default:
alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> failed -> button")];
alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Creation of \"%@\" failed.", "Create torrent -> failed -> title"),
self.fLocation.lastPathComponent];
alert.alertStyle = NSAlertStyleWarning;
if (self.fInfo->result == TrMakemetaResult::ERR_IO_READ)
{
alert.informativeText = [NSString
stringWithFormat:NSLocalizedString(@"Could not read \"%s\": %s.", "Create torrent -> failed -> warning"),
self.fInfo->errfile,
strerror(self.fInfo->my_errno)];
}
else if (self.fInfo->result == TrMakemetaResult::ERR_IO_WRITE)
{
alert.informativeText = [NSString
stringWithFormat:NSLocalizedString(@"Could not write \"%s\": %s.", "Create torrent -> failed -> warning"),
self.fInfo->errfile,
strerror(self.fInfo->my_errno)];
}
else //invalid url should have been caught before creating
{
alert.informativeText = [NSString
stringWithFormat:@"%@ (%d)",
NSLocalizedString(@"An unknown error has occurred.", "Create torrent -> failed -> warning"),
self.fInfo->result];
}
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
[alert.window orderOut:nil];
[self.window close];
}];
}
}
else
{
self.fProgressIndicator.doubleValue = (double)self.fInfo->pieceIndex / self.fInfo->pieceCount;
auto const [current, total] = self.fBuilder->checksumStatus();
self.fProgressIndicator.doubleValue = static_cast<double>(current) / total;
if (!self.fStarted)
{
@@ -725,6 +662,45 @@ NSMutableSet* creatorWindowControllerSet = nil;
[window standardWindowButton:NSWindowCloseButton].enabled = NO;
}
return;
}
// stop the timer
[self.fTimer invalidate];
self.fTimer = nil;
auto success = false;
tr_error* error = self.fFuture.get();
if (error == nullptr)
{
self.fBuilder->save(self.fLocation.path.UTF8String, &error);
}
if (error != nullptr)
{
auto* const alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK", "Create torrent -> failed -> button")];
alert.messageText = [NSString stringWithFormat:NSLocalizedString(@"Creation of \"%@\" failed.", "Create torrent -> failed -> title"),
self.fLocation.lastPathComponent];
alert.alertStyle = NSAlertStyleWarning;
alert.informativeText = [NSString stringWithFormat:@"%s (%d)", error->message, error->code];
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
[alert.window orderOut:nil];
[self.window close];
}];
tr_error_free(error);
}
else
{
if (self.fOpenWhenCreated)
{
NSDictionary* dict = @{ @"File" : self.fLocation.path, @"Path" : self.fPath.URLByDeletingLastPathComponent.path };
[NSNotificationCenter.defaultCenter postNotificationName:@"OpenCreatedTorrentFile" object:self userInfo:dict];
}
[self.window close];
}
}

View File

@@ -5,6 +5,8 @@
#include "MakeDialog.h"
#include <chrono>
#include <future>
#include <vector>
#include <QDir>
@@ -13,6 +15,7 @@
#include <QPushButton>
#include <QTimer>
#include <libtransmission/error.h>
#include <libtransmission/makemeta.h>
#include <libtransmission/transmission.h>
#include <libtransmission/utils.h>
@@ -32,7 +35,12 @@ class MakeProgressDialog : public BaseDialog
Q_OBJECT
public:
MakeProgressDialog(Session& session, tr_metainfo_builder& builder, QWidget* parent = nullptr);
MakeProgressDialog(
Session& session,
tr_metainfo_builder& builder,
std::future<tr_error*> future,
QString outfile,
QWidget* parent = nullptr);
private slots:
void onButtonBoxClicked(QAbstractButton* button);
@@ -41,16 +49,25 @@ private slots:
private:
Session& session_;
tr_metainfo_builder& builder_;
std::future<tr_error*> future_;
QString const outfile_;
Ui::MakeProgressDialog ui_ = {};
QTimer timer_;
};
} // namespace
MakeProgressDialog::MakeProgressDialog(Session& session, tr_metainfo_builder& builder, QWidget* parent)
MakeProgressDialog::MakeProgressDialog(
Session& session,
tr_metainfo_builder& builder,
std::future<tr_error*> future,
QString outfile,
QWidget* parent)
: BaseDialog(parent)
, session_(session)
, builder_(builder)
, future_(std::move(future))
, outfile_(std::move(outfile))
{
ui_.setupUi(this);
@@ -67,13 +84,11 @@ void MakeProgressDialog::onButtonBoxClicked(QAbstractButton* button)
switch (ui_.dialogButtons->standardButton(button))
{
case QDialogButtonBox::Open:
session_.addNewlyCreatedTorrent(
QString::fromUtf8(builder_.outputFile),
QFileInfo(QString::fromUtf8(builder_.top)).dir().path());
session_.addNewlyCreatedTorrent(outfile_, QFileInfo(QString::fromStdString(builder_.top())).dir().path());
break;
case QDialogButtonBox::Abort:
builder_.abortFlag = true;
builder_.cancelChecksums();
break;
default: // QDialogButtonBox::Ok:
@@ -85,47 +100,59 @@ void MakeProgressDialog::onButtonBoxClicked(QAbstractButton* button)
void MakeProgressDialog::onProgress()
{
auto const is_done = !future_.valid() || future_.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
if (is_done)
{
timer_.stop();
}
// progress bar
tr_metainfo_builder const& b = builder_;
double const denom = b.pieceCount != 0 ? b.pieceCount : 1;
ui_.progressBar->setValue(static_cast<int>((100.0 * b.pieceIndex) / denom));
auto progress = int{ 100 }; // [0..100]
if (!is_done)
{
auto const [current, total] = builder_.checksumStatus();
progress = static_cast<int>((100.0 * current) / total);
}
ui_.progressBar->setValue(progress);
// progress label
auto const top = QString::fromUtf8(b.top);
auto const top = QString::fromStdString(builder_.top());
auto const base = QFileInfo(top).completeBaseName();
QString str;
if (!b.isDone)
auto success = false;
if (!is_done)
{
str = tr("Creating \"%1\"").arg(base);
}
else if (b.result == TrMakemetaResult::OK)
else
{
str = tr("Created \"%1\"!").arg(base);
}
else if (b.result == TrMakemetaResult::CANCELLED)
{
str = tr("Cancelled");
}
else if (b.result == TrMakemetaResult::ERR_URL)
{
str = tr("Error: invalid announce URL \"%1\"").arg(QString::fromUtf8(b.errfile));
}
else if (b.result == TrMakemetaResult::ERR_IO_READ)
{
str = tr("Error reading \"%1\": %2").arg(QString::fromUtf8(b.errfile)).arg(QString::fromUtf8(tr_strerror(b.my_errno)));
}
else if (b.result == TrMakemetaResult::ERR_IO_WRITE)
{
str = tr("Error writing \"%1\": %2").arg(QString::fromUtf8(b.errfile)).arg(QString::fromUtf8(tr_strerror(b.my_errno)));
tr_error* error = future_.get();
if (error == nullptr)
{
builder_.save(outfile_.toStdString(), &error);
}
if (error == nullptr)
{
str = tr("Created \"%1\"!").arg(base);
success = true;
}
else
{
str = tr("Couldn't create \"%1\": %2 (%3)").arg(base).arg(QString::fromUtf8(error->message)).arg(error->code);
tr_error_free(error);
}
}
ui_.progressLabel->setText(str);
// buttons
ui_.dialogButtons->button(QDialogButtonBox::Abort)->setEnabled(!b.isDone);
ui_.dialogButtons->button(QDialogButtonBox::Ok)->setEnabled(b.isDone);
ui_.dialogButtons->button(QDialogButtonBox::Open)->setEnabled(b.isDone && b.result == TrMakemetaResult::OK);
ui_.dialogButtons->button(QDialogButtonBox::Abort)->setEnabled(!is_done);
ui_.dialogButtons->button(QDialogButtonBox::Ok)->setEnabled(is_done);
ui_.dialogButtons->button(QDialogButtonBox::Open)->setEnabled(is_done && success);
}
#include "MakeDialog.moc"
@@ -136,68 +163,37 @@ void MakeProgressDialog::onProgress()
void MakeDialog::makeTorrent()
{
if (builder_ == nullptr)
if (!builder_)
{
return;
}
// get the tiers
int tier = 0;
std::vector<tr_tracker_info> trackers;
for (QString const& line : ui_.trackersEdit->toPlainText().split(QLatin1Char('\n')))
{
QString const announce_url = line.trimmed();
if (announce_url.isEmpty())
{
++tier;
}
else
{
auto tmp = tr_tracker_info{};
tmp.announce = tr_strdup(announce_url.toUtf8().constData());
tmp.tier = tier;
trackers.push_back(tmp);
}
}
// get the announce list
auto trackers = tr_announce_list();
trackers.parse(ui_.trackersEdit->toPlainText().toStdString());
builder_->setAnnounceList(std::move(trackers));
// the file to create
QString const path = QString::fromUtf8(builder_->top);
auto const path = QString::fromStdString(builder_->top());
auto const torrent_name = QFileInfo(path).completeBaseName() + QStringLiteral(".torrent");
QString const target = QDir(ui_.destinationButton->path()).filePath(torrent_name);
auto const outfile = QDir(ui_.destinationButton->path()).filePath(torrent_name);
// comment
QString comment;
if (ui_.commentCheck->isChecked())
{
comment = ui_.commentEdit->text();
builder_->setComment(ui_.commentEdit->text().toStdString());
}
// source
QString source;
if (ui_.sourceCheck->isChecked())
{
source = ui_.sourceEdit->text();
builder_->setSource(ui_.sourceEdit->text().toStdString());
}
// start making the torrent
tr_makeMetaInfo(
builder_.get(),
target.toUtf8().constData(),
trackers.empty() ? nullptr : trackers.data(),
trackers.size(),
nullptr,
0,
comment.isEmpty() ? nullptr : comment.toUtf8().constData(),
ui_.privateCheck->isChecked(),
false,
source.isNull() ? nullptr : source.toUtf8().constData());
builder_->setPrivate(ui_.privateCheck->isChecked());
// pop up the dialog
auto* dialog = new MakeProgressDialog(session_, *builder_, this);
auto* dialog = new MakeProgressDialog(session_, *builder_, builder_->makeChecksums(), outfile, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->open();
}
@@ -221,24 +217,24 @@ void MakeDialog::onSourceChanged()
if (auto const filename = getSource(); !filename.isEmpty())
{
builder_.reset(tr_metaInfoBuilderCreate(filename.toUtf8().constData()));
builder_.emplace(filename.toStdString());
}
QString text;
if (builder_ == nullptr)
if (!builder_)
{
text = tr("<i>No source selected</i>");
}
else
{
QString const files = tr("%Ln File(s)", nullptr, builder_->fileCount);
QString const pieces = tr("%Ln Piece(s)", nullptr, builder_->pieceCount);
auto const files = tr("%Ln File(s)", nullptr, builder_->fileCount());
auto const pieces = tr("%Ln Piece(s)", nullptr, builder_->pieceCount());
text = tr("%1 in %2; %3 @ %4")
.arg(Formatter::get().sizeToString(builder_->totalSize))
.arg(Formatter::get().sizeToString(builder_->totalSize()))
.arg(files)
.arg(pieces)
.arg(Formatter::get().sizeToString(static_cast<uint64_t>(builder_->pieceSize)));
.arg(Formatter::get().sizeToString(static_cast<uint64_t>(builder_->pieceSize())));
}
ui_.sourceSizeLabel->setText(text);
@@ -247,7 +243,6 @@ void MakeDialog::onSourceChanged()
MakeDialog::MakeDialog(Session& session, QWidget* parent)
: BaseDialog(parent)
, session_(session)
, builder_(nullptr, &tr_metaInfoBuilderFree)
{
ui_.setupUi(this);

View File

@@ -5,22 +5,17 @@
#pragma once
#include <memory>
#include <optional>
#include <libtransmission/tr-macros.h>
#include <libtransmission/makemeta.h>
#include "BaseDialog.h"
#include "ui_MakeDialog.h"
class QAbstractButton;
class Session;
extern "C"
{
struct tr_metainfo_builder;
}
class MakeDialog : public BaseDialog
{
Q_OBJECT
@@ -45,5 +40,5 @@ private:
Ui::MakeDialog ui_ = {};
std::unique_ptr<tr_metainfo_builder, void (*)(tr_metainfo_builder*)> builder_;
std::optional<tr_metainfo_builder> builder_;
};

View File

@@ -58,7 +58,7 @@ TEST(Crypto, DH)
b.setPeerPublicKey(a.publicKey());
EXPECT_EQ(toString(a.secret()), toString(b.secret()));
EXPECT_EQ(a.secret(), b.secret());
EXPECT_EQ(96, std::size(a.secret()));
EXPECT_EQ(96U, std::size(a.secret()));
auto c = tr_message_stream_encryption::DH{};
c.setPeerPublicKey(b.publicKey());

View File

@@ -3,11 +3,15 @@
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <algorithm>
#include <array>
#include <cstdlib> // mktemp()
#include <cstring> // strlen()
#include <numeric>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <fmt/format.h>
@@ -16,6 +20,7 @@
#include "crypto-utils.h"
#include "file.h"
#include "makemeta.h"
#include "session.h" // TR_NAME
#include "torrent-metainfo.h"
#include "utils.h"
@@ -32,351 +37,188 @@ namespace test
class MakemetaTest : public SandboxedTest
{
protected:
void testSingleFileImpl(
tr_torrent_metainfo& metainfo,
tr_tracker_info const* trackers,
int const trackerCount,
char const** webseeds,
int const webseedCount,
void const* payload,
size_t const payloadSize,
char const* comment,
bool isPrivate,
bool anonymize,
std::string_view source)
static auto constexpr DefaultMaxFileCount = size_t{ 16 };
static auto constexpr DefaultMaxFileSize = size_t{ 1024 };
auto makeRandomFiles(
std::string_view top,
size_t n_files = std::max(size_t{ 1U }, static_cast<size_t>(tr_rand_int_weak(DefaultMaxFileCount))),
size_t max_size = DefaultMaxFileSize)
{
auto files = std::vector<std::pair<std::string, std::vector<std::byte>>>{};
// create a single input file
auto input_file = tr_pathbuf{ sandboxDir(), '/', "test.XXXXXX" };
createTmpfileWithContents(std::data(input_file), payload, payloadSize);
tr_sys_path_native_separators(std::data(input_file));
auto* builder = tr_metaInfoBuilderCreate(input_file);
EXPECT_EQ(tr_file_index_t{ 1 }, builder->fileCount);
EXPECT_EQ(input_file, builder->top);
EXPECT_EQ(input_file, builder->files[0].filename);
EXPECT_EQ(payloadSize, builder->files[0].size);
EXPECT_EQ(payloadSize, builder->totalSize);
EXPECT_FALSE(builder->isFolder);
EXPECT_FALSE(builder->abortFlag);
// have tr_makeMetaInfo() build the torrent file
auto const torrent_file = tr_pathbuf{ input_file, ".torrent"sv };
tr_makeMetaInfo(
builder,
torrent_file,
trackers,
trackerCount,
webseeds,
webseedCount,
comment,
isPrivate,
anonymize,
std::string(source).c_str());
EXPECT_EQ(isPrivate, builder->isPrivate);
EXPECT_EQ(anonymize, builder->anonymize);
EXPECT_EQ(torrent_file, builder->outputFile);
EXPECT_STREQ(comment, builder->comment);
EXPECT_EQ(source, builder->source);
EXPECT_EQ(trackerCount, builder->trackerCount);
while (!builder->isDone)
for (size_t i = 0; i < n_files; ++i)
{
tr_wait_msec(100);
auto payload = std::vector<std::byte>{};
// TODO(5.0.0): zero-sized files are disabled in these test
// because tr_torrent_metainfo discards them, throwing off the
// builder-to-metainfo comparisons here. tr_torrent_metainfo
// will behave when BEP52 support is added in Transmission 5.
static auto constexpr MinFileSize = size_t{ 1U };
payload.resize(std::max(MinFileSize, static_cast<size_t>(tr_rand_int_weak(max_size))));
tr_rand_buffer(std::data(payload), std::size(payload));
auto filename = tr_pathbuf{ top, '/', "test.XXXXXX" };
createTmpfileWithContents(std::data(filename), std::data(payload), std::size(payload));
tr_sys_path_native_separators(std::data(filename));
files.emplace_back(std::string{ filename.sv() }, payload);
}
sync();
// now let's check our work: parse the torrent file
EXPECT_TRUE(metainfo.parseTorrentFile(torrent_file));
// quick check of some of the parsed metainfo
EXPECT_EQ(payloadSize, metainfo.totalSize());
EXPECT_EQ(tr_sys_path_basename(input_file), metainfo.name());
EXPECT_EQ(comment, metainfo.comment());
EXPECT_EQ(isPrivate, metainfo.isPrivate());
EXPECT_EQ(size_t(trackerCount), std::size(metainfo.announceList()));
EXPECT_EQ(size_t(webseedCount), metainfo.webseedCount());
EXPECT_EQ(tr_file_index_t{ 1 }, metainfo.fileCount());
EXPECT_EQ(tr_sys_path_basename(input_file), metainfo.fileSubpath(0));
EXPECT_EQ(payloadSize, metainfo.fileSize(0));
// cleanup
tr_metaInfoBuilderFree(builder);
return files;
}
void testSingleDirectoryImpl(
tr_tracker_info const* trackers,
int const tracker_count,
char const** webseeds,
int const webseed_count,
void const** payloads,
size_t const* payload_sizes,
size_t const payload_count,
char const* comment,
bool const is_private,
bool const anonymize,
char const* source)
static auto testBuilder(tr_metainfo_builder& builder)
{
// create the top temp directory
auto top = tr_pathbuf{ sandboxDir(), '/', "folder.XXXXXX"sv };
tr_sys_path_native_separators(std::data(top));
tr_sys_dir_create_temp(std::data(top));
tr_error* error = builder.makeChecksums().get();
EXPECT_EQ(error, nullptr) << *error;
// build the payload files that go into the top temp directory
auto files = std::vector<std::string>{};
files.reserve(payload_count);
size_t total_size = 0;
for (size_t i = 0; i < payload_count; i++)
{
auto path = fmt::format(FMT_STRING("{:s}/file.{:04}XXXXXX"), top.sv(), i);
createTmpfileWithContents(std::data(path), payloads[i], payload_sizes[i]);
tr_sys_path_native_separators(std::data(path));
files.push_back(path);
total_size += payload_sizes[i];
}
sync();
// init the builder
auto* builder = tr_metaInfoBuilderCreate(top);
EXPECT_FALSE(builder->abortFlag);
EXPECT_EQ(top, builder->top);
EXPECT_EQ(payload_count, builder->fileCount);
EXPECT_EQ(total_size, builder->totalSize);
EXPECT_TRUE(builder->isFolder);
for (size_t i = 0; i < builder->fileCount; ++i)
{
EXPECT_EQ(files[i], builder->files[i].filename);
EXPECT_EQ(payload_sizes[i], builder->files[i].size);
}
// build the torrent file
auto const torrent_file = tr_pathbuf{ top, ".torrent"sv };
tr_makeMetaInfo(
builder,
torrent_file.c_str(),
trackers,
tracker_count,
webseeds,
webseed_count,
comment,
is_private,
anonymize,
source);
EXPECT_EQ(is_private, builder->isPrivate);
EXPECT_EQ(anonymize, builder->anonymize);
EXPECT_EQ(torrent_file, builder->outputFile);
EXPECT_STREQ(comment, builder->comment);
EXPECT_STREQ(source, builder->source);
EXPECT_EQ(tracker_count, builder->trackerCount);
auto test = [&builder]()
{
return builder->isDone;
};
EXPECT_TRUE(waitFor(test, 5000));
sync();
// now let's check our work: parse the torrent file
auto metainfo = tr_torrent_metainfo{};
EXPECT_TRUE(metainfo.parseTorrentFile(torrent_file));
// quick check of some of the parsed metainfo
EXPECT_EQ(total_size, metainfo.totalSize());
EXPECT_EQ(tr_sys_path_basename(top), metainfo.name());
EXPECT_EQ(comment, metainfo.comment());
EXPECT_EQ(source, metainfo.source());
EXPECT_EQ(payload_count, metainfo.fileCount());
EXPECT_EQ(is_private, metainfo.isPrivate());
EXPECT_EQ(size_t(tracker_count), std::size(metainfo.announceList()));
// cleanup
tr_metaInfoBuilderFree(builder);
}
void testSingleDirectoryRandomPayloadImpl(
tr_tracker_info const* trackers,
int const tracker_count,
char const** webseeds,
int const webseed_count,
size_t const max_file_count,
size_t const max_file_size,
char const* comment,
bool const is_private,
bool const anonymize,
char const* source)
{
// build random payloads
size_t const payload_count = 1 + tr_rand_int_weak(max_file_count);
auto** payloads = tr_new0(void*, payload_count);
auto* payload_sizes = tr_new0(size_t, payload_count);
for (size_t i = 0; i < payload_count; i++)
EXPECT_TRUE(metainfo.parseBenc(builder.benc()));
EXPECT_EQ(builder.fileCount(), metainfo.fileCount());
EXPECT_EQ(builder.pieceSize(), metainfo.pieceSize());
EXPECT_EQ(builder.totalSize(), metainfo.totalSize());
EXPECT_EQ(builder.totalSize(), metainfo.totalSize());
for (size_t i = 0, n = std::min(builder.fileCount(), metainfo.fileCount()); i < n; ++i)
{
size_t const n = 1 + tr_rand_int_weak(max_file_size);
payloads[i] = tr_new(char, n);
tr_rand_buffer(payloads[i], n);
payload_sizes[i] = n;
EXPECT_EQ(builder.fileSize(i), metainfo.files().fileSize(i));
EXPECT_EQ(builder.path(i), metainfo.files().path(i));
}
// run the test
testSingleDirectoryImpl(
trackers,
tracker_count,
webseeds,
webseed_count,
const_cast<void const**>(payloads),
payload_sizes,
payload_count,
comment,
is_private,
anonymize,
source);
// cleanup
for (size_t i = 0; i < payload_count; i++)
{
tr_free(payloads[i]);
}
tr_free(payloads);
tr_free(payload_sizes);
EXPECT_EQ(builder.name(), metainfo.name());
EXPECT_EQ(builder.comment(), metainfo.comment());
EXPECT_EQ(builder.isPrivate(), metainfo.isPrivate());
EXPECT_EQ(builder.announceList().toString(), metainfo.announceList().toString());
return metainfo;
}
};
TEST_F(MakemetaTest, comment)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
static auto constexpr Comment = "This is the comment"sv;
builder.setComment(Comment);
EXPECT_EQ(Comment, testBuilder(builder).comment());
}
TEST_F(MakemetaTest, source)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
static auto constexpr Source = "This is the source"sv;
builder.setSource(Source);
EXPECT_EQ(Source, testBuilder(builder).source());
}
TEST_F(MakemetaTest, isPrivate)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
for (bool const is_private : { true, false })
{
auto builder = tr_metainfo_builder{ filename };
builder.setPrivate(is_private);
EXPECT_EQ(is_private, testBuilder(builder).isPrivate());
}
}
TEST_F(MakemetaTest, pieceSize)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
for (uint32_t const piece_size : { 16384, 32768 })
{
auto builder = tr_metainfo_builder{ filename };
builder.setPieceSize(piece_size);
EXPECT_EQ(piece_size, testBuilder(builder).pieceSize());
}
}
TEST_F(MakemetaTest, webseeds)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
static auto constexpr Webseed = "https://www.example.com/linux.iso"sv;
builder.setWebseeds(std::vector<std::string>{ std::string{ Webseed } });
auto const metainfo = testBuilder(builder);
EXPECT_EQ(1U, metainfo.webseedCount());
EXPECT_EQ(Webseed, metainfo.webseed(0));
}
TEST_F(MakemetaTest, nameIsRootSingleFile)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
EXPECT_EQ(tr_sys_path_basename(filename), testBuilder(builder).name());
}
TEST_F(MakemetaTest, anonymizeTrue)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
builder.setAnonymize(true);
auto const metainfo = testBuilder(builder);
EXPECT_EQ(""sv, metainfo.creator());
EXPECT_EQ(time_t{}, metainfo.dateCreated());
}
TEST_F(MakemetaTest, anonymizeFalse)
{
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
builder.setAnonymize(false);
auto const metainfo = testBuilder(builder);
EXPECT_TRUE(tr_strvContains(metainfo.creator(), TR_NAME)) << metainfo.creator();
auto const now = time(nullptr);
EXPECT_LE(metainfo.dateCreated(), now);
EXPECT_LE(now - 60, metainfo.dateCreated());
}
TEST_F(MakemetaTest, nameIsRootMultifile)
{
auto const files = makeRandomFiles(sandboxDir(), 10);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
EXPECT_EQ(tr_sys_path_basename(filename), testBuilder(builder).name());
}
TEST_F(MakemetaTest, singleFile)
{
auto trackers = std::array<tr_tracker_info, 16>{};
auto tracker_count = int{};
trackers[tracker_count].tier = tracker_count;
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.openbittorrent.com:80");
++tracker_count;
trackers[tracker_count].tier = tracker_count;
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.publicbt.com:80");
++tracker_count;
auto const payload = std::string{ "Hello, World!\n" };
char const* const comment = "This is the comment";
bool const is_private = false;
bool const anonymize = false;
auto metainfo = tr_torrent_metainfo{};
testSingleFileImpl(
metainfo,
trackers.data(),
tracker_count,
nullptr,
0,
payload.data(),
payload.size(),
comment,
is_private,
anonymize,
"TESTME"sv);
}
auto const files = makeRandomFiles(sandboxDir(), 1);
auto const [filename, payload] = files.front();
auto builder = tr_metainfo_builder{ filename };
TEST_F(MakemetaTest, webseed)
{
auto trackers = std::vector<tr_tracker_info>{};
auto webseeds = std::vector<char const*>{};
auto trackers = tr_announce_list{};
trackers.add("udp://tracker.openbittorrent.com:80"sv, trackers.nextTier());
trackers.add("udp://tracker.publicbt.com:80"sv, trackers.nextTier());
builder.setAnnounceList(std::move(trackers));
webseeds.emplace_back("https://www.example.com/linux.iso");
static auto constexpr Comment = "This is the comment"sv;
builder.setComment(Comment);
auto const payload = std::string{ "Hello, World!\n" };
char const* const comment = "This is the comment";
bool const is_private = false;
bool const anonymize = false;
auto metainfo = tr_torrent_metainfo{};
testSingleFileImpl(
metainfo,
std::data(trackers),
std::size(trackers),
std::data(webseeds),
std::size(webseeds),
payload.data(),
payload.size(),
comment,
is_private,
anonymize,
"TESTME"sv);
}
static auto constexpr IsPrivate = false;
builder.setPrivate(IsPrivate);
TEST_F(MakemetaTest, singleFileDifferentSourceFlags)
{
auto trackers = std::array<tr_tracker_info, 16>{};
auto tracker_count = int{};
trackers[tracker_count].tier = tracker_count;
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.openbittorrent.com:80");
++tracker_count;
trackers[tracker_count].tier = tracker_count;
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.publicbt.com:80");
++tracker_count;
auto const payload = std::string{ "Hello, World!\n" };
char const* const comment = "This is the comment";
bool const is_private = false;
bool const anonymize = false;
static auto constexpr Anonymize = false;
builder.setAnonymize(Anonymize);
auto metainfo_foobar = tr_torrent_metainfo{};
testSingleFileImpl(
metainfo_foobar,
trackers.data(),
tracker_count,
nullptr,
0,
payload.data(),
payload.size(),
comment,
is_private,
anonymize,
"FOOBAR"sv);
auto metainfo_testme = tr_torrent_metainfo{};
testSingleFileImpl(
metainfo_testme,
trackers.data(),
tracker_count,
nullptr,
0,
payload.data(),
payload.size(),
comment,
is_private,
anonymize,
"TESTME"sv);
EXPECT_NE(metainfo_foobar.infoHash(), metainfo_testme.infoHash());
}
TEST_F(MakemetaTest, singleDirectoryRandomPayload)
{
auto constexpr DefaultMaxFileCount = size_t{ 16 };
auto constexpr DefaultMaxFileSize = size_t{ 1024 };
auto trackers = std::array<tr_tracker_info, 16>{};
auto tracker_count = int{};
trackers[tracker_count].tier = tracker_count;
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.openbittorrent.com:80");
++tracker_count;
trackers[tracker_count].tier = tracker_count;
trackers[tracker_count].announce = const_cast<char*>("udp://tracker.publicbt.com:80");
++tracker_count;
char const* const comment = "This is the comment";
bool const is_private = false;
bool const anonymize = false;
char const* const source = "TESTME";
for (size_t i = 0; i < 10; ++i)
{
testSingleDirectoryRandomPayloadImpl(
trackers.data(),
tracker_count,
nullptr,
0,
DefaultMaxFileCount,
DefaultMaxFileSize,
comment,
is_private,
anonymize,
source);
}
testBuilder(builder);
}
} // namespace test

View File

@@ -198,7 +198,7 @@ protected:
errno = tmperr;
}
static void blockingFileWrite(tr_sys_file_t fd, void const* data, size_t data_len)
static void blockingFileWrite(tr_sys_file_t fd, void const* data, size_t data_len, tr_error** error = nullptr)
{
uint64_t n_left = data_len;
auto const* left = static_cast<uint8_t const*>(data);
@@ -206,11 +206,12 @@ protected:
while (n_left > 0)
{
uint64_t n = {};
tr_error* error = nullptr;
if (!tr_sys_file_write(fd, left, n_left, &n, &error))
tr_error* local_error = nullptr;
if (!tr_sys_file_write(fd, left, n_left, &n, error))
{
fprintf(stderr, "Error writing file: '%s'\n", error->message);
tr_error_free(error);
fprintf(stderr, "Error writing file: '%s'\n", local_error->message);
tr_error_propagate(error, &local_error);
tr_error_free(local_error);
break;
}
@@ -225,10 +226,21 @@ protected:
buildParentDir(tmpl);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.InnerPointer)
auto const fd = tr_sys_file_open_temp(tmpl);
blockingFileWrite(fd, payload, n);
tr_sys_file_close(fd);
tr_error* error = nullptr;
auto const fd = tr_sys_file_open_temp(tmpl, &error);
blockingFileWrite(fd, payload, n, &error);
tr_sys_file_flush(fd, &error);
tr_sys_file_flush(fd, &error);
tr_sys_file_close(fd, &error);
if (error != nullptr)
{
fmt::print(
"Couldn't create '{path}': {error} ({error_code})\n",
fmt::arg("path", tmpl),
fmt::arg("error", error->message),
fmt::arg("error_code", error->code));
tr_error_free(error);
}
sync();
errno = tmperr;

View File

@@ -4,14 +4,14 @@
// License text can be found in the licenses/ folder.
#include <array>
#include <cinttypes> // PRIu32
#include <chrono>
#include <cstdint> // uint32_t
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <future>
#include <string>
#include <string_view>
#include <fmt/format.h>
#include <libtransmission/transmission.h>
#include <libtransmission/error.h>
@@ -19,7 +19,6 @@
#include <libtransmission/log.h>
#include <libtransmission/makemeta.h>
#include <libtransmission/tr-getopt.h>
#include <libtransmission/tr-strbuf.h>
#include <libtransmission/utils.h>
#include <libtransmission/version.h>
@@ -50,16 +49,16 @@ auto constexpr Options = std::array<tr_option, 10>{
struct app_options
{
std::vector<tr_tracker_info> trackers;
std::vector<char const*> webseeds;
tr_announce_list trackers;
std::vector<std::string> webseeds;
std::string outfile;
char const* comment = nullptr;
char const* infile = nullptr;
char const* source = nullptr;
uint32_t piecesize_kib = 0;
std::string_view comment;
std::string_view infile;
std::string_view source;
uint32_t piece_size = 0;
bool anonymize = false;
bool is_private = false;
bool show_version = false;
bool anonymize = false;
};
int parseCommandLine(app_options& options, int argc, char const* const* argv)
@@ -88,25 +87,23 @@ int parseCommandLine(app_options& options, int argc, char const* const* argv)
break;
case 't':
options.trackers.push_back(tr_tracker_info{ 0, const_cast<char*>(optarg) });
options.trackers.add(optarg, options.trackers.nextTier());
break;
case 'w':
options.webseeds.push_back(optarg);
options.webseeds.emplace_back(optarg);
break;
case 's':
if (optarg != nullptr)
{
char* endptr = nullptr;
options.piecesize_kib = strtoul(optarg, &endptr, 10);
options.piece_size = strtoul(optarg, &endptr, 10) * KiB;
if (endptr != nullptr && *endptr == 'M')
{
options.piecesize_kib *= KiB;
options.piece_size *= KiB;
}
}
break;
case 'r':
@@ -129,29 +126,24 @@ int parseCommandLine(app_options& options, int argc, char const* const* argv)
return 0;
}
char* tr_getcwd(void)
std::string tr_getcwd()
{
char* result;
tr_error* error = nullptr;
result = tr_sys_dir_get_current(&error);
if (result == nullptr)
if (char* const cur = tr_sys_dir_get_current(&error); cur != nullptr)
{
fprintf(stderr, "getcwd error: \"%s\"", error->message);
tr_error_free(error);
result = tr_strdup("");
auto path = std::string{ cur };
tr_free(cur);
return path;
}
return result;
fprintf(stderr, "getcwd error: \"%s\"", error->message);
tr_error_free(error);
return "";
}
} // namespace
int tr_main(int argc, char* argv[])
{
tr_metainfo_builder* b = nullptr;
tr_logSetLevel(TR_LOG_ERROR);
tr_formatter_mem_init(MemK, MemKStr, MemMStr, MemGStr, MemTStr);
tr_formatter_size_init(DiskK, DiskKStr, DiskMStr, DiskGStr, DiskTStr);
@@ -169,7 +161,7 @@ int tr_main(int argc, char* argv[])
return EXIT_SUCCESS;
}
if (options.infile == nullptr)
if (std::empty(options.infile))
{
fprintf(stderr, "ERROR: No input file or directory specified.\n");
tr_getopt_usage(MyName, Usage, std::data(Options));
@@ -188,9 +180,7 @@ int tr_main(int argc, char* argv[])
return EXIT_FAILURE;
}
char* const cwd = tr_getcwd();
options.outfile = tr_strvDup(tr_pathbuf{ std::string_view{ cwd }, '/', base, ".torrent"sv });
tr_free(cwd);
options.outfile = fmt::format("{:s}/{:s}.torrent"sv, tr_getcwd(), base);
}
if (std::empty(options.trackers))
@@ -206,92 +196,88 @@ int tr_main(int argc, char* argv[])
}
}
printf("Creating torrent \"%s\"\n", options.outfile.c_str());
fmt::print("Creating torrent \"{:s}\"\n", options.outfile);
b = tr_metaInfoBuilderCreate(options.infile);
if (b == nullptr)
auto builder = tr_metainfo_builder(options.infile);
auto const n_files = builder.fileCount();
if (n_files == 0U)
{
fprintf(stderr, "ERROR: Cannot find specified input file or directory.\n");
return EXIT_FAILURE;
}
for (uint32_t i = 0; i < b->fileCount; ++i)
for (tr_file_index_t i = 0; i < n_files; ++i)
{
if (auto const& file = b->files[i]; !file.is_portable)
auto const& path = builder.path(i);
if (!tr_torrent_files::isSubpathPortable(path))
{
fprintf(stderr, "WARNING: consider renaming nonportable filename \"%s\".\n", file.filename);
fmt::print(stderr, "WARNING\n");
fmt::print(stderr, "filename \"{:s}\" may not be portable on all systems.\n", path);
fmt::print(stderr, "consider \"{:s}\" instead.\n", tr_torrent_files::makeSubpathPortable(path));
}
}
if (options.piecesize_kib != 0)
if (options.piece_size != 0 && !builder.setPieceSize(options.piece_size))
{
tr_metaInfoBuilderSetPieceSize(b, options.piecesize_kib * KiB);
fmt::print(stderr, "ERROR: piece size must be at least 16 KiB and must be a power of two.\n");
return EXIT_FAILURE;
}
printf(
b->fileCount > 1 ? " %" PRIu32 " files, %s\n" : " %" PRIu32 " file, %s\n",
b->fileCount,
tr_formatter_size_B(b->totalSize).c_str());
printf(
b->pieceCount > 1 ? " %" PRIu32 " pieces, %s each\n" : " %" PRIu32 " piece, %s\n",
b->pieceCount,
tr_formatter_size_B(b->pieceSize).c_str());
fmt::print(
ngettext("{:d} files, {:s}\n", "{:d} file, {:s}\n", builder.fileCount()),
builder.fileCount(),
tr_formatter_size_B(builder.totalSize()));
tr_makeMetaInfo(
b,
options.outfile.c_str(),
std::data(options.trackers),
static_cast<int>(std::size(options.trackers)),
std::data(options.webseeds),
static_cast<int>(std::size(options.webseeds)),
options.comment,
options.is_private,
options.anonymize,
options.source);
fmt::print(
ngettext("{:d} pieces, {:s} each\n", "{:d} piece, {:s}\n", builder.pieceCount()),
builder.pieceCount(),
tr_formatter_size_B(builder.pieceSize()));
uint32_t last = UINT32_MAX;
while (!b->isDone)
if (!std::empty(options.comment))
{
tr_wait_msec(500);
builder.setComment(options.comment);
}
uint32_t current = b->pieceIndex;
if (current != last)
if (!std::empty(options.source))
{
builder.setSource(options.source);
}
builder.setPrivate(options.is_private);
builder.setAnonymize(options.anonymize);
builder.setWebseeds(std::move(options.webseeds));
builder.setAnnounceList(std::move(options.trackers));
auto future = builder.makeChecksums();
auto last = std::optional<tr_piece_index_t>{};
while (future.wait_for(std::chrono::milliseconds(500)) != std::future_status::ready)
{
auto const [current, total] = builder.checksumStatus();
if (!last || current != *last)
{
printf("\rPiece %" PRIu32 "/%" PRIu32 " ...", current, b->pieceCount);
fmt::print("\rPiece {:d}/{:d} ...", current, total);
fflush(stdout);
last = current;
}
}
putc(' ', stdout);
fmt::print(" ");
switch (b->result)
if (tr_error* error = future.get(); error != nullptr)
{
case TrMakemetaResult::OK:
printf("done!");
break;
case TrMakemetaResult::ERR_URL:
printf("bad announce URL: \"%s\"", b->errfile);
break;
case TrMakemetaResult::ERR_IO_READ:
printf("error reading \"%s\": %s", b->errfile, tr_strerror(b->my_errno));
break;
case TrMakemetaResult::ERR_IO_WRITE:
printf("error writing \"%s\": %s", b->errfile, tr_strerror(b->my_errno));
break;
case TrMakemetaResult::CANCELLED:
printf("cancelled");
break;
fmt::print("ERROR: {:s} {:d}\n", error->message, error->code);
tr_error_free(error);
return EXIT_FAILURE;
}
putc('\n', stdout);
if (tr_error* error = nullptr; !builder.save(options.outfile, &error))
{
fmt::print("ERROR: could not save \"{:s}\": {:s} {:d}\n", options.outfile, error->message, error->code);
tr_error_free(error);
return EXIT_FAILURE;
}
tr_metaInfoBuilderFree(b);
fmt::print("done!\n");
return EXIT_SUCCESS;
}