Files
Win11Debloat/Scripts/Features/RestoreRegistryBackup.ps1

221 lines
7.8 KiB
PowerShell

<#
.SYNOPSIS
Loads a registry backup from a JSON file and normalizes its contents.
.DESCRIPTION
Loads a registry backup from disk and returns a normalized representation
of its contents suitable for use by the restore workflow. Throws if the
file is missing, unreadable, or not valid JSON.
.PARAMETER FilePath
The absolute path to the registry backup JSON file to load.
.OUTPUTS
PSCustomObject
A normalized registry backup object produced by Normalize-RegistryBackup.
#>
function Load-RegistryBackupFromFile {
param(
[Parameter(Mandatory)]
[string]$FilePath
)
if (-not (Test-Path -LiteralPath $FilePath)) {
throw "Backup file was not found: $FilePath"
}
try {
$rawBackup = Get-Content -LiteralPath $FilePath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
}
catch {
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
}
return Normalize-RegistryBackup -Backup $rawBackup
}
<#
.SYNOPSIS
Validates and normalizes a raw registry backup object.
.DESCRIPTION
Validates the structure and content of the supplied backup and converts
it into a normalized representation that can be safely consumed by the
restore workflow. Throws if validation fails.
.PARAMETER Backup
The raw backup object (typically parsed from JSON) to normalize.
.OUTPUTS
PSCustomObject
A normalized backup with Version, BackupType, CreatedAt, CreatedBy,
ComputerName, Target, SelectedFeatures, SelectedUndoFeatures, and
RegistryKeys properties.
#>
function Normalize-RegistryBackup {
param(
[Parameter(Mandatory)]
$Backup
)
$errors = New-Object System.Collections.Generic.List[string]
if (-not $Backup.PSObject.Properties['Version']) {
$errors.Add('Missing property: Version')
}
elseif ([string]$Backup.Version -ne '1.0') {
$errors.Add("Unsupported backup version '$($Backup.Version)'.")
}
if (-not $Backup.PSObject.Properties['BackupType']) {
$errors.Add('Missing property: BackupType')
}
elseif ([string]$Backup.BackupType -ne 'RegistryState') {
$errors.Add("Unsupported BackupType '$($Backup.BackupType)'.")
}
$normalizedTarget = ''
if (-not $Backup.PSObject.Properties['Target'] -or [string]::IsNullOrWhiteSpace([string]$Backup.Target)) {
$errors.Add('Missing property: Target')
}
else {
$normalizedTarget = [string]$Backup.Target
if ($normalizedTarget -eq 'DefaultUserProfile') {
# Valid target format.
}
elseif ($normalizedTarget -like 'User:*') {
$targetUserName = $normalizedTarget.Substring(5)
$targetValidation = Test-TargetUserName -UserName $targetUserName
if (-not $targetValidation.IsValid) {
$errors.Add("Invalid user '$normalizedTarget'")
}
}
elseif ($normalizedTarget -like 'CurrentUser:*') {
$targetCurrentUserName = $normalizedTarget.Substring(12)
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or ($targetCurrentUserName -ne $env:USERNAME)) {
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
}
}
else {
$errors.Add("Unsupported Target '$normalizedTarget'.")
}
}
$registryKeys = @()
if (-not $Backup.PSObject.Properties['RegistryKeys']) {
$errors.Add('Missing property: RegistryKeys')
}
else {
$registryKeys = @($Backup.RegistryKeys)
}
$normalizedKeys = @()
foreach ($keySnapshot in $registryKeys) {
$normalizedKeys += @(Normalize-RegistryKeySnapshot -Snapshot $keySnapshot)
}
$selectedFeatureParseResult = Get-NormalizedSelectedFeatureIdsFromBackup -Backup $Backup
$selectedFeatures = @($selectedFeatureParseResult.SelectedFeatures)
foreach ($selectedFeatureParseError in @($selectedFeatureParseResult.Errors)) {
$errors.Add([string]$selectedFeatureParseError)
}
$selectedUndoFeatureParseResult = Get-NormalizedSelectedUndoFeatureIdsFromBackup -Backup $Backup
$selectedUndoFeatures = @($selectedUndoFeatureParseResult.SelectedUndoFeatures)
foreach ($selectedUndoFeatureParseError in @($selectedUndoFeatureParseResult.Errors)) {
$errors.Add([string]$selectedUndoFeatureParseError)
}
$allSelectedFeatures = @($selectedFeatures) + @($selectedUndoFeatures)
if ($allSelectedFeatures.Count -eq 0) {
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
}
else {
try {
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -SelectedUndoFeatureIds @($selectedUndoFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
foreach ($allowListValidationError in $allowListValidationErrors) {
$errors.Add([string]$allowListValidationError)
}
}
catch {
$errors.Add("Failed to validate backup: $($_.Exception.Message)")
}
}
if ($errors.Count -gt 0) {
Write-Error "Backup validation failed: $($errors -join ' ')"
if ($errors.Count -eq 1) {
throw ("Validation failed: $($errors[0])")
}
else {
throw ("Validation failed with $($errors.Count) errors. See console output for details.")
}
}
return [PSCustomObject]@{
Version = [string]$Backup.Version
BackupType = [string]$Backup.BackupType
CreatedAt = [string]$Backup.CreatedAt
CreatedBy = [string]$Backup.CreatedBy
ComputerName = [string]$Backup.ComputerName
Target = $normalizedTarget
SelectedFeatures = @($selectedFeatures)
SelectedUndoFeatures = @($selectedUndoFeatures)
RegistryKeys = @($normalizedKeys)
}
}
<#
.SYNOPSIS
Restores registry state from a normalized backup object.
.DESCRIPTION
Applies the registry state described by the supplied backup back to the
registry, loading the appropriate user hive when required.
.PARAMETER Backup
A normalized backup object (as produced by Normalize-RegistryBackup) whose
RegistryKeys snapshots should be restored.
.OUTPUTS
PSCustomObject
Returns an object with a Result property set to $true when the restore
completes successfully.
#>
function Restore-RegistryBackupState {
param(
[Parameter(Mandatory)]
$Backup
)
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan
return [PSCustomObject]@{ Result = $true }
}
$restoreAction = {
param($normalizedBackup)
Write-Host "Applying registry restore from $(@($normalizedBackup.RegistryKeys).Count) root snapshot(s)."
foreach ($rootSnapshot in @($normalizedBackup.RegistryKeys)) {
Restore-RegistryKeySnapshot -Snapshot $rootSnapshot
}
}
Write-Host "Starting restore for $friendlyTarget."
if ($Backup.Target -eq 'DefaultUserProfile' -or $Backup.Target -like 'User:*') {
Write-Host "Restore requires loading target user hive."
Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup
Write-Host "Restore completed for $friendlyTarget."
return [PSCustomObject]@{ Result = $true }
}
& $restoreAction $Backup
Write-Host "Restore completed for $friendlyTarget."
return [PSCustomObject]@{ Result = $true }
}