diff --git a/LANCommander.SDK/Services/SaveService.cs b/LANCommander.SDK/Services/SaveService.cs index 0e87a34b..8fe2f6bc 100644 --- a/LANCommander.SDK/Services/SaveService.cs +++ b/LANCommander.SDK/Services/SaveService.cs @@ -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); diff --git a/LANCommander.SDK/Utilities/RegistryExportUtility.cs b/LANCommander.SDK/Utilities/RegistryExportUtility.cs new file mode 100644 index 00000000..8307c2db --- /dev/null +++ b/LANCommander.SDK/Utilities/RegistryExportUtility.cs @@ -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; + +/// +/// Recursively reads one or more Windows registry paths and builds a single “.reg”-format text blob. +/// +/// +/// - Emits the standard "Windows Registry Editor Version 5.00" header (UTF-8 with BOM).
+/// - 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.
+/// - Does not call any external process or use temp files—pure .NET managed code.
+///
+/// +/// // 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()); +/// +public class RegistryExportUtility +{ + /// + /// Builds a single .reg-format string from multiple registry paths. + /// + /// Enumerable of registry key paths to export. + /// Fully-formed .reg text (excluding BOM/preamble). + 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(); + 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}") + }; + } +} diff --git a/LANCommander.SDK/Utilities/RegistryImportUtility.cs b/LANCommander.SDK/Utilities/RegistryImportUtility.cs new file mode 100644 index 00000000..528bd123 --- /dev/null +++ b/LANCommander.SDK/Utilities/RegistryImportUtility.cs @@ -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; + +/// +/// Parses and imports a Windows “.reg” file into the live registry. +/// Supports files encoded as UTF-8 (with BOM) or ANSI. +/// +/// +/// - 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. +/// +/// +/// // 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); +/// +public class RegistryImportUtility +{ + /// + /// Reads the .reg data from , detects a UTF-8 BOM, + /// and imports all keys & values into the registry. + /// + /// Stream containing .reg text (UTF-8 or ANSI). + 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); + } + + /// + /// Parses the raw .reg-format text and writes all keys & values. + /// + /// The entire .reg file contents as a string. + /// Takes the full .reg text in a string (no encoding concern here). + 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); + } +} diff --git a/LANCommander.SDK/Utilities/SavePackager.cs b/LANCommander.SDK/Utilities/SavePackager.cs index 8e463ce6..7475a02d 100644 --- a/LANCommander.SDK/Utilities/SavePackager.cs +++ b/LANCommander.SDK/Utilities/SavePackager.cs @@ -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 tempRegFiles = new List(); + 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(); - - 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; }