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;
}