Re-implement registry save paths, split registry per path, export via Microsoft.Win32
This commit is contained in:
parent
b3919b73f9
commit
2e6fdd1925
4 changed files with 356 additions and 36 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
145
LANCommander.SDK/Utilities/RegistryExportUtility.cs
Normal file
145
LANCommander.SDK/Utilities/RegistryExportUtility.cs
Normal 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}")
|
||||
};
|
||||
}
|
||||
}
|
||||
198
LANCommander.SDK/Utilities/RegistryImportUtility.cs
Normal file
198
LANCommander.SDK/Utilities/RegistryImportUtility.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue