Add comprehensive test suite, fix minor issues, rename function and file names to match approved verbs (#708)

This commit is contained in:
Jeffrey
2026-07-19 22:06:07 +02:00
committed by GitHub
parent a7292e4f35
commit 9c033dbf98
116 changed files with 4629 additions and 650 deletions
@@ -195,10 +195,18 @@ function Get-RegistryKeySnapshot {
}
}
<#
.SYNOPSIS
Converts an open registry key into a backup snapshot.
.DESCRIPTION
Captures all values or selected value names, records missing selected values,
and recursively captures subkeys when requested.
#>
function Convert-RegistryKeyToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
[string]$FullPath,
[bool]$CaptureAllValues = $false,
@@ -253,20 +261,34 @@ function Convert-RegistryKeyToSnapshot {
}
}
<#
.SYNOPSIS
Converts a registry value into a serializable backup snapshot.
.DESCRIPTION
Preserves the value kind and normalizes supported data types for JSON
serialization without expanding environment-string values. REG_NONE values
are rejected.
#>
function Convert-RegistryValueToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
[AllowEmptyString()]
[string]$ValueName
)
$valueKind = $RegistryKey.GetValueKind($ValueName)
if ($valueKind -eq [Microsoft.Win32.RegistryValueKind]::None) {
throw "REG_NONE registry values are not supported for backup. Key='$($RegistryKey.Name)' Name='$ValueName'"
}
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
try {
$normalizedValue = switch ($valueKind) {
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
# Prevent an empty byte sequence from being unrolled to $null by the switch pipeline.
([Microsoft.Win32.RegistryValueKind]::Binary) { if ($null -eq $value) { ,@() } else { ,@($value | ForEach-Object { [int]$_ }) } }
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
([Microsoft.Win32.RegistryValueKind]::DWord) { [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$value), 0) }
([Microsoft.Win32.RegistryValueKind]::QWord) { [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$value), 0) }
@@ -287,12 +309,20 @@ function Convert-RegistryValueToSnapshot {
}
}
<#
.SYNOPSIS
Describes the user profile targeted by a registry backup.
.DESCRIPTION
Returns DefaultUserProfile for Sysprep, User:<name> for an explicit user,
or CurrentUser:<name> otherwise.
#>
function Get-RegistryBackupTargetDescription {
if ($script:Params.ContainsKey('Sysprep')) {
return 'DefaultUserProfile'
}
$resolvedUserName = [string](GetUserName)
$resolvedUserName = [string](Get-UserName)
if ($script:Params.ContainsKey('User')) {
return "User:$resolvedUserName"
@@ -37,7 +37,7 @@ function New-RegistrySettingsBackup {
$backupFilePath = Join-Path $backupDirectory $backupFileName
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -UndoFeatures $undoFeatures -CreatedAt $timestamp
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
if (-not (Save-ToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
throw "Failed to save registry backup to '$backupFilePath'"
}
@@ -1,4 +1,4 @@
function CreateSystemRestorePoint {
function Invoke-SystemRestorePoint {
$SysRestore = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name "RPSessionInterval"
$failed = $false
@@ -66,7 +66,7 @@ function Test-FeatureApplied {
return (Test-StoreSearchSuggestionsDisabledForAllUsers)
}
$storeDbPath = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
$storeDbPath = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
return (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDbPath)
}
@@ -1,5 +1,5 @@
# Import & execute regfile
function ImportRegistryFile {
function Import-RegistryFile {
param (
$message,
$path
@@ -4,7 +4,7 @@
.DESCRIPTION
Handles two categories of features:
- Registry-backed: imports the .reg file via ImportRegistryFile, then runs
- Registry-backed: imports the .reg file via Import-RegistryFile, then runs
any post-import side effects (e.g., removing companion app packages).
- Custom logic: app removal, Windows optional features, start menu
replacement, and other special-case features.
@@ -21,17 +21,17 @@ function Invoke-FeatureApply {
# ---- Registry-backed features: import .reg file, then handle side effects ----
if ($feature.RegistryKey) {
ImportRegistryFile "> $applyText..." $feature.RegistryKey
Import-RegistryFile "> $applyText..." $feature.RegistryKey
# Post-import side effects for specific features
switch ($FeatureId) {
'DisableBing' {
# Also remove the app package for Bing search
RemoveApps @('Microsoft.BingSearch')
Remove-SelectedApps @('Microsoft.BingSearch')
}
'DisableCopilot' {
# Also remove the app packages for Copilot
RemoveApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
}
'DisableTelemetry' {
# Also disable telemetry scheduled tasks
@@ -44,8 +44,8 @@ function Invoke-FeatureApply {
# ---- Custom features (no registry backing, or special handling required) ----
switch ($FeatureId) {
'RemoveApps' {
Write-Host "> $applyText for $(GetFriendlyTargetUserName)..."
$appsList = GenerateAppsList
Write-Host "> $applyText for $(Get-FriendlyTargetUserName)..."
$appsList = Generate-AppsList
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
@@ -54,19 +54,19 @@ function Invoke-FeatureApply {
}
Write-Host "$($appsList.Count) apps selected for removal"
RemoveApps $appsList
Remove-SelectedApps $appsList
return
}
'RemoveGamingApps' {
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
Write-Host "> $applyText..."
RemoveApps $appsList
Remove-SelectedApps $appsList
return
}
'RemoveHPApps' {
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
Write-Host "> $applyText..."
RemoveApps $appsList
Remove-SelectedApps $appsList
return
}
'DisableWidgets' {
@@ -76,46 +76,46 @@ function Invoke-FeatureApply {
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process
}
RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
return
}
'EnableWindowsSandbox' {
Write-Host "> $applyText..."
EnableWindowsFeature "Containers-DisposableClientVM"
Enable-WindowsFeature "Containers-DisposableClientVM"
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $applyText..."
EnableWindowsFeature "VirtualMachinePlatform"
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
Enable-WindowsFeature "VirtualMachinePlatform"
Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux"
Write-Host ""
return
}
'ClearStart' {
Write-Host "> $applyText for user $(GetUserName)..."
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
ReplaceStartMenu -startMenuBinFile $startMenuBinFile
Replace-StartMenu -startMenuBinFile $startMenuBinFile
}
Write-Host ""
return
}
'ReplaceStart' {
Write-Host "> $applyText for user $(GetUserName)..."
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
ReplaceStartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart")
Replace-StartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart")
}
Write-Host ""
return
}
'ClearStartAllUsers' {
ReplaceStartMenuForAllUsers
Replace-StartMenuForAllUsers
return
}
'ReplaceStartAllUsers' {
ReplaceStartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers")
Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers")
return
}
'DisableStoreSearchSuggestions' {
@@ -126,10 +126,10 @@ function Invoke-FeatureApply {
return
}
Write-Host "> Disabling Microsoft Store search suggestions for user $(GetUserName)..."
$storeDb = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
DisableStoreSearchSuggestions -StoreAppsDatabase $storeDb
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb
}
Write-Host ""
return
@@ -145,7 +145,7 @@ function Invoke-FeatureApply {
.DESCRIPTION
Handles undo for features that require custom logic rather than a simple
.reg file import. Features with a RegistryUndoKey are handled directly
via ImportRegistryFile in Invoke-UndoFeatures.
via Import-RegistryFile in Invoke-UndoFeatures.
#>
function Invoke-FeatureUndo {
param(
@@ -164,24 +164,24 @@ function Invoke-FeatureUndo {
return
}
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..."
$storeDb = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
EnableStoreSearchSuggestions -StoreAppsDatabase $storeDb
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb
}
Write-Host ""
return
}
'EnableWindowsSandbox' {
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Containers-DisposableClientVM'
Disable-WindowsFeature 'Containers-DisposableClientVM'
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
DisableWindowsFeature 'VirtualMachinePlatform'
Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux'
Disable-WindowsFeature 'VirtualMachinePlatform'
Write-Host ""
return
}
@@ -287,7 +287,7 @@ function Invoke-UndoFeatures {
}
if ($f -and $f.RegistryUndoKey) {
ImportRegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
}
Invoke-FeatureUndo -FeatureId $featureId
@@ -311,8 +311,10 @@ function Invoke-UndoFeatures {
(used by the GUI modal). Cancellation is checked between each step.
#>
function Invoke-AllChanges {
if ($script:CancelRequested) { return }
# Guard: prevent running as SYSTEM account without explicit target user
$isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18')
$isSystem = Test-RunningAsSystem
if ($isSystem -and -not $script:Params.ContainsKey("User") -and -not $script:Params.ContainsKey("Sysprep")) {
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
}
@@ -355,6 +357,7 @@ function Invoke-AllChanges {
# Phase 1: Registry backup
# ================================================================
if ($needsBackup) {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating registry backup..."
@@ -384,6 +387,7 @@ function Invoke-AllChanges {
# Phase 2: System restore point
# ================================================================
if ($script:Params.ContainsKey("CreateRestorePoint")) {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating system restore point, this may take a moment..."
@@ -394,7 +398,7 @@ function Invoke-AllChanges {
}
else {
Write-Host "> Creating a system restore point..."
CreateSystemRestorePoint
Invoke-SystemRestorePoint
Write-Host ""
}
}
@@ -407,6 +411,8 @@ function Invoke-AllChanges {
$step += $applyIds.Count
}
if ($script:CancelRequested) { return }
# ================================================================
# Phase 4: Undo features
# ================================================================
@@ -423,3 +429,19 @@ function Invoke-AllChanges {
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
}
}
<#
.SYNOPSIS
Tests whether Win11Debloat is running under the SYSTEM account.
.DESCRIPTION
Compares the current Windows identity's security identifier (SID) with
the well-known Local System SID (S-1-5-18).
.OUTPUTS
System.Boolean
Returns $true when the current process runs as SYSTEM; otherwise, $false.
#>
function Test-RunningAsSystem {
return ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18')
}
@@ -5,7 +5,7 @@
.DESCRIPTION
Restarts the Explorer process to ensure all UI modifications take effect. Shows a warning if any of the applied features require a reboot to take full effect.
#>
function RestartExplorer {
function Invoke-RestartExplorer {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restart the Windows Explorer process" -ForegroundColor Cyan
return
@@ -32,4 +32,4 @@ function RestartExplorer {
else {
Write-Host "Unable to restart Windows Explorer process, please manually reboot your PC to apply all changes" -ForegroundColor Yellow
}
}
}
@@ -248,6 +248,14 @@ function New-RegistryBackupAllowListPlanMap {
return $planMap
}
<#
.SYNOPSIS
Converts registry value names into a case-insensitive set.
.DESCRIPTION
Preserves empty names and prevents PowerShell from enumerating the returned
HashSet.
#>
function ConvertTo-RegistryValueNameSet {
param(
[AllowEmptyCollection()]
@@ -259,9 +267,18 @@ function ConvertTo-RegistryValueNameSet {
$null = $valueNameSet.Add([string]$valueName)
}
return $valueNameSet
# Prevent PowerShell from enumerating the HashSet into an array or single string
return ,$valueNameSet
}
<#
.SYNOPSIS
Validates a registry snapshot against the selected-feature allow list.
.DESCRIPTION
Recursively validates snapshot paths, value names, value kinds, and value
data, appending validation errors to the supplied list.
#>
function Test-RegistrySnapshotAgainstAllowList {
param(
[Parameter(Mandatory)]
@@ -301,7 +318,7 @@ function Test-RegistrySnapshotAgainstAllowList {
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
}
elseif (-not (Test-RegistryValueDataMatchesKind -KindName $kindName -Data $valueSnapshot.Data)) {
$Errors.Add("Backup value '$valueReference' has Data that does not fit its Kind '$kindName'.")
$Errors.Add("Backup contains invalid registry data for kind '$kindName' at '$valueReference'.")
}
}
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
@@ -314,6 +331,64 @@ function Test-RegistrySnapshotAgainstAllowList {
}
}
<#
.SYNOPSIS
Tests whether backed-up registry data is valid for its declared value kind.
.DESCRIPTION
Rejects corrupted or hand-edited backup data that cannot be restored safely,
such as a DWord that overflows UInt32 or binary data containing an invalid byte.
This validation runs before Restore-RegistryKeySnapshot mutates the live
registry, preventing a failed conversion from leaving a partially restored key.
.PARAMETER KindName
The declared registry value kind name, such as DWord, QWord, or Binary.
.PARAMETER Data
The backed-up value data to validate against the declared kind.
.OUTPUTS
System.Boolean
#>
function Test-RegistryValueDataMatchesKind {
param(
[Parameter(Mandatory)]
[string]$KindName,
[AllowNull()]
$Data
)
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
switch ($kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
$parsed = [uint32]0
return [uint32]::TryParse([string]$Data, [System.Globalization.NumberStyles]::Integer, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
$parsed = [uint64]0
return [uint64]::TryParse([string]$Data, [System.Globalization.NumberStyles]::Integer, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)
}
([Microsoft.Win32.RegistryValueKind]::Binary) {
if ($null -eq $Data -or $Data -isnot [array]) { return $false }
foreach ($item in @($Data)) {
if ($item -isnot [ValueType] -and $item -isnot [string]) { return $false }
$parsed = 0
if (-not [int]::TryParse([string]$item, [ref]$parsed) -or $parsed -lt 0 -or $parsed -gt 255) {
return $false
}
}
return $true
}
([Microsoft.Win32.RegistryValueKind]::MultiString) {
foreach ($item in @($Data)) {
if ($item -isnot [string]) { return $false }
}
return $true
}
default { return ($null -eq $Data -or $Data -is [string]) }
}
}
function Test-RegistryValueAllowedByPlan {
param(
[Parameter(Mandatory)]
@@ -431,6 +506,14 @@ function Get-NormalizedRegistryPathKey {
return "$normalizedHive\\$normalizedSubKey"
}
<#
.SYNOPSIS
Tests whether a registry value-kind name is supported in backups.
.DESCRIPTION
Parses kind names case-insensitively and rejects empty, invalid, Unknown,
and None values.
#>
function Test-RegistryValueKindNameSupported {
param(
[string]$KindName
@@ -442,64 +525,10 @@ function Test-RegistryValueKindNameSupported {
try {
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
return $kind -ne [Microsoft.Win32.RegistryValueKind]::Unknown
return $kind -notin @([Microsoft.Win32.RegistryValueKind]::Unknown, [Microsoft.Win32.RegistryValueKind]::None)
}
catch {
return $false
}
}
<#
.SYNOPSIS
Checks whether a backed-up value's Data can be converted to its declared Kind.
.DESCRIPTION
A corrupted or hand-edited backup can have Data that doesn't fit its declared
Kind (e.g. Kind=DWord with Data=4294967296, which overflows uint32). Restore-
RegistryValueSnapshot's Convert-RegistryValueDataFromBackup performs the same
narrowing casts without a try/catch, and by the time it runs the live registry
subtree has already been deleted (Restore-RegistryKeySnapshot deletes before
rewriting) - so an invalid Data/Kind pairing must be rejected here, before any
restore begins, not left to fail mid-restore. See #686.
.PARAMETER KindName
The value's declared registry kind name (e.g. "DWord", "QWord", "String").
.PARAMETER Data
The value's backed-up data to validate against KindName.
#>
function Test-RegistryValueDataMatchesKind {
param(
[string]$KindName,
$Data
)
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
switch ($kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
try {
[void][uint32]$Data
return $true
}
catch {
return $false
}
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
try {
[void][uint64]$Data
return $true
}
catch {
return $false
}
}
default {
# String/MultiString/Binary/None conversions in Convert-RegistryValueDataFromBackup
# cannot throw for arbitrary Data - they stringify, array-map to strings, or fall
# back to an empty byte array / null.
return $true
}
}
}
@@ -14,12 +14,12 @@
bundled with the script (Assets/Start/start2.bin).
.EXAMPLE
ReplaceStartMenuForAllUsers
Replace-StartMenuForAllUsers
.EXAMPLE
ReplaceStartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
Replace-StartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
#>
function ReplaceStartMenuForAllUsers {
function Replace-StartMenuForAllUsers {
param (
[string]$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
)
@@ -34,16 +34,16 @@ function ReplaceStartMenuForAllUsers {
}
# Get path to start menu file for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and replace the start menu file
ForEach ($startMenuPath in $usersStartMenuPaths) {
ReplaceStartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate
Replace-StartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate
}
# Also replace the start menu file for the default user profile
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
$defaultStartMenuPath = Get-UserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan
@@ -57,7 +57,7 @@ function ReplaceStartMenuForAllUsers {
}
# Copy template to default profile
ReplaceStartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate
Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate
Write-Host "Replaced start menu for the default user profile"
Write-Host ""
}
@@ -83,12 +83,12 @@ function ReplaceStartMenuForAllUsers {
bundled with the script (Assets/Start/start2.bin).
.EXAMPLE
ReplaceStartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE
ReplaceStartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
#>
function ReplaceStartMenu {
function Replace-StartMenu {
param (
[Parameter(Mandatory)]
[string]$startMenuBinFile,
@@ -106,7 +106,7 @@ function ReplaceStartMenu {
return
}
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
@@ -147,12 +147,12 @@ function ReplaceStartMenu {
The target username. Pass an empty string or omit to resolve for the current user.
.EXAMPLE
GetStartMenuBinPathForUser -UserName "Jeff"
Get-StartMenuBinPathForUser -UserName "Jeff"
.EXAMPLE
GetStartMenuBinPathForUser -UserName "Default"
Get-StartMenuBinPathForUser -UserName "Default"
#>
function GetStartMenuBinPathForUser {
function Get-StartMenuBinPathForUser {
param(
[string]$UserName
)
@@ -161,7 +161,7 @@ function GetStartMenuBinPathForUser {
return "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
}
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
return (Get-UserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
}
<#
@@ -177,9 +177,9 @@ function GetStartMenuBinPathForUser {
The full path to a start2.bin file.
.EXAMPLE
GetStartMenuUserNameFromPath -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Get-StartMenuUserNameFromPath -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
#>
function GetStartMenuUserNameFromPath {
function Get-StartMenuUserNameFromPath {
param(
[string]$StartMenuBinFile
)
@@ -230,7 +230,7 @@ function Get-StartMenuBackupPath {
return $null
}
else {
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($startMenuPath in $usersStartMenuPaths) {
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuPath.FullName 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
@@ -261,19 +261,19 @@ function Get-StartMenuBackupPath {
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Restore-StartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
Restore-StartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenuFromBackup {
function Restore-StartMenuFromBackup {
param(
[Parameter(Mandatory)]
[string]$StartMenuBinFile,
[string]$BackupFilePath
)
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
# Auto-detect latest backup in the same folder as the start2.bin
$startMenuDir = Split-Path $StartMenuBinFile -Parent
@@ -342,19 +342,19 @@ function RestoreStartMenuFromBackup {
.DESCRIPTION
Resolves the start2.bin path for the currently logged-in user, then
delegates to RestoreStartMenuFromBackup.
delegates to Restore-StartMenuFromBackup.
.PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
RestoreStartMenu
Restore-StartMenu
.EXAMPLE
RestoreStartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
Restore-StartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenu {
function Restore-StartMenu {
param(
[string]$BackupFilePath
)
@@ -364,7 +364,7 @@ function RestoreStartMenu {
Write-Host "Restoring start menu for user $targetUserName from backup..."
return RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
return Restore-StartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
<#
@@ -384,17 +384,17 @@ function RestoreStartMenu {
LocalState folder.
.EXAMPLE
RestoreStartMenuForAllUsers
Restore-StartMenuForAllUsers
.EXAMPLE
RestoreStartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
Restore-StartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenuForAllUsers {
function Restore-StartMenuForAllUsers {
param(
[string]$BackupFilePath
)
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
$results = @()
@@ -402,10 +402,10 @@ function RestoreStartMenuForAllUsers {
foreach ($startMenuPath in $usersStartMenuPaths) {
$startMenuBinFile = Join-Path $startMenuPath.FullName 'start2.bin'
$results += RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
$results += Restore-StartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
$defaultStartMenuPath = Get-UserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if (Test-Path $defaultStartMenuPath) {
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
@@ -1,3 +1,16 @@
<#
.SYNOPSIS
Runs a script block against the registry hive for a backup target.
.PARAMETER Target
A supported backup target: DefaultUserProfile or User:<user name>.
.PARAMETER ScriptBlock
The operation to run after the target user hive is available.
.PARAMETER ArgumentObject
Optional object passed to the script block.
#>
function Invoke-WithLoadedRestoreHive {
param(
[Parameter(Mandatory)]
@@ -24,6 +37,13 @@ function Invoke-WithLoadedRestoreHive {
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
}
<#
.SYNOPSIS
Restores a registry key and its child keys from a backup snapshot.
.PARAMETER Snapshot
The saved registry-key state, including existence, values, and subkeys.
#>
function Restore-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
@@ -74,10 +94,20 @@ function Restore-RegistryKeySnapshot {
}
}
<#
.SYNOPSIS
Restores or removes a registry value from a backup snapshot.
.PARAMETER RegistryKey
The open registry key that contains the value.
.PARAMETER Snapshot
The saved registry-value state to apply.
#>
function Restore-RegistryValueSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
$Snapshot
)
@@ -101,21 +131,20 @@ function Restore-RegistryValueSnapshot {
$RegistryKey.SetValue($valueName, $normalizedData, $valueKind)
}
catch {
$retryBytes = Convert-BackupDataToByteArray -Data $Snapshot.Data
if ($null -ne $retryBytes) {
try {
$RegistryKey.SetValue($valueName, $retryBytes, [Microsoft.Win32.RegistryValueKind]::Binary)
return
}
catch {
# Fall through to original error message for context.
}
}
throw "Failed setting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
}
<#
.SYNOPSIS
Converts a backed-up registry value-kind name to its .NET enum value.
.PARAMETER KindName
The registry value-kind name stored in the backup.
.OUTPUTS
Microsoft.Win32.RegistryValueKind
#>
function Convert-RegistryValueKindFromBackup {
param(
[string]$KindName
@@ -133,6 +162,16 @@ function Convert-RegistryValueKindFromBackup {
}
}
<#
.SYNOPSIS
Converts backed-up data to a value suitable for registry restoration.
.PARAMETER Kind
The registry value kind that determines how the data is converted.
.PARAMETER Data
The serialized value data from the backup.
#>
function Convert-RegistryValueDataFromBackup {
param(
[Microsoft.Win32.RegistryValueKind]$Kind,
@@ -148,15 +187,20 @@ function Convert-RegistryValueDataFromBackup {
$unsigned = [uint64]$Data
return [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) }
([Microsoft.Win32.RegistryValueKind]::MultiString) { return ,([string[]]@($Data | ForEach-Object { [string]$_ })) }
([Microsoft.Win32.RegistryValueKind]::Binary) {
if ($null -eq $Data) {
return ,(New-Object byte[] 0)
}
$bytes = Convert-BackupDataToByteArray -Data $Data
if ($null -eq $bytes) {
return (New-Object byte[] 0)
throw 'Invalid binary registry data in backup. Expected byte values from 0 through 255.'
}
return $bytes
# Keep the byte array intact instead of writing each byte to the
# pipeline. RegistryKey.SetValue requires a byte[] for Binary.
return ,$bytes
}
([Microsoft.Win32.RegistryValueKind]::None) { return $null }
default {
if ($null -ne $Data) {
return [string]$Data
@@ -167,6 +211,17 @@ function Convert-RegistryValueDataFromBackup {
}
}
<#
.SYNOPSIS
Converts serialized binary backup data to a byte array.
.PARAMETER Data
A byte array or collection of integer byte values from the backup.
.OUTPUTS
System.Byte[]
Returns $null when the input contains invalid byte data.
#>
function Convert-BackupDataToByteArray {
param(
$Data
@@ -207,18 +262,3 @@ function Convert-BackupDataToByteArray {
return ,$bytes
}
function Remove-RegistrySubKeyTreeIfExists {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
$existing = $RootKey.OpenSubKey($SubKeyPath, $false)
if ($existing) {
$existing.Close()
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
}
}
@@ -12,9 +12,9 @@
.OUTPUTS
PSCustomObject
A normalized registry backup object produced by Normalize-RegistryBackup.
A normalized registry backup object produced by ConvertTo-NormalizedRegistryBackup.
#>
function Load-RegistryBackupFromFile {
function Import-RegistryBackup {
param(
[Parameter(Mandatory)]
[string]$FilePath
@@ -31,7 +31,7 @@ function Load-RegistryBackupFromFile {
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
}
return Normalize-RegistryBackup -Backup $rawBackup
return ConvertTo-NormalizedRegistryBackup -Backup $rawBackup
}
<#
@@ -52,7 +52,7 @@ function Load-RegistryBackupFromFile {
ComputerName, Target, SelectedFeatures, SelectedUndoFeatures, and
RegistryKeys properties.
#>
function Normalize-RegistryBackup {
function ConvertTo-NormalizedRegistryBackup {
param(
[Parameter(Mandatory)]
$Backup
@@ -93,7 +93,10 @@ function Normalize-RegistryBackup {
}
elseif ($normalizedTarget -like 'CurrentUser:*') {
$targetCurrentUserName = $normalizedTarget.Substring(12)
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
if (Test-RunningAsSystem) {
$errors.Add("Backup was made for '$targetCurrentUserName' and is user-scoped. Re-run as that user; SYSTEM cannot restore a CurrentUser backup.")
}
elseif ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
-not (Test-UserNameMatch -UserNameA $targetCurrentUserName -UserNameB $env:USERNAME)) {
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
}
@@ -176,7 +179,7 @@ function Normalize-RegistryBackup {
registry, loading the appropriate user hive when required.
.PARAMETER Backup
A normalized backup object (as produced by Normalize-RegistryBackup) whose
A normalized backup object (as produced by ConvertTo-NormalizedRegistryBackup) whose
RegistryKeys snapshots should be restored.
.OUTPUTS
@@ -190,7 +193,7 @@ function Restore-RegistryBackupState {
$Backup
)
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
$friendlyTarget = Get-FriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan
@@ -11,20 +11,20 @@
.EXAMPLE
DisableStoreSearchSuggestionsForAllUsers
#>
function DisableStoreSearchSuggestionsForAllUsers {
function Set-StoreSearchSuggestionsDisabledForAllUsers {
# Get path to Store app database for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and disable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) {
DisableStoreSearchSuggestions -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
# Also disable start search suggestions for the default user profile
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
DisableStoreSearchSuggestions -StoreAppsDatabase $defaultStoreDbPath
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $defaultStoreDbPath
}
}
@@ -45,7 +45,7 @@ function DisableStoreSearchSuggestionsForAllUsers {
.EXAMPLE
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
#>
function DisableStoreSearchSuggestions {
function Set-StoreSearchSuggestionsDisabled {
param (
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
@@ -95,20 +95,20 @@ function DisableStoreSearchSuggestions {
.EXAMPLE
EnableStoreSearchSuggestionsForAllUsers
#>
function EnableStoreSearchSuggestionsForAllUsers {
function Set-StoreSearchSuggestionsEnabledForAllUsers {
# Get path to Store app database for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and re-enable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) {
EnableStoreSearchSuggestions -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
# Also re-enable for the default user profile
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
EnableStoreSearchSuggestions -StoreAppsDatabase $defaultStoreDbPath
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $defaultStoreDbPath
}
}
@@ -128,7 +128,7 @@ function EnableStoreSearchSuggestionsForAllUsers {
.EXAMPLE
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
#>
function EnableStoreSearchSuggestions {
function Set-StoreSearchSuggestionsEnabled {
param (
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
@@ -201,12 +201,12 @@ function EnableStoreSearchSuggestions {
The target username. Pass an empty string or omit to resolve for the current user.
.EXAMPLE
GetStoreAppsDatabasePathForUser -UserName "Jeff"
Get-StoreAppsDatabasePathForUser -UserName "Jeff"
.EXAMPLE
GetStoreAppsDatabasePathForUser -UserName "Default"
Get-StoreAppsDatabasePathForUser -UserName "Default"
#>
function GetStoreAppsDatabasePathForUser {
function Get-StoreAppsDatabasePathForUser {
param(
[string]$UserName
)
@@ -215,7 +215,7 @@ function GetStoreAppsDatabasePathForUser {
return "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
}
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false)
return (Get-UserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false)
}
<#
@@ -287,13 +287,13 @@ function Test-StoreSearchSuggestionsDisabled {
function Test-StoreSearchSuggestionsDisabledForAllUsers {
$paths = @()
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($storeDbPath in $usersStoreDbPaths) {
$paths += ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
$paths += $defaultStoreDbPath
}
@@ -309,4 +309,4 @@ function Test-StoreSearchSuggestionsDisabledForAllUsers {
}
return $true
}
}
@@ -1,5 +1,5 @@
# Enables a Windows optional feature and pipes its output to the console
function EnableWindowsFeature {
function Enable-WindowsFeature {
param (
[string]$FeatureName
)
@@ -22,7 +22,7 @@ function EnableWindowsFeature {
}
# Disables a Windows optional feature and pipes its output to the console
function DisableWindowsFeature {
function Disable-WindowsFeature {
param (
[string]$FeatureName
)