blob: 6c1b97e923086c0abefc8dc8d421d2557b3e83a4 [file] [edit]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/win/shell.h"
#include <shlobj.h>
#include "base/base_paths.h"
#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/scoped_com_initializer.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ui::win {
class ShellTest : public testing::TestWithParam<bool> {
public:
ShellTest() {
feature_list_.InitWithFeatureState(kManuallyParsePathForShellExecute,
GetParam());
}
void SetUp() override {
#if defined(ADDRESS_SANITIZER) || defined(COMPONENT_BUILD)
GTEST_SKIP() << "Test not supported in component or asan builds because "
"binary must be standalone";
#endif // defined(ADDRESS_SANITIZER) || defined(COMPONENT_BUILD)
}
protected:
base::test::TaskEnvironment task_environment_;
base::test::ScopedFeatureList feature_list_;
};
TEST_P(ShellTest, OpenFileWithSpaceInExtension) {
base::win::ScopedCOMInitializer com_initializer;
ASSERT_TRUE(com_initializer.Succeeded());
const auto FeatureEnabled = GetParam;
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
// Create a file with a name that triggers legacy resolution (space in
// extension).
const base::FilePath file_path = temp_dir.GetPath().Append(L"test. txt");
ASSERT_TRUE(base::WriteFile(file_path, "test content"));
// Find the helper executable.
base::FilePath exe_path;
ASSERT_TRUE(base::PathService::Get(base::DIR_EXE, &exe_path));
exe_path = exe_path.Append(L"shell_test_helper.exe");
ASSERT_TRUE(base::PathExists(exe_path));
// Create the "legacy resolution" target that ShellExecute might mistakenly
// pick.
base::FilePath legacy_exe_path = temp_dir.GetPath().Append(L"test. txt.exe");
ASSERT_TRUE(base::CopyFile(exe_path, legacy_exe_path));
// The helper will create "executed.txt" in its current directory.
// ui::win::OpenFileViaShell sets the working directory to the file's DirName.
base::FilePath executed_sentinel = temp_dir.GetPath().Append(L"executed.txt");
ASSERT_FALSE(base::PathExists(executed_sentinel));
if (FeatureEnabled()) {
// When feature is enabled, ui::win uses SHParseDisplayName.
// Verify that SHParseDisplayName resolves to the correct file.
base::win::ScopedCoMem<ITEMIDLIST_ABSOLUTE> path_id_list;
ASSERT_TRUE(SUCCEEDED(::SHParseDisplayName(file_path.value().c_str(),
nullptr, &path_id_list,
SFGAO_FILESYSTEM, nullptr)));
wchar_t parsed_path[MAX_PATH];
ASSERT_TRUE(::SHGetPathFromIDList(path_id_list.get(), parsed_path));
base::FilePath parsed(parsed_path);
EXPECT_EQ(file_path, parsed);
}
base::RunLoop sentinel_run_loop;
base::FilePathWatcher watcher;
// Watch the directory for the creation of the sentinel file.
ASSERT_TRUE(watcher.Watch(
temp_dir.GetPath(), base::FilePathWatcher::Type::kNonRecursive,
base::BindLambdaForTesting([&](const base::FilePath& path, bool error) {
if (!error && base::PathExists(path.Append(L"executed.txt"))) {
sentinel_run_loop.Quit();
}
})));
ASSERT_TRUE(OpenFileViaShell(file_path));
if (FeatureEnabled()) {
// Feature enabled: it will not execute the exe, the openwith dialog will
// appear instead, which the test cannot easily check for. The runloop still
// needs to exit, so wait the time it might take to launch a process, then
// terminate it.
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, sentinel_run_loop.QuitClosure(), base::Seconds(3));
}
sentinel_run_loop.Run();
EXPECT_EQ(FeatureEnabled(), !base::PathExists(executed_sentinel));
}
INSTANTIATE_TEST_SUITE_P(, ShellTest, testing::Bool(), [](const auto& info) {
return info.param ? "ManuallyParsed" : "AutomaticallyParsed";
});
} // namespace ui::win