// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
namespace WixToolset.Harvesters
{
using System;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32;
using WixToolset.Data;
using WixToolset.Harvesters.Data;
using Wix = WixToolset.Harvesters.Serialize;
///
/// Harvest WiX authoring from the registry.
///
public sealed class RegistryHarvester : IDisposable
{
private const string HKCRPathInHKLM = @"Software\Classes";
private string remappedPath;
private static readonly int majorOSVersion = Environment.OSVersion.Version.Major;
private RegistryKey regKeyToOverride = Registry.LocalMachine;
private IntPtr regRootToOverride = NativeMethods.HkeyLocalMachine;
///
/// Instantiate a new RegistryHarvester.
///
/// Set to true to remap the entire registry to a private location for this process.
public RegistryHarvester(bool remap)
{
// Detect OS major version and set the hive to use when
// redirecting registry writes. We want to redirect registry
// writes to HKCU on Windows Vista and higher to avoid UAC
// problems, and to HKLM on downlevel OS's.
if (majorOSVersion >= 6)
{
this.regKeyToOverride = Registry.CurrentUser;
this.regRootToOverride = NativeMethods.HkeyCurrentUser;
}
// create a path in the registry for redirected keys which is process-specific
if (remap)
{
this.remappedPath = String.Concat(@"SOFTWARE\WiX\heat\", Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture));
// remove the previous remapped key if it exists
this.RemoveRemappedKey();
// remap the registry roots supported by MSI
// note - order is important here - the hive being used to redirect
// to must be overridden last to avoid creating the other override
// hives in the wrong location in the registry. For example, if HKLM is
// the redirect destination, overriding it first will cause other hives
// to be overridden under HKLM\Software\WiX\heat\HKLM\Software\WiX\HKCR
// instead of under HKLM\Software\WiX\heat\HKCR
if (majorOSVersion < 6)
{
this.RemapRegistryKey(NativeMethods.HkeyClassesRoot, String.Concat(this.remappedPath, @"\\HKEY_CLASSES_ROOT"));
this.RemapRegistryKey(NativeMethods.HkeyCurrentUser, String.Concat(this.remappedPath, @"\\HKEY_CURRENT_USER"));
this.RemapRegistryKey(NativeMethods.HkeyUsers, String.Concat(this.remappedPath, @"\\HKEY_USERS"));
this.RemapRegistryKey(NativeMethods.HkeyLocalMachine, String.Concat(this.remappedPath, @"\\HKEY_LOCAL_MACHINE"));
}
else
{
this.RemapRegistryKey(NativeMethods.HkeyClassesRoot, String.Concat(this.remappedPath, @"\\HKEY_CLASSES_ROOT"));
this.RemapRegistryKey(NativeMethods.HkeyLocalMachine, String.Concat(this.remappedPath, @"\\HKEY_LOCAL_MACHINE"));
this.RemapRegistryKey(NativeMethods.HkeyUsers, String.Concat(this.remappedPath, @"\\HKEY_USERS"));
this.RemapRegistryKey(NativeMethods.HkeyCurrentUser, String.Concat(this.remappedPath, @"\\HKEY_CURRENT_USER"));
// Typelib registration on Windows Vista requires that the key
// HKLM\Software\Classes exist, so add it to the remapped root
Registry.LocalMachine.CreateSubKey(HKCRPathInHKLM);
}
}
}
///
/// Close the RegistryHarvester and remove any remapped registry keys.
///
public void Close()
{
// note - order is important here - we must quit overriding the hive
// being used to redirect first
if (majorOSVersion < 6)
{
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyLocalMachine, IntPtr.Zero);
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyClassesRoot, IntPtr.Zero);
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyCurrentUser, IntPtr.Zero);
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyUsers, IntPtr.Zero);
}
else
{
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyCurrentUser, IntPtr.Zero);
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyClassesRoot, IntPtr.Zero);
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyLocalMachine, IntPtr.Zero);
NativeMethods.OverrideRegistryKey(NativeMethods.HkeyUsers, IntPtr.Zero);
}
this.RemoveRemappedKey();
}
///
/// Dispose the RegistryHarvester.
///
public void Dispose()
{
this.Close();
}
///
/// Harvest all registry roots supported by Windows Installer.
///
/// The registry keys and values in the registry.
public Wix.RegistryValue[] HarvestRegistry()
{
ArrayList registryValues = new ArrayList();
this.HarvestRegistryKey(Registry.ClassesRoot, registryValues);
this.HarvestRegistryKey(Registry.CurrentUser, registryValues);
this.HarvestRegistryKey(Registry.LocalMachine, registryValues);
this.HarvestRegistryKey(Registry.Users, registryValues);
return (Wix.RegistryValue[])registryValues.ToArray(typeof(Wix.RegistryValue));
}
///
/// Harvest a registry key.
///
/// The path of the registry key to harvest.
/// The registry keys and values under the key.
public Wix.RegistryValue[] HarvestRegistryKey(string path)
{
RegistryKey registryKey = null;
ArrayList registryValues = new ArrayList();
string[] parts = GetPathParts(path);
try
{
switch (parts[0])
{
case "HKEY_CLASSES_ROOT":
registryKey = Registry.ClassesRoot;
break;
case "HKEY_CURRENT_USER":
registryKey = Registry.CurrentUser;
break;
case "HKEY_LOCAL_MACHINE":
registryKey = Registry.LocalMachine;
break;
case "HKEY_USERS":
registryKey = Registry.Users;
break;
default:
// TODO: put a better exception here
throw new Exception();
}
if (1 < parts.Length)
{
registryKey = registryKey.OpenSubKey(parts[1]);
if (null == registryKey)
{
throw new WixException(HarvesterErrors.UnableToOpenRegistryKey(parts[1]));
}
}
this.HarvestRegistryKey(registryKey, registryValues);
}
finally
{
if (null != registryKey)
{
registryKey.Close();
}
}
return (Wix.RegistryValue[])registryValues.ToArray(typeof(Wix.RegistryValue));
}
///
/// Gets the parts of a registry key's path.
///
/// The registry key path.
/// The root and key parts of the registry key path.
private static string[] GetPathParts(string path)
{
return path.Split(@"\".ToCharArray(), 2);
}
///
/// Harvest a registry key.
///
/// The registry key to harvest.
/// The collected registry values.
private void HarvestRegistryKey(RegistryKey registryKey, ArrayList registryValues)
{
// harvest the sub-keys
foreach (string subKeyName in registryKey.GetSubKeyNames())
{
using (RegistryKey subKey = registryKey.OpenSubKey(subKeyName))
{
this.HarvestRegistryKey(subKey, registryValues);
}
}
string[] parts = GetPathParts(registryKey.Name);
Wix.RegistryRootType root;
switch (parts[0])
{
case "HKEY_CLASSES_ROOT":
root = Wix.RegistryRootType.HKCR;
break;
case "HKEY_CURRENT_USER":
root = Wix.RegistryRootType.HKCU;
break;
case "HKEY_LOCAL_MACHINE":
// HKLM\Software\Classes is equivalent to HKCR
if (1 < parts.Length && parts[1].StartsWith(HKCRPathInHKLM, StringComparison.OrdinalIgnoreCase))
{
root = Wix.RegistryRootType.HKCR;
parts[1] = parts[1].Remove(0, HKCRPathInHKLM.Length);
if (0 < parts[1].Length)
{
parts[1] = parts[1].TrimStart('\\');
}
if (String.IsNullOrEmpty(parts[1]))
{
parts = new [] { parts[0] };
}
}
else
{
root = Wix.RegistryRootType.HKLM;
}
break;
case "HKEY_USERS":
root = Wix.RegistryRootType.HKU;
break;
default:
// TODO: put a better exception here
throw new Exception();
}
// harvest the values
foreach (string valueName in registryKey.GetValueNames())
{
Wix.RegistryValue registryValue = new Wix.RegistryValue();
registryValue.Action = Wix.RegistryValue.ActionType.write;
registryValue.Root = root;
if (1 < parts.Length)
{
registryValue.Key = parts[1];
}
if (null != valueName && 0 < valueName.Length)
{
registryValue.Name = valueName;
}
object value = registryKey.GetValue(valueName);
if (value is byte[]) // binary
{
StringBuilder hexadecimalValue = new StringBuilder();
// convert the byte array to hexadecimal
foreach (byte byteValue in (byte[])value)
{
hexadecimalValue.Append(byteValue.ToString("X2", CultureInfo.InvariantCulture.NumberFormat));
}
registryValue.Type = Wix.RegistryValue.TypeType.binary;
registryValue.Value = hexadecimalValue.ToString();
}
else if (value is int) // integer
{
registryValue.Type = Wix.RegistryValue.TypeType.integer;
registryValue.Value = ((int)value).ToString(CultureInfo.InvariantCulture);
}
else if (value is string[]) // multi-string
{
registryValue.Type = Wix.RegistryValue.TypeType.multiString;
if (0 == ((string[])value).Length)
{
Wix.MultiStringValue multiStringValue = new Wix.MultiStringValue();
multiStringValue.Value = String.Empty;
registryValue.AddChild(multiStringValue);
}
else
{
foreach (string multiStringValueContent in (string[])value)
{
Wix.MultiStringValue multiStringValue = new Wix.MultiStringValue();
multiStringValue.Value = multiStringValueContent;
registryValue.AddChild(multiStringValue);
}
}
}
else if (value is string) // string, expandable (there is no way to differentiate a string and expandable value in .NET 1.1)
{
registryValue.Type = Wix.RegistryValue.TypeType.@string;
registryValue.Value = (string)value;
}
else
{
// TODO: put a better exception here
throw new Exception();
}
registryValues.Add(registryValue);
}
// If there were no subkeys and no values, we still need an element for this empty registry key.
// But specifically avoid SOFTWARE\Classes because it shouldn't be harvested as an empty key.
if (parts.Length > 1 && registryKey.SubKeyCount == 0 && registryKey.ValueCount == 0 &&
!String.Equals(parts[1], HKCRPathInHKLM, StringComparison.OrdinalIgnoreCase))
{
Wix.RegistryValue emptyRegistryKey = new Wix.RegistryValue();
emptyRegistryKey.Root = root;
emptyRegistryKey.Key = parts[1];
emptyRegistryKey.Type = Wix.RegistryValue.TypeType.@string;
emptyRegistryKey.Value = String.Empty;
emptyRegistryKey.Action = Wix.RegistryValue.ActionType.write;
registryValues.Add(emptyRegistryKey);
}
}
///
/// Remap a registry key to an alternative location.
///
/// The registry key to remap.
/// The path to remap the registry key to under HKLM.
private void RemapRegistryKey(IntPtr registryKey, string remappedPath)
{
IntPtr remappedKey = IntPtr.Zero;
try
{
remappedKey = NativeMethods.OpenRegistryKey(this.regRootToOverride, remappedPath);
NativeMethods.OverrideRegistryKey(registryKey, remappedKey);
}
finally
{
if (IntPtr.Zero != remappedKey)
{
NativeMethods.CloseRegistryKey(remappedKey);
}
}
}
///
/// Remove the remapped registry key.
///
private void RemoveRemappedKey()
{
try
{
this.regKeyToOverride.DeleteSubKeyTree(this.remappedPath);
}
catch (ArgumentException)
{
// ignore the error where the key does not exist
}
}
///
/// The native methods for re-mapping registry keys.
///
private sealed class NativeMethods
{
internal static readonly IntPtr HkeyClassesRoot = (IntPtr)unchecked((Int32)0x80000000);
internal static readonly IntPtr HkeyCurrentUser = (IntPtr)unchecked((Int32)0x80000001);
internal static readonly IntPtr HkeyLocalMachine = (IntPtr)unchecked((Int32)0x80000002);
internal static readonly IntPtr HkeyUsers = (IntPtr)unchecked((Int32)0x80000003);
private const uint GenericRead = 0x80000000;
private const uint GenericWrite = 0x40000000;
private const uint GenericExecute = 0x20000000;
private const uint GenericAll = 0x10000000;
private const uint StandardRightsAll = 0x001F0000;
///
/// Opens a registry key.
///
/// Base key to open.
/// Path to subkey to open.
/// Handle to new key.
internal static IntPtr OpenRegistryKey(IntPtr key, string path)
{
IntPtr newKey = IntPtr.Zero;
uint disposition = 0;
uint sam = StandardRightsAll | GenericRead | GenericWrite | GenericExecute | GenericAll;
if (0 != RegCreateKeyEx(key, path, 0, null, 0, sam, 0, out newKey, out disposition))
{
throw new Exception();
}
return newKey;
}
///
/// Closes a previously open registry key.
///
/// Handle to key to close.
internal static void CloseRegistryKey(IntPtr key)
{
if (0 != RegCloseKey(key))
{
throw new Exception();
}
}
///
/// Override a registry key.
///
/// Handle of the key to override.
/// Handle to override key.
internal static void OverrideRegistryKey(IntPtr key, IntPtr newKey)
{
if (0 != RegOverridePredefKey(key, newKey))
{
throw new Exception();
}
}
///
/// Interop to RegCreateKeyW.
///
/// Handle to base key.
/// Subkey to create.
/// Always 0
/// Just pass null.
/// Just pass 0.
/// Rights to registry key.
/// Just pass null.
/// Opened key.
/// Whether key was opened or created.
/// Handle to registry key.
[DllImport("advapi32.dll", EntryPoint = "RegCreateKeyExW", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
private static extern int RegCreateKeyEx(IntPtr key, string subkey, uint reserved, string className, uint options, uint desiredSam, uint securityAttributes, out IntPtr openedKey, out uint disposition);
///
/// Interop to RegCloseKey.
///
/// Handle to key to close.
/// 0 if success.
[DllImport("advapi32.dll", EntryPoint = "RegCloseKey", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
private static extern int RegCloseKey(IntPtr key);
///
/// Interop to RegOverridePredefKey.
///
/// Handle to key to override.
/// Handle to override key.
/// 0 if success.
[DllImport("advapi32.dll", EntryPoint = "RegOverridePredefKey", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
private static extern int RegOverridePredefKey(IntPtr key, IntPtr newKey);
}
}
}