Skip to content

Commit

Permalink
fix: Component Registration and Locales (#137)
Browse files Browse the repository at this point in the history
* feat: Enable per user COM registration

According to https://learn.microsoft.com/en-us/windows/win32/sysinfo/merged-view-of-hkey-classes-root
the registration can be performed without adminstrative permissions
if the current user can be used as root key.

* fix: Corrected ManagedCategory

According https://referencesource.microsoft.com/#mscorlib/system/runtime/interopservices/registrationservices.cs,5bc5aa43d644bb71
the ManagedCategoryGuid including the locale must be set if it
is not set correctly

* feat!: Added check and verification option

The managed category check is now optional and
will be omitted by default.
In contrast to the original implementation,
the setting of the 0 value can be omitted, enforced
or its presence can only be verified.

Closes #133

---------

Co-authored-by: Carsten Igel <[email protected]>
Co-authored-by: Mark Lechtermann <[email protected]>
  • Loading branch information
3 people authored Feb 15, 2023
1 parent af0b602 commit f16daf8
Showing 1 changed file with 185 additions and 7 deletions.
192 changes: 185 additions & 7 deletions src/dscom/RegistrationServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using Microsoft.Win32;

namespace dSPACE.Runtime.InteropServices;
Expand All @@ -25,6 +27,57 @@ namespace dSPACE.Runtime.InteropServices;
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Compatibility to the mscorelib TypeLibConverter class")]
public class RegistrationServices
{
/// <summary>
/// When registering a managed type to the classes using the
/// <see cref="RegisterAssembly(Assembly, bool, ManagedCategoryAction)"/> method,
/// the managed registration method can check whether the registry key
/// HKEY_CLASSES_ROOT\Component Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}\
/// is present and has at least one key representing a locale code having the
/// value ".NET Category".
/// </summary>
public enum ManagedCategoryAction
{
/// <summary>
/// No check or action is performed.
/// </summary>
None,

/// <summary>
/// A check is performed. If at least one numeric key exist below
/// HKEY_CLASSES_ROOT\Component Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}\
/// and all numeric keys have the correct value.
/// </summary>
FailIfNotPresent,

/// <summary>
/// A check is performed. This is performed like <see cref="FailIfNotPresent"/>, but
/// only for a neutral language, i.e. language code 0.
/// </summary>
FailIfNotPresentOnlyForNeutralLocale,

/// <summary>
/// This will actually create the key and value below
/// HKEY_CLASSES_ROOT\Component Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}\
/// concerning only neutral languages, i.e. language code 0.
/// This is the original implementation as of
/// <code>System.Runtime.InteropServices.RegisterAssembly(Assembly, RegistrationFlags)</code>.
/// If the operation fails, e.g. due to missing privileges or elevation,
/// any exception will be transmitted.
/// </summary>
Create,

/// <summary>
/// This will actually create the key and value below
/// HKEY_CLASSES_ROOT\Component Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}\
/// concerning only neutral languages, i.e. language code 0.
/// This is the original implementation as of
/// <code>System.Runtime.InteropServices.RegisterAssembly(Assembly, RegistrationFlags)</code>.
/// If the operation fails, e.g. due to missing privileges or elevation,
/// any exception will be swallowed.
/// </summary>
CreateSilently
}

private static class RegistryKeys
{
private const string Implemented = nameof(Implemented);
Expand All @@ -41,6 +94,10 @@ private static class RegistryKeys
public const string InprocServer32 = nameof(InprocServer32);
public const string ProgId = nameof(ProgId);

public const string Software = nameof(Software);

public const string Classes = nameof(Classes);

public const string ManagedCategoryGuid = "{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}"; // Found in mscorelib

public const string ImplementedCategories = $"{Implemented} {Categories}";
Expand Down Expand Up @@ -123,8 +180,9 @@ public void UnregisterTypeForComClients(int cookie)
/// </summary>
/// <param name="assembly">The assembly to register.</param>
/// <param name="registerCodeBase">If set to <c>true</c>, the code base will be added to the registry; otherwise not.</param>
/// <param name="preferredAction">The managed category action for a global registration of HKEY_CLASSES_ROOT\Component Categories\62C8FE65-4EBB-45e7-B440-6E39B2CDBF29</param>
/// <returns><c>true</c>, if at least one type from the registry has been registered.</returns>
public bool RegisterAssembly(Assembly assembly, bool registerCodeBase)
public bool RegisterAssembly(Assembly assembly, bool registerCodeBase, ManagedCategoryAction preferredAction = ManagedCategoryAction.None)
{
if (assembly is null)
{
Expand Down Expand Up @@ -172,7 +230,7 @@ public bool RegisterAssembly(Assembly assembly, bool registerCodeBase)
}
else
{
RegisterManagedType(type, fullName, assemblyVersion, codeBase, runtimeVersion);
RegisterManagedType(type, fullName, assemblyVersion, codeBase, runtimeVersion, preferredAction);
}

// Skip: CustomRegistrationFunction
Expand Down Expand Up @@ -305,7 +363,8 @@ private static void RegisterValueType(Type type, string assemblyName, string ass

var recordId = $"{{{MarshalExtension.GetClassInterfaceGuidForType(type).ToString().ToUpperInvariant()}}}";

using var recordRootKey = Registry.ClassesRoot.CreateSubKey(RegistryKeys.Record);
using var rootKey = GetTargetRootKey();
using var recordRootKey = rootKey.CreateSubKey(RegistryKeys.Record);
using var recordKey = recordRootKey.CreateSubKey(recordId);
using var recordVersionKey = recordKey.CreateSubKey(assemblyVersion);

Expand All @@ -325,7 +384,8 @@ private static bool UnregisterValueType(Type type, string assemblyVersion)
{
var recordId = $"{{{MarshalExtension.GetClassInterfaceGuidForType(type).ToString().ToUpperInvariant()}}}";

using var recordRootKey = Registry.ClassesRoot.OpenSubKey(RegistryKeys.Record, true);
using var rootKey = GetTargetRootKey();
using var recordRootKey = rootKey.OpenSubKey(RegistryKeys.Record, true);
using var recordKey = recordRootKey?.OpenSubKey(recordId, true);
using var recordVersionKey = recordKey?.OpenSubKey(assemblyVersion, true);

Expand Down Expand Up @@ -357,7 +417,7 @@ private static bool UnregisterValueType(Type type, string assemblyVersion)
return allVersionsGone;
}

private static void RegisterManagedType(Type type, string assemblyName, string assemblyVersion, string? codeBase, string runtimeVersion)
private static void RegisterManagedType(Type type, string assemblyName, string assemblyVersion, string? codeBase, string runtimeVersion, ManagedCategoryAction preferredAction)
{
if (type.FullName is null)
{
Expand All @@ -368,9 +428,10 @@ private static void RegisterManagedType(Type type, string assemblyName, string a
var clsId = $"{{{Marshal.GenerateGuidForType(type).ToString().ToUpperInvariant()}}}";
var progId = Marshal.GenerateProgIdForType(type);

using var rootKey = GetTargetRootKey();
if (!string.IsNullOrWhiteSpace(progId))
{
using var typeNameKey = Registry.ClassesRoot.CreateSubKey(progId!);
using var typeNameKey = rootKey.CreateSubKey(progId!);

typeNameKey.SetValue(string.Empty, docString);

Expand All @@ -379,7 +440,7 @@ private static void RegisterManagedType(Type type, string assemblyName, string a
progIdClsIdKey.SetValue(string.Empty, clsId);
}

using var clsIdRootKey = Registry.ClassesRoot.CreateSubKey(RegistryKeys.CLSID);
using var clsIdRootKey = rootKey.CreateSubKey(RegistryKeys.CLSID);

using var clsIdKey = clsIdRootKey.CreateSubKey(clsId);

Expand Down Expand Up @@ -428,6 +489,90 @@ private static void RegisterManagedType(Type type, string assemblyName, string a

using var managedCategoryKeyForImplemented = implementedCategoryKey.CreateSubKey(RegistryKeys.ManagedCategoryGuid);

SetupGlobalManagedCategoryAction(preferredAction);
}

private static void SetupGlobalManagedCategoryAction(ManagedCategoryAction preferredAction)
{
switch (preferredAction)
{
case ManagedCategoryAction.Create:
case ManagedCategoryAction.CreateSilently:
{
try
{
EnsureManageCategoryIsPresent();
}
catch (Exception e) when ((preferredAction is ManagedCategoryAction.CreateSilently)
&& (e is UnauthorizedAccessException or SecurityException))
{
Debug.WriteLine(e);
}
}
break;
case ManagedCategoryAction.FailIfNotPresent:
case ManagedCategoryAction.FailIfNotPresentOnlyForNeutralLocale:
{
if (!HasManagedCategory(preferredAction == ManagedCategoryAction.FailIfNotPresentOnlyForNeutralLocale))
{
var message = $"HKEY_CLASSES_ROOT\\{RegistryKeys.ComponentCategories}\\{RegistryKeys.ManagedCategoryGuid} does not provide a localized or neutral definition value containing '{RegistryValues.ManagedCategoryDescription}'";

throw new InvalidOperationException(message);
}
}
break;
}
}

private static bool HasManagedCategory(bool checkForNeutralLocale = true)
{
using var componentCategoryKey = Registry.ClassesRoot.OpenSubKey(RegistryKeys.ComponentCategories, false);
if (componentCategoryKey is null)
{
return false;
}

using var managedCategoryKeyCheck = componentCategoryKey.OpenSubKey(RegistryKeys.ManagedCategoryGuid, false);
if (managedCategoryKeyCheck is null)
{
return false;
}

if (checkForNeutralLocale)
{
var key0 = Convert.ToString(0, CultureInfo.InvariantCulture);
var value = managedCategoryKeyCheck.GetValue(key0);
if (value is null || value.GetType() != typeof(string))
{
return false;
}

var exactValue = (string)value;
return StringComparer.InvariantCulture.Equals(exactValue, RegistryValues.ManagedCategoryDescription);
}
else
{
var valueNames = managedCategoryKeyCheck.GetValueNames().Where(item => int.TryParse(item, out _)).ToArray();
return valueNames.LongLength > 0 && valueNames.All(key =>
{
var value = managedCategoryKeyCheck.GetValue(key);
if (value is not null and string exactValue)
{
return StringComparer.InvariantCulture.Equals(exactValue, RegistryValues.ManagedCategoryDescription);
}

return false;
});
}
}

private static void EnsureManageCategoryIsPresent()
{
if (HasManagedCategory())
{
return;
}

using var componentCategoryKey = Registry.ClassesRoot.CreateSubKey(RegistryKeys.ComponentCategories);
using var managedCategoryKey = componentCategoryKey.CreateSubKey(RegistryKeys.ManagedCategoryGuid);

Expand Down Expand Up @@ -674,4 +819,37 @@ private static bool IsEmptyRegistryKey(RegistryKey? key)

return key!.SubKeyCount == 0 && key!.ValueCount == 0;
}

private static RegistryKey GetTargetRootKey()
{
// According to
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/merged-view-of-hkey-classes-root
// COM registration can take place per user without elevated permissions.
if (CanWriteGlobalRegistry())
{
return Registry.ClassesRoot;
}

var root = Registry.CurrentUser;
using var software = root.CreateSubKey(RegistryKeys.Software, true);
var classes = software.CreateSubKey(RegistryKeys.Software, true);
return classes;
}

private static bool CanWriteGlobalRegistry()
{
try
{
_ = Registry.ClassesRoot.OpenSubKey(RegistryKeys.Software.ToUpperInvariant());
// This must be done using exceptions, since RegistryPermissions from CAS
// Will no longer work in .NET 6 and above and will return true always.

return true;
}
catch (Exception e) when (e is UnauthorizedAccessException or SecurityException)
{
return false;
}
}

}

0 comments on commit f16daf8

Please sign in to comment.