fix: preserve selections when config import is invalid (#4994)

* fix: validate imports before replacing selections

* test: cover atomic config imports

* docs: explain stale config imports

---------

Co-authored-by: Guilherme de Oliveira Rocha <contatoguilhermeoliveira@protonmail.com>
Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
Guilherme Oliveira
2026-08-19 15:30:46 -05:00
committed by GitHub
co-authored by Guilherme de Oliveira Rocha Chris Titus
parent 2d0fd43bd3
commit 792122e998
4 changed files with 183 additions and 13 deletions
@@ -40,6 +40,12 @@ This is useful for:
- Reusing a known-good baseline after reinstalling Windows
- Standardizing deployments for labs, workstations, or personal setups
:::caution[Keep exported configurations current]
Exported configurations contain the WinUtil catalog keys that existed when the file was created. If a later WinUtil version removes or renames one of those keys, the import is rejected before any current selections are changed. PowerShell reports the stale entry as `Unknown selection key '<key>'`.
To recover, compare the reported key with the current files in the [WinUtil configuration catalog](https://github.com/ChrisTitusTech/winutil/tree/main/config). Remove or replace the stale key in your JSON file, or create and export a new configuration with the current WinUtil version, then run the import again. Re-export long-lived baselines after catalog changes so they remain compatible.
:::
:::note
Run the command in an elevated PowerShell session so WinUtil can apply system-level changes.
:::
+56 -2
View File
@@ -1,4 +1,19 @@
function Update-WinUtilSelections ($flatJson) {
function Update-WinUtilSelections {
param(
[Parameter(Mandatory)]
[string[]]$flatJson,
[switch]$Replace
)
$nextSelections = @{
selectedApps = [System.Collections.Generic.List[string]]::new()
selectedTweaks = [System.Collections.Generic.List[string]]::new()
selectedToggles = [System.Collections.Generic.List[string]]::new()
selectedFeatures = [System.Collections.Generic.List[string]]::new()
selectedAppx = [System.Collections.Generic.List[string]]::new()
}
foreach ($cbkey in $flatJson) {
$listName = switch -Regex ($cbkey) {
@@ -9,6 +24,45 @@ function Update-WinUtilSelections ($flatJson) {
'^WPFAppx' { 'selectedAppx' }
}
$sync.$listName.Add($cbkey)
if (-not $listName) {
throw "Unsupported selection key '$cbkey'."
}
$isKnownSelection = switch ($listName) {
'selectedApps' {
$sync.configs.applicationsHashtable.ContainsKey($cbkey)
}
'selectedTweaks' {
$null -ne $sync.configs.tweaks.PSObject.Properties[$cbkey]
}
'selectedToggles' {
$null -ne $sync.configs.tweaks.PSObject.Properties[$cbkey]
}
'selectedFeatures' {
$null -ne $sync.configs.feature.PSObject.Properties[$cbkey]
}
'selectedAppx' {
$sync.configs.appxHashtable.ContainsKey($cbkey)
}
}
if (-not $isKnownSelection) {
throw "Unknown selection key '$cbkey'."
}
$nextSelections[$listName].Add($cbkey)
}
if ($Replace) {
foreach ($listName in $nextSelections.Keys) {
$sync[$listName] = $nextSelections[$listName]
}
return
}
foreach ($listName in $nextSelections.Keys) {
foreach ($cbkey in $nextSelections[$listName]) {
$sync.$listName.Add($cbkey)
}
}
}
+3 -9
View File
@@ -84,15 +84,9 @@ function Invoke-WPFImpex {
return
}
# Clear all existing selections before importing so the import replaces
# the current state rather than merging with it
$sync.selectedAppx = [System.Collections.Generic.List[string]]::new()
$sync.selectedApps = [System.Collections.Generic.List[string]]::new()
$sync.selectedTweaks = [System.Collections.Generic.List[string]]::new()
$sync.selectedToggles = [System.Collections.Generic.List[string]]::new()
$sync.selectedFeatures = [System.Collections.Generic.List[string]]::new()
Update-WinUtilSelections -flatJson $flattenedJson
# Build and validate every imported selection before replacing the current
# state. This keeps a malformed config from leaving partial selections behind.
Update-WinUtilSelections -flatJson $flattenedJson -Replace
if ($sync.Form) {
Reset-WPFCheckBoxes -doToggles $true
+118 -2
View File
@@ -48,12 +48,17 @@ namespace System.Windows.Controls
public class WrapPanel
{
public global::Windows.Visibility Visibility { get; set; }
public object Visibility { get; set; }
}
public class StackPanel
{
public System.Collections.ArrayList Children { get; } = new System.Collections.ArrayList();
public System.Collections.ArrayList Children { get; private set; }
public StackPanel()
{
Children = new System.Collections.ArrayList();
}
}
}
"@
@@ -61,6 +66,7 @@ namespace System.Windows.Controls
. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
. (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFImpex.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFGetInstalled.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
@@ -127,6 +133,16 @@ namespace System.Windows.Controls
Content = "Git"
}
}
appxHashtable = @{
WPFAppxExample = [pscustomobject]@{}
}
tweaks = [pscustomobject]@{
WPFTweaksTelemetry = [pscustomobject]@{}
WPFToggleDarkMode = [pscustomobject]@{}
}
feature = [pscustomobject]@{
WPFFeatureSandbox = [pscustomobject]@{}
}
}
WPFselectedAppsButton = [pscustomobject]@{
Content = ""
@@ -174,6 +190,106 @@ Describe "Update-WinUtilSelections" {
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
}
It "replaces selections only after every imported key is validated" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
Update-WinUtilSelections -flatJson @(
"WPFInstallGit",
"WPFFeatureSandbox"
) -Replace
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @()
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
}
It "preserves existing selections when an imported key is unsupported" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
{
Update-WinUtilSelections -flatJson @(
"WPFInstallGit",
"NotAWinUtilKey"
) -Replace
} | Should -Throw "Unsupported selection key 'NotAWinUtilKey'."
@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksExisting")
@($script:sync.selectedFeatures) | Should -Be @()
}
It "preserves existing selections when an imported key is not in the current catalog" {
$script:sync.selectedApps.Add("WPFInstallExisting")
{
Update-WinUtilSelections -flatJson @(
"WPFInstallGit",
"WPFInstallUnknown"
) -Replace
} | Should -Throw "Unknown selection key 'WPFInstallUnknown'."
@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
@($script:sync.selectedTweaks) | Should -Be @()
@($script:sync.selectedFeatures) | Should -Be @()
}
}
Describe "Invoke-WPFImpex import selection state" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.Form = [pscustomobject]@{}
Mock Reset-WPFCheckBoxes { }
Mock Write-Error { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
It "replaces selections and resets the UI after a valid import" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
$configPath = Join-Path $TestDrive "valid-config.json"
@(
"WPFInstallGit",
"WPFFeatureSandbox"
) | ConvertTo-Json | Set-Content -LiteralPath $configPath
Invoke-WPFImpex -type "import" -Config $configPath
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
@($script:sync.selectedTweaks) | Should -Be @()
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 1 -Exactly -ParameterFilter {
$doToggles -eq $true
}
Should -Invoke -CommandName Write-Error -Times 0 -Exactly
}
It "preserves selections and does not reset the UI after an invalid import" {
$script:sync.selectedApps.Add("WPFInstallExisting")
$script:sync.selectedTweaks.Add("WPFTweaksExisting")
$configPath = Join-Path $TestDrive "invalid-config.json"
@(
"WPFInstallGit",
"WPFInstallUnknown"
) | ConvertTo-Json | Set-Content -LiteralPath $configPath
Invoke-WPFImpex -type "import" -Config $configPath
@($script:sync.selectedApps) | Should -Be @("WPFInstallExisting")
@($script:sync.selectedTweaks) | Should -Be @("WPFTweaksExisting")
@($script:sync.selectedFeatures) | Should -Be @()
Should -Invoke -CommandName Reset-WPFCheckBoxes -Times 0 -Exactly
Should -Invoke -CommandName Write-Error -Times 1 -Exactly -ParameterFilter {
$Message -like "An error occurred while importing: *Unknown selection key 'WPFInstallUnknown'.*"
}
}
}
Describe "Invoke-WPFSelectedCheckboxesUpdate" {