blob: f1f29e14e5c2d5db7a969ed481abf837fac1fcc3 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/updater/win/app_command_runner.h"
#include <shellapi.h>
#include <shlobj.h>
#include <windows.h>
#include <array>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/test_timeouts.h"
#include "build/branding_buildflags.h"
#include "chrome/updater/test_scope.h"
#include "chrome/updater/updater_branding.h"
#include "chrome/updater/updater_version.h"
#include "chrome/updater/util/unittest_util_win.h"
#include "chrome/updater/util/win_util.h"
#include "chrome/updater/win/test/test_executables.h"
#include "chrome/updater/win/win_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace updater {
namespace {
constexpr wchar_t kAppId1[] = L"{3B1A3CCA-0525-4418-93E6-A0DB3398EC9B}";
constexpr wchar_t kCmdLineValid[] =
L"\"C:\\Program Files\\Windows Media Player\\wmpnscfg.exe\" /Close";
constexpr wchar_t kCmdId1[] = L"command 1";
constexpr wchar_t kCmdId2[] = L"command 2";
std::wstring GetCommandLine(int key, const std::wstring& exe_name) {
base::FilePath programfiles_path;
EXPECT_TRUE(base::PathService::Get(key, &programfiles_path));
return base::CommandLine(programfiles_path.Append(exe_name))
.GetCommandLineString();
}
} // namespace
class AppCommandRunnerTest : public testing::Test {
protected:
AppCommandRunnerTest() = default;
~AppCommandRunnerTest() override = default;
void SetUp() override {
SetupCmdExe(GetTestScope(), cmd_exe_command_line_, temp_programfiles_dir_);
}
void TearDown() override { DeleteAppClientKey(GetTestScope(), kAppId1); }
HRESULT CreateAppCommandRunner(const std::wstring& app_id,
const std::wstring& command_id,
const std::wstring& command_line_format,
AppCommandRunner& app_command_runner) {
CreateAppCommandRegistry(GetTestScope(), app_id, command_id,
command_line_format);
return AppCommandRunner::LoadAppCommand(GetTestScope(), app_id, command_id,
app_command_runner);
}
HRESULT CreateProcessLauncherRunner(const std::wstring& app_id,
const std::wstring& name,
const std::wstring& pv,
const std::wstring& command_id,
const std::wstring& command_line_format,
AppCommandRunner& app_command_runner) {
EXPECT_TRUE(IsSystemInstall(GetTestScope()));
CreateLaunchCmdElevatedRegistry(app_id, name, pv, command_id,
command_line_format);
return AppCommandRunner::LoadAppCommand(GetTestScope(), app_id, command_id,
app_command_runner);
}
base::CommandLine cmd_exe_command_line_{base::CommandLine::NO_PROGRAM};
base::ScopedTempDir temp_programfiles_dir_;
};
TEST_F(AppCommandRunnerTest, GetAppCommandFormatComponents_InvalidPaths) {
const struct {
const UpdaterScope scope;
const wchar_t* command_format;
} test_cases[] = {
// Relative paths are invalid.
{UpdaterScope::kUser, L"process.exe"},
// Paths not under %ProgramFiles% or %ProgramFilesX86% are invalid for
// system.
{UpdaterScope::kSystem, L"\"C:\\foobar\\process.exe\""},
{UpdaterScope::kSystem, L"C:\\ProgramFiles\\process.exe"},
{UpdaterScope::kSystem, L"C:\\windows\\system32\\cmd.exe"},
};
base::FilePath executable;
std::vector<std::wstring> parameters;
for (const auto& test_case : test_cases) {
EXPECT_EQ(
AppCommandRunner::GetAppCommandFormatComponents(
test_case.scope, test_case.command_format, executable, parameters),
E_INVALIDARG);
}
}
TEST_F(AppCommandRunnerTest, GetAppCommandFormatComponents_ProgramFilesPaths) {
base::FilePath executable;
std::vector<std::wstring> parameters;
for (const int key : {base::DIR_PROGRAM_FILES, base::DIR_PROGRAM_FILESX86,
base::DIR_PROGRAM_FILES6432}) {
const std::wstring process_command_line =
GetCommandLine(key, L"process.exe");
ASSERT_EQ(AppCommandRunner::GetAppCommandFormatComponents(
GetTestScope(), process_command_line, executable, parameters),
S_OK);
EXPECT_EQ(executable,
base::CommandLine::FromString(process_command_line).GetProgram());
EXPECT_TRUE(parameters.empty());
}
}
TEST_F(AppCommandRunnerTest,
GetAppCommandFormatComponents_And_FormatAppCommandLine) {
const std::vector<std::wstring> nosubstitutions = {};
const std::vector<std::wstring> p1p2p3 = {L"p1", L"p2", L"p3"};
const struct {
std::vector<std::wstring> input;
const wchar_t* output;
const std::vector<std::wstring>& substitutions;
} test_cases[] = {
// Unformatted parameters.
{{L"abc=1"}, L"abc=1", nosubstitutions},
{{L"abc=1", L"xyz=2"}, L"abc=1 xyz=2", nosubstitutions},
{{L"abc=1", L"xyz=2", L"q"}, L"abc=1 xyz=2 q", nosubstitutions},
{{L" abc=1 ", L" xyz=2", L"q "}, L"abc=1 xyz=2 q", nosubstitutions},
{{L"\"abc = 1\""}, L"\"abc = 1\"", nosubstitutions},
{{L"abc\" = \"1", L"xyz=2"}, L"\"abc = 1\" xyz=2", nosubstitutions},
{{L"\"abc = 1\""}, L"\"abc = 1\"", nosubstitutions},
{{L"abc\" = \"1"}, L"\"abc = 1\"", nosubstitutions},
// Simple parameters.
{{L"abc=%1"}, L"abc=p1", p1p2p3},
{{L"abc=%1 ", L" %3", L" %2=x "}, L"abc=p1 p3 p2=x", p1p2p3},
// Escaping valid `%` signs.
{{L"%1"}, L"p1", p1p2p3},
{{L"%%1"}, L"%1", p1p2p3},
{{L"%%%1"}, L"%p1", p1p2p3},
{{L"abc%%def%%"}, L"abc%def%", p1p2p3},
{{L"%12"}, L"p12", p1p2p3},
{{L"%1%2"}, L"p1p2", p1p2p3},
// Invalid `%` signs.
{{L"unescaped", L"percent", L"%"}, nullptr, p1p2p3},
{{L"unescaped", L"%%%", L"percents"}, nullptr, p1p2p3},
{{L"always", L"escape", L"percent", L"otherwise", L"%foobar"},
nullptr,
p1p2p3},
{{L"%", L"percents", L"need", L"to", L"be", L"escaped%"},
nullptr,
p1p2p3},
// Parameter index invalid or greater than substitutions.
{{L"placeholder", L"needs", L"to", L"be", L"between", L"1", L"and", L"9,",
L"not", L"%A"},
nullptr,
p1p2p3},
{{L"placeholder", L"%4 ", L"is", L">", L"size", L"of", L"input",
L"substitutions"},
nullptr,
p1p2p3},
{{L"%1", L"is", L"ok,", L"but", L"%8", L"or", L"%9", L"is", L"not",
L"ok"},
nullptr,
p1p2p3},
{{L"%4"}, nullptr, p1p2p3},
{{L"abc=%1"}, nullptr, nosubstitutions},
// Special characters in the substitution.
// embedded \ and \\.
{{L"%1"}, L"a\\b\\\\c", {L"a\\b\\\\c"}},
// trailing \.
{{L"%1"}, L"a\\", {L"a\\"}},
// trailing \\.
{{L"%1"}, L"a\\\\", {L"a\\\\"}},
// only \\.
{{L"%1"}, L"\\\\", {L"\\\\"}},
// empty.
{{L"%1"}, L"\"\"", {L""}},
// embedded quote.
{{L"%1"}, L"a\\\"b", {L"a\"b"}},
// trailing quote.
{{L"%1"}, L"abc\\\"", {L"abc\""}},
// embedded \\".
{{L"%1"}, L"a\\\\\\\\\\\"b", {L"a\\\\\"b"}},
// trailing \\".
{{L"%1"}, L"abc\\\\\\\\\\\"", {L"abc\\\\\""}},
// embedded space.
{{L"%1"}, L"\"abc def\"", {L"abc def"}},
// trailing space.
{{L"%1"}, L"\"abcdef \"", {L"abcdef "}},
// leading space.
{{L"%1"}, L"\" abcdef\"", {L" abcdef"}},
};
const std::wstring process_command_line =
GetCommandLine(base::DIR_PROGRAM_FILES, L"process.exe");
base::FilePath executable;
std::vector<std::wstring> parameters;
for (const auto& test_case : test_cases) {
ASSERT_EQ(AppCommandRunner::GetAppCommandFormatComponents(
GetTestScope(),
base::StrCat({process_command_line, L" ",
base::JoinString(test_case.input, L" ")}),
executable, parameters),
S_OK);
EXPECT_EQ(executable,
base::CommandLine::FromString(process_command_line).GetProgram());
EXPECT_EQ(parameters.size(), test_case.input.size());
absl::optional<std::wstring> command_line =
AppCommandRunner::FormatAppCommandLine(parameters,
test_case.substitutions);
if (!test_case.output) {
EXPECT_EQ(command_line, absl::nullopt);
continue;
}
EXPECT_EQ(command_line.value(), test_case.output);
if (test_case.input[0] != L"%1" || test_case.substitutions.size() != 1)
continue;
// The formatted output is now sent through ::CommandLineToArgvW to
// verify that it produces the original substitution.
std::wstring cmd = base::StrCat({L"process.exe ", command_line.value()});
int num_args = 0;
ScopedLocalAlloc argv_handle(::CommandLineToArgvW(&cmd[0], &num_args));
ASSERT_TRUE(argv_handle.is_valid());
EXPECT_EQ(num_args, 2) << "substitution '" << test_case.substitutions[0]
<< "' gave command line '" << cmd
<< "' which unexpectedly did not parse to a single "
<< "argument.";
EXPECT_EQ(reinterpret_cast<const wchar_t**>(argv_handle.get())[1],
test_case.substitutions[0])
<< "substitution '" << test_case.substitutions[0]
<< "' gave command line '" << cmd
<< "' which did not parse back to the "
<< "original substitution";
}
}
TEST_F(AppCommandRunnerTest, ExecuteAppCommand) {
const struct {
const std::vector<std::wstring> input;
const std::vector<std::wstring> substitutions;
const int expected_exit_code;
} test_cases[] = {
{{L"/c", L"exit 7"}, {}, 7},
{{L"/c", L"exit %1"}, {L"5420"}, 5420},
};
for (const auto& test_case : test_cases) {
base::Process process;
ASSERT_HRESULT_SUCCEEDED(AppCommandRunner::ExecuteAppCommand(
cmd_exe_command_line_.GetProgram(), test_case.input,
test_case.substitutions, process));
int exit_code = 0;
EXPECT_TRUE(process.WaitForExitWithTimeout(
TestTimeouts::action_max_timeout(), &exit_code));
EXPECT_EQ(exit_code, test_case.expected_exit_code);
}
}
TEST_F(AppCommandRunnerTest, NoApp) {
AppCommandRunner app_command_runner;
EXPECT_HRESULT_FAILED(AppCommandRunner::LoadAppCommand(
GetTestScope(), kAppId1, kCmdId1, app_command_runner));
}
TEST_F(AppCommandRunnerTest, NoCmd) {
AppCommandRunner app_command_runner;
CreateAppCommandRegistry(GetTestScope(), kAppId1, kCmdId1, kCmdLineValid);
EXPECT_HRESULT_FAILED(AppCommandRunner::LoadAppCommand(
GetTestScope(), kAppId1, kCmdId2, app_command_runner));
}
TEST_F(AppCommandRunnerTest, RunAppCommandFormat) {
const struct {
const std::vector<std::wstring> input;
const std::vector<std::wstring> substitutions;
const int expected_exit_code;
} test_cases[] = {
{{L"/c", L"exit 7"}, {}, 7},
{{L"/c", L"exit %1"}, {L"5420"}, 5420},
};
for (const auto& test_case : test_cases) {
AppCommandRunner app_command_runner;
base::Process process;
ASSERT_EQ(app_command_runner.Run(test_case.substitutions, process),
E_UNEXPECTED);
ASSERT_HRESULT_SUCCEEDED(CreateAppCommandRunner(
kAppId1, kCmdId1,
base::StrCat({cmd_exe_command_line_.GetCommandLineString(), L" ",
base::JoinString(test_case.input, L" ")}),
app_command_runner));
ASSERT_HRESULT_SUCCEEDED(
app_command_runner.Run(test_case.substitutions, process));
int exit_code = 0;
EXPECT_TRUE(process.WaitForExitWithTimeout(
TestTimeouts::action_max_timeout(), &exit_code));
EXPECT_EQ(exit_code, test_case.expected_exit_code);
}
}
TEST_F(AppCommandRunnerTest, CheckChromeBrandedName) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
EXPECT_STREQ("Google Chrome", BROWSER_PRODUCT_NAME_STRING);
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
}
TEST_F(AppCommandRunnerTest, RunProcessLauncherFormat) {
if (!IsSystemInstall(GetTestScope()))
return;
const struct {
const wchar_t* app_name;
const wchar_t* app_version;
const wchar_t* cmd_id;
const std::vector<std::wstring> input;
const int expected_exit_code;
const int expected_hr;
} test_cases[] = {
{L"foo", L"1.0.0.0", L"cmd1", {L"/c", L"exit 7"}, 7, E_INVALIDARG},
{L"foo", L"110.0.5434.0", L"cmd", {L"/c", L"exit 7"}, 7, E_INVALIDARG},
{L"Chrome", L"110.0.5434.0", L"cmd", {L"/c", L"exit 7"}, 7, E_INVALIDARG},
{L"Not" BROWSER_PRODUCT_NAME_STRING,
L"110.0.5434.0",
L"cmd",
{L"/c", L"exit 7"},
7,
E_INVALIDARG},
{L"" BROWSER_PRODUCT_NAME_STRING,
L"110.0.5434.0",
L"cmd",
{L"/c", L"exit 7"},
7,
S_OK},
{L"" BROWSER_PRODUCT_NAME_STRING L" Beta",
L"1.0.0.0",
L"cmd",
{L"/c", L"exit 7"},
7,
S_OK},
{L"" BROWSER_PRODUCT_NAME_STRING " Dev",
L"110.0.0.0",
L"cmd",
{L"/c", L"exit 7"},
7,
S_OK},
{L"" BROWSER_PRODUCT_NAME_STRING " SxS",
L"110.0.0.0",
L"cmd",
{L"/c", L"exit 7"},
7,
S_OK},
};
for (const auto& test_case : test_cases) {
AppCommandRunner app_command_runner;
base::Process process;
ASSERT_EQ(app_command_runner.Run({}, process), E_UNEXPECTED);
ASSERT_EQ(CreateProcessLauncherRunner(
kAppId1, test_case.app_name, test_case.app_version,
test_case.cmd_id,
base::StrCat({cmd_exe_command_line_.GetCommandLineString(),
L" ", base::JoinString(test_case.input, L" ")}),
app_command_runner),
test_case.expected_hr);
if (FAILED(test_case.expected_hr))
continue;
ASSERT_HRESULT_SUCCEEDED(app_command_runner.Run({}, process));
int exit_code = 0;
EXPECT_TRUE(process.WaitForExitWithTimeout(
TestTimeouts::action_max_timeout(), &exit_code));
EXPECT_EQ(exit_code, test_case.expected_exit_code);
}
}
TEST_F(AppCommandRunnerTest, RunBothFormats) {
if (!IsSystemInstall(GetTestScope()))
return;
const struct {
const wchar_t* cmd_id_to_execute;
const wchar_t* cmd_id_appcommand;
const std::vector<std::wstring> input_appcommand;
const wchar_t* cmd_id_processlauncher;
const std::vector<std::wstring> input_processlauncher;
const int expected_exit_code;
} test_cases[] = {
// both formats in registry; AppCommand overrides ProcessLauncher entry.
{L"cmd", L"cmd", {L"/c", L"exit 7"}, L"cmd", {L"/c", L"exit 14"}, 7},
// only AppCommand format in registry.
{L"cmd", L"cmd", {L"/c", L"exit 21"}, {}, {}, 21},
// only ProcessLauncher format in registry.
{L"cmd", {}, {}, L"cmd", {L"/c", L"exit 28"}, 28},
// both formats in registry, but AppCommand has a different command ID, so
// does not override ProcessLauncher entry.
{L"cmd", L"cmd2", {L"/c", L"exit 7"}, L"cmd", {L"/c", L"exit 35"}, 35},
};
for (const auto& test_case : test_cases) {
AppCommandRunner app_command_runner;
base::Process process;
ASSERT_EQ(app_command_runner.Run({}, process), E_UNEXPECTED);
if (test_case.cmd_id_appcommand) {
CreateAppCommandRegistry(
GetTestScope(), kAppId1, test_case.cmd_id_appcommand,
base::StrCat({cmd_exe_command_line_.GetCommandLineString(), L" ",
base::JoinString(test_case.input_appcommand, L" ")}));
}
if (test_case.cmd_id_processlauncher) {
CreateLaunchCmdElevatedRegistry(
kAppId1, L"" BROWSER_PRODUCT_NAME_STRING, L"1.0.0.0",
test_case.cmd_id_processlauncher,
base::StrCat(
{cmd_exe_command_line_.GetCommandLineString(), L" ",
base::JoinString(test_case.input_processlauncher, L" ")}));
}
ASSERT_HRESULT_SUCCEEDED(AppCommandRunner::LoadAppCommand(
GetTestScope(), kAppId1, test_case.cmd_id_to_execute,
app_command_runner));
ASSERT_HRESULT_SUCCEEDED(app_command_runner.Run({}, process));
int exit_code = 0;
EXPECT_TRUE(process.WaitForExitWithTimeout(
TestTimeouts::action_max_timeout(), &exit_code));
EXPECT_EQ(exit_code, test_case.expected_exit_code);
DeleteAppClientKey(GetTestScope(), kAppId1);
}
}
TEST_F(AppCommandRunnerTest, LoadAutoRunOnOsUpgradeAppCommands) {
const struct {
const std::vector<std::wstring> input;
const wchar_t* command_id;
} test_cases[] = {
{{L"/c", L"exit 7"}, kCmdId1},
{{L"/c", L"exit 5420"}, kCmdId2},
};
base::ranges::for_each(
test_cases, [&](const auto& test_case) {
CreateAppCommandOSUpgradeRegistry(
GetTestScope(), kAppId1, test_case.command_id,
base::StrCat({cmd_exe_command_line_.GetCommandLineString(), L" ",
base::JoinString(test_case.input, L" ")}));
});
const std::vector<AppCommandRunner> app_command_runners =
AppCommandRunner::LoadAutoRunOnOsUpgradeAppCommands(GetTestScope(),
kAppId1);
ASSERT_EQ(std::size(app_command_runners), std::size(test_cases));
base::ranges::for_each(
app_command_runners, [&](const auto& app_command_runner) {
base::Process process;
EXPECT_HRESULT_SUCCEEDED(app_command_runner.Run({}, process));
EXPECT_TRUE(process.WaitForExit(/*exit_code=*/nullptr));
});
}
} // namespace updater