mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-07-02 22:58:34 +00:00
Guard against loading, saving & executing undefined features (#665)
This commit is contained in:
@@ -368,7 +368,7 @@
|
||||
"Label": "Create a system restore point",
|
||||
"Category": null,
|
||||
"RegistryKey": null,
|
||||
"ApplyText": null,
|
||||
"ApplyText": "Creating system restore point",
|
||||
"UndoLabel": null,
|
||||
"ApplyUndoText": null,
|
||||
"RegistryUndoKey": null,
|
||||
|
||||
@@ -45,14 +45,8 @@ function PrintPendingChanges {
|
||||
continue
|
||||
}
|
||||
default {
|
||||
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
|
||||
$message = $script:Features[$parameterName].Label
|
||||
Write-Output "- $message"
|
||||
}
|
||||
else {
|
||||
# Fallback: show the parameter name if no feature description is available
|
||||
Write-Output "- $parameterName"
|
||||
}
|
||||
$message = $script:Features[$parameterName].Label
|
||||
Write-Output "- $message"
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
function Get-FeatureId {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
$Feature
|
||||
)
|
||||
|
||||
$featureId = [string]$Feature.FeatureId
|
||||
if ([string]::IsNullOrWhiteSpace($featureId)) {
|
||||
throw 'Selected feature is missing required FeatureId.'
|
||||
}
|
||||
|
||||
return $featureId
|
||||
}
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Filters a list of features to those that have a non-empty RegistryKey.
|
||||
|
||||
.PARAMETER Features
|
||||
An array of feature objects to filter.
|
||||
#>
|
||||
function Get-RegistryBackedFeatures {
|
||||
param(
|
||||
[object[]]$Features = @()
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a timestamped JSON backup of registry state for selected features.
|
||||
|
||||
.DESCRIPTION
|
||||
Resolves selected and undo features from the provided keys, captures their
|
||||
registry state, and saves the result as a JSON file in the Backups/ folder.
|
||||
Returns the file path on success, $null if no registry-backed features exist.
|
||||
|
||||
.PARAMETER ActionableKeys
|
||||
Param keys from $script:Params to resolve into apply features.
|
||||
|
||||
.PARAMETER ExtraFeatures
|
||||
Additional synthetic feature objects (e.g. undo features) to include.
|
||||
#>
|
||||
function New-RegistrySettingsBackup {
|
||||
param(
|
||||
[string[]]$ActionableKeys,
|
||||
@@ -32,6 +47,13 @@ function New-RegistrySettingsBackup {
|
||||
return $backupFilePath
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves param keys into deduplicated feature objects from the catalog.
|
||||
|
||||
.PARAMETER ActionableKeys
|
||||
Param keys to look up in $script:Features.
|
||||
#>
|
||||
function Get-SelectedFeatures {
|
||||
param(
|
||||
[string[]]$ActionableKeys
|
||||
@@ -46,8 +68,7 @@ function Get-SelectedFeatures {
|
||||
$feature = $script:Features[$paramKey]
|
||||
if (-not $feature) { continue }
|
||||
|
||||
$featureId = Get-FeatureId -Feature $feature
|
||||
|
||||
$featureId = [string]$feature.FeatureId
|
||||
if ($selectedFeatureIds.Add($featureId)) {
|
||||
$selectedFeatures.Add($feature)
|
||||
}
|
||||
@@ -56,6 +77,23 @@ function Get-SelectedFeatures {
|
||||
return @($selectedFeatures.ToArray())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the full backup payload object from selected and undo features.
|
||||
|
||||
.DESCRIPTION
|
||||
Deduplicates feature IDs, resolves registry capture plans, snapshots all
|
||||
registry keys, and assembles the final backup hashtable with metadata.
|
||||
|
||||
.PARAMETER SelectedFeatures
|
||||
Feature objects from the apply side.
|
||||
|
||||
.PARAMETER UndoFeatures
|
||||
Synthetic feature objects from the undo side.
|
||||
|
||||
.PARAMETER CreatedAt
|
||||
Timestamp recorded in the backup metadata.
|
||||
#>
|
||||
function Get-RegistryBackupPayload {
|
||||
param(
|
||||
[object[]]$SelectedFeatures = @(),
|
||||
@@ -67,8 +105,7 @@ function Get-RegistryBackupPayload {
|
||||
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($feature in $SelectedFeatures) {
|
||||
$featureId = Get-FeatureId -Feature $feature
|
||||
|
||||
$featureId = [string]$feature.FeatureId
|
||||
if ($seenSelectedFeatureIds.Add($featureId)) {
|
||||
$selectedFeatureIds.Add($featureId)
|
||||
}
|
||||
@@ -77,8 +114,7 @@ function Get-RegistryBackupPayload {
|
||||
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
|
||||
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($feature in $UndoFeatures) {
|
||||
$featureId = Get-FeatureId -Feature $feature
|
||||
|
||||
$featureId = [string]$feature.FeatureId
|
||||
if ($seenUndoFeatureIds.Add($featureId)) {
|
||||
$selectedUndoFeatureIds.Add($featureId)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Tests whether the registry operations in a feature's .reg file currently match the live registry.
|
||||
# Returns $true if ALL operations in the apply reg file match current system state.
|
||||
# Returns $false if the feature has no RegistryKey, the file is missing, or any operation mismatches.
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Maps a .reg file value type string to its RegistryValueKind enum.
|
||||
|
||||
.PARAMETER Operation
|
||||
A parsed .reg operation object containing a ValueType property.
|
||||
#>
|
||||
function Get-ExpectedRegistryValueKind {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -18,13 +22,25 @@ function Get-ExpectedRegistryValueKind {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests whether a feature's registry operations currently match the live registry.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns $true when ALL operations in the apply .reg file match current system
|
||||
state. Returns $false if the feature has no RegistryKey, the reg file is
|
||||
missing, or any operation mismatches. Special-cased features (Widgets, Store
|
||||
suggestions, Windows Sandbox, WSL) bypass .reg checking entirely.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to test.
|
||||
#>
|
||||
function Test-FeatureApplied {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string]$FeatureId
|
||||
)
|
||||
|
||||
if (-not $script:Features.ContainsKey($FeatureId)) { return $false }
|
||||
$feature = $script:Features[$FeatureId]
|
||||
|
||||
switch ($FeatureId) {
|
||||
@@ -147,8 +163,18 @@ function Test-FeatureApplied {
|
||||
return $true
|
||||
}
|
||||
|
||||
# Returns the 1-based index of the UiGroup option whose features all match current system state,
|
||||
# or 0 if no option fully matches (meaning the current state is unknown / "No Change").
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the 1-based index of the UiGroup option whose features all match
|
||||
current system state.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns 0 if no option fully matches, meaning the current state is unknown
|
||||
or represents "No Change".
|
||||
|
||||
.PARAMETER Group
|
||||
A UiGroup object whose Values array contains options with FeatureIds.
|
||||
#>
|
||||
function Get-CurrentGroupActiveIndex {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
|
||||
@@ -16,15 +16,11 @@ function Invoke-FeatureApply {
|
||||
)
|
||||
|
||||
# Resolve feature metadata from Features.json
|
||||
$feature = $null
|
||||
if ($script:Features.ContainsKey($FeatureId)) {
|
||||
$feature = $script:Features[$FeatureId]
|
||||
}
|
||||
|
||||
$applyText = if ($feature -and $feature.ApplyText) { $feature.ApplyText } else { $FeatureId }
|
||||
$feature = $script:Features[$FeatureId]
|
||||
$applyText = $feature.ApplyText
|
||||
|
||||
# ---- Registry-backed features: import .reg file, then handle side effects ----
|
||||
if ($feature -and $feature.RegistryKey) {
|
||||
if ($feature.RegistryKey) {
|
||||
ImportRegistryFile "> $applyText..." $feature.RegistryKey
|
||||
|
||||
# Post-import side effects for specific features
|
||||
@@ -177,15 +173,13 @@ function Invoke-FeatureUndo {
|
||||
return
|
||||
}
|
||||
'EnableWindowsSandbox' {
|
||||
$undoText = if ($feature) { $feature.ApplyUndoText } else { 'Disabling Windows Sandbox' }
|
||||
Write-Host "> $undoText..."
|
||||
Write-Host "> $($feature.ApplyUndoText)..."
|
||||
DisableWindowsFeature 'Containers-DisposableClientVM'
|
||||
Write-Host ""
|
||||
return
|
||||
}
|
||||
'EnableWindowsSubsystemForLinux' {
|
||||
$undoText = if ($feature) { $feature.ApplyUndoText } else { 'Disabling Windows Subsystem for Linux' }
|
||||
Write-Host "> $undoText..."
|
||||
Write-Host "> $($feature.ApplyUndoText)..."
|
||||
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
|
||||
DisableWindowsFeature 'VirtualMachinePlatform'
|
||||
Write-Host ""
|
||||
@@ -245,15 +239,8 @@ function Invoke-ApplyFeatures {
|
||||
if ($script:CancelRequested) { return }
|
||||
|
||||
# Resolve display name for the progress indicator
|
||||
$displayName = $featureId
|
||||
if ($script:Features.ContainsKey($featureId)) {
|
||||
$f = $script:Features[$featureId]
|
||||
if ($f.ApplyText) {
|
||||
$displayName = $f.ApplyText
|
||||
} elseif ($f.Label) {
|
||||
$displayName = $f.Label
|
||||
}
|
||||
}
|
||||
$f = $script:Features[$featureId]
|
||||
$displayName = $f.ApplyText
|
||||
|
||||
if ($script:ApplyProgressCallback) {
|
||||
& $script:ApplyProgressCallback $step $TotalSteps $displayName
|
||||
@@ -345,7 +332,6 @@ function Invoke-AllChanges {
|
||||
# ---- Determine if registry backup is needed ----
|
||||
$needsBackup = $false
|
||||
foreach ($id in $applyIds) {
|
||||
if (-not $script:Features.ContainsKey($id)) { continue }
|
||||
$f = $script:Features[$id]
|
||||
if ($f -and -not [string]::IsNullOrWhiteSpace([string]$f.RegistryKey)) {
|
||||
$needsBackup = $true
|
||||
|
||||
@@ -21,6 +21,9 @@ function LoadSettings {
|
||||
|
||||
$feature = $script:Features[$setting.Name]
|
||||
|
||||
# Skip unknown settings that aren't defined in Features.json
|
||||
if (-not $feature) { continue }
|
||||
|
||||
# Check version and feature compatibility using Features.json
|
||||
if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) {
|
||||
continue
|
||||
|
||||
@@ -11,7 +11,7 @@ function SaveSettings {
|
||||
}
|
||||
|
||||
foreach ($param in $script:Params.Keys) {
|
||||
if ($script:ControlParams -notcontains $param) {
|
||||
if ($script:ControlParams -notcontains $param -and $script:Features.ContainsKey($param)) {
|
||||
$value = $script:Params[$param]
|
||||
|
||||
$settings.Settings += @{
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
# MainWindow-Deployment.ps1
|
||||
# Overview generation, pending tweak actions, feature labels, tweak preset maps, apply logic, user mode state, user selection, and validation.
|
||||
|
||||
function Get-FeatureLabel {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
$FallbackLabel = $null
|
||||
)
|
||||
|
||||
$label = $script:FeatureLabelLookup[$FeatureId]
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$label)) {
|
||||
return [string]$label
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$FallbackLabel)) {
|
||||
return [string]$FallbackLabel
|
||||
}
|
||||
|
||||
return [string]$FeatureId
|
||||
}
|
||||
|
||||
function Get-UndoFeatureLabel {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
$FallbackLabel = $null
|
||||
)
|
||||
param([string]$FeatureId)
|
||||
|
||||
$undoLabel = $script:UndoFeatureLabelLookup[$FeatureId]
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$undoLabel)) {
|
||||
@@ -31,8 +10,7 @@ function Get-UndoFeatureLabel {
|
||||
}
|
||||
|
||||
# Fall back to the regular label (prefixed for undo context)
|
||||
$label = Get-FeatureLabel -FeatureId $FeatureId -FallbackLabel $FallbackLabel
|
||||
return [string]$label
|
||||
return [string]$script:FeatureLabelLookup[$FeatureId]
|
||||
}
|
||||
|
||||
function Get-PendingTweakActions {
|
||||
@@ -69,14 +47,14 @@ function Get-PendingTweakActions {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Apply'
|
||||
FeatureId = [string]$mapping.FeatureId
|
||||
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||
Label = [string]$script:FeatureLabelLookup[$mapping.FeatureId]
|
||||
})
|
||||
}
|
||||
elseif ($wasApplied -and -not $isNowChecked) {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Undo'
|
||||
FeatureId = [string]$mapping.FeatureId
|
||||
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
|
||||
Label = [string]$script:FeatureLabelLookup[$mapping.FeatureId]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -101,7 +79,7 @@ function Get-PendingTweakActions {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Apply'
|
||||
FeatureId = [string]$fid
|
||||
Label = (Get-FeatureLabel -FeatureId $fid)
|
||||
Label = [string]$script:FeatureLabelLookup[$fid]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a lightweight state object for the restore-backup dialog result.
|
||||
|
||||
.DESCRIPTION
|
||||
Encapsulates the user's dialog choice (Result), the selected backup file
|
||||
path, and the parsed backup payload so callers receive a single object.
|
||||
#>
|
||||
function New-RestoreDialogState {
|
||||
param(
|
||||
[string]$Result = 'Cancel',
|
||||
@@ -8,6 +16,16 @@ function New-RestoreDialogState {
|
||||
return @{ Result = $Result; SelectedFile = $SelectedFile; Backup = $Backup }
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Looks up a feature definition by ID from the provided feature catalog.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The identifier to search for (e.g. 'DisableTelemetry').
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json (FeatureId -> feature object).
|
||||
#>
|
||||
function Get-RestoreDialogFeatureDefinition {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -25,6 +43,21 @@ function Get-RestoreDialogFeatureDefinition {
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Determines whether a feature can be automatically reverted via registry restore.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns $true when the feature has a non-empty RegistryKey, indicating
|
||||
an apply .reg file exists that can be undone automatically. Features
|
||||
with custom logic (no RegistryKey) must be manually reverted.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to check.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Test-RestoreDialogFeatureCanAutoRevert {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -43,6 +76,20 @@ function Test-RestoreDialogFeatureCanAutoRevert {
|
||||
return $false
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves a human-readable label for a feature shown in the restore dialog.
|
||||
|
||||
.DESCRIPTION
|
||||
Returns the feature's Label from Features.json when found, falling back
|
||||
to the raw FeatureId string. For null/empty FeatureIds returns 'Unknown feature'.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to resolve a label for.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Get-RestoreDialogFeatureDisplayLabel {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -54,13 +101,28 @@ function Get-RestoreDialogFeatureDisplayLabel {
|
||||
}
|
||||
|
||||
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
|
||||
if ($featureDefinition -and -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Label)) {
|
||||
if ($featureDefinition) {
|
||||
return [string]$featureDefinition.Label
|
||||
}
|
||||
|
||||
return $FeatureId
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Checks whether a feature should appear in the restore dialog's overview list.
|
||||
|
||||
.DESCRIPTION
|
||||
A feature is considered visible when it exists in the catalog and has
|
||||
a non-empty Category (meaning it belongs to a UI grouping). Features
|
||||
without a Category are hidden from the overview.
|
||||
|
||||
.PARAMETER FeatureId
|
||||
The feature identifier to check.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Test-RestoreDialogFeatureVisibleInOverview {
|
||||
param(
|
||||
[string]$FeatureId,
|
||||
@@ -79,6 +141,13 @@ function Test-RestoreDialogFeatureVisibleInOverview {
|
||||
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts deduplicated forward (apply) feature IDs from a backup payload.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object containing a SelectedFeatures property.
|
||||
#>
|
||||
function Get-SelectedForwardFeatureIdsFromBackup {
|
||||
param($SelectedBackup)
|
||||
|
||||
@@ -99,6 +168,13 @@ function Get-SelectedForwardFeatureIdsFromBackup {
|
||||
return @($selectedFeatureIds.ToArray())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts deduplicated undo feature IDs from a backup payload.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object containing a SelectedUndoFeatures property.
|
||||
#>
|
||||
function Get-SelectedUndoFeatureIdsFromBackup {
|
||||
param($SelectedBackup)
|
||||
|
||||
@@ -119,6 +195,13 @@ function Get-SelectedUndoFeatureIdsFromBackup {
|
||||
return @($selectedUndoFeatureIds.ToArray())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Merges forward and undo feature IDs from a backup into a single deduplicated list.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object containing SelectedFeatures and SelectedUndoFeatures.
|
||||
#>
|
||||
function Get-CombinedSelectedFeatureIdsFromBackup {
|
||||
param($SelectedBackup)
|
||||
|
||||
@@ -139,12 +222,34 @@ function Get-CombinedSelectedFeatureIdsFromBackup {
|
||||
return @($featureIds.ToArray())
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Convenience wrapper that returns all combined feature IDs from a backup.
|
||||
|
||||
.PARAMETER SelectedBackup
|
||||
The parsed backup object.
|
||||
#>
|
||||
function Get-SelectedFeatureIdsFromBackup {
|
||||
param($SelectedBackup)
|
||||
|
||||
return @(Get-CombinedSelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Splits selected feature IDs into revertible and non-revertible lists for display.
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates the provided feature IDs, filters to those visible in the overview,
|
||||
and separates them into auto-revertible (has a RegistryKey) and non-revertible
|
||||
(requires manual undo) buckets. Each entry includes a display label.
|
||||
|
||||
.PARAMETER SelectedFeatureIds
|
||||
The list of feature IDs to categorize.
|
||||
|
||||
.PARAMETER Features
|
||||
A hashtable loaded from Features.json.
|
||||
#>
|
||||
function Get-RestoreBackupFeatureLists {
|
||||
param(
|
||||
[string[]]$SelectedFeatureIds,
|
||||
|
||||
@@ -242,6 +242,10 @@ $script:Features = @{}
|
||||
try {
|
||||
$featuresData = Get-Content -Path $script:FeaturesFilePath -Raw | ConvertFrom-Json
|
||||
foreach ($feature in $featuresData.Features) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$feature.FeatureId) -or [string]::IsNullOrWhiteSpace([string]$feature.Label) -or [string]::IsNullOrWhiteSpace([string]$feature.ApplyText)) {
|
||||
Write-Warning "Feature '$($feature.FeatureId)' is missing a FeatureId, Label, or ApplyText in Features.json and will be skipped."
|
||||
continue
|
||||
}
|
||||
$script:Features[$feature.FeatureId] = $feature
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user