/ src / common / utils / registry.h
registry.h
  1  #pragma once
  2  
  3  #include <Windows.h>
  4  
  5  #include <functional>
  6  #include <string>
  7  #include <variant>
  8  #include <vector>
  9  #include <optional>
 10  #include <cassert>
 11  #include <sstream>
 12  
 13  #include "../logger/logger.h"
 14  #include "../utils/winapi_error.h"
 15  #include "../version/version.h"
 16  
 17  namespace registry
 18  {
 19      namespace detail
 20      {
 21          struct on_exit
 22          {
 23              std::function<void()> f;
 24  
 25              on_exit(std::function<void()> f) :
 26                  f{ std::move(f) } {}
 27              ~on_exit() { f(); }
 28          };
 29  
 30          template<class... Ts>
 31          struct overloaded : Ts...
 32          {
 33              using Ts::operator()...;
 34          };
 35  
 36          template<class... Ts>
 37          overloaded(Ts...) -> overloaded<Ts...>;
 38  
 39          inline const wchar_t* getScopeName(HKEY scope)
 40          {
 41              if (scope == HKEY_LOCAL_MACHINE)
 42              {
 43                  return L"HKLM";
 44              }
 45              else if (scope == HKEY_CURRENT_USER)
 46              {
 47                  return L"HKCU";
 48              }
 49              else if (scope == HKEY_CLASSES_ROOT)
 50              {
 51                  return L"HKCR";
 52              }
 53              else
 54              {
 55                  return L"HK??";
 56              }
 57          }
 58      }
 59  
 60      namespace install_scope
 61      {
 62          const wchar_t INSTALL_SCOPE_REG_KEY[] = L"Software\\Classes\\powertoys\\";
 63          const wchar_t UNINSTALL_REG_KEY[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
 64          
 65          // Bundle UpgradeCode from PowerToys.wxs (with braces as stored in registry)
 66          const wchar_t BUNDLE_UPGRADE_CODE[] = L"{6341382D-C0A9-4238-9188-BE9607E3FAB2}";
 67  
 68          enum class InstallScope
 69          {
 70              PerMachine = 0,
 71              PerUser,
 72          };
 73  
 74          // Helper function to find PowerToys bundle in Windows Uninstall registry by BundleUpgradeCode
 75          inline bool find_powertoys_bundle_in_uninstall_registry(HKEY rootKey)
 76          {
 77              HKEY uninstallKey{};
 78              if (RegOpenKeyExW(rootKey, UNINSTALL_REG_KEY, 0, KEY_READ, &uninstallKey) != ERROR_SUCCESS)
 79              {
 80                  return false;
 81              }
 82              detail::on_exit closeUninstallKey{ [uninstallKey] { RegCloseKey(uninstallKey); } };
 83  
 84              DWORD index = 0;
 85              wchar_t subKeyName[256];
 86  
 87              // Enumerate all subkeys under Uninstall
 88              while (RegEnumKeyW(uninstallKey, index++, subKeyName, 256) == ERROR_SUCCESS)
 89              {
 90                  HKEY productKey{};
 91                  if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ, &productKey) != ERROR_SUCCESS)
 92                  {
 93                      continue;
 94                  }
 95                  detail::on_exit closeProductKey{ [productKey] { RegCloseKey(productKey); } };
 96  
 97                  // Check BundleUpgradeCode value (specific to WiX Bundle installations)
 98                  wchar_t bundleUpgradeCode[256]{};
 99                  DWORD bundleUpgradeCodeSize = sizeof(bundleUpgradeCode);
100  
101                  if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, nullptr,
102                                      reinterpret_cast<LPBYTE>(bundleUpgradeCode), &bundleUpgradeCodeSize) == ERROR_SUCCESS)
103                  {
104                      if (_wcsicmp(bundleUpgradeCode, BUNDLE_UPGRADE_CODE) == 0)
105                      {
106                          return true;
107                      }
108                  }
109              }
110  
111              return false;
112          }
113  
114          inline const InstallScope get_current_install_scope()
115          {
116              // 1. Check HKCU Uninstall registry first (user-level bundle)
117              // Note: MSI components are always in HKLM regardless of install scope,
118              // but the Bundle entry will be in HKCU for per-user installations
119              if (find_powertoys_bundle_in_uninstall_registry(HKEY_CURRENT_USER))
120              {
121                  Logger::info(L"Found user-level PowerToys bundle via BundleUpgradeCode in HKCU");
122                  return InstallScope::PerUser;
123              }
124  
125              // 2. Check HKLM Uninstall registry (machine-level bundle)
126              if (find_powertoys_bundle_in_uninstall_registry(HKEY_LOCAL_MACHINE))
127              {
128                  Logger::info(L"Found machine-level PowerToys bundle via BundleUpgradeCode in HKLM");
129                  return InstallScope::PerMachine;
130              }
131  
132              // 3. Fallback to legacy custom registry key detection
133              Logger::info(L"PowerToys bundle not found in Uninstall registry, falling back to legacy detection");
134  
135              // Open HKLM key
136              HKEY perMachineKey{};
137              if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
138                                INSTALL_SCOPE_REG_KEY,
139                                0,
140                                KEY_READ,
141                                &perMachineKey) != ERROR_SUCCESS)
142              {
143                  // Open HKCU key
144                  HKEY perUserKey{};
145                  if (RegOpenKeyExW(HKEY_CURRENT_USER,
146                                    INSTALL_SCOPE_REG_KEY,
147                                    0,
148                                    KEY_READ,
149                                    &perUserKey) != ERROR_SUCCESS)
150                  {
151                      // both keys are missing
152                      Logger::warn(L"No PowerToys installation detected, defaulting to PerMachine");
153                      return InstallScope::PerMachine;
154                  }
155                  else
156                  {
157                      DWORD dataSize{};
158                      if (RegGetValueW(
159                          perUserKey,
160                          nullptr,
161                          L"InstallScope",
162                          RRF_RT_REG_SZ,
163                          nullptr,
164                          nullptr,
165                          &dataSize) != ERROR_SUCCESS)
166                      {
167                          // HKCU key is missing
168                          RegCloseKey(perUserKey);
169                          return InstallScope::PerMachine;
170                      }
171  
172                      std::wstring data;
173                      data.resize(dataSize / sizeof(wchar_t));
174  
175                      if (RegGetValueW(
176                              perUserKey,
177                              nullptr,
178                              L"InstallScope",
179                              RRF_RT_REG_SZ,
180                              nullptr,
181                              &data[0],
182                              &dataSize) != ERROR_SUCCESS)
183                      {
184                          // HKCU key is missing
185                          RegCloseKey(perUserKey);
186                          return InstallScope::PerMachine;
187                      }
188                      RegCloseKey(perUserKey);
189  
190                      if (data.contains(L"perUser"))
191                      {
192                          return InstallScope::PerUser;
193                      }
194                  }
195              }
196  
197              return InstallScope::PerMachine;
198          }
199      }
200  
201      template<class>
202      inline constexpr bool always_false_v = false;
203  
204      struct ValueChange
205      {
206          using value_t = std::variant<DWORD, std::wstring>;
207          static constexpr size_t VALUE_BUFFER_SIZE = 512;
208  
209          HKEY scope{};
210          std::wstring path;
211          std::optional<std::wstring> name; // none == default
212          value_t value;
213          bool required = true;
214  
215          ValueChange(const HKEY scope, std::wstring path, std::optional<std::wstring> name, value_t value, bool required = true) :
216              scope{ scope }, path{ std::move(path) }, name{ std::move(name) }, value{ std::move(value) }, required{ required }
217          {
218          }
219  
220          std::wstring toString() const
221          {
222              using namespace detail;
223  
224              std::wstring value_str;
225              std::visit(overloaded{ [&](DWORD value) {
226                                        std::wostringstream oss;
227                                        oss << value;
228                                        value_str = oss.str();
229                                    },
230                                     [&](const std::wstring& value) { value_str = value; } },
231                         value);
232  
233              return fmt::format(L"{}\\{}\\{}:{}", detail::getScopeName(scope), path, name ? *name : L"Default", value_str);
234          }
235  
236          bool isApplied() const
237          {
238              HKEY key{};
239              if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_READ, &key); res != ERROR_SUCCESS)
240              {
241                  Logger::info(L"isApplied of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
242                  return false;
243              }
244              detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
245  
246              const DWORD expectedType = valueTypeToWinapiType(value);
247  
248              DWORD retrievedType{};
249              wchar_t buffer[VALUE_BUFFER_SIZE];
250              DWORD valueSize = sizeof(buffer);
251              if (auto res = RegQueryValueExW(key,
252                                              name.has_value() ? name->c_str() : nullptr,
253                                              0,
254                                              &retrievedType,
255                                              reinterpret_cast<LPBYTE>(&buffer),
256                                              &valueSize);
257                  res != ERROR_SUCCESS)
258              {
259                  Logger::info(L"isApplied of {}: RegQueryValueExW failed: {}", toString(), get_last_error_or_default(res));
260                  return false;
261              }
262  
263              if (expectedType != retrievedType)
264              {
265                  return false;
266              }
267  
268              if (const auto retrievedValue = bufferToValue(buffer, valueSize, retrievedType))
269              {
270                  return value == retrievedValue;
271              }
272              else
273              {
274                  return false;
275              }
276          }
277  
278          bool apply() const
279          {
280              HKEY key{};
281  
282              if (auto res = RegCreateKeyExW(scope, path.c_str(), 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &key, nullptr); res !=
283                                                                                                                                           ERROR_SUCCESS)
284              {
285                  Logger::error(L"apply of {}: RegCreateKeyExW failed: {}", toString(), get_last_error_or_default(res));
286                  return false;
287              }
288              detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
289  
290              wchar_t buffer[VALUE_BUFFER_SIZE];
291              DWORD valueSize;
292              DWORD valueType;
293  
294              valueToBuffer(value, buffer, valueSize, valueType);
295              if (auto res = RegSetValueExW(key,
296                                            name.has_value() ? name->c_str() : nullptr,
297                                            0,
298                                            valueType,
299                                            reinterpret_cast<BYTE*>(buffer),
300                                            valueSize);
301                  res != ERROR_SUCCESS)
302              {
303                  Logger::error(L"apply of {}: RegSetValueExW failed: {}", toString(), get_last_error_or_default(res));
304                  return false;
305              }
306  
307              return true;
308          }
309  
310          bool unApply() const
311          {
312              HKEY key{};
313              if (auto res = RegOpenKeyExW(scope, path.c_str(), 0, KEY_ALL_ACCESS, &key); res != ERROR_SUCCESS)
314              {
315                  Logger::error(L"unApply of {}: RegOpenKeyExW failed: {}", toString(), get_last_error_or_default(res));
316                  return false;
317              }
318              detail::on_exit closeKey{ [key] { RegCloseKey(key); } };
319  
320              // delete the value itself
321              if (auto res = RegDeleteKeyValueW(scope, path.c_str(), name.has_value() ? name->c_str() : nullptr); res != ERROR_SUCCESS)
322              {
323                  Logger::error(L"unApply of {}: RegDeleteKeyValueW failed: {}", toString(), get_last_error_or_default(res));
324                  return false;
325              }
326  
327              // Check if the path doesn't contain anything and delete it if so
328              DWORD nValues = 0;
329              DWORD maxValueLen = 0;
330              const auto ok =
331                  RegQueryInfoKeyW(
332                      key, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &nValues, nullptr, &maxValueLen, nullptr, nullptr) ==
333                  ERROR_SUCCESS;
334  
335              if (ok && (!nValues || !maxValueLen))
336              {
337                  RegDeleteTreeW(scope, path.c_str());
338              }
339              return true;
340          }
341  
342          bool requiresElevation() const { return scope == HKEY_LOCAL_MACHINE; }
343  
344      private:
345          static DWORD valueTypeToWinapiType(const value_t& v)
346          {
347              return std::visit(
348                  [](auto&& arg) {
349                      using T = std::decay_t<decltype(arg)>;
350                      if constexpr (std::is_same_v<T, DWORD>)
351                          return REG_DWORD;
352                      else if constexpr (std::is_same_v<T, std::wstring>)
353                          return REG_SZ;
354                      else
355                          static_assert(always_false_v<T>, "support for this registry type is not implemented");
356                  },
357                  v);
358          }
359  
360          static void valueToBuffer(const value_t& value, wchar_t buffer[VALUE_BUFFER_SIZE], DWORD& valueSize, DWORD& type)
361          {
362              using detail::overloaded;
363  
364              std::visit(overloaded{ [&](DWORD value) {
365                                        *reinterpret_cast<DWORD*>(buffer) = value;
366                                        type = REG_DWORD;
367                                        valueSize = sizeof(value);
368                                    },
369                                     [&](const std::wstring& value) {
370                                         assert(value.size() < VALUE_BUFFER_SIZE);
371                                         value.copy(buffer, value.size());
372                                         type = REG_SZ;
373                                         valueSize = static_cast<DWORD>(sizeof(wchar_t) * value.size());
374                                     } },
375                         value);
376          }
377  
378          static std::optional<value_t> bufferToValue(const wchar_t buffer[VALUE_BUFFER_SIZE],
379                                                      const DWORD valueSize,
380                                                      const DWORD type)
381          {
382              switch (type)
383              {
384              case REG_DWORD:
385                  return *reinterpret_cast<const DWORD*>(buffer);
386              case REG_SZ:
387              {
388                  if (!valueSize)
389                  {
390                      return std::wstring{};
391                  }
392                  std::wstring result{ buffer, valueSize / sizeof(wchar_t) };
393                  while (result[result.size() - 1] == L'\0')
394                  {
395                      result.resize(result.size() - 1);
396                  }
397                  return result;
398              }
399              default:
400                  return std::nullopt;
401              }
402          }
403      };
404  
405      struct ChangeSet
406      {
407          std::vector<ValueChange> changes;
408  
409          bool isApplied() const
410          {
411              for (const auto& c : changes)
412              {
413                  if (c.required && !c.isApplied())
414                  {
415                      return false;
416                  }
417              }
418              return true;
419          }
420  
421          bool apply() const
422          {
423              bool ok = true;
424              for (const auto& c : changes)
425              {
426                  ok = (c.apply()||!c.required) && ok;
427              }
428              return ok;
429          }
430  
431          bool unApply() const
432          {
433              bool ok = true;
434              for (const auto& c : changes)
435              {
436                  ok = (c.unApply()||!c.required) && ok;
437              }
438              return ok;
439          }
440      };
441  
442      const inline std::wstring DOTNET_COMPONENT_CATEGORY_CLSID = L"{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}";
443      const inline std::wstring ITHUMBNAIL_PROVIDER_CLSID = L"{E357FCCD-A995-4576-B01F-234630154E96}";
444      const inline std::wstring IPREVIEW_HANDLER_CLSID = L"{8895b1c6-b41f-4c1c-a562-0d564250836f}";
445  
446      namespace shellex
447      {
448          enum PreviewHandlerType
449          {
450              preview,
451              thumbnail
452          };
453  
454          inline registry::ChangeSet generatePreviewHandler(const PreviewHandlerType handlerType,
455                                                            const bool perUser,
456                                                            std::wstring handlerClsid,
457                                                            std::wstring powertoysVersion,
458                                                            std::wstring fullPathToHandler,
459                                                            std::wstring className,
460                                                            std::wstring displayName,
461                                                            std::vector<std::wstring> fileTypes,
462                                                            std::wstring perceivedType = L"",
463                                                            std::wstring fileKindType = L"")
464          {
465              const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
466  
467              std::wstring clsidPath = L"Software\\Classes\\CLSID";
468              clsidPath += L'\\';
469              clsidPath += handlerClsid;
470  
471              std::wstring inprocServerPath = clsidPath;
472              inprocServerPath += L'\\';
473              inprocServerPath += L"InprocServer32";
474  
475              std::wstring assemblyKeyValue;
476              if (const auto lastDotPos = className.rfind(L'.'); lastDotPos != std::wstring::npos)
477              {
478                  assemblyKeyValue = L"PowerToys." + className.substr(lastDotPos + 1);
479              }
480              else
481              {
482                  assemblyKeyValue = L"PowerToys." + className;
483              }
484  
485              assemblyKeyValue += L", Version=";
486              assemblyKeyValue += powertoysVersion;
487              assemblyKeyValue += L", Culture=neutral";
488  
489              std::wstring versionPath = inprocServerPath;
490              versionPath += L'\\';
491              versionPath += powertoysVersion;
492  
493              using vec_t = std::vector<registry::ValueChange>;
494              // TODO: verify that we actually need all of those
495              vec_t changes = { { scope, clsidPath, L"DisplayName", displayName },
496                                { scope, clsidPath, std::nullopt, className },
497                                { scope, inprocServerPath, std::nullopt, fullPathToHandler },
498                                { scope, inprocServerPath, L"Assembly", assemblyKeyValue },
499                                { scope, inprocServerPath, L"Class", className },
500                                { scope, inprocServerPath, L"ThreadingModel", L"Apartment" } };
501  
502              for (const auto& fileType : fileTypes)
503              {
504                  std::wstring fileTypePath = L"Software\\Classes\\" + fileType;
505                  std::wstring fileAssociationPath = fileTypePath + L"\\shellex\\";
506                  fileAssociationPath += handlerType == PreviewHandlerType::preview ? IPREVIEW_HANDLER_CLSID : ITHUMBNAIL_PROVIDER_CLSID;
507                  changes.push_back({ scope, fileAssociationPath, std::nullopt, handlerClsid });
508                  if (!fileKindType.empty())
509                  {
510                      // Registering a file type as a kind needs to be done at the HKEY_LOCAL_MACHINE level.
511                      // Make it optional as well so that we don't fail registering the handler if we can't write to HKEY_LOCAL_MACHINE.
512                      std::wstring kindMapPath = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\KindMap";
513                      changes.push_back({ HKEY_LOCAL_MACHINE, kindMapPath, fileType, fileKindType, false});
514                  }
515                  if (!perceivedType.empty())
516                  {
517                      changes.push_back({ scope, fileTypePath, L"PerceivedType", perceivedType });
518                  }
519                  if (handlerType == PreviewHandlerType::preview && fileType == L".reg")
520                  {
521                      // this regfile registry key has precedence over Software\Classes\.reg for .reg files
522                      std::wstring regfilePath = L"Software\\Classes\\regfile\\shellex\\" + IPREVIEW_HANDLER_CLSID + L"\\";
523                      changes.push_back({ scope, regfilePath, std::nullopt, handlerClsid });
524                  }
525              }
526  
527              if (handlerType == PreviewHandlerType::preview)
528              {
529                  const std::wstring previewHostClsid = L"{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}";
530                  const std::wstring previewHandlerListPath = LR"(Software\Microsoft\Windows\CurrentVersion\PreviewHandlers)";
531  
532                  changes.push_back({ scope, clsidPath, L"AppID", previewHostClsid });
533                  changes.push_back({ scope, previewHandlerListPath, handlerClsid, displayName });
534              }
535  
536              return registry::ChangeSet{ .changes = std::move(changes) };
537          }
538      }
539  }