// This file Copyright © 2011-2022 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 #include #include #include "transmission.h" #include "error.h" #include "subprocess.h" #include "tr-assert.h" #include "utils.h" using namespace std::literals; enum tr_app_type { TR_APP_TYPE_EXE, TR_APP_TYPE_BATCH }; static void set_system_error(tr_error** error, DWORD code, std::string_view what) { if (error == nullptr) { return; } if (char* message = tr_win32_format_message(code); message != nullptr) { tr_error_set(error, code, fmt::format(FMT_STRING("{:s} failed: {:s}"), what, message)); tr_free(message); } else { tr_error_set(error, code, fmt::format(FMT_STRING("{:s} failed: Unknown error: {:#08x}"), what, code)); } } static void append_to_env_block(wchar_t** env_block, size_t* env_block_len, wchar_t const* part, size_t part_len) { *env_block = tr_renew(wchar_t, *env_block, *env_block_len + part_len + 1); wmemcpy(*env_block + *env_block_len, part, part_len); *env_block_len += part_len; } static bool parse_env_block_part(wchar_t const* part, size_t* full_len, size_t* name_len) { TR_ASSERT(part != nullptr); auto const* const equals_pos = wcschr(part, L'='); if (equals_pos == nullptr) { /* Invalid part */ return false; } ptrdiff_t const my_name_len = equals_pos - part; if (my_name_len > SIZE_MAX) { /* Invalid part */ return false; } if (full_len != nullptr) { /* Includes terminating '\0' */ *full_len = wcslen(part) + 1; } if (name_len != nullptr) { *name_len = (size_t)my_name_len; } return true; } static int compare_wide_strings_ci(wchar_t const* lhs, size_t lhs_len, wchar_t const* rhs, size_t rhs_len) { int diff = wcsnicmp(lhs, rhs, std::min(lhs_len, rhs_len)); if (diff == 0) { diff = lhs_len < rhs_len ? -1 : (lhs_len > rhs_len ? 1 : 0); } return diff; } static int compare_env_part_names(void const* vlhs, void const* vrhs) { int ret = 0; auto const* const* const lhs = reinterpret_cast(vlhs); auto const* const* const rhs = reinterpret_cast(vrhs); size_t lhs_part_len; size_t lhs_name_len; if (parse_env_block_part(*lhs, &lhs_part_len, &lhs_name_len)) { size_t rhs_part_len; size_t rhs_name_len; if (parse_env_block_part(*rhs, &rhs_part_len, &rhs_name_len)) { ret = compare_wide_strings_ci(*lhs, lhs_name_len, *rhs, rhs_name_len); } } return ret; } static wchar_t** to_wide_env(std::map const& env) { auto const part_count = std::size(env); wchar_t** const wide_env = tr_new(wchar_t*, part_count + 1); int i = 0; for (auto const& [key_sv, val_sv] : env) { auto const line = tr_strvJoin(key_sv, "="sv, val_sv); wide_env[i++] = tr_win32_utf8_to_native(std::data(line), std::size(line)); } wide_env[i] = nullptr; TR_ASSERT(i == part_count); /* "The sort is case-insensitive, Unicode order, without regard to locale" © MSDN */ qsort(wide_env, part_count, sizeof(wchar_t*), &compare_env_part_names); return wide_env; } static void tr_free_ptrv(void* const* p) { if (p == nullptr) { return; } while (*p != nullptr) { tr_free(*p); ++p; } } static bool create_env_block(std::map const& env, wchar_t** env_block, tr_error** error) { wchar_t** wide_env = to_wide_env(env); if (wide_env == nullptr) { *env_block = nullptr; return true; } wchar_t* const old_env_block = GetEnvironmentStringsW(); if (old_env_block == nullptr) { set_system_error(error, GetLastError(), "Call to GetEnvironmentStrings()"); return false; } *env_block = nullptr; wchar_t const* old_part = old_env_block; size_t env_block_len = 0; for (size_t i = 0; wide_env[i] != nullptr; ++i) { wchar_t const* const part = wide_env[i]; size_t part_len; size_t name_len; if (!parse_env_block_part(part, &part_len, &name_len)) { continue; } while (*old_part != L'\0') { size_t old_part_len; size_t old_name_len; if (!parse_env_block_part(old_part, &old_part_len, &old_name_len)) { continue; } int const name_diff = compare_wide_strings_ci(old_part, old_name_len, part, name_len); if (name_diff < 0) { append_to_env_block(env_block, &env_block_len, old_part, old_part_len); } if (name_diff <= 0) { old_part += old_part_len; } if (name_diff >= 0) { break; } } append_to_env_block(env_block, &env_block_len, part, part_len); } while (*old_part != L'\0') { size_t old_part_len; if (!parse_env_block_part(old_part, &old_part_len, nullptr)) { continue; } append_to_env_block(env_block, &env_block_len, old_part, old_part_len); old_part += old_part_len; } (*env_block)[env_block_len] = '\0'; FreeEnvironmentStringsW(old_env_block); tr_free_ptrv((void* const*)wide_env); tr_free(wide_env); return true; } static void append_argument(char** arguments, char const* argument) { size_t arguments_len = *arguments != nullptr ? strlen(*arguments) : 0u; size_t const argument_len = strlen(argument); if (arguments_len > 0) { (*arguments)[arguments_len++] = ' '; } if (!tr_str_is_empty(argument) && strpbrk(argument, " \t\n\v\"") == nullptr) { *arguments = tr_renew(char, *arguments, arguments_len + argument_len + 2); strcpy(*arguments + arguments_len, argument); return; } *arguments = tr_renew(char, *arguments, arguments_len + argument_len * 2 + 4); char* dst = *arguments + arguments_len; *(dst++) = '"'; for (char const* src = argument; *src != '\0';) { size_t backslash_count = 0; while (*src == '\\') { ++backslash_count; ++src; } switch (*src) { case '\0': backslash_count = backslash_count * 2; break; case '"': backslash_count = backslash_count * 2 + 1; break; } if (backslash_count != 0) { memset(dst, '\\', backslash_count); dst += backslash_count; } if (*src != '\0') { *(dst++) = *(src++); } } *(dst++) = '"'; *(dst++) = '\0'; } static bool contains_batch_metachars(char const* text) { /* First part - chars explicitly documented by `cmd.exe /?` as "special" */ return strpbrk( text, "&<>()@^|" "%!^\"") != nullptr; } static enum tr_app_type get_app_type(char const* app) { if (tr_str_has_suffix(app, ".cmd") || tr_str_has_suffix(app, ".bat")) { return TR_APP_TYPE_BATCH; } /* TODO: Support other types? */ return TR_APP_TYPE_EXE; } static void append_app_launcher_arguments(enum tr_app_type app_type, char** args) { switch (app_type) { case TR_APP_TYPE_EXE: break; case TR_APP_TYPE_BATCH: append_argument(args, "cmd.exe"); append_argument(args, "/d"); append_argument(args, "/e:off"); append_argument(args, "/v:off"); append_argument(args, "/s"); append_argument(args, "/c"); break; default: TR_ASSERT_MSG(false, fmt::format(FMT_STRING("unsupported application type {:d}"), app_type)); break; } } static bool construct_cmd_line(char const* const* cmd, wchar_t** cmd_line) { enum tr_app_type const app_type = get_app_type(cmd[0]); char* args = nullptr; size_t arg_count = 0; bool ret = false; append_app_launcher_arguments(app_type, &args); for (size_t i = 0; cmd[i] != nullptr; ++i) { if (app_type == TR_APP_TYPE_BATCH && i > 0 && contains_batch_metachars(cmd[i])) { /* FIXME: My attempts to escape them one or another way didn't lead to anything good so far */ goto cleanup; } append_argument(&args, cmd[i]); ++arg_count; } *cmd_line = args != nullptr ? tr_win32_utf8_to_native(args, -1) : nullptr; ret = true; cleanup: tr_free(args); return ret; } bool tr_spawn_async( char const* const* cmd, std::map const& env, char const* work_dir, tr_error** error) { wchar_t* env_block = nullptr; if (!create_env_block(env, &env_block, error)) { return false; } wchar_t* cmd_line; if (!construct_cmd_line(cmd, &cmd_line)) { set_system_error(error, ERROR_INVALID_PARAMETER, "Constructing command line"); return false; } wchar_t* current_dir = work_dir != nullptr ? tr_win32_utf8_to_native(work_dir, -1) : nullptr; auto si = STARTUPINFOW{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; PROCESS_INFORMATION pi; bool const ret = CreateProcessW( nullptr, cmd_line, nullptr, nullptr, FALSE, NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | CREATE_DEFAULT_ERROR_MODE, env_block, current_dir, &si, &pi); if (ret) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } else { set_system_error(error, GetLastError(), "Call to CreateProcess()"); } tr_free(current_dir); tr_free(cmd_line); tr_free(env_block); return ret; }