Win32Program.cs
   1  // Copyright (c) Microsoft Corporation
   2  // The Microsoft Corporation licenses this file to you under the MIT license.
   3  // See the LICENSE file in the project root for more information.
   4  
   5  using System;
   6  using System.Collections.Generic;
   7  using System.Diagnostics;
   8  using System.Globalization;
   9  using System.IO;
  10  using System.IO.Abstractions;
  11  using System.Linq;
  12  using System.Reflection;
  13  using System.Security;
  14  using System.Text.RegularExpressions;
  15  using System.Threading.Tasks;
  16  using System.Windows.Input;
  17  
  18  using Microsoft.Plugin.Program.Logger;
  19  using Microsoft.Plugin.Program.Utils;
  20  using Microsoft.Win32;
  21  using Wox.Infrastructure;
  22  using Wox.Infrastructure.FileSystemHelper;
  23  using Wox.Plugin;
  24  using Wox.Plugin.Logger;
  25  
  26  using DirectoryWrapper = Wox.Infrastructure.FileSystemHelper.DirectoryWrapper;
  27  
  28  namespace Microsoft.Plugin.Program.Programs
  29  {
  30      [Serializable]
  31      public class Win32Program : IProgram
  32      {
  33          public static readonly Win32Program InvalidProgram = new Win32Program { Valid = false, Enabled = false };
  34  
  35          private static readonly IFileSystem FileSystem = new FileSystem();
  36          private static readonly IPath Path = FileSystem.Path;
  37          private static readonly IFile File = FileSystem.File;
  38          private static readonly IDirectory Directory = FileSystem.Directory;
  39  
  40          public string Name { get; set; }
  41  
  42          // Localized name based on windows display language
  43          public string NameLocalized { get; set; } = string.Empty;
  44  
  45          public string UniqueIdentifier { get; set; }
  46  
  47          public string IcoPath { get; set; }
  48  
  49          public string Description { get; set; } = string.Empty;
  50  
  51          // Path of app executable or lnk target executable
  52          public string FullPath { get; set; }
  53  
  54          // Localized path based on windows display language
  55          public string FullPathLocalized { get; set; } = string.Empty;
  56  
  57          public string ParentDirectory { get; set; }
  58  
  59          public string ExecutableName { get; set; }
  60  
  61          // Localized executable name based on windows display language
  62          public string ExecutableNameLocalized { get; set; } = string.Empty;
  63  
  64          // Path to the lnk file on LnkProgram
  65          public string LnkFilePath { get; set; }
  66  
  67          public string LnkResolvedExecutableName { get; set; }
  68  
  69          // Localized path based on windows display language
  70          public string LnkResolvedExecutableNameLocalized { get; set; } = string.Empty;
  71  
  72          public bool Valid { get; set; }
  73  
  74          public bool Enabled { get; set; }
  75  
  76          public bool HasArguments => !string.IsNullOrEmpty(Arguments);
  77  
  78          public string Arguments { get; set; } = string.Empty;
  79  
  80          public string Location => ParentDirectory;
  81  
  82          public ApplicationType AppType { get; set; }
  83  
  84          // Wrappers for File Operations
  85          public static IFileVersionInfoWrapper FileVersionInfoWrapper { get; set; } = new Wox.Infrastructure.FileSystemHelper.FileVersionInfoWrapper();
  86  
  87          public static IFile FileWrapper { get; set; } = new FileSystem().File;
  88  
  89          public static IShellLinkHelper ShellLinkHelper { get; set; } = new ShellLinkHelper();
  90  
  91          public static IDirectoryWrapper DirectoryWrapper { get; set; } = new DirectoryWrapper();
  92  
  93          private const string ShortcutExtension = "lnk";
  94          private const string ApplicationReferenceExtension = "appref-ms";
  95          private const string InternetShortcutExtension = "url";
  96          private static readonly HashSet<string> ExecutableApplicationExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "exe", "bat", "bin", "com", "cpl", "msc", "msi", "cmd", "ps1", "job", "msp", "mst", "sct", "ws", "wsh", "wsf" };
  97  
  98          private const string ProxyWebApp = "_proxy.exe";
  99          private const string AppIdArgument = "--app-id";
 100  
 101          public enum ApplicationType
 102          {
 103              WebApplication = 0,
 104              InternetShortcutApplication = 1,
 105              Win32Application = 2,
 106              ShortcutApplication = 3,
 107              ApprefApplication = 4,
 108              RunCommand = 5,
 109              Folder = 6,
 110              GenericFile = 7,
 111          }
 112  
 113          // Function to calculate the score of a result
 114          private int Score(string query)
 115          {
 116              var nameMatch = StringMatcher.FuzzySearch(query, Name);
 117              var locNameMatch = StringMatcher.FuzzySearch(query, NameLocalized);
 118              var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
 119              var executableNameMatch = StringMatcher.FuzzySearch(query, ExecutableName);
 120              var locExecutableNameMatch = StringMatcher.FuzzySearch(query, ExecutableNameLocalized);
 121              var lnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableName);
 122              var locLnkResolvedExecutableNameMatch = StringMatcher.FuzzySearch(query, LnkResolvedExecutableNameLocalized);
 123              var score = new[] { nameMatch.Score, locNameMatch.Score, descriptionMatch.Score / 2, executableNameMatch.Score, locExecutableNameMatch.Score, lnkResolvedExecutableNameMatch.Score, locLnkResolvedExecutableNameMatch.Score }.Max();
 124              return score;
 125          }
 126  
 127          public bool IsWebApplication()
 128          {
 129              // To Filter PWAs when the user searches for the main application
 130              // All Chromium based applications contain the --app-id argument
 131              // Reference : https://codereview.chromium.org/399045
 132              // Using Ordinal IgnoreCase since this is used internally
 133              return !string.IsNullOrEmpty(FullPath) &&
 134                     !string.IsNullOrEmpty(Arguments) &&
 135                     FullPath.Contains(ProxyWebApp, StringComparison.OrdinalIgnoreCase) &&
 136                     Arguments.Contains(AppIdArgument, StringComparison.OrdinalIgnoreCase);
 137          }
 138  
 139          // Condition to Filter pinned Web Applications or PWAs when searching for the main application
 140          public bool FilterWebApplication(string query)
 141          {
 142              // If the app is not a web application, then do not filter it
 143              if (!IsWebApplication())
 144              {
 145                  return false;
 146              }
 147  
 148              string[] subqueries = query?.Split() ?? Array.Empty<string>();
 149              bool nameContainsQuery = false;
 150              bool pathContainsQuery = false;
 151  
 152              // check if any space separated query is a part of the app name or path name
 153              foreach (var subquery in subqueries)
 154              {
 155                  // Using OrdinalIgnoreCase since these are used internally
 156                  if (FullPath.Contains(subquery, StringComparison.OrdinalIgnoreCase))
 157                  {
 158                      pathContainsQuery = true;
 159                  }
 160  
 161                  if (Name.Contains(subquery, StringComparison.OrdinalIgnoreCase))
 162                  {
 163                      nameContainsQuery = true;
 164                  }
 165              }
 166  
 167              return pathContainsQuery && !nameContainsQuery;
 168          }
 169  
 170          // Function to set the subtitle based on the Type of application
 171          private string GetSubtitle()
 172          {
 173              switch (AppType)
 174              {
 175                  case ApplicationType.Win32Application:
 176                  case ApplicationType.ShortcutApplication:
 177                  case ApplicationType.ApprefApplication:
 178                      return Properties.Resources.powertoys_run_plugin_program_win32_application;
 179                  case ApplicationType.InternetShortcutApplication:
 180                      return Properties.Resources.powertoys_run_plugin_program_internet_shortcut_application;
 181                  case ApplicationType.WebApplication:
 182                      return Properties.Resources.powertoys_run_plugin_program_web_application;
 183                  case ApplicationType.RunCommand:
 184                      return Properties.Resources.powertoys_run_plugin_program_run_command;
 185                  case ApplicationType.Folder:
 186                      return Properties.Resources.powertoys_run_plugin_program_folder_type;
 187                  case ApplicationType.GenericFile:
 188                      return Properties.Resources.powertoys_run_plugin_program_generic_file_type;
 189                  default:
 190                      return string.Empty;
 191              }
 192          }
 193  
 194          public bool QueryEqualsNameForRunCommands(string query)
 195          {
 196              if (query != null && AppType == ApplicationType.RunCommand)
 197              {
 198                  // Using OrdinalIgnoreCase since this is used internally
 199                  if (!query.Equals(Name, StringComparison.OrdinalIgnoreCase) && !query.Equals(ExecutableName, StringComparison.OrdinalIgnoreCase))
 200                  {
 201                      return false;
 202                  }
 203              }
 204  
 205              return true;
 206          }
 207  
 208          public Result Result(string query, string queryArguments, IPublicAPI api)
 209          {
 210              ArgumentNullException.ThrowIfNull(api);
 211  
 212              var score = Score(query);
 213              if (score <= 0)
 214              { // no need to create result if this is zero
 215                  return null;
 216              }
 217  
 218              if (!HasArguments)
 219              {
 220                  var noArgumentScoreModifier = 5;
 221                  score += noArgumentScoreModifier;
 222              }
 223              else
 224              {
 225                  // Filter Web Applications when searching for the main application
 226                  if (FilterWebApplication(query))
 227                  {
 228                      return null;
 229                  }
 230              }
 231  
 232              // NOTE: This is to display run commands only when there is an exact match, like in start menu
 233              if (!QueryEqualsNameForRunCommands(query))
 234              {
 235                  return null;
 236              }
 237  
 238              var result = new Result
 239              {
 240                  // To set the title for the result to always be the name of the application
 241                  Title = !string.IsNullOrEmpty(NameLocalized) ? NameLocalized : Name,
 242                  SubTitle = GetSubtitle(),
 243                  IcoPath = IcoPath,
 244                  Score = score,
 245                  ContextData = this,
 246                  ProgramArguments = queryArguments,
 247                  Action = e =>
 248                  {
 249                      var info = GetProcessStartInfo(queryArguments);
 250  
 251                      Main.StartProcess(Process.Start, info);
 252  
 253                      return true;
 254                  },
 255              };
 256  
 257              // Adjust title of RunCommand result
 258              if (AppType == ApplicationType.RunCommand)
 259              {
 260                  result.Title = ExecutableName;
 261              }
 262  
 263              result.TitleHighlightData = StringMatcher.FuzzySearch(query, result.Title).MatchData;
 264  
 265              // Using CurrentCulture since this is user facing
 266              var toolTipTitle = result.Title;
 267              string filePath = !string.IsNullOrEmpty(FullPathLocalized) ? FullPathLocalized : FullPath;
 268              var toolTipText = filePath;
 269              result.ToolTipData = new ToolTipData(toolTipTitle, toolTipText);
 270  
 271              return result;
 272          }
 273  
 274          public List<ContextMenuResult> ContextMenus(string queryArguments, IPublicAPI api)
 275          {
 276              ArgumentNullException.ThrowIfNull(api);
 277  
 278              var contextMenus = new List<ContextMenuResult>();
 279  
 280              if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile)
 281              {
 282                  contextMenus.Add(new ContextMenuResult
 283                  {
 284                      PluginName = Assembly.GetExecutingAssembly().GetName().Name,
 285                      Title = Properties.Resources.wox_plugin_program_run_as_administrator,
 286                      Glyph = "\xE7EF",
 287                      FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
 288                      AcceleratorKey = Key.Enter,
 289                      AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
 290                      Action = _ =>
 291                      {
 292                          var info = GetProcessStartInfo(queryArguments, RunAsType.Administrator);
 293                          Task.Run(() => Main.StartProcess(Process.Start, info));
 294  
 295                          return true;
 296                      },
 297                  });
 298  
 299                  contextMenus.Add(new ContextMenuResult
 300                  {
 301                      PluginName = Assembly.GetExecutingAssembly().GetName().Name,
 302                      Title = Properties.Resources.wox_plugin_program_run_as_user,
 303                      Glyph = "\xE7EE",
 304                      FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
 305                      AcceleratorKey = Key.U,
 306                      AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
 307                      Action = _ =>
 308                      {
 309                          var info = GetProcessStartInfo(queryArguments, RunAsType.OtherUser);
 310                          Task.Run(() => Main.StartProcess(Process.Start, info));
 311  
 312                          return true;
 313                      },
 314                  });
 315              }
 316  
 317              contextMenus.Add(
 318                  new ContextMenuResult
 319                  {
 320                      PluginName = Assembly.GetExecutingAssembly().GetName().Name,
 321                      Title = Properties.Resources.wox_plugin_program_open_containing_folder,
 322                      Glyph = "\xE838",
 323                      FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
 324                      AcceleratorKey = Key.E,
 325                      AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
 326                      Action = _ =>
 327                      {
 328                          Helper.OpenInShell(ParentDirectory);
 329                          return true;
 330                      },
 331                  });
 332  
 333              contextMenus.Add(
 334                  new ContextMenuResult
 335                  {
 336                      PluginName = Assembly.GetExecutingAssembly().GetName().Name,
 337                      Title = Properties.Resources.wox_plugin_program_open_in_console,
 338                      Glyph = "\xE756",
 339                      FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
 340                      AcceleratorKey = Key.C,
 341                      AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
 342                      Action = (context) =>
 343                      {
 344                          try
 345                          {
 346                              Helper.OpenInConsole(ParentDirectory);
 347                              return true;
 348                          }
 349                          catch (Exception e)
 350                          {
 351                              Log.Exception($"|Failed to open {Name} in console, {e.Message}", e, GetType());
 352                              return false;
 353                          }
 354                      },
 355                  });
 356  
 357              return contextMenus;
 358          }
 359  
 360          private ProcessStartInfo GetProcessStartInfo(string programArguments, RunAsType runAs = RunAsType.None)
 361          {
 362              return new ProcessStartInfo
 363              {
 364                  FileName = LnkFilePath ?? FullPath,
 365                  WorkingDirectory = ParentDirectory,
 366                  UseShellExecute = true,
 367                  Arguments = programArguments,
 368                  Verb = runAs == RunAsType.Administrator ? "runAs" : runAs == RunAsType.OtherUser ? "runAsUser" : string.Empty,
 369              };
 370          }
 371  
 372          private enum RunAsType
 373          {
 374              None,
 375              Administrator,
 376              OtherUser,
 377          }
 378  
 379          public override string ToString()
 380          {
 381              return ExecutableName;
 382          }
 383  
 384          private static Win32Program CreateWin32Program(string path)
 385          {
 386              try
 387              {
 388                  return new Win32Program
 389                  {
 390                      Name = Path.GetFileNameWithoutExtension(path),
 391                      ExecutableName = Path.GetFileName(path),
 392                      IcoPath = path,
 393  
 394                      // Using InvariantCulture since this is user facing
 395                      FullPath = path,
 396                      UniqueIdentifier = path,
 397                      ParentDirectory = Directory.GetParent(path).FullName,
 398                      Description = string.Empty,
 399                      Valid = true,
 400                      Enabled = true,
 401                      AppType = ApplicationType.Win32Application,
 402  
 403                      // Localized name, path and executable based on windows display language
 404                      NameLocalized = Main.ShellLocalizationHelper.GetLocalizedName(path),
 405                      FullPathLocalized = Main.ShellLocalizationHelper.GetLocalizedPath(path),
 406                      ExecutableNameLocalized = Path.GetFileName(Main.ShellLocalizationHelper.GetLocalizedPath(path)),
 407                  };
 408              }
 409              catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
 410              {
 411                  ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 412  
 413                  return InvalidProgram;
 414              }
 415              catch (Exception e)
 416              {
 417                  ProgramLogger.Exception($"|An unexpected error occurred in the calling method CreateWin32Program at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 418  
 419                  return InvalidProgram;
 420              }
 421          }
 422  
 423          private static readonly Regex InternetShortcutURLPrefixes = new Regex(@"^steam:\/\/(rungameid|run|open)\/|^com\.epicgames\.launcher:\/\/apps\/", RegexOptions.Compiled);
 424  
 425          // This function filters Internet Shortcut programs
 426          private static Win32Program InternetShortcutProgram(string path)
 427          {
 428              try
 429              {
 430                  // We don't want to read the whole file if we don't need to
 431                  var lines = FileWrapper.ReadLines(path);
 432                  string iconPath = string.Empty;
 433                  string urlPath = string.Empty;
 434                  bool validApp = false;
 435  
 436                  const string urlPrefix = "URL=";
 437                  const string iconFilePrefix = "IconFile=";
 438  
 439                  foreach (string line in lines)
 440                  {
 441                      // Using OrdinalIgnoreCase since this is used internally
 442                      if (line.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase))
 443                      {
 444                          urlPath = line.Substring(urlPrefix.Length);
 445  
 446                          if (!Uri.TryCreate(urlPath, UriKind.RelativeOrAbsolute, out Uri _))
 447                          {
 448                              ProgramLogger.Warn("url could not be parsed", null, MethodBase.GetCurrentMethod().DeclaringType, urlPath);
 449                              return InvalidProgram;
 450                          }
 451  
 452                          // To filter out only those steam shortcuts which have 'run' or 'rungameid' as the hostname
 453                          if (InternetShortcutURLPrefixes.Match(urlPath).Success)
 454                          {
 455                              validApp = true;
 456                          }
 457                      }
 458                      else if (line.StartsWith(iconFilePrefix, StringComparison.OrdinalIgnoreCase))
 459                      {
 460                          iconPath = line.Substring(iconFilePrefix.Length);
 461                      }
 462  
 463                      // If we resolved an urlPath & and an iconPath quit reading the file
 464                      if (!string.IsNullOrEmpty(urlPath) && !string.IsNullOrEmpty(iconPath))
 465                      {
 466                          break;
 467                      }
 468                  }
 469  
 470                  if (!validApp)
 471                  {
 472                      return InvalidProgram;
 473                  }
 474  
 475                  try
 476                  {
 477                      return new Win32Program
 478                      {
 479                          Name = Path.GetFileNameWithoutExtension(path),
 480                          ExecutableName = Path.GetFileName(path),
 481                          IcoPath = iconPath,
 482                          FullPath = urlPath,
 483                          UniqueIdentifier = path,
 484                          ParentDirectory = Directory.GetParent(path).FullName,
 485                          Valid = true,
 486                          Enabled = true,
 487                          AppType = ApplicationType.InternetShortcutApplication,
 488                      };
 489                  }
 490                  catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
 491                  {
 492                      ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 493  
 494                      return InvalidProgram;
 495                  }
 496              }
 497              catch (Exception e)
 498              {
 499                  ProgramLogger.Exception($"|An unexpected error occurred in the calling method InternetShortcutProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 500  
 501                  return InvalidProgram;
 502              }
 503          }
 504  
 505          private static Win32Program LnkProgram(string path)
 506          {
 507              try
 508              {
 509                  var program = CreateWin32Program(path);
 510                  string target = ShellLinkHelper.RetrieveTargetPath(path);
 511  
 512                  if (!string.IsNullOrEmpty(target))
 513                  {
 514                      if (!(File.Exists(target) || Directory.Exists(target)))
 515                      {
 516                          // If the link points nowhere, consider it invalid.
 517                          return InvalidProgram;
 518                      }
 519  
 520                      program.LnkFilePath = program.FullPath;
 521                      program.LnkResolvedExecutableName = Path.GetFileName(target);
 522                      program.LnkResolvedExecutableNameLocalized = Path.GetFileName(Main.ShellLocalizationHelper.GetLocalizedPath(target));
 523  
 524                      // Using CurrentCulture since this is user facing
 525                      program.FullPath = Path.GetFullPath(target);
 526                      program.FullPathLocalized = Main.ShellLocalizationHelper.GetLocalizedPath(target);
 527  
 528                      program.Arguments = ShellLinkHelper.Arguments;
 529  
 530                      // A .lnk could be a (Chrome) PWA, set correct AppType
 531                      program.AppType = program.IsWebApplication()
 532                          ? ApplicationType.WebApplication
 533                          : GetAppTypeFromPath(target);
 534  
 535                      var description = ShellLinkHelper.Description;
 536                      if (!string.IsNullOrEmpty(description))
 537                      {
 538                          program.Description = description;
 539                      }
 540                      else
 541                      {
 542                          var info = FileVersionInfoWrapper.GetVersionInfo(target);
 543                          if (!string.IsNullOrEmpty(info?.FileDescription))
 544                          {
 545                              program.Description = info.FileDescription;
 546                          }
 547                      }
 548                  }
 549  
 550                  return program;
 551              }
 552              catch (System.IO.FileLoadException e)
 553              {
 554                  ProgramLogger.Warn($"Couldn't load the link file at {path}. This might be caused by a new link being created and locked by the OS.", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 555                  return InvalidProgram;
 556              }
 557  
 558              // Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
 559              // Error caused likely due to trying to get the description of the program
 560              catch (Exception e)
 561              {
 562                  ProgramLogger.Exception($"|An unexpected error occurred in the calling method LnkProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 563  
 564                  return InvalidProgram;
 565              }
 566          }
 567  
 568          private static Win32Program ExeProgram(string path)
 569          {
 570              try
 571              {
 572                  var program = CreateWin32Program(path);
 573                  var info = FileVersionInfoWrapper.GetVersionInfo(path);
 574                  if (!string.IsNullOrEmpty(info?.FileDescription))
 575                  {
 576                      program.Description = info.FileDescription;
 577                  }
 578  
 579                  return program;
 580              }
 581              catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
 582              {
 583                  ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 584  
 585                  return InvalidProgram;
 586              }
 587              catch (FileNotFoundException e)
 588              {
 589                  ProgramLogger.Warn($"|Unable to locate exe file at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 590  
 591                  return InvalidProgram;
 592              }
 593              catch (Exception e)
 594              {
 595                  ProgramLogger.Exception($"|An unexpected error occurred in the calling method ExeProgram at {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 596  
 597                  return InvalidProgram;
 598              }
 599          }
 600  
 601          // Function to get the application type, given the path to the application
 602          public static ApplicationType GetAppTypeFromPath(string path)
 603          {
 604              ArgumentNullException.ThrowIfNull(path);
 605  
 606              string extension = Extension(path);
 607  
 608              // Using OrdinalIgnoreCase since these are used internally with paths
 609              if (ExecutableApplicationExtensions.Contains(extension))
 610              {
 611                  return ApplicationType.Win32Application;
 612              }
 613              else if (extension.Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase))
 614              {
 615                  return ApplicationType.ShortcutApplication;
 616              }
 617              else if (extension.Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase))
 618              {
 619                  return ApplicationType.ApprefApplication;
 620              }
 621              else if (extension.Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase))
 622              {
 623                  return ApplicationType.InternetShortcutApplication;
 624              }
 625              else if (string.IsNullOrEmpty(extension) && DirectoryWrapper.Exists(path))
 626              {
 627                  return ApplicationType.Folder;
 628              }
 629  
 630              return ApplicationType.GenericFile;
 631          }
 632  
 633          // Function to get the Win32 application, given the path to the application
 634          public static Win32Program GetAppFromPath(string path)
 635          {
 636              ArgumentNullException.ThrowIfNull(path);
 637  
 638              Win32Program app;
 639              switch (GetAppTypeFromPath(path))
 640              {
 641                  case ApplicationType.Win32Application:
 642                      app = ExeProgram(path);
 643                      break;
 644                  case ApplicationType.ShortcutApplication:
 645                      app = LnkProgram(path);
 646                      break;
 647                  case ApplicationType.ApprefApplication:
 648                      app = CreateWin32Program(path);
 649                      app.AppType = ApplicationType.ApprefApplication;
 650                      break;
 651                  case ApplicationType.InternetShortcutApplication:
 652                      app = InternetShortcutProgram(path);
 653                      break;
 654                  case ApplicationType.WebApplication:
 655                  case ApplicationType.RunCommand:
 656                  case ApplicationType.Folder:
 657                  case ApplicationType.GenericFile:
 658                  default:
 659                      app = null;
 660                      break;
 661              }
 662  
 663              // if the app is valid, only then return the application, else return null
 664              return app?.Valid == true
 665                  ? app
 666                  : null;
 667          }
 668  
 669          private static IEnumerable<string> ProgramPaths(string directory, IList<string> suffixes, bool recursiveSearch = true)
 670          {
 671              if (!Directory.Exists(directory))
 672              {
 673                  return Array.Empty<string>();
 674              }
 675  
 676              var files = new List<string>();
 677              var folderQueue = new Queue<string>();
 678              folderQueue.Enqueue(directory);
 679  
 680              // Keep track of already visited directories to avoid cycles.
 681              var alreadyVisited = new HashSet<string>();
 682  
 683              do
 684              {
 685                  var currentDirectory = folderQueue.Dequeue();
 686  
 687                  if (alreadyVisited.Contains(currentDirectory))
 688                  {
 689                      continue;
 690                  }
 691  
 692                  alreadyVisited.Add(currentDirectory);
 693  
 694                  try
 695                  {
 696                      foreach (var suffix in suffixes)
 697                      {
 698                          try
 699                          {
 700                              files.AddRange(Directory.EnumerateFiles(currentDirectory, $"*.{suffix}", SearchOption.TopDirectoryOnly));
 701                          }
 702                          catch (DirectoryNotFoundException e)
 703                          {
 704                              ProgramLogger.Warn("|The directory trying to load the program from does not exist", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory);
 705                          }
 706                      }
 707                  }
 708                  catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
 709                  {
 710                      ProgramLogger.Warn($"|Permission denied when trying to load programs from {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory);
 711                  }
 712                  catch (Exception e)
 713                  {
 714                      ProgramLogger.Exception($"|An unexpected error occurred in the calling method ProgramPaths at {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory);
 715                  }
 716  
 717                  try
 718                  {
 719                      // If the search is set to be non-recursive, then do not enqueue the child directories.
 720                      if (!recursiveSearch)
 721                      {
 722                          continue;
 723                      }
 724  
 725                      foreach (var childDirectory in Directory.EnumerateDirectories(currentDirectory, "*", new EnumerationOptions()
 726                      {
 727                          // https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions?view=net-6.0
 728                          // Exclude directories with the Reparse Point file attribute, to avoid loops due to symbolic links / directory junction / mount points.
 729                          AttributesToSkip = FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReparsePoint,
 730                          RecurseSubdirectories = false,
 731                      }))
 732                      {
 733                          folderQueue.Enqueue(childDirectory);
 734                      }
 735                  }
 736                  catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
 737                  {
 738                      ProgramLogger.Warn($"|Permission denied when trying to load programs from {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory);
 739                  }
 740                  catch (Exception e)
 741                  {
 742                      ProgramLogger.Exception($"|An unexpected error occurred in the calling method ProgramPaths at {currentDirectory}", e, MethodBase.GetCurrentMethod().DeclaringType, currentDirectory);
 743                  }
 744              }
 745              while (folderQueue.Count > 0);
 746  
 747              return files;
 748          }
 749  
 750          private static string Extension(string path)
 751          {
 752              // Using InvariantCulture since this is user facing
 753              var extension = Path.GetExtension(path)?.ToLowerInvariant();
 754  
 755              return !string.IsNullOrEmpty(extension)
 756                  ? extension.Substring(1)
 757                  : string.Empty;
 758          }
 759  
 760          private static IEnumerable<string> CustomProgramPaths(IEnumerable<ProgramSource> sources, IList<string> suffixes)
 761              => sources?.Where(programSource => Directory.Exists(programSource.Location) && programSource.Enabled)
 762                  .SelectMany(programSource => ProgramPaths(programSource.Location, suffixes))
 763                  .ToList() ?? Enumerable.Empty<string>();
 764  
 765          // Function to obtain the list of applications, the locations of which have been added to the env variable PATH
 766          private static List<string> PathEnvironmentProgramPaths(IList<string> suffixes)
 767          {
 768              // To get all the locations stored in the PATH env variable
 769              var pathEnvVariable = Environment.GetEnvironmentVariable("PATH");
 770              string[] searchPaths = pathEnvVariable.Split(Path.PathSeparator);
 771              var toFilterAllPaths = new List<string>();
 772              bool isRecursiveSearch = true;
 773  
 774              foreach (string path in searchPaths)
 775              {
 776                  if (path.Length > 0)
 777                  {
 778                      // to expand any environment variables present in the path
 779                      string directory = Environment.ExpandEnvironmentVariables(path);
 780                      var paths = ProgramPaths(directory, suffixes, !isRecursiveSearch);
 781                      toFilterAllPaths.AddRange(paths);
 782                  }
 783              }
 784  
 785              return toFilterAllPaths;
 786          }
 787  
 788          private static List<string> IndexPath(IList<string> suffixes, List<string> indexLocations)
 789               => indexLocations
 790                  .SelectMany(indexLocation => ProgramPaths(indexLocation, suffixes))
 791                  .ToList();
 792  
 793          private static List<string> StartMenuProgramPaths(IList<string> suffixes)
 794          {
 795              var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
 796              var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu);
 797              var indexLocation = new List<string>() { directory1, directory2 };
 798  
 799              return IndexPath(suffixes, indexLocation);
 800          }
 801  
 802          private static List<string> DesktopProgramPaths(IList<string> suffixes)
 803          {
 804              var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
 805              var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
 806  
 807              var indexLocation = new List<string>() { directory1, directory2 };
 808  
 809              return IndexPath(suffixes, indexLocation);
 810          }
 811  
 812          private static List<string> RegistryAppProgramPaths(IList<string> suffixes)
 813          {
 814              // https://msdn.microsoft.com/library/windows/desktop/ee872121
 815              const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
 816              var paths = new List<string>();
 817              using (var root = Registry.LocalMachine.OpenSubKey(appPaths))
 818              {
 819                  if (root != null)
 820                  {
 821                      paths.AddRange(GetPathsFromRegistry(root));
 822                  }
 823              }
 824  
 825              using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
 826              {
 827                  if (root != null)
 828                  {
 829                      paths.AddRange(GetPathsFromRegistry(root));
 830                  }
 831              }
 832  
 833              return paths
 834                  .Where(path => suffixes.Any(suffix => path.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)))
 835                  .Select(ExpandEnvironmentVariables)
 836                  .ToList();
 837          }
 838  
 839          private static IEnumerable<string> GetPathsFromRegistry(RegistryKey root)
 840              => root
 841                  .GetSubKeyNames()
 842                  .Select(x => GetPathFromRegistrySubkey(root, x));
 843  
 844          private static string GetPathFromRegistrySubkey(RegistryKey root, string subkey)
 845          {
 846              var path = string.Empty;
 847              try
 848              {
 849                  using (var key = root.OpenSubKey(subkey))
 850                  {
 851                      if (key == null)
 852                      {
 853                          return string.Empty;
 854                      }
 855  
 856                      var defaultValue = string.Empty;
 857                      path = key.GetValue(defaultValue) as string;
 858                  }
 859  
 860                  if (string.IsNullOrEmpty(path))
 861                  {
 862                      return string.Empty;
 863                  }
 864  
 865                  // fix path like this: ""\"C:\\folder\\executable.exe\""
 866                  return path = path.Trim('"', ' ');
 867              }
 868              catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
 869              {
 870                  ProgramLogger.Warn($"|Permission denied when trying to load the program from {path}", e, MethodBase.GetCurrentMethod().DeclaringType, path);
 871  
 872                  return string.Empty;
 873              }
 874          }
 875  
 876          private static string ExpandEnvironmentVariables(string path) =>
 877              path != null
 878                  ? Environment.ExpandEnvironmentVariables(path)
 879                  : null;
 880  
 881          // Overriding the object.GetHashCode() function to aid in removing duplicates while adding and removing apps from the concurrent dictionary storage
 882          public override int GetHashCode()
 883              => Win32ProgramEqualityComparer.Default.GetHashCode(this);
 884  
 885          public override bool Equals(object obj)
 886              => obj is Win32Program win32Program && Win32ProgramEqualityComparer.Default.Equals(this, win32Program);
 887  
 888          private class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
 889          {
 890              public static readonly Win32ProgramEqualityComparer Default = new Win32ProgramEqualityComparer();
 891  
 892              public bool Equals(Win32Program app1, Win32Program app2)
 893              {
 894                  if (app1 == null && app2 == null)
 895                  {
 896                      return true;
 897                  }
 898  
 899                  return app1 != null
 900                         && app2 != null
 901                         && (app1.Name?.ToUpperInvariant(), app1.ExecutableName?.ToUpperInvariant(), app1.FullPath?.ToUpperInvariant())
 902                         .Equals((app2.Name?.ToUpperInvariant(), app2.ExecutableName?.ToUpperInvariant(), app2.FullPath?.ToUpperInvariant()));
 903              }
 904  
 905              public int GetHashCode(Win32Program obj)
 906                  => (obj.Name?.ToUpperInvariant(), obj.ExecutableName?.ToUpperInvariant(), obj.FullPath?.ToUpperInvariant()).GetHashCode();
 907          }
 908  
 909          public static List<Win32Program> DeduplicatePrograms(IEnumerable<Win32Program> programs)
 910              => new HashSet<Win32Program>(programs, Win32ProgramEqualityComparer.Default).ToList();
 911  
 912          private static Win32Program GetProgramFromPath(string path)
 913          {
 914              var extension = Extension(path);
 915              if (ExecutableApplicationExtensions.Contains(extension))
 916              {
 917                  return ExeProgram(path);
 918              }
 919  
 920              switch (extension)
 921              {
 922                  case ShortcutExtension:
 923                      return LnkProgram(path);
 924                  case ApplicationReferenceExtension:
 925                      return CreateWin32Program(path);
 926                  case InternetShortcutExtension:
 927                      return InternetShortcutProgram(path);
 928                  default:
 929                      return null;
 930              }
 931          }
 932  
 933          private static bool TryGetIcoPathForRunCommandProgram(Win32Program program, out string icoPath)
 934          {
 935              icoPath = null;
 936  
 937              if (program.AppType != ApplicationType.RunCommand)
 938              {
 939                  return false;
 940              }
 941  
 942              if (string.IsNullOrEmpty(program.FullPath))
 943              {
 944                  return false;
 945              }
 946  
 947              // https://msdn.microsoft.com/library/windows/desktop/ee872121
 948              try
 949              {
 950                  var redirectionPath = ReparsePoint.GetTarget(program.FullPath);
 951                  if (string.IsNullOrEmpty(redirectionPath))
 952                  {
 953                      return false;
 954                  }
 955  
 956                  icoPath = ExpandEnvironmentVariables(redirectionPath);
 957                  return true;
 958              }
 959              catch (IOException e)
 960              {
 961                  ProgramLogger.Warn($"|Error whilst retrieving the redirection path from app execution alias {program.FullPath}", e, MethodBase.GetCurrentMethod().DeclaringType, program.FullPath);
 962              }
 963  
 964              icoPath = null;
 965              return false;
 966          }
 967  
 968          private static Win32Program GetRunCommandProgramFromPath(string path)
 969          {
 970              var program = GetProgramFromPath(path);
 971              program.AppType = ApplicationType.RunCommand;
 972  
 973              if (TryGetIcoPathForRunCommandProgram(program, out var icoPath))
 974              {
 975                  program.IcoPath = icoPath;
 976              }
 977  
 978              return program;
 979          }
 980  
 981          public static IList<Win32Program> All(ProgramPluginSettings settings)
 982          {
 983              ArgumentNullException.ThrowIfNull(settings);
 984  
 985              try
 986              {
 987                  // Set an initial size to an expected size to prevent multiple hashSet resizes
 988                  const int defaultHashsetSize = 1000;
 989  
 990                  // Multiple paths could have the same programPaths and we don't want to resolve / lookup them multiple times
 991                  var paths = new HashSet<string>(defaultHashsetSize);
 992                  var runCommandPaths = new HashSet<string>(defaultHashsetSize);
 993  
 994                  // Parallelize multiple sources, and priority based on paths which most likely contain .lnk files which are formatted
 995                  var sources = new (bool IsEnabled, Func<IEnumerable<string>> GetPaths)[]
 996                  {
 997                      (true, () => CustomProgramPaths(settings.ProgramSources, settings.ProgramSuffixes)),
 998                      (settings.EnableStartMenuSource, () => StartMenuProgramPaths(settings.ProgramSuffixes)),
 999                      (settings.EnableDesktopSource, () => DesktopProgramPaths(settings.ProgramSuffixes)),
1000                      (settings.EnableRegistrySource, () => RegistryAppProgramPaths(settings.ProgramSuffixes)),
1001                  };
1002  
1003                  // Run commands are always set as AppType "RunCommand"
1004                  var runCommandSources = new (bool IsEnabled, Func<IEnumerable<string>> GetPaths)[]
1005                  {
1006                      (settings.EnablePathEnvironmentVariableSource, () => PathEnvironmentProgramPaths(settings.RunCommandSuffixes)),
1007                  };
1008  
1009                  var disabledProgramsList = settings.DisabledProgramSources;
1010  
1011                  // Get all paths but exclude all normal .Executables
1012                  paths.UnionWith(sources
1013                      .AsParallel()
1014                      .SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
1015                      .Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath))
1016                      .Where(path => !ExecutableApplicationExtensions.Contains(Extension(path))));
1017                  runCommandPaths.UnionWith(runCommandSources
1018                      .AsParallel()
1019                      .SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
1020                      .Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath)));
1021  
1022                  var programs = paths.AsParallel().Select(source => GetProgramFromPath(source));
1023                  var runCommandPrograms = runCommandPaths.AsParallel().Select(source => GetRunCommandProgramFromPath(source));
1024  
1025                  return DeduplicatePrograms(programs.Concat(runCommandPrograms).Where(program => program?.Valid == true));
1026              }
1027              catch (Exception e)
1028              {
1029                  ProgramLogger.Exception("An unexpected error occurred", e, MethodBase.GetCurrentMethod().DeclaringType, "Not available");
1030  
1031                  return Array.Empty<Win32Program>();
1032              }
1033          }
1034      }
1035  }