From 0ac735ee25694b7fde24eaa4c9f09617cdffeb36 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:20:14 -0500 Subject: [PATCH 01/18] Code cleanup, bug fixes, and style improvements Removed unused usings, improved struct marshalling, standardized variable declarations, and refactored method visibility and formatting. Fixed XML formatting in App.config and improved error logging. Minor bug fixes and code consistency updates throughout. --- RenProfile/App.config | 4 +-- RenProfile/NativeMethods.cs | 9 +++---- RenProfile/Privileges.cs | 7 +----- RenProfile/Program.cs | 35 ++++++++++++--------------- RenProfile/Properties/AssemblyInfo.cs | 1 - RenProfile/RegistryUtils.cs | 5 ---- 6 files changed, 21 insertions(+), 40 deletions(-) diff --git a/RenProfile/App.config b/RenProfile/App.config index 9d2c7ad..67c4d8f 100644 --- a/RenProfile/App.config +++ b/RenProfile/App.config @@ -1,6 +1,6 @@ - - + + diff --git a/RenProfile/NativeMethods.cs b/RenProfile/NativeMethods.cs index d12963b..b93d2da 100644 --- a/RenProfile/NativeMethods.cs +++ b/RenProfile/NativeMethods.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; namespace RenProfileConsole { @@ -16,6 +13,7 @@ internal class Privileges internal struct TOKEN_PRIVILEGES { public int PrivilegeCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } @@ -29,7 +27,6 @@ internal struct LUID_AND_ATTRIBUTES internal struct LUID { - public UInt32 LowPart; public Int32 HighPart; } @@ -37,7 +34,7 @@ internal struct LUID [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool AdjustTokenPrivileges(IntPtr hTok, - [MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges, + [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, UInt32 BufferLengthInBytes, ref TOKEN_PRIVILEGES PreviousState, @@ -46,7 +43,7 @@ internal static extern bool AdjustTokenPrivileges(IntPtr hTok, [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool AdjustTokenPrivileges(IntPtr hTok, - [MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges, + [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, UInt32 BufferLengthInBytes, IntPtr PreviousState, diff --git a/RenProfile/Privileges.cs b/RenProfile/Privileges.cs index bcb253f..9d35118 100644 --- a/RenProfile/Privileges.cs +++ b/RenProfile/Privileges.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; namespace RenProfileConsole { @@ -19,7 +14,7 @@ public static bool EnablePrivileges(string privilege) NativeMethods.Privileges.LUID luid = new NativeMethods.Privileges.LUID(); if (NativeMethods.Privileges.LookupPrivilegeValue(null, privilege, ref luid)) { - var tokenPrivileges = new NativeMethods.Privileges.TOKEN_PRIVILEGES(); + NativeMethods.Privileges.TOKEN_PRIVILEGES tokenPrivileges = new NativeMethods.Privileges.TOKEN_PRIVILEGES(); tokenPrivileges.PrivilegeCount = 1; tokenPrivileges.Privileges = new NativeMethods.Privileges.LUID_AND_ATTRIBUTES[1]; tokenPrivileges.Privileges[0].Attributes = NativeMethods.Privileges.SE_PRIVILEGE_ENABLED; diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 06315eb..c9427b2 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using Microsoft.Win32; +using System; using System.IO; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.Win32; namespace RenProfileConsole { @@ -16,17 +11,18 @@ namespace RenProfileConsole /// A command line tool to rename a user profile by updating all references to the profile folder in the registry /// and renaming the folder. /// - class Program + internal class Program { //- Define Variables public static bool logErrs = false; public static int errors = 0; public static int successful = 0; public static int processed = 0; + //- Define Paths - public static String oldDir=""; - public static String newDir=""; - public static String errPath=""; + public static String oldDir = ""; + public static String newDir = ""; + public static String errPath = ""; private static void Main(string[] args) { @@ -58,8 +54,9 @@ private static void Main(string[] args) else if (Directory.Exists(newDir)) { Console.WriteLine("New user profile path already exists. Use another name."); ExitConsole(-1); return; } } catch (Exception ex) - { - if (logErrs) { LogError(ex.ToString()); Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); } ExitConsole(-1); return; + { + if (logErrs) { LogError(ex.ToString()); Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); } + ExitConsole(-1); return; } //- No errors and parameters are all as expected. @@ -77,7 +74,6 @@ public static void Start() // is restricted without requesting extra privilges. if (Privileges.EnablePrivileges("SeRestorePrivilege")) { - int loadCode = RegistryUtils.LoadUserHive($"{oldDir}\\NTUSER.dat", tempUserHiveName); using (RegistryKey userHive = Registry.Users.OpenSubKey(tempUserHiveName)) @@ -108,7 +104,7 @@ public static void Start() } } - static void ExitConsole(int exitCode) + private static void ExitConsole(int exitCode) { Console.WriteLine(""); Console.WriteLine("Exit code is " + exitCode + " (-1=fatal error, 0=fail, 1=success"); @@ -117,20 +113,19 @@ static void ExitConsole(int exitCode) Environment.Exit(exitCode); } - static void LogError(string errorMessage) + private static void LogError(string errorMessage) { try { //- Condition ? True : False - using (var file = File.Exists(errPath) ? File.Open(errPath, FileMode.Append) : File.Open(errPath, FileMode.CreateNew)) - using (var sw = new StreamWriter(file)) + using (FileStream file = File.Exists(errPath) ? File.Open(errPath, FileMode.Append) : File.Open(errPath, FileMode.CreateNew)) + using (StreamWriter sw = new StreamWriter(file)) { sw.WriteLine($"{DateTime.Now.ToString()}: {errorMessage} \n==========="); } } catch { /*Error writing to error log*/} } - - static void UpdateKey(RegistryKey key, string oldDir, string newDir) + private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { try { diff --git a/RenProfile/Properties/AssemblyInfo.cs b/RenProfile/Properties/AssemblyInfo.cs index e29cb44..674abc3 100644 --- a/RenProfile/Properties/AssemblyInfo.cs +++ b/RenProfile/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/RenProfile/RegistryUtils.cs b/RenProfile/RegistryUtils.cs index be65cfa..865ac0b 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -1,9 +1,5 @@ using Microsoft.Win32; using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; namespace RenProfileConsole @@ -54,7 +50,6 @@ public static void CloneSubKey(RegistryKey root, RegistryKey source, string newN { using (RegistryKey target = root.CreateSubKey(newName)) { - foreach (string name in source.GetValueNames()) { target.SetValue(name, source.GetValue(name), source.GetValueKind(name)); From 1d31939b019dfa1708fb61818efc5460d53b6f06 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:24:35 -0500 Subject: [PATCH 02/18] Refactor for readability and code style consistency Reformat P/Invoke signatures and method calls across NativeMethods.cs and Privileges.cs for improved clarity. Expand conditional logic in Program.cs to multi-line blocks for better readability and explicit error handling. In RegistryUtils.cs, simplify exception handling and add comments for intent. No functional changes; all updates focus on code style and maintainability. --- RenProfile/NativeMethods.cs | 21 +++++++--- RenProfile/Privileges.cs | 16 +++++++- RenProfile/Program.cs | 81 +++++++++++++++++++++++++++++++------ RenProfile/RegistryUtils.cs | 4 +- 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/RenProfile/NativeMethods.cs b/RenProfile/NativeMethods.cs index b93d2da..256cce5 100644 --- a/RenProfile/NativeMethods.cs +++ b/RenProfile/NativeMethods.cs @@ -33,7 +33,8 @@ internal struct LUID [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool AdjustTokenPrivileges(IntPtr hTok, + internal static extern bool AdjustTokenPrivileges( + IntPtr hTok, [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, UInt32 BufferLengthInBytes, @@ -42,7 +43,8 @@ internal static extern bool AdjustTokenPrivileges(IntPtr hTok, [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool AdjustTokenPrivileges(IntPtr hTok, + internal static extern bool AdjustTokenPrivileges( + IntPtr hTok, [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, UInt32 BufferLengthInBytes, @@ -51,7 +53,10 @@ internal static extern bool AdjustTokenPrivileges(IntPtr hTok, [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool LookupPrivilegeValue(string lpsystemname, string lpname, [MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid); + internal static extern bool LookupPrivilegeValue( + string lpsystemname, + string lpname, + [MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid); } internal class Process @@ -66,7 +71,10 @@ internal enum TokenAccess : uint internal static extern IntPtr GetCurrentProcess(); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] - internal static extern bool OpenProcessToken(IntPtr processHandle, uint desiredAccess, out IntPtr tokenHandle); + internal static extern bool OpenProcessToken( + IntPtr processHandle, + uint desiredAccess, + out IntPtr tokenHandle); } internal class Registry @@ -83,7 +91,10 @@ internal class Registry internal class FileSystem { [DllImport("kernel32.dll", SetLastError = true)] - public static extern int GetShortPathName(String pathName, StringBuilder shortName, int cbShortName); + public static extern int GetShortPathName( + String pathName, + StringBuilder shortName, + int cbShortName); } } } diff --git a/RenProfile/Privileges.cs b/RenProfile/Privileges.cs index 9d35118..cdd9543 100644 --- a/RenProfile/Privileges.cs +++ b/RenProfile/Privileges.cs @@ -12,9 +12,11 @@ public class Privileges public static bool EnablePrivileges(string privilege) { NativeMethods.Privileges.LUID luid = new NativeMethods.Privileges.LUID(); + if (NativeMethods.Privileges.LookupPrivilegeValue(null, privilege, ref luid)) { NativeMethods.Privileges.TOKEN_PRIVILEGES tokenPrivileges = new NativeMethods.Privileges.TOKEN_PRIVILEGES(); + tokenPrivileges.PrivilegeCount = 1; tokenPrivileges.Privileges = new NativeMethods.Privileges.LUID_AND_ATTRIBUTES[1]; tokenPrivileges.Privileges[0].Attributes = NativeMethods.Privileges.SE_PRIVILEGE_ENABLED; @@ -24,12 +26,22 @@ public static bool EnablePrivileges(string privilege) if (hProc != null) { - if (NativeMethods.Process.OpenProcessToken(hProc, (uint)NativeMethods.Process.TokenAccess.AdjustPrivileges, out IntPtr hToken)) + if (NativeMethods.Process.OpenProcessToken( + hProc, + (uint)NativeMethods.Process.TokenAccess.AdjustPrivileges, + out IntPtr hToken)) { - return NativeMethods.Privileges.AdjustTokenPrivileges(hToken, false, ref tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero); + return NativeMethods.Privileges.AdjustTokenPrivileges( + hToken, + false, + ref tokenPrivileges, + 0, + IntPtr.Zero, + IntPtr.Zero); } } } + return false; } } diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index c9427b2..c2f25e8 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -15,12 +15,14 @@ internal class Program { //- Define Variables public static bool logErrs = false; + public static int errors = 0; public static int successful = 0; public static int processed = 0; //- Define Paths public static String oldDir = ""; + public static String newDir = ""; public static String errPath = ""; @@ -29,34 +31,89 @@ private static void Main(string[] args) try { //- Parameter error checking - if (args.Length == 0) { Console.WriteLine("Missing all parameters."); ExitConsole(-1); return; } + if (args.Length == 0) + { + Console.WriteLine("Missing all parameters."); + ExitConsole(-1); + return; + } else if (args.Length == 1) { - if (args[0] == "?") { Console.WriteLine(@"Syntax: C:\Full\Path\To\Profile C:\Path\To\Desired\Profile C:\Optional\Path\To\Write\Errors.txt"); ExitConsole(1); return; } - else { Console.WriteLine("Missing 1 parameter."); ExitConsole(-1); return; } + if (args[0] == "?") + { + Console.WriteLine(@"Syntax: C:\Full\Path\To\Profile C:\Path\To\Desired\Profile C:\Optional\Path\To\Write\Errors.txt"); + ExitConsole(1); + return; + } + else + { + Console.WriteLine("Missing 1 parameter."); + ExitConsole(-1); + return; + } } else if (args.Length == 3) { - errPath = args[2]; if (!String.IsNullOrWhiteSpace(errPath) || errPath.Length > 4) { logErrs = true; } - else { Console.WriteLine("Error log was specified but path is invalid."); ExitConsole(-1); return; } + errPath = args[2]; + + if (!String.IsNullOrWhiteSpace(errPath) || errPath.Length > 4) + { + logErrs = true; + } + else + { + Console.WriteLine("Error log was specified but path is invalid."); + ExitConsole(-1); + return; + } } else if (args.Length > 3) - { Console.WriteLine("Missing 1 parameter."); ExitConsole(-1); } + { + Console.WriteLine("Missing 1 parameter."); + ExitConsole(-1); + return; + } //- Set variables oldDir = args[0]; newDir = args[1]; //- Path error checking - if (String.IsNullOrWhiteSpace(oldDir) || oldDir.Length < 4) { Console.WriteLine("Old user profile path is invalid."); ExitConsole(-1); return; } - else if (String.IsNullOrWhiteSpace(oldDir) || oldDir.Length < 4) { Console.WriteLine("New user profile path is invalid."); ExitConsole(-1); return; } - else if (!Directory.Exists(oldDir)) { Console.WriteLine("Old user profile path does not exist."); ExitConsole(-1); return; } - else if (Directory.Exists(newDir)) { Console.WriteLine("New user profile path already exists. Use another name."); ExitConsole(-1); return; } + if (String.IsNullOrWhiteSpace(oldDir) || oldDir.Length < 4) + { + Console.WriteLine("Old user profile path is invalid."); + ExitConsole(-1); + return; + } + else if (String.IsNullOrWhiteSpace(newDir) || newDir.Length < 4) + { + Console.WriteLine("New user profile path is invalid."); + ExitConsole(-1); + return; + } + else if (!Directory.Exists(oldDir)) + { + Console.WriteLine("Old user profile path does not exist."); + ExitConsole(-1); + return; + } + else if (Directory.Exists(newDir)) + { + Console.WriteLine("New user profile path already exists. Use another name."); + ExitConsole(-1); + return; + } } catch (Exception ex) { - if (logErrs) { LogError(ex.ToString()); Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); } - ExitConsole(-1); return; + if (logErrs) + { + LogError(ex.ToString()); + Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); + } + + ExitConsole(-1); + return; } //- No errors and parameters are all as expected. diff --git a/RenProfile/RegistryUtils.cs b/RenProfile/RegistryUtils.cs index 865ac0b..5cec8cc 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -22,8 +22,9 @@ public static void IterateKeys(this RegistryKey root, Action action key.IterateKeys(action); } } - catch (Exception e) + catch (Exception) { + // Swallow exception, continue } }); @@ -33,6 +34,7 @@ public static void IterateKeys(this RegistryKey root, Action action public static void RenameValue(this RegistryKey key, string oldName, string newName) { key.SetValue(newName, key.GetValue(oldName), key.GetValueKind(oldName)); + key.DeleteValue(oldName); } From d7a1deaffaa30058c7bf369c8c254ecf52154e2a Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:30:37 -0500 Subject: [PATCH 03/18] Refactor catch blocks for readability and clarity Refactored several catch blocks in Program.cs from single-line to expanded, multi-line format. This improves code readability and maintainability by clearly separating error logging, console output, and error counting. No changes were made to the underlying logic. --- RenProfile/Program.cs | 49 +++++++++++++++++++++++++++++++++---- RenProfile/RegistryUtils.cs | 1 - 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index c2f25e8..5c7846f 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -214,7 +214,15 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) Console.WriteLine(String.Join(":", newValues)); successful++; } - catch (Exception ex) { if (logErrs) { LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error setting: {newValues}"); errors++; } + catch (Exception ex) + { + if (logErrs) + { + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); + } + Console.WriteLine($"Error setting: {newValues}"); + errors++; + } } } else if (key.GetValueKind(name) == RegistryValueKind.String || key.GetValueKind(name) == RegistryValueKind.ExpandString) @@ -230,7 +238,15 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) Console.WriteLine($"Set: {newValue}"); successful++; } - catch (Exception ex) { if (logErrs) { LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error setting: {newValue}"); errors++; } + catch (Exception ex) + { + if (logErrs) + { + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); + } + Console.WriteLine($"Error setting: {newValue}"); + errors++; + } } } @@ -247,7 +263,15 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) catch { Console.WriteLine($"Error renaming: {newValueName}"); errors++; } } } - catch (Exception ex) { if (logErrs) { LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error accessing: {name}"); errors++; } + catch (Exception ex) + { + if (logErrs) + { + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); + } + Console.WriteLine($"Error accessing: {name}"); + errors++; + } } string keypattern = pattern.Replace(@"\", @"%5C"); @@ -266,11 +290,26 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) Console.WriteLine($"Updated: {newKeyName}"); successful++; } - catch (Exception ex) { if (logErrs) { LogError($"Error accessing/writing {keyName}\n{ex.ToString()}"); } Console.WriteLine($"Error updating: {newKeyName}"); errors++; } + catch (Exception ex) + { + if (logErrs) + { + LogError($"Error accessing/writing {keyName}\n{ex.ToString()}"); + } + Console.WriteLine($"Error updating: {newKeyName}"); + errors++; + } } } } - catch (Exception ex) { if (logErrs) { LogError($"Error accessing/writing {key.ToString()}\n{ex.ToString()}"); } errors++; } + catch (Exception ex) + { + if (logErrs) + { + LogError($"Error accessing/writing {key.ToString()}\n{ex.ToString()}"); + } + errors++; + } } } } diff --git a/RenProfile/RegistryUtils.cs b/RenProfile/RegistryUtils.cs index 5cec8cc..809bc11 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -34,7 +34,6 @@ public static void IterateKeys(this RegistryKey root, Action action public static void RenameValue(this RegistryKey key, string oldName, string newName) { key.SetValue(newName, key.GetValue(oldName), key.GetValueKind(oldName)); - key.DeleteValue(oldName); } From 37707723559c3d85706f4f6d24bfc28b61cab462 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:35:07 -0500 Subject: [PATCH 04/18] Improve error path validation in argument parsing Enhanced the validation logic for the error path (errPath) argument. Now checks for non-whitespace, minimum length, absence of invalid path characters, and existence of the directory. This prevents invalid or non-existent paths from being accepted. --- RenProfile/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 5c7846f..1d4a29e 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -56,7 +56,10 @@ private static void Main(string[] args) { errPath = args[2]; - if (!String.IsNullOrWhiteSpace(errPath) || errPath.Length > 4) + if (!String.IsNullOrWhiteSpace(errPath) + && errPath.Length > 4 + && errPath.IndexOfAny(Path.GetInvalidPathChars()) == -1 + && Directory.Exists(Path.GetDirectoryName(errPath))) { logErrs = true; } From 1fac181b238f637de5dacd1e0e5901a9bfad478a Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:39:14 -0500 Subject: [PATCH 05/18] Improve privilege handling and thread safety Added CloseHandle P/Invoke and refactored privilege enabling to ensure proper handle cleanup and error checking. Replaced direct counter increments with Interlocked.Increment for thread safety. Enhanced error handling throughout registry operations. --- RenProfile/NativeMethods.cs | 4 +++ RenProfile/Privileges.cs | 65 ++++++++++++++++++++++--------------- RenProfile/Program.cs | 29 ++++++++++------- 3 files changed, 59 insertions(+), 39 deletions(-) diff --git a/RenProfile/NativeMethods.cs b/RenProfile/NativeMethods.cs index 256cce5..2b60b97 100644 --- a/RenProfile/NativeMethods.cs +++ b/RenProfile/NativeMethods.cs @@ -75,6 +75,10 @@ internal static extern bool OpenProcessToken( IntPtr processHandle, uint desiredAccess, out IntPtr tokenHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CloseHandle(IntPtr hObject); } internal class Registry diff --git a/RenProfile/Privileges.cs b/RenProfile/Privileges.cs index cdd9543..08b4111 100644 --- a/RenProfile/Privileges.cs +++ b/RenProfile/Privileges.cs @@ -13,36 +13,47 @@ public static bool EnablePrivileges(string privilege) { NativeMethods.Privileges.LUID luid = new NativeMethods.Privileges.LUID(); - if (NativeMethods.Privileges.LookupPrivilegeValue(null, privilege, ref luid)) + if (!NativeMethods.Privileges.LookupPrivilegeValue(null, privilege, ref luid)) { - NativeMethods.Privileges.TOKEN_PRIVILEGES tokenPrivileges = new NativeMethods.Privileges.TOKEN_PRIVILEGES(); - - tokenPrivileges.PrivilegeCount = 1; - tokenPrivileges.Privileges = new NativeMethods.Privileges.LUID_AND_ATTRIBUTES[1]; - tokenPrivileges.Privileges[0].Attributes = NativeMethods.Privileges.SE_PRIVILEGE_ENABLED; - tokenPrivileges.Privileges[0].Luid = luid; - - IntPtr hProc = NativeMethods.Process.GetCurrentProcess(); - - if (hProc != null) - { - if (NativeMethods.Process.OpenProcessToken( - hProc, - (uint)NativeMethods.Process.TokenAccess.AdjustPrivileges, - out IntPtr hToken)) - { - return NativeMethods.Privileges.AdjustTokenPrivileges( - hToken, - false, - ref tokenPrivileges, - 0, - IntPtr.Zero, - IntPtr.Zero); - } - } + return false; } - return false; + NativeMethods.Privileges.TOKEN_PRIVILEGES tokenPrivileges = new NativeMethods.Privileges.TOKEN_PRIVILEGES(); + + tokenPrivileges.PrivilegeCount = 1; + tokenPrivileges.Privileges = new NativeMethods.Privileges.LUID_AND_ATTRIBUTES[1]; + tokenPrivileges.Privileges[0].Attributes = NativeMethods.Privileges.SE_PRIVILEGE_ENABLED; + tokenPrivileges.Privileges[0].Luid = luid; + + IntPtr hProc = NativeMethods.Process.GetCurrentProcess(); + + if (hProc == IntPtr.Zero) + { + return false; + } + + if (!NativeMethods.Process.OpenProcessToken( + hProc, + (uint)NativeMethods.Process.TokenAccess.AdjustPrivileges, + out IntPtr hToken)) + { + return false; + } + + try + { + return NativeMethods.Privileges.AdjustTokenPrivileges( + hToken, + false, + ref tokenPrivileges, + 0, + IntPtr.Zero, + IntPtr.Zero); + } + finally + { + NativeMethods.Process.CloseHandle(hToken); + } } } } diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 1d4a29e..76700b2 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; namespace RenProfileConsole { @@ -200,7 +201,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) foreach (string name in names) { if (String.IsNullOrWhiteSpace(name)) { continue; } - processed++; + Interlocked.Increment(ref processed); try { @@ -215,7 +216,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { key.SetValue(name, newValues); Console.WriteLine(String.Join(":", newValues)); - successful++; + Interlocked.Increment(ref successful); } catch (Exception ex) { @@ -224,7 +225,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error setting: {newValues}"); - errors++; + Interlocked.Increment(ref errors); } } } @@ -239,7 +240,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { key.SetValue(name, newValue); Console.WriteLine($"Set: {newValue}"); - successful++; + Interlocked.Increment(ref successful); } catch (Exception ex) { @@ -248,7 +249,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error setting: {newValue}"); - errors++; + Interlocked.Increment(ref errors); } } } @@ -261,9 +262,13 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { key.RenameValue(name, newValueName); Console.WriteLine($"Renamed: {newValueName}"); - successful++; + Interlocked.Increment(ref successful); + } + catch + { + Console.WriteLine($"Error renaming: {newValueName}"); + Interlocked.Increment(ref errors); } - catch { Console.WriteLine($"Error renaming: {newValueName}"); errors++; } } } catch (Exception ex) @@ -273,7 +278,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error accessing: {name}"); - errors++; + Interlocked.Increment(ref errors); } } @@ -282,7 +287,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) foreach (string keyName in key.GetSubKeyNames()) { if (String.IsNullOrWhiteSpace(keyName)) { continue; } - processed++; + Interlocked.Increment(ref processed); string newKeyName = Regex.Replace(keyName, keypattern, keyNewDir, RegexOptions.IgnoreCase); if (keyName != newKeyName) @@ -291,7 +296,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { key.RenameSubKey(keyName, newKeyName); Console.WriteLine($"Updated: {newKeyName}"); - successful++; + Interlocked.Increment(ref successful); } catch (Exception ex) { @@ -300,7 +305,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) LogError($"Error accessing/writing {keyName}\n{ex.ToString()}"); } Console.WriteLine($"Error updating: {newKeyName}"); - errors++; + Interlocked.Increment(ref errors); } } } @@ -311,7 +316,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { LogError($"Error accessing/writing {key.ToString()}\n{ex.ToString()}"); } - errors++; + Interlocked.Increment(ref errors); } } } From 6f38fee967ec6135c29dc5c8a48cd84bd6d09780 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:42:01 -0500 Subject: [PATCH 06/18] Improve error logging for registry key access failures Changed LogError to internal for cross-class access. Now log detailed exceptions to file and console when registry subkey access fails, enhancing error reporting and diagnostics. --- RenProfile/Program.cs | 2 +- RenProfile/RegistryUtils.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 76700b2..39f2dd3 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -174,7 +174,7 @@ private static void ExitConsole(int exitCode) Environment.Exit(exitCode); } - private static void LogError(string errorMessage) + internal static void LogError(string errorMessage) { try { diff --git a/RenProfile/RegistryUtils.cs b/RenProfile/RegistryUtils.cs index 809bc11..b093ba2 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -22,9 +22,14 @@ public static void IterateKeys(this RegistryKey root, Action action key.IterateKeys(action); } } - catch (Exception) + catch (Exception ex) { - // Swallow exception, continue + Console.WriteLine($"Failed to open subkey '{keyname}' under '{root.Name}': {ex.Message}"); + + if (Program.logErrs) + { + Program.LogError($"Failed to open subkey '{keyname}' under '{root.Name}'\n{ex}"); + } } }); From f2d68ed7ee5478c159c1c7563d62b17b1c5a41ad Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:56:42 -0500 Subject: [PATCH 07/18] Add XML docs and inline comments for clarity and maintainability Refactored the codebase to include detailed XML documentation comments and explanatory inline comments across all major files. Documented P/Invoke declarations, privilege management, registry operations, and main program logic. Improved error handling comments and clarified Windows API workarounds (e.g., privilege requirements, 8.3 path handling, URL-encoded key names). These changes enhance code readability and make the codebase more maintainable for future development. --- RenProfile/NativeMethods.cs | 31 +++++++++++++++++ RenProfile/Privileges.cs | 16 +++++++-- RenProfile/Program.cs | 67 ++++++++++++++++++++++++++----------- RenProfile/RegistryUtils.cs | 46 +++++++++++++++++++++++-- 4 files changed, 134 insertions(+), 26 deletions(-) diff --git a/RenProfile/NativeMethods.cs b/RenProfile/NativeMethods.cs index 2b60b97..26aa5b3 100644 --- a/RenProfile/NativeMethods.cs +++ b/RenProfile/NativeMethods.cs @@ -4,20 +4,30 @@ namespace RenProfileConsole { + /// + /// P/Invoke declarations for Windows API functions used in profile renaming operations. + /// internal class NativeMethods { + /// + /// Native methods and structures for privilege management. + /// internal class Privileges { + // Flag to enable a privilege in the token internal const int SE_PRIVILEGE_ENABLED = 0x00000002; internal struct TOKEN_PRIVILEGES { public int PrivilegeCount; + // Fixed-size array required for P/Invoke compatibility with Win32 TOKEN_PRIVILEGES structure + // SizeConst=1 because we typically adjust one privilege at a time [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } + // Pack=4 ensures struct layout matches Win32 API expectations (important for 32/64-bit compatibility) [StructLayout(LayoutKind.Sequential, Pack = 4)] internal struct LUID_AND_ATTRIBUTES { @@ -25,12 +35,14 @@ internal struct LUID_AND_ATTRIBUTES public UInt32 Attributes; } + // LUID (Locally Unique Identifier) - 64-bit value split into low/high parts for marshaling internal struct LUID { public UInt32 LowPart; public Int32 HighPart; } + // First overload: Use when you need to retrieve the previous privilege state [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool AdjustTokenPrivileges( @@ -41,6 +53,7 @@ internal static extern bool AdjustTokenPrivileges( ref TOKEN_PRIVILEGES PreviousState, out UInt32 ReturnLengthInBytes); + // Second overload: Use when you don't need the previous state (pass IntPtr.Zero for last two params) [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool AdjustTokenPrivileges( @@ -51,6 +64,7 @@ internal static extern bool AdjustTokenPrivileges( IntPtr PreviousState, IntPtr ReturnLengthInBytes); + // Converts a privilege name string (e.g., "SeRestorePrivilege") to its LUID representation [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool LookupPrivilegeValue( @@ -59,6 +73,9 @@ internal static extern bool LookupPrivilegeValue( [MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid); } + /// + /// Native methods for process and token management. + /// internal class Process { [Flags] @@ -81,19 +98,33 @@ internal static extern bool OpenProcessToken( internal static extern bool CloseHandle(IntPtr hObject); } + /// + /// Native methods for registry hive loading and unloading. + /// internal class Registry { + // Loads a registry hive file (e.g., NTUSER.DAT) into a temporary location under a registry key + // Requires SeRestorePrivilege and SeBackupPrivilege to succeed [DllImport("advapi32.dll")] public static extern int RegLoadKey(IntPtr hkey, string lpSubKey, string lpFile); + // Unloads a previously loaded registry hive - must have no open handles to the hive [DllImport("advapi32.dll")] public static extern int RegUnLoadKey(IntPtr hkey, string lpSubKey); + // Predefined registry root key handle for HKEY_USERS + // unchecked cast needed because 0x80000003 is larger than int.MaxValue when signed internal static readonly IntPtr HKEY_USERS = new IntPtr(unchecked((int)0x80000003)); } + /// + /// Native methods for file system operations. + /// internal class FileSystem { + // Converts long path names to 8.3 format short path names + // Returns the length of the short path, or 0 on failure + // Useful for working with paths that may contain spaces or special characters [DllImport("kernel32.dll", SetLastError = true)] public static extern int GetShortPathName( String pathName, diff --git a/RenProfile/Privileges.cs b/RenProfile/Privileges.cs index 08b4111..58fd386 100644 --- a/RenProfile/Privileges.cs +++ b/RenProfile/Privileges.cs @@ -2,15 +2,19 @@ namespace RenProfileConsole { + /// + /// Provides functionality to request and enable Windows privileges for the current process. + /// public class Privileges { /// - /// Request extra privileges for the process + /// Enables a specific privilege for the current process. /// - /// The string value specifying the desired privilege as specified in the Windows headers - /// + /// The privilege name as defined in Windows headers (e.g., "SeRestorePrivilege") + /// True if the privilege was successfully enabled; otherwise, false public static bool EnablePrivileges(string privilege) { + // First, convert the privilege name string to a LUID (Locally Unique Identifier) NativeMethods.Privileges.LUID luid = new NativeMethods.Privileges.LUID(); if (!NativeMethods.Privileges.LookupPrivilegeValue(null, privilege, ref luid)) @@ -18,6 +22,7 @@ public static bool EnablePrivileges(string privilege) return false; } + // Build the TOKEN_PRIVILEGES structure to tell Windows which privilege to enable NativeMethods.Privileges.TOKEN_PRIVILEGES tokenPrivileges = new NativeMethods.Privileges.TOKEN_PRIVILEGES(); tokenPrivileges.PrivilegeCount = 1; @@ -25,6 +30,7 @@ public static bool EnablePrivileges(string privilege) tokenPrivileges.Privileges[0].Attributes = NativeMethods.Privileges.SE_PRIVILEGE_ENABLED; tokenPrivileges.Privileges[0].Luid = luid; + // Get a handle to the current process IntPtr hProc = NativeMethods.Process.GetCurrentProcess(); if (hProc == IntPtr.Zero) @@ -32,6 +38,7 @@ public static bool EnablePrivileges(string privilege) return false; } + // Open the process token with permission to adjust privileges if (!NativeMethods.Process.OpenProcessToken( hProc, (uint)NativeMethods.Process.TokenAccess.AdjustPrivileges, @@ -42,6 +49,8 @@ public static bool EnablePrivileges(string privilege) try { + // Actually enable the privilege on the token + // Using the simpler overload since we don't need to retrieve the previous state return NativeMethods.Privileges.AdjustTokenPrivileges( hToken, false, @@ -52,6 +61,7 @@ public static bool EnablePrivileges(string privilege) } finally { + // Always close the token handle to avoid leaks, even if AdjustTokenPrivileges fails NativeMethods.Process.CloseHandle(hToken); } } diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 39f2dd3..6dc10b4 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -14,24 +14,23 @@ namespace RenProfileConsole /// internal class Program { - //- Define Variables public static bool logErrs = false; - public static int errors = 0; public static int successful = 0; public static int processed = 0; - //- Define Paths public static String oldDir = ""; - public static String newDir = ""; public static String errPath = ""; + /// + /// Entry point for the console application. Validates parameters and initiates profile renaming. + /// + /// Command line arguments: oldProfilePath newProfilePath [errorLogPath] private static void Main(string[] args) { try { - //- Parameter error checking if (args.Length == 0) { Console.WriteLine("Missing all parameters."); @@ -57,6 +56,8 @@ private static void Main(string[] args) { errPath = args[2]; + // Validate error log path: must be long enough, contain no invalid chars, + // and its parent directory must exist (file itself doesn't need to exist yet) if (!String.IsNullOrWhiteSpace(errPath) && errPath.Length > 4 && errPath.IndexOfAny(Path.GetInvalidPathChars()) == -1 @@ -78,11 +79,8 @@ private static void Main(string[] args) return; } - //- Set variables oldDir = args[0]; newDir = args[1]; - - //- Path error checking if (String.IsNullOrWhiteSpace(oldDir) || oldDir.Length < 4) { Console.WriteLine("Old user profile path is invalid."); @@ -120,29 +118,29 @@ private static void Main(string[] args) return; } - //- No errors and parameters are all as expected. - //- Let's get started! Start(); } + /// + /// Performs the profile renaming operation by loading the user registry hive, + /// updating all references in HKLM and HKU, and renaming the physical directory. + /// public static void Start() { - //- Moved meat of the code into Start mathod to making it cleaner when creating UI tool next. - + // Create a temporary unique name to mount the target user's registry hive string tempUserHiveName = $"__tmpMoveUserProfile"; - // Even running as administrator, the functionality to load and unload registry hives - // is restricted without requesting extra privilges. + // SeRestorePrivilege is required to load/unload registry hives, even when running as administrator if (Privileges.EnablePrivileges("SeRestorePrivilege")) { + // Load the user's NTUSER.DAT file into the registry temporarily int loadCode = RegistryUtils.LoadUserHive($"{oldDir}\\NTUSER.dat", tempUserHiveName); + // Keep the hive loaded while we update references using (RegistryKey userHive = Registry.Users.OpenSubKey(tempUserHiveName)) { - // Only need to deal with two trees - HKEY_LOCAL_MACHINE and HKEY_USERS. - // HKEY_CURRENT_USER won't be the user being modified (that hive is mounted under - // HKEY_USER, and other trees that may contain the path are apparently reflections of - // subtrees of one of the trees being modified. + // Update HKLM and HKU only; HKCU is for the current user (not the target profile), + // and other hives (HKCR, HKCC) are symlinks to subtrees of these two Console.WriteLine("==========Updating HKLM=========="); try { Registry.LocalMachine.IterateKeys((key) => UpdateKey(key, oldDir, newDir)); } catch { Console.WriteLine("Error occurred when updating HKLM keys. Check output."); } @@ -152,7 +150,10 @@ public static void Start() Console.WriteLine($"Processed: {processed}, Updated: {successful}, Failed: {errors}"); } + // Unload the hive before attempting to rename the directory RegistryUtils.UnloadUserHive(tempUserHiveName); + + // Rename the actual profile directory on disk try { Directory.Move(oldDir, newDir); } catch { Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); } @@ -165,6 +166,10 @@ public static void Start() } } + /// + /// Displays the exit code and waits for user input before terminating the application. + /// + /// -1 for fatal error, 0 for failure, 1 for success private static void ExitConsole(int exitCode) { Console.WriteLine(""); @@ -174,26 +179,40 @@ private static void ExitConsole(int exitCode) Environment.Exit(exitCode); } + /// + /// Writes an error message to the error log file if logging is enabled. + /// + /// The error message to log internal static void LogError(string errorMessage) { try { - //- Condition ? True : False using (FileStream file = File.Exists(errPath) ? File.Open(errPath, FileMode.Append) : File.Open(errPath, FileMode.CreateNew)) using (StreamWriter sw = new StreamWriter(file)) { sw.WriteLine($"{DateTime.Now.ToString()}: {errorMessage} \n==========="); } } - catch { /*Error writing to error log*/} + catch { /* Silently fail if unable to write to error log */ } } + /// + /// Updates all values and subkeys in a registry key that contain the old directory path, + /// replacing them with the new directory path. Handles both standard and short (8.3) path formats. + /// + /// The registry key to update + /// The old profile directory path + /// The new profile directory path private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { try { + // Get the 8.3 short path format (e.g., "C:\Users\JOHNDO~1") because some registry + // entries may store paths in this format for backwards compatibility StringBuilder sb = new StringBuilder(300); int n = NativeMethods.FileSystem.GetShortPathName(oldDir, sb, 300); string shortOldDir = sb.ToString(); + // Create a regex pattern that matches either the long or short path format + // Regex.Escape ensures special chars in paths don't break the pattern string pattern = $"({Regex.Escape(oldDir)}|{Regex.Escape(shortOldDir)})"; string[] names = key.GetValueNames(); @@ -205,11 +224,13 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) try { + // Handle REG_MULTI_SZ (array of strings) - some registry values store multiple paths if (key.GetValueKind(name) == RegistryValueKind.MultiString) { string[] oldValues = (string[])key.GetValue(name); string[] newValues = oldValues.Select(value => Regex.Replace(value, pattern, newDir, RegexOptions.IgnoreCase)).ToArray(); + // Only update if the replacement actually changed something if (!newValues.SequenceEqual(oldValues)) { try @@ -229,6 +250,8 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } } } + // Handle REG_SZ and REG_EXPAND_SZ (regular and expandable strings) + // ExpandString is used for paths with environment variables like %USERPROFILE% else if (key.GetValueKind(name) == RegistryValueKind.String || key.GetValueKind(name) == RegistryValueKind.ExpandString) { string oldValue = (string)key.GetValue(name); @@ -254,6 +277,8 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } } + // Check if the value name itself contains the old path and needs renaming + // (not just the value data, but the name of the registry entry) string newValueName = Regex.Replace(name, pattern, newDir, RegexOptions.IgnoreCase); if (newValueName != name) @@ -282,6 +307,8 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } } + // Registry subkey names can't contain backslashes, but paths are sometimes URL-encoded + // in key names, so we need to match %5C (the URL encoding for backslash) instead of \ string keypattern = pattern.Replace(@"\", @"%5C"); string keyNewDir = newDir.Replace(@"\", @"%5C"); foreach (string keyName in key.GetSubKeyNames()) diff --git a/RenProfile/RegistryUtils.cs b/RenProfile/RegistryUtils.cs index b093ba2..cdf019c 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -4,8 +4,17 @@ namespace RenProfileConsole { + /// + /// Extension methods for registry manipulation operations including iteration, renaming, and hive management. + /// public static class RegistryUtils { + /// + /// Recursively iterates through all subkeys of a registry key and executes an action on each. + /// Uses parallel processing for improved performance. + /// + /// The root registry key to iterate from + /// The action to execute on each registry key public static void IterateKeys(this RegistryKey root, Action action) { if (root == null) @@ -13,12 +22,17 @@ public static void IterateKeys(this RegistryKey root, Action action return; } + // Process subkeys in parallel for better performance on large registry trees + // Each subkey and its descendants are processed independently Parallel.ForEach(root.GetSubKeyNames(), keyname => { try { + // Open with write access (true) so the action can modify the key + // Using statement ensures the key is closed even if an exception occurs using (RegistryKey key = root.OpenSubKey(keyname, true)) { + // Recursively process all descendants first (depth-first traversal) key.IterateKeys(action); } } @@ -33,34 +47,60 @@ public static void IterateKeys(this RegistryKey root, Action action } }); + // Execute the action on this key AFTER processing all children + // This ensures we handle leaf nodes first, which is safer when renaming/deleting action(root); } + /// + /// Renames a registry value by copying it to a new name and deleting the old one. + /// + /// The registry key containing the value + /// The current name of the value + /// The new name for the value public static void RenameValue(this RegistryKey key, string oldName, string newName) { + // Windows registry API doesn't have a direct rename operation + // so we copy the value (preserving its type) then delete the original key.SetValue(newName, key.GetValue(oldName), key.GetValueKind(oldName)); key.DeleteValue(oldName); } + /// + /// Renames a registry subkey by cloning it to a new name and deleting the original. + /// + /// The parent registry key + /// The current name of the subkey + /// The new name for the subkey public static void RenameSubKey(this RegistryKey root, string oldName, string newName) { + // Clone the entire subkey tree to the new location using (RegistryKey subKey = root.OpenSubKey(oldName)) { CloneSubKey(root, subKey, newName); } + // Delete the old subkey tree after successful cloning root.DeleteSubKeyTree(oldName); } + /// + /// Recursively clones a registry subkey and all its values and nested subkeys to a new location. + /// + /// The parent key where the clone will be created + /// The source subkey to clone + /// The name for the cloned subkey public static void CloneSubKey(RegistryKey root, RegistryKey source, string newName) { using (RegistryKey target = root.CreateSubKey(newName)) { + // Copy all values from source to target, preserving their types foreach (string name in source.GetValueNames()) { target.SetValue(name, source.GetValue(name), source.GetValueKind(name)); } + // Recursively clone all subkeys foreach (string name in source.GetSubKeyNames()) { using (RegistryKey subKey = source.OpenSubKey(name)) @@ -74,8 +114,8 @@ public static void CloneSubKey(RegistryKey root, RegistryKey source, string newN /// /// Load a registry hive file under HKEY_USERS /// - /// The path to the registry hive file to load - /// The name of the subkey of HKEY_CURRENT_USER to load the hive under + /// The path to the registry hive file to load (typically NTUSER.DAT) + /// The name of the subkey of HKEY_USERS to load the hive under /// 0 on success, an error code on failure as defined by RegLoadKey in the Windows APIs public static int LoadUserHive(string hiveFilePath, string subKeyName) { @@ -85,7 +125,7 @@ public static int LoadUserHive(string hiveFilePath, string subKeyName) /// /// Unload a registry hive file under HKEY_USERS /// - /// The name of the subkey of HKEY_CURRENT_USER to unload + /// The name of the subkey of HKEY_USERS to unload /// 0 on success, an error code on failure as defined by RegUnLoadKey in the Windows APIs public static int UnloadUserHive(string subKeyName) { From fa5af3f560dd3dcbd8b122a1ebb4129242f30d1f Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:52:04 -0500 Subject: [PATCH 08/18] Standardize exit codes and update ExitConsole usage Standardized all exit codes to use 0 for success and 1 for errors, removing use of -1. Updated ExitConsole to default to 0 and revised user messages to reflect the new convention. Simplified calls to ExitConsole where appropriate. --- RenProfile/Program.cs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 6dc10b4..92411ee 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -34,7 +34,7 @@ private static void Main(string[] args) if (args.Length == 0) { Console.WriteLine("Missing all parameters."); - ExitConsole(-1); + ExitConsole(1); return; } else if (args.Length == 1) @@ -42,13 +42,13 @@ private static void Main(string[] args) if (args[0] == "?") { Console.WriteLine(@"Syntax: C:\Full\Path\To\Profile C:\Path\To\Desired\Profile C:\Optional\Path\To\Write\Errors.txt"); - ExitConsole(1); + ExitConsole(); return; } else { Console.WriteLine("Missing 1 parameter."); - ExitConsole(-1); + ExitConsole(1); return; } } @@ -68,14 +68,14 @@ private static void Main(string[] args) else { Console.WriteLine("Error log was specified but path is invalid."); - ExitConsole(-1); + ExitConsole(1); return; } } else if (args.Length > 3) { Console.WriteLine("Missing 1 parameter."); - ExitConsole(-1); + ExitConsole(1); return; } @@ -84,25 +84,25 @@ private static void Main(string[] args) if (String.IsNullOrWhiteSpace(oldDir) || oldDir.Length < 4) { Console.WriteLine("Old user profile path is invalid."); - ExitConsole(-1); + ExitConsole(1); return; } else if (String.IsNullOrWhiteSpace(newDir) || newDir.Length < 4) { Console.WriteLine("New user profile path is invalid."); - ExitConsole(-1); + ExitConsole(1); return; } else if (!Directory.Exists(oldDir)) { Console.WriteLine("Old user profile path does not exist."); - ExitConsole(-1); + ExitConsole(1); return; } else if (Directory.Exists(newDir)) { Console.WriteLine("New user profile path already exists. Use another name."); - ExitConsole(-1); + ExitConsole(1); return; } } @@ -114,7 +114,7 @@ private static void Main(string[] args) Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); } - ExitConsole(-1); + ExitConsole(1); return; } @@ -157,23 +157,23 @@ public static void Start() try { Directory.Move(oldDir, newDir); } catch { Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); } - ExitConsole(1); + ExitConsole(); } else { Console.WriteLine("Load key privilege not granted."); - ExitConsole(-1); + ExitConsole(1); } } /// /// Displays the exit code and waits for user input before terminating the application. /// - /// -1 for fatal error, 0 for failure, 1 for success - private static void ExitConsole(int exitCode) + /// Code to exit with. Default: 0 (Success) + private static void ExitConsole(int exitCode = 0) { Console.WriteLine(""); - Console.WriteLine("Exit code is " + exitCode + " (-1=fatal error, 0=fail, 1=success"); + Console.WriteLine("Exit code is " + exitCode + " (0=success)"); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); Environment.Exit(exitCode); From 395680efedc812aa839c9fb3affe7567f5efbcda Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:07:15 -0500 Subject: [PATCH 09/18] Improve README clarity; add README to Solution Items Rewrote and reformatted README.md for clearer usage instructions. Added README.md to the Visual Studio solution as a Solution Item for easier access. --- README.md | 24 ++++++++++++++++-------- RenProfile.sln | 5 +++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 76fb0f3..ad0d784 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,27 @@ RenProfile is a tool to rename the Windows profile folder path of your user acco Technically, there's no reason why this *should* cause problems, except in poorly written programs that don't properly check the user account path using relevant APIs (like `%userprofile%`) or programs that hard-code this path in settings files (which no program *should* ever do). While some developers might follow these bad practices, in my testing, this seems rare. (Windows Server environments, for example, offer built-in user profile migration.) In such cases, you might need to inform the affected program of the new path. ## Usage Info -RenProfile is a CLI app. It must be executed from a a separate Administrator account and run from an Admin terminal / CMD Prompt. It takes 2 required arguments and 1 optional argument. No other steps are necessary, RenProfile handles the `Find & Replace All` operation in the registry and renames the physical target user folder. However, after the operation navigate to `C:\Users` and verify the user's profile folder name was changed. If, for some reason, the user's folder name was not changed, then manually rename the folder, and reboot. Why this could happen: Occasionally, RenProfile may fail to rename the physical user folder due to NTFS permissions; in such circumstances, this is considered intended behavior, as we will not change NTFS permissions for you due to the implications this could cause. This is for the administrator, ie you, to figure out, if it comes to that. (You may want to use the move command to do this, *if* RenProfile experiences this issue.) -Required Arguments:
-RenProfile C:\Users\OldUserPath C:\Users\NewDesiredUserPath +RenProfile is a CLI application that must be run from a separate Administrator account in an elevated Command Prompt or PowerShell. It takes 2 required arguments and 1 optional argument. -Optional Arguments:
-3rd argument, specify log file path where you want the log file saved. Example: `C:\IT\RenProfile.log` +**Basic Command:** +``` +RenProfile C:\Users\OldUserPath C:\Users\NewDesiredUserPath [LogFilePath] +``` -
+**Arguments:** +- **Required:** Old and new profile paths (full absolute paths, not relative) +- **Optional:** Log file path (e.g., `C:\IT\RenProfile.log`) -**If RenProfile doesn't work or causes problems, follow these troubleshooting steps:** +**After Running RenProfile:** + +Navigate to `C:\Users` and verify the user profile folder was renamed. If the folder wasn't renamed, it's likely due to NTFS permissions. In such cases, this is considered intended behavior—we will not change NTFS permissions for you due to the serious implications this could cause. You'll need to: -
+1. Manually rename the folder +2. Reboot the system +3. (Optional) Use the `move` command if you prefer a command-line approach + +**If RenProfile doesn't work or causes problems, follow these troubleshooting steps:** ## Troubleshooting Steps #### If renaming the user profile folder path causes issues with some applications, or otherwise doesn't work as expected, go through these troubleshooting steps. If you need to reverse the changes, you can find steps to do this further below. diff --git a/RenProfile.sln b/RenProfile.sln index afc4b8e..fe7ce38 100644 --- a/RenProfile.sln +++ b/RenProfile.sln @@ -5,6 +5,11 @@ VisualStudioVersion = 17.9.34321.82 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RenProfile", "RenProfile\RenProfile.csproj", "{EECB2C88-5D1F-49E0-881F-CCD7E9CBDB4D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 5114db90319863fe5da0115248c77bc0e9c43ce1 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:11:05 -0500 Subject: [PATCH 10/18] Updated the README to replace a double dash with a single dash for consistency. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad0d784..5468ccb 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ RenProfile C:\Users\OldUserPath C:\Users\NewDesiredUserPath [LogFilePath] **After Running RenProfile:** -Navigate to `C:\Users` and verify the user profile folder was renamed. If the folder wasn't renamed, it's likely due to NTFS permissions. In such cases, this is considered intended behavior—we will not change NTFS permissions for you due to the serious implications this could cause. You'll need to: +Navigate to `C:\Users` and verify the user profile folder was renamed. If the folder wasn't renamed, it's likely due to NTFS permissions. In such cases, this is considered intended behavior - we will not change NTFS permissions for you due to the serious implications this could cause. You'll need to: 1. Manually rename the folder 2. Reboot the system From 5aab8495c656f10ddfd291e1091b8250c923c467 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:44:17 -0500 Subject: [PATCH 11/18] Remove redundant returns after ExitConsole() calls Refactored Main method to eliminate unnecessary return statements immediately following ExitConsole() calls, as these were unreachable due to application termination. This improves code clarity and removes dead code. --- RenProfile/Program.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 92411ee..3606b94 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -29,13 +29,13 @@ internal class Program /// Command line arguments: oldProfilePath newProfilePath [errorLogPath] private static void Main(string[] args) { + // Validate command line arguments try { if (args.Length == 0) { Console.WriteLine("Missing all parameters."); ExitConsole(1); - return; } else if (args.Length == 1) { @@ -43,13 +43,11 @@ private static void Main(string[] args) { Console.WriteLine(@"Syntax: C:\Full\Path\To\Profile C:\Path\To\Desired\Profile C:\Optional\Path\To\Write\Errors.txt"); ExitConsole(); - return; } else { Console.WriteLine("Missing 1 parameter."); ExitConsole(1); - return; } } else if (args.Length == 3) @@ -69,14 +67,12 @@ private static void Main(string[] args) { Console.WriteLine("Error log was specified but path is invalid."); ExitConsole(1); - return; } } else if (args.Length > 3) { Console.WriteLine("Missing 1 parameter."); ExitConsole(1); - return; } oldDir = args[0]; @@ -85,25 +81,21 @@ private static void Main(string[] args) { Console.WriteLine("Old user profile path is invalid."); ExitConsole(1); - return; } else if (String.IsNullOrWhiteSpace(newDir) || newDir.Length < 4) { Console.WriteLine("New user profile path is invalid."); ExitConsole(1); - return; } else if (!Directory.Exists(oldDir)) { Console.WriteLine("Old user profile path does not exist."); ExitConsole(1); - return; } else if (Directory.Exists(newDir)) { Console.WriteLine("New user profile path already exists. Use another name."); ExitConsole(1); - return; } } catch (Exception ex) @@ -115,7 +107,6 @@ private static void Main(string[] args) } ExitConsole(1); - return; } Start(); From 6825b63a44b51aa0be98a99caefe2174156c3d91 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:45:30 -0500 Subject: [PATCH 12/18] Rename Start() to PerformProfileRename() for clarity Renamed the Start() method to PerformProfileRename() and updated its invocation in the main program flow. This improves code readability by making the method's purpose explicit. --- RenProfile/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 3606b94..a0cdfaf 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -109,14 +109,14 @@ private static void Main(string[] args) ExitConsole(1); } - Start(); + PerformProfileRename(); } /// /// Performs the profile renaming operation by loading the user registry hive, /// updating all references in HKLM and HKU, and renaming the physical directory. /// - public static void Start() + public static void PerformProfileRename() { // Create a temporary unique name to mount the target user's registry hive string tempUserHiveName = $"__tmpMoveUserProfile"; From 146d7a11d2379b42a3a15a032004515b59f2cdf0 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:23:21 -0500 Subject: [PATCH 13/18] Improve profile rename checks and error reporting - Update syntax help message for clarity and accuracy. - Add pre-check to test profile directory renaming before registry changes. - Enhance error handling and logging for directory rename failures. - Make registry subkey access error messages more consistent and concise. --- RenProfile/Program.cs | 42 +++++++++++++++++++++++++++++++++++-- RenProfile/RegistryUtils.cs | 6 ++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index a0cdfaf..7226c6b 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -41,7 +41,7 @@ private static void Main(string[] args) { if (args[0] == "?") { - Console.WriteLine(@"Syntax: C:\Full\Path\To\Profile C:\Path\To\Desired\Profile C:\Optional\Path\To\Write\Errors.txt"); + Console.WriteLine(@"Syntax: RenProfile.exe C:\Full\Path\To\OldProfile C:\Full\Path\To\NewProfile C:\Optional\Path\To\Log\Errors.txt"); ExitConsole(); } else @@ -118,6 +118,35 @@ private static void Main(string[] args) /// public static void PerformProfileRename() { + // Ensure we can rename the folder before touching the registry + string tempTestDir = newDir + ".tmp_test_move"; + + try + { + if (Directory.Exists(tempTestDir)) + { + Console.WriteLine($"Temporary test directory already exists: {tempTestDir}"); + Console.WriteLine("Delete or rename that folder and try again."); + ExitConsole(1); + } + + Directory.Move(oldDir, tempTestDir); + Directory.Move(tempTestDir, oldDir); + } + catch (Exception ex) + { + Console.WriteLine("Unable to rename the profile directory. The folder may be in use or you may not have sufficient permissions."); + Console.WriteLine($"Error details: {ex.Message}"); + Console.WriteLine("Try running this tool again from Safe Mode as an administrator account that is NOT the owner of the source user profile folder."); + + if (logErrs) + { + LogError($"Test move failed when renaming '{oldDir}' to '{tempTestDir}' and back.\n{ex}"); + } + + ExitConsole(1); + } + // Create a temporary unique name to mount the target user's registry hive string tempUserHiveName = $"__tmpMoveUserProfile"; @@ -146,7 +175,16 @@ public static void PerformProfileRename() // Rename the actual profile directory on disk try { Directory.Move(oldDir, newDir); } - catch { Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); } + catch (Exception ex) + { + Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); + Console.WriteLine($"Error details: {ex.Message}"); + + if (logErrs) + { + LogError($"Final move failed when renaming '{oldDir}' to '{newDir}'.\n{ex}"); + } + } ExitConsole(); } diff --git a/RenProfile/RegistryUtils.cs b/RenProfile/RegistryUtils.cs index cdf019c..19e481b 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -38,11 +38,13 @@ public static void IterateKeys(this RegistryKey root, Action action } catch (Exception ex) { - Console.WriteLine($"Failed to open subkey '{keyname}' under '{root.Name}': {ex.Message}"); + string errMsg = $"Failed to open subkey '{keyname}' under '{root.Name}': {ex.Message}"; + + Console.WriteLine(errMsg); if (Program.logErrs) { - Program.LogError($"Failed to open subkey '{keyname}' under '{root.Name}'\n{ex}"); + Program.LogError(errMsg); } } }); From 5f38e84b6c369b973eb2609e6c56ea7e176b0d49 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:30:26 -0500 Subject: [PATCH 14/18] Refactor error logging to centralize logErrs check Move logErrs check into LogError method to eliminate redundant conditional checks before logging errors. This simplifies error handling code and ensures consistent logging behavior. --- RenProfile/Program.cs | 47 +++++++++++-------------------------- RenProfile/RegistryUtils.cs | 5 +--- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 7226c6b..d62e573 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -100,11 +100,8 @@ private static void Main(string[] args) } catch (Exception ex) { - if (logErrs) - { - LogError(ex.ToString()); - Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); - } + LogError(ex.ToString()); + Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); ExitConsole(1); } @@ -139,10 +136,7 @@ public static void PerformProfileRename() Console.WriteLine($"Error details: {ex.Message}"); Console.WriteLine("Try running this tool again from Safe Mode as an administrator account that is NOT the owner of the source user profile folder."); - if (logErrs) - { - LogError($"Test move failed when renaming '{oldDir}' to '{tempTestDir}' and back.\n{ex}"); - } + LogError($"Test move failed when renaming '{oldDir}' to '{tempTestDir}' and back.\n{ex}"); ExitConsole(1); } @@ -180,10 +174,7 @@ public static void PerformProfileRename() Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); Console.WriteLine($"Error details: {ex.Message}"); - if (logErrs) - { - LogError($"Final move failed when renaming '{oldDir}' to '{newDir}'.\n{ex}"); - } + LogError($"Final move failed when renaming '{oldDir}' to '{newDir}'.\n{ex}"); } ExitConsole(); @@ -214,6 +205,11 @@ private static void ExitConsole(int exitCode = 0) /// The error message to log internal static void LogError(string errorMessage) { + if (!logErrs) + { + return; + } + try { using (FileStream file = File.Exists(errPath) ? File.Open(errPath, FileMode.Append) : File.Open(errPath, FileMode.CreateNew)) @@ -270,10 +266,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } catch (Exception ex) { - if (logErrs) - { - LogError($"Error accessing/writing {name}\n{ex.ToString()}"); - } + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); Console.WriteLine($"Error setting: {newValues}"); Interlocked.Increment(ref errors); } @@ -296,10 +289,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } catch (Exception ex) { - if (logErrs) - { - LogError($"Error accessing/writing {name}\n{ex.ToString()}"); - } + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); Console.WriteLine($"Error setting: {newValue}"); Interlocked.Increment(ref errors); } @@ -327,10 +317,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } catch (Exception ex) { - if (logErrs) - { - LogError($"Error accessing/writing {name}\n{ex.ToString()}"); - } + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); Console.WriteLine($"Error accessing: {name}"); Interlocked.Increment(ref errors); } @@ -356,10 +343,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } catch (Exception ex) { - if (logErrs) - { - LogError($"Error accessing/writing {keyName}\n{ex.ToString()}"); - } + LogError($"Error accessing/writing {keyName}\n{ex.ToString()}"); Console.WriteLine($"Error updating: {newKeyName}"); Interlocked.Increment(ref errors); } @@ -368,10 +352,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) } catch (Exception ex) { - if (logErrs) - { - LogError($"Error accessing/writing {key.ToString()}\n{ex.ToString()}"); - } + LogError($"Error accessing/writing {key.ToString()}\n{ex.ToString()}"); Interlocked.Increment(ref errors); } } diff --git a/RenProfile/RegistryUtils.cs b/RenProfile/RegistryUtils.cs index 19e481b..a272a76 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -42,10 +42,7 @@ public static void IterateKeys(this RegistryKey root, Action action Console.WriteLine(errMsg); - if (Program.logErrs) - { - Program.LogError(errMsg); - } + Program.LogError(errMsg); } }); From f9007f09b0bbb9f816983d5af9f75780c7cf90f1 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:51:11 -0500 Subject: [PATCH 15/18] Refactor Program fields and improve code formatting Refactored Program class fields from public to private and updated naming to follow C# conventions. Reformatted try/catch and if statements for readability, improved summary output formatting, and enhanced error logging structure. Made minor formatting fixes, including Console.WriteLine usage and .csproj indentation. --- RenProfile/Program.cs | 62 +++++++++++++++++++++++++----------- RenProfile/RenProfile.csproj | 2 +- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index d62e573..a16a1ac 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -14,14 +14,14 @@ namespace RenProfileConsole /// internal class Program { - public static bool logErrs = false; - public static int errors = 0; - public static int successful = 0; - public static int processed = 0; + private static bool logErrs = false; + private static int errors = 0; + private static int successful = 0; + private static int processed = 0; - public static String oldDir = ""; - public static String newDir = ""; - public static String errPath = ""; + private static string oldDir = ""; + private static string newDir = ""; + private static string errPath = ""; /// /// Entry point for the console application. Validates parameters and initiates profile renaming. @@ -156,19 +156,34 @@ public static void PerformProfileRename() // Update HKLM and HKU only; HKCU is for the current user (not the target profile), // and other hives (HKCR, HKCC) are symlinks to subtrees of these two Console.WriteLine("==========Updating HKLM=========="); - try { Registry.LocalMachine.IterateKeys((key) => UpdateKey(key, oldDir, newDir)); } - catch { Console.WriteLine("Error occurred when updating HKLM keys. Check output."); } + try + { + Registry.LocalMachine.IterateKeys((key) => UpdateKey(key, oldDir, newDir)); + } + catch + { + Console.WriteLine("Error occurred when updating HKLM keys. Check output."); + } Console.WriteLine("==========Updating HKU=========="); - try { Registry.Users.IterateKeys((key) => UpdateKey(key, oldDir, newDir)); } - catch { Console.WriteLine("Error occurred when updating HKU keys. Check output."); } - Console.WriteLine($"Processed: {processed}, Updated: {successful}, Failed: {errors}"); + try + { + Registry.Users.IterateKeys((key) => UpdateKey(key, oldDir, newDir)); + } + catch + { + Console.WriteLine("Error occurred when updating HKU keys. Check output."); + } + Console.WriteLine($"Processed: {processed}\nUpdated : {successful}\nFailed : {errors}"); } // Unload the hive before attempting to rename the directory RegistryUtils.UnloadUserHive(tempUserHiveName); // Rename the actual profile directory on disk - try { Directory.Move(oldDir, newDir); } + try + { + Directory.Move(oldDir, newDir); + } catch (Exception ex) { Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); @@ -192,7 +207,7 @@ public static void PerformProfileRename() /// Code to exit with. Default: 0 (Success) private static void ExitConsole(int exitCode = 0) { - Console.WriteLine(""); + Console.WriteLine(); Console.WriteLine("Exit code is " + exitCode + " (0=success)"); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); @@ -214,9 +229,14 @@ internal static void LogError(string errorMessage) { using (FileStream file = File.Exists(errPath) ? File.Open(errPath, FileMode.Append) : File.Open(errPath, FileMode.CreateNew)) using (StreamWriter sw = new StreamWriter(file)) - { sw.WriteLine($"{DateTime.Now.ToString()}: {errorMessage} \n==========="); } + { + sw.WriteLine($"{DateTime.Now.ToString()}: {errorMessage} \n==========="); + } + } + catch + { + /* Silently fail if unable to write to error log */ } - catch { /* Silently fail if unable to write to error log */ } } /// @@ -244,7 +264,10 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) Console.WriteLine($"={{{key.ToString()}}}="); foreach (string name in names) { - if (String.IsNullOrWhiteSpace(name)) { continue; } + if (String.IsNullOrWhiteSpace(name)) + { + continue; + } Interlocked.Increment(ref processed); try @@ -329,7 +352,10 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) string keyNewDir = newDir.Replace(@"\", @"%5C"); foreach (string keyName in key.GetSubKeyNames()) { - if (String.IsNullOrWhiteSpace(keyName)) { continue; } + if (String.IsNullOrWhiteSpace(keyName)) + { + continue; + } Interlocked.Increment(ref processed); string newKeyName = Regex.Replace(keyName, keypattern, keyNewDir, RegexOptions.IgnoreCase); diff --git a/RenProfile/RenProfile.csproj b/RenProfile/RenProfile.csproj index b6d26ff..705d2b0 100644 --- a/RenProfile/RenProfile.csproj +++ b/RenProfile/RenProfile.csproj @@ -9,7 +9,7 @@ RenProfile RenProfile v4.7 - preview + preview 512 true true From 9755e504b50437921090bc40e1cbce074599b246 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:02:10 -0500 Subject: [PATCH 16/18] Add Safe Mode detection and user prompt at startup Added NativeMethods.User32 for Safe Mode detection via user32.dll. Program now warns and prompts user if not running in Safe Mode, exiting unless 'Y' is pressed. Improved error handling for detection failures and clarified path examples in help text. --- RenProfile/NativeMethods.cs | 31 +++++++++++++++++++++++++++++++ RenProfile/Program.cs | 28 ++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/RenProfile/NativeMethods.cs b/RenProfile/NativeMethods.cs index 26aa5b3..c8c1c72 100644 --- a/RenProfile/NativeMethods.cs +++ b/RenProfile/NativeMethods.cs @@ -131,5 +131,36 @@ public static extern int GetShortPathName( StringBuilder shortName, int cbShortName); } + + /// + /// Native methods for UI/system metrics. + /// + internal class User32 + { + private const int SM_CLEANBOOT = 67; + + [DllImport("user32.dll")] + private static extern int GetSystemMetrics(int nIndex); + + internal static BootMode GetBootMode() + { + int value = GetSystemMetrics(SM_CLEANBOOT); + switch (value) + { + case 1: return BootMode.FailSafe; + case 2: return BootMode.FailSafeWithNetwork; + default: return BootMode.Normal; + } + } + + internal static bool IsSafeMode() => GetBootMode() != BootMode.Normal; + } + + internal enum BootMode + { + Normal = 0, + FailSafe = 1, + FailSafeWithNetwork = 2 + } } } diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index a16a1ac..584d14b 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -29,6 +29,30 @@ internal class Program /// Command line arguments: oldProfilePath newProfilePath [errorLogPath] private static void Main(string[] args) { + // Check Safe Mode and prompt if not + try + { + if (!NativeMethods.User32.IsSafeMode()) + { + Console.WriteLine("Warning: It is recommended to run this tool in Windows Safe Mode as an administrator (not the owner of the source profile)."); + Console.Write("Continue? (y/N) "); + // Single keypress: 'y' or 'Y' continues; anything else treated as 'N' + ConsoleKeyInfo key = Console.ReadKey(intercept: true); + Console.WriteLine(); + + // If the key is not 'Y' (covers Enter and any other key), exit with code 1 + if (key.Key != ConsoleKey.Y) + { + ExitConsole(1); + } + } + } + catch (Exception ex) + { + // If detection fails, proceed but log + LogError($"Safe Mode detection failed. Proceeding.\n{ex}"); + } + // Validate command line arguments try { @@ -41,7 +65,7 @@ private static void Main(string[] args) { if (args[0] == "?") { - Console.WriteLine(@"Syntax: RenProfile.exe C:\Full\Path\To\OldProfile C:\Full\Path\To\NewProfile C:\Optional\Path\To\Log\Errors.txt"); + Console.WriteLine(@"Syntax: RenProfile.exe C:\\Full\\Path\\To\\OldProfile C:\\Full\\Path\\To\\NewProfile C:\\Optional\\Path\\To\\Log\\Errors.txt"); ExitConsole(); } else @@ -250,7 +274,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { try { - // Get the 8.3 short path format (e.g., "C:\Users\JOHNDO~1") because some registry + // Get the 8.3 short path format (e.g., "C:\\Users\\JOHNDO~1") because some registry // entries may store paths in this format for backwards compatibility StringBuilder sb = new StringBuilder(300); int n = NativeMethods.FileSystem.GetShortPathName(oldDir, sb, 300); From a08dcc66e65df9eebf2011b7b7ce3a7b30656f6a Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:09:09 -0500 Subject: [PATCH 17/18] Expand and clarify README with detailed usage and internals Added a comprehensive "How RenProfile Works" section detailing internal logic and workflow. Clarified and reformatted usage instructions, argument descriptions, and exit codes. Enhanced troubleshooting guidance and provided explicit reversal instructions. Added a "Technical Details" section covering requirements and performance. Fixed minor typos and improved overall clarity and formatting. --- README.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5468ccb..801bf99 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,54 @@ RenProfile is a tool to rename the Windows profile folder path of your user acco Technically, there's no reason why this *should* cause problems, except in poorly written programs that don't properly check the user account path using relevant APIs (like `%userprofile%`) or programs that hard-code this path in settings files (which no program *should* ever do). While some developers might follow these bad practices, in my testing, this seems rare. (Windows Server environments, for example, offer built-in user profile migration.) In such cases, you might need to inform the affected program of the new path. +## How RenProfile Works (Internal Logic) + +RenProfile operates through a carefully orchestrated sequence of steps to ensure safe profile renaming: + +### 1. **Prerequisites and Safety Checks** +- **Administrator Privileges Required:** Must be run as Administrator from a **different** user account than the profile being renamed +- **Safe Mode Recommended:** The tool detects if Windows is running in Safe Mode. If not, it prompts for confirmation before proceeding. Safe Mode ensures minimal processes are locking files or registry keys. +- **Path Validation:** Both old and new paths are validated to ensure they exist (old) or don't exist (new), preventing accidental overwrites +- **Test Move:** Before touching the registry, RenProfile attempts a test rename of the profile folder to verify permissions and detect locks + +### 2. **Registry Hive Loading** +- **SeRestorePrivilege:** The tool requests and enables the `SeRestorePrivilege` Windows privilege, which is required to load and unload registry hives (even for administrators) +- **NTUSER.DAT Loading:** The target user's `NTUSER.DAT` file (their user registry hive) is temporarily loaded under `HKEY_USERS` with a unique temporary name +- This allows the tool to modify the user's registry settings even when they're not logged in + +### 3. **Recursive Registry Update** +RenProfile performs a comprehensive search-and-replace operation across the entire registry: + +- **Hives Updated:** + - `HKEY_LOCAL_MACHINE` (HKLM) - system-wide settings + - `HKEY_USERS` (HKU) - all user profiles including the loaded target profile + - Note: HKCU is skipped because it belongs to the current user (the admin running the tool), not the target profile + +- **Path Format Handling:** + - **Long Path Format:** Standard paths like `C:\Users\OldName` + - **Short (8.3) Path Format:** Legacy DOS paths like `C:\Users\OLDNAM~1` + - Both formats are detected and replaced using regex pattern matching + +- **Registry Data Types:** + - `REG_SZ` (String values) + - `REG_EXPAND_SZ` (Expandable strings with environment variables) + - `REG_MULTI_SZ` (Multi-string arrays) + +- **What Gets Updated:** + - **Value Data:** The content/data stored in registry values + - **Value Names:** The names of registry entries themselves (if they contain the path) + - **Subkey Names:** Registry key names that contain paths (with special handling for URL-encoded paths using `%5C` instead of `\`) + +- **Parallel Processing:** Registry keys are processed in parallel for improved performance, with proper error handling for locked or inaccessible keys + +### 4. **Physical Directory Rename** +- After all registry updates complete successfully, the user hive is unloaded +- The physical profile folder is renamed from the old path to the new path using a standard directory move operation + +### 5. **Error Logging** +- If a log file path is provided, all errors encountered during the process are written with timestamps +- Errors don't stop the process; the tool continues and reports the count of successful vs. failed operations + ## Usage Info RenProfile is a CLI application that must be run from a separate Administrator account in an elevated Command Prompt or PowerShell. It takes 2 required arguments and 1 optional argument. @@ -20,8 +68,32 @@ RenProfile C:\Users\OldUserPath C:\Users\NewDesiredUserPath [LogFilePath] ``` **Arguments:** -- **Required:** Old and new profile paths (full absolute paths, not relative) -- **Optional:** Log file path (e.g., `C:\IT\RenProfile.log`) +- **Argument 1 (Required):** Old profile path - Full absolute path to the existing profile folder (e.g., `C:\Users\OldUserName`) +- **Argument 2 (Required):** New profile path - Full absolute path for the renamed profile folder (e.g., `C:\Users\NewUserName`) +- **Argument 3 (Optional):** Log file path - Full path to where error logs should be written (e.g., `C:\IT\RenProfile.log`) + +**Important Usage Notes:** +- **Paths must be absolute:** Relative paths like `..\Users\NewName` are not supported +- **Minimum path length:** Paths must be at least 4 characters long +- **Case-insensitive matching:** The find-and-replace operation is case-insensitive, so it will match paths regardless of capitalization +- **No quotes needed:** Unless your paths contain spaces, you don't need to wrap them in quotes (though it doesn't hurt) + +**Help Command:** +``` +RenProfile ? +``` +This displays the syntax help and exits. + +**Example with Error Logging:** +``` +RenProfile C:\Users\JohnDoe C:\Users\John.Doe C:\Logs\ProfileRename.log +``` + +### Exit Codes +- **0:** Success - All operations completed successfully +- **1:** Failure - Missing parameters, invalid paths, insufficient permissions, or critical errors occurred + +The tool will display the exit code and wait for a keypress before closing, giving you time to review the output. **After Running RenProfile:** @@ -31,24 +103,61 @@ Navigate to `C:\Users` and verify the user profile folder was renamed. If the fo 2. Reboot the system 3. (Optional) Use the `move` command if you prefer a command-line approach +**Understanding the Output:** + +During execution, RenProfile displays: +- **Current registry key being processed:** Shown as `={HKEY_PATH}=` +- **Updated values:** Shown with `Set:` or `Renamed:` prefix +- **Errors:** Displayed immediately with descriptive messages +- **Final statistics:** + - `Processed:` Total registry entries examined + - `Updated:` Successfully modified entries + - `Failed:` Entries that couldn't be updated (check log for details) + **If RenProfile doesn't work or causes problems, follow these troubleshooting steps:** ## Troubleshooting Steps #### If renaming the user profile folder path causes issues with some applications, or otherwise doesn't work as expected, go through these troubleshooting steps. If you need to reverse the changes, you can find steps to do this further below. * **Double-Check Paths:** Carefully verify the old and new profile paths you entered. Even a small typo can cause issues. The paths should be the full, absolute paths (e.g., `C:\Users\OldUsername` and `C:\Users\NewUsername`), not relative paths. * **Check for Open Files/Processes:** Before running RenProfile, close all programs running under the user account you're modifying, then reboot. Open files or running processes can lock registry keys and prevent changes. Use Task Manager to ensure no lingering processes are associated with the target user. -* **Run RenProfile Again:** It won't harm the system to reboot and run it again to ensure that all registry paths have been successfully modified. You could also use `msconfig`, select 🠆 Boot 🠆 Boot options 🠆 Safe mode 🠆 Network, which will reboot into Safe Mode with Networking, and from there you can run RenProfile again without having to worry about locking programs. -* **Check Event Viewer:** Look in the Windows Event Viewer for any errors or warnings related to user profiles, applications, or the registry around the time you ran RenProfile. This can give clues about the cause of the problem. +* **Run from Safe Mode:** Reboot into Safe Mode with Networking using `msconfig` (🠆 Boot 🠆 Boot options 🠆 Safe mode 🠆 Network). This minimizes locking programs and provides a cleaner environment for the operation. +* **Run RenProfile Again:** It won't harm the system to reboot and run it again to ensure that all registry paths have been successfully modified. Check the error count in the final statistics - if errors occurred, the log file will contain details. +* **Check Event Viewer:** Look in the Windows Event Viewer for any errors or warnings related to user profiles, applications, or the registry around the time you ran RenProfile. This can give clues about the cause of the problem. +* **Review the Log File:** If you specified a log file path, examine it for specific errors. Each error entry includes a timestamp and details about what failed. * **Hidden Files/Folders:** Some profile data might be stored in hidden folders. Check `%AppData%` (Roaming AppData), `%LocalAppData%` (Local AppData), and `%ProgramData%` for config files related to the application * **Long Paths:** Be mindful of long file paths. Windows has limitations on path lengths, so extremely long profile paths can cause issues. Might also be helpful to enable Long Path support in Windows to be sure this isn't causing you an issue. * **Reinstall the Problematic Application:** If a specific application is misbehaving after renaming the profile, check their documentation, but we at Invise Labs and Invise Solutions, have had success with backing up the program's locations, such as it's folder in `%ProgramFiles%` (should be `C:\Program Files`), `%ProgramData%` (should be `C:\ProgramData`), `%LocalAppData%` (Local AppData), and `%AppData%` (Roaming AppData), then after the backup, check if the program has a backup or export option. Then reinstall the program. We've even had success with just using the relevant program's backup and restore method and then reinstalling the program, but you should backup the app's data locations to be on the safe side. Such steps would take you less than 30 min and if the user really wants their name changed or it's in the best interest that it is changed, such as for standardization, etc. * **Open an Issue Report:** We've never had a case of RenProfile not working but if it doesn't, feel free to open a discussion or issue report with the relevant details. When doing so, you will need to provide a good detailed description of the problem, screenshot snippet of any errors, and any relevant logs. This would be worst case scenario, as we have yet to come across any situations where renaming the Windows user's profile path did not work. **Important Considerations for User Profile Paths:** -* **NTFS Permissions:** In rare cases, incorrect NTFS permissions on the profile folder can cause problems. Check the permissions to ensure the user account has the necessary access rights. If permissions are suspected to be an issue, use a Take Onwership right-click registry file to create this option for you. - * Check that your username has Full Control of the user folder. Go to `C:\Users` right-click on the user folder 🠆 Security 🠆 Edit 🠆 check `Replace all child object permission entries with inheritable permission entries from this object` and ensure that your username has full permissions, including all special permissions. Then click OK and OK. Windows should have these permissions set, but this will take Full Control of all files over again, which can ensure that any applications configuration data is owned by the user. You can also download a right-click Take Ownership registry modification and forcibly take onwership of the profile folder. -* **Special Characters:** Avoid using special characters in user profile names or paths. This can also lead to compatibility problems. +* **NTFS Permissions:** In rare cases, incorrect NTFS permissions on the profile folder can cause problems. Check the permissions to ensure the user account has the necessary access rights. If permissions are suspected to be an issue, use a Take Ownership right-click registry file to create this option for you. + * Check that your username has Full Control of the user folder. Go to `C:\Users` right-click on the user folder 🠆 Security 🠆 Edit 🠆 check `Replace all child object permission entries with inheritable permission entries from this object` and ensure that your username has full permissions, including all special permissions. Then click OK and OK. Windows should have these permissions set, but this will take Full Control of all files over again, which can ensure that any applications configuration data is owned by the user. You can also download a right-click Take Ownership registry modification and forcibly take ownership of the profile folder. +* **Special Characters:** Avoid using special characters in user profile names or paths. This can also lead to compatibility problems. **Reversing the Changes** -* **Run RenProfile Again (Reversal):** The quickest way to undo changes is to run RenProfile again, specifying the *old* profile path as the *new* path. This effectively reverses the initial operation. -* **System Restore (If Available):** If you have a recent system restore point created *before* running RenProfile, restoring to that point can revert all system changes, including registry modifications. This is a more drastic step, but it can be effective. +* **Run RenProfile Again (Reversal):** The quickest way to undo changes is to run RenProfile again, specifying the *new* profile path as argument 1 (old path) and the *original* profile path as argument 2 (new path). This effectively reverses the initial operation. + ``` + RenProfile C:\Users\NewUserName C:\Users\OldUserName + ``` +* **System Restore (If Available):** If you have a recent system restore point created *before* running RenProfile, restoring to that point can revert all system changes, including registry modifications. This is a more drastic step, but it can be effective. + +## Technical Details + +**System Requirements:** +- Windows 7 or higher (tested extensively on Windows 10 and Windows 11) +- .NET Framework 4.7 or higher +- Administrator privileges +- Access to a separate administrator account (not the profile being renamed) + +**Registry Hives Modified:** +- `HKEY_LOCAL_MACHINE` (HKLM) +- `HKEY_USERS` (HKU) +- Target user's NTUSER.DAT (loaded temporarily) + +**Windows Privileges Required:** +- `SeRestorePrivilege` - Required for loading and unloading registry hives + +**Performance:** +- Uses parallel processing for registry iteration +- Typical execution time: 2-10 minutes depending on system size and registry complexity +- Real-time progress display shows current registry key being processed From 4fb66a05bfbf27385d5c6ade3c848cd22f24dd25 Mon Sep 17 00:00:00 2001 From: techie007 <11600782+dpo007@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:26:16 -0500 Subject: [PATCH 18/18] Fix path display to use single backslashes in help and comments Corrected file path examples in help text and comments by replacing double backslashes with single backslashes, ensuring accurate and standard Windows path notation throughout the codebase. --- RenProfile/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 584d14b..60e36e2 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -65,7 +65,7 @@ private static void Main(string[] args) { if (args[0] == "?") { - Console.WriteLine(@"Syntax: RenProfile.exe C:\\Full\\Path\\To\\OldProfile C:\\Full\\Path\\To\\NewProfile C:\\Optional\\Path\\To\\Log\\Errors.txt"); + Console.WriteLine(@"Syntax: RenProfile.exe C:\Full\Path\To\OldProfile C:\Full\Path\To\NewProfile C:\Optional\Path\To\Log\Errors.txt"); ExitConsole(); } else @@ -274,7 +274,7 @@ private static void UpdateKey(RegistryKey key, string oldDir, string newDir) { try { - // Get the 8.3 short path format (e.g., "C:\\Users\\JOHNDO~1") because some registry + // Get the 8.3 short path format (e.g., "C:\Users\JOHNDO~1") because some registry // entries may store paths in this format for backwards compatibility StringBuilder sb = new StringBuilder(300); int n = NativeMethods.FileSystem.GetShortPathName(oldDir, sb, 300);