diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Microsoft.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Microsoft.psm1 new file mode 100644 index 0000000..2151752 --- /dev/null +++ b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Microsoft.psm1 @@ -0,0 +1,591 @@ +<# +.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 + + # Surface cache TTL (7 days): treat stale caches as missing so we re-download Sources A/B/C as needed. + $cacheTtlDays = 7 + if (-not (Test-Path -Path $cachePath -PathType Leaf)) { + return [pscustomobject]@{ + ModelIndex = @() + SkuIndex = @() + DownloadCenterDetails = @() + } + } + + try { + $cacheAgeDays = ((Get-Date) - (Get-Item -Path $cachePath -ErrorAction Stop).LastWriteTime).TotalDays + if ($cacheAgeDays -ge $cacheTtlDays) { + WriteLog "Surface cache: Cache file '$cachePath' is older than $cacheTtlDays days ($([math]::Round($cacheAgeDays, 1)) days). Refreshing." + return [pscustomobject]@{ + ModelIndex = @() + SkuIndex = @() + DownloadCenterDetails = @() + } + } + + WriteLog "Surface cache: Loading cached SurfaceDriverIndex.json from '$cachePath' (age: $([math]::Round($cacheAgeDays, 1)) days)." + } + catch { + WriteLog "Surface cache: Failed to read cache timestamp for '$cachePath'. Refreshing. Error: $($_.Exception.Message)" + 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, '
]*>)?(.*?)(?:
)?\s*