diff --git a/.github/workflows/powershell-analysis.yml b/.github/workflows/powershell-analysis.yml
index 0eb466b..074d5e3 100644
--- a/.github/workflows/powershell-analysis.yml
+++ b/.github/workflows/powershell-analysis.yml
@@ -55,3 +55,7 @@ jobs:
- name: Test operating system catalog history
shell: pwsh
run: ./Tests/Test-OSCatalogHistory.ps1
+
+ - name: Test Intel wireless catalog fallback
+ shell: pwsh
+ run: ./Tests/Test-IntelWirelessCatalogFallback.ps1
diff --git a/Scripts/Update-IntelWirelessCatalog.ps1 b/Scripts/Update-IntelWirelessCatalog.ps1
index fc75d17..d5bf069 100644
--- a/Scripts/Update-IntelWirelessCatalog.ps1
+++ b/Scripts/Update-IntelWirelessCatalog.ps1
@@ -383,6 +383,81 @@ function Get-IntelWirelessMicrosoftUpdateMetadata {
throw "Microsoft Update Catalog returned Intel networking updates, but none exposed a CAB download with a SHA256 hash."
}
+function Get-ExistingIntelCatalogMetadata {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Path
+ )
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return $null
+ }
+
+ [xml]$xml = Get-Content -LiteralPath $Path -Raw
+ $item = $xml.IntelCatalog.Items.Item
+ if (-not $item) {
+ return $null
+ }
+
+ $metadata = [ordered]@{
+ Version = [string]$item.version
+ FileName = [string]$item.fileName
+ ReleaseDate = [string]$item.releaseDate
+ Sha256 = [string]$item.hashSHA256
+ DownloadUrl = [string]$item.downloadUrl
+ UpdateId = [string]$xml.IntelCatalog.Metadata.updateId
+ Query = [string]$xml.IntelCatalog.Metadata.query
+ }
+
+ foreach ($value in $metadata.Values) {
+ if ([string]::IsNullOrWhiteSpace([string]$value)) {
+ return $null
+ }
+ }
+
+ return $metadata
+}
+
+function Get-IntelWirelessMetadataWithFallback {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string[]]$HardwareIds,
+
+ [Parameter(Mandatory = $true)]
+ [int]$TimeoutSeconds,
+
+ [Parameter()]
+ [string]$ExistingCatalogPath
+ )
+
+ try {
+ return [pscustomobject]@{
+ Metadata = Get-IntelWirelessMicrosoftUpdateMetadata -HardwareIds $HardwareIds -TimeoutSeconds $TimeoutSeconds
+ UsedExisting = $false
+ }
+ }
+ catch {
+ $refreshException = $_.Exception
+ try {
+ $existingMetadata = Get-ExistingIntelCatalogMetadata -Path $ExistingCatalogPath
+ }
+ catch {
+ Write-Warning ("Existing Intel wireless catalog is unusable and cannot be used as a fallback. {0}" -f $_.Exception.Message)
+ throw $refreshException
+ }
+
+ if (-not $existingMetadata) {
+ throw $refreshException
+ }
+
+ Write-Warning ("Failed to refresh Intel wireless metadata. Reusing the existing known-good catalog entry. {0}" -f $refreshException.Message)
+ return [pscustomobject]@{
+ Metadata = $existingMetadata
+ UsedExisting = $true
+ }
+ }
+}
+
function Write-IntelCatalogXml {
param(
[Parameter(Mandatory = $true)]
@@ -443,6 +518,42 @@ function Write-IntelCatalogXml {
}
}
+function Invoke-IntelWirelessCatalogUpdate {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$OutputPath,
+
+ [Parameter(Mandatory = $true)]
+ [string[]]$HardwareIds,
+
+ [Parameter(Mandatory = $true)]
+ [int]$TimeoutSeconds
+ )
+
+ $metadataResult = Get-IntelWirelessMetadataWithFallback `
+ -HardwareIds $HardwareIds `
+ -TimeoutSeconds $TimeoutSeconds `
+ -ExistingCatalogPath $OutputPath
+ $metadata = $metadataResult.Metadata
+
+ if (-not $metadataResult.UsedExisting) {
+ Write-IntelCatalogXml -Path $OutputPath -Metadata $metadata
+ }
+
+ return [pscustomobject]@{
+ OutputPath = $OutputPath
+ Version = $metadata.Version
+ FileName = $metadata.FileName
+ ReleaseDate = $metadata.ReleaseDate
+ Sha256 = $metadata.Sha256
+ DownloadUrl = $metadata.DownloadUrl
+ Source = 'MicrosoftUpdateCatalog'
+ UpdateId = $metadata.UpdateId
+ Query = $metadata.Query
+ UsedExisting = $metadataResult.UsedExisting
+ }
+}
+
#endregion Functions
#region Main Execution
@@ -452,19 +563,9 @@ if (-not (Test-Path -Path $WinPEOutputDirectory)) {
$null = New-Item -Path $WinPEOutputDirectory -ItemType Directory -Force
}
-$metadata = Get-IntelWirelessMicrosoftUpdateMetadata -HardwareIds $HardwareIds -TimeoutSeconds $TimeoutSeconds
-Write-IntelCatalogXml -Path $outputPath -Metadata $metadata
-
-[pscustomobject]@{
- OutputPath = $outputPath
- Version = $metadata.Version
- FileName = $metadata.FileName
- ReleaseDate = $metadata.ReleaseDate
- Sha256 = $metadata.Sha256
- DownloadUrl = $metadata.DownloadUrl
- Source = 'MicrosoftUpdateCatalog'
- UpdateId = $metadata.UpdateId
- Query = $metadata.Query
-}
+Invoke-IntelWirelessCatalogUpdate `
+ -OutputPath $outputPath `
+ -HardwareIds $HardwareIds `
+ -TimeoutSeconds $TimeoutSeconds
#endregion Main Execution
diff --git a/Tests/Test-IntelWirelessCatalogFallback.ps1 b/Tests/Test-IntelWirelessCatalogFallback.ps1
new file mode 100644
index 0000000..946ffe5
--- /dev/null
+++ b/Tests/Test-IntelWirelessCatalogFallback.ps1
@@ -0,0 +1,160 @@
+[CmdletBinding()]
+param()
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+function Assert-Equal {
+ param(
+ [Parameter(Mandatory = $true)]
+ [AllowNull()]
+ [object]$Expected,
+
+ [Parameter(Mandatory = $true)]
+ [AllowNull()]
+ [object]$Actual,
+
+ [Parameter(Mandatory = $true)]
+ [string]$Message
+ )
+
+ if ($Expected -ne $Actual) {
+ throw "$Message Expected '$Expected', got '$Actual'."
+ }
+}
+
+function Assert-Throws {
+ param(
+ [Parameter(Mandatory = $true)]
+ [scriptblock]$Action,
+
+ [Parameter(Mandatory = $true)]
+ [string]$ExpectedMessage,
+
+ [Parameter(Mandatory = $true)]
+ [string]$Message
+ )
+
+ try {
+ & $Action
+ }
+ catch {
+ Assert-Equal -Expected $ExpectedMessage -Actual $_.Exception.Message -Message $Message
+ return
+ }
+
+ throw "$Message Expected an exception."
+}
+
+$scriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\Scripts\Update-IntelWirelessCatalog.ps1'
+$tokens = $null
+$parseErrors = $null
+$ast = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$tokens, [ref]$parseErrors)
+if ($parseErrors.Count -gt 0) {
+ throw "Intel wireless catalog script has parser errors: $($parseErrors.Message -join '; ')"
+}
+
+$functionDefinitions = $ast.FindAll({
+ param($node)
+ $node -is [System.Management.Automation.Language.FunctionDefinitionAst]
+ }, $true)
+foreach ($functionDefinition in $functionDefinitions) {
+ . ([scriptblock]::Create($functionDefinition.Extent.Text))
+}
+
+$tempDirectory = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('foundry-intel-wireless-test-' + [guid]::NewGuid())
+try {
+ $null = New-Item -Path $tempDirectory -ItemType Directory -Force
+ $catalogPath = Join-Path -Path $tempDirectory -ChildPath 'WinPE_Intel.xml'
+ [System.IO.File]::WriteAllText($catalogPath, @'
+
+
+
+
+ -
+ intel-wireless-winre-x64
+ intel-wireless-winre-x64
+ Intel Wireless Wi-Fi Drivers from Microsoft Update Catalog
+ 24.40.0.4
+ intel-wireless.cab
+ https://download.windowsupdate.com/intel-wireless.cab
+ cab
+ WifiSupplement
+ IntelWireless
+ 2026-04-12
+ WinPE
+ 11
+ x64
+ 4365da6d3ad51e942d5c091372536d2163dc3ca17fa5e389feedcb29ad5e339f
+
+
+
+'@)
+
+ $existingMetadata = Get-ExistingIntelCatalogMetadata -Path $catalogPath
+ Assert-Equal -Expected '34cf8ffa-450f-4dcd-bc20-35590c6d5e17' -Actual $existingMetadata.UpdateId -Message 'Existing update ID was not preserved.'
+ Assert-Equal -Expected 'PCI\VEN_8086&DEV_7E40' -Actual $existingMetadata.Query -Message 'Existing hardware ID query was not preserved.'
+ Assert-Equal -Expected '24.40.0.4' -Actual $existingMetadata.Version -Message 'Existing version was not preserved.'
+
+ $malformedCatalogPath = Join-Path -Path $tempDirectory -ChildPath 'Malformed_WinPE_Intel.xml'
+ [System.IO.File]::WriteAllText($malformedCatalogPath, '')
+
+ function Get-IntelWirelessMicrosoftUpdateMetadata {
+ param(
+ [string[]]$HardwareIds,
+ [int]$TimeoutSeconds
+ )
+
+ return [ordered]@{
+ Version = '24.50.0.1'
+ FileName = 'refreshed-intel-wireless.cab'
+ ReleaseDate = '2026-08-22'
+ Sha256 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+ DownloadUrl = 'https://download.windowsupdate.com/refreshed-intel-wireless.cab'
+ UpdateId = 'a7b2b487-0b2c-4ea3-9f73-a3b1220d77df'
+ Query = $HardwareIds[0]
+ }
+ }
+
+ $refreshResult = Get-IntelWirelessMetadataWithFallback `
+ -HardwareIds @('PCI\VEN_8086&DEV_7E40') `
+ -TimeoutSeconds 45 `
+ -ExistingCatalogPath $malformedCatalogPath
+
+ Assert-Equal -Expected $false -Actual $refreshResult.UsedExisting -Message 'Healthy refresh incorrectly depended on the malformed existing catalog.'
+ Assert-Equal -Expected '24.50.0.1' -Actual $refreshResult.Metadata.Version -Message 'Healthy refresh metadata is incorrect.'
+
+ function Get-IntelWirelessMicrosoftUpdateMetadata {
+ throw 'Simulated Microsoft Update Catalog failure.'
+ }
+
+ $beforeHash = (Get-FileHash -LiteralPath $catalogPath -Algorithm SHA256).Hash
+ $beforeWriteTime = (Get-Item -LiteralPath $catalogPath).LastWriteTimeUtc
+ $fallbackResult = Invoke-IntelWirelessCatalogUpdate `
+ -OutputPath $catalogPath `
+ -HardwareIds @('PCI\VEN_8086&DEV_7E40') `
+ -TimeoutSeconds 45
+
+ Assert-Equal -Expected $true -Actual $fallbackResult.UsedExisting -Message 'Existing metadata was not selected after refresh failure.'
+ Assert-Equal -Expected '24.40.0.4' -Actual $fallbackResult.Version -Message 'Fallback metadata version is incorrect.'
+ Assert-Equal -Expected '34cf8ffa-450f-4dcd-bc20-35590c6d5e17' -Actual $fallbackResult.UpdateId -Message 'Fallback metadata update ID is incorrect.'
+ Assert-Equal -Expected $beforeHash -Actual (Get-FileHash -LiteralPath $catalogPath -Algorithm SHA256).Hash -Message 'Fallback rewrote the existing catalog content.'
+ Assert-Equal -Expected $beforeWriteTime -Actual (Get-Item -LiteralPath $catalogPath).LastWriteTimeUtc -Message 'Fallback rewrote the existing catalog file.'
+
+ Assert-Throws `
+ -Action {
+ Get-IntelWirelessMetadataWithFallback `
+ -HardwareIds @('PCI\VEN_8086&DEV_7E40') `
+ -TimeoutSeconds 45 `
+ -ExistingCatalogPath (Join-Path -Path $tempDirectory -ChildPath 'Missing_WinPE_Intel.xml')
+ } `
+ -ExpectedMessage 'Simulated Microsoft Update Catalog failure.' `
+ -Message 'Refresh failure without existing metadata was suppressed.'
+}
+finally {
+ if (Test-Path -LiteralPath $tempDirectory) {
+ Remove-Item -LiteralPath $tempDirectory -Recurse -Force
+ }
+}
+
+Write-Output 'Intel wireless catalog fallback tests passed.'