-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp_index.cpp
More file actions
258 lines (226 loc) · 7.52 KB
/
Copy pathapp_index.cpp
File metadata and controls
258 lines (226 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#include "app_index.hpp"
#include <algorithm>
#include <cctype>
#include <system_error>
#include <vector>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <filesystem>
#endif
namespace droidcli::cli {
#ifdef _WIN32
namespace {
namespace fs = std::filesystem;
core::String read_registry_string(HKEY key, const char* value_name)
{
DWORD type = 0;
DWORD size = 0;
if (RegQueryValueExA(key, value_name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS || size == 0)
{
return {};
}
if (type != REG_SZ && type != REG_EXPAND_SZ)
{
return {};
}
std::vector<char> buffer(static_cast<size_t>(size) + 1, '\0');
if (RegQueryValueExA(key, value_name, nullptr, &type, reinterpret_cast<LPBYTE>(buffer.data()), &size) != ERROR_SUCCESS)
{
return {};
}
return core::String(buffer.data());
}
bool read_registry_dword(HKEY key, const char* value_name, DWORD& out_value)
{
DWORD type = 0;
DWORD size = sizeof(DWORD);
if (RegQueryValueExA(key, value_name, nullptr, &type, reinterpret_cast<LPBYTE>(&out_value), &size) != ERROR_SUCCESS)
{
return false;
}
return type == REG_DWORD;
}
// "C:\Path\App.exe,0" -> "C:\Path\App.exe". Only strips a trailing comma
// suffix if what follows it is a plain (optionally negative) integer, since
// some installers legitimately put a comma inside the path itself.
core::String strip_icon_index(const core::String& display_icon)
{
const size_t comma = display_icon.rfind(',');
if (comma == core::String::npos)
{
return display_icon;
}
bool looks_like_index = comma + 1 < display_icon.size();
for (size_t i = comma + 1; i < display_icon.size() && looks_like_index; ++i)
{
if (std::isdigit(static_cast<unsigned char>(display_icon[i])) == 0 && display_icon[i] != '-')
{
looks_like_index = false;
}
}
return looks_like_index ? display_icon.substr(0, comma) : display_icon;
}
core::String to_lower(const core::String& value)
{
core::String result = value;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
// Some installers put a non-executable path in DisplayIcon (a bare .ico
// file, an Uninstall.exe, etc.) - checking existence alone isn't enough to
// call it a usable launch target.
bool is_usable_exe_path(const fs::path& path)
{
std::error_code error;
return to_lower(path.extension().string()) == ".exe"
&& fs::exists(path, error) && !fs::is_directory(path, error);
}
// Shallow (depth-limited) search of `directory` for a .exe whose stem
// resembles `display_name`, falling back to the first .exe found. Used when
// DisplayIcon isn't a usable direct path - InstallLocation points at a
// folder, not a specific executable, so this is a best-effort guess.
core::String find_exe_in_directory(const fs::path& directory, const core::String& display_name)
{
if (directory.empty())
{
return {};
}
std::error_code error;
if (!fs::exists(directory, error) || !fs::is_directory(directory, error))
{
return {};
}
const core::String lower_name = to_lower(display_name);
core::String fallback;
constexpr int kMaxDepth = 2;
fs::recursive_directory_iterator it(directory, fs::directory_options::skip_permission_denied, error);
const fs::recursive_directory_iterator end;
for (; it != end && !error; it.increment(error))
{
if (it.depth() > kMaxDepth)
{
it.disable_recursion_pending();
continue;
}
if (it->path().extension() != ".exe")
{
continue;
}
const core::String stem = to_lower(it->path().stem().string());
if (!lower_name.empty()
&& (lower_name.find(stem) != core::String::npos || stem.find(lower_name) != core::String::npos))
{
return it->path().string();
}
if (fallback.empty())
{
fallback = it->path().string();
}
}
return fallback;
}
void scan_uninstall_hive(HKEY root, const char* subkey_path, core::Array<InstalledApp>& out_apps)
{
HKEY uninstall_key = nullptr;
if (RegOpenKeyExA(root, subkey_path, 0, KEY_READ, &uninstall_key) != ERROR_SUCCESS)
{
return;
}
for (DWORD index = 0;; ++index)
{
char subkey_name[256] = {};
DWORD subkey_name_size = sizeof(subkey_name);
const LONG enum_result = RegEnumKeyExA(
uninstall_key, index, subkey_name, &subkey_name_size, nullptr, nullptr, nullptr, nullptr);
if (enum_result == ERROR_NO_MORE_ITEMS)
{
break;
}
if (enum_result != ERROR_SUCCESS)
{
continue;
}
HKEY entry_key = nullptr;
if (RegOpenKeyExA(uninstall_key, subkey_name, 0, KEY_READ, &entry_key) != ERROR_SUCCESS)
{
continue;
}
DWORD system_component = 0;
read_registry_dword(entry_key, "SystemComponent", system_component);
const core::String display_name = read_registry_string(entry_key, "DisplayName");
if (!display_name.empty() && system_component == 0)
{
core::String path = strip_icon_index(read_registry_string(entry_key, "DisplayIcon"));
if (path.empty() || !is_usable_exe_path(fs::path(path)))
{
const core::String install_location = read_registry_string(entry_key, "InstallLocation");
path = find_exe_in_directory(fs::path(install_location), display_name);
}
if (!path.empty())
{
out_apps.push_back(InstalledApp{display_name, path});
}
}
RegCloseKey(entry_key);
}
RegCloseKey(uninstall_key);
}
// Windows accessories/system tools (Notepad, Calculator, Paint, ...) are not
// installed applications in the Add/Remove Programs sense - they ship with
// the OS and never register an Uninstall entry, so scan_uninstall_hive()
// above can never find them. They resolve fine via bare PATH lookup
// (System32 is always on PATH) once launch_application() tries them, but
// find_application()/find_installed_app_match() only searches this index -
// without an entry here, a query like "notepad" or "calculator" comes back
// with zero matches even though opening it would have worked. Listing them
// explicitly closes that gap and lets find_application answer confidently
// for the apps a user is most likely to ask for by a common name.
void add_builtin_accessories(core::Array<InstalledApp>& out_apps)
{
static const struct { const char* name; const char* exe; } kBuiltins[] = {
{"Notepad", "notepad.exe"},
{"Calculator", "calc.exe"},
{"Paint", "mspaint.exe"},
{"WordPad", "write.exe"},
{"Command Prompt", "cmd.exe"},
{"PowerShell", "powershell.exe"},
{"File Explorer", "explorer.exe"},
{"Task Manager", "taskmgr.exe"},
{"Control Panel", "control.exe"},
{"Snipping Tool", "snippingtool.exe"},
{"Magnifier", "magnify.exe"},
{"Registry Editor", "regedit.exe"},
{"Character Map", "charmap.exe"},
{"Remote Desktop Connection", "mstsc.exe"},
{"Disk Cleanup", "cleanmgr.exe"},
};
for (const auto& builtin : kBuiltins)
{
out_apps.push_back(InstalledApp{builtin.name, builtin.exe});
}
}
} // namespace
core::Array<InstalledApp> scan_installed_applications()
{
core::Array<InstalledApp> apps;
// Native view (64-bit apps on 64-bit Windows) and the explicit
// WOW6432Node path (32-bit apps) - using the literal WOW6432Node path
// rather than a KEY_WOW64_32KEY flag so this works the same whether
// droidcli itself is built 32- or 64-bit.
scan_uninstall_hive(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", apps);
scan_uninstall_hive(HKEY_LOCAL_MACHINE, "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", apps);
scan_uninstall_hive(HKEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", apps);
add_builtin_accessories(apps);
return apps;
}
#else
core::Array<InstalledApp> scan_installed_applications()
{
return {};
}
#endif
} // namespace droidcli::cli