diff --git a/Get-BroadcomEthernetDrivers.ps1 b/Get-BroadcomEthernetDrivers.ps1 new file mode 100644 index 0000000..05e023c --- /dev/null +++ b/Get-BroadcomEthernetDrivers.ps1 @@ -0,0 +1,25 @@ +#Requires -Version 7.0 +<# + .SYNOPSIS + Fetches the latest Broadcom NetXtreme-E Ethernet drivers from the Microsoft Update Catalog. + + .DESCRIPTION + Thin shim over catalogscrape\CatalogScrape.psm1, driven by catalogscrape\broadcom.psd1. + Covers the modern NetXtreme-E 10/25GbE controller (BCM57416, VEN_14E4&DEV_16D8), which the + other scrapers and build_win11pxe.ps1's in-box Broadcom services do NOT cover. Ported from + the gonefishin branch. Selection is highest-version-first. + + .NOTES + Verify BCM57416 / DEV_16D8 is actually in your hardware scope before relying on this. +#> +[CmdletBinding()] +param( + [switch]$Install, + [string]$DownloadPath = 'C:\Temp', + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64' +) + +$csDir = Join-Path $PSScriptRoot 'catalogscrape' +Import-Module (Join-Path $csDir 'CatalogScrape.psm1') -Force +$config = Import-PowerShellDataFile (Join-Path $csDir 'broadcom.psd1') +Invoke-DriverScrape -Config $config -Install:$Install -DownloadPath $DownloadPath -Architecture $Architecture diff --git a/Get-IntelEthernetDrivers.ps1 b/Get-IntelEthernetDrivers.ps1 index 7f0451e..a9b8ce2 100644 --- a/Get-IntelEthernetDrivers.ps1 +++ b/Get-IntelEthernetDrivers.ps1 @@ -4,306 +4,24 @@ Fetches the latest Intel Ethernet drivers from the Microsoft Update Catalog. .DESCRIPTION - Scrapes the Microsoft Update Catalog for specific Intel Ethernet hardware families - (e.g., I225-V, I219-V, X540, X550, X710, E810) to download targeted CAB packages - rather than downloading the monolithic multi-gigabyte Intel ZIP, ensuring a clean, - offline-ready set of INFs for DISM injection without downloading hundreds of CABs. + Thin shim over catalogscrape\CatalogScrape.psm1, driven by catalogscrape\intel-eth.psd1. + Targets specific Intel Ethernet families (I225/I226, I219/I210, X540/X550/X710, E810, IAVF) + and downloads the targeted CAB packages for DISM/pnputil injection rather than the + monolithic Intel ZIP. Selection is highest-version-first (catalog date as tiebreak). + + .EXAMPLE + .\Get-IntelEthernetDrivers.ps1 + .EXAMPLE + .\Get-IntelEthernetDrivers.ps1 -Install #> - [CmdletBinding()] -param ( +param( [switch]$Install, - [string]$DownloadPath = 'C:\Temp\Intel_Ethernet', - - [ValidateSet('x64','arm64','all')] - [string]$Architecture = 'x64' -) - -$AcceptedArchs = switch ($Architecture) { - 'x64' { @('AMD64') } - 'arm64' { @('ARM64') } - 'all' { @('AMD64','ARM64') } -} - -if ($Install -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error "The -Install flag requires Administrator privileges. Please run PowerShell as Administrator." - return -} - -# ============================================================ -# Helper: retry-with-backoff wrapper around Invoke-WebRequest -# ============================================================ -# Defined locally so this script stays independently runnable. The Update Catalog -# throttles aggressively; retry up to 3 times with increasing delay, then re-throw so -# callers can try/catch and skip. NOTE: not available inside ForEach-Object -Parallel -# runspaces (those carry their own inline retry loop). -function Invoke-CatalogRequest { - param ( - [Parameter(Mandatory)][hashtable]$Params, - [int]$MaxAttempts = 3 - ) - for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { - try { - return Invoke-WebRequest @Params - } - catch { - if ($attempt -ge $MaxAttempts) { throw } - Start-Sleep -Seconds ($attempt * 2) - } - } -} - -$Targets = @( - @{ - Name = "Intel_2.5G_Family" - Devices = @( - @{ Prefix = "I225-V"; HWID = "VEN_8086&DEV_15F3"; FamilyName = "I225" }, - @{ Prefix = "I226-V"; HWID = "VEN_8086&DEV_125C"; FamilyName = "I226" } - ) - }, - @{ - Name = "Intel_1G_Family" - Devices = @( - # Intel's e1d driver is unified: the INF inside any single package covers ALL - # I219-V/LM generations (DEV_15B8, 15FA, 0DC8, 0D4F, 15D8, 15B3, etc.). - # Use DEV_15B8 (gen-2, most widely listed on the catalog) as the representative. - @{ Prefix = "I219-V"; HWID = "VEN_8086&DEV_15B8"; FamilyName = "I219" }, - @{ Prefix = "I210"; HWID = "VEN_8086&DEV_1533"; FamilyName = "I210" } - ) - }, - @{ - Name = "Intel_10G_Family" - Devices = @( - @{ Prefix = "X540"; HWID = "VEN_8086&DEV_1528"; FamilyName = "X540" }, - @{ Prefix = "X550"; HWID = "VEN_8086&DEV_1563"; FamilyName = "X550" }, - @{ Prefix = "X710"; HWID = "VEN_8086&DEV_1572"; FamilyName = "X710" } - ) - }, - @{ - Name = "Intel_100G_Family" - Devices = @( - @{ Prefix = "E810"; HWID = "VEN_8086&DEV_1592"; FamilyName = "E810" } - ) - }, - @{ - Name = "Intel_AVF_Family" - Devices = @( - @{ Prefix = "IAVF"; HWID = "VEN_8086&DEV_1889"; FamilyName = "IAVF" } - ) - } + [string]$DownloadPath = 'C:\Temp', + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64' ) -if (-not (Test-Path $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } - -# Acquisition manifest — greppable ACQUIRED:/SKIPPED: lines emitted at the end so a -# throttled/partial parallel run is auditable. -$Manifest = [System.Collections.Generic.List[string]]::new() - -foreach ($Target in $Targets) { - Write-Host "`n=> Investigating Microsoft Update Catalog for $($Target.Name)..." -ForegroundColor Cyan - - $AvailablePackages = @() - - foreach ($Device in $Target.Devices) { - $Prefix = $Device.Prefix - $HWID = $Device.HWID - $FamilyName = $Device.FamilyName - $Query = "$HWID Windows 11" - Write-Host " -> Searching specific HWID for Prefix $Prefix ($Query)..." - - # Reset per-iteration so a failed fetch can't silently reuse the PREVIOUS - # device's page (which would parse the wrong update IDs for this device). - $SearchPage = $null - try { - $SearchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$([uri]::EscapeDataString($Query))" - $SearchPage = Invoke-CatalogRequest -Params @{ Uri = $SearchUrl; UseBasicParsing = $true } - } - catch { - Write-Warning "Search request failed for $Prefix ($Query): $_" - $Manifest.Add("SKIPPED: $FamilyName (search request failed)") - continue - } - - # LIMITATION: Search.aspx returns only the first 25 relevance-sorted rows; the - # "newest by date" pick below only sees those 25. Full pagination needs - # __EVENTTARGET POST-backs (invasive). Parse the "1 - N of M" total and warn if M > 25. - if ($SearchPage.Content -match '(\d+)\s*-\s*(\d+)\s+of\s+(\d+)') { - $totalResults = [int]$matches[3] - if ($totalResults -gt 25) { - Write-Warning "Catalog reports $totalResults results for '$Query' but only the first 25 are parsed (pagination not implemented)." - } - } - - $UpdateIds = [regex]::Matches($SearchPage.Content, "goToDetails\(['""]([a-f0-9\-]+)['""]\)") | - ForEach-Object { $_.Groups[1].Value } | - Select-Object -Unique - - if (-not $UpdateIds) { - Write-Host " [!] No candidates found." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $FamilyName (no catalog candidates)") - continue - } - - Write-Host " -> Found $($UpdateIds.Count) packages. Fetching deep versions..." -NoNewline - - # Parallel detail page fetches — ~10x faster than sequential - $DetailResults = $UpdateIds | ForEach-Object -Parallel { - $Id = $_ - $DetailsUrl = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$Id" - try { - # Inline retry-with-backoff (the script-scope helper is not visible here). - $DetailsPage = $null - for ($attempt = 1; $attempt -le 3; $attempt++) { - try { $DetailsPage = Invoke-WebRequest -Uri $DetailsUrl -UseBasicParsing; break } - catch { if ($attempt -ge 3) { throw } else { Start-Sleep -Seconds ($attempt * 2) } } - } - $DateString = if ($DetailsPage.Content -match 'id="ScopedViewHandler_versionDate">([^<]+)') { $matches[1].Trim() } - $Version = if ($DetailsPage.Content -match 'id="ScopedViewHandler_version">([^<]+)') { $matches[1].Trim() } - - if ($Version -and $DateString) { - $DateObj = [datetime]::Parse($DateString) - $Arch = if ($DetailsPage.Content -match "ARM64") { "ARM64" } elseif ($DetailsPage.Content -match "AMD64|x64|amd64") { "AMD64" } else { "x86" } - [PSCustomObject]@{ - Version = $Version - DateObj = $DateObj - Id = $Id - Arch = $Arch - } - } - } - catch { - Write-Warning "Detail fetch/parse failed for update $Id : $($_.Exception.Message)" - } - } -ThrottleLimit 8 - - Write-Host " Done." - - foreach ($result in $DetailResults) { - if ($result -and $result.Arch -in $AcceptedArchs) { - $AvailablePackages += [PSCustomObject]@{ - Prefix = $Prefix - FamilyName = $FamilyName - Version = $result.Version - DateObj = $result.DateObj - Id = $result.Id - Arch = $result.Arch - } - } - } - } - - if (-not $AvailablePackages) { - Write-Host " [!] No matching prefixes found within candidate packages." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $($Target.Name) (no packages matched accepted architectures)") - continue - } - - $GroupedPackages = $AvailablePackages | Group-Object Prefix, Arch - - foreach ($Group in $GroupedPackages) { - $FirstObj = $Group.Group[0] - $Prefix = $FirstObj.Prefix - $Arch = $FirstObj.Arch - $FamilyName = $FirstObj.FamilyName - - $BestPackage = $Group.Group | Sort-Object DateObj -Descending | Select-Object -First 1 - Write-Host " -> Prefix $($Prefix) [$Arch]: Selected $($BestPackage.Version) (Update ID: $($BestPackage.Id))" -ForegroundColor Green - - $DownloadPage = $null - $PostData = "[{`"size`":0,`"updateID`":`"$($BestPackage.Id)`",`"uidInfo`":`"$($BestPackage.Id)`"}]" - try { - $DownloadPage = Invoke-CatalogRequest -Params @{ Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx"; Method = 'Post'; Body = @{updateIDs = $PostData }; UseBasicParsing = $true } - } - catch { - Write-Warning "Download dialog request failed for $FamilyName [$Arch]: $_" - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (download dialog failed)") - continue - } - - $CabUrl = [regex]::Match($DownloadPage.Content, 'https://[^''\"<]+\.cab').Value - - if (-not $CabUrl) { - Write-Host " [!] Could not extract .cab URL from payload." -ForegroundColor Red - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (no .cab URL in payload)") - continue - } - - $CabFile = Join-Path $DownloadPath "$($Target.Name)_$($Prefix)_$($Arch).cab" - $ExtractDir = Join-Path $DownloadPath "$($Target.Name)\$FamilyName\$Arch" - - Write-Host " -> Downloading raw $Arch driver package..." - try { - Invoke-CatalogRequest -Params @{ Uri = $CabUrl; OutFile = $CabFile; UseBasicParsing = $true } | Out-Null - } - catch { - Write-Warning "CAB download failed for $FamilyName [$Arch]: $_" - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (download failed)") - continue - } - - # AUTHENTICITY: this CAB is extracted into boot-start kernel drivers. Verify the - # publisher's Authenticode signature and SKIP (do not extract/inject) anything - # that is not 'Valid', rather than silently trusting it. - $sig = Get-AuthenticodeSignature -FilePath $CabFile - if ($sig.Status -ne 'Valid') { - Write-Warning "Authenticode signature for $FamilyName [$Arch] CAB is '$($sig.Status)' (not Valid) — skipping extraction/injection." - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (signature $($sig.Status))") - Remove-Item $CabFile -Force -ErrorAction SilentlyContinue - continue - } - - Write-Host " -> Extracting payload using expand.exe..." - if (-not (Test-Path $ExtractDir)) { New-Item -ItemType Directory -Path $ExtractDir -Force | Out-Null } - - # Capture expand.exe's exit code. On failure KEEP the source CAB for retry/inspection. - $expandProc = Start-Process "expand.exe" -ArgumentList "-F:* `"$CabFile`" `"$ExtractDir`"" -NoNewWindow -PassThru - $expandProc.WaitForExit() - if ($expandProc.ExitCode -ne 0) { - Write-Warning "expand.exe exited with code $($expandProc.ExitCode) for $FamilyName [$Arch] — keeping source CAB '$CabFile' for retry/inspection." - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (expand.exe exit $($expandProc.ExitCode))") - continue - } - Remove-Item $CabFile -Force - - # Manifest: confirm an actual .inf + .sys landed (an empty/partial extract is a skip). - $infCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.inf -ErrorAction SilentlyContinue).Count - $sysCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.sys -ErrorAction SilentlyContinue).Count - if ($infCount -gt 0 -and $sysCount -gt 0) { - $Manifest.Add("ACQUIRED: $FamilyName [$Arch]") - } - else { - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (no .inf/.sys after extract)") - } - - if ($Install) { - $SysArch = $env:PROCESSOR_ARCHITECTURE - if ($SysArch -eq $Arch) { - Write-Host " -> System is $SysArch. Injecting $Arch driver into Driver Store via pnputil..." -ForegroundColor Green - pnputil.exe /add-driver "$ExtractDir\*.inf" /install | Out-Null - Write-Host " -> Injection complete." -ForegroundColor Green - } - else { - Write-Host " -> System is $SysArch. Skipping $Arch driver installation." -ForegroundColor DarkGray - } - } - else { - Write-Host " -> Extracted to: $ExtractDir (Skipping installation)" -ForegroundColor DarkGray - } - } -} - -# ============================================================ -# Acquisition manifest (greppable) -# ============================================================ -Write-Host "`n=> Acquisition manifest:" -ForegroundColor Cyan -if ($Manifest.Count -eq 0) { - Write-Host " (no device families processed)" -ForegroundColor DarkGray -} -else { - foreach ($line in $Manifest) { - if ($line -like 'ACQUIRED:*') { Write-Host " $line" -ForegroundColor Green } - else { Write-Host " $line" -ForegroundColor Yellow } - } -} - -Write-Host "`nProcess complete." -ForegroundColor Cyan +$csDir = Join-Path $PSScriptRoot 'catalogscrape' +Import-Module (Join-Path $csDir 'CatalogScrape.psm1') -Force +$config = Import-PowerShellDataFile (Join-Path $csDir 'intel-eth.psd1') +Invoke-DriverScrape -Config $config -Install:$Install -DownloadPath $DownloadPath -Architecture $Architecture diff --git a/Get-IntelWiFiDrivers.ps1 b/Get-IntelWiFiDrivers.ps1 index a914759..601c8dd 100644 --- a/Get-IntelWiFiDrivers.ps1 +++ b/Get-IntelWiFiDrivers.ps1 @@ -4,287 +4,22 @@ Fetches the latest Intel PROSet/Wireless Wi-Fi drivers from the Microsoft Update Catalog. .DESCRIPTION - Scrapes the Microsoft Update Catalog for Intel Wi-Fi hardware families. - Intel bundles all supported adapters into unified driver packages, so two - searches cover everything from AC 9260 through BE200: - - BE200 package → BE200, AX411, AX211, AX210 (WiFi 7 / 6E) - - AX200 package → AX200, AX201, AC 9560, AC 9462, AC 9260 (WiFi 6 / AC) + Thin shim over catalogscrape\CatalogScrape.psm1, driven by catalogscrape\intel-wifi.psd1. + Intel bundles all supported adapters into unified packages, so two searches (BE200, AX200) + cover everything from AC 9260 through BE200. Selection is highest-version-first. .NOTES - SCOPE: These are POST-BOOT CONVENIENCE drivers only. Wi-Fi adapters cannot serve - iSCSI / network (PXE) boot, so nothing here is boot-critical — it only restores - wireless connectivity after the OS is already running. + SCOPE: POST-BOOT CONVENIENCE drivers only. Wi-Fi cannot serve iSCSI/PXE boot, so nothing + here is boot-critical — it only restores wireless connectivity after the OS is running. #> - [CmdletBinding()] -param ( +param( [switch]$Install, - [string]$DownloadPath = 'C:\Temp\Intel_WiFi', - - [ValidateSet('x64','arm64','all')] - [string]$Architecture = 'x64' -) - -$AcceptedArchs = switch ($Architecture) { - 'x64' { @('AMD64') } - 'arm64' { @('ARM64') } - 'all' { @('AMD64','ARM64') } -} - -if ($Install -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error "The -Install flag requires Administrator privileges. Please run PowerShell as Administrator." - return -} - -# ============================================================ -# Helper: retry-with-backoff wrapper around Invoke-WebRequest -# ============================================================ -# Defined locally so this script stays independently runnable. The Update Catalog -# throttles aggressively; retry up to 3 times with increasing delay, then re-throw so -# callers can try/catch and skip. NOTE: not available inside ForEach-Object -Parallel -# runspaces (those carry their own inline retry loop). -function Invoke-CatalogRequest { - param ( - [Parameter(Mandatory)][hashtable]$Params, - [int]$MaxAttempts = 3 - ) - for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { - try { - return Invoke-WebRequest @Params - } - catch { - if ($attempt -ge $MaxAttempts) { throw } - Start-Sleep -Seconds ($attempt * 2) - } - } -} - -$Targets = @( - @{ - Name = "Intel_WiFi7_Family" - Devices = @( - @{ Prefix = "BE200"; HWID = "VEN_8086&DEV_272B"; FamilyName = "BE200" } - ) - }, - @{ - Name = "Intel_WiFi6_Family" - Devices = @( - @{ Prefix = "AX200"; HWID = "VEN_8086&DEV_2723"; FamilyName = "AX200" } - ) - } + [string]$DownloadPath = 'C:\Temp', + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64' ) -if (-not (Test-Path $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } - -# Acquisition manifest — greppable ACQUIRED:/SKIPPED: lines emitted at the end so a -# throttled/partial parallel run is auditable. -$Manifest = [System.Collections.Generic.List[string]]::new() - -foreach ($Target in $Targets) { - Write-Host "`n=> Investigating Microsoft Update Catalog for $($Target.Name)..." -ForegroundColor Cyan - - $AvailablePackages = @() - - foreach ($Device in $Target.Devices) { - $Prefix = $Device.Prefix - $HWID = $Device.HWID - $FamilyName = $Device.FamilyName - $Query = "$HWID Windows 11" - Write-Host " -> Searching specific HWID for Prefix $Prefix ($Query)..." - - # Reset per-iteration so a failed fetch can't silently reuse the PREVIOUS - # device's page (which would parse the wrong update IDs for this device). - $SearchPage = $null - try { - $SearchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$([uri]::EscapeDataString($Query))" - $SearchPage = Invoke-CatalogRequest -Params @{ Uri = $SearchUrl; UseBasicParsing = $true } - } - catch { - Write-Warning "Search request failed for $Prefix ($Query): $_" - $Manifest.Add("SKIPPED: $FamilyName (search request failed)") - continue - } - - # LIMITATION: Search.aspx returns only the first 25 relevance-sorted rows; the - # "newest by date" pick below only sees those 25. Full pagination needs - # __EVENTTARGET POST-backs (invasive). Parse the "1 - N of M" total and warn if M > 25. - if ($SearchPage.Content -match '(\d+)\s*-\s*(\d+)\s+of\s+(\d+)') { - $totalResults = [int]$matches[3] - if ($totalResults -gt 25) { - Write-Warning "Catalog reports $totalResults results for '$Query' but only the first 25 are parsed (pagination not implemented)." - } - } - - $UpdateIds = [regex]::Matches($SearchPage.Content, "goToDetails\(['\`"]([a-f0-9\-]+)['\`"]\)") | - ForEach-Object { $_.Groups[1].Value } | - Select-Object -Unique - - if (-not $UpdateIds) { - Write-Host " [!] No candidates found." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $FamilyName (no catalog candidates)") - continue - } - - Write-Host " -> Found $($UpdateIds.Count) packages. Fetching deep versions..." -NoNewline - - $DetailResults = $UpdateIds | ForEach-Object -Parallel { - $Id = $_ - $DetailsUrl = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$Id" - try { - # Inline retry-with-backoff (the script-scope helper is not visible here). - $DetailsPage = $null - for ($attempt = 1; $attempt -le 3; $attempt++) { - try { $DetailsPage = Invoke-WebRequest -Uri $DetailsUrl -UseBasicParsing; break } - catch { if ($attempt -ge 3) { throw } else { Start-Sleep -Seconds ($attempt * 2) } } - } - $DateString = if ($DetailsPage.Content -match 'id="ScopedViewHandler_versionDate">([^<]+)') { $matches[1].Trim() } - $Version = if ($DetailsPage.Content -match 'id="ScopedViewHandler_version">([^<]+)') { $matches[1].Trim() } - - if ($Version -and $DateString) { - $DateObj = [datetime]::Parse($DateString) - $Arch = if ($DetailsPage.Content -match "ARM64") { "ARM64" } - elseif ($DetailsPage.Content -match "AMD64|x64|amd64") { "AMD64" } - else { "x86" } - [PSCustomObject]@{ - Version = $Version - DateObj = $DateObj - Id = $Id - Arch = $Arch - } - } - } - catch { - Write-Warning "Detail fetch/parse failed for update $Id : $($_.Exception.Message)" - } - } -ThrottleLimit 8 - - Write-Host " Done." - - foreach ($result in $DetailResults) { - if ($result -and $result.Arch -in $AcceptedArchs) { - $AvailablePackages += [PSCustomObject]@{ - Prefix = $Prefix - FamilyName = $FamilyName - Version = $result.Version - DateObj = $result.DateObj - Id = $result.Id - Arch = $result.Arch - } - } - } - } - - if (-not $AvailablePackages) { - Write-Host " [!] No matching prefixes found within candidate packages." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $($Target.Name) (no packages matched accepted architectures)") - continue - } - - $GroupedPackages = $AvailablePackages | Group-Object Prefix, Arch - - foreach ($Group in $GroupedPackages) { - $FirstObj = $Group.Group[0] - $Prefix = $FirstObj.Prefix - $Arch = $FirstObj.Arch - $FamilyName = $FirstObj.FamilyName - - $BestPackage = $Group.Group | Sort-Object DateObj -Descending | Select-Object -First 1 - Write-Host " -> Prefix $($Prefix) [$Arch]: Selected $($BestPackage.Version) (Update ID: $($BestPackage.Id))" -ForegroundColor Green - - $DownloadPage = $null - $PostData = "[{`"size`":0,`"updateID`":`"$($BestPackage.Id)`",`"uidInfo`":`"$($BestPackage.Id)`"}]" - try { - $DownloadPage = Invoke-CatalogRequest -Params @{ Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx"; Method = 'Post'; Body = @{updateIDs = $PostData }; UseBasicParsing = $true } - } - catch { - Write-Warning "Download dialog request failed for $FamilyName [$Arch]: $_" - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (download dialog failed)") - continue - } - - $CabUrl = [regex]::Match($DownloadPage.Content, 'https://[^''\\"<]+\.cab').Value - - if (-not $CabUrl) { - Write-Host " [!] Could not extract .cab URL from payload." -ForegroundColor Red - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (no .cab URL in payload)") - continue - } - - $CabFile = Join-Path $DownloadPath "$($Target.Name)_$($Prefix)_$($Arch).cab" - $ExtractDir = Join-Path $DownloadPath "$($Target.Name)\$FamilyName\$Arch" - - Write-Host " -> Downloading raw $Arch driver package..." - try { - Invoke-CatalogRequest -Params @{ Uri = $CabUrl; OutFile = $CabFile; UseBasicParsing = $true } | Out-Null - } - catch { - Write-Warning "CAB download failed for $FamilyName [$Arch]: $_" - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (download failed)") - continue - } - - # AUTHENTICITY: verify the publisher's Authenticode signature and SKIP (do not - # extract/inject) anything that is not 'Valid', rather than silently trusting it. - $sig = Get-AuthenticodeSignature -FilePath $CabFile - if ($sig.Status -ne 'Valid') { - Write-Warning "Authenticode signature for $FamilyName [$Arch] CAB is '$($sig.Status)' (not Valid) — skipping extraction/injection." - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (signature $($sig.Status))") - Remove-Item $CabFile -Force -ErrorAction SilentlyContinue - continue - } - - Write-Host " -> Extracting payload using expand.exe..." - if (-not (Test-Path $ExtractDir)) { New-Item -ItemType Directory -Path $ExtractDir -Force | Out-Null } - - # Capture expand.exe's exit code. On failure KEEP the source CAB for retry/inspection. - $expandProc = Start-Process "expand.exe" -ArgumentList "-F:* `"$CabFile`" `"$ExtractDir`"" -NoNewWindow -PassThru - $expandProc.WaitForExit() - if ($expandProc.ExitCode -ne 0) { - Write-Warning "expand.exe exited with code $($expandProc.ExitCode) for $FamilyName [$Arch] — keeping source CAB '$CabFile' for retry/inspection." - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (expand.exe exit $($expandProc.ExitCode))") - continue - } - Remove-Item $CabFile -Force - - # Manifest: confirm an actual .inf + .sys landed (an empty/partial extract is a skip). - $infCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.inf -ErrorAction SilentlyContinue).Count - $sysCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.sys -ErrorAction SilentlyContinue).Count - if ($infCount -gt 0 -and $sysCount -gt 0) { - $Manifest.Add("ACQUIRED: $FamilyName [$Arch]") - } - else { - $Manifest.Add("SKIPPED: $FamilyName [$Arch] (no .inf/.sys after extract)") - } - - if ($Install) { - $SysArch = $env:PROCESSOR_ARCHITECTURE - if ($SysArch -eq $Arch) { - Write-Host " -> System is $SysArch. Injecting $Arch driver into Driver Store via pnputil..." -ForegroundColor Green - pnputil.exe /add-driver "$ExtractDir\*.inf" /install | Out-Null - Write-Host " -> Injection complete." -ForegroundColor Green - } - else { - Write-Host " -> System is $SysArch. Skipping $Arch driver installation." -ForegroundColor DarkGray - } - } - else { - Write-Host " -> Extracted to: $ExtractDir (Skipping installation)" -ForegroundColor DarkGray - } - } -} - -# ============================================================ -# Acquisition manifest (greppable) -# ============================================================ -Write-Host "`n=> Acquisition manifest:" -ForegroundColor Cyan -if ($Manifest.Count -eq 0) { - Write-Host " (no device families processed)" -ForegroundColor DarkGray -} -else { - foreach ($line in $Manifest) { - if ($line -like 'ACQUIRED:*') { Write-Host " $line" -ForegroundColor Green } - else { Write-Host " $line" -ForegroundColor Yellow } - } -} - -Write-Host "`nProcess complete." -ForegroundColor Cyan +$csDir = Join-Path $PSScriptRoot 'catalogscrape' +Import-Module (Join-Path $csDir 'CatalogScrape.psm1') -Force +$config = Import-PowerShellDataFile (Join-Path $csDir 'intel-wifi.psd1') +Invoke-DriverScrape -Config $config -Install:$Install -DownloadPath $DownloadPath -Architecture $Architecture diff --git a/Get-MarvellEthernetDrivers.ps1 b/Get-MarvellEthernetDrivers.ps1 index 38f7dde..e160b03 100644 --- a/Get-MarvellEthernetDrivers.ps1 +++ b/Get-MarvellEthernetDrivers.ps1 @@ -1,292 +1,27 @@ #Requires -Version 7.0 <# .SYNOPSIS - Fetches the latest Marvell/Aquantia 10GbE and 5GbE drivers from the Microsoft Update Catalog. + Fetches the latest Marvell/Aquantia 10GbE/5GbE drivers from the Microsoft Update Catalog. .DESCRIPTION - Scrapes the Microsoft Update Catalog for specific Aquantia/Marvell hardware IDs (AQC107, AQC113, AQC111U) - and downloads the latest driver CAB packages. + Thin shim over catalogscrape\CatalogScrape.psm1, driven by catalogscrape\marvell.psd1. + Covers Marvell's two NIC lines: Aquantia/AQtion PCIe (AQC107, AQC113) + USB AQC111U + (VID_2ECA&PID_C101), and QLogic-origin FastLinQ 41xxx 10GBASE-T (QL41162/QL41164). + Selection is highest-version-first (catalog date as tiebreak). + + .EXAMPLE + .\Get-MarvellEthernetDrivers.ps1 + .EXAMPLE + .\Get-MarvellEthernetDrivers.ps1 -Install #> - [CmdletBinding()] -param ( +param( [switch]$Install, - [string]$DownloadPath = 'C:\Temp\Marvell_Ethernet', - - [ValidateSet('x64','arm64','all')] - [string]$Architecture = 'x64' -) - -$AcceptedArchs = switch ($Architecture) { - 'x64' { @('AMD64') } - 'arm64' { @('ARM64') } - 'all' { @('AMD64','ARM64') } -} - -if ($Install -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error "The -Install flag requires Administrator privileges. Please run PowerShell as Administrator." - return -} - -# ============================================================ -# Helper: retry-with-backoff wrapper around Invoke-WebRequest -# ============================================================ -# Defined locally so this script stays independently runnable. The Update Catalog -# throttles aggressively; retry up to 3 times with increasing delay, then re-throw so -# callers can try/catch and skip. NOTE: not available inside ForEach-Object -Parallel -# runspaces (those carry their own inline retry loop). -function Invoke-CatalogRequest { - param ( - [Parameter(Mandatory)][hashtable]$Params, - [int]$MaxAttempts = 3 - ) - for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { - try { - return Invoke-WebRequest @Params - } - catch { - if ($attempt -ge $MaxAttempts) { throw } - Start-Sleep -Seconds ($attempt * 2) - } - } -} - -$Targets = @( - @{ - Name = "Marvell_Aquantia_PCIe" - Devices = @( - @{ Prefix = "AQC107"; HWID = "VEN_1D6A&DEV_D107"; FamilyName = "AQC107" }, - @{ Prefix = "AQC113"; HWID = "VEN_1D6A&DEV_04C0"; FamilyName = "AQC113" } - ) - }, - @{ - Name = "Marvell_Aquantia_USB" - Devices = @( - # DEFERRED: AQC111U USB HWIDs (VID_1D6A...) return no catalog results — 1D6A is Aquantia's PCI vendor ID, not USB. Real first-party adapter is USB\VID_2ECA&PID_C101; TRENDnet/ASIX HWIDs need verification. Pending hardware-ID verification before correction. - # - # Aquantia's own VID covers most 1st-party and reference-design adapters. - # OEM adapters (TRENDnet VID_20F4, ASIX VID_0B95) may use different VIDs - # but the same underlying Aquantia driver, so we search multiple. - @{ Prefix = "AQC111U"; HWID = "VID_1D6A&PID_D111"; FamilyName = "AQC111U" }, - @{ Prefix = "AQC111U-TRENDnet"; HWID = "VID_20F4&PID_E05A"; FamilyName = "AQC111U" }, - @{ Prefix = "AQC111U-ASIX"; HWID = "VID_0B95&PID_2790"; FamilyName = "AQC111U" } - ) - } + [string]$DownloadPath = 'C:\Temp', + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64' ) -if (-not (Test-Path $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } - -# Acquisition manifest — greppable ACQUIRED:/SKIPPED: lines emitted at the end so a -# throttled/partial parallel run is auditable. -$Manifest = [System.Collections.Generic.List[string]]::new() - -foreach ($Target in $Targets) { - Write-Host "`n=> Investigating Microsoft Update Catalog for $($Target.Name)..." -ForegroundColor Cyan - - $AvailablePackages = @() - - foreach ($Device in $Target.Devices) { - $Prefix = $Device.Prefix - $HWID = $Device.HWID - $FamilyName = $Device.FamilyName - $Query = "$HWID Windows 11" - Write-Host " -> Searching specific HWID for Prefix $Prefix ($Query)..." - - # Reset per-iteration so a failed fetch can't silently reuse the PREVIOUS - # device's page (which would parse the wrong update IDs for this device). - $SearchPage = $null - try { - $SearchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$([uri]::EscapeDataString($Query))" - $SearchPage = Invoke-CatalogRequest -Params @{ Uri = $SearchUrl; UseBasicParsing = $true } - } - catch { - Write-Warning "Search request failed for $Prefix ($Query): $_" - $Manifest.Add("SKIPPED: $Prefix (search request failed)") - continue - } - - # LIMITATION: Search.aspx returns only the first 25 relevance-sorted rows; the - # "newest by date" pick below only sees those 25. Full pagination needs - # __EVENTTARGET POST-backs (invasive). Parse the "1 - N of M" total and warn if M > 25. - if ($SearchPage.Content -match '(\d+)\s*-\s*(\d+)\s+of\s+(\d+)') { - $totalResults = [int]$matches[3] - if ($totalResults -gt 25) { - Write-Warning "Catalog reports $totalResults results for '$Query' but only the first 25 are parsed (pagination not implemented)." - } - } - - $UpdateIds = [regex]::Matches($SearchPage.Content, "goToDetails\(['""]([a-f0-9\-]+)['""]\)") | - ForEach-Object { $_.Groups[1].Value } | - Select-Object -Unique - - if (-not $UpdateIds) { - Write-Host " [!] No candidates found." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $Prefix (no catalog candidates)") - continue - } - - Write-Host " -> Found $($UpdateIds.Count) packages. Fetching deep versions..." -NoNewline - - $DetailResults = $UpdateIds | ForEach-Object -Parallel { - $Id = $_ - $DetailsUrl = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$Id" - try { - # Inline retry-with-backoff (the script-scope helper is not visible here). - $DetailsPage = $null - for ($attempt = 1; $attempt -le 3; $attempt++) { - try { $DetailsPage = Invoke-WebRequest -Uri $DetailsUrl -UseBasicParsing; break } - catch { if ($attempt -ge 3) { throw } else { Start-Sleep -Seconds ($attempt * 2) } } - } - $DateString = if ($DetailsPage.Content -match 'id="ScopedViewHandler_versionDate">([^<]+)') { $matches[1].Trim() } - $Version = if ($DetailsPage.Content -match 'id="ScopedViewHandler_version">([^<]+)') { $matches[1].Trim() } - - if ($Version -and $DateString) { - $DateObj = [datetime]::Parse($DateString) - $Arch = if ($DetailsPage.Content -match "ARM64") { "ARM64" } elseif ($DetailsPage.Content -match "AMD64|x64|amd64") { "AMD64" } else { "x86" } - [PSCustomObject]@{ - Version = $Version - DateObj = $DateObj - Id = $Id - Arch = $Arch - } - } - } - catch { - Write-Warning "Detail fetch/parse failed for update $Id : $($_.Exception.Message)" - } - } -ThrottleLimit 8 - - Write-Host " Done." - - foreach ($result in $DetailResults) { - if ($result -and $result.Arch -in $AcceptedArchs) { - $AvailablePackages += [PSCustomObject]@{ - Prefix = $Prefix - FamilyName = $FamilyName - Version = $result.Version - DateObj = $result.DateObj - Id = $result.Id - Arch = $result.Arch - } - } - } - } - - if (-not $AvailablePackages) { - Write-Host " [!] No matching prefixes found within candidate packages." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $($Target.Name) (no packages matched accepted architectures)") - continue - } - - $GroupedPackages = $AvailablePackages | Group-Object Prefix, Arch - - foreach ($Group in $GroupedPackages) { - $FirstObj = $Group.Group[0] - $Prefix = $FirstObj.Prefix - $Arch = $FirstObj.Arch - $FamilyName = $FirstObj.FamilyName - - $BestPackage = $Group.Group | Sort-Object DateObj -Descending | Select-Object -First 1 - Write-Host " -> Prefix $($Prefix) [$Arch]: Selected $($BestPackage.Version) (Update ID: $($BestPackage.Id))" -ForegroundColor Green - - $DownloadPage = $null - $PostData = "[{`"size`":0,`"updateID`":`"$($BestPackage.Id)`",`"uidInfo`":`"$($BestPackage.Id)`"}]" - try { - $DownloadPage = Invoke-CatalogRequest -Params @{ Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx"; Method = 'Post'; Body = @{updateIDs = $PostData }; UseBasicParsing = $true } - } - catch { - Write-Warning "Download dialog request failed for $Prefix [$Arch]: $_" - $Manifest.Add("SKIPPED: $Prefix [$Arch] (download dialog failed)") - continue - } - - $CabUrl = [regex]::Match($DownloadPage.Content, 'https://[^''"<]+\.cab').Value - - if (-not $CabUrl) { - Write-Host " [!] Could not extract .cab URL from payload." -ForegroundColor Red - $Manifest.Add("SKIPPED: $Prefix [$Arch] (no .cab URL in payload)") - continue - } - - $CabFile = Join-Path $DownloadPath "$($Target.Name)_$($Prefix)_$($Arch).cab" - # ExtractDir keyed on the (unique) Prefix, not FamilyName — the three AQC111U USB - # entries share FamilyName "AQC111U", so keying on FamilyName would collide them - # into one directory. Prefix is unique per device definition. - $ExtractDir = Join-Path $DownloadPath "$($Target.Name)\$Prefix\$Arch" - - Write-Host " -> Downloading raw $Arch driver package..." - try { - Invoke-CatalogRequest -Params @{ Uri = $CabUrl; OutFile = $CabFile; UseBasicParsing = $true } | Out-Null - } - catch { - Write-Warning "CAB download failed for $Prefix [$Arch]: $_" - $Manifest.Add("SKIPPED: $Prefix [$Arch] (download failed)") - continue - } - - # AUTHENTICITY: this CAB is extracted into boot-start kernel drivers. Verify the - # publisher's Authenticode signature and SKIP (do not extract/inject) anything - # that is not 'Valid', rather than silently trusting it. - $sig = Get-AuthenticodeSignature -FilePath $CabFile - if ($sig.Status -ne 'Valid') { - Write-Warning "Authenticode signature for $Prefix [$Arch] CAB is '$($sig.Status)' (not Valid) — skipping extraction/injection." - $Manifest.Add("SKIPPED: $Prefix [$Arch] (signature $($sig.Status))") - Remove-Item $CabFile -Force -ErrorAction SilentlyContinue - continue - } - - Write-Host " -> Extracting payload using expand.exe..." - if (-not (Test-Path $ExtractDir)) { New-Item -ItemType Directory -Path $ExtractDir -Force | Out-Null } - - # Capture expand.exe's exit code. On failure KEEP the source CAB for retry/inspection. - $expandProc = Start-Process "expand.exe" -ArgumentList "-F:* `"$CabFile`" `"$ExtractDir`"" -NoNewWindow -PassThru - $expandProc.WaitForExit() - if ($expandProc.ExitCode -ne 0) { - Write-Warning "expand.exe exited with code $($expandProc.ExitCode) for $Prefix [$Arch] — keeping source CAB '$CabFile' for retry/inspection." - $Manifest.Add("SKIPPED: $Prefix [$Arch] (expand.exe exit $($expandProc.ExitCode))") - continue - } - Remove-Item $CabFile -Force - - # Manifest: confirm an actual .inf + .sys landed (an empty/partial extract is a skip). - $infCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.inf -ErrorAction SilentlyContinue).Count - $sysCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.sys -ErrorAction SilentlyContinue).Count - if ($infCount -gt 0 -and $sysCount -gt 0) { - $Manifest.Add("ACQUIRED: $Prefix [$Arch]") - } - else { - $Manifest.Add("SKIPPED: $Prefix [$Arch] (no .inf/.sys after extract)") - } - - if ($Install) { - $SysArch = $env:PROCESSOR_ARCHITECTURE - if ($SysArch -eq $Arch) { - Write-Host " -> System is $SysArch. Injecting $Arch driver into Driver Store via pnputil..." -ForegroundColor Green - pnputil.exe /add-driver "$ExtractDir\*.inf" /install | Out-Null - Write-Host " -> Injection complete." -ForegroundColor Green - } - else { - Write-Host " -> System is $SysArch. Skipping $Arch driver installation." -ForegroundColor DarkGray - } - } - else { - Write-Host " -> Extracted to: $ExtractDir (Skipping installation)" -ForegroundColor DarkGray - } - } -} - -# ============================================================ -# Acquisition manifest (greppable) -# ============================================================ -Write-Host "`n=> Acquisition manifest:" -ForegroundColor Cyan -if ($Manifest.Count -eq 0) { - Write-Host " (no device families processed)" -ForegroundColor DarkGray -} -else { - foreach ($line in $Manifest) { - if ($line -like 'ACQUIRED:*') { Write-Host " $line" -ForegroundColor Green } - else { Write-Host " $line" -ForegroundColor Yellow } - } -} - -Write-Host "`nProcess complete." -ForegroundColor Cyan +$csDir = Join-Path $PSScriptRoot 'catalogscrape' +Import-Module (Join-Path $csDir 'CatalogScrape.psm1') -Force +$config = Import-PowerShellDataFile (Join-Path $csDir 'marvell.psd1') +Invoke-DriverScrape -Config $config -Install:$Install -DownloadPath $DownloadPath -Architecture $Architecture diff --git a/Get-MediatekWiFiDrivers.ps1 b/Get-MediatekWiFiDrivers.ps1 index 827c1f1..a753499 100644 --- a/Get-MediatekWiFiDrivers.ps1 +++ b/Get-MediatekWiFiDrivers.ps1 @@ -1,361 +1,26 @@ #Requires -Version 7.0 <# .SYNOPSIS - Fetches and optionally installs the absolute latest MediaTek Wi-Fi drivers - directly from the Microsoft Update Catalog. + Fetches the latest MediaTek Wi-Fi drivers from the Microsoft Update Catalog. .DESCRIPTION - This script queries the Catalog for MediaTek Wi-Fi adapters (MT7921, MT7921K, MT7922, MT7925, MT7927), - pulls the detailed version for every package, and prioritizes packages using - standard Semantic Versioning or simply taking the global highest version available. - Native ARM64 support is explicitly accounted for automatically, as MediaTek is - heavily featured in modern Copilot+ and Surface devices. - - .EXAMPLE - .\Get-MediatekWiFiDrivers.ps1 - Downloads and extracts the newest drivers to C:\Temp\MediaTek_WiFi without installing. - - .EXAMPLE - .\Get-MediatekWiFiDrivers.ps1 -Install - Downloads, extracts, installs the drivers to the Driver Store, and cleans up the CABs. + Thin shim over catalogscrape\CatalogScrape.psm1, driven by catalogscrape\mediatek.psd1. + Covers MT7921/MT7921K/MT7922/MT7925/MT7927 (native ARM64 included via -Architecture). + Bluetooth/UART combo-chip entries are excluded by title; selection is highest-version-first, + so the current 26.30 branch wins for MT7925/MT7927. .NOTES - SCOPE: These are POST-BOOT CONVENIENCE drivers only. Wi-Fi adapters cannot serve - iSCSI / network (PXE) boot, so nothing here is boot-critical — it only restores - wireless connectivity after the OS is already running. + SCOPE: POST-BOOT CONVENIENCE drivers only. Wi-Fi cannot serve iSCSI/PXE boot, so nothing + here is boot-critical — it only restores wireless connectivity after the OS is running. #> - [CmdletBinding()] -param ( +param( [switch]$Install, - [string]$DownloadPath = 'C:\Temp\MediaTek_WiFi', - - [ValidateSet('x64','arm64','all')] - [string]$Architecture = 'x64' -) - -$AcceptedArchs = switch ($Architecture) { - 'x64' { @('AMD64') } - 'arm64' { @('ARM64') } - 'all' { @('AMD64','ARM64') } -} - -# Dynamic Admin Check -if ($Install -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error "The -Install flag requires Administrator privileges. Please run PowerShell as Administrator." - return -} - -# ============================================================ -# Helper: retry-with-backoff wrapper around Invoke-WebRequest -# ============================================================ -# Defined locally so this script stays independently runnable. The Update Catalog -# throttles aggressively; retry up to 3 times with increasing delay, then re-throw so -# callers can try/catch and skip. NOTE: not available inside ForEach-Object -Parallel -# runspaces (those carry their own inline retry loop). -function Invoke-CatalogRequest { - param ( - [Parameter(Mandatory)][hashtable]$Params, - [int]$MaxAttempts = 3 - ) - for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { - try { - return Invoke-WebRequest @Params - } - catch { - if ($attempt -ge $MaxAttempts) { throw } - Start-Sleep -Seconds ($attempt * 2) - } - } -} - -$Targets = @( - @{ - Name = "MediaTek_WiFi_Family" - Devices = @( - @{ - ModelId = "7961" - ModelName = "MT7921_Filogic330" - Queries = @("VEN_14C3&DEV_7961", "MT7921") - PreferredBranches = @("3.5") - }, - @{ - ModelId = "0608" - ModelName = "MT7921K_RZ608" - Queries = @("VEN_14C3&DEV_0608", "RZ608") - PreferredBranches = @("3.5") - }, - @{ - ModelId = "0616" - ModelName = "MT7922_RZ616" - Queries = @("VEN_14C3&DEV_0616", "MT7922", "RZ616") - PreferredBranches = @("3.5") - }, - @{ - ModelId = "7925" - ModelName = "MT7925_Filogic380" - Queries = @("VEN_14C3&DEV_7925", "MT7925") - PreferredBranches = @("25.30", "5.7") - }, - @{ - ModelId = "7927" - ModelName = "MT7927_Filogic380High" - Queries = @("VEN_14C3&DEV_7927", "MT7927") - PreferredBranches = @("25.30", "5.7") - } - ) - } + [string]$DownloadPath = 'C:\Temp', + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64' ) -if (-not (Test-Path $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } - -# Acquisition manifest — greppable ACQUIRED:/SKIPPED: lines emitted at the end so a -# throttled/partial parallel run is auditable. -$Manifest = [System.Collections.Generic.List[string]]::new() - -foreach ($Target in $Targets) { - Write-Host "`n=> Investigating Microsoft Update Catalog for $($Target.Name)..." -ForegroundColor Cyan - - $AvailablePackages = @() - - foreach ($Device in $Target.Devices) { - $ModelId = $Device.ModelId - - foreach ($Query in $Device.Queries) { - Write-Host " -> Searching for ModelId $ModelId ($Query)..." - - # Reset per-iteration so a failed fetch can't silently reuse the PREVIOUS - # query's page (which would parse the wrong update IDs for this query). - $SearchPage = $null - try { - $SearchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$([uri]::EscapeDataString($Query))" - $SearchPage = Invoke-CatalogRequest -Params @{ Uri = $SearchUrl; UseBasicParsing = $true } - } - catch { - Write-Warning "Search request failed for $($Device.ModelName) ($Query): $_" - $Manifest.Add("SKIPPED: $($Device.ModelName) (search request failed: $Query)") - continue - } - - # LIMITATION: Search.aspx returns only the first 25 relevance-sorted rows; the - # "highest version" pick below only sees those 25. Full pagination needs - # __EVENTTARGET POST-backs (invasive). Parse the "1 - N of M" total and warn if M > 25. - if ($SearchPage.Content -match '(\d+)\s*-\s*(\d+)\s+of\s+(\d+)') { - $totalResults = [int]$matches[3] - if ($totalResults -gt 25) { - Write-Warning "Catalog reports $totalResults results for '$Query' but only the first 25 are parsed (pagination not implemented)." - } - } - - # Extract all update IDs from the search results table - $UpdateIds = [regex]::Matches($SearchPage.Content, "goToDetails\(['""]([a-f0-9\-]+)['""]\)") | - ForEach-Object { $_.Groups[1].Value } | - Select-Object -Unique - - if (-not $UpdateIds) { - Write-Host " [!] No candidates found for this query." -ForegroundColor Yellow - continue - } - - Write-Host " -> Found $($UpdateIds.Count) packages for query '$Query'. Fetching deep versions..." -NoNewline - - $DetailResults = $UpdateIds | ForEach-Object -Parallel { - $Id = $_ - $DetailsUrl = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$Id" - try { - # Inline retry-with-backoff (the script-scope helper is not visible here). - $DetailsPage = $null - for ($attempt = 1; $attempt -le 3; $attempt++) { - try { $DetailsPage = Invoke-WebRequest -Uri $DetailsUrl -UseBasicParsing; break } - catch { if ($attempt -ge 3) { throw } else { Start-Sleep -Seconds ($attempt * 2) } } - } - $DateString = if ($DetailsPage.Content -match 'id="ScopedViewHandler_versionDate">([^<]+)') { $matches[1].Trim() } - $Version = if ($DetailsPage.Content -match 'id="ScopedViewHandler_version">([^<]+)') { $matches[1].Trim() } - $Title = if ($DetailsPage.Content -match 'id="ScopedViewHandler_titleText">([^<]+)') { $matches[1].Trim() } - - if ($Version -and $DateString) { - $DateObj = [datetime]::Parse($DateString) - $Arch = if ($DetailsPage.Content -match "ARM64") { "ARM64" } elseif ($DetailsPage.Content -match "AMD64|x64|amd64") { "AMD64" } else { "x86" } - [PSCustomObject]@{ - Version = $Version - DateObj = $DateObj - Id = $Id - Arch = $Arch - Title = $Title - } - } - } - catch { - Write-Warning "Detail fetch/parse failed for update $Id : $($_.Exception.Message)" - } - } -ThrottleLimit 8 - - Write-Host " Done." - - foreach ($result in $DetailResults) { - # The marketing-name queries (MT7921, RZ616, ...) also return the combo-chip's - # Bluetooth/UART driver entries, which would otherwise pass the lone -notmatch - # 'NDIS' filter. Exclude Title 'bluetooth'/'uart' so only the Wi-Fi NIC package - # is selected. - if ($result -and $result.Arch -in $AcceptedArchs -and $result.Title -notmatch "NDIS" -and $result.Title -notmatch '(?i)bluetooth|uart') { - try { - $AvailablePackages += [PSCustomObject]@{ - ModelId = $ModelId - ModelName = $Device.ModelName - Version = [version]$result.Version - DateObj = $result.DateObj - Id = $result.Id - Title = $result.Title - PreferredBranches = $Device.PreferredBranches - Arch = $result.Arch - } - } - catch {} - } - } - } - } - - if (-not $AvailablePackages) { - Write-Host " [!] No matching ModelIds found within candidate packages." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $($Target.Name) (no packages matched accepted architectures)") - continue - } - - # Record models that yielded no usable Wi-Fi candidate so the manifest stays complete - # (a model is only marked here if NONE of its queries produced a package). - $FoundModelIds = @($AvailablePackages.ModelId | Select-Object -Unique) - foreach ($Device in $Target.Devices) { - if ($Device.ModelId -notin $FoundModelIds) { - $Manifest.Add("SKIPPED: $($Device.ModelName) (no Wi-Fi catalog candidates)") - } - } - - # Group all valid candidate packages into their correct respective HWModelId families and Architecture, and find the freshest. - $GroupedPackages = $AvailablePackages | Group-Object ModelId, Arch - - foreach ($Group in $GroupedPackages) { - $FirstObj = $Group.Group[0] - $ModelId = $FirstObj.ModelId - $Arch = $FirstObj.Arch - $ModelName = $FirstObj.ModelName - - $BestPackage = $null - $TargetBranches = $FirstObj.PreferredBranches - - if ($TargetBranches) { - foreach ($Branch in $TargetBranches) { - $BranchMatches = $Group.Group | Where-Object { $_.Version.ToString().StartsWith("$Branch.") } - if ($BranchMatches.Count -gt 0) { - $BestPackage = $BranchMatches | Sort-Object Version -Descending | Select-Object -First 1 - Write-Host " -> ModelId $($ModelId) [$Arch]: Selected v$($BestPackage.Version) [Preferred Branch: $Branch] (Update ID: $($BestPackage.Id))" -ForegroundColor Green - break - } - } - if (-not $BestPackage) { - $BestPackage = $Group.Group | Sort-Object Version -Descending | Select-Object -First 1 - Write-Host " -> ModelId $($ModelId) [$Arch]: Selected v$($BestPackage.Version) [Global Highest] (Update ID: $($BestPackage.Id))" -ForegroundColor Green - } - } - else { - $BestPackage = $Group.Group | Sort-Object Version -Descending | Select-Object -First 1 - Write-Host " -> ModelId $($ModelId) [$Arch]: Selected v$($BestPackage.Version) [Global Highest] (Update ID: $($BestPackage.Id))" -ForegroundColor Green - } - - $DownloadPage = $null - $PostData = "[{`"size`":0,`"updateID`":`"$($BestPackage.Id)`",`"uidInfo`":`"$($BestPackage.Id)`"}]" - try { - $DownloadPage = Invoke-CatalogRequest -Params @{ Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx"; Method = 'Post'; Body = @{updateIDs = $PostData }; UseBasicParsing = $true } - } - catch { - Write-Warning "Download dialog request failed for $ModelName [$Arch]: $_" - $Manifest.Add("SKIPPED: $ModelName [$Arch] (download dialog failed)") - continue - } - - $CabUrl = [regex]::Match($DownloadPage.Content, 'https://[^''"<]+\.cab').Value - - if (-not $CabUrl) { - Write-Host " [!] Could not extract .cab URL from payload." -ForegroundColor Red - $Manifest.Add("SKIPPED: $ModelName [$Arch] (no .cab URL in payload)") - continue - } - - $CabFile = Join-Path $DownloadPath "$($Target.Name)_$($ModelId)_$($Arch).cab" - $ExtractDir = Join-Path $DownloadPath "$($Target.Name)\$ModelName\$Arch" - - Write-Host " -> Downloading raw $Arch driver package..." - try { - Invoke-CatalogRequest -Params @{ Uri = $CabUrl; OutFile = $CabFile; UseBasicParsing = $true } | Out-Null - } - catch { - Write-Warning "CAB download failed for $ModelName [$Arch]: $_" - $Manifest.Add("SKIPPED: $ModelName [$Arch] (download failed)") - continue - } - - # AUTHENTICITY: verify the publisher's Authenticode signature and SKIP (do not - # extract/inject) anything that is not 'Valid', rather than silently trusting it. - $sig = Get-AuthenticodeSignature -FilePath $CabFile - if ($sig.Status -ne 'Valid') { - Write-Warning "Authenticode signature for $ModelName [$Arch] CAB is '$($sig.Status)' (not Valid) — skipping extraction/injection." - $Manifest.Add("SKIPPED: $ModelName [$Arch] (signature $($sig.Status))") - Remove-Item $CabFile -Force -ErrorAction SilentlyContinue - continue - } - - Write-Host " -> Extracting payload using expand.exe..." - if (-not (Test-Path $ExtractDir)) { New-Item -ItemType Directory -Path $ExtractDir -Force | Out-Null } - - # Capture expand.exe's exit code. On failure KEEP the source CAB for retry/inspection. - $expandProc = Start-Process "expand.exe" -ArgumentList "-F:* `"$CabFile`" `"$ExtractDir`"" -NoNewWindow -PassThru - $expandProc.WaitForExit() - if ($expandProc.ExitCode -ne 0) { - Write-Warning "expand.exe exited with code $($expandProc.ExitCode) for $ModelName [$Arch] — keeping source CAB '$CabFile' for retry/inspection." - $Manifest.Add("SKIPPED: $ModelName [$Arch] (expand.exe exit $($expandProc.ExitCode))") - continue - } - Remove-Item $CabFile -Force - - # Manifest: confirm an actual .inf + .sys landed (an empty/partial extract is a skip). - $infCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.inf -ErrorAction SilentlyContinue).Count - $sysCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.sys -ErrorAction SilentlyContinue).Count - if ($infCount -gt 0 -and $sysCount -gt 0) { - $Manifest.Add("ACQUIRED: $ModelName [$Arch]") - } - else { - $Manifest.Add("SKIPPED: $ModelName [$Arch] (no .inf/.sys after extract)") - } - - if ($Install) { - $SysArch = $env:PROCESSOR_ARCHITECTURE - if ($SysArch -eq $Arch) { - Write-Host " -> System is $SysArch. Injecting $Arch driver into Driver Store via pnputil..." -ForegroundColor Green - pnputil.exe /add-driver "$ExtractDir\*.inf" /install | Out-Null - Write-Host " -> Injection complete." -ForegroundColor Green - } - else { - Write-Host " -> System is $SysArch. Skipping $Arch driver installation." -ForegroundColor DarkGray - } - } - else { - Write-Host " -> Extracted to: $ExtractDir (Skipping installation)" -ForegroundColor DarkGray - } - } -} - -# ============================================================ -# Acquisition manifest (greppable) -# ============================================================ -Write-Host "`n=> Acquisition manifest:" -ForegroundColor Cyan -if ($Manifest.Count -eq 0) { - Write-Host " (no device families processed)" -ForegroundColor DarkGray -} -else { - foreach ($line in $Manifest) { - if ($line -like 'ACQUIRED:*') { Write-Host " $line" -ForegroundColor Green } - else { Write-Host " $line" -ForegroundColor Yellow } - } -} - -Write-Host "`nProcess complete." -ForegroundColor Cyan \ No newline at end of file +$csDir = Join-Path $PSScriptRoot 'catalogscrape' +Import-Module (Join-Path $csDir 'CatalogScrape.psm1') -Force +$config = Import-PowerShellDataFile (Join-Path $csDir 'mediatek.psd1') +Invoke-DriverScrape -Config $config -Install:$Install -DownloadPath $DownloadPath -Architecture $Architecture diff --git a/Get-QualcommWiFiDrivers.ps1 b/Get-QualcommWiFiDrivers.ps1 index 5eaf8f7..21f5ec8 100644 --- a/Get-QualcommWiFiDrivers.ps1 +++ b/Get-QualcommWiFiDrivers.ps1 @@ -1,340 +1,26 @@ #Requires -Version 7.0 <# .SYNOPSIS - Fetches and optionally installs the absolute latest Qualcomm Wi-Fi drivers - directly from the Microsoft Update Catalog. + Fetches the latest Qualcomm Wi-Fi drivers from the Microsoft Update Catalog. .DESCRIPTION - This script queries the Catalog for Qualcomm Wi-Fi adapters (QCA6390, WCN6855, WCN7850), - pulls the detailed version for every package, and prioritizes packages using - standard Semantic Versioning (e.g., 2.0.0.x or 3.0.0.x), while explicitly ignoring - older "NDIS" legacy titles to guarantee modern NetAdapterCx or modern WDI drivers. - - .EXAMPLE - .\Get-QualcommWiFiDrivers.ps1 - Downloads and extracts the newest drivers to C:\Temp\Qualcomm_WiFi without installing. - - .EXAMPLE - .\Get-QualcommWiFiDrivers.ps1 -Install - Downloads, extracts, installs the drivers to the Driver Store, and cleans up the CABs. + Thin shim over catalogscrape\CatalogScrape.psm1, driven by catalogscrape\qualcomm.psd1. + Covers QCA6390, WCN6855, WCN7850 (modern VEN_17CB ids + marketing-name queries). Bluetooth/ + UART combo-chip entries are excluded by title; selection is highest-version-first with the + preferred branch as a same-version tiebreak. .NOTES - SCOPE: These are POST-BOOT CONVENIENCE drivers only. Wi-Fi adapters cannot serve - iSCSI / network (PXE) boot, so nothing here is boot-critical — it only restores - wireless connectivity after the OS is already running. + SCOPE: POST-BOOT CONVENIENCE drivers only. Wi-Fi cannot serve iSCSI/PXE boot, so nothing + here is boot-critical — it only restores wireless connectivity after the OS is running. #> - [CmdletBinding()] -param ( +param( [switch]$Install, - [string]$DownloadPath = 'C:\Temp\Qualcomm_WiFi', - - [ValidateSet('x64','arm64','all')] - [string]$Architecture = 'x64' -) - -$AcceptedArchs = switch ($Architecture) { - 'x64' { @('AMD64') } - 'arm64' { @('ARM64') } - 'all' { @('AMD64','ARM64') } -} - -# Dynamic Admin Check -if ($Install -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error "The -Install flag requires Administrator privileges. Please run PowerShell as Administrator." - return -} - -# ============================================================ -# Helper: retry-with-backoff wrapper around Invoke-WebRequest -# ============================================================ -# Defined locally so this script stays independently runnable. The Update Catalog -# throttles aggressively; retry up to 3 times with increasing delay, then re-throw so -# callers can try/catch and skip. NOTE: not available inside ForEach-Object -Parallel -# runspaces (those carry their own inline retry loop). -function Invoke-CatalogRequest { - param ( - [Parameter(Mandatory)][hashtable]$Params, - [int]$MaxAttempts = 3 - ) - for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { - try { - return Invoke-WebRequest @Params - } - catch { - if ($attempt -ge $MaxAttempts) { throw } - Start-Sleep -Seconds ($attempt * 2) - } - } -} - -$Targets = @( - @{ - Name = "Qualcomm_PCIe_Family" - Devices = @( - @{ - ModelId = "1101" - ModelName = "QCA6390" - Queries = @("VEN_17CB&DEV_1101", "Killer AX500") - PreferredBranch = "3.0" - }, - @{ - ModelId = "1103" - ModelName = "WCN6855" - Queries = @("VEN_17CB&DEV_1103", "FastConnect 6900") - PreferredBranch = "3.0" - }, - @{ - ModelId = "1107" - ModelName = "WCN7850" - Queries = @("VEN_17CB&DEV_1107", "FastConnect 7800") - PreferredBranch = "3.1" - } - ) - } + [string]$DownloadPath = 'C:\Temp', + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64' ) - -if (-not (Test-Path $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } - -# Acquisition manifest — greppable ACQUIRED:/SKIPPED: lines emitted at the end so a -# throttled/partial parallel run is auditable. -$Manifest = [System.Collections.Generic.List[string]]::new() - -foreach ($Target in $Targets) { - Write-Host "`n=> Investigating Microsoft Update Catalog for $($Target.Name)..." -ForegroundColor Cyan - - $AvailablePackages = @() - - foreach ($Device in $Target.Devices) { - $ModelId = $Device.ModelId - - foreach ($Query in $Device.Queries) { - Write-Host " -> Searching for ModelId $ModelId ($Query)..." - - # Reset per-iteration so a failed fetch can't silently reuse the PREVIOUS - # query's page (which would parse the wrong update IDs for this query). - $SearchPage = $null - try { - $SearchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$([uri]::EscapeDataString($Query))" - $SearchPage = Invoke-CatalogRequest -Params @{ Uri = $SearchUrl; UseBasicParsing = $true } - } - catch { - Write-Warning "Search request failed for $($Device.ModelName) ($Query): $_" - $Manifest.Add("SKIPPED: $($Device.ModelName) (search request failed: $Query)") - continue - } - - # LIMITATION: Search.aspx returns only the first 25 relevance-sorted rows; the - # "highest version" pick below only sees those 25. Full pagination needs - # __EVENTTARGET POST-backs (invasive). Parse the "1 - N of M" total and warn if M > 25. - if ($SearchPage.Content -match '(\d+)\s*-\s*(\d+)\s+of\s+(\d+)') { - $totalResults = [int]$matches[3] - if ($totalResults -gt 25) { - Write-Warning "Catalog reports $totalResults results for '$Query' but only the first 25 are parsed (pagination not implemented)." - } - } - - # Extract all update IDs from the search results table - $UpdateIds = [regex]::Matches($SearchPage.Content, "goToDetails\(['""]([a-f0-9\-]+)['""]\)") | - ForEach-Object { $_.Groups[1].Value } | - Select-Object -Unique - - if (-not $UpdateIds) { - Write-Host " [!] No candidates found for this query." -ForegroundColor Yellow - continue - } - - Write-Host " -> Found $($UpdateIds.Count) packages for query '$Query'. Fetching deep versions..." -NoNewline - - $DetailResults = $UpdateIds | ForEach-Object -Parallel { - $Id = $_ - $DetailsUrl = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$Id" - try { - # Inline retry-with-backoff (the script-scope helper is not visible here). - $DetailsPage = $null - for ($attempt = 1; $attempt -le 3; $attempt++) { - try { $DetailsPage = Invoke-WebRequest -Uri $DetailsUrl -UseBasicParsing; break } - catch { if ($attempt -ge 3) { throw } else { Start-Sleep -Seconds ($attempt * 2) } } - } - $DateString = if ($DetailsPage.Content -match 'id="ScopedViewHandler_versionDate">([^<]+)') { $matches[1].Trim() } - $Version = if ($DetailsPage.Content -match 'id="ScopedViewHandler_version">([^<]+)') { $matches[1].Trim() } - $Title = if ($DetailsPage.Content -match 'id="ScopedViewHandler_titleText">([^<]+)') { $matches[1].Trim() } - - if ($Version -and $DateString) { - $DateObj = [datetime]::Parse($DateString) - $Arch = if ($DetailsPage.Content -match "ARM64") { "ARM64" } elseif ($DetailsPage.Content -match "AMD64|x64|amd64") { "AMD64" } else { "x86" } - [PSCustomObject]@{ - Version = $Version - DateObj = $DateObj - Id = $Id - Arch = $Arch - Title = $Title - } - } - } - catch { - Write-Warning "Detail fetch/parse failed for update $Id : $($_.Exception.Message)" - } - } -ThrottleLimit 8 - - Write-Host " Done." - - foreach ($result in $DetailResults) { - # The marketing-name queries (Killer AX500, FastConnect 6900, ...) also return - # the combo-chip's Bluetooth/UART driver entries, which would otherwise pass the - # lone -notmatch 'NDIS' filter. Exclude Title 'bluetooth'/'uart' so only the - # Wi-Fi NIC package is selected. - if ($result -and $result.Arch -in $AcceptedArchs -and $result.Title -notmatch "NDIS" -and $result.Title -notmatch '(?i)bluetooth|uart') { - try { - $AvailablePackages += [PSCustomObject]@{ - ModelId = $ModelId - ModelName = $Device.ModelName - Version = [version]$result.Version - DateObj = $result.DateObj - Id = $result.Id - Title = $result.Title - PreferredBranch = $Device.PreferredBranch - Arch = $result.Arch - } - } - catch {} - } - } - } - } - - if (-not $AvailablePackages) { - Write-Host " [!] No matching ModelIds found within candidate packages." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $($Target.Name) (no packages matched accepted architectures)") - continue - } - - # Record models that yielded no usable Wi-Fi candidate so the manifest stays complete - # (a model is only marked here if NONE of its queries produced a package). - $FoundModelIds = @($AvailablePackages.ModelId | Select-Object -Unique) - foreach ($Device in $Target.Devices) { - if ($Device.ModelId -notin $FoundModelIds) { - $Manifest.Add("SKIPPED: $($Device.ModelName) (no Wi-Fi catalog candidates)") - } - } - - # Group all valid candidate packages into their correct respective HWModelId families and Architecture, and find the freshest. - $GroupedPackages = $AvailablePackages | Group-Object ModelId, Arch - - foreach ($Group in $GroupedPackages) { - $FirstObj = $Group.Group[0] - $ModelId = $FirstObj.ModelId - $Arch = $FirstObj.Arch - $ModelName = $FirstObj.ModelName - - # Check if any packages exactly match the preferred major.minor branch - $TargetBranch = $FirstObj.PreferredBranch - $BranchMatches = $Group.Group | Where-Object { $_.Version.ToString().StartsWith("$TargetBranch.") } - - if ($BranchMatches.Count -gt 0) { - $BestPackage = $BranchMatches | Sort-Object Version -Descending | Select-Object -First 1 - Write-Host " -> ModelId $($ModelId) [$Arch]: Selected v$($BestPackage.Version) [Preferred Branch] (Update ID: $($BestPackage.Id))" -ForegroundColor Green - } - else { - $BestPackage = $Group.Group | Sort-Object Version -Descending | Select-Object -First 1 - Write-Host " -> ModelId $($ModelId) [$Arch]: Selected v$($BestPackage.Version) [Global Highest] (Update ID: $($BestPackage.Id))" -ForegroundColor Green - } - - $DownloadPage = $null - $PostData = "[{`"size`":0,`"updateID`":`"$($BestPackage.Id)`",`"uidInfo`":`"$($BestPackage.Id)`"}]" - try { - $DownloadPage = Invoke-CatalogRequest -Params @{ Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx"; Method = 'Post'; Body = @{updateIDs = $PostData }; UseBasicParsing = $true } - } - catch { - Write-Warning "Download dialog request failed for $ModelName [$Arch]: $_" - $Manifest.Add("SKIPPED: $ModelName [$Arch] (download dialog failed)") - continue - } - - $CabUrl = [regex]::Match($DownloadPage.Content, 'https://[^''"<]+\.cab').Value - - if (-not $CabUrl) { - Write-Host " [!] Could not extract .cab URL from payload." -ForegroundColor Red - $Manifest.Add("SKIPPED: $ModelName [$Arch] (no .cab URL in payload)") - continue - } - - $CabFile = Join-Path $DownloadPath "$($Target.Name)_$($ModelId)_$($Arch).cab" - $ExtractDir = Join-Path $DownloadPath "$($Target.Name)\$ModelName\$Arch" - - Write-Host " -> Downloading raw $Arch driver package..." - try { - Invoke-CatalogRequest -Params @{ Uri = $CabUrl; OutFile = $CabFile; UseBasicParsing = $true } | Out-Null - } - catch { - Write-Warning "CAB download failed for $ModelName [$Arch]: $_" - $Manifest.Add("SKIPPED: $ModelName [$Arch] (download failed)") - continue - } - - # AUTHENTICITY: verify the publisher's Authenticode signature and SKIP (do not - # extract/inject) anything that is not 'Valid', rather than silently trusting it. - $sig = Get-AuthenticodeSignature -FilePath $CabFile - if ($sig.Status -ne 'Valid') { - Write-Warning "Authenticode signature for $ModelName [$Arch] CAB is '$($sig.Status)' (not Valid) — skipping extraction/injection." - $Manifest.Add("SKIPPED: $ModelName [$Arch] (signature $($sig.Status))") - Remove-Item $CabFile -Force -ErrorAction SilentlyContinue - continue - } - - Write-Host " -> Extracting payload using expand.exe..." - if (-not (Test-Path $ExtractDir)) { New-Item -ItemType Directory -Path $ExtractDir -Force | Out-Null } - - # Capture expand.exe's exit code. On failure KEEP the source CAB for retry/inspection. - $expandProc = Start-Process "expand.exe" -ArgumentList "-F:* `"$CabFile`" `"$ExtractDir`"" -NoNewWindow -PassThru - $expandProc.WaitForExit() - if ($expandProc.ExitCode -ne 0) { - Write-Warning "expand.exe exited with code $($expandProc.ExitCode) for $ModelName [$Arch] — keeping source CAB '$CabFile' for retry/inspection." - $Manifest.Add("SKIPPED: $ModelName [$Arch] (expand.exe exit $($expandProc.ExitCode))") - continue - } - Remove-Item $CabFile -Force - - # Manifest: confirm an actual .inf + .sys landed (an empty/partial extract is a skip). - $infCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.inf -ErrorAction SilentlyContinue).Count - $sysCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.sys -ErrorAction SilentlyContinue).Count - if ($infCount -gt 0 -and $sysCount -gt 0) { - $Manifest.Add("ACQUIRED: $ModelName [$Arch]") - } - else { - $Manifest.Add("SKIPPED: $ModelName [$Arch] (no .inf/.sys after extract)") - } - - if ($Install) { - $SysArch = $env:PROCESSOR_ARCHITECTURE - if ($SysArch -eq $Arch) { - Write-Host " -> System is $SysArch. Injecting $Arch driver into Driver Store via pnputil..." -ForegroundColor Green - pnputil.exe /add-driver "$ExtractDir\*.inf" /install | Out-Null - Write-Host " -> Injection complete." -ForegroundColor Green - } - else { - Write-Host " -> System is $SysArch. Skipping $Arch driver installation." -ForegroundColor DarkGray - } - } - else { - Write-Host " -> Extracted to: $ExtractDir (Skipping installation)" -ForegroundColor DarkGray - } - } -} - -# ============================================================ -# Acquisition manifest (greppable) -# ============================================================ -Write-Host "`n=> Acquisition manifest:" -ForegroundColor Cyan -if ($Manifest.Count -eq 0) { - Write-Host " (no device families processed)" -ForegroundColor DarkGray -} -else { - foreach ($line in $Manifest) { - if ($line -like 'ACQUIRED:*') { Write-Host " $line" -ForegroundColor Green } - else { Write-Host " $line" -ForegroundColor Yellow } - } -} - -Write-Host "`nProcess complete." -ForegroundColor Cyan \ No newline at end of file +$csDir = Join-Path $PSScriptRoot 'catalogscrape' +Import-Module (Join-Path $csDir 'CatalogScrape.psm1') -Force +$config = Import-PowerShellDataFile (Join-Path $csDir 'qualcomm.psd1') +Invoke-DriverScrape -Config $config -Install:$Install -DownloadPath $DownloadPath -Architecture $Architecture diff --git a/Get-RealtekEthernetDrivers.ps1 b/Get-RealtekEthernetDrivers.ps1 index 140c29c..bc263a6 100644 --- a/Get-RealtekEthernetDrivers.ps1 +++ b/Get-RealtekEthernetDrivers.ps1 @@ -1,302 +1,31 @@ #Requires -Version 7.0 <# .SYNOPSIS - Fetches and optionally installs the absolute latest Realtek NetAdapterCx (11.x) drivers for - all modern PCIe and USB Realtek NIC families directly from the Microsoft Update Catalog. + Fetches the latest Realtek NetAdapterCx (11.x) drivers for modern PCIe and USB Realtek + NIC families from the Microsoft Update Catalog. .DESCRIPTION - Realtek driver Versioning is based on an OS/Hardware matrix suffix, not pure chronology. - e.g. 1168 (PCIe 1G) > 1159 (USB 10G) incorrectly looks newer, but they are different hardware. - The true release date is encoded at the end: Prefix.Revision.MMDD.YYYY (or YY.MMDD). - This script queries the Catalog, pulls the detailed version for every package, decodes - the true release date, and downloads the absolute newest driver for each specific HW family. + Thin shim over catalogscrape\CatalogScrape.psm1, driven by catalogscrape\realtek.psd1. + Covers PCIe (RTL8125/8126/8127/8168) and USB (RTL8153/8156/8157/8159). + + Selection is by highest parsed driver [version] (with catalog publish date as a tiebreak), + NOT by the catalog "Version Date" field: many Realtek USB packages carry a stale 2016/2018 + INF date, so sorting by date would pick an older build than the version string actually + indicates (e.g. RTL8159 11.19.602.2025 vs the 2018-dated 11.19.20.602). .EXAMPLE .\Get-RealtekEthernetDrivers.ps1 - Downloads and extracts the newest drivers to C:\Temp\Realtek_NetAdapterCx without installing. - .EXAMPLE .\Get-RealtekEthernetDrivers.ps1 -Install - Downloads, extracts, installs the drivers to the Driver Store, and cleans up the CABs. #> - [CmdletBinding()] -param ( +param( [switch]$Install, - [string]$DownloadPath = 'C:\Temp\Realtek_Ethernet', - - [ValidateSet('x64','arm64','all')] - [string]$Architecture = 'x64' -) - -$AcceptedArchs = switch ($Architecture) { - 'x64' { @('AMD64') } - 'arm64' { @('ARM64') } - 'all' { @('AMD64','ARM64') } -} - -# Dynamic Admin Check -if ($Install -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Error "The -Install flag requires Administrator privileges. Please run PowerShell as Administrator." - return -} - -# ============================================================ -# Helper: retry-with-backoff wrapper around Invoke-WebRequest -# ============================================================ -# Defined locally so this script stays independently runnable. The Update Catalog -# throttles aggressively; retry up to 3 times with increasing delay, then re-throw so -# callers can try/catch and skip. NOTE: not available inside ForEach-Object -Parallel -# runspaces (those carry their own inline retry loop). -function Invoke-CatalogRequest { - param ( - [Parameter(Mandatory)][hashtable]$Params, - [int]$MaxAttempts = 3 - ) - for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { - try { - return Invoke-WebRequest @Params - } - catch { - if ($attempt -ge $MaxAttempts) { throw } - Start-Sleep -Seconds ($attempt * 2) - } - } -} - -$Targets = @( - @{ - Name = "RTL_PCIe_Family" - Devices = @( - @{ Prefix = "1125"; HWID = "VEN_10EC&DEV_8125"; RTLName = "RTL8125" }, - @{ Prefix = "1126"; HWID = "VEN_10EC&DEV_8126"; RTLName = "RTL8126" }, - @{ Prefix = "1127"; HWID = "VEN_10EC&DEV_8127"; RTLName = "RTL8127" }, - @{ Prefix = "1168"; HWID = "VEN_10EC&DEV_8168"; RTLName = "RTL8168" } - ) - }, - @{ - Name = "RTL_USB_Family" - Devices = @( - @{ Prefix = "1153"; HWID = "VID_0BDA&PID_8153"; RTLName = "RTL8153" }, - @{ Prefix = "1156"; HWID = "VID_0BDA&PID_8156"; RTLName = "RTL8156" }, - @{ Prefix = "1157"; HWID = "VID_0BDA&PID_8157"; RTLName = "RTL8157" }, - @{ Prefix = "1159"; HWID = "VID_0BDA&PID_815A"; RTLName = "RTL8159" } - ) - } + [string]$DownloadPath = 'C:\Temp', + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64' ) -if (-not (Test-Path $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } - -# Acquisition manifest — greppable ACQUIRED:/SKIPPED: lines emitted at the end so a -# throttled/partial parallel run is auditable. -$Manifest = [System.Collections.Generic.List[string]]::new() - -foreach ($Target in $Targets) { - Write-Host "`n=> Investigating Microsoft Update Catalog for $($Target.Name)..." -ForegroundColor Cyan - - $AvailablePackages = @() - - foreach ($Device in $Target.Devices) { - $Prefix = $Device.Prefix - $HWID = $Device.HWID - $RTLName = $Device.RTLName - $Query = "$HWID Windows 11" - Write-Host " -> Searching specific HWID for Prefix $Prefix ($Query)..." - - # Reset per-iteration so a failed fetch can't silently reuse the PREVIOUS - # device's page (which would parse the wrong update IDs for this device). - $SearchPage = $null - try { - $SearchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$([uri]::EscapeDataString($Query))" - $SearchPage = Invoke-CatalogRequest -Params @{ Uri = $SearchUrl; UseBasicParsing = $true } - } - catch { - Write-Warning "Search request failed for $Prefix ($Query): $_" - $Manifest.Add("SKIPPED: $RTLName (search request failed)") - continue - } - - # LIMITATION: Search.aspx returns only the first 25 relevance-sorted rows; the - # "newest by date" pick below only sees those 25. Full pagination needs - # __EVENTTARGET POST-backs (invasive). Parse the "1 - N of M" total and warn if M > 25. - if ($SearchPage.Content -match '(\d+)\s*-\s*(\d+)\s+of\s+(\d+)') { - $totalResults = [int]$matches[3] - if ($totalResults -gt 25) { - Write-Warning "Catalog reports $totalResults results for '$Query' but only the first 25 are parsed (pagination not implemented)." - } - } - - # Extract all update IDs from the search results table - $UpdateIds = [regex]::Matches($SearchPage.Content, "goToDetails\(['""]([a-f0-9\-]+)['""]\)") | - ForEach-Object { $_.Groups[1].Value } | - Select-Object -Unique - - if (-not $UpdateIds) { - Write-Host " [!] No candidates found." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $RTLName (no catalog candidates)") - continue - } - - Write-Host " -> Found $($UpdateIds.Count) packages. Fetching deep versions..." -NoNewline - - $DetailResults = $UpdateIds | ForEach-Object -Parallel { - $Id = $_ - $DetailsUrl = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$Id" - try { - # Inline retry-with-backoff (the script-scope helper is not visible here). - $DetailsPage = $null - for ($attempt = 1; $attempt -le 3; $attempt++) { - try { $DetailsPage = Invoke-WebRequest -Uri $DetailsUrl -UseBasicParsing; break } - catch { if ($attempt -ge 3) { throw } else { Start-Sleep -Seconds ($attempt * 2) } } - } - $DateString = if ($DetailsPage.Content -match 'id="ScopedViewHandler_versionDate">([^<]+)') { $matches[1].Trim() } - $Version = if ($DetailsPage.Content -match 'id="ScopedViewHandler_version">([^<]+)') { $matches[1].Trim() } - - if ($Version -and $DateString) { - $DateObj = [datetime]::Parse($DateString) - $Arch = if ($DetailsPage.Content -match "ARM64") { "ARM64" } elseif ($DetailsPage.Content -match "AMD64|x64|amd64") { "AMD64" } else { "x86" } - [PSCustomObject]@{ - Version = $Version - DateObj = $DateObj - Id = $Id - Arch = $Arch - } - } - } - catch { - Write-Warning "Detail fetch/parse failed for update $Id : $($_.Exception.Message)" - } - } -ThrottleLimit 8 - - Write-Host " Done." - - foreach ($result in $DetailResults) { - if ($result -and $result.Arch -in $AcceptedArchs) { - $AvailablePackages += [PSCustomObject]@{ - Prefix = $Prefix - RTLName = $RTLName - Version = $result.Version - DateObj = $result.DateObj - Id = $result.Id - Arch = $result.Arch - } - } - } - } - - if (-not $AvailablePackages) { - Write-Host " [!] No matching prefixes found within candidate packages." -ForegroundColor Yellow - $Manifest.Add("SKIPPED: $($Target.Name) (no packages matched accepted architectures)") - continue - } - - # Group all valid candidate packages into their correct respective HWPrefix families and Architecture, and find the freshest. - $GroupedPackages = $AvailablePackages | Group-Object Prefix, Arch - - foreach ($Group in $GroupedPackages) { - $FirstObj = $Group.Group[0] - $Prefix = $FirstObj.Prefix - $Arch = $FirstObj.Arch - $RTLName = $FirstObj.RTLName - - $BestPackage = $Group.Group | Sort-Object DateObj -Descending | Select-Object -First 1 - Write-Host " -> Prefix $($Prefix) [$Arch]: Selected $($BestPackage.Version) (Update ID: $($BestPackage.Id))" -ForegroundColor Green - - $DownloadPage = $null - $PostData = "[{`"size`":0,`"updateID`":`"$($BestPackage.Id)`",`"uidInfo`":`"$($BestPackage.Id)`"}]" - try { - $DownloadPage = Invoke-CatalogRequest -Params @{ Uri = "https://www.catalog.update.microsoft.com/DownloadDialog.aspx"; Method = 'Post'; Body = @{updateIDs = $PostData }; UseBasicParsing = $true } - } - catch { - Write-Warning "Download dialog request failed for $RTLName [$Arch]: $_" - $Manifest.Add("SKIPPED: $RTLName [$Arch] (download dialog failed)") - continue - } - - $CabUrl = [regex]::Match($DownloadPage.Content, 'https://[^''"<]+\.cab').Value - - if (-not $CabUrl) { - Write-Host " [!] Could not extract .cab URL from payload." -ForegroundColor Red - $Manifest.Add("SKIPPED: $RTLName [$Arch] (no .cab URL in payload)") - continue - } - - $CabFile = Join-Path $DownloadPath "$($Target.Name)_$($Prefix)_$($Arch).cab" - $ExtractDir = Join-Path $DownloadPath "$($Target.Name)\$RTLName\$Arch" - - Write-Host " -> Downloading raw $Arch driver package..." - try { - Invoke-CatalogRequest -Params @{ Uri = $CabUrl; OutFile = $CabFile; UseBasicParsing = $true } | Out-Null - } - catch { - Write-Warning "CAB download failed for $RTLName [$Arch]: $_" - $Manifest.Add("SKIPPED: $RTLName [$Arch] (download failed)") - continue - } - - # AUTHENTICITY: this CAB is extracted into boot-start kernel drivers. Verify the - # publisher's Authenticode signature and SKIP (do not extract/inject) anything - # that is not 'Valid', rather than silently trusting it. - $sig = Get-AuthenticodeSignature -FilePath $CabFile - if ($sig.Status -ne 'Valid') { - Write-Warning "Authenticode signature for $RTLName [$Arch] CAB is '$($sig.Status)' (not Valid) — skipping extraction/injection." - $Manifest.Add("SKIPPED: $RTLName [$Arch] (signature $($sig.Status))") - Remove-Item $CabFile -Force -ErrorAction SilentlyContinue - continue - } - - Write-Host " -> Extracting payload using expand.exe..." - if (-not (Test-Path $ExtractDir)) { New-Item -ItemType Directory -Path $ExtractDir -Force | Out-Null } - - # Capture expand.exe's exit code. On failure KEEP the source CAB for retry/inspection. - $expandProc = Start-Process "expand.exe" -ArgumentList "-F:* `"$CabFile`" `"$ExtractDir`"" -NoNewWindow -PassThru - $expandProc.WaitForExit() - if ($expandProc.ExitCode -ne 0) { - Write-Warning "expand.exe exited with code $($expandProc.ExitCode) for $RTLName [$Arch] — keeping source CAB '$CabFile' for retry/inspection." - $Manifest.Add("SKIPPED: $RTLName [$Arch] (expand.exe exit $($expandProc.ExitCode))") - continue - } - Remove-Item $CabFile -Force - - # Manifest: confirm an actual .inf + .sys landed (an empty/partial extract is a skip). - $infCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.inf -ErrorAction SilentlyContinue).Count - $sysCount = @(Get-ChildItem -Path $ExtractDir -Recurse -Filter *.sys -ErrorAction SilentlyContinue).Count - if ($infCount -gt 0 -and $sysCount -gt 0) { - $Manifest.Add("ACQUIRED: $RTLName [$Arch]") - } - else { - $Manifest.Add("SKIPPED: $RTLName [$Arch] (no .inf/.sys after extract)") - } - - if ($Install) { - $SysArch = $env:PROCESSOR_ARCHITECTURE - if ($SysArch -eq $Arch) { - Write-Host " -> System is $SysArch. Injecting $Arch driver into Driver Store via pnputil..." -ForegroundColor Green - pnputil.exe /add-driver "$ExtractDir\*.inf" /install | Out-Null - Write-Host " -> Injection complete." -ForegroundColor Green - } - else { - Write-Host " -> System is $SysArch. Skipping $Arch driver installation." -ForegroundColor DarkGray - } - } - else { - Write-Host " -> Extracted to: $ExtractDir (Skipping installation)" -ForegroundColor DarkGray - } - } -} - -# ============================================================ -# Acquisition manifest (greppable) -# ============================================================ -Write-Host "`n=> Acquisition manifest:" -ForegroundColor Cyan -if ($Manifest.Count -eq 0) { - Write-Host " (no device families processed)" -ForegroundColor DarkGray -} -else { - foreach ($line in $Manifest) { - if ($line -like 'ACQUIRED:*') { Write-Host " $line" -ForegroundColor Green } - else { Write-Host " $line" -ForegroundColor Yellow } - } -} - -Write-Host "`nProcess complete." -ForegroundColor Cyan \ No newline at end of file +$csDir = Join-Path $PSScriptRoot 'catalogscrape' +Import-Module (Join-Path $csDir 'CatalogScrape.psm1') -Force +$config = Import-PowerShellDataFile (Join-Path $csDir 'realtek.psd1') +Invoke-DriverScrape -Config $config -Install:$Install -DownloadPath $DownloadPath -Architecture $Architecture diff --git a/catalogscrape/CatalogScrape.psm1 b/catalogscrape/CatalogScrape.psm1 new file mode 100644 index 0000000..5dc8ecf --- /dev/null +++ b/catalogscrape/CatalogScrape.psm1 @@ -0,0 +1,474 @@ +#Requires -Version 7.0 +<# + CatalogScrape.psm1 — shared engine for the Microsoft Update Catalog driver scrapers. + + Replaces ~120 lines of boilerplate that were copy-pasted (and had drifted) across the + six Get-*Drivers.ps1 scripts. Each vendor is now a thin shim that loads a per-vendor + .psd1 device table and calls Invoke-DriverScrape. + + Selection policy is "absolute newest" (per project decision): highest parsed [version] + wins, with catalog date as a tiebreak and any PreferredBranches as a final tiebreak. + This single rule fixes the prior date-vs-version divergence (Intel re-release picking a + lower build, Realtek USB picking a stale-dated older build, MediaTek preferred-branch + filtering out the newer 26.30 branch). + + Pure helpers (parsing/version/date/selection) are factored out and unit-tested on any OS + via catalogscrape/Test-CatalogScrape.ps1; the Windows-only I/O (Authenticode/expand/ + pnputil) mirrors the original scripts verbatim. + + NOTE: the per-update detail fetch uses ForEach-Object -Parallel. Module functions are NOT + visible inside -Parallel runspaces, so that block only does the network fetch (built-in + cmdlets + $using:) and returns raw HTML; parsing happens afterward in module scope via the + single ConvertFrom-CsDetailHtml function (no duplicated parse logic). +#> + +# ============================================================ +# Pure helpers (OS-agnostic, network-free, unit-tested) +# ============================================================ + +function Get-CsAcceptedArch { + param([string]$Architecture) + $arr = switch ($Architecture) { + 'x64' { @('AMD64') } + 'arm64' { @('ARM64') } + 'all' { @('AMD64', 'ARM64') } + default { @('AMD64') } + } + return , $arr +} + +function Get-CsCatalogTotal { + # Parse the "N - M of TOTAL" row counter; -1 when absent. + param([string]$Content) + if ($Content -match '(\d+)\s*-\s*(\d+)\s+of\s+(\d+)') { return [int]$matches[3] } + return -1 +} + +function Get-CsCatalogUpdateId { + # Extract the update GUIDs from goToDetails(''). The JS function definition + # goToDetails(updateID) carries no quotes so it cannot match; the trailing GUID-shape + # filter is belt-and-suspenders. + param([string]$Content) + $ids = [regex]::Matches($Content, "goToDetails\(['""]([a-f0-9\-]+)['""]\)") | + ForEach-Object { $_.Groups[1].Value } | + Select-Object -Unique + return @($ids | Where-Object { $_ -match '^[a-f0-9]{8}-' }) +} + +function Expand-CsQuery { + # Dual-query union: HWID-shaped queries (VEN_xxxx&DEV_xxxx / VID_xxxx&PID_xxxx) are issued + # in BOTH the bare and the "+ Windows 11" form, to sample two independent relevance windows. + # The catalog returns only the first 25 relevance-sorted rows per query and REJECTS scripted + # sort/pagination postbacks (verified: both MSCatalog and a hand-rolled VIEWSTATE replay get + # an error page), so two windows widen coverage without depending on either form being the + # "right" one — update IDs are deduped across the union and highest [version] wins. Name / + # phrase queries (marketing names like "Killer AX500", "Marvell FastLinQ") are used as-is; + # appending an OS token to a name distorts or zeroes the search. + param([string]$Query) + if ($Query -match '^(VEN|VID)_[0-9A-Fa-f]+&(DEV|PID)_[0-9A-Fa-f]+$') { + return @($Query, "$Query Windows 11") + } + return , @($Query) +} + +function ConvertTo-CsVersion { + # Tolerant version parse: strip a leading v, take up to 4 leading numeric groups, + # default the missing components to 0. Returns $null only when there is no leading + # number at all (caller logs + drops). Replaces the bare [version] cast that silently + # discarded any non-strictly-dotted string inside an empty catch{}. + param([string]$Raw) + if ([string]::IsNullOrWhiteSpace($Raw)) { return $null } + $m = [regex]::Match($Raw.Trim(), '^[vV]?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?') + if (-not $m.Success) { return $null } + # The [int] cast is inside the try so an out-of-Int32-range component returns $null + # (per contract) instead of throwing and aborting the scrape. + try { + $parts = for ($i = 1; $i -le 4; $i++) { + if ($m.Groups[$i].Success) { [int]$m.Groups[$i].Value } else { 0 } + } + return [version]::new($parts[0], $parts[1], $parts[2], $parts[3]) + } + catch { return $null } +} + +function ConvertTo-CsDate { + # Locale-independent date parse (catalog serves US M/d/yyyy). Replaces + # [datetime]::Parse which is current-culture and can scramble the sort on non-US hosts. + param([string]$Raw) + if ([string]::IsNullOrWhiteSpace($Raw)) { return $null } + # Must be [string[]] or overload resolution picks the single-format ParseExact and fails. + [string[]]$fmts = @('M/d/yyyy', 'MM/dd/yyyy', 'yyyy-MM-dd', 'M/d/yyyy h:mm:ss tt') + try { + return [datetime]::ParseExact($Raw.Trim(), $fmts, [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) + } + catch { return $null } +} + +function ConvertFrom-CsDetailHtml { + # Parse one ScopedViewInline detail page. Returns $null unless BOTH version and + # versionDate are present (matches the original contract). Version is returned RAW; + # the orchestrator converts it via ConvertTo-CsVersion. Arch heuristic preserved + # verbatim from the originals (known-good in practice; the catalog exposes no clean + # single-token Architecture value). + param( + [Parameter(Mandatory)][string]$Content, + [Parameter(Mandatory)][string]$Id + ) + $title = if ($Content -match 'id="ScopedViewHandler_titleText">([^<]+)') { $matches[1].Trim() } else { $null } + $verRaw = if ($Content -match 'id="ScopedViewHandler_version">([^<]+)') { $matches[1].Trim() } else { $null } + $dateRaw = if ($Content -match 'id="ScopedViewHandler_versionDate">([^<]+)') { $matches[1].Trim() } else { $null } + if (-not $verRaw -or -not $dateRaw) { return $null } + $dateObj = ConvertTo-CsDate $dateRaw + if (-not $dateObj) { $dateObj = [datetime]::MinValue } + $arch = if ($Content -match 'ARM64') { 'ARM64' } + elseif ($Content -match 'AMD64|x64|amd64') { 'AMD64' } + else { 'x86' } + return [pscustomobject]@{ + Id = $Id + Title = $title + Version = $verRaw + DateObj = $dateObj + Arch = $arch + } +} + +function Test-CsTitleExcluded { + # True if Title matches any exclude pattern (NDIS / bluetooth|uart for WiFi combos). + param([string]$Title, [string[]]$Patterns) + if (-not $Patterns) { return $false } + if ([string]::IsNullOrEmpty($Title)) { return $false } + foreach ($p in $Patterns) { if ($Title -match $p) { return $true } } + return $false +} + +function Get-CsBranchRank { + # Index of the first PreferredBranch the version's major.minor begins with, else MaxValue. + param([version]$Version, [string[]]$PreferredBranches) + if (-not $PreferredBranches) { return [int]::MaxValue } + $v = $Version.ToString() + for ($i = 0; $i -lt $PreferredBranches.Count; $i++) { + if ($v.StartsWith("$($PreferredBranches[$i]).")) { return $i } + } + return [int]::MaxValue +} + +function Select-CsBestPackage { + # "Absolute newest": highest [version] first, then newest DateObj, then preferred-branch + # rank as a final tiebreak. Packages must already carry a [version] Version and a DateObj. + param( + [Parameter(Mandatory)][object[]]$Packages, + [string[]]$PreferredBranches = @() + ) + if (-not $Packages -or $Packages.Count -eq 0) { return $null } + return $Packages | Sort-Object ` + @{ Expression = { $_.Version }; Descending = $true }, ` + @{ Expression = { $_.DateObj }; Descending = $true }, ` + @{ Expression = { Get-CsBranchRank -Version $_.Version -PreferredBranches $PreferredBranches }; Descending = $false } | + Select-Object -First 1 +} + +# ============================================================ +# Network helpers +# ============================================================ + +function Test-CsAdmin { + try { + return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } + catch { return $false } +} + +function Invoke-CsRequest { + # Retry-with-backoff wrapper. Adds a bounded -TimeoutSec (was absent everywhere — a + # stalled connection used to hang the whole run) and a browser User-Agent. + param( + [Parameter(Mandatory)][hashtable]$Params, + [int]$MaxAttempts = 3, + [int]$TimeoutSec = 60 + ) + if (-not $Params.ContainsKey('TimeoutSec')) { $Params['TimeoutSec'] = $TimeoutSec } + if (-not $Params.ContainsKey('UserAgent')) { $Params['UserAgent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) lan-ipxe-catalog-scrape' } + for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { + try { return Invoke-WebRequest @Params } + catch { + if ($attempt -ge $MaxAttempts) { throw } + Start-Sleep -Seconds ($attempt * 2) + } + } +} + +function Get-CsDetail { + # Parallel detail-page FETCH only (self-contained -Parallel block: built-ins + $using + # plus an inline retry, because module functions are invisible in the runspace). Parsing + # is done afterward in module scope so the regexes live in exactly one place. + param( + [string[]]$UpdateIds, + [int]$ThrottleLimit = 6, + [int]$TimeoutSec = 60 + ) + if (-not $UpdateIds) { return @() } + $raw = $UpdateIds | ForEach-Object -Parallel { + $ProgressPreference = 'SilentlyContinue' # parallel runspaces start fresh; suppress progress bars + $Id = $_ + $url = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$Id" + $to = $using:TimeoutSec + $content = $null + for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + $content = (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec $to -UserAgent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) lan-ipxe-catalog-scrape').Content + break + } + catch { + if ($attempt -ge 3) { Write-Warning "Detail fetch failed for update $Id : $($_.Exception.Message)" } + else { Start-Sleep -Seconds ($attempt * 2) } + } + } + [pscustomobject]@{ Id = $Id; Content = $content } + } -ThrottleLimit $ThrottleLimit + + $out = foreach ($r in $raw) { + if (-not $r.Content) { continue } + ConvertFrom-CsDetailHtml -Content $r.Content -Id $r.Id + } + return @($out) +} + +# ============================================================ +# Orchestrator +# ============================================================ + +function Invoke-DriverScrape { + [CmdletBinding()] + param( + [Parameter(Mandatory)][hashtable]$Config, + [switch]$Install, + [Parameter(Mandatory)][string]$DownloadPath, + [ValidateSet('x64', 'arm64', 'all')][string]$Architecture = 'x64', + [int]$MetadataTimeoutSec = 60, + [int]$DownloadTimeoutSec = 600, + [int]$ThrottleLimit = 6 + ) + + $ProgressPreference = 'SilentlyContinue' + $AcceptedArchs = Get-CsAcceptedArch $Architecture + + if ($Install -and -not (Test-CsAdmin)) { + Write-Error "The -Install flag requires Administrator privileges. Please run PowerShell as Administrator." + return + } + + if (-not (Test-Path $DownloadPath)) { New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null } + $DownloadPath = [System.IO.Path]::GetFullPath($DownloadPath) + # Namespace by vendor so concurrent scrapers sharing one root path never collide on + # CAB filenames or extract dirs (build_win11pxe.ps1 hands every scraper the same root). + $vendorRoot = Join-Path $DownloadPath $Config.VendorKey + if (-not (Test-Path $vendorRoot)) { New-Item -ItemType Directory -Path $vendorRoot -Force | Out-Null } + + $Manifest = [System.Collections.Generic.List[string]]::new() + + foreach ($Target in $Config.Targets) { + Write-Host "`n=> Investigating Microsoft Update Catalog for $($Target.Name)..." -ForegroundColor Cyan + $AvailablePackages = @() + + foreach ($Device in $Target.Devices) { + $deviceKey = $Device.Key + $label = $Device.Label + $titleExclude = if ($Device.ContainsKey('TitleExclude')) { @($Device.TitleExclude) } else { @() } + $seenIds = [System.Collections.Generic.HashSet[string]]::new() + + # Expand each base query into its dual-query-union variants, then dedup. + $queries = @() + foreach ($baseQuery in @($Device.Queries)) { $queries += Expand-CsQuery $baseQuery } + $queries = @($queries | Select-Object -Unique) + + foreach ($query in $queries) { + Write-Host " -> Searching $label ($query)..." + + $searchPage = $null + try { + $searchUrl = "https://www.catalog.update.microsoft.com/Search.aspx?q=$([uri]::EscapeDataString($query))" + $searchPage = Invoke-CsRequest -Params @{ Uri = $searchUrl; UseBasicParsing = $true } -TimeoutSec $MetadataTimeoutSec + } + catch { + Write-Warning "Search request failed for $label ($query): $_" + continue + } + + $total = Get-CsCatalogTotal $searchPage.Content + if ($total -gt 25) { + Write-Warning "Catalog reports $total results for '$query' but only the first 25 are parsed (pagination not implemented)." + } + + $updateIds = Get-CsCatalogUpdateId $searchPage.Content + if (-not $updateIds) { + Write-Host " [!] No candidates for this query." -ForegroundColor Yellow + continue + } + + Write-Host " -> $($updateIds.Count) candidates. Fetching detail pages..." -NoNewline + $details = Get-CsDetail -UpdateIds $updateIds -ThrottleLimit $ThrottleLimit -TimeoutSec $MetadataTimeoutSec + Write-Host " Done." + + foreach ($d in $details) { + if (-not $d) { continue } + if ($d.Arch -notin $AcceptedArchs) { continue } + if (Test-CsTitleExcluded -Title $d.Title -Patterns $titleExclude) { continue } + if (-not $seenIds.Add($d.Id)) { continue } # dedup across this device's queries + + $ver = ConvertTo-CsVersion $d.Version + if (-not $ver) { + Write-Warning "Dropped $label candidate $($d.Id): unparseable version '$($d.Version)'." + continue + } + + $AvailablePackages += [pscustomobject]@{ + Key = $deviceKey + Label = $label + Version = $ver + VersionRaw = $d.Version + DateObj = $d.DateObj + Id = $d.Id + Arch = $d.Arch + Title = $d.Title + PreferredBranches = if ($Device.ContainsKey('PreferredBranches')) { @($Device.PreferredBranches) } else { @() } + } + } + } + } + + if (-not $AvailablePackages) { + Write-Host " [!] No packages matched accepted architectures." -ForegroundColor Yellow + $Manifest.Add("SKIPPED: $($Target.Name) (no packages matched accepted architectures)") + continue + } + + # Mark devices that yielded nothing so the manifest stays complete. + $foundKeys = @($AvailablePackages.Key | Select-Object -Unique) + foreach ($Device in $Target.Devices) { + if ($Device.Key -notin $foundKeys) { + $Manifest.Add("SKIPPED: $($Device.Label) (no catalog candidates)") + } + } + + $groups = $AvailablePackages | Group-Object Key, Arch + foreach ($group in $groups) { + $first = $group.Group[0] + $deviceKey = $first.Key + $label = $first.Label + $arch = $first.Arch + $best = Select-CsBestPackage -Packages $group.Group -PreferredBranches @($first.PreferredBranches) + + $branchNote = '' + if ($first.PreferredBranches) { + $rank = Get-CsBranchRank -Version $best.Version -PreferredBranches @($first.PreferredBranches) + $branchNote = if ($rank -lt [int]::MaxValue) { " [branch $($first.PreferredBranches[$rank])]" } else { " [highest version]" } + } + Write-Host " -> $label [$arch]: selected v$($best.VersionRaw)$branchNote (Update ID: $($best.Id))" -ForegroundColor Green + + # --- DownloadDialog -> .cab URL --- + $postData = "[{`"size`":0,`"updateID`":`"$($best.Id)`",`"uidInfo`":`"$($best.Id)`"}]" + $downloadPage = $null + try { + $downloadPage = Invoke-CsRequest -Params @{ Uri = 'https://www.catalog.update.microsoft.com/DownloadDialog.aspx'; Method = 'Post'; Body = @{ updateIDs = $postData }; UseBasicParsing = $true } -TimeoutSec $MetadataTimeoutSec + } + catch { + Write-Warning "Download dialog request failed for $label [$arch]: $_" + $Manifest.Add("SKIPPED: $label [$arch] (download dialog failed)") + continue + } + + $cabUrl = [regex]::Match($downloadPage.Content, 'https://[^''"<]+\.cab').Value + if (-not $cabUrl) { + Write-Host " [!] Could not extract .cab URL from payload." -ForegroundColor Red + $Manifest.Add("SKIPPED: $label [$arch] (no .cab URL in payload)") + continue + } + + $cabFile = Join-Path $vendorRoot "$($Target.Name)_$($deviceKey)_$($arch).cab" + $extractDir = Join-Path $vendorRoot "$($Target.Name)\$deviceKey\$arch" + + Write-Host " -> Downloading $arch package..." + try { + Invoke-CsRequest -Params @{ Uri = $cabUrl; OutFile = $cabFile; UseBasicParsing = $true } -TimeoutSec $DownloadTimeoutSec | Out-Null + } + catch { + Write-Warning "CAB download failed for $label [$arch]: $_" + $Manifest.Add("SKIPPED: $label [$arch] (download failed)") + continue + } + + # AUTHENTICITY: catalog driver CABs carry an embedded WHQL Authenticode signature + # (verified: PKCS#7 in the cabinet reserve, chains to Microsoft Root CA 2010), so + # Get-AuthenticodeSignature returns 'Valid'. Fail closed: skip anything not Valid. + $sig = Get-AuthenticodeSignature -FilePath $cabFile + if ($sig.Status -ne 'Valid') { + Write-Warning "Authenticode signature for $label [$arch] CAB is '$($sig.Status)' (not Valid) — skipping extraction/injection." + $Manifest.Add("SKIPPED: $label [$arch] (signature $($sig.Status))") + Remove-Item $cabFile -Force -ErrorAction SilentlyContinue + continue + } + + Write-Host " -> Extracting via expand.exe..." + if (-not (Test-Path $extractDir)) { New-Item -ItemType Directory -Path $extractDir -Force | Out-Null } + + $expandProc = Start-Process 'expand.exe' -ArgumentList "-F:* `"$cabFile`" `"$extractDir`"" -NoNewWindow -PassThru -Wait + # Non-zero is a failure; a $null ExitCode (rare) is treated as success and caught + # by the .inf/.sys verification below rather than misflagged as a failure. + if ($expandProc.ExitCode) { + Write-Warning "expand.exe exited $($expandProc.ExitCode) for $label [$arch] — keeping CAB '$cabFile' for inspection." + $Manifest.Add("SKIPPED: $label [$arch] (expand.exe exit $($expandProc.ExitCode))") + continue + } + Remove-Item $cabFile -Force -ErrorAction SilentlyContinue + + $infCount = @(Get-ChildItem -Path $extractDir -Recurse -Filter *.inf -ErrorAction SilentlyContinue).Count + $sysCount = @(Get-ChildItem -Path $extractDir -Recurse -Filter *.sys -ErrorAction SilentlyContinue).Count + if ($infCount -gt 0 -and $sysCount -gt 0) { + $Manifest.Add("ACQUIRED: $label [$arch]") + } + else { + # Remove the incomplete extract so a downstream DISM /Add-Driver /Recurse + # cannot pick up a broken package. + Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue + $Manifest.Add("SKIPPED: $label [$arch] (no .inf/.sys after extract)") + continue + } + + if ($Install) { + $sysArch = $env:PROCESSOR_ARCHITECTURE + if ($sysArch -eq $arch) { + Write-Host " -> System is $sysArch. Injecting via pnputil (/subdirs)..." -ForegroundColor Green + # /subdirs so injection matches the recursive .inf detection above. + pnputil.exe /add-driver (Join-Path $extractDir '*.inf') /subdirs /install | Out-Null + Write-Host " -> Injection complete." -ForegroundColor Green + } + else { + Write-Host " -> System is $sysArch. Skipping $arch injection." -ForegroundColor DarkGray + } + } + else { + Write-Host " -> Extracted to: $extractDir (not installing)" -ForegroundColor DarkGray + } + } + } + + Write-Host "`n=> Acquisition manifest:" -ForegroundColor Cyan + if ($Manifest.Count -eq 0) { + Write-Host " (no device families processed)" -ForegroundColor DarkGray + } + else { + foreach ($line in $Manifest) { + if ($line -like 'ACQUIRED:*') { Write-Host " $line" -ForegroundColor Green } + else { Write-Host " $line" -ForegroundColor Yellow } + } + } + Write-Host "`nProcess complete." -ForegroundColor Cyan +} + +Export-ModuleMember -Function @( + 'Invoke-DriverScrape', + 'Invoke-CsRequest', 'Get-CsDetail', + 'Get-CsAcceptedArch', 'Get-CsCatalogTotal', 'Get-CsCatalogUpdateId', 'Expand-CsQuery', + 'ConvertTo-CsVersion', 'ConvertTo-CsDate', 'ConvertFrom-CsDetailHtml', + 'Test-CsTitleExcluded', 'Get-CsBranchRank', 'Select-CsBestPackage' +) diff --git a/catalogscrape/Test-CatalogScrape.ps1 b/catalogscrape/Test-CatalogScrape.ps1 new file mode 100644 index 0000000..0f22e81 --- /dev/null +++ b/catalogscrape/Test-CatalogScrape.ps1 @@ -0,0 +1,150 @@ +#Requires -Version 7.0 +# Network-free unit tests for CatalogScrape.psm1 pure logic. Runs on any OS. +# Usage: pwsh -NoProfile -File catalogscrape/Test-CatalogScrape.ps1 [fixtureDir] +param([string]$FixtureDir = '/tmp') + +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot 'CatalogScrape.psm1') -Force + +$script:pass = 0; $script:fail = 0 +function Assert([scriptblock]$Test, [string]$Name) { + $ok = $false + try { $ok = [bool](& $Test) } catch { $Name += " [ERR: $($_.Exception.Message)]" } + if ($ok) { $script:pass++; " ok $Name" } else { $script:fail++; " FAIL $Name" } +} +function Pkg($ver, $date, $branches = @()) { + [pscustomobject]@{ Version = [version]$ver; DateObj = [datetime]$date; PreferredBranches = $branches; VersionRaw = $ver } +} + +'== Get-CsAcceptedArch ==' +Assert { ((Get-CsAcceptedArch 'x64') -join ',') -eq 'AMD64' } 'x64 -> AMD64' +Assert { ((Get-CsAcceptedArch 'all') -join ',') -eq 'AMD64,ARM64' } 'all -> AMD64,ARM64' +Assert { ((Get-CsAcceptedArch 'arm64') -join ',') -eq 'ARM64' } 'arm64 -> ARM64' + +'== ConvertTo-CsVersion (tolerant) ==' +Assert { (ConvertTo-CsVersion '2.1.5.10') -eq [version]'2.1.5.10' } 'plain 4-part' +Assert { (ConvertTo-CsVersion 'v1.2') -eq [version]'1.2.0.0' } 'leading v, pad' +Assert { (ConvertTo-CsVersion '11.19.602.2025') -eq [version]'11.19.602.2025' } 'realtek build' +Assert { (ConvertTo-CsVersion '26.30.3.62') -eq [version]'26.30.3.62' } 'mediatek 26.30' +Assert { (ConvertTo-CsVersion '23.110.0.0 (WHQL)') -eq [version]'23.110.0.0' } 'trailing junk stripped' +Assert { $null -eq (ConvertTo-CsVersion 'garbage') } 'non-numeric -> null' +Assert { $null -eq (ConvertTo-CsVersion '') } 'empty -> null' + +'== ConvertTo-CsDate (invariant) ==' +Assert { (ConvertTo-CsDate '12/29/2025') -eq [datetime]'2025-12-29' } 'M/d/yyyy' +Assert { (ConvertTo-CsDate '3/30/2018') -eq [datetime]'2018-03-30' } 'single-digit M' +Assert { (ConvertTo-CsDate '5/13/2019 12:00:00 AM') -eq [datetime]'2019-05-13' } 'with time suffix' +Assert { $null -eq (ConvertTo-CsDate 'not a date') } 'invalid -> null' + +'== Get-CsCatalogTotal / Get-CsCatalogUpdateId (inline fixture matching live HTML shape) ==' +# If real captured fixtures exist in $FixtureDir they are used; otherwise an inline fixture +# that reproduces the exact live markup shape keeps the test self-contained on any machine. +$searchFixture = Join-Path $FixtureDir 'fixture_search_intel.html' +if (Test-Path $searchFixture) { + $searchHtml = Get-Content $searchFixture -Raw + $expectedTotal = 307; $expectedIds = 25 +} +else { + $searchHtml = @' + +1 - 3 of 307 +x +y +dup +z +'@ + $expectedTotal = 307; $expectedIds = 3 +} +Assert { (Get-CsCatalogTotal $searchHtml) -eq $expectedTotal } "total = $expectedTotal" +$ids = Get-CsCatalogUpdateId $searchHtml +Assert { $ids.Count -eq $expectedIds } "$expectedIds unique GUIDs (got $($ids.Count))" +Assert { $ids -notcontains 'updateID' } 'JS function-def placeholder excluded' +Assert { $ids[0] -match '^[a-f0-9]{8}-[a-f0-9]{4}-' } 'GUID shape' + +'== ConvertFrom-CsDetailHtml ==' +$detailFixture = Join-Path $FixtureDir 'fixture_detail_intel.html' +$detailHtml = if (Test-Path $detailFixture) { Get-Content $detailFixture -Raw } else { @' +Intel Net Driver Update (2.1.5.10) +
Architecture: AMD64
+2.1.5.10 +12/29/2025 +'@ } +$d = ConvertFrom-CsDetailHtml -Content $detailHtml -Id 'test-id' +Assert { $null -ne $d } 'parsed non-null' +Assert { $d.Version -eq '2.1.5.10' } "version 2.1.5.10 (got '$($d.Version)')" +Assert { $d.DateObj -eq [datetime]'2025-12-29' } 'versionDate 12/29/2025' +Assert { $d.Arch -eq 'AMD64' } "arch AMD64 (got '$($d.Arch)')" +Assert { $d.Title -like 'Intel Net Driver Update*' } 'title parsed' +Assert { $null -eq (ConvertFrom-CsDetailHtml -Content 'no fields' -Id 'x') } 'missing version/date -> null' + +'== Test-CsTitleExcluded ==' +Assert { Test-CsTitleExcluded 'Qualcomm Bluetooth UART' @('NDIS', '(?i)bluetooth|uart') } 'bluetooth excluded' +Assert { Test-CsTitleExcluded 'Legacy NDIS adapter' @('NDIS') } 'NDIS excluded' +Assert { -not (Test-CsTitleExcluded 'WCN6855 Wi-Fi' @('NDIS', '(?i)bluetooth|uart')) } 'wifi kept' +Assert { -not (Test-CsTitleExcluded 'anything' @()) } 'empty patterns -> kept' + +'== Expand-CsQuery (dual-query union for HWID-shaped queries) ==' +$exHwid = Expand-CsQuery 'VEN_8086&DEV_1592' +Assert { @($exHwid).Count -eq 2 } 'HWID expands to 2 variants' +Assert { $exHwid -contains 'VEN_8086&DEV_1592' -and $exHwid -contains 'VEN_8086&DEV_1592 Windows 11' } 'HWID -> bare + Windows 11' +$exUsb = Expand-CsQuery 'VID_2ECA&PID_C101' +Assert { (@($exUsb).Count -eq 2) -and ($exUsb -contains 'VID_2ECA&PID_C101 Windows 11') } 'USB VID/PID expands too' +Assert { @(Expand-CsQuery 'Killer AX500').Count -eq 1 } 'marketing name not expanded' +Assert { @(Expand-CsQuery 'Marvell FastLinQ')[0] -eq 'Marvell FastLinQ' } 'name query passes through bare' +Assert { @(Expand-CsQuery 'BCM57416 RDMA Ethernet').Count -eq 1 } 'phrase query not expanded' + +'== Get-CsBranchRank ==' +Assert { (Get-CsBranchRank ([version]'26.30.3.62') @('26.30', '25.30', '5.7')) -eq 0 } 'first branch' +Assert { (Get-CsBranchRank ([version]'5.7.0.5659') @('26.30', '25.30', '5.7')) -eq 2 } 'third branch' +Assert { (Get-CsBranchRank ([version]'9.9.9.9') @('26.30')) -eq ([int]::MaxValue) } 'no branch -> max' + +'== Select-CsBestPackage (ALWAYS NEWEST = highest version, date tiebreak, branch tiebreak) ==' +# MediaTek MT7925: 26.30 must beat the lower 5.7 branch (higher version AND newer date) +$bestMt = Select-CsBestPackage -Packages @((Pkg '5.7.0.5659' '2026-03-06'), (Pkg '26.30.3.62' '2026-04-08')) -PreferredBranches @('26.30', '25.30', '5.7') +Assert { $bestMt.Version -eq [version]'26.30.3.62' } "MediaTek picks 26.30.3.62 (got $($bestMt.Version))" + +# Realtek RTL8159: higher version wins DESPITE older catalog date (the core date-vs-version fix) +$bestRtl = Select-CsBestPackage -Packages @((Pkg '11.19.602.2025' '2016-03-30'), (Pkg '11.19.20.602' '2018-03-30')) +Assert { $bestRtl.Version -eq [version]'11.19.602.2025' } "Realtek picks 11.19.602.2025 over newer-dated 11.19.20.602 (got $($bestRtl.Version))" + +# Intel I226-V: re-released older version has a NEWER date; highest version must still win +$bestIntel = Select-CsBestPackage -Packages @((Pkg '2.1.5.10' '2025-12-29'), (Pkg '2.1.5.7' '2026-01-19')) +Assert { $bestIntel.Version -eq [version]'2.1.5.10' } "Intel picks 2.1.5.10 over re-released 2.1.5.7 (got $($bestIntel.Version))" + +# Equal version+date: branch tiebreak must still return a deterministic, non-null pick +$bestTie = Select-CsBestPackage -Packages @((Pkg '3.0.0.1' '2026-01-01'), (Pkg '3.0.0.1' '2026-01-01')) -PreferredBranches @('3.0') +Assert { $null -ne $bestTie } 'equal-version tiebreak returns a deterministic pick' + +'== .psd1 data tables load + schema ==' +$expectKeys = @{ + 'intel-eth.psd1' = 10; 'intel-wifi.psd1' = 2; 'marvell.psd1' = 4 + 'realtek.psd1' = 8; 'qualcomm.psd1' = 3; 'mediatek.psd1' = 5; 'broadcom.psd1' = 3 +} +foreach ($f in $expectKeys.Keys) { + $cfg = Import-PowerShellDataFile (Join-Path $PSScriptRoot $f) + Assert { $cfg.ContainsKey('VendorKey') -and $cfg.ContainsKey('Targets') } "$f has VendorKey+Targets" + $devs = @($cfg.Targets.Devices) + Assert { $devs.Count -eq $expectKeys[$f] } "$f device count = $($expectKeys[$f]) (got $($devs.Count))" + $bad = $devs | Where-Object { -not ($_.ContainsKey('Key') -and $_.ContainsKey('Label') -and $_.ContainsKey('Queries')) } + Assert { $bad.Count -eq 0 } "$f all devices have Key/Label/Queries" +} +# Specific fix assertions +$ie = Import-PowerShellDataFile (Join-Path $PSScriptRoot 'intel-eth.psd1') +$e810 = $ie.Targets.Devices | Where-Object Key -eq 'E810' +Assert { $e810.Queries[0] -eq 'VEN_8086&DEV_1592' } 'E810 bare HWID query' +$x520 = $ie.Targets.Devices | Where-Object Key -eq 'X520' +Assert { ($null -ne $x520) -and ($x520.Queries -contains 'VEN_8086&DEV_10FB') } 'X520 (82599) added, includes 10FB' +$mv = Import-PowerShellDataFile (Join-Path $PSScriptRoot 'marvell.psd1') +$aqc = $mv.Targets.Devices | Where-Object Key -eq 'AQC111U' +Assert { $aqc.Queries[0] -eq 'VID_2ECA&PID_C101' } 'AQC111U fixed USB id' +$fl = $mv.Targets.Devices | Where-Object Key -eq 'FastLinQ' +Assert { ($null -ne $fl) -and ($fl.Queries -contains 'Marvell FastLinQ') } 'FastLinQ consolidated into Marvell module' +$iw = Import-PowerShellDataFile (Join-Path $PSScriptRoot 'intel-wifi.psd1') +$be = $iw.Targets.Devices | Where-Object Key -eq 'BE200' +Assert { $be.Queries[0] -eq 'VEN_8086&DEV_272B' } 'BE200 keeps correct 272B (not gonefishin 2725)' +$md = Import-PowerShellDataFile (Join-Path $PSScriptRoot 'mediatek.psd1') +$mt7925 = $md.Targets.Devices | Where-Object Key -eq '7925' +Assert { $mt7925.PreferredBranches[0] -eq '26.30' } 'MT7925 preferred branch refreshed to 26.30' + +"`n== RESULT: $script:pass passed, $script:fail failed ==" +if ($script:fail -gt 0) { exit 1 } else { exit 0 } diff --git a/catalogscrape/broadcom.psd1 b/catalogscrape/broadcom.psd1 new file mode 100644 index 0000000..5a01231 --- /dev/null +++ b/catalogscrape/broadcom.psd1 @@ -0,0 +1,45 @@ +@{ + VendorKey = 'Broadcom_Ethernet' + Targets = @( + # Broadcom NetXtreme-E / NetXtreme-C (bnxt) 10GbE+ controllers. All SKUs resolve to the + # SAME unified driver on the catalog (v210.0.71.0, 2017-12-21) under per-SKU listings, so + # the family is grouped by media type into one download each (the INF is family-wide). + # + # COVERAGE LIMITS (catalog-only; documented intentionally): + # * Catalog tops out at v210.0.71.0 (2017). Newer Broadcom releases (220.x+) are only on + # Broadcom's site / OEM channels, NOT the Microsoft Update Catalog. + # * Thor BCM575xx/576xx (100/200/400G: DEV_1750/1751/1752/1760) have NO catalog drivers. + # * NetXtreme II 10G (bnx2x: BCM578xx DEV_164x/166x/168x/16Ax) has NO per-HWID catalog + # driver — the only catalog entry is an HP-OEM "BCM57810 NetXtreme II" v7.4.25.0 (2013) + # found by name search. By choice it is NOT scraped: build_win11pxe.ps1 promotes the + # in-box b06bdrv/bxvbda.sys IF the Win11 image still ships it (NOT guaranteed on 24H2+). + # All HWIDs below were verified to return Broadcom NIC drivers on the live catalog. + @{ + Name = 'Broadcom_NetXtremeE_10GBASE-T' + Devices = @( + @{ Key = 'NetXtremeE-T'; Label = 'NetXtreme-E 10GBASE-T (BCM57406/57407/57416/57417)'; + Queries = @('VEN_14E4&DEV_16D2', 'VEN_14E4&DEV_16D5', 'VEN_14E4&DEV_16D8', 'VEN_14E4&DEV_16D9'); + TitleExclude = @('NDIS') } + ) + } + @{ + Name = 'Broadcom_NetXtremeEC_SFP' + Devices = @( + @{ Key = 'NetXtremeEC-SFP'; Label = 'NetXtreme-E/C 10/25/40/50G SFP (BCM573xx/574xx)'; + Queries = @( + 'VEN_14E4&DEV_16C8', 'VEN_14E4&DEV_16C9', 'VEN_14E4&DEV_16CA', + 'VEN_14E4&DEV_16CE', 'VEN_14E4&DEV_16CF', 'VEN_14E4&DEV_16D0', + 'VEN_14E4&DEV_16D1', 'VEN_14E4&DEV_16D6', 'VEN_14E4&DEV_16D7' + ); + TitleExclude = @('NDIS') } + ) + } + @{ + Name = 'Broadcom_NetXtremeE_100G' + Devices = @( + @{ Key = 'NetXtremeE-100G'; Label = 'NetXtreme-E 50/100G (BCM57454)'; + Queries = @('VEN_14E4&DEV_1614'); TitleExclude = @('NDIS') } + ) + } + ) +} diff --git a/catalogscrape/intel-eth.psd1 b/catalogscrape/intel-eth.psd1 new file mode 100644 index 0000000..cc6522e --- /dev/null +++ b/catalogscrape/intel-eth.psd1 @@ -0,0 +1,52 @@ +@{ + VendorKey = 'Intel_Ethernet' + Targets = @( + @{ + Name = 'Intel_2.5G_Family' + Devices = @( + @{ Key = 'I225-V'; Label = 'I225'; Queries = @('VEN_8086&DEV_15F3') } + @{ Key = 'I226-V'; Label = 'I226'; Queries = @('VEN_8086&DEV_125C') } + ) + } + @{ + Name = 'Intel_1G_Family' + Devices = @( + # e1d is unified: any single package's INF covers all I219-V/LM generations. + @{ Key = 'I219-V'; Label = 'I219'; Queries = @('VEN_8086&DEV_15B8') } + @{ Key = 'I210'; Label = 'I210'; Queries = @('VEN_8086&DEV_1533') } + ) + } + @{ + Name = 'Intel_10G_Family' + Devices = @( + @{ Key = 'X540'; Label = 'X540'; Queries = @('VEN_8086&DEV_1528') } + @{ Key = 'X550'; Label = 'X550'; Queries = @('VEN_8086&DEV_1563') } + @{ Key = 'X710'; Label = 'X710'; Queries = @('VEN_8086&DEV_1572') } + # X520 = 82599 (ixgbe/ixn), EOL, NOT in-box and not covered by any other scraper. + # Newest catalog driver is v3.9.58.9101 (2017) — but ONLY via the BARE HWID query; + # appending "Windows 11" returns an older 2012 v2.11 set instead. Multiple 82599 + # DEV ids (SFP/backplane/T3/EN/QSFP variants) all carry the same X520 driver. + @{ Key = 'X520'; Label = 'X520'; Queries = @( + 'VEN_8086&DEV_10FB', 'VEN_8086&DEV_10F8', 'VEN_8086&DEV_151C', + 'VEN_8086&DEV_154A', 'VEN_8086&DEV_154D', 'VEN_8086&DEV_1557', 'VEN_8086&DEV_1558' + ) + } + ) + } + @{ + Name = 'Intel_100G_Family' + Devices = @( + # FIX: E810 datacenter driver packages are NOT tagged "Windows 11", so the + # " Windows 11" AND-search returns 0 results and the family was silently + # skipped every run. Query the bare HWID instead (live: 0 -> 4 results). + @{ Key = 'E810'; Label = 'E810'; Queries = @('VEN_8086&DEV_1592') } + ) + } + @{ + Name = 'Intel_AVF_Family' + Devices = @( + @{ Key = 'IAVF'; Label = 'IAVF'; Queries = @('VEN_8086&DEV_1889') } + ) + } + ) +} diff --git a/catalogscrape/intel-wifi.psd1 b/catalogscrape/intel-wifi.psd1 new file mode 100644 index 0000000..aa52a0a --- /dev/null +++ b/catalogscrape/intel-wifi.psd1 @@ -0,0 +1,20 @@ +@{ + VendorKey = 'Intel_WiFi' + Targets = @( + # Intel bundles all supported adapters into unified packages, so two searches cover + # AC 9260 through BE200. DEV_272B is the correct Wi-Fi 7 BE200 PCI id (do NOT use + # DEV_2725 — that is the AX210; gonefishin's providers.yaml has that wrong). + @{ + Name = 'Intel_WiFi7_Family' + Devices = @( + @{ Key = 'BE200'; Label = 'BE200'; Queries = @('VEN_8086&DEV_272B') } + ) + } + @{ + Name = 'Intel_WiFi6_Family' + Devices = @( + @{ Key = 'AX200'; Label = 'AX200'; Queries = @('VEN_8086&DEV_2723') } + ) + } + ) +} diff --git a/catalogscrape/marvell.psd1 b/catalogscrape/marvell.psd1 new file mode 100644 index 0000000..493ea2d --- /dev/null +++ b/catalogscrape/marvell.psd1 @@ -0,0 +1,45 @@ +@{ + # Marvell Ethernet NICs. Marvell's modern NIC portfolio is two acquired lines: + # * Aquantia AQtion (AQC1xx, multi-gig / 10G) — acquired 2019 + # * QLogic FastLinQ (QL41xxx/45xxx, 10/25/100G) — acquired via Cavium + # Both are current Marvell products, so both live here. (Marvell's own-design "Yukon" + # 88E80xx is GbE-only and in-box; out of this 10GbE+ scope.) + VendorKey = 'Marvell_Ethernet' + Targets = @( + @{ + Name = 'Marvell_Aquantia_PCIe' + Devices = @( + @{ Key = 'AQC107'; Label = 'AQC107'; Queries = @('VEN_1D6A&DEV_D107') } + @{ Key = 'AQC113'; Label = 'AQC113'; Queries = @('VEN_1D6A&DEV_04C0') } + ) + } + @{ + Name = 'Marvell_Aquantia_USB' + Devices = @( + # FIX: the old VID_1D6A&PID_D111 (+TRENDnet VID_20F4 / ASIX VID_0B95) entries + # all returned 0 results — 1D6A is Aquantia's PCIe vendor id, not a USB VID. + # The real first-party USB id is VID_2ECA&PID_C101 (live: 20 results, Aquantia + # Net v1.8.0.0). These are Win10-era WHQL packages, so the "Windows 11" suffix + # also zeroes them: query bare. Drivers are chipset-wide, so the single + # first-party id covers the TRENDnet/ASIX rebrands too. + @{ Key = 'AQC111U'; Label = 'AQC111U'; Queries = @('VID_2ECA&PID_C101') } + ) + } + @{ + Name = 'Marvell_FastLinQ' + Devices = @( + # FastLinQ (qend) is ONE unified Windows driver across the whole 41000 (QL41xxx, + # 10/25/40/50G incl. QL41162/41164 10GBASE-T) AND 45000 (QL45xxx, up to 100G) + # series, so the single name query covers both. The catalog has NO 45xxx- or HWID- + # specific entry (every QL45000/FastLinQ 45000/VEN_1077&DEV_* search returns 0); + # the driver is reachable only by NAME, as OEM (Lenovo/Dell/HPE) "Marvell - Net" + # listings — newest v3.1.4.0 (2021-08). Bare (name-based); not in-box; storage + # personalities excluded. Adding 45xxx-specific queries would be dead (0 results) — + # 45xxx is covered by the same unified package this already pulls. + @{ Key = 'FastLinQ'; Label = 'FastLinQ 41xxx/45xxx 10-100G (QL41162/41164, QL45xxx)'; + Queries = @('Marvell FastLinQ', 'FastLinQ'); + TitleExclude = @('(?i)FCoE', '(?i)iSCSI', '(?i)storage') } + ) + } + ) +} diff --git a/catalogscrape/mediatek.psd1 b/catalogscrape/mediatek.psd1 new file mode 100644 index 0000000..f559c5e --- /dev/null +++ b/catalogscrape/mediatek.psd1 @@ -0,0 +1,18 @@ +@{ + VendorKey = 'MediaTek_WiFi' + Targets = @( + @{ + Name = 'MediaTek_WiFi_Family' + Devices = @( + @{ Key = '7961'; Label = 'MT7921_Filogic330'; Queries = @('VEN_14C3&DEV_7961', 'MT7921'); PreferredBranches = @('3.5'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + @{ Key = '0608'; Label = 'MT7921K_RZ608'; Queries = @('VEN_14C3&DEV_0608', 'RZ608'); PreferredBranches = @('3.5'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + @{ Key = '0616'; Label = 'MT7922_RZ616'; Queries = @('VEN_14C3&DEV_0616', 'MT7922', 'RZ616'); PreferredBranches = @('3.5'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + # 26.30 is the current top branch for MT7925/MT7927 (newer than 25.30/5.7). + # Selection is highest-version-first so 26.30.x wins regardless; the branch list + # is updated only so the [branch ...] log line stays informative. + @{ Key = '7925'; Label = 'MT7925_Filogic380'; Queries = @('VEN_14C3&DEV_7925', 'MT7925'); PreferredBranches = @('26.30', '25.30', '5.7'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + @{ Key = '7927'; Label = 'MT7927_Filogic380High'; Queries = @('VEN_14C3&DEV_7927', 'MT7927'); PreferredBranches = @('26.30', '25.30', '5.7'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + ) + } + ) +} diff --git a/catalogscrape/qualcomm.psd1 b/catalogscrape/qualcomm.psd1 new file mode 100644 index 0000000..d750560 --- /dev/null +++ b/catalogscrape/qualcomm.psd1 @@ -0,0 +1,17 @@ +@{ + VendorKey = 'Qualcomm_WiFi' + Targets = @( + @{ + Name = 'Qualcomm_PCIe_Family' + Devices = @( + # Queries use the modern Qualcomm vendor id (VEN_17CB) + marketing name. The + # marketing-name search drags in the combo chip's Bluetooth/UART entries, hence + # the TitleExclude. PreferredBranches now acts only as a same-version tiebreak + # (selection is highest-version-first); kept for traceability. + @{ Key = '1101'; Label = 'QCA6390'; Queries = @('VEN_17CB&DEV_1101', 'Killer AX500'); PreferredBranches = @('3.0'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + @{ Key = '1103'; Label = 'WCN6855'; Queries = @('VEN_17CB&DEV_1103', 'FastConnect 6900'); PreferredBranches = @('3.0'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + @{ Key = '1107'; Label = 'WCN7850'; Queries = @('VEN_17CB&DEV_1107', 'FastConnect 7800'); PreferredBranches = @('3.1'); TitleExclude = @('NDIS', '(?i)bluetooth|uart') } + ) + } + ) +} diff --git a/catalogscrape/realtek.psd1 b/catalogscrape/realtek.psd1 new file mode 100644 index 0000000..3ab2221 --- /dev/null +++ b/catalogscrape/realtek.psd1 @@ -0,0 +1,27 @@ +@{ + VendorKey = 'Realtek_Ethernet' + Targets = @( + @{ + Name = 'RTL_PCIe_Family' + Devices = @( + @{ Key = '1125'; Label = 'RTL8125'; Queries = @('VEN_10EC&DEV_8125') } + @{ Key = '1126'; Label = 'RTL8126'; Queries = @('VEN_10EC&DEV_8126') } + @{ Key = '1127'; Label = 'RTL8127'; Queries = @('VEN_10EC&DEV_8127') } + @{ Key = '1168'; Label = 'RTL8168'; Queries = @('VEN_10EC&DEV_8168') } + ) + } + @{ + Name = 'RTL_USB_Family' + Devices = @( + # USB packages carry a stale 2016/2018 catalog versionDate, so selection-by-date + # used to pick the OLDER build. The engine now selects by highest [version] + # (date as tiebreak), so e.g. RTL8159 correctly picks 11.19.602.2025 over the + # 2018-dated 11.19.20.602. + @{ Key = '1153'; Label = 'RTL8153'; Queries = @('VID_0BDA&PID_8153') } + @{ Key = '1156'; Label = 'RTL8156'; Queries = @('VID_0BDA&PID_8156') } + @{ Key = '1157'; Label = 'RTL8157'; Queries = @('VID_0BDA&PID_8157') } + @{ Key = '1159'; Label = 'RTL8159'; Queries = @('VID_0BDA&PID_815A') } + ) + } + ) +}