diff --git a/libtransmission/api-compat.cc b/libtransmission/api-compat.cc index acc43d3cf..f207e2b6c 100644 --- a/libtransmission/api-compat.cc +++ b/libtransmission/api-compat.cc @@ -246,8 +246,8 @@ auto constexpr RpcKeys = std::array{ { } }; auto constexpr SessionKeys = std::array{ { - { TR_KEY_activity_date, TR_KEY_activity_date_kebab }, // TODO(ckerr) legacy duplicate - { TR_KEY_added_date, TR_KEY_added_date_kebab }, // TODO(ckerr) legacy duplicate + { TR_KEY_activity_date, TR_KEY_activity_date_kebab_APICOMPAT }, + { TR_KEY_added_date, TR_KEY_added_date_kebab_APICOMPAT }, { TR_KEY_alt_speed_down, TR_KEY_alt_speed_down_kebab }, { TR_KEY_alt_speed_enabled, TR_KEY_alt_speed_enabled_kebab }, { TR_KEY_alt_speed_time_begin, TR_KEY_alt_speed_time_begin_kebab }, @@ -259,7 +259,7 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_announce_ip_enabled, TR_KEY_announce_ip_enabled_kebab }, { TR_KEY_anti_brute_force_enabled, TR_KEY_anti_brute_force_enabled_kebab }, { TR_KEY_anti_brute_force_threshold, TR_KEY_anti_brute_force_threshold_kebab }, - { TR_KEY_bandwidth_priority, TR_KEY_bandwidth_priority_kebab }, // TODO(ckerr) legacy duplicate + { TR_KEY_bandwidth_priority, TR_KEY_bandwidth_priority_kebab_APICOMPAT }, { TR_KEY_bind_address_ipv4, TR_KEY_bind_address_ipv4_kebab }, { TR_KEY_bind_address_ipv6, TR_KEY_bind_address_ipv6_kebab }, { TR_KEY_blocklist_date, TR_KEY_blocklist_date_kebab }, @@ -272,18 +272,18 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_details_window_height, TR_KEY_details_window_height_kebab }, { TR_KEY_details_window_width, TR_KEY_details_window_width_kebab }, { TR_KEY_dht_enabled, TR_KEY_dht_enabled_kebab }, - { TR_KEY_done_date, TR_KEY_done_date_kebab }, // TODO(ckerr) legacy duplicate + { TR_KEY_done_date, TR_KEY_done_date_kebab_APICOMPAT }, { TR_KEY_download_dir, TR_KEY_download_dir_kebab }, // TODO(ckerr) legacy duplicate { TR_KEY_download_queue_enabled, TR_KEY_download_queue_enabled_kebab }, { TR_KEY_download_queue_size, TR_KEY_download_queue_size_kebab }, { TR_KEY_downloaded_bytes, TR_KEY_downloaded_bytes_kebab_APICOMPAT }, - { TR_KEY_downloading_time_seconds, TR_KEY_downloading_time_seconds_kebab }, + { TR_KEY_downloading_time_seconds, TR_KEY_downloading_time_seconds_kebab_APICOMPAT }, { TR_KEY_files_added, TR_KEY_files_added_kebab_APICOMPAT }, { TR_KEY_filter_mode, TR_KEY_filter_mode_kebab }, { TR_KEY_filter_text, TR_KEY_filter_text_kebab }, { TR_KEY_filter_trackers, TR_KEY_filter_trackers_kebab }, - { TR_KEY_idle_limit, TR_KEY_idle_limit_kebab }, - { TR_KEY_idle_mode, TR_KEY_idle_mode_kebab }, + { TR_KEY_idle_limit, TR_KEY_idle_limit_kebab_APICOMPAT }, + { TR_KEY_idle_mode, TR_KEY_idle_mode_kebab_APICOMPAT }, { TR_KEY_idle_seeding_limit, TR_KEY_idle_seeding_limit_kebab }, { TR_KEY_idle_seeding_limit_enabled, TR_KEY_idle_seeding_limit_enabled_kebab }, { TR_KEY_incomplete_dir, TR_KEY_incomplete_dir_kebab }, @@ -296,7 +296,7 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_main_window_width, TR_KEY_main_window_width_kebab }, { TR_KEY_main_window_x, TR_KEY_main_window_x_kebab }, { TR_KEY_main_window_y, TR_KEY_main_window_y_kebab }, - { TR_KEY_max_peers, TR_KEY_max_peers_kebab }, + { TR_KEY_max_peers, TR_KEY_max_peers_kebab_APICOMPAT }, { TR_KEY_message_level, TR_KEY_message_level_kebab }, { TR_KEY_open_dialog_dir, TR_KEY_open_dialog_dir_kebab }, { TR_KEY_peer_congestion_algorithm, TR_KEY_peer_congestion_algorithm_kebab }, @@ -307,7 +307,7 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_peer_port_random_low, TR_KEY_peer_port_random_low_kebab }, { TR_KEY_peer_port_random_on_start, TR_KEY_peer_port_random_on_start_kebab }, { TR_KEY_peer_socket_tos, TR_KEY_peer_socket_tos_kebab }, - { TR_KEY_peers2_6, TR_KEY_peers2_6_kebab }, + { TR_KEY_peers2_6, TR_KEY_peers2_6_kebab_APICOMPAT }, { TR_KEY_pex_enabled, TR_KEY_pex_enabled_kebab }, { TR_KEY_port_forwarding_enabled, TR_KEY_port_forwarding_enabled_kebab }, { TR_KEY_prompt_before_exit, TR_KEY_prompt_before_exit_kebab }, @@ -315,7 +315,7 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_queue_stalled_minutes, TR_KEY_queue_stalled_minutes_kebab }, { TR_KEY_ratio_limit, TR_KEY_ratio_limit_kebab }, { TR_KEY_ratio_limit_enabled, TR_KEY_ratio_limit_enabled_kebab }, - { TR_KEY_ratio_mode, TR_KEY_ratio_mode_kebab }, + { TR_KEY_ratio_mode, TR_KEY_ratio_mode_kebab_APICOMPAT }, { TR_KEY_read_clipboard, TR_KEY_read_clipboard_kebab }, { TR_KEY_remote_session_enabled, TR_KEY_remote_session_enabled_kebab }, { TR_KEY_remote_session_host, TR_KEY_remote_session_host_kebab }, @@ -347,7 +347,7 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_seconds_active, TR_KEY_seconds_active_kebab_APICOMPAT }, { TR_KEY_seed_queue_enabled, TR_KEY_seed_queue_enabled_kebab }, { TR_KEY_seed_queue_size, TR_KEY_seed_queue_size_kebab }, - { TR_KEY_seeding_time_seconds, TR_KEY_seeding_time_seconds_kebab }, + { TR_KEY_seeding_time_seconds, TR_KEY_seeding_time_seconds_kebab_APICOMPAT }, { TR_KEY_session_count, TR_KEY_session_count_kebab_APICOMPAT }, { TR_KEY_show_backup_trackers, TR_KEY_show_backup_trackers_kebab }, { TR_KEY_show_extra_peer_details, TR_KEY_show_extra_peer_details_kebab }, @@ -360,7 +360,7 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_sleep_per_seconds_during_verify, TR_KEY_sleep_per_seconds_during_verify_kebab }, { TR_KEY_sort_mode, TR_KEY_sort_mode_kebab }, { TR_KEY_sort_reversed, TR_KEY_sort_reversed_kebab }, - { TR_KEY_speed_Bps, TR_KEY_speed_Bps_kebab }, + { TR_KEY_speed_Bps, TR_KEY_speed_Bps_kebab_APICOMPAT }, { TR_KEY_speed_limit_down, TR_KEY_speed_limit_down_kebab }, { TR_KEY_speed_limit_down_enabled, TR_KEY_speed_limit_down_enabled_kebab }, { TR_KEY_speed_limit_up, TR_KEY_speed_limit_up_kebab }, @@ -379,8 +379,8 @@ auto constexpr SessionKeys = std::array{ { { TR_KEY_trash_original_torrent_files, TR_KEY_trash_original_torrent_files_kebab }, { TR_KEY_upload_slots_per_torrent, TR_KEY_upload_slots_per_torrent_kebab }, { TR_KEY_uploaded_bytes, TR_KEY_uploaded_bytes_kebab_APICOMPAT }, - { TR_KEY_use_global_speed_limit, TR_KEY_use_global_speed_limit_kebab }, - { TR_KEY_use_speed_limit, TR_KEY_use_speed_limit_kebab }, + { TR_KEY_use_global_speed_limit, TR_KEY_use_global_speed_limit_kebab_APICOMPAT }, + { TR_KEY_use_speed_limit, TR_KEY_use_speed_limit_kebab_APICOMPAT }, { TR_KEY_utp_enabled, TR_KEY_utp_enabled_kebab }, { TR_KEY_watch_dir, TR_KEY_watch_dir_kebab }, { TR_KEY_watch_dir_enabled, TR_KEY_watch_dir_enabled_kebab }, diff --git a/libtransmission/quark.h b/libtransmission/quark.h index 0f12a50b0..ec40cd766 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -35,11 +35,11 @@ enum // NOLINT(performance-enum-size) TR_KEY_NONE, /* represented as an empty string */ TR_KEY_active_torrent_count_camel, /* rpc (deprecated) */ TR_KEY_active_torrent_count, /* rpc */ - TR_KEY_activity_date_kebab, /* resume file (legacy) */ + TR_KEY_activity_date_kebab_APICOMPAT, TR_KEY_activity_date_camel, /* rpc (deprecated) */ TR_KEY_activity_date, /* rpc, resume file */ TR_KEY_added, /* pex */ - TR_KEY_added_date_kebab, /* resume file (legacy) */ + TR_KEY_added_date_kebab_APICOMPAT, /* resume file (legacy) */ TR_KEY_added_f, /* pex */ TR_KEY_added6, /* pex */ TR_KEY_added6_f, /* pex */ @@ -74,7 +74,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_anti_brute_force_threshold, /* rpc, settings */ TR_KEY_arguments, /* rpc */ TR_KEY_availability, // rpc - TR_KEY_bandwidth_priority_kebab, + TR_KEY_bandwidth_priority_kebab_APICOMPAT, TR_KEY_bandwidth_priority_camel, TR_KEY_bandwidth_priority, TR_KEY_begin_piece, @@ -143,7 +143,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_dht_enabled_kebab, TR_KEY_dht_enabled, TR_KEY_dnd, - TR_KEY_done_date_kebab, + TR_KEY_done_date_kebab_APICOMPAT, TR_KEY_done_date_camel, TR_KEY_done_date, TR_KEY_download_dir_kebab, @@ -170,7 +170,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_downloaded_bytes, TR_KEY_downloaded_ever, TR_KEY_downloader_count, - TR_KEY_downloading_time_seconds_kebab, + TR_KEY_downloading_time_seconds_kebab_APICOMPAT, TR_KEY_downloading_time_seconds, TR_KEY_dropped, TR_KEY_dropped6, @@ -246,8 +246,8 @@ enum // NOLINT(performance-enum-size) TR_KEY_host, TR_KEY_id, TR_KEY_id_timestamp, - TR_KEY_idle_limit_kebab, - TR_KEY_idle_mode_kebab, + TR_KEY_idle_limit_kebab_APICOMPAT, + TR_KEY_idle_mode_kebab_APICOMPAT, TR_KEY_idle_seeding_limit_kebab, TR_KEY_idle_seeding_limit_enabled_kebab, TR_KEY_idle_limit, @@ -333,7 +333,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_main_window_y, TR_KEY_manual_announce_time_camel, TR_KEY_manual_announce_time, - TR_KEY_max_peers_kebab, + TR_KEY_max_peers_kebab_APICOMPAT, TR_KEY_max_connected_peers_camel, TR_KEY_max_connected_peers, TR_KEY_max_peers, @@ -392,7 +392,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_peer_socket_tos, TR_KEY_peers, TR_KEY_peers2, - TR_KEY_peers2_6_kebab, + TR_KEY_peers2_6_kebab_APICOMPAT, TR_KEY_peers2_6, TR_KEY_peers_connected_camel, TR_KEY_peers_from_camel, @@ -464,7 +464,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_rate_upload, TR_KEY_ratio_limit_kebab, TR_KEY_ratio_limit_enabled_kebab, - TR_KEY_ratio_mode_kebab, + TR_KEY_ratio_mode_kebab_APICOMPAT, TR_KEY_ratio_limit, TR_KEY_ratio_limit_enabled, TR_KEY_ratio_mode, @@ -564,7 +564,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_seed_ratio_mode, TR_KEY_seeder_count_camel, TR_KEY_seeder_count, - TR_KEY_seeding_time_seconds_kebab, + TR_KEY_seeding_time_seconds_kebab_APICOMPAT, TR_KEY_seeding_time_seconds, TR_KEY_sequential_download, TR_KEY_sequential_download_from_piece, @@ -613,7 +613,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_sort_reversed, TR_KEY_source, TR_KEY_speed, - TR_KEY_speed_Bps_kebab, + TR_KEY_speed_Bps_kebab_APICOMPAT, TR_KEY_speed_bytes_kebab, TR_KEY_speed_limit_down_kebab, TR_KEY_speed_limit_down_enabled_kebab, @@ -722,8 +722,8 @@ enum // NOLINT(performance-enum-size) TR_KEY_uploaded_bytes, TR_KEY_uploaded_ever, TR_KEY_url_list, - TR_KEY_use_global_speed_limit_kebab, - TR_KEY_use_speed_limit_kebab, + TR_KEY_use_global_speed_limit_kebab_APICOMPAT, + TR_KEY_use_speed_limit_kebab_APICOMPAT, TR_KEY_use_global_speed_limit, TR_KEY_use_speed_limit, TR_KEY_ut_holepunch, diff --git a/libtransmission/resume.cc b/libtransmission/resume.cc index da6d28ccb..657201fc0 100644 --- a/libtransmission/resume.cc +++ b/libtransmission/resume.cc @@ -16,6 +16,7 @@ #include "libtransmission/transmission.h" +#include "libtransmission/api-compat.h" #include "libtransmission/bitfield.h" #include "libtransmission/error.h" #include "libtransmission/file.h" @@ -74,7 +75,7 @@ auto load_peers(tr_variant::Map const& map, tr_torrent* tor) ret = tr_resume::Peers; } - if (auto const* l = map.find_if({ TR_KEY_peers2_6, TR_KEY_peers2_6_kebab }); l != nullptr) + if (auto const* l = map.find_if(TR_KEY_peers2_6)) { auto const num_added = add_peers(tor, *l); tr_logAddTraceTor(tor, fmt::format("Loaded {} IPv6 peers from resume file", num_added)); @@ -266,7 +267,7 @@ void save_idle_limits(tr_variant::Map& map, tr_torrent const* tor) void load_single_speed_limit(tr_variant::Map const& map, tr_direction dir, tr_torrent* tor) { - if (auto const i = map.value_if({ TR_KEY_speed_Bps, TR_KEY_speed_Bps_kebab }); i) + if (auto const i = map.value_if(TR_KEY_speed_Bps)) { tor->set_speed_limit(dir, Speed{ *i, Speed::Units::Byps }); } @@ -275,12 +276,12 @@ void load_single_speed_limit(tr_variant::Map const& map, tr_direction dir, tr_to tor->set_speed_limit(dir, Speed{ *i2, Speed::Units::KByps }); } - if (auto const b = map.value_if({ TR_KEY_use_speed_limit, TR_KEY_use_speed_limit_kebab }); b) + if (auto const b = map.value_if(TR_KEY_use_speed_limit)) { tor->use_speed_limit(dir, *b); } - if (auto const b = map.value_if({ TR_KEY_use_global_speed_limit, TR_KEY_use_global_speed_limit_kebab }); b) + if (auto const b = map.value_if(TR_KEY_use_global_speed_limit)) { tr_torrentUseSessionLimits(tor, *b); } @@ -290,15 +291,13 @@ auto load_speed_limits(tr_variant::Map const& map, tr_torrent* tor) { auto ret = tr_resume::fields_t{}; - if (auto const* child = map.find_if({ TR_KEY_speed_limit_up, TR_KEY_speed_limit_up_kebab }); - child != nullptr) + if (auto const* child = map.find_if(TR_KEY_speed_limit_up)) { load_single_speed_limit(*child, TR_UP, tor); ret = tr_resume::Speedlimit; } - if (auto const* child = map.find_if({ TR_KEY_speed_limit_down, TR_KEY_speed_limit_down_kebab }); - child != nullptr) + if (auto const* child = map.find_if(TR_KEY_speed_limit_down)) { load_single_speed_limit(*child, TR_DOWN, tor); ret = tr_resume::Speedlimit; @@ -309,18 +308,18 @@ auto load_speed_limits(tr_variant::Map const& map, tr_torrent* tor) tr_resume::fields_t load_ratio_limits(tr_variant::Map const& map, tr_torrent* tor) { - auto const* const d = map.find_if({ TR_KEY_ratio_limit, TR_KEY_ratio_limit_kebab }); + auto const* const d = map.find_if(TR_KEY_ratio_limit); if (d == nullptr) { return {}; } - if (auto const dratio = d->value_if({ TR_KEY_ratio_limit, TR_KEY_ratio_limit_kebab }); dratio) + if (auto const dratio = d->value_if(TR_KEY_ratio_limit)) { tor->set_seed_ratio(*dratio); } - if (auto const i = d->value_if({ TR_KEY_ratio_mode, TR_KEY_ratio_mode_kebab }); i) + if (auto const i = d->value_if(TR_KEY_ratio_mode)) { tor->set_seed_ratio_mode(static_cast(*i)); } @@ -330,18 +329,18 @@ tr_resume::fields_t load_ratio_limits(tr_variant::Map const& map, tr_torrent* to tr_resume::fields_t load_idle_limits(tr_variant::Map const& map, tr_torrent* tor) { - auto const* const d = map.find_if({ TR_KEY_idle_limit, TR_KEY_idle_limit_kebab }); + auto const* const d = map.find_if(TR_KEY_idle_limit); if (d == nullptr) { return {}; } - if (auto const imin = d->value_if({ TR_KEY_idle_limit, TR_KEY_idle_limit_kebab }); imin) + if (auto const imin = d->value_if(TR_KEY_idle_limit)) { tor->set_idle_limit_minutes(*imin); } - if (auto const i = d->value_if({ TR_KEY_idle_mode, TR_KEY_idle_mode_kebab }); i) + if (auto const i = d->value_if(TR_KEY_idle_mode)) { tor->set_idle_limit_mode(static_cast(*i)); } @@ -636,6 +635,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he return {}; } + otop = libtransmission::api_compat::convert_incoming_data(*otop); auto const* const p_map = otop->get_if(); if (p_map == nullptr) { @@ -667,8 +667,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & (tr_resume::Progress | tr_resume::IncompleteDir)) != 0) { - if (auto sv = map.value_if({ TR_KEY_incomplete_dir, TR_KEY_incomplete_dir_kebab }); - sv && !std::empty(*sv)) + if (auto sv = map.value_if(TR_KEY_incomplete_dir); sv && !std::empty(*sv)) { helper.load_incomplete_dir(*sv); fields_loaded |= tr_resume::IncompleteDir; @@ -695,7 +694,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & tr_resume::MaxPeers) != 0) { - if (auto i = map.value_if({ TR_KEY_max_peers, TR_KEY_max_peers_kebab }); i) + if (auto const i = map.value_if(TR_KEY_max_peers)) { tor->set_peer_limit(static_cast(*i)); fields_loaded |= tr_resume::MaxPeers; @@ -713,7 +712,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & tr_resume::AddedDate) != 0) { - if (auto i = map.value_if({ TR_KEY_added_date, TR_KEY_added_date_kebab }); i) + if (auto const i = map.value_if(TR_KEY_added_date)) { helper.load_date_added(static_cast(*i)); fields_loaded |= tr_resume::AddedDate; @@ -722,7 +721,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & tr_resume::DoneDate) != 0) { - if (auto i = map.value_if({ TR_KEY_done_date, TR_KEY_done_date_kebab }); i) + if (auto const i = map.value_if(TR_KEY_done_date)) { helper.load_date_done(static_cast(*i)); fields_loaded |= tr_resume::DoneDate; @@ -731,7 +730,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & tr_resume::ActivityDate) != 0) { - if (auto i = map.value_if({ TR_KEY_activity_date, TR_KEY_activity_date_kebab }); i) + if (auto const i = map.value_if(TR_KEY_activity_date)) { tor->set_date_active(*i); fields_loaded |= tr_resume::ActivityDate; @@ -740,7 +739,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & tr_resume::TimeSeeding) != 0) { - if (auto i = map.value_if({ TR_KEY_seeding_time_seconds, TR_KEY_seeding_time_seconds_kebab }); i) + if (auto const i = map.value_if(TR_KEY_seeding_time_seconds)) { helper.load_seconds_seeding_before_current_start(*i); fields_loaded |= tr_resume::TimeSeeding; @@ -749,7 +748,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & tr_resume::TimeDownloading) != 0) { - if (auto i = map.value_if({ TR_KEY_downloading_time_seconds, TR_KEY_downloading_time_seconds_kebab }); i) + if (auto const i = map.value_if(TR_KEY_downloading_time_seconds)) { helper.load_seconds_downloading_before_current_start(*i); fields_loaded |= tr_resume::TimeDownloading; @@ -758,8 +757,7 @@ tr_resume::fields_t load_from_file(tr_torrent* tor, tr_torrent::ResumeHelper& he if ((fields_to_load & tr_resume::BandwidthPriority) != 0) { - if (auto i = map.value_if({ TR_KEY_bandwidth_priority, TR_KEY_bandwidth_priority_kebab }); - i && tr_isPriority(static_cast(*i))) + if (auto const i = map.value_if(TR_KEY_bandwidth_priority); i && tr_isPriority(static_cast(*i))) { tr_torrentSetPriority(tor, static_cast(*i)); fields_loaded |= tr_resume::BandwidthPriority; @@ -985,8 +983,9 @@ void save(tr_torrent* const tor, tr_torrent::ResumeHelper const& helper) save_labels(map, tor); save_group(map, tor); + auto const out = libtransmission::api_compat::convert_outgoing_data(std::move(map)); auto serde = tr_variant_serde::benc(); - if (!serde.to_file(std::move(map), tor->resume_file())) + if (!serde.to_file(out, tor->resume_file())) { tor->error().set_local_error(fmt::format("Unable to save resume file: {:s}", serde.error_.message())); } diff --git a/tests/assets/benc2cpp.py b/tests/assets/benc2cpp.py new file mode 100644 index 000000000..4ed80172c --- /dev/null +++ b/tests/assets/benc2cpp.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +# +# Created by GitHub Copilot (GPT-5.2 (Preview)). +# +# License: Same terms as Transmission itself (see COPYING). Transmission +# permits redistribution/modification under GNU GPLv2, GPLv3, or any future +# license endorsed by Mnemosyne LLC. +# +# Purpose: +# Convert a bencoded (benc) file into a C++ concatenated string-literal +# fragment that preserves the exact original bytes. Output is whitespace-only +# formatted for readability (4-space indentation), similar in spirit to +# pretty-printed JSON. +# +# Usage: +# tests/assets/benc2cpp.py path/to/file.benc > out.cppfrag + +from __future__ import annotations + +import sys +from pathlib import Path + + +def bytes_to_cpp_string_literal(data: bytes) -> str: + r"""Return a single C++ string literal token for arbitrary bytes. + + Uses normal (non-raw) string literals and emits \xNN for bytes that are not + safe/pleasant as-is. + """ + + out = '"' + prev_was_hex_escape = False + for b in data: + ch = chr(b) + + # C/C++ rule: \x escapes consume *all following hex digits*. + # If we emit "\xNN" and then a literal '0'..'9'/'a'..'f'/'A'..'F', + # it becomes a single (larger) hex escape and may fail to compile. + if ( + prev_was_hex_escape + and ( + (ord('0') <= b <= ord('9')) + or (ord('a') <= b <= ord('f')) + or (ord('A') <= b <= ord('F')) + ) + ): + out += f"\\x{b:02x}" + prev_was_hex_escape = True + continue + + if ch == "\\": + out += r"\\\\" + prev_was_hex_escape = False + elif ch == '"': + out += r"\\\"" + prev_was_hex_escape = False + elif 0x20 <= b <= 0x7E: + out += ch + prev_was_hex_escape = False + else: + out += f"\\x{b:02x}" + prev_was_hex_escape = True + out += '"' + return out + + +def bencode_tokenize(data: bytes) -> list[bytes]: + r"""Tokenize bencode into syntactic units without changing bytes. + + Tokens are: + - b"d", b"l", b"e" + - b"i...e" (entire integer token) + - b":" (entire string token, including length and colon) + + This is a tokenizer only. It assumes the input is valid bencode. + """ + + tokens: list[bytes] = [] + i = 0 + n = len(data) + + def need(cond: bool, msg: str) -> None: + if not cond: + raise ValueError(f"Invalid bencode at offset {i}: {msg}") + + while i < n: + b = data[i] + + if b in (ord('d'), ord('l'), ord('e')): + tokens.append(bytes([b])) + i += 1 + continue + + if b == ord('i'): + j = data.find(b'e', i + 1) + need(j != -1, "unterminated integer") + tokens.append(data[i:j + 1]) + i = j + 1 + continue + + if ord('0') <= b <= ord('9'): + j = i + while j < n and ord('0') <= data[j] <= ord('9'): + j += 1 + need(j < n and data[j] == ord(':'), "string length missing colon") + strlen = int(data[i:j].decode('ascii')) + start = j + 1 + end = start + strlen + need(end <= n, "string payload truncated") + tokens.append(data[i:end]) + i = end + continue + + msg = f"Invalid bencode at offset {i}: unexpected byte 0x{b:02x}" + raise ValueError(msg) + + return tokens + + +def render_bencode_tokens_pretty( + tokens: list[bytes], + *, + base_indent: int = 4, + indent_step: int = 4, +) -> list[str]: + """Render bencode tokens into indented C++ string literal lines. + + Whitespace-only pretty-printing rules: + - One token per line by default. + - For dictionaries, if a key's value is a scalar (string or integer), + render the key and value on the same line separated by a space. + + This changes only whitespace between C string fragments; the concatenated + bytes are identical to the input. + """ + + lines: list[str] = [] + + # Stack entries are either: + # ('list', None) + # ('dict', expecting_key: bool) + stack: list[tuple[str, bool | None]] = [] + pending_dict_key: bytes | None = None + + def depth() -> int: + return len(stack) + + def indent() -> str: + return ' ' * (base_indent + depth() * indent_step) + + def is_scalar_token(t: bytes) -> bool: + return t.startswith(b'i') or (t[:1].isdigit()) + + i = 0 + while i < len(tokens): + tok = tokens[i] + + if tok == b'e': + if pending_dict_key is not None: + key_lit = bytes_to_cpp_string_literal(pending_dict_key) + lines.append(indent() + key_lit) + pending_dict_key = None + + if stack: + stack.pop() + + lines.append(indent() + bytes_to_cpp_string_literal(tok)) + + # If this closed a value container in a dict, + # the parent dict is now ready for next key. + if stack and stack[-1][0] == 'dict' and stack[-1][1] is False: + stack[-1] = ('dict', True) + + i += 1 + continue + + # Dict key collection + if stack and stack[-1][0] == 'dict' and stack[-1][1] is True: + pending_dict_key = tok + stack[-1] = ('dict', False) + i += 1 + continue + + # Dict value emission + is_dict_value = ( + stack + and stack[-1][0] == 'dict' + and stack[-1][1] is False + and pending_dict_key is not None + ) + if is_dict_value: + if is_scalar_token(tok): + lines.append( + indent() + + bytes_to_cpp_string_literal(pending_dict_key) + + ' ' + + bytes_to_cpp_string_literal(tok) + ) + pending_dict_key = None + stack[-1] = ('dict', True) + i += 1 + continue + + # Non-scalar (container) value: key on its own line, then container + # token. + key_lit = bytes_to_cpp_string_literal(pending_dict_key) + lines.append(indent() + key_lit) + pending_dict_key = None + + lines.append(indent() + bytes_to_cpp_string_literal(tok)) + if tok == b'd': + stack.append(('dict', True)) + elif tok == b'l': + stack.append(('list', None)) + else: + stack[-1] = ('dict', True) + + i += 1 + continue + + # Default emission + lines.append(indent() + bytes_to_cpp_string_literal(tok)) + if tok == b'd': + stack.append(('dict', True)) + elif tok == b'l': + stack.append(('list', None)) + + i += 1 + + if pending_dict_key is not None: + lines.append(indent() + bytes_to_cpp_string_literal(pending_dict_key)) + + return lines + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + sys.stderr.write(f"Usage: {Path(argv[0]).name} path/to/file.benc\n") + return 2 + + in_path = Path(argv[1]) + data = in_path.read_bytes() + + tokens = bencode_tokenize(data) + pretty_lines = render_bencode_tokens_pretty(tokens) + + sys.stdout.write("// clang-format off\n") + sys.stdout.write("constexpr std::string_view Benc =\n") + if not pretty_lines: + sys.stdout.write(" \"\";\n") + else: + for line in pretty_lines[:-1]: + sys.stdout.write(line) + sys.stdout.write("\n") + sys.stdout.write(pretty_lines[-1]) + sys.stdout.write(";\n") + sys.stdout.write("// clang-format on\n") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/tests/libtransmission/api-compat-test.cc b/tests/libtransmission/api-compat-test.cc index be78c613f..75f31b86e 100644 --- a/tests/libtransmission/api-compat-test.cc +++ b/tests/libtransmission/api-compat-test.cc @@ -644,6 +644,306 @@ constexpr std::string_view UnrecognisedInfoLegacyResponse = R"json({ "tag": 10 })json"; +// clang-format off +constexpr std::string_view LegacyResumeBenc = + "d" + "13:activity-date" "i1765724117e" + "10:added-date" "i1756689559e" + "18:bandwidth-priority" "i0e" + "7:corrupt" "i0e" + "11:destination" "30:/data/trackers/untracked/Books" + "3:dnd" + "l" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "e" + "9:done-date" "i1756689845e" + "10:downloaded" "i4830420542e" + "24:downloading-time-seconds" "i286e" + "5:files" + "l" + "102:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v01 - Oz - Baum - The Wonderful Wizard of Oz (1990).epub" + "100:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v02 - Oz - Baum - The Marvelous Land of Oz (1904).epub" + "86:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v03 - Oz - Baum - Ozma of Oz (1907).epub" + "104:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v04 - Oz - Baum - Dorothy and the Wizard in Oz (1908).epub" + "90:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v05 - Oz - Baum - The Road to Oz (1909).epub" + "98:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v06 - Oz - Baum - The Emerald City of Oz (1910).epub" + "100:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v07 - Oz - Baum - The Patchwork Girl of Oz (1913).epub" + "e" + "5:group" "0:" + "10:idle-limit" + "d" + "10:idle-limit" "i30e" + "9:idle-mode" "i0e" + "e" + "6:labels" + "l" + "e" + "9:max-peers" "i20e" + "4:name" "45:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7" + "6:paused" "i0e" + "6:peers2" + "l" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x80\x3b\xac\x8f\x3b\x1c" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xe2\xa1\xe3\x25\x2c\xfa" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xf2\x50\x82\xab\xed\x08" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xeb\xb2\x8c\xa1\x1e\xc6" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xe8\x92\x9e\x87\xd1\xb4" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x0a\xca\x51\xdd\x61\x52" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x3d\xf0\x9c\x23\x55\x20" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x4d\x9f\x2e\xd9\x40\x9e" + "e" + "d" + "5:flags" "i12e" + "14:socket_address" "6:\x83\xa6\xd7\x7f\xa3\x4c" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xcd\x4a\xdf\x95\xc8\xaa" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x78\x5b\x6c\x9b\xa8\x38" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x60\xc6\xe0\x11\xc5\x76" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xd4\xfa\x37\x77\x0f\xe4" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xfb\x28\x6c\x4d\xc3\x02" + "e" + "e" + "8:priority" + "l" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "e" + "8:progress" + "d" + "6:blocks" "3:all" + "6:mtimes" + "l" + "i1756689844e" + "i1756689800e" + "i1756689836e" + "i1756689812e" + "i1756689839e" + "i1756689844e" + "i1756689804e" + "e" + "6:pieces" "3:all" + "e" + "11:ratio-limit" + "d" + "11:ratio-limit" "8:2.000000" + "10:ratio-mode" "i0e" + "e" + "20:seeding-time-seconds" "i7373039e" + "19:sequential_download" "i0e" + "30:sequential_download_from_piece" "i0e" + "16:speed-limit-down" + "d" + "9:speed-Bps" "i2000000e" + "22:use-global-speed-limit" "i1e" + "15:use-speed-limit" "i0e" + "e" + "14:speed-limit-up" + "d" + "9:speed-Bps" "i5000000e" + "22:use-global-speed-limit" "i1e" + "15:use-speed-limit" "i0e" + "e" + "8:uploaded" "i98667375637e" + "e"; + +constexpr std::string_view ResumeBenc = + "d" + "13:activity_date" "i1765724117e" + "10:added_date" "i1756689559e" + "18:bandwidth_priority" "i0e" + "7:corrupt" "i0e" + "11:destination" "30:/data/trackers/untracked/Books" + "3:dnd" + "l" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "e" + "9:done_date" "i1756689845e" + "10:downloaded" "i4830420542e" + "24:downloading_time_seconds" "i286e" + "5:files" + "l" + "102:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v01 - Oz - Baum - The Wonderful Wizard of Oz (1990).epub" + "100:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v02 - Oz - Baum - The Marvelous Land of Oz (1904).epub" + "86:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v03 - Oz - Baum - Ozma of Oz (1907).epub" + "104:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v04 - Oz - Baum - Dorothy and the Wizard in Oz (1908).epub" + "90:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v05 - Oz - Baum - The Road to Oz (1909).epub" + "98:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v06 - Oz - Baum - The Emerald City of Oz (1910).epub" + "100:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7/v07 - Oz - Baum - The Patchwork Girl of Oz (1913).epub" + "e" + "5:group" "0:" + "10:idle_limit" + "d" + "10:idle_limit" "i30e" + "9:idle_mode" "i0e" + "e" + "6:labels" + "l" + "e" + "9:max_peers" "i20e" + "4:name" "45:Oz Series - Frank L Baum [PUBLIC DOMAIN] v1-7" + "6:paused" "i0e" + "6:peers2" + "l" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x80\x3b\xac\x8f\x3b\x1c" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xe2\xa1\xe3\x25\x2c\xfa" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xf2\x50\x82\xab\xed\x08" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xeb\xb2\x8c\xa1\x1e\xc6" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xe8\x92\x9e\x87\xd1\xb4" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x0a\xca\x51\xdd\x61\x52" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x3d\xf0\x9c\x23\x55\x20" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x4d\x9f\x2e\xd9\x40\x9e" + "e" + "d" + "5:flags" "i12e" + "14:socket_address" "6:\x83\xa6\xd7\x7f\xa3\x4c" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xcd\x4a\xdf\x95\xc8\xaa" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x78\x5b\x6c\x9b\xa8\x38" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\x60\xc6\xe0\x11\xc5\x76" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xd4\xfa\x37\x77\x0f\xe4" + "e" + "d" + "5:flags" "i0e" + "14:socket_address" "6:\xfb\x28\x6c\x4d\xc3\x02" + "e" + "e" + "8:priority" + "l" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "i0e" + "e" + "8:progress" + "d" + "6:blocks" "3:all" + "6:mtimes" + "l" + "i1756689844e" + "i1756689800e" + "i1756689836e" + "i1756689812e" + "i1756689839e" + "i1756689844e" + "i1756689804e" + "e" + "6:pieces" "3:all" + "e" + "11:ratio_limit" + "d" + "11:ratio_limit" "8:2.000000" + "10:ratio_mode" "i0e" + "e" + "20:seeding_time_seconds" "i7373039e" + "19:sequential_download" "i0e" + "30:sequential_download_from_piece" "i0e" + "16:speed_limit_down" + "d" + "9:speed_Bps" "i2000000e" + "22:use_global_speed_limit" "i1e" + "15:use_speed_limit" "i0e" + "e" + "14:speed_limit_up" + "d" + "9:speed_Bps" "i5000000e" + "22:use_global_speed_limit" "i1e" + "15:use_speed_limit" "i0e" + "e" + "8:uploaded" "i98667375637e" + "e"; +// clang-format on + } // namespace TEST(ApiCompatTest, canConvertRpc) @@ -708,12 +1008,11 @@ TEST(ApiCompatTest, canConvertRpc) } } -TEST(ApiCompatTest, canConvertDataFiles) +TEST(ApiCompatTest, canConvertJsonDataFiles) { using Style = libtransmission::api_compat::Style; using TestCase = std::tuple; - // clang-format off static auto constexpr TestCases = std::array{ { { "settings tr5 -> tr5", CurrentSettingsJson, Style::Tr5, CurrentSettingsJson }, { "settings tr5 -> tr4", CurrentSettingsJson, Style::Tr4, LegacySettingsJson }, @@ -725,14 +1024,39 @@ TEST(ApiCompatTest, canConvertDataFiles) { "stats tr4 -> tr5", LegacyStatsJson, Style::Tr5, CurrentStatsJson }, { "stats tr4 -> tr4", LegacyStatsJson, Style::Tr4, LegacyStatsJson }, } }; - // clang-format on for (auto const& [name, src, tgt_style, expected] : TestCases) { auto serde = tr_variant_serde::json(); + serde.inplace(); + auto parsed = serde.parse(src); ASSERT_TRUE(parsed.has_value()); auto converted = libtransmission::api_compat::convert(*parsed, tgt_style); EXPECT_EQ(expected, serde.to_string(converted)) << name; } } + +TEST(ApiCompatTest, canConvertBencDataFiles) +{ + using Style = libtransmission::api_compat::Style; + using TestCase = std::tuple; + + static auto constexpr TestCases = std::array{ { + { "resume tr5 -> tr5", ResumeBenc, Style::Tr5, ResumeBenc }, + { "resume tr5 -> tr4", ResumeBenc, Style::Tr4, LegacyResumeBenc }, + { "resume tr4 -> tr5", LegacyResumeBenc, Style::Tr5, ResumeBenc }, + { "resume tr4 -> tr4", LegacyResumeBenc, Style::Tr4, LegacyResumeBenc }, + } }; + + for (auto const& [name, src, tgt_style, expected] : TestCases) + { + auto serde = tr_variant_serde::benc(); + serde.inplace(); + + auto parsed = serde.parse(src); + ASSERT_TRUE(parsed.has_value()) << name; + auto converted = libtransmission::api_compat::convert(*parsed, tgt_style); + EXPECT_EQ(expected, serde.to_string(converted)) << name; + } +}