diff --git a/docs/rpc-spec.md b/docs/rpc-spec.md index 99d63c7d3..7abc1d53a 100644 --- a/docs/rpc-spec.md +++ b/docs/rpc-spec.md @@ -336,7 +336,7 @@ Files are returned in the order they are laid out in the torrent. References to | Key | Value Type | transmission.h source |:--|:--|:-- | `bytes_completed` | number | tr_file_view -| `wanted` | boolean | tr_file_view (**Note:** Not to be confused with `torrent_get.wanted`, which is an array of 0/1 instead of boolean) +| `wanted` | boolean | tr_file_view | `priority` | number | tr_file_view `peers`: an array of objects, each containing: @@ -437,10 +437,9 @@ Files are returned in the order they are laid out in the torrent. References to | `tier` | number | tr_tracker_view -`wanted`: An array of `tr_torrentFileCount()` 0/1, 1 (true) if the corresponding file is to be downloaded. (Source: `tr_file_view`) +`wanted`: An array of `tr_torrentFileCount()` booleans, true if the corresponding file is to be downloaded. (Source: `tr_file_view`) -**Note:** For backwards compatibility, in `4.x.x`, `wanted` is serialized as an array of `0` or `1` that should be treated as booleans. -This will be fixed in `5.0.0` to return an array of booleans. +**Note:** For backwards compatibility, in the old bespoke API, `wanted` is serialized as an array of `0` or `1` that should be treated as booleans. Example: diff --git a/libtransmission/api-compat.cc b/libtransmission/api-compat.cc index 915211dce..8e70324c6 100644 --- a/libtransmission/api-compat.cc +++ b/libtransmission/api-compat.cc @@ -670,9 +670,96 @@ void convert_keys(tr_variant& var, State& state) }); } +namespace convert_jsonrpc_helpers +{ +void convert_files_wanted(tr_variant::Vector& wanted, State const& state) +{ + auto ret = tr_variant::Vector{}; + ret.reserve(std::size(wanted)); + for (auto const& var : wanted) + { + if (state.style == Style::Tr5) + { + if (auto const val = var.value_if()) + { + ret.emplace_back(*val); + } + else + { + return; + } + } + else + { + if (auto const val = var.value_if(); val == 0 || val == 1) + { + ret.emplace_back(*val); + } + else + { + return; + } + } + } + + wanted = std::move(ret); +} + +void convert_files_wanted_response(tr_variant::Map& top, State const& state) +{ + if (auto* const args = top.find_if(state.style == Style::Tr5 ? TR_KEY_result : TR_KEY_arguments)) + { + if (auto* const torrents = args->find_if(TR_KEY_torrents); + torrents != nullptr && !std::empty(*torrents)) + { + // TrFormat::Table + if (auto* const first_vec = torrents->front().get_if(); + first_vec != nullptr && !std::empty(*first_vec)) + { + if (auto const wanted_iter = std::find_if( + std::begin(*first_vec), + std::end(*first_vec), + [](tr_variant const& v) + { return v.value_if() == tr_quark_get_string_view(TR_KEY_wanted); }); + wanted_iter != std::end(*first_vec)) + { + auto const wanted_idx = static_cast(wanted_iter - std::begin(*first_vec)); + for (auto it = std::next(std::begin(*torrents)); it != std::end(*torrents); ++it) + { + if (auto* const row = it->get_if(); row != nullptr && wanted_idx < std::size(*row)) + { + if (auto* const wanted = (*row)[wanted_idx].get_if()) + { + convert_files_wanted(*wanted, state); + } + } + } + } + } + // TrFormat::Object + else if (torrents->front().index() == tr_variant::MapIndex) + { + for (auto& var : *torrents) + { + if (auto* const map = var.get_if()) + { + if (auto* const wanted = map->find_if(TR_KEY_wanted)) + { + convert_files_wanted(*wanted, state); + } + } + } + } + } + } +} +} // namespace convert_jsonrpc_helpers + // jsonrpc <-> legacy rpc conversion void convert_jsonrpc(tr_variant::Map& top, State const& state) { + using namespace convert_jsonrpc_helpers; + if (!state.is_rpc) { return; @@ -705,6 +792,8 @@ void convert_jsonrpc(tr_variant::Map& top, State const& state) // - add `result: "success"` top.replace_key(TR_KEY_result, TR_KEY_arguments); top.try_emplace(TR_KEY_result, tr_variant::unmanaged_string("success")); + + convert_files_wanted_response(top, state); } if (state.is_response && is_legacy && !state.is_success) @@ -751,6 +840,8 @@ void convert_jsonrpc(tr_variant::Map& top, State const& state) { top.erase(TR_KEY_result); top.replace_key(TR_KEY_arguments, TR_KEY_result); + + convert_files_wanted_response(top, state); } if (state.is_response && is_jsonrpc && !state.is_success && state.was_legacy) diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index a1d57e589..fd3c4bcca 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -458,7 +458,7 @@ namespace make_torrent_field_helpers vec.reserve(n_files); for (tr_file_index_t idx = 0U; idx != n_files; ++idx) { - vec.emplace_back(tr_torrentFile(&tor, idx).wanted ? 1 : 0); + vec.emplace_back(tr_torrentFile(&tor, idx).wanted); } return tr_variant{ std::move(vec) }; } diff --git a/tests/libtransmission/api-compat-test.cc b/tests/libtransmission/api-compat-test.cc index aedb34ede..debfab5bf 100644 --- a/tests/libtransmission/api-compat-test.cc +++ b/tests/libtransmission/api-compat-test.cc @@ -300,6 +300,128 @@ constexpr std::string_view CurrentTorrentGetJson = R"json({ } })json"; +constexpr std::string_view CurrentFilesWantedResponseObjectJson = R"json({ + "id": 6, + "jsonrpc": "2.0", + "result": { + "torrents": [ + { + "wanted": [ + false, + true, + true, + false + ] + }, + { + "wanted": [ + true, + false, + true, + false, + true + ] + } + ] + } +})json"; + +constexpr std::string_view LegacyFilesWantedResponseObjectJson = R"json({ + "arguments": { + "torrents": [ + { + "wanted": [ + 0, + 1, + 1, + 0 + ] + }, + { + "wanted": [ + 1, + 0, + 1, + 0, + 1 + ] + } + ] + }, + "result": "success", + "tag": 6 +})json"; + +constexpr std::string_view CurrentFilesWantedResponseArrayJson = R"json({ + "id": 6, + "jsonrpc": "2.0", + "result": { + "torrents": [ + [ + "comment", + "wanted", + "id" + ], + [ + "id 1", + [ + false, + true, + true, + false + ], + 1 + ], + [ + "id 2", + [ + true, + false, + true, + false, + true + ], + 2 + ] + ] + } +})json"; + +constexpr std::string_view LegacyFilesWantedResponseArrayJson = R"json({ + "arguments": { + "torrents": [ + [ + "comment", + "wanted", + "id" + ], + [ + "id 1", + [ + 0, + 1, + 1, + 0 + ], + 1 + ], + [ + "id 2", + [ + 1, + 0, + 1, + 0, + 1 + ], + 2 + ] + ] + }, + "result": "success", + "tag": 6 +})json"; + constexpr std::string_view CurrentPortTestErrorResponse = R"json({ "error": { "code": 8, @@ -963,7 +1085,7 @@ TEST_F(ApiCompatTest, canConvertRpc) using TestCase = std::tuple; // clang-format off - static auto constexpr TestCases = std::array{ { + static auto constexpr TestCases = std::array{ { { "free_space tr5 -> tr5", BadFreeSpaceRequest, Style::Tr5, BadFreeSpaceRequest }, { "free_space tr5 -> tr4", BadFreeSpaceRequest, Style::Tr4, BadFreeSpaceRequestLegacy }, { "free_space tr4 -> tr5", BadFreeSpaceRequestLegacy, Style::Tr5, BadFreeSpaceRequest }, @@ -1006,6 +1128,14 @@ TEST_F(ApiCompatTest, canConvertRpc) { "unrecognised info tr4 -> tr4", UnrecognisedInfoLegacyResponse, Style::Tr4, UnrecognisedInfoLegacyResponse}, { "non-int tag tr4 -> tr5", LegacyNonIntTagRequest, Style::Tr5, LegacyNonIntTagRequestResult }, { "non-int tag tr4 -> tr4", LegacyNonIntTagRequest, Style::Tr4, LegacyNonIntTagRequest }, + { "files wanted response object tr5 -> tr5", CurrentFilesWantedResponseObjectJson, Style::Tr5, CurrentFilesWantedResponseObjectJson }, + { "files wanted response object tr5 -> tr4", CurrentFilesWantedResponseObjectJson, Style::Tr4, LegacyFilesWantedResponseObjectJson }, + { "files wanted response object tr4 -> tr5", LegacyFilesWantedResponseObjectJson, Style::Tr5, CurrentFilesWantedResponseObjectJson }, + { "files wanted response object tr5 -> tr4", LegacyFilesWantedResponseObjectJson, Style::Tr4, LegacyFilesWantedResponseObjectJson }, + { "files wanted response array tr5 -> tr5", CurrentFilesWantedResponseArrayJson, Style::Tr5, CurrentFilesWantedResponseArrayJson }, + { "files wanted response array tr5 -> tr4", CurrentFilesWantedResponseArrayJson, Style::Tr4, LegacyFilesWantedResponseArrayJson }, + { "files wanted response array tr4 -> tr5", LegacyFilesWantedResponseArrayJson, Style::Tr5, CurrentFilesWantedResponseArrayJson }, + { "files wanted response array tr5 -> tr4", LegacyFilesWantedResponseArrayJson, Style::Tr4, LegacyFilesWantedResponseArrayJson }, // TODO(ckerr): torrent-get with 'table' } };