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.'