Skip to content

Commit

Permalink
Allow to detect products by ComponentCode
Browse files Browse the repository at this point in the history
  • Loading branch information
nirbar committed Sep 21, 2022
1 parent bcc66dd commit 383031d
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 34 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: MsiZapEx Build
on:
push:
tags:
- v*
workflow_dispatch:
jobs:
Build:
Expand Down Expand Up @@ -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
Expand Down
115 changes: 87 additions & 28 deletions MsiZapEx/ComponentInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Guid, string> productToKeyPath = new Dictionary<Guid, string>();
public List<ProductKeyPath> ProductsKeyPath { get; } = new List<ProductKeyPath>();

internal static List<ComponentInfo> GetComponents(Guid productCode)
{
Expand All @@ -47,7 +60,7 @@ internal static List<ComponentInfo> 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);
}
Expand All @@ -57,6 +70,11 @@ internal static List<ComponentInfo> 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))
Expand All @@ -67,41 +85,80 @@ 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;
}
}
}
}
}
}

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(@"^(?<root>[0-9]+):\\?(?<path>.+)$", RegexOptions.Compiled);
private void ValidateKeyPath()
private static readonly Regex registryKeyPath_ = new Regex(@"^(?<root>[0-9]+):\\?(?<path>.+)$", 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;
Expand All @@ -124,7 +181,7 @@ private void ValidateKeyPath()
hive = RegistryHive.Users;
break;
default:
return;
return false;
}

string path = regMatch.Groups["path"].Value;
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion MsiZapEx/ProductInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void PrintState()
{
if (!ci.Status.Equals(ComponentInfo.StatusFlags.Good))
{
ci.PrintState();
ci.PrintProductState(ProductCode);
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion MsiZapEx/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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)
{
Expand Down
5 changes: 4 additions & 1 deletion MsiZapEx/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion TidyBuild.custom.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<Import Project="$(SolutionDir)TidyBuild.user.props" Condition="Exists('$(SolutionDir)TidyBuild.user.props')" />
<PropertyGroup>
<FullVersion Condition="'$(FullVersion)'==''">0.0.4</FullVersion>
<FullVersion Condition="'$(FullVersion)'==''">0.0.5</FullVersion>
<DefineConstants Condition="'$(MSBuildProjectExtension)'=='.wixproj'">FullVersion=$(FullVersion);$(DefineConstants)</DefineConstants>
</PropertyGroup>
</Project>

0 comments on commit 383031d

Please sign in to comment.