<# .SYNOPSIS Common Microsoft/Surface driver helpers (cache index, SKU mapping). .DESCRIPTION This module contains Microsoft/Surface-specific functions used by the UI and scripts to map Surface driver packs to System SKU values using: - Source A: Surface System SKU reference (Learn) - Source B: Support page model list - Source C: Download Center details (window.__DLCDetails__) #> # -------------------------------------------------------------------------- # SECTION: Microsoft Surface Driver Index Cache (Sources A/B/C) # -------------------------------------------------------------------------- function Get-SurfaceDriverIndexCachePath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DriversFolder ) # Store the cache under Drivers\Microsoft so it travels with the driver content $microsoftDriversFolder = Join-Path -Path $DriversFolder -ChildPath 'Microsoft' if (-not (Test-Path -Path $microsoftDriversFolder -PathType Container)) { New-Item -Path $microsoftDriversFolder -ItemType Directory -Force | Out-Null } return (Join-Path -Path $microsoftDriversFolder -ChildPath 'SurfaceDriverIndex.json') } function Import-SurfaceDriverIndexCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DriversFolder ) $cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder if (-not (Test-Path -Path $cachePath -PathType Leaf)) { return [pscustomobject]@{ ModelIndex = @() SkuIndex = @() DownloadCenterDetails = @() } } try { $cache = Get-Content -Path $cachePath -Raw | ConvertFrom-Json -ErrorAction Stop } catch { WriteLog "Warning: Could not read Surface driver cache '$cachePath'. Creating a new cache. Error: $($_.Exception.Message)" return [pscustomobject]@{ ModelIndex = @() SkuIndex = @() DownloadCenterDetails = @() } } if ($null -eq $cache) { return [pscustomobject]@{ ModelIndex = @() SkuIndex = @() DownloadCenterDetails = @() } } # Ensure expected properties exist (backward compatible with earlier cache shapes) if (-not $cache.PSObject.Properties['ModelIndex']) { $cache | Add-Member -NotePropertyName ModelIndex -NotePropertyValue @() } if (-not $cache.PSObject.Properties['SkuIndex']) { $cache | Add-Member -NotePropertyName SkuIndex -NotePropertyValue @() } if (-not $cache.PSObject.Properties['DownloadCenterDetails']) { $cache | Add-Member -NotePropertyName DownloadCenterDetails -NotePropertyValue @() } return $cache } function Save-SurfaceDriverIndexCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$Cache, [Parameter(Mandatory = $true)] [string]$DriversFolder ) $cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder $Cache | ConvertTo-Json -Depth 10 | Set-Content -Path $cachePath -Encoding UTF8 } function ConvertTo-SurfaceComparableName { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Text ) # Normalize Surface marketing strings into a comparable family key. # This intentionally strips consumer/commercial/processor qualifiers so we can join Sources A/B/C. $value = [System.Net.WebUtility]::HtmlDecode($Text) if ([string]::IsNullOrWhiteSpace($value)) { return $null } $value = $value.Trim() $value = $value -replace '\(', ' ' $value = $value -replace '\)', ' ' $value = $value -replace ',', ' ' # Normalize punctuation that frequently differs between Support/Learn pages # (e.g. Wi‑Fi unicode hyphen, AT&T, Y!mobile) $value = $value -replace '[-\u2010\u2011\u2012\u2013\u2014\u2212]', ' ' $value = $value -replace '&', ' ' $value = $value -replace '!', ' ' $value = $value -replace '™', ' ' $value = $value -replace '(?i)\bMicrosoft\b', '' $value = $value -replace '(?i)\bfor\s+Business\b', '' $value = $value -replace '(?i)\bConsumer\b', '' $value = $value -replace '(?i)\bCommercial\b', '' # Strip processor/connection qualifiers that cause mismatches between WMI, Learn, and Support naming. $value = $value -replace '(?i)\bwith\s+Intel\b', '' $value = $value -replace '(?i)\bIntel\s+processor\b', '' $value = $value -replace '(?i)\bIntel\b', '' $value = $value -replace '(?i)\bSnapdragon\s+processor\b', '' $value = $value -replace '(?i)\bSnapdragon\b', '' $value = $value -replace '(?i)\bwith\s+5G\b', '' $value = $value -replace '(?i)\bLTE\b', '' $value = $value -replace '(?i)\b4G\b', '' $value = $value -replace '(?i)\bprocessor\b', '' # Cleanup: remove orphaned "with" left behind by earlier removals (e.g., "Surface Pro 9 with Intel Processor") $value = $value -replace '(?i)\bwith\b', '' $value = $value -replace '\s+', ' ' return $value.Trim().ToUpperInvariant() } function Get-SurfaceSystemSkuReferenceIndex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DriversFolder ) # Source A: Learn page with authoritative Device / System Model / System SKU table $cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder if ($cache.SkuIndex -and $cache.SkuIndex.Count -gt 0) { return @($cache.SkuIndex) } $url = 'https://learn.microsoft.com/en-us/surface/surface-system-sku-reference' WriteLog "Surface cache: Downloading System SKU reference table from $url" $headers = @{ 'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' } $webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $headers $html = $webContent.Content $skuRows = [System.Collections.Generic.List[pscustomobject]]::new() $rowMatches = [regex]::Matches($html, ']*>(.*?)', [System.Text.RegularExpressions.RegexOptions]::Singleline) foreach ($rowMatch in $rowMatches) { $rowContent = $rowMatch.Groups[1].Value $cellMatches = [regex]::Matches($rowContent, ']*>\s*(?:]*>)?(.*?)(?:

)?\s*', [System.Text.RegularExpressions.RegexOptions]::Singleline) if ($cellMatches.Count -lt 3) { continue } $device = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[0].Groups[1].Value).Trim())) $systemModel = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[1].Groups[1].Value).Trim())) $systemSkuRaw = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[2].Groups[1].Value).Trim())) if ([string]::IsNullOrWhiteSpace($device) -or [string]::IsNullOrWhiteSpace($systemSkuRaw)) { continue } $skuList = @($systemSkuRaw) foreach ($sku in $skuList) { if ([string]::IsNullOrWhiteSpace($sku)) { continue } $skuRows.Add([pscustomobject]@{ Device = $device SystemModel = $systemModel SystemSku = $sku.Trim().ToUpperInvariant() }) } } $cache.SkuIndex = @($skuRows) Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder WriteLog "Surface cache: Stored $($skuRows.Count) SKU entries." return @($skuRows) } function Get-SurfaceDownloadCenterDetails { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DriversFolder, [Parameter(Mandatory = $true)] [string]$ModelLink, [Parameter()] [string]$ModelName = $null ) # Source C: Download Center details page (window.__DLCDetails__) containing file names + direct URLs $cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder $existing = @($cache.DownloadCenterDetails | Where-Object { $_.Link -eq $ModelLink } | Select-Object -First 1) if ($existing.Count -gt 0 -and $existing[0].Files -and $existing[0].Files.Count -gt 0) { # Backfill Model into cache when available if (-not [string]::IsNullOrWhiteSpace($ModelName)) { if (-not $existing[0].PSObject.Properties['Model'] -or [string]::IsNullOrWhiteSpace($existing[0].Model)) { try { $existing[0] | Add-Member -NotePropertyName Model -NotePropertyValue $ModelName -Force $newDetails = [System.Collections.Generic.List[pscustomobject]]::new() foreach ($item in @($cache.DownloadCenterDetails)) { if ($null -ne $item -and $item.PSObject.Properties['Link'] -and $item.Link -ne $ModelLink) { $newDetails.Add($item) } } $newDetails.Add($existing[0]) $cache.DownloadCenterDetails = @($newDetails) Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder } catch { WriteLog "Surface cache: Failed to backfill Model for DownloadCenterDetails entry '$ModelLink'. Error: $($_.Exception.Message)" } } } return @($existing[0].Files) } WriteLog "Surface cache: Downloading Download Center details from $ModelLink" $headers = @{ 'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' } $downloadPageContent = Invoke-WebRequest -Uri $ModelLink -UseBasicParsing -Headers $headers $scriptPattern = '