diff --git a/Scripts/Features/ImportRegistryFile.ps1 b/Scripts/Features/ImportRegistryFile.ps1 index d0ae8f1..24925ce 100644 --- a/Scripts/Features/ImportRegistryFile.ps1 +++ b/Scripts/Features/ImportRegistryFile.ps1 @@ -1,156 +1,3 @@ -function Convert-RegOperationKeyToProviderPath { - param( - [Parameter(Mandatory)] - [string]$RegistryPath - ) - - $parts = Split-RegistryPath -path $RegistryPath - if (-not $parts) { - throw "Unsupported registry path: $RegistryPath" - } - - $driveRoot = switch ($parts.Hive.ToUpperInvariant()) { - 'HKEY_LOCAL_MACHINE' { 'HKLM:' } - 'HKEY_CURRENT_USER' { 'HKCU:' } - 'HKEY_CLASSES_ROOT' { 'HKCR:' } - 'HKEY_USERS' { 'HKU:' } - 'HKEY_CURRENT_CONFIG' { 'HKCC:' } - default { throw "Unsupported registry hive '$($parts.Hive)' in path '$RegistryPath'" } - } - - if ([string]::IsNullOrWhiteSpace($parts.SubKey)) { - return $driveRoot - } - - return "$driveRoot\$($parts.SubKey)" -} - -function Convert-RegOperationToSetItemPropertyArguments { - param( - [Parameter(Mandatory)] - $Operation - ) - - $valueName = if ([string]::IsNullOrEmpty([string]$Operation.ValueName)) { '(default)' } else { [string]$Operation.ValueName } - $valueType = [string]$Operation.ValueType - - switch ($valueType) { - 'DWord' { - $unsigned = [uint32]$Operation.ValueData - $value = [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0) - return @{ Name = $valueName; Type = 'DWord'; Value = $value } - } - 'QWord' { - $unsigned = [uint64]$Operation.ValueData - $value = [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0) - return @{ Name = $valueName; Type = 'QWord'; Value = $value } - } - 'String' { - return @{ Name = $valueName; Type = 'String'; Value = [string]$Operation.ValueData } - } - 'Hex2' { - return @{ Name = $valueName; Type = 'ExpandString'; Value = [string]$Operation.ValueData } - } - 'Hex1' { - $stringValue = ([System.Text.Encoding]::Unicode.GetString([byte[]]$Operation.ValueData)).TrimEnd([char]0) - return @{ Name = $valueName; Type = 'String'; Value = $stringValue } - } - 'Hex7' { - return @{ Name = $valueName; Type = 'MultiString'; Value = @($Operation.ValueData) } - } - 'Binary' { - return @{ Name = $valueName; Type = 'Binary'; Value = [byte[]]$Operation.ValueData } - } - default { - if ($valueType -like 'Hex*') { - throw "Unsupported hex value type '$valueType' while applying reg operation for '$($Operation.KeyPath)'." - } - - throw "Unsupported value type '$valueType' while applying reg operation for '$($Operation.KeyPath)'" - } - } -} - -function Invoke-RegistryOperationsFromRegFile { - param( - [Parameter(Mandatory)] - [string]$RegFilePath - ) - - foreach ($operation in @(Get-RegFileOperations -regFilePath $RegFilePath)) { - $providerPath = Convert-RegOperationKeyToProviderPath -RegistryPath $operation.KeyPath - - switch ($operation.OperationType) { - 'DeleteKey' { - if (Test-Path -LiteralPath $providerPath) { - Remove-Item -LiteralPath $providerPath -Recurse -Force -ErrorAction Stop - } - } - 'DeleteValue' { - if (Test-Path -LiteralPath $providerPath) { - $valueName = if ([string]::IsNullOrEmpty([string]$operation.ValueName)) { '(default)' } else { [string]$operation.ValueName } - Remove-ItemProperty -Path $providerPath -Name $valueName -ErrorAction SilentlyContinue - } - } - 'SetValue' { - if (-not (Test-Path -LiteralPath $providerPath)) { - New-Item -Path $providerPath -Force -ErrorAction Stop | Out-Null - } - $setArgs = Convert-RegOperationToSetItemPropertyArguments -Operation $operation - Set-ItemProperty -Path $providerPath -Name $setArgs.Name -Value $setArgs.Value -Type $setArgs.Type -Force -ErrorAction Stop - } - default { - throw "Unsupported reg operation type '$($operation.OperationType)' in '$RegFilePath'" - } - } - } -} - -function Invoke-RegistryImportViaPowerShell { - param( - [Parameter(Mandatory)] - [string]$RegFilePath, - [switch]$UseOfflineHive, - [string]$OfflineHiveDatPath - ) - - $applyScript = { - param($targetRegFilePath) - Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath - } - - if ($UseOfflineHive) { - if (Get-Command -Name Invoke-WithLoadedBackupHive -ErrorAction SilentlyContinue) { - return Invoke-WithLoadedBackupHive -ScriptBlock $applyScript -ArgumentObject $RegFilePath - } - - if ([string]::IsNullOrWhiteSpace($OfflineHiveDatPath)) { - throw "Offline hive path was not provided for fallback import of '$RegFilePath'" - } - - $global:LASTEXITCODE = 0 - reg load "HKU\Default" $OfflineHiveDatPath | Out-Null - $loadExitCode = $LASTEXITCODE - if ($loadExitCode -ne 0) { - throw "Failed to load user hive at '$OfflineHiveDatPath' for fallback import (exit code: $loadExitCode)" - } - - try { - return & $applyScript $RegFilePath - } - finally { - $global:LASTEXITCODE = 0 - reg unload "HKU\Default" | Out-Null - $unloadExitCode = $LASTEXITCODE - if ($unloadExitCode -ne 0) { - Write-Warning "Fallback import completed, but unloading HKU\Default failed (exit code: $unloadExitCode)." - } - } - } - - return & $applyScript $RegFilePath -} - # Import & execute regfile function ImportRegistryFile { param ( diff --git a/Scripts/Helpers/ApplyRegistryRegFile.ps1 b/Scripts/Helpers/ApplyRegistryRegFile.ps1 new file mode 100644 index 0000000..a5ef7a3 --- /dev/null +++ b/Scripts/Helpers/ApplyRegistryRegFile.ps1 @@ -0,0 +1,198 @@ +function Convert-RegOperationToValueKind { + param( + [Parameter(Mandatory)] + $Operation + ) + + $valueName = if ([string]::IsNullOrEmpty([string]$Operation.ValueName)) { '' } else { [string]$Operation.ValueName } + $valueType = [string]$Operation.ValueType + + switch ($valueType) { + 'DWord' { + $unsigned = [uint32]$Operation.ValueData + $value = [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0) + return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::DWord; Value = $value } + } + 'QWord' { + $unsigned = [uint64]$Operation.ValueData + $value = [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0) + return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::QWord; Value = $value } + } + 'String' { + return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::String; Value = [string]$Operation.ValueData } + } + 'Hex2' { + return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::ExpandString; Value = [string]$Operation.ValueData } + } + 'Hex1' { + $stringValue = ([System.Text.Encoding]::Unicode.GetString([byte[]]$Operation.ValueData)).TrimEnd([char]0) + return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::String; Value = $stringValue } + } + 'Hex7' { + return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::MultiString; Value = [string[]]@($Operation.ValueData | ForEach-Object { [string]$_ }) } + } + 'Binary' { + return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::Binary; Value = [byte[]]$Operation.ValueData } + } + default { + if ($valueType -like 'Hex*') { + throw "Unsupported hex value type '$valueType' while applying reg operation for '$($Operation.KeyPath)'." + } + + throw "Unsupported value type '$valueType' while applying reg operation for '$($Operation.KeyPath)'" + } + } +} + +function Remove-RegistrySubKeyTreeIfExists { + param( + [Parameter(Mandatory)] + [Microsoft.Win32.RegistryKey]$RootKey, + [Parameter(Mandatory)] + [string]$SubKeyPath + ) + + $existingKey = $RootKey.OpenSubKey($SubKeyPath, $true) + if ($null -eq $existingKey) { + return + } + + $existingKey.Close() + + try { + $RootKey.DeleteSubKeyTree($SubKeyPath, $false) + } + catch { + # Best-effort cleanup only; missing keys are fine. + } +} + +function Get-RegistryKeyForOperation { + param( + [Parameter(Mandatory)] + [string]$RegistryPath, + [switch]$CreateIfMissing + ) + + $parts = Split-RegistryPath -path $RegistryPath + if (-not $parts) { + throw "Unsupported registry path: $RegistryPath" + } + + $rootKey = Get-RegistryRootKey -hiveName $parts.Hive + if (-not $rootKey) { + throw "Unsupported registry hive '$($parts.Hive)' in path '$RegistryPath'" + } + + $subKeyPath = $parts.SubKey + if ([string]::IsNullOrWhiteSpace($subKeyPath)) { + return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $null; Key = $rootKey } + } + + $key = if ($CreateIfMissing) { + $rootKey.CreateSubKey($subKeyPath) + } + else { + $rootKey.OpenSubKey($subKeyPath, $true) + } + + return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $key } +} + +function Invoke-RegistryOperationsFromRegFile { + param( + [Parameter(Mandatory)] + [string]$RegFilePath + ) + + foreach ($operation in @(Get-RegFileOperations -regFilePath $RegFilePath)) { + $keyInfo = Get-RegistryKeyForOperation -RegistryPath $operation.KeyPath -CreateIfMissing:($operation.OperationType -eq 'SetValue') + + switch ($operation.OperationType) { + 'DeleteKey' { + if ($null -ne $keyInfo.Key) { + try { + if ($null -ne $keyInfo.SubKeyPath) { + Remove-RegistrySubKeyTreeIfExists -RootKey $keyInfo.RootKey -SubKeyPath $keyInfo.SubKeyPath + } + } + finally { + $keyInfo.Key.Close() + } + } + } + 'DeleteValue' { + if ($null -ne $keyInfo.Key) { + try { + $valueName = if ([string]::IsNullOrEmpty([string]$operation.ValueName)) { '' } else { [string]$operation.ValueName } + $keyInfo.Key.DeleteValue($valueName, $false) + } + finally { + $keyInfo.Key.Close() + } + } + } + 'SetValue' { + if ($null -eq $keyInfo.Key) { + throw "Unable to open or create registry key '$($operation.KeyPath)'" + } + + try { + $setArgs = Convert-RegOperationToValueKind -Operation $operation + $keyInfo.Key.SetValue($setArgs.Name, $setArgs.Value, $setArgs.Kind) + } + finally { + $keyInfo.Key.Close() + } + } + default { + throw "Unsupported reg operation type '$($operation.OperationType)' in '$RegFilePath'" + } + } + } +} + +function Invoke-RegistryImportViaPowerShell { + param( + [Parameter(Mandatory)] + [string]$RegFilePath, + [switch]$UseOfflineHive, + [string]$OfflineHiveDatPath + ) + + $applyScript = { + param($targetRegFilePath) + Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath + } + + if ($UseOfflineHive) { + if (Get-Command -Name Invoke-WithLoadedBackupHive -ErrorAction SilentlyContinue) { + return Invoke-WithLoadedBackupHive -ScriptBlock $applyScript -ArgumentObject $RegFilePath + } + + if ([string]::IsNullOrWhiteSpace($OfflineHiveDatPath)) { + throw "Offline hive path was not provided for fallback import of '$RegFilePath'" + } + + $global:LASTEXITCODE = 0 + reg load "HKU\Default" $OfflineHiveDatPath | Out-Null + $loadExitCode = $LASTEXITCODE + if ($loadExitCode -ne 0) { + throw "Failed to load user hive at '$OfflineHiveDatPath' for fallback import (exit code: $loadExitCode)" + } + + try { + return & $applyScript $RegFilePath + } + finally { + $global:LASTEXITCODE = 0 + reg unload "HKU\Default" | Out-Null + $unloadExitCode = $LASTEXITCODE + if ($unloadExitCode -ne 0) { + Write-Warning "Fallback import completed, but unloading HKU\Default failed (exit code: $unloadExitCode)." + } + } + } + + return & $applyScript $RegFilePath +} \ No newline at end of file diff --git a/Scripts/Helpers/Get-RegFileOperations.ps1 b/Scripts/Helpers/Get-RegFileOperations.ps1 index e3dea03..c78cdad 100644 --- a/Scripts/Helpers/Get-RegFileOperations.ps1 +++ b/Scripts/Helpers/Get-RegFileOperations.ps1 @@ -170,5 +170,23 @@ function Convert-RegistryByteArrayToMultiString { [byte[]]$byteData ) - return @(([System.Text.Encoding]::Unicode.GetString($byteData)).TrimEnd([char]0) -split "`0" | Where-Object { $_ -ne '' }) + $decoded = [System.Text.Encoding]::Unicode.GetString($byteData) + $values = New-Object 'System.Collections.Generic.List[string]' + $current = New-Object System.Text.StringBuilder + + foreach ($character in $decoded.ToCharArray()) { + if ($character -eq [char]0) { + $values.Add($current.ToString()) + $null = $current.Clear() + continue + } + + $null = $current.Append($character) + } + + if ($values.Count -gt 0 -and $values[$values.Count - 1] -eq '') { + $values.RemoveAt($values.Count - 1) + } + + return @($values.ToArray()) } diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index c532767..44f421e 100644 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -349,6 +349,7 @@ if (-not $script:WingetInstalled -and -not $Silent) { . "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1" . "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1" +. "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1" . "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1" # Threading functions @@ -401,7 +402,7 @@ else { } if ($script:Params.ContainsKey("Sysprep")) { - $defaultUserPath = GetUserDirectory -userName "Default" + GetUserDirectory -userName "Default" | Out-Null # Exit script if run in Sysprep mode on Windows 10 if ($WinVersion -lt 22000) { @@ -412,10 +413,10 @@ if ($script:Params.ContainsKey("Sysprep")) { # Ensure that target user exists, if User or AppRemovalTarget parameter was provided if ($script:Params.ContainsKey("User")) { - $userPath = GetUserDirectory -userName $script:Params.Item("User") + GetUserDirectory -userName $script:Params.Item("User") | Out-Null } if ($script:Params.ContainsKey("AppRemovalTarget")) { - $userPath = GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") + GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") | Out-Null } # Remove LastUsedSettings.json file if it exists and is empty