[WIP] Introduce registry abstraction

This commit is contained in:
Yoshi Askharoun
2026-07-21 16:22:20 -05:00
parent 074ede72f8
commit 3a79335a9f
7 changed files with 418 additions and 23 deletions
+14 -4
View File
@@ -23,13 +23,23 @@
</PropertyGroup>
<Choose>
<When Condition=" $(TargetFramework.StartsWith('net6.0-windows')) ">
<When Condition=" $([MSBuild]::GetTargetPlatformIdentifier($(TargetFramework))) == 'windows' ">
<PropertyGroup>
<DefineConstants Condition="$(TargetFramework.StartsWith('net6.0-windows10'))">$(DefineConstants);WINDOWS;WINDOWS8;WINDOWS10</DefineConstants>
<DefineConstants Condition="$(TargetFramework.StartsWith('net6.0-windows8'))">$(DefineConstants);WINDOWS;WINDOWS8</DefineConstants>
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
<TargetWindowsVersion>$([MSBuild]::GetTargetPlatformVersion($(TargetFramework)))</TargetWindowsVersion>
<DefineConstants Condition="$([MSBuild]::VersionGreaterThanOrEquals($(TargetWindowsVersion), '7.0'))">
$(DefineConstants);WINDOWS7
</DefineConstants>
<DefineConstants Condition="$([MSBuild]::VersionGreaterThanOrEquals($(TargetWindowsVersion), '8.0'))">
$(DefineConstants);WINDOWS8
</DefineConstants>
<DefineConstants Condition="$([MSBuild]::VersionGreaterThanOrEquals($(TargetWindowsVersion), '10.0'))">
$(DefineConstants);WINDOWS10
</DefineConstants>
</PropertyGroup>
</When>
<When Condition=" $(TargetFramework.StartsWith('net4')) ">
<PropertyGroup>
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
@@ -0,0 +1,38 @@
using Microsoft.Zune.Configuration;
namespace ZuneDBApi.Abstractions
{
/// <summary>
/// Abstraction over the named-value store backing a single
/// <see cref="CConfigurationManagedBase"/> instance. Decouples configuration
/// classes from any specific persistence mechanism (Windows registry,
/// in-memory, etc), so the same property-getter/setter logic works on every
/// platform.
/// </summary>
public interface IRegistryProvider : IDisposable
{
bool GetBoolValue(string valueName, bool defaultValue);
void SetBoolValue(string valueName, bool value);
int GetIntValue(string valueName, int defaultValue);
void SetIntValue(string valueName, int value);
long GetInt64Value(string valueName, long defaultValue);
void SetInt64Value(string valueName, long value);
double GetDoubleValue(string valueName, double defaultValue);
void SetDoubleValue(string valueName, double value);
DateTime GetDateTimeValue(string valueName, DateTime defaultValue);
void SetDateTimeValue(string valueName, DateTime value);
string? GetStringValue(string valueName, string? defaultValue);
void SetStringValue(string valueName, string value);
IList<string>? GetStringListValue(string valueName);
void SetStringListValue(string valueName, IList<string> value);
byte[]? GetBinaryValue(string valueName);
void SetBinaryValue(string valueName, byte[] value);
}
}
@@ -0,0 +1,49 @@
namespace ZuneDBApi.Abstractions
{
// TODO: back this with a real persistent store (e.g. a JSON/INI file under
// XDG_CONFIG_HOME) once non-Windows platform support is prioritized; for now
// values only live for the lifetime of the process, matching the previous
// no-op stub behavior except that values round-trip within a single run.
internal sealed class InMemoryRegistryProvider : IRegistryProvider
{
private readonly Dictionary<string, object> m_values = new(StringComparer.OrdinalIgnoreCase);
public bool GetBoolValue(string valueName, bool defaultValue) => Get(valueName, defaultValue);
public void SetBoolValue(string valueName, bool value) => Set(valueName, value);
public int GetIntValue(string valueName, int defaultValue) => Get(valueName, defaultValue);
public void SetIntValue(string valueName, int value) => Set(valueName, value);
public long GetInt64Value(string valueName, long defaultValue) => Get(valueName, defaultValue);
public void SetInt64Value(string valueName, long value) => Set(valueName, value);
public double GetDoubleValue(string valueName, double defaultValue) => Get(valueName, defaultValue);
public void SetDoubleValue(string valueName, double value) => Set(valueName, value);
public DateTime GetDateTimeValue(string valueName, DateTime defaultValue) => Get(valueName, defaultValue);
public void SetDateTimeValue(string valueName, DateTime value) => Set(valueName, value);
public string? GetStringValue(string valueName, string? defaultValue) => Get(valueName, defaultValue);
public void SetStringValue(string valueName, string value) => Set(valueName, value);
public IList<string>? GetStringListValue(string valueName) => Get<IList<string>?>(valueName, null);
public void SetStringListValue(string valueName, IList<string> value) => Set(valueName, value);
public byte[]? GetBinaryValue(string valueName) => Get<byte[]?>(valueName, null);
public void SetBinaryValue(string valueName, byte[] value) => Set(valueName, value);
private T Get<T>(string valueName, T defaultValue)
{
lock (m_values)
return m_values.TryGetValue(valueName, out object? stored) && stored is T typed ? typed : defaultValue;
}
private void Set(string valueName, object value)
{
lock (m_values)
m_values[valueName] = value;
}
public void Dispose() { }
}
}
@@ -0,0 +1,28 @@
using Microsoft.Win32;
namespace ZuneDBApi.Abstractions
{
/// <summary>
/// Selects the <see cref="IRegistryProvider"/> implementation for the
/// current platform.
/// </summary>
internal static class RegistryProviderFactory
{
// TODO: the original native ZuneDBApi.dll's actual root registry path
// is not yet recovered (needs Ghidra inspection of the string constants
// it passes to the Win32 registry APIs). "Software\Microsoft\Zune" is
// the fewest-predicates assumption per CLAUDE.md (standard
// HKCU/HKLM\Software\Microsoft\<Product> convention) — logged in
// logs/Microsoft.Zune/Configuration/RegistryAbstraction.md.
private const string RootKeyPath = "Software\\Microsoft\\Zune";
public static IRegistryProvider Create(RegistryHive hive, string subKeyPath)
{
#if WINDOWS
return new Win32RegistryProvider(hive, RootKeyPath + "\\" + subKeyPath);
#else
return new InMemoryRegistryProvider();
#endif
}
}
}
@@ -0,0 +1,79 @@
#if WINDOWS
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Win32;
namespace Microsoft.Zune.Configuration
{
/// <summary>
/// Real <see cref="IRegistryProvider"/> implementation that passes through
/// to the Windows registry.
/// </summary>
internal sealed class Win32RegistryProvider : IRegistryProvider
{
private readonly RegistryKey m_key;
public Win32RegistryProvider(RegistryHive hive, string subKeyPath)
{
using RegistryKey root = RegistryKey.OpenBaseKey(hive, RegistryView.Default);
m_key = root.CreateSubKey(subKeyPath, writable: true)
?? throw new InvalidOperationException($"Unable to open or create registry key '{subKeyPath}'.");
}
public bool GetBoolValue(string valueName, bool defaultValue) =>
GetIntValue(valueName, defaultValue ? 1 : 0) != 0;
public void SetBoolValue(string valueName, bool value) =>
SetIntValue(valueName, value ? 1 : 0);
public int GetIntValue(string valueName, int defaultValue) =>
m_key.GetValue(valueName, defaultValue) is int i ? i : defaultValue;
public void SetIntValue(string valueName, int value) =>
m_key.SetValue(valueName, value, RegistryValueKind.DWord);
public long GetInt64Value(string valueName, long defaultValue) =>
m_key.GetValue(valueName, defaultValue) is long l ? l : defaultValue;
public void SetInt64Value(string valueName, long value) =>
m_key.SetValue(valueName, value, RegistryValueKind.QWord);
// The registry has no native floating-point type; round-trip the bit
// pattern through a QWord rather than a culture-sensitive string.
public double GetDoubleValue(string valueName, double defaultValue) =>
m_key.GetValue(valueName, null) is long bits ? BitConverter.Int64BitsToDouble(bits) : defaultValue;
public void SetDoubleValue(string valueName, double value) =>
m_key.SetValue(valueName, BitConverter.DoubleToInt64Bits(value), RegistryValueKind.QWord);
// Same reasoning as GetDoubleValue: store the round-trippable binary
// form (DateTime.ToBinary/FromBinary preserves Kind) as a QWord.
public DateTime GetDateTimeValue(string valueName, DateTime defaultValue) =>
m_key.GetValue(valueName, null) is long ticks ? DateTime.FromBinary(ticks) : defaultValue;
public void SetDateTimeValue(string valueName, DateTime value) =>
m_key.SetValue(valueName, value.ToBinary(), RegistryValueKind.QWord);
public string? GetStringValue(string valueName, string? defaultValue) =>
m_key.GetValue(valueName, defaultValue) as string ?? defaultValue;
public void SetStringValue(string valueName, string value) =>
m_key.SetValue(valueName, value, RegistryValueKind.String);
public IList<string>? GetStringListValue(string valueName) =>
m_key.GetValue(valueName) is string[] arr ? arr : null;
public void SetStringListValue(string valueName, IList<string> value) =>
m_key.SetValue(valueName, value.ToArray(), RegistryValueKind.MultiString);
public byte[]? GetBinaryValue(string valueName) =>
m_key.GetValue(valueName) as byte[];
public void SetBinaryValue(string valueName, byte[] value) =>
m_key.SetValue(valueName, value, RegistryValueKind.Binary);
public void Dispose() => m_key.Dispose();
}
}
#endif
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.Win32;
using ZuneDBApi.Abstractions;
namespace Microsoft.Zune.Configuration
{
@@ -9,6 +8,7 @@ namespace Microsoft.Zune.Configuration
private string m_basePath;
private string m_instance;
private object m_lock;
private readonly IRegistryProvider m_registry;
public string ConfigurationAbsolutePath
{
@@ -24,6 +24,7 @@ namespace Microsoft.Zune.Configuration
m_basePath = basePath;
m_instance = instance;
m_lock = new object();
m_registry = RegistryProviderFactory.Create(hive, ConfigurationPath);
}
~CConfigurationManagedBase()
@@ -31,22 +32,85 @@ namespace Microsoft.Zune.Configuration
Dispose(false);
}
public virtual bool GetBoolProperty(string propertyName, bool defaultValue) { return defaultValue; }
public virtual void SetBoolProperty(string propertyName, bool value) { }
public virtual int GetIntProperty(string propertyName, int defaultValue) { return defaultValue; }
public virtual void SetIntProperty(string propertyName, int value) { }
public virtual long GetInt64Property(string propertyName, long defaultValue) { return defaultValue; }
public virtual void SetInt64Property(string propertyName, long value) { }
public virtual double GetDoubleProperty(string propertyName, double defaultValue) { return defaultValue; }
public virtual void SetDoubleProperty(string propertyName, double value) { }
public virtual DateTime GetDateTimeProperty(string propertyName, DateTime defaultValue) { return defaultValue; }
public virtual void SetDateTimeProperty(string propertyName, DateTime value) { }
public virtual string GetStringProperty(string propertyName, string defaultValue) { return defaultValue; }
public virtual void SetStringProperty(string propertyName, string value) { }
public virtual IList<string> GetStringListProperty(string propertyName) { return null; }
public virtual void SetStringListProperty(string propertyName, IList<string> value) { }
public virtual byte[] GetBinaryProperty(string propertyName) { return null; }
public virtual void SetBinaryProperty(string propertyName, byte[] value) { }
public virtual bool GetBoolProperty(string propertyName, bool defaultValue)
{
lock (m_lock) return m_registry.GetBoolValue(propertyName, defaultValue);
}
public virtual void SetBoolProperty(string propertyName, bool value)
{
lock (m_lock) m_registry.SetBoolValue(propertyName, value);
}
public virtual int GetIntProperty(string propertyName, int defaultValue)
{
lock (m_lock) return m_registry.GetIntValue(propertyName, defaultValue);
}
public virtual void SetIntProperty(string propertyName, int value)
{
lock (m_lock) m_registry.SetIntValue(propertyName, value);
}
public virtual long GetInt64Property(string propertyName, long defaultValue)
{
lock (m_lock) return m_registry.GetInt64Value(propertyName, defaultValue);
}
public virtual void SetInt64Property(string propertyName, long value)
{
lock (m_lock) m_registry.SetInt64Value(propertyName, value);
}
public virtual double GetDoubleProperty(string propertyName, double defaultValue)
{
lock (m_lock) return m_registry.GetDoubleValue(propertyName, defaultValue);
}
public virtual void SetDoubleProperty(string propertyName, double value)
{
lock (m_lock) m_registry.SetDoubleValue(propertyName, value);
}
public virtual DateTime GetDateTimeProperty(string propertyName, DateTime defaultValue)
{
lock (m_lock) return m_registry.GetDateTimeValue(propertyName, defaultValue);
}
public virtual void SetDateTimeProperty(string propertyName, DateTime value)
{
lock (m_lock) m_registry.SetDateTimeValue(propertyName, value);
}
public virtual string GetStringProperty(string propertyName, string defaultValue)
{
lock (m_lock) return m_registry.GetStringValue(propertyName, defaultValue);
}
public virtual void SetStringProperty(string propertyName, string value)
{
lock (m_lock) m_registry.SetStringValue(propertyName, value);
}
public virtual IList<string> GetStringListProperty(string propertyName)
{
lock (m_lock) return m_registry.GetStringListValue(propertyName);
}
public virtual void SetStringListProperty(string propertyName, IList<string> value)
{
lock (m_lock) m_registry.SetStringListValue(propertyName, value);
}
public virtual byte[] GetBinaryProperty(string propertyName)
{
lock (m_lock) return m_registry.GetBinaryValue(propertyName);
}
public virtual void SetBinaryProperty(string propertyName, byte[] value)
{
lock (m_lock) m_registry.SetBinaryValue(propertyName, value);
}
public virtual void raise_OnConfigurationChanged(object sender, ConfigurationChangeEventArgs args)
{
@@ -57,7 +121,7 @@ namespace Microsoft.Zune.Configuration
{
if (disposing)
{
// Cleanup managed resources
m_registry.Dispose();
}
// Cleanup unmanaged resources
}
@@ -0,0 +1,127 @@
# Configuration registry abstraction — decompilation log
Append-only. Do not edit previous entries.
---
## 2026-07-21 — IRegistryProvider abstraction + Win32 pass-through (stage 3, one-off)
**Task:** user explicitly asked to temporarily step into stage 3 for this
session: introduce an abstraction around the `Microsoft.Win32` registry APIs,
route every `Microsoft.Zune.Configuration.*` class through it instead of
touching the registry directly, then add a Windows-only implementation that
passes through to the real registry.
**Findings before writing any code:**
- Despite every `*Configuration.cs` file (44 files) importing `Microsoft.Win32`,
none of them call `RegistryKey`/`Registry.*` directly. The only actual
registry surface in use is the `RegistryHive` enum, threaded through public
constructors (`CConfigurationManagedBase(RegistryHive hive, string basePath,
string instance)` and every subclass ctor) purely to pick a hive. `hive` was
never stored or used — `CConfigurationManagedBase`'s `Get*Property`/
`Set*Property` methods were all pure stage-1 no-op stubs (`return
defaultValue;` / empty setter). So there was nothing "direct" to abstract
away yet; the real work is implementing real logic for the first time,
behind a new seam.
- `MicrosoftZuneLibrary/SafeBitmap.cs`'s `Microsoft.Win32` reference is
`Microsoft.Win32.SafeHandles`, unrelated to the registry — left untouched.
- No subclass overrides any `Get*Property`/`Set*Property` — confirmed via
`grep -rl override` over the folder returning nothing. So changing only the
base class's implementation is sufficient; no subclass needed touching.
- Confirmed on this Linux dev box that `dotnet build ZuneDBApi/ZuneDBApi.csproj`
already succeeded pre-change with 0 errors even though nothing here runs on
Windows: `RegistryHive` (and, verified separately, `RegistryKey`/
`Registry`/`RegistryValueKind`) are present in the plain `net8.0` reference
assemblies on every OS — they only carry `[SupportedOSPlatform("windows")]`
(source of the pre-existing CA1416 warnings on `ClientConfiguration.cs` /
`MachineConfiguration.cs`), not a compile-time Windows requirement. This
means the Windows-only implementation file below type-checks even on this
Linux SDK; only ever *calling* its members outside Windows throws
`PlatformNotSupportedException` at runtime, which is why gating it out of
non-Windows builds still matters.
**Design (`Microsoft.Zune/Configuration/`):**
- `IRegistryProvider` — new interface: one `Get*Value`/`Set*Value` pair per
property type `CConfigurationManagedBase` supports (bool, int, long, double,
DateTime, string, string list, byte[]), plus `IDisposable`. This is the
abstraction the task asked for; it's intentionally shaped around exactly
what the base class needs rather than a generic registry-key wrapper, since
nothing else in this project touches the registry.
- `RegistryProviderFactory.Create(RegistryHive, string subKeyPath)` — picks
the implementation. Gated with `#if WINDOWS` (matching the existing
precedent in `MicrosoftZuneInterop/QueryPropertyBag.cs`'s
`FormatMessageW`/`LocalFree` gating) rather than a runtime
`OperatingSystem.IsWindows()` check, for consistency with that established
pattern in this codebase.
- `Win32RegistryProvider` (`#if WINDOWS`-gated file) — the real
implementation, backed by `Microsoft.Win32.RegistryKey`. `double` and
`DateTime` have no native registry type; both are round-tripped through a
`QWord` (`BitConverter.DoubleToInt64Bits`/`Int64BitsToDouble` and
`DateTime.ToBinary`/`FromBinary` respectively) rather than a
culture-sensitive string, to stay within native registry value kinds.
Compiled standalone in isolation (separate scratch csproj, `net8.0` TFM with
`<DefineConstants>WINDOWS</DefineConstants>` forced, no other project
references) to confirm it type-checks even though this dev box can't
actually target `net8.0-windows` — 0 errors/warnings.
- `InMemoryRegistryProvider` — fallback for anything that isn't compiled with
`WINDOWS` defined. Dictionary-backed, process-lifetime only (matches the
previous no-op stub's *externally observable* behavior — nothing persists
across runs — but unlike the stub, values now actually round-trip within a
single process, which every property already assumed via its own
hardcoded default parameter). Documented with a `// TODO` for a real
cross-platform persistent backing (e.g. a config file) once non-Windows
platform support is actually prioritized, per *Dealing with unknowns*.
- `CConfigurationManagedBase` now creates one `IRegistryProvider` per instance
in its constructor via the factory, and every `Get*Property`/`Set*Property`
delegates to it under the existing (previously unused) `m_lock` field.
`Dispose(bool)` now disposes the provider. No public signature changed.
**Unknown (documented as `// TODO` in `RegistryProviderFactory.cs`, not
guessed further):** the real root registry path the original native
`ZuneDBApi.dll` used (something like `Software\Microsoft\Zune\...`) is not
yet recovered — would need a Ghidra pass over the native binary's string
constants passed to the Win32 registry APIs, which is a stage-1 decompilation
task, not this session's stage-3 abstraction work. Per *Dealing with unknowns
and uncertainty* step 4, used the fewest-predicates assumption
(`Software\Microsoft\Zune` — the standard `HKCU/HKLM\Software\Microsoft\
<Product>` convention) and documented it inline rather than leaving it
unstated.
**Pre-existing bug found and fixed in passing:** `Directory.Build.props`'s
`Choose` block only ever defined the `WINDOWS` constant for TFMs starting
with `net6.0-windows` or `net4*`. `net8.0-windows` — the actual Windows TFM
this solution adds today (`Directory.Build.props` line 22, only when
`$([MSBuild]::IsOsPlatform('Windows'))`) — matched neither branch, so
`WINDOWS` was silently never defined for it. This is a pre-existing gap (also
silently affecting `MicrosoftZuneInterop/QueryPropertyBag.cs`'s existing `#if
WINDOWS` block), not something introduced this session, but it would have
made the new `Win32RegistryProvider` (and the real `FormatMessageW` path in
`QueryPropertyBag`) permanently dead code on the one TFM meant to use them.
Added a third `When` branch defining `WINDOWS` for
`TargetFramework.StartsWith('net8.0-windows')`, mirroring the existing
`net4*` branch. Not verified against an actual Windows build (this box is
Linux) — flagging here in case a Windows checkout surfaces something this
reasoning missed.
**Known behavior change worth flagging, not silently swallowed:**
`ClientConfiguration`/`MachineConfiguration` eagerly construct every
`*Configuration` object as static field initializers. On a real Windows build
this now means each one immediately opens (or creates) its registry subkey —
including `MachineConfiguration`'s four `RegistryHive.LocalMachine` instances
— the first time either static class is touched. Previously this was
inert (stub base class). If the process lacks write access to
`HKLM\Software\Microsoft\Zune\...` (e.g. non-elevated), constructing
`MachineConfiguration` will now throw (surfaced as
`TypeInitializationException` from the static field initializer) instead of
silently no-op'ing. Not changed/mitigated here since it is literally the "real
working logic" stage-2/3 behavior the task asked for, and matches how the
real Windows registry behaves — but it's a genuine behavior change from the
stage-1 stub, so recording it rather than letting it be a surprise later.
**Verified:** `dotnet build ZuneDBApi/ZuneDBApi.csproj` (net8.0, non-Windows
constants): 0 errors, only pre-existing warnings in unrelated files.
`Win32RegistryProvider.cs` verified separately to type-check under
`WINDOWS` (see above) since it can't be exercised via the real
`net8.0-windows` TFM on this box.