diff --git a/README.md b/README.md index 76fb0f3..801bf99 100644 --- a/README.md +++ b/README.md @@ -10,37 +10,154 @@ 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 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:** +- **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`) -**If RenProfile doesn't work or causes problems, follow these troubleshooting steps:** +**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:** + +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 + +**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 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 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..c8c1c72 100644 --- a/RenProfile/NativeMethods.cs +++ b/RenProfile/NativeMethods.cs @@ -1,25 +1,33 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; 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 { @@ -27,36 +35,47 @@ 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(IntPtr hTok, - [MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges, + internal static extern bool AdjustTokenPrivileges( + IntPtr hTok, + [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, UInt32 BufferLengthInBytes, 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(IntPtr hTok, - [MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges, + internal static extern bool AdjustTokenPrivileges( + IntPtr hTok, + [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, UInt32 BufferLengthInBytes, 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(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); } + /// + /// Native methods for process and token management. + /// internal class Process { [Flags] @@ -69,24 +88,79 @@ 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); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + 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, StringBuilder shortName, int cbShortName); + public static extern int GetShortPathName( + String pathName, + 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/Privileges.cs b/RenProfile/Privileges.cs index bcb253f..58fd386 100644 --- a/RenProfile/Privileges.cs +++ b/RenProfile/Privileges.cs @@ -1,41 +1,69 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; 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)) + + if (!NativeMethods.Privileges.LookupPrivilegeValue(null, privilege, ref luid)) + { + 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; + tokenPrivileges.Privileges = new NativeMethods.Privileges.LUID_AND_ATTRIBUTES[1]; + 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) + { + return false; + } + + // Open the process token with permission to adjust privileges + if (!NativeMethods.Process.OpenProcessToken( + hProc, + (uint)NativeMethods.Process.TokenAccess.AdjustPrivileges, + out IntPtr hToken)) + { + return false; + } + + 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, + ref tokenPrivileges, + 0, + IntPtr.Zero, + IntPtr.Zero); + } + finally { - var 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); - } - } + // Always close the token handle to avoid leaks, even if AdjustTokenPrivileges fails + NativeMethods.Process.CloseHandle(hToken); } - return false; } } } diff --git a/RenProfile/Program.cs b/RenProfile/Program.cs index 06315eb..60e36e2 100644 --- a/RenProfile/Program.cs +++ b/RenProfile/Program.cs @@ -1,14 +1,10 @@ -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; +using System.Threading; namespace RenProfileConsole { @@ -16,155 +12,315 @@ 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=""; + private static bool logErrs = false; + private static int errors = 0; + private static int successful = 0; + private static int processed = 0; + private static string oldDir = ""; + private static string newDir = ""; + private 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) { + // 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 { - //- 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); + } 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: RenProfile.exe C:\Full\Path\To\OldProfile C:\Full\Path\To\NewProfile C:\Optional\Path\To\Log\Errors.txt"); + ExitConsole(); + } + else + { + Console.WriteLine("Missing 1 parameter."); + ExitConsole(1); + } } 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]; + + // 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 + && Directory.Exists(Path.GetDirectoryName(errPath))) + { + logErrs = true; + } + else + { + Console.WriteLine("Error log was specified but path is invalid."); + ExitConsole(1); + } } else if (args.Length > 3) - { Console.WriteLine("Missing 1 parameter."); ExitConsole(-1); } + { + Console.WriteLine("Missing 1 parameter."); + ExitConsole(1); + } - //- 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); + } + else if (String.IsNullOrWhiteSpace(newDir) || newDir.Length < 4) + { + Console.WriteLine("New user profile path is invalid."); + ExitConsole(1); + } + else if (!Directory.Exists(oldDir)) + { + Console.WriteLine("Old user profile path does not exist."); + ExitConsole(1); + } + else if (Directory.Exists(newDir)) + { + Console.WriteLine("New user profile path already exists. Use another name."); + ExitConsole(1); + } } catch (Exception ex) - { - if (logErrs) { LogError(ex.ToString()); Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); } ExitConsole(-1); return; + { + LogError(ex.ToString()); + Console.WriteLine("Unexpected error occurred, cannot continue. Check error log."); + + ExitConsole(1); } - //- No errors and parameters are all as expected. - //- Let's get started! - Start(); + PerformProfileRename(); } - public static void 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 PerformProfileRename() { - //- Moved meat of the code into Start mathod to making it cleaner when creating UI tool next. + // 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."); + + 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"; - // 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."); } + 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); - try { Directory.Move(oldDir, newDir); } - catch { Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); } - ExitConsole(1); + // Rename the actual profile directory on disk + try + { + Directory.Move(oldDir, newDir); + } + catch (Exception ex) + { + Console.WriteLine("Error renaming physical profile directories, another process might have a lock still."); + Console.WriteLine($"Error details: {ex.Message}"); + + LogError($"Final move failed when renaming '{oldDir}' to '{newDir}'.\n{ex}"); + } + + ExitConsole(); } else { Console.WriteLine("Load key privilege not granted."); - ExitConsole(-1); + ExitConsole(1); } } - static void ExitConsole(int exitCode) + /// + /// Displays the exit code and waits for user input before terminating the application. + /// + /// 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(); + Console.WriteLine("Exit code is " + exitCode + " (0=success)"); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); Environment.Exit(exitCode); } - static void LogError(string errorMessage) + /// + /// Writes an error message to the error log file if logging is enabled. + /// + /// The error message to log + internal static void LogError(string errorMessage) { + if (!logErrs) + { + return; + } + 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)) - { sw.WriteLine($"{DateTime.Now.ToString()}: {errorMessage} \n==========="); } + 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 + { + /* Silently fail if unable to write to error log */ } - catch { /*Error writing to error log*/} } - - static void UpdateKey(RegistryKey key, string oldDir, string newDir) + /// + /// 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(); Console.WriteLine($"={{{key.ToString()}}}="); foreach (string name in names) { - if (String.IsNullOrWhiteSpace(name)) { continue; } - processed++; + if (String.IsNullOrWhiteSpace(name)) + { + continue; + } + Interlocked.Increment(ref processed); 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 { key.SetValue(name, newValues); Console.WriteLine(String.Join(":", newValues)); - successful++; + Interlocked.Increment(ref successful); + } + catch (Exception ex) + { + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); + Console.WriteLine($"Error setting: {newValues}"); + Interlocked.Increment(ref errors); } - catch (Exception ex) { if (logErrs) { LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error setting: {newValues}"); errors++; } } } + // 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); @@ -176,12 +332,19 @@ 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) + { + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); + Console.WriteLine($"Error setting: {newValue}"); + Interlocked.Increment(ref errors); } - catch (Exception ex) { if (logErrs) { LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error setting: {newValue}"); errors++; } } } + // 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) @@ -190,20 +353,34 @@ 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) { if (logErrs) { LogError($"Error accessing/writing {name}\n{ex.ToString()}"); } Console.WriteLine($"Error accessing: {name}"); errors++; } + catch (Exception ex) + { + LogError($"Error accessing/writing {name}\n{ex.ToString()}"); + Console.WriteLine($"Error accessing: {name}"); + Interlocked.Increment(ref errors); + } } + // 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()) { - if (String.IsNullOrWhiteSpace(keyName)) { continue; } - processed++; + if (String.IsNullOrWhiteSpace(keyName)) + { + continue; + } + Interlocked.Increment(ref processed); string newKeyName = Regex.Replace(keyName, keypattern, keyNewDir, RegexOptions.IgnoreCase); if (keyName != newKeyName) @@ -212,13 +389,22 @@ 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) + { + LogError($"Error accessing/writing {keyName}\n{ex.ToString()}"); + Console.WriteLine($"Error updating: {newKeyName}"); + Interlocked.Increment(ref 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) + { + LogError($"Error accessing/writing {key.ToString()}\n{ex.ToString()}"); + Interlocked.Increment(ref errors); + } } } } 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..a272a76 100644 --- a/RenProfile/RegistryUtils.cs +++ b/RenProfile/RegistryUtils.cs @@ -1,15 +1,20 @@ 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 { + /// + /// 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) @@ -17,49 +22,84 @@ 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); } } - catch (Exception e) + catch (Exception ex) { + string errMsg = $"Failed to open subkey '{keyname}' under '{root.Name}': {ex.Message}"; + + Console.WriteLine(errMsg); + + Program.LogError(errMsg); } }); + // 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)) @@ -73,8 +113,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) { @@ -84,7 +124,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) { 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