2026-05-08 21:19:52 +02:00
function Get-RegistryBackupCapturePlans {
param (
2026-06-10 17:40:31 +02:00
[ object[] ] $SelectedRegistryFeatures = @ (),
[ object[] ] $UndoRegistryFeatures = @ (),
2026-05-09 21:56:58 +02:00
[ switch ] $UseSysprepRegFiles
2026-05-08 21:19:52 +02:00
)
$planMap = @ {}
2026-06-10 17:40:31 +02:00
2026-05-08 21:19:52 +02:00
foreach ( $feature in $SelectedRegistryFeatures ) {
2026-06-10 17:40:31 +02:00
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $feature . RegistryKey -UseSysprepRegFiles: $UseSysprepRegFiles
2026-05-08 21:19:52 +02:00
if ( -not ( Test-Path $regFilePath )) {
throw "Unable to find registry file for backup: $( $feature . RegistryKey ) ( $regFilePath )"
}
foreach ( $operation in @ ( Get-RegFileOperations -regFilePath $regFilePath )) {
if ( -not $operation . KeyPath ) { continue }
2026-06-10 17:40:31 +02:00
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
}
}
2026-05-08 21:19:52 +02:00
2026-06-10 17:40:31 +02:00
foreach ( $feature in $UndoRegistryFeatures ) {
$regFilePath = Resolve-RegistryBackupUndoFilePath -Feature $feature
if ([ string ]:: IsNullOrWhiteSpace ( $regFilePath )) {
continue
}
if ( -not ( Test-Path $regFilePath )) {
$undoKeyDescription = if ( -not [ string ]:: IsNullOrWhiteSpace ([ string ] $feature . RegistryUndoKey )) {
[ string ] $feature . RegistryUndoKey
}
else {
[ string ] $feature . RegistryKey
2026-05-08 21:19:52 +02:00
}
2026-06-10 17:40:31 +02:00
throw "Unable to find registry undo file for backup: $undoKeyDescription ( $regFilePath )"
}
foreach ( $operation in @ ( Get-RegFileOperations -regFilePath $regFilePath )) {
if ( -not $operation . KeyPath ) { continue }
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
2026-05-08 21:19:52 +02:00
}
}
return @ (
foreach ( $entry in $planMap . Values ) {
[ PSCustomObject ] @ {
Path = $entry . Path
IncludeSubKeys = [ bool ] $entry . IncludeSubKeys
CaptureAllValues = [ bool ] $entry . CaptureAllValues
ValueNames = @ ( $entry . ValueNames )
}
}
)
}
2026-06-10 17:40:31 +02:00
function Add-RegistryPlanOperation {
param (
[ hashtable ] $PlanMap ,
[ PSCustomObject ] $Operation
)
$mapKey = $Operation . KeyPath . ToLowerInvariant ()
if ( -not $PlanMap . ContainsKey ( $mapKey )) {
$PlanMap [ $mapKey ] = [ PSCustomObject ] @ {
Path = $Operation . KeyPath
IncludeSubKeys = $false
CaptureAllValues = $false
ValueNames = New-Object 'System.Collections.Generic.HashSet[string]' ([ System.StringComparer ]:: OrdinalIgnoreCase )
}
}
$plan = $PlanMap [ $mapKey ]
switch ( $Operation . OperationType ) {
'DeleteKey' {
$plan . IncludeSubKeys = $true
$plan . CaptureAllValues = $true
}
'SetValue' {
if ( -not $plan . CaptureAllValues ) {
$null = $plan . ValueNames . Add ([ string ] $Operation . ValueName )
}
}
'DeleteValue' {
if ( -not $plan . CaptureAllValues ) {
$null = $plan . ValueNames . Add ([ string ] $Operation . ValueName )
}
}
}
}
function Resolve-RegistryBackupUndoFilePath {
2026-05-08 21:19:52 +02:00
param (
[ Parameter ( Mandatory )]
2026-06-10 17:40:31 +02:00
$Feature
)
$undoRegistryKey = [ string ] $Feature . RegistryUndoKey
if ( -not [ string ]:: IsNullOrWhiteSpace ( $undoRegistryKey )) {
$resolvedUndoPath = Resolve-UndoRegFilePath -FileName $undoRegistryKey
return Join-Path $script:RegfilesPath $resolvedUndoPath
}
$resolvedRegistryKey = [ string ] $Feature . RegistryKey
if ([ string ]:: IsNullOrWhiteSpace ( $resolvedRegistryKey )) {
return $null
}
if ([ System.IO.Path ]:: IsPathRooted ( $resolvedRegistryKey )) {
return $resolvedRegistryKey
}
return Join-Path $script:RegfilesPath $resolvedRegistryKey
}
function Get-RegistrySnapshotsForBackup {
param (
[ object[] ] $CapturePlans = @ ()
2026-05-08 21:19:52 +02:00
)
if ( $CapturePlans . Count -eq 0 ) {
return @ ()
}
$snapshotScript = {
param ( $plans )
$snapshots = @ ()
foreach ( $plan in $plans ) {
$snapshots += Get-RegistryKeySnapshot -KeyPath $plan . Path -CaptureAllValues: $plan . CaptureAllValues -ValueNames @ ( $plan . ValueNames ) -IncludeSubKeys: $plan . IncludeSubKeys
}
return @ ( $snapshots )
}
if ( $script:Params . ContainsKey ( 'Sysprep' ) -or $script:Params . ContainsKey ( 'User' )) {
return Invoke-WithLoadedBackupHive -ScriptBlock $snapshotScript -ArgumentObject @ ( $CapturePlans )
}
return & $snapshotScript $CapturePlans
}
function Invoke-WithLoadedBackupHive {
param (
[ Parameter ( Mandatory )]
[ scriptblock ] $ScriptBlock ,
$ArgumentObject = $null
)
2026-06-07 22:51:01 +02:00
$targetUserName = if ( $script:Params . ContainsKey ( 'Sysprep' )) {
'Default'
2026-05-08 21:19:52 +02:00
}
else {
2026-06-07 22:51:01 +02:00
$script:Params . Item ( 'User' )
2026-05-08 21:19:52 +02:00
}
2026-06-07 22:51:01 +02:00
return Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
2026-05-08 21:19:52 +02:00
}
function Get-RegistryKeySnapshot {
param (
[ Parameter ( Mandatory )]
[ string ] $KeyPath ,
[ bool ] $CaptureAllValues = $false ,
[ string[] ] $ValueNames = @ (),
[ bool ] $IncludeSubKeys = $false
)
$registryParts = Split-RegistryPath -path $KeyPath
if ( -not $registryParts ) {
throw "Unsupported registry path in backup: $KeyPath "
}
$rootKey = Get-RegistryRootKey -hiveName $registryParts . Hive
if ( -not $rootKey ) {
throw "Unsupported registry hive in backup: $( $registryParts . Hive ) "
}
$subKeyPath = $registryParts . SubKey
$key = $rootKey . OpenSubKey ( $subKeyPath , $false )
if ( $null -eq $key ) {
return @ {
Path = $KeyPath
Exists = $false
Values = @ ()
SubKeys = @ ()
}
}
try {
return ( Convert-RegistryKeyToSnapshot -RegistryKey $key -FullPath $KeyPath -CaptureAllValues: $CaptureAllValues -ValueNames $ValueNames -IncludeSubKeys: $IncludeSubKeys )
}
finally {
$key . Close ()
}
}
2026-07-19 22:06:07 +02:00
<#
. SYNOPSIS
Converts an open registry key into a backup snapshot.
. DESCRIPTION
Captures all values or selected value names, records missing selected values,
2026-07-25 20:05:49 +02:00
and recursively captures subkeys when requested. Throws if a requested subkey
cannot be read.
2026-07-19 22:06:07 +02:00
#>
2026-05-08 21:19:52 +02:00
function Convert-RegistryKeyToSnapshot {
param (
[ Parameter ( Mandatory )]
2026-07-19 22:06:07 +02:00
$RegistryKey ,
2026-05-08 21:19:52 +02:00
[ Parameter ( Mandatory )]
[ string ] $FullPath ,
[ bool ] $CaptureAllValues = $false ,
[ string[] ] $ValueNames = @ (),
[ bool ] $IncludeSubKeys = $false
)
$values = @ ()
if ( $CaptureAllValues ) {
foreach ( $valueName in @ ( $RegistryKey . GetValueNames ())) {
$values += @ ( Convert-RegistryValueToSnapshot -RegistryKey $RegistryKey -ValueName $valueName )
}
}
else {
foreach ( $valueName in @ ( $ValueNames | Sort-Object -Unique )) {
$exists = ( $RegistryKey . GetValueNames () -contains $valueName )
if ( $exists ) {
$values += @ ( Convert-RegistryValueToSnapshot -RegistryKey $RegistryKey -ValueName $valueName )
}
else {
$values += @ {
Name = $valueName
Exists = $false
Kind = $null
Data = $null
}
}
}
}
$subKeys = @ ()
if ( $IncludeSubKeys ) {
foreach ( $subKeyName in @ ( $RegistryKey . GetSubKeyNames ())) {
$childKey = $RegistryKey . OpenSubKey ( $subKeyName , $false )
2026-07-25 20:05:49 +02:00
if ( $null -eq $childKey ) {
throw "Unable to read registry subkey ' $( $RegistryKey . Name ) \ $subKeyName ' while creating a backup snapshot. The backup was not created."
}
2026-05-08 21:19:52 +02:00
try {
$childPath = if ([ string ]:: IsNullOrWhiteSpace ( $FullPath )) { $subKeyName } else { " $FullPath \ $subKeyName " }
$subKeys += @ ( Convert-RegistryKeyToSnapshot -RegistryKey $childKey -FullPath $childPath -CaptureAllValues: $true -IncludeSubKeys: $true )
}
finally {
$childKey . Close ()
}
}
}
return @ {
Path = $FullPath
Exists = $true
Values = $values
SubKeys = $subKeys
}
}
2026-07-19 22:06:07 +02:00
<#
. 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.
#>
2026-05-08 21:19:52 +02:00
function Convert-RegistryValueToSnapshot {
param (
[ Parameter ( Mandatory )]
2026-07-19 22:06:07 +02:00
$RegistryKey ,
2026-05-08 21:19:52 +02:00
[ Parameter ( Mandatory )]
[ AllowEmptyString ()]
[ string ] $ValueName
)
$valueKind = $RegistryKey . GetValueKind ( $ValueName )
2026-07-19 22:06:07 +02:00
if ( $valueKind -eq [ Microsoft.Win32.RegistryValueKind ]:: None ) {
throw "REG_NONE registry values are not supported for backup. Key=' $( $RegistryKey . Name ) ' Name=' $ValueName '"
}
2026-05-08 21:19:52 +02:00
$value = $RegistryKey . GetValue ( $ValueName , $null , [ Microsoft.Win32.RegistryValueOptions ]:: DoNotExpandEnvironmentNames )
2026-05-11 19:14:08 +02:00
try {
$normalizedValue = switch ( $valueKind ) {
2026-07-19 22:06:07 +02:00
# 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 ] $_ }) } }
2026-05-11 19:14:08 +02:00
([ 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 ) }
default { if ( $null -ne $value ) { [ string ] $value } else { $null } }
}
}
catch {
$valueType = if ( $null -ne $value ) { $value . GetType (). FullName } else { '<null>' }
$valueForLog = if ( $null -eq $value ) { '<null>' } elseif ( $value -is [ array ]) { ( $value -join ',' ) } else { [ string ] $value }
throw "Failed to normalize registry value for backup. Key=' $( $RegistryKey . Name ) ' Name=' $ValueName ' Kind=' $valueKind ' RawType=' $valueType ' RawValue=' $valueForLog '. InnerError: $( $_ . Exception . Message ) "
2026-05-08 21:19:52 +02:00
}
return @ {
Name = $ValueName
Exists = $true
Kind = $valueKind . ToString ()
Data = $normalizedValue
}
}
2026-07-19 22:06:07 +02:00
<#
. 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.
#>
2026-05-08 21:19:52 +02:00
function Get-RegistryBackupTargetDescription {
if ( $script:Params . ContainsKey ( 'Sysprep' )) {
return 'DefaultUserProfile'
}
2026-07-19 22:06:07 +02:00
$resolvedUserName = [ string ]( Get-UserName )
2026-05-08 21:19:52 +02:00
if ( $script:Params . ContainsKey ( 'User' )) {
return "User: $resolvedUserName "
}
return "CurrentUser: $resolvedUserName "
}