diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index 5028027f6..24fa8aff2 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -480,6 +480,8 @@ EDBAAC8C29E486BC00D9495F /* ip-cache.h in Headers */ = {isa = PBXBuildFile; fileRef = EDBAAC8B29E486BC00D9495F /* ip-cache.h */; }; EDBAAC8E29E486C200D9495F /* ip-cache.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDBAAC8D29E486C200D9495F /* ip-cache.cc */; }; EDBDFA9E25AFCCA60093D9C1 /* evutil_time.c in Sources */ = {isa = PBXBuildFile; fileRef = EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */; }; + EDC37BCD2EE9C2AD001E2612 /* api-compat.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDC37BCC2EE9C2AD001E2612 /* api-compat.cc */; }; + EDC37BCE2EE9C2AD001E2612 /* api-compat.h in Headers */ = {isa = PBXBuildFile; fileRef = EDC37BCB2EE9C2AD001E2612 /* api-compat.h */; }; EDC749F92D98AE3000A12D0F /* PowerManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDC749F82D98AE2900A12D0F /* PowerManager.mm */; }; F11545ACA7C4D7A464F703AB /* block-info.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A044CBD8C049AFCBD4DB411 /* block-info.h */; settings = {ATTRIBUTES = (Project, ); }; }; F63480631E1D7274005B9E09 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F63480621E1D7274005B9E09 /* Images.xcassets */; }; @@ -1472,6 +1474,8 @@ EDBAAC8B29E486BC00D9495F /* ip-cache.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; fileEncoding = 4; path = "ip-cache.h"; sourceTree = ""; }; EDBAAC8D29E486C200D9495F /* ip-cache.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "ip-cache.cc"; sourceTree = ""; }; EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = evutil_time.c; sourceTree = ""; }; + EDC37BCB2EE9C2AD001E2612 /* api-compat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "api-compat.h"; sourceTree = ""; }; + EDC37BCC2EE9C2AD001E2612 /* api-compat.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "api-compat.cc"; sourceTree = ""; }; EDC749F72D98ADE200A12D0F /* PowerManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PowerManager.h; sourceTree = ""; }; EDC749F82D98AE2900A12D0F /* PowerManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PowerManager.mm; sourceTree = ""; }; F63480621E1D7274005B9E09 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Images/Images.xcassets; sourceTree = ""; }; @@ -1919,6 +1923,8 @@ 4D1838DC09DEC04A0047D688 /* libtransmission */ = { isa = PBXGroup; children = ( + EDC37BCB2EE9C2AD001E2612 /* api-compat.h */, + EDC37BCC2EE9C2AD001E2612 /* api-compat.cc */, 66F977825E65AD498C028BB1 /* announce-list.cc */, 66F977825E65AD498C028BB3 /* announce-list.h */, A23F299F132A447400E9A83B /* announcer-common.h */, @@ -2561,6 +2567,7 @@ buildActionMask = 2147483647; files = ( C1077A51183EB29600634C22 /* file.h in Headers */, + EDC37BCE2EE9C2AD001E2612 /* api-compat.h in Headers */, BEFC1E290C07861A00B0BB3C /* version.h in Headers */, BEFC1E2A0C07861A00B0BB3C /* utils.h in Headers */, BE7AA337F6752914B0C416B0 /* utils-ev.h in Headers */, @@ -3489,6 +3496,7 @@ C1FEE57A1C3223CC00D62832 /* watchdir.cc in Sources */, A23547E211CD0B090046EAE6 /* cache.cc in Sources */, C843FC8429C51B9400491854 /* utils.mm in Sources */, + EDC37BCD2EE9C2AD001E2612 /* api-compat.cc in Sources */, A284214412DA663E00FBDDBB /* tr-udp.cc in Sources */, C17740D5273A002C00E455D2 /* web-utils.cc in Sources */, A2679294130E00A000CB7464 /* tr-utp.cc in Sources */, diff --git a/docs/Environment-Variables.md b/docs/Environment-Variables.md index bec58e1db..137bb5f6c 100644 --- a/docs/Environment-Variables.md +++ b/docs/Environment-Variables.md @@ -6,6 +6,7 @@ Users can set environmental variables to override Transmission's default behavio * If `TR_CURL_SSL_NO_VERIFY` is set, Transmission will not validate SSL certificate for HTTPS connections when talking to trackers. See CURL's documentation ([CURLOPT_SSL_VERIFYHOST](https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html) and [CURLOPT_SSL_VERIFYPEER](https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html)) for more details. * If `TR_CURL_VERBOSE` is set, debugging information for libcurl will be enabled. More information about libcurl's debugging mode [is available here](https://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTVERBOSE). * If `TR_DHT_VERBOSE` is set, Transmission will log all of the DHT's activities in excruciating detail to standard error. + * If `TR_SAVE_VERSION_FORMAT` is set to `4` or `5`, it will save settings.json, stats.json, etc. files to either Transmission 4 or Transmission 5 format. ## Standard Variables Used by Transmission * If `TRANSMISSION_WEB_HOME` is _not_ set, non-Mac platforms will look for the [Web Interface](Web-Interface.md) files in `XDG_DATA_HOME` and in `XDG_DATA_DIRS` as described in [the XDG Base Directory Specification](https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables). `XDG_DATA_HOME` has a default value of `$HOME/.local/share/`. diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index b76522577..04d90ec12 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -28,6 +28,8 @@ target_sources(${TR_NAME} announcer-udp.cc announcer.cc announcer.h + api-compat.cc + api-compat.h bandwidth.cc bandwidth.h benc.h diff --git a/libtransmission/api-compat.cc b/libtransmission/api-compat.cc new file mode 100644 index 000000000..06f0d6bc0 --- /dev/null +++ b/libtransmission/api-compat.cc @@ -0,0 +1,800 @@ +// This file Copyright © Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#include + +#include +#include +#include +#include +#include + +#include "libtransmission/api-compat.h" +#include "libtransmission/quark.h" +#include "libtransmission/rpcimpl.h" +#include "libtransmission/utils.h" +#include "libtransmission/variant.h" + +namespace libtransmission::api_compat +{ +namespace +{ +struct ApiKey +{ + // snake-case quark + tr_quark current; + + // legacy mixed-case RPC quark (pre-05aef3e7) + tr_quark legacy; +}; + +auto constexpr RpcKeys = std::array{ { + { TR_KEY_active_torrent_count, TR_KEY_active_torrent_count_camel }, + { TR_KEY_activity_date, TR_KEY_activity_date_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_added_date, TR_KEY_added_date_camel }, // TODO(ckerr) legacy duplicate + { 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 }, + { TR_KEY_alt_speed_time_day, TR_KEY_alt_speed_time_day_kebab }, + { TR_KEY_alt_speed_time_enabled, TR_KEY_alt_speed_time_enabled_kebab }, + { TR_KEY_alt_speed_time_end, TR_KEY_alt_speed_time_end_kebab }, + { TR_KEY_alt_speed_up, TR_KEY_alt_speed_up_kebab }, + { TR_KEY_announce_state, TR_KEY_announce_state_camel }, + { 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_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_blocklist_enabled, TR_KEY_blocklist_enabled_kebab }, + { TR_KEY_blocklist_size, TR_KEY_blocklist_size_kebab }, + { TR_KEY_blocklist_url, TR_KEY_blocklist_url_kebab }, + { TR_KEY_bytes_completed, TR_KEY_bytes_completed_camel }, + { TR_KEY_cache_size_mb, TR_KEY_cache_size_mb_kebab }, + { TR_KEY_client_is_choked, TR_KEY_client_is_choked_camel }, + { TR_KEY_client_is_interested, TR_KEY_client_is_interested_camel }, + { TR_KEY_client_name, TR_KEY_client_name_camel }, + { TR_KEY_config_dir, TR_KEY_config_dir_kebab }, + { TR_KEY_corrupt_ever, TR_KEY_corrupt_ever_camel }, + { TR_KEY_cumulative_stats, TR_KEY_cumulative_stats_kebab }, + { TR_KEY_current_stats, TR_KEY_current_stats_kebab }, + { TR_KEY_date_created, TR_KEY_date_created_camel }, + { TR_KEY_default_trackers, TR_KEY_default_trackers_kebab }, + { TR_KEY_delete_local_data, TR_KEY_delete_local_data_kebab }, + { TR_KEY_desired_available, TR_KEY_desired_available_camel }, + { TR_KEY_dht_enabled, TR_KEY_dht_enabled_kebab }, + { TR_KEY_done_date, TR_KEY_done_date_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_download_count, TR_KEY_download_count_camel }, + { TR_KEY_download_dir, TR_KEY_download_dir_kebab }, // crazy case 1: camel in torrent-get/set, kebab everywhere else + { TR_KEY_download_dir_free_space, TR_KEY_download_dir_free_space_kebab }, + { TR_KEY_download_limit, TR_KEY_download_limit_camel }, + { TR_KEY_download_limited, TR_KEY_download_limited_camel }, + { TR_KEY_download_queue_enabled, TR_KEY_download_queue_enabled_kebab }, + { TR_KEY_download_queue_size, TR_KEY_download_queue_size_kebab }, + { TR_KEY_download_speed, TR_KEY_download_speed_camel }, + { TR_KEY_downloaded_bytes, TR_KEY_downloaded_bytes_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_downloaded_ever, TR_KEY_downloaded_ever_camel }, + { TR_KEY_edit_date, TR_KEY_edit_date_camel }, + { TR_KEY_error_string, TR_KEY_error_string_camel }, + { TR_KEY_eta_idle, TR_KEY_eta_idle_camel }, + { TR_KEY_file_count, TR_KEY_file_count_kebab }, + { TR_KEY_file_stats, TR_KEY_file_stats_camel }, + { TR_KEY_files_added, TR_KEY_files_added_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_files_unwanted, TR_KEY_files_unwanted_kebab }, + { TR_KEY_files_wanted, TR_KEY_files_wanted_kebab }, + { TR_KEY_flag_str, TR_KEY_flag_str_camel }, + { TR_KEY_from_cache, TR_KEY_from_cache_camel }, + { TR_KEY_from_dht, TR_KEY_from_dht_camel }, + { TR_KEY_from_incoming, TR_KEY_from_incoming_camel }, + { TR_KEY_from_lpd, TR_KEY_from_lpd_camel }, + { TR_KEY_from_ltep, TR_KEY_from_ltep_camel }, + { TR_KEY_from_pex, TR_KEY_from_pex_camel }, + { TR_KEY_from_tracker, TR_KEY_from_tracker_camel }, + { TR_KEY_has_announced, TR_KEY_has_announced_camel }, + { TR_KEY_has_scraped, TR_KEY_has_scraped_camel }, + { TR_KEY_hash_string, TR_KEY_hash_string_camel }, + { TR_KEY_have_unchecked, TR_KEY_have_unchecked_camel }, + { TR_KEY_have_valid, TR_KEY_have_valid_camel }, + { TR_KEY_honors_session_limits, TR_KEY_honors_session_limits_camel }, + { 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 }, + { TR_KEY_incomplete_dir_enabled, TR_KEY_incomplete_dir_enabled_kebab }, + { TR_KEY_is_backup, TR_KEY_is_backup_camel }, + { TR_KEY_is_downloading_from, TR_KEY_is_downloading_from_camel }, + { TR_KEY_is_encrypted, TR_KEY_is_encrypted_camel }, + { TR_KEY_is_finished, TR_KEY_is_finished_camel }, + { TR_KEY_is_incoming, TR_KEY_is_incoming_camel }, + { TR_KEY_is_private, TR_KEY_is_private_camel }, + { TR_KEY_is_stalled, TR_KEY_is_stalled_camel }, + { TR_KEY_is_uploading_to, TR_KEY_is_uploading_to_camel }, + { TR_KEY_is_utp, TR_KEY_is_utp_camel }, + { TR_KEY_last_announce_peer_count, TR_KEY_last_announce_peer_count_camel }, + { TR_KEY_last_announce_result, TR_KEY_last_announce_result_camel }, + { TR_KEY_last_announce_start_time, TR_KEY_last_announce_start_time_camel }, + { TR_KEY_last_announce_succeeded, TR_KEY_last_announce_succeeded_camel }, + { TR_KEY_last_announce_time, TR_KEY_last_announce_time_camel }, + { TR_KEY_last_announce_timed_out, TR_KEY_last_announce_timed_out_camel }, + { TR_KEY_last_scrape_result, TR_KEY_last_scrape_result_camel }, + { TR_KEY_last_scrape_start_time, TR_KEY_last_scrape_start_time_camel }, + { TR_KEY_last_scrape_succeeded, TR_KEY_last_scrape_succeeded_camel }, + { TR_KEY_last_scrape_time, TR_KEY_last_scrape_time_camel }, + { TR_KEY_last_scrape_timed_out, TR_KEY_last_scrape_timed_out_camel }, + { TR_KEY_leecher_count, TR_KEY_leecher_count_camel }, + { TR_KEY_left_until_done, TR_KEY_left_until_done_camel }, + { TR_KEY_lpd_enabled, TR_KEY_lpd_enabled_kebab }, + { TR_KEY_magnet_link, TR_KEY_magnet_link_camel }, + { TR_KEY_manual_announce_time, TR_KEY_manual_announce_time_camel }, + { TR_KEY_max_connected_peers, TR_KEY_max_connected_peers_camel }, + { TR_KEY_memory_bytes, TR_KEY_memory_bytes_kebab }, + { TR_KEY_memory_units, TR_KEY_memory_units_kebab }, + { TR_KEY_metadata_percent_complete, TR_KEY_metadata_percent_complete_camel }, + { TR_KEY_next_announce_time, TR_KEY_next_announce_time_camel }, + { TR_KEY_next_scrape_time, TR_KEY_next_scrape_time_camel }, + { TR_KEY_paused_torrent_count, TR_KEY_paused_torrent_count_camel }, + { TR_KEY_peer_is_choked, TR_KEY_peer_is_choked_camel }, + { TR_KEY_peer_is_interested, TR_KEY_peer_is_interested_camel }, + { TR_KEY_peer_limit, TR_KEY_peer_limit_kebab }, + { TR_KEY_peer_limit_global, TR_KEY_peer_limit_global_kebab }, + { TR_KEY_peer_limit_per_torrent, TR_KEY_peer_limit_per_torrent_kebab }, + { TR_KEY_peer_port, TR_KEY_peer_port_kebab }, + { TR_KEY_peer_port_random_on_start, TR_KEY_peer_port_random_on_start_kebab }, + { TR_KEY_peers_connected, TR_KEY_peers_connected_camel }, + { TR_KEY_peers_from, TR_KEY_peers_from_camel }, + { TR_KEY_peers_getting_from_us, TR_KEY_peers_getting_from_us_camel }, + { TR_KEY_peers_sending_to_us, TR_KEY_peers_sending_to_us_camel }, + { TR_KEY_percent_complete, TR_KEY_percent_complete_camel }, + { TR_KEY_percent_done, TR_KEY_percent_done_camel }, + { TR_KEY_pex_enabled, TR_KEY_pex_enabled_kebab }, + { TR_KEY_piece_count, TR_KEY_piece_count_camel }, + { TR_KEY_piece_size, TR_KEY_piece_size_camel }, + { TR_KEY_port_forwarding_enabled, TR_KEY_port_forwarding_enabled_kebab }, + { TR_KEY_port_is_open, TR_KEY_port_is_open_kebab }, + { TR_KEY_primary_mime_type, TR_KEY_primary_mime_type_kebab }, + { TR_KEY_priority_high, TR_KEY_priority_high_kebab }, + { TR_KEY_priority_low, TR_KEY_priority_low_kebab }, + { TR_KEY_priority_normal, TR_KEY_priority_normal_kebab }, + { TR_KEY_queue_position, TR_KEY_queue_position_camel }, + { TR_KEY_queue_stalled_enabled, TR_KEY_queue_stalled_enabled_kebab }, + { TR_KEY_queue_stalled_minutes, TR_KEY_queue_stalled_minutes_kebab }, + { TR_KEY_rate_download, TR_KEY_rate_download_camel }, + { TR_KEY_rate_to_client, TR_KEY_rate_to_client_camel }, + { TR_KEY_rate_to_peer, TR_KEY_rate_to_peer_camel }, + { TR_KEY_rate_upload, TR_KEY_rate_upload_camel }, + { TR_KEY_recently_active, TR_KEY_recently_active_kebab }, + { TR_KEY_recheck_progress, TR_KEY_recheck_progress_camel }, + { TR_KEY_rename_partial_files, TR_KEY_rename_partial_files_kebab }, + { TR_KEY_rpc_host_whitelist, TR_KEY_rpc_host_whitelist_kebab }, + { TR_KEY_rpc_host_whitelist_enabled, TR_KEY_rpc_host_whitelist_enabled_kebab }, + { TR_KEY_rpc_version, TR_KEY_rpc_version_kebab }, + { TR_KEY_rpc_version_minimum, TR_KEY_rpc_version_minimum_kebab }, + { TR_KEY_rpc_version_semver, TR_KEY_rpc_version_semver_kebab }, + { TR_KEY_scrape_state, TR_KEY_scrape_state_camel }, + { TR_KEY_script_torrent_added_enabled, TR_KEY_script_torrent_added_enabled_kebab }, + { TR_KEY_script_torrent_added_filename, TR_KEY_script_torrent_added_filename_kebab }, + { TR_KEY_script_torrent_done_enabled, TR_KEY_script_torrent_done_enabled_kebab }, + { TR_KEY_script_torrent_done_filename, TR_KEY_script_torrent_done_filename_kebab }, + { TR_KEY_script_torrent_done_seeding_enabled, TR_KEY_script_torrent_done_seeding_enabled_kebab }, + { TR_KEY_script_torrent_done_seeding_filename, TR_KEY_script_torrent_done_seeding_filename_kebab }, + { TR_KEY_seconds_active, TR_KEY_seconds_active_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_seconds_downloading, TR_KEY_seconds_downloading_camel }, + { TR_KEY_seconds_seeding, TR_KEY_seconds_seeding_camel }, + { TR_KEY_seed_idle_limit, TR_KEY_seed_idle_limit_camel }, + { TR_KEY_seed_idle_mode, TR_KEY_seed_idle_mode_camel }, + { TR_KEY_seed_queue_enabled, TR_KEY_seed_queue_enabled_kebab }, + { TR_KEY_seed_queue_size, TR_KEY_seed_queue_size_kebab }, + { TR_KEY_seed_ratio_limit, TR_KEY_seed_ratio_limit_camel }, + { TR_KEY_seed_ratio_limited, TR_KEY_seed_ratio_limited_camel }, + { TR_KEY_seed_ratio_mode, TR_KEY_seed_ratio_mode_camel }, + { TR_KEY_seeder_count, TR_KEY_seeder_count_camel }, + { TR_KEY_session_count, TR_KEY_session_count_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_session_id, TR_KEY_session_id_kebab }, + { TR_KEY_size_bytes, TR_KEY_size_bytes_kebab }, + { TR_KEY_size_units, TR_KEY_size_units_kebab }, + { TR_KEY_size_when_done, TR_KEY_size_when_done_camel }, + { TR_KEY_speed_bytes, TR_KEY_speed_bytes_kebab }, + { 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 }, + { TR_KEY_speed_limit_up_enabled, TR_KEY_speed_limit_up_enabled_kebab }, + { TR_KEY_speed_units, TR_KEY_speed_units_kebab }, + { TR_KEY_start_added_torrents, TR_KEY_start_added_torrents_kebab }, + { TR_KEY_start_date, TR_KEY_start_date_camel }, + { TR_KEY_tcp_enabled, TR_KEY_tcp_enabled_kebab }, + { TR_KEY_torrent_added, TR_KEY_torrent_added_kebab }, + { TR_KEY_torrent_count, TR_KEY_torrent_count_camel }, + { TR_KEY_torrent_duplicate, TR_KEY_torrent_duplicate_kebab }, + { TR_KEY_torrent_file, TR_KEY_torrent_file_camel }, + { TR_KEY_total_size, TR_KEY_total_size_camel }, + { TR_KEY_tracker_add, TR_KEY_tracker_add_camel }, + { TR_KEY_tracker_list, TR_KEY_tracker_list_camel }, + { TR_KEY_tracker_remove, TR_KEY_tracker_remove_camel }, + { TR_KEY_tracker_replace, TR_KEY_tracker_replace_camel }, + { TR_KEY_tracker_stats, TR_KEY_tracker_stats_camel }, + { TR_KEY_trash_original_torrent_files, TR_KEY_trash_original_torrent_files_kebab }, + { TR_KEY_upload_limit, TR_KEY_upload_limit_camel }, + { TR_KEY_upload_limited, TR_KEY_upload_limited_camel }, + { TR_KEY_upload_ratio, TR_KEY_upload_ratio_camel }, + { TR_KEY_upload_speed, TR_KEY_upload_speed_camel }, + { TR_KEY_uploaded_bytes, TR_KEY_uploaded_bytes_camel }, // TODO(ckerr) legacy duplicate + { TR_KEY_uploaded_ever, TR_KEY_uploaded_ever_camel }, + { TR_KEY_utp_enabled, TR_KEY_utp_enabled_kebab }, + { TR_KEY_webseeds_sending_to_us, TR_KEY_webseeds_sending_to_us_camel }, + { TR_KEY_blocklist_update, TR_KEY_blocklist_update_kebab }, + { TR_KEY_free_space, TR_KEY_free_space_kebab }, + { TR_KEY_group_get, TR_KEY_group_get_kebab }, + { TR_KEY_group_set, TR_KEY_group_set_kebab }, + { TR_KEY_port_test, TR_KEY_port_test_kebab }, + { TR_KEY_queue_move_bottom, TR_KEY_queue_move_bottom_kebab }, + { TR_KEY_queue_move_down, TR_KEY_queue_move_down_kebab }, + { TR_KEY_queue_move_top, TR_KEY_queue_move_top_kebab }, + { TR_KEY_queue_move_up, TR_KEY_queue_move_up_kebab }, + { TR_KEY_session_close, TR_KEY_session_close_kebab }, + { TR_KEY_session_get, TR_KEY_session_get_kebab }, + { TR_KEY_session_set, TR_KEY_session_set_kebab }, + { TR_KEY_session_stats, TR_KEY_session_stats_kebab }, + { TR_KEY_torrent_add, TR_KEY_torrent_add_kebab }, + { TR_KEY_torrent_get, TR_KEY_torrent_get_kebab }, + { TR_KEY_torrent_reannounce, TR_KEY_torrent_reannounce_kebab }, + { TR_KEY_torrent_remove, TR_KEY_torrent_remove_kebab }, + { TR_KEY_torrent_rename_path, TR_KEY_torrent_rename_path_kebab }, + { TR_KEY_torrent_set, TR_KEY_torrent_set_kebab }, + { TR_KEY_torrent_set_location, TR_KEY_torrent_set_location_kebab }, + { TR_KEY_torrent_start, TR_KEY_torrent_start_kebab }, + { TR_KEY_torrent_start_now, TR_KEY_torrent_start_now_kebab }, + { TR_KEY_torrent_stop, TR_KEY_torrent_stop_kebab }, + { TR_KEY_torrent_verify, TR_KEY_torrent_verify_kebab }, +} }; + +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_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 }, + { TR_KEY_alt_speed_time_day, TR_KEY_alt_speed_time_day_kebab }, + { TR_KEY_alt_speed_time_enabled, TR_KEY_alt_speed_time_enabled_kebab }, + { TR_KEY_alt_speed_time_end, TR_KEY_alt_speed_time_end_kebab }, + { TR_KEY_alt_speed_up, TR_KEY_alt_speed_up_kebab }, + { TR_KEY_announce_ip, TR_KEY_announce_ip_kebab }, + { 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_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 }, + { TR_KEY_blocklist_enabled, TR_KEY_blocklist_enabled_kebab }, + { TR_KEY_blocklist_updates_enabled, TR_KEY_blocklist_updates_enabled_kebab }, + { TR_KEY_blocklist_url, TR_KEY_blocklist_url_kebab }, + { TR_KEY_cache_size_mb, TR_KEY_cache_size_mb_kebab }, + { TR_KEY_compact_view, TR_KEY_compact_view_kebab }, + { TR_KEY_default_trackers, TR_KEY_default_trackers_kebab }, + { 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_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 }, // TODO(ckerr) legacy duplicate + { TR_KEY_downloading_time_seconds, TR_KEY_downloading_time_seconds_kebab }, + { TR_KEY_files_added, TR_KEY_files_added_kebab }, // TODO(ckerr) legacy duplicate + { 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_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 }, + { TR_KEY_incomplete_dir_enabled, TR_KEY_incomplete_dir_enabled_kebab }, + { TR_KEY_inhibit_desktop_hibernation, TR_KEY_inhibit_desktop_hibernation_kebab }, + { TR_KEY_lpd_enabled, TR_KEY_lpd_enabled_kebab }, + { TR_KEY_main_window_height, TR_KEY_main_window_height_kebab }, + { TR_KEY_main_window_is_maximized, TR_KEY_main_window_is_maximized_kebab }, + { TR_KEY_main_window_layout_order, TR_KEY_main_window_layout_order_kebab }, + { 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_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 }, + { TR_KEY_peer_limit_global, TR_KEY_peer_limit_global_kebab }, + { TR_KEY_peer_limit_per_torrent, TR_KEY_peer_limit_per_torrent_kebab }, + { TR_KEY_peer_port, TR_KEY_peer_port_kebab }, + { TR_KEY_peer_port_random_high, TR_KEY_peer_port_random_high_kebab }, + { 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_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 }, + { TR_KEY_queue_stalled_enabled, TR_KEY_queue_stalled_enabled_kebab }, + { 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_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 }, + { TR_KEY_remote_session_https, TR_KEY_remote_session_https_kebab }, + { TR_KEY_remote_session_password, TR_KEY_remote_session_password_kebab }, + { TR_KEY_remote_session_port, TR_KEY_remote_session_port_kebab }, + { TR_KEY_remote_session_requires_authentication, TR_KEY_remote_session_requres_authentication_kebab }, + { TR_KEY_remote_session_username, TR_KEY_remote_session_username_kebab }, + { TR_KEY_rename_partial_files, TR_KEY_rename_partial_files_kebab }, + { TR_KEY_rpc_authentication_required, TR_KEY_rpc_authentication_required_kebab }, + { TR_KEY_rpc_bind_address, TR_KEY_rpc_bind_address_kebab }, + { TR_KEY_rpc_enabled, TR_KEY_rpc_enabled_kebab }, + { TR_KEY_rpc_host_whitelist, TR_KEY_rpc_host_whitelist_kebab }, + { TR_KEY_rpc_host_whitelist_enabled, TR_KEY_rpc_host_whitelist_enabled_kebab }, + { TR_KEY_rpc_password, TR_KEY_rpc_password_kebab }, + { TR_KEY_rpc_port, TR_KEY_rpc_port_kebab }, + { TR_KEY_rpc_socket_mode, TR_KEY_rpc_socket_mode_kebab }, + { TR_KEY_rpc_url, TR_KEY_rpc_url_kebab }, + { TR_KEY_rpc_username, TR_KEY_rpc_username_kebab }, + { TR_KEY_rpc_whitelist, TR_KEY_rpc_whitelist_kebab }, + { TR_KEY_rpc_whitelist_enabled, TR_KEY_rpc_whitelist_enabled_kebab }, + { TR_KEY_scrape_paused_torrents_enabled, TR_KEY_scrape_paused_torrents_enabled_kebab }, + { TR_KEY_script_torrent_added_enabled, TR_KEY_script_torrent_added_enabled_kebab }, + { TR_KEY_script_torrent_added_filename, TR_KEY_script_torrent_added_filename_kebab }, + { TR_KEY_script_torrent_done_enabled, TR_KEY_script_torrent_done_enabled_kebab }, + { TR_KEY_script_torrent_done_filename, TR_KEY_script_torrent_done_filename_kebab }, + { TR_KEY_script_torrent_done_seeding_enabled, TR_KEY_script_torrent_done_seeding_enabled_kebab }, + { TR_KEY_script_torrent_done_seeding_filename, TR_KEY_script_torrent_done_seeding_filename_kebab }, + { TR_KEY_seconds_active, TR_KEY_seconds_active_kebab }, // TODO(ckerr) legacy duplicate + { 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_session_count, TR_KEY_session_count_kebab }, // TODO(ckerr) legacy duplicate + { TR_KEY_show_backup_trackers, TR_KEY_show_backup_trackers_kebab }, + { TR_KEY_show_extra_peer_details, TR_KEY_show_extra_peer_details_kebab }, + { TR_KEY_show_filterbar, TR_KEY_show_filterbar_kebab }, + { TR_KEY_show_notification_area_icon, TR_KEY_show_notification_area_icon_kebab }, + { TR_KEY_show_options_window, TR_KEY_show_options_window_kebab }, + { TR_KEY_show_statusbar, TR_KEY_show_statusbar_kebab }, + { TR_KEY_show_toolbar, TR_KEY_show_toolbar_kebab }, + { TR_KEY_show_tracker_scrapes, TR_KEY_show_tracker_scrapes_kebab }, + { 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_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 }, + { TR_KEY_speed_limit_up_enabled, TR_KEY_speed_limit_up_enabled_kebab }, + { TR_KEY_start_added_torrents, TR_KEY_start_added_torrents_kebab }, + { TR_KEY_start_minimized, TR_KEY_start_minimized_kebab }, + { TR_KEY_statusbar_stats, TR_KEY_statusbar_stats_kebab }, + { TR_KEY_tcp_enabled, TR_KEY_tcp_enabled_kebab }, + { TR_KEY_time_checked, TR_KEY_time_checked_kebab }, + { TR_KEY_torrent_added_notification_enabled, TR_KEY_torrent_added_notification_enabled_kebab }, + { TR_KEY_torrent_added_verify_mode, TR_KEY_torrent_added_verify_mode_kebab }, + { TR_KEY_torrent_complete_notification_enabled, TR_KEY_torrent_complete_notification_enabled_kebab }, + { TR_KEY_torrent_complete_sound_command, TR_KEY_torrent_complete_sound_command_kebab }, + { TR_KEY_torrent_complete_sound_enabled, TR_KEY_torrent_complete_sound_enabled_kebab }, + { TR_KEY_trash_can_enabled, TR_KEY_trash_can_enabled_kebab }, + { 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 }, // TODO(ckerr) legacy duplicate + { 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_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 }, + { TR_KEY_watch_dir_force_generic, TR_KEY_watch_dir_force_generic_kebab }, +} }; + +auto constexpr MethodNotFoundLegacyErrmsg = std::string_view{ "no method name" }; + +[[nodiscard]] constexpr tr_quark convert_key(tr_quark const src, Style const style, bool const is_rpc) +{ + if (style == Style::Tr5) + { + for (auto const [current, legacy] : RpcKeys) + { + if (src == current || src == legacy) + { + return current; + } + } + for (auto const [current, legacy] : SessionKeys) + { + if (src == current || src == legacy) + { + return current; + } + } + } + else if (is_rpc) // legacy RPC + { + for (auto const [current, legacy] : RpcKeys) + { + if (src == current || src == legacy) + { + return legacy; + } + } + } + else // legacy datafiles + { + for (auto const [current, legacy] : SessionKeys) + { + if (src == current || src == legacy) + { + return legacy; + } + } + } + + return src; +} + +/** + * Guess the error code from a legacy RPC response message. + * + * Use case: a modern Transmission client that parses jsonrpc needs to + * connect to a legacy Transmission RPC server. + * + * We're not going to always get this right: There are some edge cases + * where legacy Transmission's error messages are unhelpful. + */ +[[nodiscard]] JsonRpc::Error::Code guess_error_code(std::string_view const result_in) +{ + using namespace JsonRpc; + + auto const result = tr_strlower(result_in); + + if (result == "success") + { + return Error::SUCCESS; + } + + static auto constexpr Phrases = std::array, 14U>{ { + { "absolute", Error::PATH_NOT_ABSOLUTE }, + { "couldn't fetch blocklist", Error::HTTP_ERROR }, + { "couldn't save", Error::SYSTEM_ERROR }, + { "couldn't test port", Error::HTTP_ERROR }, + { "file index out of range", Error::FILE_IDX_OOR }, + { "invalid ip protocol", Error::INVALID_PARAMS }, + { "invalid or corrupt torrent", Error::CORRUPT_TORRENT }, + { "invalid tracker list", Error::INVALID_TRACKER_LIST }, + { "labels cannot", Error::INVALID_PARAMS }, + { "no filename or metainfo specified", Error::INVALID_PARAMS }, + { "no location", Error::INVALID_PARAMS }, + { "torrent-rename-path requires 1 torrent", Error::INVALID_PARAMS }, + { "unrecognized info", Error::UNRECOGNIZED_INFO }, + { MethodNotFoundLegacyErrmsg, Error::METHOD_NOT_FOUND }, + } }; + + for (auto const& [substr, code] : Phrases) + { + if (tr_strv_contains(result, substr)) + { + return code; + } + } + + return {}; +} + +struct CloneState +{ + api_compat::Style style = {}; + bool is_rpc = false; + bool convert_strings = false; + bool is_torrent = false; + bool is_free_space_response = false; +}; + +[[nodiscard]] tr_variant convert_impl(tr_variant const& self, CloneState& state) +{ + struct Visitor + { + tr_variant operator()(std::monostate const& /*unused*/) const + { + return tr_variant{}; + } + + tr_variant operator()(std::nullptr_t const& /*unused*/) const + { + return tr_variant{ nullptr }; + } + + tr_variant operator()(bool const& val) const + { + return tr_variant{ val }; + } + + tr_variant operator()(int64_t const& val) const + { + return tr_variant{ val }; + } + + tr_variant operator()(double const& val) const + { + return tr_variant{ val }; + } + + tr_variant operator()(std::string_view const& sv) const + { + if (!state_.convert_strings) + { + return sv; + } + + auto const lookup = tr_quark_lookup(sv); + if (!lookup) + { + return sv; + } + + auto key = *lookup; + + // crazy case 1: downloadDir in torrent-get, download-dir in session-get + if (state_.is_rpc && + (key == TR_KEY_download_dir_camel || key == TR_KEY_download_dir_kebab || key == TR_KEY_download_dir)) + { + if (state_.style == Style::Tr5) + { + key = TR_KEY_download_dir; + } + else + { + key = state_.is_torrent ? TR_KEY_download_dir_camel : TR_KEY_download_dir_kebab; + } + } + else + { + key = convert_key(key, state_.style, state_.is_rpc); + } + + return tr_variant::unmanaged_string(key); + } + + tr_variant operator()(tr_variant::Vector const& src) + { + auto tgt = tr_variant::Vector(); + tgt.reserve(std::size(src)); + for (auto const& val : src) + { + tgt.emplace_back(convert_impl(val, state_)); + } + return tgt; + } + + tr_variant operator()(tr_variant::Map const& src) + { + auto tgt = tr_variant::Map{ std::size(src) }; + for (auto const& [key, val] : src) + { + auto const pop = state_.convert_strings; + auto new_key = convert_key(key, state_.style, state_.is_rpc); + auto const special = + (state_.is_rpc && + (new_key == TR_KEY_method || new_key == TR_KEY_fields || new_key == TR_KEY_ids || + new_key == TR_KEY_torrents)); + // TODO(ckerr): replace `new_key == TR_KEY_TORRENTS` on previous line with logic to turn on convert + // if it's an array inside an array val whose key was `torrents`. + // This is for the edge case of table mode: `torrents : [ [ 'key1', 'key2' ], [ ... ] ]` + state_.convert_strings |= special; + + // Crazy case: total_size in free-space, totalSize in torrent-get + if (state_.is_free_space_response && new_key == TR_KEY_total_size_camel) + { + new_key = TR_KEY_total_size; + } + + tgt.insert_or_assign(new_key, convert_impl(val, state_)); + state_.convert_strings = pop; + } + return tgt; + } + + CloneState& state_; + }; + + return self.visit(Visitor{ state }); +} +} // namespace + +tr_variant convert(tr_variant const& src, Style const tgt_style) +{ + // TODO: yes I know this method is ugly rn. + // I've just been trying to get the tests passing. + + auto const* const src_top = src.get_if(); + + // if it's not a Map, just clone it + if (src_top == nullptr) + { + return src.clone(); + } + + auto const is_request = src_top->contains(TR_KEY_method); + + auto const was_jsonrpc = src_top->contains(TR_KEY_jsonrpc); + auto const was_legacy = !was_jsonrpc; + auto const was_jsonrpc_response = was_jsonrpc && (src_top->contains(TR_KEY_result) || src_top->contains(TR_KEY_error)); + auto const was_legacy_response = was_legacy && src_top->contains(TR_KEY_result); + auto const is_response = was_jsonrpc_response || was_legacy_response; + auto const is_rpc = is_request || is_response; + + auto state = CloneState{}; + state.style = tgt_style; + state.is_rpc = is_rpc; + + auto const is_success = is_response && + (was_jsonrpc_response ? src_top->contains(TR_KEY_result) : + src_top->value_if(TR_KEY_result).value_or("") == "success"); + + if (auto const method = src_top->value_if(TR_KEY_method)) + { + auto const key = tr_quark_convert(tr_quark_new(*method)); + state.is_torrent = key == TR_KEY_torrent_get || key == TR_KEY_torrent_set; + } + + if (is_response) + { + if (auto const* const args = src_top->find_if(was_jsonrpc ? TR_KEY_result : TR_KEY_arguments)) + { + state.is_free_space_response = args->contains(TR_KEY_path) && + args->contains(was_jsonrpc ? TR_KEY_size_bytes : TR_KEY_size_bytes_kebab); + } + } + + auto ret = convert_impl(src, state); + + auto* const tgt_top = ret.get_if(); + + // jsonrpc <-> legacy rpc conversion + if (is_rpc) + { + auto const is_jsonrpc = tgt_style == Style::Tr5; + auto const is_legacy = tgt_style != Style::Tr5; + + // - use `jsonrpc` in jsonrpc, but not in legacy + // - use `id` in jsonrpc; use `tag` in legacy + if (is_jsonrpc) + { + tgt_top->try_emplace(TR_KEY_jsonrpc, tr_variant::unmanaged_string(JsonRpc::Version)); + tgt_top->replace_key(TR_KEY_tag, TR_KEY_id); + } + else + { + tgt_top->erase(TR_KEY_jsonrpc); + tgt_top->replace_key(TR_KEY_id, TR_KEY_tag); + } + + if (is_response && is_legacy && is_success && was_jsonrpc) + { + // in legacy messages: + // - move `result` to `arguments` + // - add `result: "success"` + tgt_top->replace_key(TR_KEY_result, TR_KEY_arguments); + tgt_top->try_emplace(TR_KEY_result, tr_variant::unmanaged_string("success")); + } + + if (is_response && is_legacy && !is_success) + { + // in legacy error responses: + // - copy `error.data.error_string` to `result` + // - remove `error` object + // - add an empty `arguments` object + if (auto* error_ptr = tgt_top->find_if(TR_KEY_error)) + { + // move the `error` object before memory reallocations invalidate the pointer + auto error = std::move(*error_ptr); + tgt_top->erase(TR_KEY_error); + + // crazy case: current and legacy METHOD_NOT_FOUND has different error messages + if (auto const code = error.value_if(TR_KEY_code); code && *code == JsonRpc::Error::METHOD_NOT_FOUND) + { + tgt_top->try_emplace(TR_KEY_result, tr_variant::unmanaged_string(MethodNotFoundLegacyErrmsg)); + } + + if (auto* data = error.find_if(TR_KEY_data)) + { + if (auto const* errmsg = data->find_if(TR_KEY_error_string_camel)) + { + tgt_top->try_emplace(TR_KEY_result, *errmsg); + } + + if (auto const result = data->find(TR_KEY_result); result != std::end(*data)) + { + tgt_top->try_emplace(TR_KEY_arguments, std::move(result->second)); + } + } + + if (auto const* errmsg = error.find_if(TR_KEY_message)) + { + tgt_top->try_emplace(TR_KEY_result, *errmsg); + } + } + + tgt_top->try_emplace(TR_KEY_arguments, tr_variant::make_map()); + } + + if (is_response && is_jsonrpc && is_success && was_legacy) + { + tgt_top->erase(TR_KEY_result); + tgt_top->replace_key(TR_KEY_arguments, TR_KEY_result); + } + + if (is_response && is_jsonrpc && !is_success && was_legacy) + { + // in jsonrpc error message: + // - copy `result` to `error.data.error_string` + // - ensure `error` object exists and is well-formatted + // - remove `result` + auto const errstr = tgt_top->value_if(TR_KEY_result).value_or("unknown error"); + auto error = tr_variant::Map{ 3U }; + auto data = tr_variant::Map{ 2U }; + auto const code = guess_error_code(errstr); + auto const errmsg = JsonRpc::Error::to_string(code); + error.try_emplace(TR_KEY_code, code); + error.try_emplace(TR_KEY_message, errmsg); + // crazy case: current and legacy METHOD_NOT_FOUND has different error messages + if (errstr != errmsg && errstr != MethodNotFoundLegacyErrmsg) + { + data.try_emplace(TR_KEY_error_string, errstr); + } + tgt_top->erase(TR_KEY_result); + + if (auto const args_it = tgt_top->find(TR_KEY_arguments); args_it != std::end(*tgt_top)) + { + auto args = std::move(args_it->second); + tgt_top->erase(TR_KEY_arguments); + + if (auto const* args_map = args.get_if(); args_map != nullptr && !std::empty(*args_map)) + { + data.try_emplace(TR_KEY_result, std::move(args)); + } + } + + if (!std::empty(data)) + { + error.try_emplace(TR_KEY_data, std::move(data)); + } + tgt_top->try_emplace(TR_KEY_error, std::move(error)); + } + + if (is_request && is_jsonrpc) + { + tgt_top->replace_key(TR_KEY_arguments, TR_KEY_params); + } + + if (is_request && is_legacy) + { + tgt_top->replace_key(TR_KEY_params, TR_KEY_arguments); + } + } + + return ret; +} + +[[nodiscard]] Style get_export_settings_style() +{ + // TODO: change default to Tr5 in transmission 5.0.0-beta.1 + static auto const style = tr_env_get_string("TR_SAVE_VERSION_FORMAT", "4") == "5" ? Style::Tr5 : Style::Tr4; + return style; +} + +[[nodiscard]] tr_variant convert_outgoing_data(tr_variant const& src) +{ + return convert(src, get_export_settings_style()); +} + +[[nodiscard]] tr_variant convert_incoming_data(tr_variant const& src) +{ + return convert(src, Style::Tr5); +} +} // namespace libtransmission::api_compat + +tr_quark tr_quark_convert(tr_quark const quark) +{ + using namespace libtransmission::api_compat; + return convert_key(quark, Style::Tr5, false /*ignored for Style::Tr5*/); +} diff --git a/libtransmission/api-compat.h b/libtransmission/api-compat.h new file mode 100644 index 000000000..1dda04182 --- /dev/null +++ b/libtransmission/api-compat.h @@ -0,0 +1,36 @@ +// This file Copyright © Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#pragma once + +#include // uint8_t + +#include "libtransmission/quark.h" + +struct tr_variant; + +namespace libtransmission::api_compat +{ +enum class Style : uint8_t +{ + Tr4, // bespoke RPC, mixed-case keys, + Tr5, // jsonrpc, all snake_case keys +}; + +[[nodiscard]] tr_variant convert(tr_variant const& src, Style tgt_style); + +[[nodiscard]] Style get_export_settings_style(); + +[[nodiscard]] tr_variant convert_incoming_data(tr_variant const& src); +[[nodiscard]] tr_variant convert_outgoing_data(tr_variant const& src); + +} // namespace libtransmission::api_compat + +/** + * Get the replacement quark from old deprecated quarks. + * + * Note: Temporary shim just for the transition period to snake_case. + */ +[[nodiscard]] tr_quark tr_quark_convert(tr_quark quark); diff --git a/libtransmission/quark.cc b/libtransmission/quark.cc index 023e8321f..2bbd8934f 100644 --- a/libtransmission/quark.cc +++ b/libtransmission/quark.cc @@ -795,324 +795,3 @@ std::string_view tr_quark_get_string_view(tr_quark q) { return q < TR_N_KEYS ? MyStatic[q] : my_runtime[q - TR_N_KEYS]; } - -tr_quark tr_quark_convert(tr_quark const q) -{ - // clang-format off - switch (q) - { - case TR_KEY_active_torrent_count_camel: return TR_KEY_active_torrent_count; - case TR_KEY_activity_date_camel: - case TR_KEY_activity_date_kebab: - return TR_KEY_activity_date; - case TR_KEY_added_date_camel: - case TR_KEY_added_date_kebab: - return TR_KEY_added_date; - case TR_KEY_alt_speed_down_kebab: return TR_KEY_alt_speed_down; - case TR_KEY_alt_speed_enabled_kebab: return TR_KEY_alt_speed_enabled; - case TR_KEY_alt_speed_time_begin_kebab: return TR_KEY_alt_speed_time_begin; - case TR_KEY_alt_speed_time_day_kebab: return TR_KEY_alt_speed_time_day; - case TR_KEY_alt_speed_time_enabled_kebab: return TR_KEY_alt_speed_time_enabled; - case TR_KEY_alt_speed_time_end_kebab: return TR_KEY_alt_speed_time_end; - case TR_KEY_alt_speed_up_kebab: return TR_KEY_alt_speed_up; - case TR_KEY_announce_ip_kebab: return TR_KEY_announce_ip; - case TR_KEY_announce_ip_enabled_kebab: return TR_KEY_announce_ip_enabled; - case TR_KEY_announce_state_camel: return TR_KEY_announce_state; - case TR_KEY_anti_brute_force_enabled_kebab: return TR_KEY_anti_brute_force_enabled; - case TR_KEY_anti_brute_force_threshold_kebab: return TR_KEY_anti_brute_force_threshold; - case TR_KEY_bandwidth_priority_camel: - case TR_KEY_bandwidth_priority_kebab: - return TR_KEY_bandwidth_priority; - case TR_KEY_bind_address_ipv4_kebab: return TR_KEY_bind_address_ipv4; - case TR_KEY_bind_address_ipv6_kebab: return TR_KEY_bind_address_ipv6; - case TR_KEY_blocklist_date_kebab: return TR_KEY_blocklist_date; - case TR_KEY_blocklist_enabled_kebab: return TR_KEY_blocklist_enabled; - case TR_KEY_blocklist_size_kebab: return TR_KEY_blocklist_size; - case TR_KEY_blocklist_update_kebab: return TR_KEY_blocklist_update; - case TR_KEY_blocklist_updates_enabled_kebab: return TR_KEY_blocklist_updates_enabled; - case TR_KEY_blocklist_url_kebab: return TR_KEY_blocklist_url; - case TR_KEY_bytes_completed_camel: return TR_KEY_bytes_completed; - case TR_KEY_cache_size_mb_kebab: return TR_KEY_cache_size_mb; - case TR_KEY_client_is_choked_camel: return TR_KEY_client_is_choked; - case TR_KEY_client_is_interested_camel: return TR_KEY_client_is_interested; - case TR_KEY_client_name_camel: return TR_KEY_client_name; - case TR_KEY_compact_view_kebab: return TR_KEY_compact_view; - case TR_KEY_config_dir_kebab: return TR_KEY_config_dir; - case TR_KEY_corrupt_ever_camel: return TR_KEY_corrupt_ever; - case TR_KEY_cumulative_stats_kebab: return TR_KEY_cumulative_stats; - case TR_KEY_current_stats_kebab: return TR_KEY_current_stats; - case TR_KEY_date_created_camel: return TR_KEY_date_created; - case TR_KEY_default_trackers_kebab: return TR_KEY_default_trackers; - case TR_KEY_delete_local_data_kebab: return TR_KEY_delete_local_data; - case TR_KEY_desired_available_camel: return TR_KEY_desired_available; - case TR_KEY_details_window_height_kebab: return TR_KEY_details_window_height; - case TR_KEY_details_window_width_kebab: return TR_KEY_details_window_width; - case TR_KEY_dht_enabled_kebab: return TR_KEY_dht_enabled; - case TR_KEY_done_date_camel: - case TR_KEY_done_date_kebab: - return TR_KEY_done_date; - case TR_KEY_download_count_camel: return TR_KEY_download_count; - case TR_KEY_download_dir_camel: - case TR_KEY_download_dir_kebab: - return TR_KEY_download_dir; - case TR_KEY_download_dir_free_space_kebab: return TR_KEY_download_dir_free_space; - case TR_KEY_download_limit_camel: return TR_KEY_download_limit; - case TR_KEY_download_limited_camel: return TR_KEY_download_limited; - case TR_KEY_download_queue_enabled_kebab: return TR_KEY_download_queue_enabled; - case TR_KEY_download_queue_size_kebab: return TR_KEY_download_queue_size; - case TR_KEY_download_speed_camel: return TR_KEY_download_speed; - case TR_KEY_downloaded_bytes_camel: - case TR_KEY_downloaded_bytes_kebab: - return TR_KEY_downloaded_bytes; - case TR_KEY_downloaded_ever_camel: return TR_KEY_downloaded_ever; - case TR_KEY_downloading_time_seconds_kebab: return TR_KEY_downloading_time_seconds; - case TR_KEY_edit_date_camel: return TR_KEY_edit_date; - case TR_KEY_error_string_camel: return TR_KEY_error_string; - case TR_KEY_eta_idle_camel: return TR_KEY_eta_idle; - case TR_KEY_file_count_kebab: return TR_KEY_file_count; - case TR_KEY_file_stats_camel: return TR_KEY_file_stats; - case TR_KEY_files_added_camel: - case TR_KEY_files_added_kebab: - return TR_KEY_files_added; - case TR_KEY_files_unwanted_kebab: return TR_KEY_files_unwanted; - case TR_KEY_files_wanted_kebab: return TR_KEY_files_wanted; - case TR_KEY_filter_mode_kebab: return TR_KEY_filter_mode; - case TR_KEY_filter_text_kebab: return TR_KEY_filter_text; - case TR_KEY_filter_trackers_kebab: return TR_KEY_filter_trackers; - case TR_KEY_flag_str_camel: return TR_KEY_flag_str; - case TR_KEY_free_space_kebab: return TR_KEY_free_space; - case TR_KEY_from_cache_camel: return TR_KEY_from_cache; - case TR_KEY_from_dht_camel: return TR_KEY_from_dht; - case TR_KEY_from_incoming_camel: return TR_KEY_from_incoming; - case TR_KEY_from_lpd_camel: return TR_KEY_from_lpd; - case TR_KEY_from_ltep_camel: return TR_KEY_from_ltep; - case TR_KEY_from_pex_camel: return TR_KEY_from_pex; - case TR_KEY_from_tracker_camel: return TR_KEY_from_tracker; - case TR_KEY_group_get_kebab: return TR_KEY_group_get; - case TR_KEY_group_set_kebab: return TR_KEY_group_set; - case TR_KEY_hash_string_camel: return TR_KEY_hash_string; - case TR_KEY_has_announced_camel: return TR_KEY_has_announced; - case TR_KEY_has_scraped_camel: return TR_KEY_has_scraped; - case TR_KEY_have_unchecked_camel: return TR_KEY_have_unchecked; - case TR_KEY_have_valid_camel: return TR_KEY_have_valid; - case TR_KEY_honors_session_limits_camel: return TR_KEY_honors_session_limits; - case TR_KEY_idle_limit_kebab: return TR_KEY_idle_limit; - case TR_KEY_idle_mode_kebab: return TR_KEY_idle_mode; - case TR_KEY_idle_seeding_limit_kebab: return TR_KEY_idle_seeding_limit; - case TR_KEY_idle_seeding_limit_enabled_kebab: return TR_KEY_idle_seeding_limit_enabled; - case TR_KEY_incomplete_dir_kebab: return TR_KEY_incomplete_dir; - case TR_KEY_incomplete_dir_enabled_kebab: return TR_KEY_incomplete_dir_enabled; - case TR_KEY_inhibit_desktop_hibernation_kebab: return TR_KEY_inhibit_desktop_hibernation; - case TR_KEY_is_backup_camel: return TR_KEY_is_backup; - case TR_KEY_is_downloading_from_camel: return TR_KEY_is_downloading_from; - case TR_KEY_is_encrypted_camel: return TR_KEY_is_encrypted; - case TR_KEY_is_finished_camel: return TR_KEY_is_finished; - case TR_KEY_is_incoming_camel: return TR_KEY_is_incoming; - case TR_KEY_is_private_camel: return TR_KEY_is_private; - case TR_KEY_is_stalled_camel: return TR_KEY_is_stalled; - case TR_KEY_is_uploading_to_camel: return TR_KEY_is_uploading_to; - case TR_KEY_is_utp_camel: return TR_KEY_is_utp; - case TR_KEY_last_announce_peer_count_camel: return TR_KEY_last_announce_peer_count; - case TR_KEY_last_announce_result_camel: return TR_KEY_last_announce_result; - case TR_KEY_last_announce_start_time_camel: return TR_KEY_last_announce_start_time; - case TR_KEY_last_announce_succeeded_camel: return TR_KEY_last_announce_succeeded; - case TR_KEY_last_announce_time_camel: return TR_KEY_last_announce_time; - case TR_KEY_last_announce_timed_out_camel: return TR_KEY_last_announce_timed_out; - case TR_KEY_last_scrape_result_camel: return TR_KEY_last_scrape_result; - case TR_KEY_last_scrape_start_time_camel: return TR_KEY_last_scrape_start_time; - case TR_KEY_last_scrape_succeeded_camel: return TR_KEY_last_scrape_succeeded; - case TR_KEY_last_scrape_time_camel: return TR_KEY_last_scrape_time; - case TR_KEY_last_scrape_timed_out_camel: return TR_KEY_last_scrape_timed_out; - case TR_KEY_leecher_count_camel: return TR_KEY_leecher_count; - case TR_KEY_left_until_done_camel: return TR_KEY_left_until_done; - case TR_KEY_lpd_enabled_kebab: return TR_KEY_lpd_enabled; - case TR_KEY_magnet_link_camel: return TR_KEY_magnet_link; - case TR_KEY_main_window_height_kebab: return TR_KEY_main_window_height; - case TR_KEY_main_window_is_maximized_kebab: return TR_KEY_main_window_is_maximized; - case TR_KEY_main_window_layout_order_kebab: return TR_KEY_main_window_layout_order; - case TR_KEY_main_window_width_kebab: return TR_KEY_main_window_width; - case TR_KEY_main_window_x_kebab: return TR_KEY_main_window_x; - case TR_KEY_main_window_y_kebab: return TR_KEY_main_window_y; - case TR_KEY_manual_announce_time_camel: return TR_KEY_manual_announce_time; - case TR_KEY_max_connected_peers_camel: return TR_KEY_max_connected_peers; - case TR_KEY_max_peers_kebab: return TR_KEY_max_peers; - case TR_KEY_memory_bytes_kebab: return TR_KEY_memory_bytes; - case TR_KEY_memory_units_kebab: return TR_KEY_memory_units; - case TR_KEY_message_level_kebab: return TR_KEY_message_level; - case TR_KEY_metadata_percent_complete_camel: return TR_KEY_metadata_percent_complete; - case TR_KEY_next_announce_time_camel: return TR_KEY_next_announce_time; - case TR_KEY_next_scrape_time_camel: return TR_KEY_next_scrape_time; - case TR_KEY_open_dialog_dir_kebab: return TR_KEY_open_dialog_dir; - case TR_KEY_paused_torrent_count_camel: return TR_KEY_paused_torrent_count; - case TR_KEY_peer_congestion_algorithm_kebab: return TR_KEY_peer_congestion_algorithm; - case TR_KEY_peer_is_choked_camel: return TR_KEY_peer_is_choked; - case TR_KEY_peer_is_interested_camel: return TR_KEY_peer_is_interested; - case TR_KEY_peer_limit_kebab: return TR_KEY_peer_limit; - case TR_KEY_peer_limit_global_kebab: return TR_KEY_peer_limit_global; - case TR_KEY_peer_limit_per_torrent_kebab: return TR_KEY_peer_limit_per_torrent; - case TR_KEY_peer_port_kebab: return TR_KEY_peer_port; - case TR_KEY_peer_port_random_high_kebab: return TR_KEY_peer_port_random_high; - case TR_KEY_peer_port_random_low_kebab: return TR_KEY_peer_port_random_low; - case TR_KEY_peer_port_random_on_start_kebab: return TR_KEY_peer_port_random_on_start; - case TR_KEY_peer_socket_tos_kebab: return TR_KEY_peer_socket_tos; - case TR_KEY_peers2_6_kebab: return TR_KEY_peers2_6; - case TR_KEY_peers_connected_camel: return TR_KEY_peers_connected; - case TR_KEY_peers_from_camel: return TR_KEY_peers_from; - case TR_KEY_peers_getting_from_us_camel: return TR_KEY_peers_getting_from_us; - case TR_KEY_peers_sending_to_us_camel: return TR_KEY_peers_sending_to_us; - case TR_KEY_percent_complete_camel: return TR_KEY_percent_complete; - case TR_KEY_percent_done_camel: return TR_KEY_percent_done; - case TR_KEY_pex_enabled_kebab: return TR_KEY_pex_enabled; - case TR_KEY_piece_count_camel: return TR_KEY_piece_count; - case TR_KEY_piece_size_camel: return TR_KEY_piece_size; - case TR_KEY_port_forwarding_enabled_kebab: return TR_KEY_port_forwarding_enabled; - case TR_KEY_port_is_open_kebab: return TR_KEY_port_is_open; - case TR_KEY_port_test_kebab: return TR_KEY_port_test; - case TR_KEY_primary_mime_type_kebab: return TR_KEY_primary_mime_type; - case TR_KEY_priority_high_kebab: return TR_KEY_priority_high; - case TR_KEY_priority_low_kebab: return TR_KEY_priority_low; - case TR_KEY_priority_normal_kebab: return TR_KEY_priority_normal; - case TR_KEY_prompt_before_exit_kebab: return TR_KEY_prompt_before_exit; - case TR_KEY_queue_position_camel: return TR_KEY_queue_position; - case TR_KEY_queue_move_bottom_kebab: return TR_KEY_queue_move_bottom; - case TR_KEY_queue_move_down_kebab: return TR_KEY_queue_move_down; - case TR_KEY_queue_move_top_kebab: return TR_KEY_queue_move_top; - case TR_KEY_queue_move_up_kebab: return TR_KEY_queue_move_up; - case TR_KEY_queue_stalled_enabled_kebab: return TR_KEY_queue_stalled_enabled; - case TR_KEY_queue_stalled_minutes_kebab: return TR_KEY_queue_stalled_minutes; - case TR_KEY_rate_download_camel: return TR_KEY_rate_download; - case TR_KEY_rate_to_client_camel: return TR_KEY_rate_to_client; - case TR_KEY_rate_to_peer_camel: return TR_KEY_rate_to_peer; - case TR_KEY_rate_upload_camel: return TR_KEY_rate_upload; - case TR_KEY_ratio_limit_kebab: return TR_KEY_ratio_limit; - case TR_KEY_ratio_limit_enabled_kebab: return TR_KEY_ratio_limit_enabled; - case TR_KEY_ratio_mode_kebab: return TR_KEY_ratio_mode; - case TR_KEY_read_clipboard_kebab: return TR_KEY_read_clipboard; - case TR_KEY_recheck_progress_camel: return TR_KEY_recheck_progress; - case TR_KEY_remote_session_enabled_kebab: return TR_KEY_remote_session_enabled; - case TR_KEY_remote_session_host_kebab: return TR_KEY_remote_session_host; - case TR_KEY_remote_session_https_kebab: return TR_KEY_remote_session_https; - case TR_KEY_remote_session_password_kebab: return TR_KEY_remote_session_password; - case TR_KEY_remote_session_port_kebab: return TR_KEY_remote_session_port; - case TR_KEY_remote_session_requres_authentication_kebab: return TR_KEY_remote_session_requires_authentication; - case TR_KEY_remote_session_username_kebab: return TR_KEY_remote_session_username; - case TR_KEY_rename_partial_files_kebab: return TR_KEY_rename_partial_files; - case TR_KEY_rpc_authentication_required_kebab: return TR_KEY_rpc_authentication_required; - case TR_KEY_rpc_bind_address_kebab: return TR_KEY_rpc_bind_address; - case TR_KEY_rpc_enabled_kebab: return TR_KEY_rpc_enabled; - case TR_KEY_rpc_host_whitelist_kebab: return TR_KEY_rpc_host_whitelist; - case TR_KEY_rpc_host_whitelist_enabled_kebab: return TR_KEY_rpc_host_whitelist_enabled; - case TR_KEY_rpc_password_kebab: return TR_KEY_rpc_password; - case TR_KEY_rpc_port_kebab: return TR_KEY_rpc_port; - case TR_KEY_rpc_socket_mode_kebab: return TR_KEY_rpc_socket_mode; - case TR_KEY_rpc_url_kebab: return TR_KEY_rpc_url; - case TR_KEY_rpc_username_kebab: return TR_KEY_rpc_username; - case TR_KEY_rpc_version_kebab: return TR_KEY_rpc_version; - case TR_KEY_rpc_version_minimum_kebab: return TR_KEY_rpc_version_minimum; - case TR_KEY_rpc_version_semver_kebab: return TR_KEY_rpc_version_semver; - case TR_KEY_rpc_whitelist_kebab: return TR_KEY_rpc_whitelist; - case TR_KEY_rpc_whitelist_enabled_kebab: return TR_KEY_rpc_whitelist_enabled; - case TR_KEY_seconds_downloading_camel: return TR_KEY_seconds_downloading; - case TR_KEY_scrape_paused_torrents_enabled_kebab: return TR_KEY_scrape_paused_torrents_enabled; - case TR_KEY_scrape_state_camel: return TR_KEY_scrape_state; - case TR_KEY_script_torrent_added_enabled_kebab: return TR_KEY_script_torrent_added_enabled; - case TR_KEY_script_torrent_added_filename_kebab: return TR_KEY_script_torrent_added_filename; - case TR_KEY_script_torrent_done_enabled_kebab: return TR_KEY_script_torrent_done_enabled; - case TR_KEY_script_torrent_done_filename_kebab: return TR_KEY_script_torrent_done_filename; - case TR_KEY_script_torrent_done_seeding_enabled_kebab: return TR_KEY_script_torrent_done_seeding_enabled; - case TR_KEY_script_torrent_done_seeding_filename_kebab: return TR_KEY_script_torrent_done_seeding_filename; - case TR_KEY_seconds_active_camel: - case TR_KEY_seconds_active_kebab: - return TR_KEY_seconds_active; - case TR_KEY_seconds_seeding_camel: return TR_KEY_seconds_seeding; - case TR_KEY_seed_idle_limit_camel: return TR_KEY_seed_idle_limit; - case TR_KEY_seed_idle_mode_camel: return TR_KEY_seed_idle_mode; - case TR_KEY_seed_queue_enabled_kebab: return TR_KEY_seed_queue_enabled; - case TR_KEY_seed_queue_size_kebab: return TR_KEY_seed_queue_size; - case TR_KEY_seed_ratio_limit_camel: return TR_KEY_seed_ratio_limit; - case TR_KEY_seed_ratio_limited_camel: return TR_KEY_seed_ratio_limited; - case TR_KEY_seed_ratio_mode_camel: return TR_KEY_seed_ratio_mode; - case TR_KEY_seeding_time_seconds_kebab: return TR_KEY_seeding_time_seconds; - case TR_KEY_seeder_count_camel: return TR_KEY_seeder_count; - case TR_KEY_session_close_kebab: return TR_KEY_session_close; - case TR_KEY_session_count_camel: - case TR_KEY_session_count_kebab: - return TR_KEY_session_count; - case TR_KEY_session_get_kebab: return TR_KEY_session_get; - case TR_KEY_session_id_kebab: return TR_KEY_session_id; - case TR_KEY_session_set_kebab: return TR_KEY_session_set; - case TR_KEY_session_stats_kebab: return TR_KEY_session_stats; - case TR_KEY_show_backup_trackers_kebab: return TR_KEY_show_backup_trackers; - case TR_KEY_show_extra_peer_details_kebab: return TR_KEY_show_extra_peer_details; - case TR_KEY_show_filterbar_kebab: return TR_KEY_show_filterbar; - case TR_KEY_show_notification_area_icon_kebab: return TR_KEY_show_notification_area_icon; - case TR_KEY_show_options_window_kebab: return TR_KEY_show_options_window; - case TR_KEY_show_statusbar_kebab: return TR_KEY_show_statusbar; - case TR_KEY_show_toolbar_kebab: return TR_KEY_show_toolbar; - case TR_KEY_show_tracker_scrapes_kebab: return TR_KEY_show_tracker_scrapes; - case TR_KEY_size_bytes_kebab: return TR_KEY_size_bytes; - case TR_KEY_size_units_kebab: return TR_KEY_size_units; - case TR_KEY_size_when_done_camel: return TR_KEY_size_when_done; - case TR_KEY_sleep_per_seconds_during_verify_kebab: return TR_KEY_sleep_per_seconds_during_verify; - case TR_KEY_sort_mode_kebab: return TR_KEY_sort_mode; - case TR_KEY_sort_reversed_kebab: return TR_KEY_sort_reversed; - case TR_KEY_speed_Bps_kebab: return TR_KEY_speed_Bps; - case TR_KEY_speed_bytes_kebab: return TR_KEY_speed_bytes; - case TR_KEY_speed_limit_down_kebab: return TR_KEY_speed_limit_down; - case TR_KEY_speed_limit_down_enabled_kebab: return TR_KEY_speed_limit_down_enabled; - case TR_KEY_speed_limit_up_kebab: return TR_KEY_speed_limit_up; - case TR_KEY_speed_limit_up_enabled_kebab: return TR_KEY_speed_limit_up_enabled; - case TR_KEY_speed_units_kebab: return TR_KEY_speed_units; - case TR_KEY_start_added_torrents_kebab: return TR_KEY_start_added_torrents; - case TR_KEY_start_date_camel: return TR_KEY_start_date; - case TR_KEY_start_minimized_kebab: return TR_KEY_start_minimized; - case TR_KEY_statusbar_stats_kebab: return TR_KEY_statusbar_stats; - case TR_KEY_tcp_enabled_kebab: return TR_KEY_tcp_enabled; - case TR_KEY_torrent_add_kebab: return TR_KEY_torrent_add; - case TR_KEY_torrent_added_kebab: return TR_KEY_torrent_added; - case TR_KEY_torrent_added_notification_enabled_kebab: return TR_KEY_torrent_added_notification_enabled; - case TR_KEY_torrent_added_verify_mode_kebab: return TR_KEY_torrent_added_verify_mode; - case TR_KEY_torrent_complete_notification_enabled_kebab: return TR_KEY_torrent_complete_notification_enabled; - case TR_KEY_torrent_complete_sound_command_kebab: return TR_KEY_torrent_complete_sound_command; - case TR_KEY_torrent_complete_sound_enabled_kebab: return TR_KEY_torrent_complete_sound_enabled; - case TR_KEY_torrent_count_camel: return TR_KEY_torrent_count; - case TR_KEY_torrent_duplicate_kebab: return TR_KEY_torrent_duplicate; - case TR_KEY_torrent_file_camel: return TR_KEY_torrent_file; - case TR_KEY_torrent_get_kebab: return TR_KEY_torrent_get; - case TR_KEY_torrent_reannounce_kebab: return TR_KEY_torrent_reannounce; - case TR_KEY_torrent_remove_kebab: return TR_KEY_torrent_remove; - case TR_KEY_torrent_rename_path_kebab: return TR_KEY_torrent_rename_path; - case TR_KEY_torrent_set_kebab: return TR_KEY_torrent_set; - case TR_KEY_torrent_set_location_kebab: return TR_KEY_torrent_set_location; - case TR_KEY_torrent_start_kebab: return TR_KEY_torrent_start; - case TR_KEY_torrent_start_now_kebab: return TR_KEY_torrent_start_now; - case TR_KEY_torrent_stop_kebab: return TR_KEY_torrent_stop; - case TR_KEY_torrent_verify_kebab: return TR_KEY_torrent_verify; - case TR_KEY_total_size_camel: return TR_KEY_total_size; - case TR_KEY_tracker_add_camel: return TR_KEY_tracker_add; - case TR_KEY_tracker_list_camel: return TR_KEY_tracker_list; - case TR_KEY_tracker_remove_camel: return TR_KEY_tracker_remove; - case TR_KEY_tracker_replace_camel: return TR_KEY_tracker_replace; - case TR_KEY_tracker_stats_camel: return TR_KEY_tracker_stats; - case TR_KEY_trash_can_enabled_kebab: return TR_KEY_trash_can_enabled; - case TR_KEY_trash_original_torrent_files_kebab: return TR_KEY_trash_original_torrent_files; - case TR_KEY_upload_limit_camel: return TR_KEY_upload_limit; - case TR_KEY_upload_limited_camel: return TR_KEY_upload_limited; - case TR_KEY_upload_slots_per_torrent_kebab: return TR_KEY_upload_slots_per_torrent; - case TR_KEY_upload_ratio_camel: return TR_KEY_upload_ratio; - case TR_KEY_upload_speed_camel: return TR_KEY_upload_speed; - case TR_KEY_uploaded_bytes_camel: - case TR_KEY_uploaded_bytes_kebab: - return TR_KEY_uploaded_bytes; - case TR_KEY_uploaded_ever_camel: return TR_KEY_uploaded_ever; - case TR_KEY_use_global_speed_limit_kebab: return TR_KEY_use_global_speed_limit; - case TR_KEY_use_speed_limit_kebab: return TR_KEY_use_speed_limit; - case TR_KEY_utp_enabled_kebab: return TR_KEY_utp_enabled; - case TR_KEY_watch_dir_kebab: return TR_KEY_watch_dir; - case TR_KEY_watch_dir_enabled_kebab: return TR_KEY_watch_dir_enabled; - case TR_KEY_watch_dir_force_generic_kebab: return TR_KEY_watch_dir_force_generic; - case TR_KEY_webseeds_sending_to_us_camel: return TR_KEY_webseeds_sending_to_us; - default: return q; - } - // clang-format on -} diff --git a/libtransmission/quark.h b/libtransmission/quark.h index 665894e78..8ff90b394 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -752,10 +752,3 @@ enum // NOLINT(performance-enum-size) * created. */ [[nodiscard]] tr_quark tr_quark_new(std::string_view str); - -/** - * Get the replacement quark from old deprecated quarks. - * - * Note: Temporary shim just for the transition period to snake_case. - */ -[[nodiscard]] tr_quark tr_quark_convert(tr_quark quark); diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index 37cad142e..07485b2aa 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -32,6 +32,7 @@ #include "libtransmission/log.h" #include "libtransmission/net.h" #include "libtransmission/peer-mgr.h" +#include "libtransmission/api-compat.h" #include "libtransmission/quark.h" #include "libtransmission/rpcimpl.h" #include "libtransmission/session.h" @@ -54,9 +55,7 @@ namespace JsonRpc // https://www.jsonrpc.org/specification#error_object namespace Error { -namespace -{ -[[nodiscard]] constexpr std::string_view get_message(Code code) +[[nodiscard]] std::string_view to_string(Code const code) { switch (code) { @@ -95,6 +94,8 @@ namespace } } +namespace +{ [[nodiscard]] tr_variant::Map build_data(std::string_view error_string, tr_variant::Map&& result) { auto ret = tr_variant::Map{ 2U }; @@ -116,7 +117,7 @@ namespace { auto ret = tr_variant::Map{ 3U }; ret.try_emplace(TR_KEY_code, code); - ret.try_emplace(TR_KEY_message, tr_variant::unmanaged_string(get_message(code))); + ret.try_emplace(TR_KEY_message, tr_variant::unmanaged_string(to_string(code))); if (!std::empty(data)) { ret.try_emplace(TR_KEY_data, std::move(data)); @@ -205,7 +206,7 @@ void tr_rpc_idle_done_legacy(struct tr_rpc_idle_data* data, JsonRpc::Error::Code // build the response auto response_map = tr_variant::Map{ 3U }; response_map.try_emplace(TR_KEY_arguments, std::move(data->args_out)); - response_map.try_emplace(TR_KEY_result, std::empty(result) ? JsonRpc::Error::get_message(code) : result); + response_map.try_emplace(TR_KEY_result, std::empty(result) ? JsonRpc::Error::to_string(code) : result); if (auto& tag = data->id; tag.has_value()) { response_map.try_emplace(TR_KEY_tag, std::move(tag)); diff --git a/libtransmission/rpcimpl.h b/libtransmission/rpcimpl.h index 9ee14c83e..09e70ea05 100644 --- a/libtransmission/rpcimpl.h +++ b/libtransmission/rpcimpl.h @@ -5,6 +5,7 @@ #pragma once +#include // int16_t #include struct tr_session; @@ -34,7 +35,9 @@ enum Code : int16_t HTTP_ERROR, CORRUPT_TORRENT }; -} + +[[nodiscard]] std::string_view to_string(Code code); +} // namespace Error } // namespace JsonRpc using tr_rpc_response_func = std::function; diff --git a/libtransmission/variant.cc b/libtransmission/variant.cc index cb1ed1df5..c014304fd 100644 --- a/libtransmission/variant.cc +++ b/libtransmission/variant.cc @@ -22,6 +22,7 @@ #define LIBTRANSMISSION_VARIANT_MODULE +#include "libtransmission/api-compat.h" #include "libtransmission/error.h" #include "libtransmission/log.h" #include "libtransmission/quark.h" diff --git a/qt/Prefs.cc b/qt/Prefs.cc index 10c525ace..0333f0133 100644 --- a/qt/Prefs.cc +++ b/qt/Prefs.cc @@ -19,6 +19,8 @@ #endif #include + +#include #include #include "CustomVariantType.h" diff --git a/tests/libtransmission/CMakeLists.txt b/tests/libtransmission/CMakeLists.txt index 76ba4950a..d215a122c 100644 --- a/tests/libtransmission/CMakeLists.txt +++ b/tests/libtransmission/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(libtransmission-test announce-list-test.cc announcer-test.cc announcer-udp-test.cc + api-compat-test.cc benc-test.cc bitfield-test.cc block-info-test.cc @@ -16,8 +17,8 @@ target_sources(libtransmission-test completion-test.cc copy-test.cc crypto-test.cc - error-test.cc dht-test.cc + error-test.cc file-piece-map-test.cc file-test.cc getopt-test.cc diff --git a/tests/libtransmission/api-compat-test.cc b/tests/libtransmission/api-compat-test.cc new file mode 100644 index 000000000..be78c613f --- /dev/null +++ b/tests/libtransmission/api-compat-test.cc @@ -0,0 +1,738 @@ +// This file Copyright © Mnemosyne LLC. +// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), +// or any future license endorsed by Mnemosyne LLC. +// License text can be found in the licenses/ folder. + +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "test-fixtures.h" + +namespace +{ + +constexpr std::string_view LegacySessionGetJson = R"json({ + "method": "session-get", + "tag": 0 +})json"; + +constexpr std::string_view CurrentSessionGetJson = R"json({ + "id": 0, + "jsonrpc": "2.0", + "method": "session_get" +})json"; + +constexpr std::string_view LegacySessionGetResponseJson = R"json({ + "arguments": { + "alt-speed-down": 50, + "alt-speed-enabled": false, + "alt-speed-time-begin": 540, + "alt-speed-time-day": 127, + "alt-speed-time-enabled": false, + "alt-speed-time-end": 1020, + "alt-speed-up": 50, + "anti-brute-force-enabled": false, + "anti-brute-force-threshold": 100, + "blocklist-enabled": false, + "blocklist-size": 0, + "blocklist-url": "http://www.example.com/blocklist", + "cache-size-mb": 4, + "config-dir": "/home/user/.config/transmission", + "default-trackers": "", + "dht-enabled": true, + "download-dir": "/home/user/Downloads", + "download-dir-free-space": 2199023255552, + "download-queue-enabled": true, + "download-queue-size": 4, + "encryption": "preferred", + "idle-seeding-limit": 30, + "idle-seeding-limit-enabled": false, + "incomplete-dir": "/home/user/Downloads", + "incomplete-dir-enabled": false, + "lpd-enabled": false, + "peer-limit-global": 200, + "peer-limit-per-torrent": 50, + "peer-port": 51413, + "peer-port-random-on-start": false, + "pex-enabled": true, + "port-forwarding-enabled": true, + "preferred_transports": [ + "utp", + "tcp" + ], + "queue-stalled-enabled": true, + "queue-stalled-minutes": 30, + "rename-partial-files": true, + "reqq": 2000, + "rpc-version": 18, + "rpc-version-minimum": 14, + "rpc-version-semver": "6.0.0", + "script-torrent-added-enabled": false, + "script-torrent-added-filename": "", + "script-torrent-done-enabled": false, + "script-torrent-done-filename": "/home/user/scripts/script.sh", + "script-torrent-done-seeding-enabled": false, + "script-torrent-done-seeding-filename": "", + "seed-queue-enabled": false, + "seed-queue-size": 10, + "seedRatioLimit": 2.0, + "seedRatioLimited": false, + "sequential_download": false, + "session-id": "pdvuklydohaohwwluzpmpmllkaopzzlzzpvupkpuavhjhlzhyjfwoly", + "speed-limit-down": 100.0, + "speed-limit-down-enabled": false, + "speed-limit-up": 100.0, + "speed-limit-up-enabled": false, + "start-added-torrents": true, + "tcp-enabled": true, + "trash-original-torrent-files": false, + "units": { + "memory-bytes": 1024, + "memory-units": [ + "B", + "KiB", + "MiB", + "GiB", + "TiB" + ], + "size-bytes": 1000, + "size-units": [ + "B", + "kB", + "MB", + "GB", + "TB" + ], + "speed-bytes": 1000, + "speed-units": [ + "B/s", + "kB/s", + "MB/s", + "GB/s", + "TB/s" + ] + }, + "utp-enabled": true, + "version": "4.1.0-beta.4 (b11bfd9712)" + }, + "result": "success", + "tag": 40 +})json"; + +constexpr std::string_view CurrentSessionGetResponseJson = R"json({ + "id": 40, + "jsonrpc": "2.0", + "result": { + "alt_speed_down": 50, + "alt_speed_enabled": false, + "alt_speed_time_begin": 540, + "alt_speed_time_day": 127, + "alt_speed_time_enabled": false, + "alt_speed_time_end": 1020, + "alt_speed_up": 50, + "anti_brute_force_enabled": false, + "anti_brute_force_threshold": 100, + "blocklist_enabled": false, + "blocklist_size": 0, + "blocklist_url": "http://www.example.com/blocklist", + "cache_size_mb": 4, + "config_dir": "/home/user/.config/transmission", + "default_trackers": "", + "dht_enabled": true, + "download_dir": "/home/user/Downloads", + "download_dir_free_space": 2199023255552, + "download_queue_enabled": true, + "download_queue_size": 4, + "encryption": "preferred", + "idle_seeding_limit": 30, + "idle_seeding_limit_enabled": false, + "incomplete_dir": "/home/user/Downloads", + "incomplete_dir_enabled": false, + "lpd_enabled": false, + "peer_limit_global": 200, + "peer_limit_per_torrent": 50, + "peer_port": 51413, + "peer_port_random_on_start": false, + "pex_enabled": true, + "port_forwarding_enabled": true, + "preferred_transports": [ + "utp", + "tcp" + ], + "queue_stalled_enabled": true, + "queue_stalled_minutes": 30, + "rename_partial_files": true, + "reqq": 2000, + "rpc_version": 18, + "rpc_version_minimum": 14, + "rpc_version_semver": "6.0.0", + "script_torrent_added_enabled": false, + "script_torrent_added_filename": "", + "script_torrent_done_enabled": false, + "script_torrent_done_filename": "/home/user/scripts/script.sh", + "script_torrent_done_seeding_enabled": false, + "script_torrent_done_seeding_filename": "", + "seed_queue_enabled": false, + "seed_queue_size": 10, + "seed_ratio_limit": 2.0, + "seed_ratio_limited": false, + "sequential_download": false, + "session_id": "pdvuklydohaohwwluzpmpmllkaopzzlzzpvupkpuavhjhlzhyjfwoly", + "speed_limit_down": 100.0, + "speed_limit_down_enabled": false, + "speed_limit_up": 100.0, + "speed_limit_up_enabled": false, + "start_added_torrents": true, + "tcp_enabled": true, + "trash_original_torrent_files": false, + "units": { + "memory_bytes": 1024, + "memory_units": [ + "B", + "KiB", + "MiB", + "GiB", + "TiB" + ], + "size_bytes": 1000, + "size_units": [ + "B", + "kB", + "MB", + "GB", + "TB" + ], + "speed_bytes": 1000, + "speed_units": [ + "B/s", + "kB/s", + "MB/s", + "GB/s", + "TB/s" + ] + }, + "utp_enabled": true, + "version": "4.1.0-beta.4 (b11bfd9712)" + } +})json"; + +constexpr std::string_view LegacyTorrentGetJson = R"json({ + "arguments": { + "fields": [ + "downloadDir", + "downloadedEver", + "editDate", + "error", + "errorString", + "eta", + "haveUnchecked", + "haveValid", + "id", + "isFinished", + "leftUntilDone", + "manualAnnounceTime", + "metadataPercentComplete", + "name", + "peersConnected", + "peersGettingFromUs", + "peersSendingToUs", + "percentDone", + "queuePosition", + "rateDownload", + "rateUpload", + "recheckProgress", + "seedRatioLimit", + "seedRatioMode", + "sizeWhenDone", + "status", + "uploadRatio", + "uploadedEver", + "webseedsSendingToUs" + ], + "ids": "recently-active" + }, + "method": "torrent-get", + "tag": 6 +})json"; + +constexpr std::string_view CurrentTorrentGetJson = R"json({ + "id": 6, + "jsonrpc": "2.0", + "method": "torrent_get", + "params": { + "fields": [ + "download_dir", + "downloaded_ever", + "edit_date", + "error", + "error_string", + "eta", + "have_unchecked", + "have_valid", + "id", + "is_finished", + "left_until_done", + "manual_announce_time", + "metadata_percent_complete", + "name", + "peers_connected", + "peers_getting_from_us", + "peers_sending_to_us", + "percent_done", + "queue_position", + "rate_download", + "rate_upload", + "recheck_progress", + "seed_ratio_limit", + "seed_ratio_mode", + "size_when_done", + "status", + "upload_ratio", + "uploaded_ever", + "webseeds_sending_to_us" + ], + "ids": "recently_active" + } +})json"; + +constexpr std::string_view CurrentPortTestErrorResponse = R"json({ + "error": { + "code": 8, + "data": { + "error_string": "Couldn't test port: No Response (0)", + "result": { + "ip_protocol": "ipv6" + } + }, + "message": "HTTP error from backend service" + }, + "id": 9, + "jsonrpc": "2.0" +})json"; + +constexpr std::string_view LegacyPortTestErrorResponse = R"json({ + "arguments": { + "ip_protocol": "ipv6" + }, + "result": "Couldn't test port: No Response (0)", + "tag": 9 +})json"; + +constexpr std::string_view LegacyStatsJson = R"json({ + "downloaded-bytes": 12, + "files-added": 34, + "seconds-active": 56, + "session-count": 78, + "uploaded-bytes": 90 +})json"; + +constexpr std::string_view CurrentStatsJson = R"json({ + "downloaded_bytes": 12, + "files_added": 34, + "seconds_active": 56, + "session_count": 78, + "uploaded_bytes": 90 +})json"; + +constexpr std::string_view LegacySettingsJson = R"json({ + "alt-speed-down": 50, + "alt-speed-enabled": false, + "alt-speed-time-begin": 540, + "alt-speed-time-day": 127, + "alt-speed-time-enabled": false, + "alt-speed-time-end": 1020, + "alt-speed-up": 50, + "blocklist-date": 0, + "blocklist-enabled": false, + "blocklist-updates-enabled": true, + "blocklist-url": "http://www.example.com/blocklist", + "compact-view": false, + "default-trackers": "", + "dht-enabled": true, + "download-dir": "/home/user/Downloads", + "download-queue-enabled": true, + "download-queue-size": 5, + "encryption": 1, + "filter-mode": "show-all", + "filter-trackers": "", + "idle-seeding-limit": 30, + "idle-seeding-limit-enabled": false, + "incomplete-dir": "/home/user/Downloads", + "incomplete-dir-enabled": false, + "inhibit-desktop-hibernation": false, + "lpd-enabled": true, + "main-window-height": 500, + "main-window-layout-order": "menu,toolbar,filter,list,statusbar", + "main-window-width": 650, + "main-window-x": 3840, + "main-window-y": 0, + "message-level": 4, + "open-dialog-dir": "/home/user", + "peer-limit-global": 200, + "peer-limit-per-torrent": 50, + "peer-port": 51413, + "peer-port-random-high": 65535, + "peer-port-random-low": 49152, + "peer-port-random-on-start": false, + "peer-socket-tos": "le", + "pex-enabled": true, + "port-forwarding-enabled": true, + "preallocation": 1, + "prompt-before-exit": true, + "queue-stalled-minutes": 30, + "ratio-limit": 2.0, + "ratio-limit-enabled": false, + "read-clipboard": false, + "remote-session-enabled": false, + "remote-session-host": "localhost", + "remote-session-https": false, + "remote-session-password": "", + "remote-session-port": 9091, + "remote-session-requres-authentication": false, + "remote-session-username": "", + "rename-partial-files": true, + "rpc-authentication-required": false, + "rpc-enabled": false, + "rpc-password": "", + "rpc-port": 9091, + "rpc-username": "", + "rpc-whitelist": "127.0.0.1,::1", + "rpc-whitelist-enabled": true, + "script-torrent-done-enabled": false, + "script-torrent-done-filename": "", + "script-torrent-done-seeding-enabled": false, + "script-torrent-done-seeding-filename": "", + "show-backup-trackers": false, + "show-filterbar": true, + "show-notification-area-icon": false, + "show-options-window": true, + "show-statusbar": true, + "show-toolbar": true, + "show-tracker-scrapes": false, + "sleep-per-seconds-during-verify": 100, + "sort-mode": "sort-by-name", + "sort-reversed": false, + "speed-limit-down": 100, + "speed-limit-down-enabled": false, + "speed-limit-up": 100, + "speed-limit-up-enabled": false, + "start-added-torrents": true, + "start-minimized": false, + "statusbar-stats": "total-ratio", + "torrent-added-notification-enabled": true, + "torrent-complete-notification-enabled": true, + "torrent-complete-sound-command": [ + "canberra-gtk-play", + "-i", + "complete-download", + "-d", + "transmission torrent downloaded" + ], + "torrent-complete-sound-enabled": true, + "trash-original-torrent-files": false, + "upload-slots-per-torrent": 8, + "utp-enabled": true, + "watch-dir": "/home/user/Downloads", + "watch-dir-enabled": false +})json"; + +constexpr std::string_view CurrentSettingsJson = R"json({ + "alt_speed_down": 50, + "alt_speed_enabled": false, + "alt_speed_time_begin": 540, + "alt_speed_time_day": 127, + "alt_speed_time_enabled": false, + "alt_speed_time_end": 1020, + "alt_speed_up": 50, + "blocklist_date": 0, + "blocklist_enabled": false, + "blocklist_updates_enabled": true, + "blocklist_url": "http://www.example.com/blocklist", + "compact_view": false, + "default_trackers": "", + "dht_enabled": true, + "download_dir": "/home/user/Downloads", + "download_queue_enabled": true, + "download_queue_size": 5, + "encryption": 1, + "filter_mode": "show-all", + "filter_trackers": "", + "idle_seeding_limit": 30, + "idle_seeding_limit_enabled": false, + "incomplete_dir": "/home/user/Downloads", + "incomplete_dir_enabled": false, + "inhibit_desktop_hibernation": false, + "lpd_enabled": true, + "main_window_height": 500, + "main_window_layout_order": "menu,toolbar,filter,list,statusbar", + "main_window_width": 650, + "main_window_x": 3840, + "main_window_y": 0, + "message_level": 4, + "open_dialog_dir": "/home/user", + "peer_limit_global": 200, + "peer_limit_per_torrent": 50, + "peer_port": 51413, + "peer_port_random_high": 65535, + "peer_port_random_low": 49152, + "peer_port_random_on_start": false, + "peer_socket_tos": "le", + "pex_enabled": true, + "port_forwarding_enabled": true, + "preallocation": 1, + "prompt_before_exit": true, + "queue_stalled_minutes": 30, + "ratio_limit": 2.0, + "ratio_limit_enabled": false, + "read_clipboard": false, + "remote_session_enabled": false, + "remote_session_host": "localhost", + "remote_session_https": false, + "remote_session_password": "", + "remote_session_port": 9091, + "remote_session_requires_authentication": false, + "remote_session_username": "", + "rename_partial_files": true, + "rpc_authentication_required": false, + "rpc_enabled": false, + "rpc_password": "", + "rpc_port": 9091, + "rpc_username": "", + "rpc_whitelist": "127.0.0.1,::1", + "rpc_whitelist_enabled": true, + "script_torrent_done_enabled": false, + "script_torrent_done_filename": "", + "script_torrent_done_seeding_enabled": false, + "script_torrent_done_seeding_filename": "", + "show_backup_trackers": false, + "show_filterbar": true, + "show_notification_area_icon": false, + "show_options_window": true, + "show_statusbar": true, + "show_toolbar": true, + "show_tracker_scrapes": false, + "sleep_per_seconds_during_verify": 100, + "sort_mode": "sort-by-name", + "sort_reversed": false, + "speed_limit_down": 100, + "speed_limit_down_enabled": false, + "speed_limit_up": 100, + "speed_limit_up_enabled": false, + "start_added_torrents": true, + "start_minimized": false, + "statusbar_stats": "total-ratio", + "torrent_added_notification_enabled": true, + "torrent_complete_notification_enabled": true, + "torrent_complete_sound_command": [ + "canberra-gtk-play", + "-i", + "complete-download", + "-d", + "transmission torrent downloaded" + ], + "torrent_complete_sound_enabled": true, + "trash_original_torrent_files": false, + "upload_slots_per_torrent": 8, + "utp_enabled": true, + "watch_dir": "/home/user/Downloads", + "watch_dir_enabled": false +})json"; + +constexpr std::string_view BadFreeSpaceRequest = R"json({ + "id": 39693, + "jsonrpc": "2.0", + "method": "free_space", + "params": { + "path": "this/path/is/not/absolute" + } +})json"; + +constexpr std::string_view BadFreeSpaceRequestLegacy = R"json({ + "arguments": { + "path": "this/path/is/not/absolute" + }, + "method": "free-space", + "tag": 39693 +})json"; + +constexpr std::string_view BadFreeSpaceResponse = R"json({ + "error": { + "code": 3, + "data": { + "error_string": "directory path is not absolute" + }, + "message": "path is not absolute" + }, + "id": 39693, + "jsonrpc": "2.0" +})json"; + +constexpr std::string_view BadFreeSpaceResponseLegacy = R"json({ + "arguments": {}, + "result": "directory path is not absolute", + "tag": 39693 +})json"; + +constexpr std::string_view WellFormedFreeSpaceRequest = R"json({ + "id": 41414, + "jsonrpc": "2.0", + "method": "free_space", + "params": { + "path": "/this/path/does/not/exist" + } +})json"; + +constexpr std::string_view WellFormedFreeSpaceResponse = R"json({ + "id": 41414, + "jsonrpc": "2.0", + "result": { + "path": "/this/path/does/not/exist", + "size_bytes": -1, + "total_size": -1 + } +})json"; + +constexpr std::string_view WellFormedFreeSpaceLegacyRequest = R"json({ + "arguments": { + "path": "/this/path/does/not/exist" + }, + "method": "free-space", + "tag": 41414 +})json"; + +constexpr std::string_view WellFormedFreeSpaceLegacyResponse = R"json({ + "arguments": { + "path": "/this/path/does/not/exist", + "size-bytes": -1, + "total_size": -1 + }, + "result": "success", + "tag": 41414 +})json"; + +constexpr std::string_view BadMethodNameResponse = R"json({ + "error": { + "code": -32601, + "message": "Method not found" + }, + "id": 39693, + "jsonrpc": "2.0" +})json"; + +constexpr std::string_view BadMethodNameLegacyResponse = R"json({ + "arguments": {}, + "result": "no method name", + "tag": 39693 +})json"; + +constexpr std::string_view UnrecognisedInfoResponse = R"json({ + "error": { + "code": 4, + "message": "unrecognized info" + }, + "id": 10, + "jsonrpc": "2.0" +})json"; + +constexpr std::string_view UnrecognisedInfoLegacyResponse = R"json({ + "arguments": {}, + "result": "unrecognized info", + "tag": 10 +})json"; + +} // namespace + +TEST(ApiCompatTest, canConvertRpc) +{ + using Style = libtransmission::api_compat::Style; + using TestCase = std::tuple; + + // clang-format off + 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 }, + { "free_space tr4 -> tr4", BadFreeSpaceRequestLegacy, Style::Tr4, BadFreeSpaceRequestLegacy }, + { "free_space error response tr5 -> tr5", BadFreeSpaceResponse, Style::Tr5, BadFreeSpaceResponse }, + { "free_space error response tr5 -> tr4", BadFreeSpaceResponse, Style::Tr4, BadFreeSpaceResponseLegacy }, + { "free_space error response tr4 -> tr5", BadFreeSpaceResponseLegacy, Style::Tr5, BadFreeSpaceResponse }, + { "free_space error response tr4 -> tr4", BadFreeSpaceResponseLegacy, Style::Tr4, BadFreeSpaceResponseLegacy }, + { "free_space req tr5 -> tr5", WellFormedFreeSpaceRequest, Style::Tr5, WellFormedFreeSpaceRequest }, + { "free_space req tr5 -> tr4", WellFormedFreeSpaceRequest, Style::Tr4, WellFormedFreeSpaceLegacyRequest }, + { "free_space req tr4 -> tr5", WellFormedFreeSpaceLegacyRequest, Style::Tr5, WellFormedFreeSpaceRequest }, + { "free_space req tr4 -> tr4", WellFormedFreeSpaceLegacyRequest, Style::Tr4, WellFormedFreeSpaceLegacyRequest }, + { "free_space response tr5 -> tr5", WellFormedFreeSpaceResponse, Style::Tr5, WellFormedFreeSpaceResponse }, + { "free_space response tr5 -> tr4", WellFormedFreeSpaceResponse, Style::Tr4, WellFormedFreeSpaceLegacyResponse }, + { "free_space response tr4 -> tr5", WellFormedFreeSpaceLegacyResponse, Style::Tr5, WellFormedFreeSpaceResponse }, + { "free_space response tr4 -> tr4", WellFormedFreeSpaceLegacyResponse, Style::Tr4, WellFormedFreeSpaceLegacyResponse }, + { "session_get tr5 -> tr5", CurrentSessionGetJson, Style::Tr5, CurrentSessionGetJson }, + { "session_get tr5 -> tr4", CurrentSessionGetJson, Style::Tr4, LegacySessionGetJson }, + { "session_get tr4 -> tr5", LegacySessionGetJson, Style::Tr5, CurrentSessionGetJson }, + { "session_get tr4 -> tr4", LegacySessionGetJson, Style::Tr4, LegacySessionGetJson }, + { "session_get response tr5 -> tr5", CurrentSessionGetResponseJson, Style::Tr5, CurrentSessionGetResponseJson }, + { "session_get response tr5 -> tr4", CurrentSessionGetResponseJson, Style::Tr4, LegacySessionGetResponseJson }, + { "session_get response tr4 -> tr5", LegacySessionGetResponseJson, Style::Tr5, CurrentSessionGetResponseJson }, + { "session_get response tr4 -> tr4", LegacySessionGetResponseJson, Style::Tr4, LegacySessionGetResponseJson }, + { "torrent_get tr5 -> tr5", CurrentTorrentGetJson, Style::Tr5, CurrentTorrentGetJson }, + { "torrent_get tr5 -> tr4", CurrentTorrentGetJson, Style::Tr4, LegacyTorrentGetJson }, + { "torrent_get tr4 -> tr5", LegacyTorrentGetJson, Style::Tr5, CurrentTorrentGetJson }, + { "torrent_get tr4 -> tr4", LegacyTorrentGetJson, Style::Tr4, LegacyTorrentGetJson }, + { "port_test error response tr5 -> tr5", CurrentPortTestErrorResponse, Style::Tr5, CurrentPortTestErrorResponse }, + { "port_test error response tr5 -> tr4", CurrentPortTestErrorResponse, Style::Tr4, LegacyPortTestErrorResponse }, + { "port_test error response tr4 -> tr5", LegacyPortTestErrorResponse, Style::Tr5, CurrentPortTestErrorResponse }, + { "port_test error response tr4 -> tr4", LegacyPortTestErrorResponse, Style::Tr4, LegacyPortTestErrorResponse }, + { "bad method name tr5 -> tr5", BadMethodNameResponse, Style::Tr5, BadMethodNameResponse }, + { "bad method name tr5 -> tr4", BadMethodNameResponse, Style::Tr4, BadMethodNameLegacyResponse }, + { "bad method name tr4 -> tr5", BadMethodNameLegacyResponse, Style::Tr5, BadMethodNameResponse }, + { "bad method name tr4 -> tr4", BadMethodNameLegacyResponse, Style::Tr4, BadMethodNameLegacyResponse }, + { "unrecognised info tr5 -> tr5", UnrecognisedInfoResponse, Style::Tr5, UnrecognisedInfoResponse}, + { "unrecognised info tr5 -> tr4", UnrecognisedInfoResponse, Style::Tr4, UnrecognisedInfoLegacyResponse}, + { "unrecognised info tr4 -> tr5", UnrecognisedInfoLegacyResponse, Style::Tr5, UnrecognisedInfoResponse}, + { "unrecognised info tr4 -> tr4", UnrecognisedInfoLegacyResponse, Style::Tr4, UnrecognisedInfoLegacyResponse}, + + // TODO(ckerr): torrent-get with 'table' + } }; + // clang-format on + + for (auto const& [name, src, tgt_style, expected] : TestCases) + { + auto serde = tr_variant_serde::json(); + auto parsed = serde.parse(src); + ASSERT_TRUE(parsed.has_value()) << name << ": " << serde.error_; + auto converted = libtransmission::api_compat::convert(*parsed, tgt_style); + EXPECT_EQ(expected, serde.to_string(converted)) << name; + } +} + +TEST(ApiCompatTest, canConvertDataFiles) +{ + 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 }, + { "settings tr4 -> tr5", LegacySettingsJson, Style::Tr5, CurrentSettingsJson }, + { "settings tr4 -> tr4", LegacySettingsJson, Style::Tr4, LegacySettingsJson }, + + { "stats tr5 -> tr5", CurrentStatsJson, Style::Tr5, CurrentStatsJson }, + { "stats tr5 -> tr4", CurrentStatsJson, Style::Tr4, LegacyStatsJson }, + { "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(); + 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; + } +}