From 383031d689139fb92e247d4356917cd10f923f88 Mon Sep 17 00:00:00 2001 From: Nir Bar Date: Wed, 21 Sep 2022 08:30:25 +0300 Subject: [PATCH] Allow to detect products by ComponentCode --- .github/workflows/build.yml | 4 +- MsiZapEx/ComponentInfo.cs | 115 +++++++++++++++++++++++++++--------- MsiZapEx/ProductInfo.cs | 2 +- MsiZapEx/Program.cs | 9 ++- MsiZapEx/Settings.cs | 5 +- README.md | 3 +- TidyBuild.custom.props | 2 +- 7 files changed, 106 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26de5a1..057b63a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,8 @@ name: MsiZapEx Build on: push: + tags: + - v* workflow_dispatch: jobs: Build: @@ -45,7 +47,7 @@ jobs: with: prerelease: false generate_release_notes: true - tag_name: Build_${{ github.run_number }}_${{ github.ref_type }}_${{ github.ref_name }} + tag_name: ${{ github.ref_name }} files: | MsiZapEx.${{ steps.tagName.outputs.group1 }}.nupkg build\bin\Release\MsiZapEx\MsiZapEx.exe diff --git a/MsiZapEx/ComponentInfo.cs b/MsiZapEx/ComponentInfo.cs index 13db2c8..e24a069 100644 --- a/MsiZapEx/ComponentInfo.cs +++ b/MsiZapEx/ComponentInfo.cs @@ -13,16 +13,29 @@ public class ComponentInfo public enum StatusFlags { None = 0, - KeyPath = 1, + Products = 1, + KeyPath = 2 * Products, - Good = KeyPath + Good = Products | KeyPath + } + + public struct ProductKeyPath + { + internal ProductKeyPath(Guid productCode, string keyPath, bool exists) + { + ProductCode = productCode; + KeyPath = keyPath; + KeyPathExists = exists; + } + + public Guid ProductCode { get; private set; } + public string KeyPath { get; private set; } + public bool KeyPathExists { get; private set; } } public Guid ComponentCode { get; private set; } - public string KeyPath { get; private set; } public StatusFlags Status { get; private set; } = StatusFlags.None; - - Dictionary productToKeyPath = new Dictionary(); + public List ProductsKeyPath { get; } = new List(); internal static List GetComponents(Guid productCode) { @@ -47,7 +60,7 @@ internal static List GetComponents(Guid productCode) } ComponentInfo ci = new ComponentInfo(c); - if (ci.productToKeyPath.ContainsKey(productCode)) + if (ci.ProductsKeyPath.Any(p => p.ProductCode.Equals(productCode))) { components.Add(ci); } @@ -57,6 +70,11 @@ internal static List GetComponents(Guid productCode) return components; } + public ComponentInfo(Guid componentCode) + : this(componentCode.MsiObfuscate()) + { + } + internal ComponentInfo(string obfuscatedGuid) { using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)) @@ -67,22 +85,26 @@ internal ComponentInfo(string obfuscatedGuid) { ComponentCode = GuidEx.MsiObfuscate(obfuscatedGuid); - foreach (string n in k.GetValueNames()) + foreach (string obfuscatedProductCode in k.GetValueNames()) { - if (string.IsNullOrWhiteSpace(n) || n.Equals("@")) + if (string.IsNullOrWhiteSpace(obfuscatedProductCode) || obfuscatedProductCode.Equals("@")) { continue; } - if (Guid.TryParse(n, out Guid id)) + if (Guid.TryParse(obfuscatedProductCode, out Guid id)) { - KeyPath = k.GetValue(n)?.ToString(); - productToKeyPath[GuidEx.MsiObfuscate(n)] = KeyPath; + Guid productCode = GuidEx.MsiObfuscate(obfuscatedProductCode); + string keyPath = k.GetValue(obfuscatedProductCode)?.ToString(); + bool exists = ValidateKeyPath(keyPath); + + ProductsKeyPath.Add(new ProductKeyPath(productCode, keyPath, exists)); + Status |= StatusFlags.Products; + } - if (!string.IsNullOrWhiteSpace(KeyPath)) - { - ValidateKeyPath(); - } + if (!Status.HasFlag(StatusFlags.Products) || ProductsKeyPath.TrueForAll(p => p.KeyPathExists)) + { + Status |= StatusFlags.KeyPath; } } } @@ -90,18 +112,53 @@ internal ComponentInfo(string obfuscatedGuid) } } - internal void PrintState() + internal void PrintProducts() + { + if (ProductsKeyPath.Count == 0) + { + Console.WriteLine($"Component '{ComponentCode}' is not related to any product"); + } + + Console.WriteLine($"Component '{ComponentCode}' belongs to {ProductsKeyPath.Count} products"); + foreach (ProductKeyPath product in ProductsKeyPath) + { + Console.WriteLine($"\tBelongs to product '{product.ProductCode}' with key path '{product.KeyPath}'"); + if (!product.KeyPathExists) + { + Console.WriteLine($"\t\tKeyPath is missing"); + } + + //TODO How does the registry reflect a permanent component with different key paths? + if (product.ProductCode.Equals(Guid.Empty)) + { + Console.WriteLine($"\t\tThis KeyPath is permanent for this component"); + } + } + } + + internal void PrintProductState(Guid productCode) { - if (!Status.HasFlag(StatusFlags.KeyPath)) + ProductKeyPath keyPath = ProductsKeyPath.FirstOrDefault(p => p.ProductCode.Equals(productCode)); + if (!keyPath.ProductCode.Equals(productCode)) + { + Console.WriteLine($"\tComponent '{ComponentCode}' is not related to product {productCode}"); + return; + } + if (!keyPath.KeyPathExists) { - Console.WriteLine($"\tKeyPath '{KeyPath}' not found for component '{ComponentCode}'"); + Console.WriteLine($"\tKeyPath '{keyPath.KeyPath}' not found for component '{ComponentCode}'"); } } - private Regex registryKeyPath_ = new Regex(@"^(?[0-9]+):\\?(?.+)$", RegexOptions.Compiled); - private void ValidateKeyPath() + private static readonly Regex registryKeyPath_ = new Regex(@"^(?[0-9]+):\\?(?.+)$", RegexOptions.Compiled); + private static bool ValidateKeyPath(string keyPath) { - Match regMatch = registryKeyPath_.Match(KeyPath); + if (string.IsNullOrWhiteSpace(keyPath)) + { + return true; + } + + Match regMatch = registryKeyPath_.Match(keyPath); if (regMatch.Success) { string root = regMatch.Groups["root"].Value; @@ -124,7 +181,7 @@ private void ValidateKeyPath() hive = RegistryHive.Users; break; default: - return; + return false; } string path = regMatch.Groups["path"].Value; @@ -136,23 +193,25 @@ private void ValidateKeyPath() { if ((hk != null) && hk.GetValueNames().Contains(name)) { - Status |= StatusFlags.KeyPath; + return true; } } } } } - else if (KeyPath.EndsWith($"{Path.DirectorySeparatorChar}") || KeyPath.EndsWith($"{Path.VolumeSeparatorChar}")) + else if (keyPath.EndsWith($"{Path.DirectorySeparatorChar}") || keyPath.EndsWith($"{Path.VolumeSeparatorChar}")) { - if (Directory.Exists(KeyPath)) + if (Directory.Exists(keyPath)) { - Status |= StatusFlags.KeyPath; + return true; } } - else if (File.Exists(KeyPath)) + else if (File.Exists(keyPath)) { - Status |= StatusFlags.KeyPath; + return true; } + + return false; } internal void Prune(Guid productCode, RegistryModifier modifier) diff --git a/MsiZapEx/ProductInfo.cs b/MsiZapEx/ProductInfo.cs index 91e8df8..2f9f9cc 100644 --- a/MsiZapEx/ProductInfo.cs +++ b/MsiZapEx/ProductInfo.cs @@ -81,7 +81,7 @@ public void PrintState() { if (!ci.Status.Equals(ComponentInfo.StatusFlags.Good)) { - ci.PrintState(); + ci.PrintProductState(ProductCode); } } } diff --git a/MsiZapEx/Program.cs b/MsiZapEx/Program.cs index c5840b3..edf3c40 100644 --- a/MsiZapEx/Program.cs +++ b/MsiZapEx/Program.cs @@ -89,7 +89,7 @@ static void Main(string[] args) Console.WriteLine($"No UpgradeCode '{upgradeCode}' was found"); } } - else if (!string.IsNullOrEmpty(Settings.Instance.ProductCode)) + if (!string.IsNullOrEmpty(Settings.Instance.ProductCode)) { Guid productCode = Settings.Instance.Obfuscated ? GuidEx.MsiObfuscate(Settings.Instance.ProductCode) : new Guid(Settings.Instance.ProductCode); @@ -124,6 +124,13 @@ static void Main(string[] args) } } } + if (!string.IsNullOrEmpty(Settings.Instance.ComponentCode)) + { + Guid componentCode = Settings.Instance.Obfuscated ? GuidEx.MsiObfuscate(Settings.Instance.ComponentCode) : new Guid(Settings.Instance.ComponentCode); + + ComponentInfo component = new ComponentInfo(componentCode); + component.PrintProducts(); + } } catch (Exception ex) { diff --git a/MsiZapEx/Settings.cs b/MsiZapEx/Settings.cs index 56d3bc7..b9df1f4 100644 --- a/MsiZapEx/Settings.cs +++ b/MsiZapEx/Settings.cs @@ -18,13 +18,16 @@ public class Settings [Option("product-code", Required = false, HelpText = "Detect products by ProductCode", Group = "codes")] public string ProductCode { get; set; } + [Option("component-code", Required = false, HelpText = "Detect products by ComponentCode", Group = "codes")] + public string ComponentCode { get; set; } + [Option("delete", Required = false, HelpText = "Forcibly remove product's Windows Installer entries from the registry")] public bool ForceClean { get; set; } [Option("dry-run", Required = false, HelpText = "Do not delete Windows Installer entries. Instead, print anything that would have been deleted")] public bool DryRun { get; set; } - [Option("obfuscated", Required = false, HelpText = "The upgrade code or product code were supplied in their obfuscated form")] + [Option("obfuscated", Required = false, HelpText = "The upgrade code or product code or component code were supplied in their obfuscated form")] public bool Obfuscated { get; set; } [Option("verbose", Required = false, HelpText = "Verbose logging")] diff --git a/README.md b/README.md index 5a84760..d95a853 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ MsiZapEx is a command line utility and .NET assembly that enumerates Windows Ins - --upgrade-code _UUID_: List all Windows Installer products for the given UpgradeCode - --product-code _UUID_: Detect Windows Installer product for the given ProductCode +- --component-code _UUID_: Detect Windows Installer products for the given ComponentCode - --bundle-upgrade-code _UUID_: List all WiX bundles for the given bundle UpgradeCode - --bundle-product-code _UUID_: Detect WiX bundle for the given bundle Id (AKA ProductCode) - --delete: Delete all WiX and Windows Installer entries for the provided UUID. Note that if multiple bundles or products are detected for a given UpgradeCode then you must delete each ProductCode separately - --dry-run: May be specified with --delete only. Print all WiX and Windows Installer entries for the provided UUID that would be deleted -- --obfuscated: For a Windows Installer ProductCode or UpgradeCode, the UUID is provided in its obfuscated form +- --obfuscated: For a Windows Installer ProductCode or UpgradeCode or ComponentCode, the UUID is provided in its obfuscated form - --verbose: Print each registry modification # Open Issues diff --git a/TidyBuild.custom.props b/TidyBuild.custom.props index 6a555c1..06d0c40 100644 --- a/TidyBuild.custom.props +++ b/TidyBuild.custom.props @@ -2,7 +2,7 @@ - 0.0.4 + 0.0.5 FullVersion=$(FullVersion);$(DefineConstants) \ No newline at end of file