Re-implement registry save paths, split registry per path, export via Microsoft.Win32

This commit is contained in:
RattleSN4K3 2025-06-28 00:10:54 +02:00
parent b3919b73f9
commit 2e6fdd1925
4 changed files with 356 additions and 36 deletions

View file

@ -198,9 +198,10 @@ namespace LANCommander.SDK.Services
#endregion
#region Handle registry importing
var registryImportFilePath = Path.Combine(tempLocation, "_registry.reg");
var registryImportFilePaths = Directory.GetFiles(tempLocation, "_registry*.reg");
var importer = new RegistryImportUtility();
if (File.Exists(registryImportFilePath))
foreach (var registryImportFilePath in registryImportFilePaths)
{
var registryImportFileContents = File.ReadAllText(registryImportFilePath);

View file

@ -0,0 +1,145 @@
#pragma warning disable CA1416
using Microsoft.Win32;
using System;
using System.IO;
using System.Linq;
using System.Text;
namespace LANCommander.SDK.Utilities;
/// <summary>
/// Recursively reads one or more Windows registry paths and builds a single “.reg”-format text blob.
/// </summary>
/// <remarks>
/// - Emits the standard "Windows Registry Editor Version 5.00" header (UTF-8 with BOM). <br/>
/// - Walks each key and all its subkeys, serializing REG_SZ, REG_EXPAND_SZ, REG_DWORD,
/// REG_QWORD, REG_MULTI_SZ and REG_BINARY with correct syntax. <br/>
/// - Does not call any external process or use temp files—pure .NET managed code. <br/>
/// </remarks>
/// <example>
/// // 1) Export two registry branches into a single .reg string
/// var exporter = new RegistryExportUtility();
/// var paths = new[] {
/// @"HKEY_CURRENT_USER\Software\MyApp",
/// @"HKLM\SOFTWARE\Vendor\Product"
/// };
/// string blob = exporter.Export(paths);
///
/// // 2) Persist to disk (must be UTF-8 for regedit.exe)
/// var utf8 = new UTF8Encoding(true);
/// File.WriteAllBytes(@"C:\temp\myexport.reg", utf8.GetPreamble()
/// .Concat(unicode.GetBytes(blob))
/// .ToArray());
/// </example>
public class RegistryExportUtility
{
/// <summary>
/// Builds a single .reg-format string from multiple registry paths.
/// </summary>
/// <param name="registryPaths">Enumerable of registry key paths to export.</param>
/// <returns>Fully-formed .reg text (excluding BOM/preamble).</returns>
public string Export(params string[] registryPaths)
{
var sb = new StringBuilder();
sb.AppendLine("Windows Registry Editor Version 5.00");
sb.AppendLine();
foreach (var fullPath in registryPaths)
{
var (hive, subKey) = ParseHive(fullPath);
using (var key = hive.OpenSubKey(subKey))
{
if (key != null)
ExportKeyRecursive(key, sb);
}
}
return sb.ToString();
}
private void ExportKeyRecursive(RegistryKey key, StringBuilder sb)
{
sb.AppendLine($"[{key.Name}]");
foreach (var name in key.GetValueNames())
sb.AppendLine(FormatValue(key, name));
sb.AppendLine();
foreach (var child in key.GetSubKeyNames())
{
using (var sub = key.OpenSubKey(child))
{
if (sub != null)
ExportKeyRecursive(sub, sb);
}
}
}
private string FormatValue(RegistryKey key, string name)
{
var kind = key.GetValueKind(name);
var data = key.GetValue(name);
var label = string.IsNullOrEmpty(name) ? "@" : $"\"{name}\"";
switch (kind)
{
case RegistryValueKind.String:
return $"{label}=\"{Escape((string)data)}\"";
case RegistryValueKind.ExpandString:
return $"{label}=hex(2):{ToHex(Encoding.Unicode.GetBytes((string)data + "\0"))}";
case RegistryValueKind.DWord:
return $"{label}=dword:{((uint)(int)data):x8}";
case RegistryValueKind.QWord:
return $"{label}=hex(b):{ToHex(BitConverter.GetBytes((ulong)data))}";
case RegistryValueKind.MultiString:
return $"{label}=hex(7):{ToHex(EncodeMulti((string[])data))}";
case RegistryValueKind.Binary:
return $"{label}=hex:{ToHex((byte[])data)}";
default:
// fallback as raw binary
var raw = key.GetValue(name, null, RegistryValueOptions.DoNotExpandEnvironmentNames) as byte[] ?? Array.Empty<byte>();
return $"{label}=hex:{ToHex(raw)}";
}
}
private static byte[] EncodeMulti(string[] values)
{
using var ms = new MemoryStream();
foreach (var s in values)
ms.Write(Encoding.Unicode.GetBytes(s + "\0"));
// extra null terminator
ms.Write(new byte[2], 0, 2);
return ms.ToArray();
}
private static string Escape(string s) =>
s.Replace("\\", "\\\\").Replace("\"", "\\\"");
private static string ToHex(byte[] data) =>
string.Join(",", data.Select(b => b.ToString("x2")));
private (RegistryKey hive, string subKey) ParseHive(string full)
{
var parts = full.Split(['\\'], 2);
var root = parts[0]?.ToUpperInvariant().Trim().Trim(':') ?? "HKCU";
var tail = parts.Length > 1 ? parts[1] : string.Empty;
return root switch
{
"HKLM" or "HKEY_LOCAL_MACHINE" => (Registry.LocalMachine, tail),
"HKCU" or "HKEY_CURRENT_USER" => (Registry.CurrentUser, tail),
"HKCR" or "HKEY_CLASSES_ROOT" => (Registry.ClassesRoot, tail),
"HKU" or "HKEY_USERS" => (Registry.Users, tail),
"HKCC" or "HKEY_CURRENT_CONFIG" => (Registry.CurrentConfig, tail),
_ => throw new ArgumentException($"Unknown registry hive: {root}")
};
}
}

View file

@ -0,0 +1,198 @@
#pragma warning disable CA1416
using Microsoft.Win32;
using System;
using System.IO;
using System.Linq;
using System.Text;
namespace LANCommander.SDK.Utilities;
/// <summary>
/// Parses and imports a Windows “.reg” file into the live registry.
/// Supports files encoded as UTF-8 (with BOM) or ANSI.
/// </summary>
/// <remarks>
/// - Handles key sections ([HIVE\Path\…]) and value lines ("Name"=…, @=…).
/// - Recognizes REG_SZ, REG_EXPAND_SZ, REG_DWORD, REG_QWORD, REG_MULTI_SZ, REG_BINARY.
/// - Ignores security/ACLs; only writes values.
/// </remarks>
/// <example>
/// // 1) Import directly from a file stream (UTF-8 BOM is auto-detected):
/// var importer = new RegistryImportUtility();
/// using var fs = File.OpenRead(@"C:\temp\export.reg");
/// importer.Import(fs);
///
/// // 2) Or read into a string and import:
/// string content = File.ReadAllText(@"C:\temp\export.reg", Encoding.UTF8);
/// importer.ImportFromString(content);
/// </example>
public class RegistryImportUtility
{
/// <summary>
/// Reads the .reg data from <paramref name="regStream"/>, detects a UTF-8 BOM,
/// and imports all keys & values into the registry.
/// </summary>
/// <param name="regStream">Stream containing .reg text (UTF-8 or ANSI).</param>
public void Import(Stream regStream)
{
// detectEncodingFromByteOrderMarks = true will strip BOM for us
using var reader = new StreamReader(regStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
string content = reader.ReadToEnd();
ImportFromString(content);
}
/// <summary>
/// Parses the raw .reg-format text and writes all keys & values.
/// </summary>
/// <param name="regFileContent">The entire .reg file contents as a string.</param>
/// <remarks>Takes the full .reg text in a string (no encoding concern here).</remarks>
public void ImportFromString(string regFileContent)
{
using var reader = new StringReader(regFileContent);
string? currentFullPath = null;
RegistryKey? currentKey = null;
string? line;
while ((line = reader.ReadLine()) != null)
{
line = line.Trim();
if (line.Length == 0 || line.StartsWith(";"))
continue;
// Skip header
if (line.StartsWith("Windows Registry Editor Version", StringComparison.OrdinalIgnoreCase))
continue;
if (line.Equals("REGEDIT4", StringComparison.OrdinalIgnoreCase))
continue;
// New section: [HIVE\SubKey\…]
if (line.StartsWith("[") && line.EndsWith("]"))
{
// close previous
currentKey?.Dispose();
currentFullPath = line[1..^1];
var (hive, subKey) = ParseHive(currentFullPath);
currentKey = hive.CreateSubKey(subKey, writable: true);
continue;
}
// Value line: "Name"=type:data or @=...
if (currentKey == null)
throw new InvalidOperationException("Value outside of any key section.");
var eq = line.IndexOf('=');
if (eq < 0)
continue;
var nameToken = line[..eq].Trim();
var dataToken = line[(eq + 1)..].Trim();
var valueName = nameToken == "@" ? "" : Unquote(nameToken);
// Dispatch by prefix
if (dataToken.StartsWith("\""))
{
// simple string
var s = Unquote(dataToken);
currentKey.SetValue(valueName, UnescapeString(s), RegistryValueKind.String);
}
else if (dataToken.StartsWith("dword:", StringComparison.OrdinalIgnoreCase))
{
var hex = dataToken["dword:".Length..];
var d = Convert.ToUInt32(hex, 16);
currentKey.SetValue(valueName, (int)d, RegistryValueKind.DWord);
}
else if (dataToken.StartsWith("hex(", StringComparison.OrdinalIgnoreCase))
{
// e.g. hex(2):00,FF,00,… or hex(b):… or hex(7):…
var kindEnd = dataToken.IndexOf(')');
var kindId = dataToken["hex(".Length..kindEnd];
var payload = dataToken[(kindEnd + 2)..]; // skip "):"
var bytes = ParseHex(payload);
var rvk = kindId switch
{
"0" => RegistryValueKind.None,
"1" => RegistryValueKind.String,
"2" => RegistryValueKind.ExpandString,
"3" => RegistryValueKind.Binary,
"7" => RegistryValueKind.MultiString,
"b" => RegistryValueKind.QWord,
_ => RegistryValueKind.Binary
};
object finalData = rvk switch
{
RegistryValueKind.ExpandString => Encoding.Unicode.GetString(bytes).TrimEnd('\0'),
RegistryValueKind.MultiString => ParseMultiString(bytes),
RegistryValueKind.QWord => BitConverter.ToUInt64(bytes, 0),
_ => bytes
};
currentKey.SetValue(valueName, finalData, rvk);
}
else if (dataToken.StartsWith("hex:", StringComparison.OrdinalIgnoreCase))
{
// raw binary
var payload = dataToken["hex:".Length..];
var bytes = ParseHex(payload);
currentKey.SetValue(valueName, bytes, RegistryValueKind.Binary);
}
else
{
// unknown, skip
}
}
currentKey?.Dispose();
}
private static (RegistryKey hive, string subKey) ParseHive(string full)
{
var parts = full.Split(new[] { '\\' }, 2);
var root = parts[0].ToUpperInvariant().Trim().Trim(':');
var tail = parts.Length > 1 ? parts[1] : "";
return root switch
{
"HKLM" or "HKEY_LOCAL_MACHINE" => (Registry.LocalMachine, tail),
"HKCU" or "HKEY_CURRENT_USER" => (Registry.CurrentUser, tail),
"HKCR" or "HKEY_CLASSES_ROOT" => (Registry.ClassesRoot, tail),
"HKU" or "HKEY_USERS" => (Registry.Users, tail),
"HKCC" or "HKEY_CURRENT_CONFIG" => (Registry.CurrentConfig, tail),
_ => throw new ArgumentException($"Unknown hive: {root}")
};
}
private static string Unquote(string s) =>
s.Length >= 2 && s[0] == '"' && s[^1] == '"'
? s[1..^1]
: s;
private static string UnescapeString(string s) =>
s.Replace(@"\\", @"\").Replace("\\\"", "\"");
private static byte[] ParseHex(string hexData)
{
// split by commas, remove any trailing commas or backslashes
var tokens = hexData
.TrimEnd('\\')
.Split(',')
.Where(tok => tok.Length > 0)
.ToArray();
var bytes = new byte[tokens.Length];
for (int i = 0; i < tokens.Length; i++)
bytes[i] = Convert.ToByte(tokens[i], 16);
return bytes;
}
private static string[] ParseMultiString(byte[] raw)
{
// UTF-8, strings nul-terminated, ends with double null
var all = Encoding.UTF8.GetString(raw);
return all
.TrimEnd('\0')
.Split('\0', StringSplitOptions.RemoveEmptyEntries);
}
}

View file

@ -64,43 +64,19 @@ public class SavePacker : IDisposable
public SavePacker AddRegistryPath(SavePath registryPath)
{
throw new NotImplementedException("Registry stuff has to be rebuilt to avoid reg.exe");
/*
List<string> tempRegFiles = new List<string>();
if (registryPath.Type != SavePathType.Registry)
return this;
Logger?.LogTrace("Building registry export file");
// outsource export
var exporter = new RegistryExportUtility();
string regFileContent = exporter.Export(registryPath.Path);
var exportCommand = new StringBuilder();
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == Enums.SavePathType.Registry))
{
var tempRegFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".reg");
exportCommand.AppendLine($"reg.exe export \"{savePath.Path.Replace(":\\", "\\")}\" \"{tempRegFile}\"");
tempRegFiles.Add(tempRegFile);
}
var script = new PowerShellScript(Enums.ScriptType.SaveUpload);
script.UseInline(exportCommand.ToString());
if (Client.Scripts.Debug)
script.EnableDebug();
await script.ExecuteAsync<int>();
var exportFile = new StringBuilder();
foreach (var tempRegFile in tempRegFiles)
{
exportFile.AppendLine(File.ReadAllText(tempRegFile));
File.Delete(tempRegFile);
}
writer.Write("_registry.reg", new MemoryStream(Encoding.UTF8.GetBytes(exportFile.ToString())));
*/
// write out as UTF8 .reg
var bytes = Encoding.UTF8.GetBytes(regFileContent);
var file = new MemoryStream(bytes);
var index = _archive.Entries.Count(x => x.Key?.StartsWith("_registry") ?? false);
_archive.AddEntry($"_registry{index}.reg", file);
return this;
}