Private/GpoBackupHelpers.ps1

<#
    GPO backup helpers (from PoSHRepo Windows\GPO\ReplaceImportGPO.ps1 + ActiveDirectory\GPO\ExportGPO.ps1).
    A "portable" backup has its source domain replaced by placeholders, in this order:
        DC.contoso.com <- the domain controller FQDN the backup was taken on (PDC emulator)
        contoso.com <- the DNS domain
        contoso <- the NetBIOS domain
    Order matters both ways: the DC FQDN contains the DNS name, and the DNS name contains the
    NetBIOS name in most domains.
#>


$script:NSPADGpoPlaceholder = [ordered]@{ Pdc = 'DC.contoso.com'; Dns = 'contoso.com'; NetBios = 'contoso' }
# The files Backup-GPO writes that carry domain names; manifest.xml sits beside the {GUID} folder.
$script:NSPADGpoBackupFiles = @('gpreport.xml', 'Backup.xml', 'bkupInfo.xml')

function Get-NSPADGpoBackupInfo {
    # One backup ({GUID} folder holding bkupInfo.xml): display name, backup ID, GPO GUID, time, domain.
    param([Parameter(Mandatory)][string]$BackupFolder)
    $info = Get-ChildItem -LiteralPath $BackupFolder -Filter 'bkupInfo.xml' -Force -File -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $info) { return $null }
    try { $x = [xml](Get-Content -LiteralPath $info.FullName -Raw) } catch { return $null }
    $bi = $x.DocumentElement
    $text = { param($n) $node = $bi.ChildNodes | Where-Object { $_.LocalName -eq $n } | Select-Object -First 1; if ($node) { "$($node.InnerText)".Trim() } }
    [pscustomobject]@{
        DisplayName  = & $text 'GPODisplayName'
        BackupId     = & $text 'ID'
        GpoGuid      = & $text 'GPOGuid'
        BackupTime   = & $text 'BackupTime'
        SourceDomain = & $text 'GPODomain'
        Comment      = & $text 'Comment'
        BackupFolder = (Get-Item -LiteralPath $BackupFolder).FullName
        BackupRoot   = (Split-Path -Parent (Get-Item -LiteralPath $BackupFolder).FullName)
    }
}

function Get-NSPADGpoDomainNames {
    # This domain's names in placeholder order: Pdc, Dns, NetBios.
    $d = Get-ADDomain -ErrorAction Stop
    [ordered]@{ Pdc = "$($d.PDCEmulator)"; Dns = "$($d.DNSRoot)"; NetBios = "$($d.NetBIOSName)" }
}

function Convert-NSPADGpoBackupText {
    <#
        PURE. Replaces each -From value with the matching -To value (keys Pdc, Dns, NetBios, in that
        order) in -Text. Case-insensitive, so a backup taken on CONTOSO.COM still matches contoso.com.
    #>

    param([AllowEmptyString()][string]$Text, [Parameter(Mandatory)][Collections.IDictionary]$From, [Parameter(Mandatory)][Collections.IDictionary]$To)
    foreach ($key in 'Pdc', 'Dns', 'NetBios') {
        if ([string]::IsNullOrEmpty($From[$key]) -or $null -eq $To[$key]) { continue }
        $Text = [regex]::Replace($Text, [regex]::Escape([string]$From[$key]), ([string]$To[$key]).Replace('$', '$$'), 'IgnoreCase')
    }
    return $Text
}

function Convert-NSPADGpoBackupFolder {
    # Rewrites the domain names in one backup root (manifest.xml + the {GUID} folder's files). Keeps
    # each file's encoding (Backup-GPO writes UTF-16 for some of them).
    param([Parameter(Mandatory)][string]$BackupRoot, [Parameter(Mandatory)][Collections.IDictionary]$From, [Parameter(Mandatory)][Collections.IDictionary]$To)
    $files = @(Get-ChildItem -LiteralPath $BackupRoot -Filter 'manifest.xml' -Force -File -ErrorAction SilentlyContinue)
    foreach ($guidDir in @(Get-ChildItem -LiteralPath $BackupRoot -Directory -Force -Filter '{*}')) {
        $files += @(Get-ChildItem -LiteralPath $guidDir.FullName -Force -File | Where-Object { $script:NSPADGpoBackupFiles -contains $_.Name })
    }
    foreach ($f in $files) {
        Edit-NSPADGpoBackupFile -File $f -Transform { param($text) Convert-NSPADGpoBackupText -Text $text -From $From -To $To }
    }
}

function Convert-NSPADGpoBackupToken {
    # Literal find -> replace (case-insensitive) in every .xml file under one backup root, the
    # {GUID}\DomainSysvol preference files included - for values a template carries as tokens (a
    # file-share path, a script name). Values go in as-is: XML-escape them first.
    param([Parameter(Mandatory)][string]$BackupRoot, [Parameter(Mandatory)][Collections.IDictionary]$Replace)
    foreach ($f in @(Get-ChildItem -LiteralPath $BackupRoot -Recurse -Force -File -Filter '*.xml')) {
        Edit-NSPADGpoBackupFile -File $f -Transform {
            param($text)
            foreach ($key in $Replace.Keys) {
                if ([string]::IsNullOrEmpty([string]$key)) { continue }
                $text = [regex]::Replace($text, [regex]::Escape([string]$key), ([string]$Replace[$key]).Replace('$', '$$'), 'IgnoreCase')
            }
            $text
        }
    }
}

function Edit-NSPADGpoBackupFile {
    # Rewrites one backup file through -Transform, keeping its encoding (Backup-GPO writes UTF-16
    # for some files) and its Hidden attribute (bkupInfo.xml). Untouched when nothing changes.
    param([Parameter(Mandatory)][IO.FileInfo]$File, [Parameter(Mandatory)][scriptblock]$Transform)
    $bytes = [IO.File]::ReadAllBytes($File.FullName)
    $enc = if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { [Text.Encoding]::Unicode }
           elseif ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { New-Object Text.UTF8Encoding($true) }
           else { New-Object Text.UTF8Encoding($false) }
    $reader = New-Object IO.StreamReader((New-Object IO.MemoryStream(, $bytes)), $enc, $true)
    try { $text = $reader.ReadToEnd() } finally { $reader.Dispose() }
    $new = [string](& $Transform $text)
    if ($new -cne $text) {
        $attrs = $File.Attributes
        if ($attrs -band [IO.FileAttributes]::Hidden) { $File.Attributes = $attrs -band (-bnot [IO.FileAttributes]::Hidden) }
        try { [IO.File]::WriteAllText($File.FullName, $new, $enc) } finally { $File.Attributes = $attrs }
    }
}