// 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 // for wchar_t support #include #include "transmission.h" #include "error.h" #include "subprocess.h" #include "tr-assert.h" #include "utils.h" using namespace std::literals; namespace { enum class tr_app_type { EXE, BATCH }; void set_system_error(tr_error** error, DWORD code, std::string_view what) { if (error == nullptr) { return; } if (auto const message = tr_win32_format_message(code); !std::empty(message)) { tr_error_set(error, code, fmt::format(FMT_STRING("{:s} failed: {:s}"), what, message)); } else { tr_error_set(error, code, fmt::format(FMT_STRING("{:s} failed: Unknown error: {:#08x}"), what, code)); } } // "The sort is case-insensitive, Unicode order, without regard to locale" © MSDN class WStrICompare { public: [[nodiscard]] auto compare(std::wstring_view a, std::wstring_view b) const noexcept // <=> { int diff = wcsnicmp(std::data(a), std::data(b), std::min(std::size(a), std::size(b))); if (diff == 0) { diff = std::size(a) < std::size(b) ? -1 : (std::size(a) > std::size(b) ? 1 : 0); } return diff; } [[nodiscard]] auto operator()(std::wstring_view a, std::wstring_view b) const noexcept // < { return compare(a, b) < 0; } }; using SortedWideEnv = std::map; /* * Var1=Value1\0 * Var2=Value2\0 * Var3=Value3\0 * ... * VarN=ValueN\0\0 */ auto to_env_string(SortedWideEnv const& wide_env) { auto ret = std::vector{}; for (auto const& [key, val] : wide_env) { fmt::format_to(std::back_inserter(ret), FMT_STRING(L"{:s}={:s}"), key, val); ret.insert(std::end(ret), L'\0'); } ret.insert(std::end(ret), L'\0'); return ret; } /* * Var1=Value1\0 * Var2=Value2\0 * Var3=Value3\0 * ... * VarN=ValueN\0\0 */ auto parse_env_string(wchar_t const* env) { auto sorted = SortedWideEnv{}; for (;;) { auto const line = std::wstring_view{ env }; if (std::empty(line)) { break; } if (auto const pos = line.find(L'='); pos != std::string_view::npos) { sorted.insert_or_assign(std::wstring{ line.substr(0, pos) }, std::wstring{ line.substr(pos + 1) }); } env += std::size(line) + 1 /*'\0'*/; } return sorted; } auto get_current_env() { auto env = SortedWideEnv{}; if (auto* pwch = GetEnvironmentStringsW(); pwch != nullptr) { env = parse_env_string(pwch); FreeEnvironmentStringsW(pwch); } return env; } 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'; } bool contains_batch_metachars(char const* text) { /* First part - chars explicitly documented by `cmd.exe /?` as "special" */ return strpbrk( text, "&<>()@^|" "%!^\"") != nullptr; } auto get_app_type(char const* app) { auto const lower = tr_strlower(app); if (tr_strvEndsWith(lower, ".cmd") || tr_strvEndsWith(lower, ".bat")) { return tr_app_type::BATCH; } /* TODO: Support other types? */ return tr_app_type::EXE; } void append_app_launcher_arguments(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, "unsupported application type"); break; } } std::wstring construct_cmd_line(char const* const* cmd) { auto const app_type = get_app_type(cmd[0]); char* args = nullptr; 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 */ tr_free(args); args = nullptr; break; } append_argument(&args, cmd[i]); } if (args != nullptr) { auto cmd_line = tr_win32_utf8_to_native(args); tr_free(args); return cmd_line; } return {}; } } // namespace bool tr_spawn_async( char const* const* cmd, std::map const& env, std::string_view work_dir, tr_error** error) { // full_env = current_env + env; auto full_env = get_current_env(); for (auto const& [key, val] : env) { full_env.insert_or_assign(tr_win32_utf8_to_native(key), tr_win32_utf8_to_native(val)); } auto cmd_line = construct_cmd_line(cmd); if (std::empty(cmd_line)) { set_system_error(error, ERROR_INVALID_PARAMETER, "Constructing command line"); return false; } auto const current_dir = tr_win32_utf8_to_native(work_dir); auto si = STARTUPINFOW{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; PROCESS_INFORMATION pi; bool const ret = CreateProcessW( nullptr, std::data(cmd_line), nullptr, nullptr, FALSE, NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | CREATE_DEFAULT_ERROR_MODE, std::empty(full_env) ? nullptr : to_env_string(full_env).data(), std::empty(current_dir) ? nullptr : current_dir.c_str(), &si, &pi); if (ret) { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } else { set_system_error(error, GetLastError(), "Call to CreateProcess()"); } return ret; }