diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index 1554d87..f7d16f8 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -816,78 +816,11 @@ function Get-MicrosoftDrivers { [int]$WindowsRelease ) - $url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120" - - ### DOWNLOAD DRIVER PAGE CONTENT - WriteLog "Getting Surface driver information from $url" - $OriginalVerbosePreference = $VerbosePreference - $VerbosePreference = 'SilentlyContinue' - $webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent - $VerbosePreference = $OriginalVerbosePreference + # Use the shared Learn-based Microsoft model index so the CLI stays aligned with the UI. + WriteLog "Getting Surface driver information from the shared Microsoft model index" + $models = @(Get-SurfaceDriverModelIndex -DriversFolder $DriversFolder | Select-Object Model, Link) WriteLog "Complete" - - ### PARSE THE DRIVER PAGE CONTENT FOR MODELS AND DOWNLOAD LINKS - WriteLog "Parsing web content for models and download links" - $html = $webContent.Content - - # Regex to match divs with selectable-content-options__option-content classes - $divPattern = ']*class="selectable-content-options__option-content(?: ocHidden)?"[^>]*>(.*?)' - $divMatches = [regex]::Matches($html, $divPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - $models = @() - - foreach ($divMatch in $divMatches) { - $divContent = $divMatch.Groups[1].Value - - # Find all tables within the div - $tablePattern = ']*>(.*?)' - $tableMatches = [regex]::Matches($divContent, $tablePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - foreach ($tableMatch in $tableMatches) { - $tableContent = $tableMatch.Groups[1].Value - - # Find all rows in the table - $rowPattern = ']*>(.*?)' - $rowMatches = [regex]::Matches($tableContent, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - foreach ($rowMatch in $rowMatches) { - $rowContent = $rowMatch.Groups[1].Value - - # Extract cells from the row - $cellPattern = ']*>\s*(?:]*>)?(.*?)(?:

)?\s*' - $cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - if ($cellMatches.Count -ge 2) { - # Model name in the first TD - $modelName = ($cellMatches[0].Groups[1].Value).Trim() - - # # Remove

and

tags if present - # $modelName = $modelName -replace ']*>', '' -replace '

', '' - # $modelName = $modelName.Trim() - - - # The second TD might contain a link or just text - $secondTdContent = $cellMatches[1].Groups[1].Value.Trim() - - # Look for a link in the second TD - $linkPattern = ']+href="([^"]+)"[^>]*>' - $linkMatch = [regex]::Match($secondTdContent, $linkPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) - - if ($linkMatch.Success) { - $modelLink = $linkMatch.Groups[1].Value - } - else { - # No link, just text instructions - $modelLink = $secondTdContent - } - - $models += [PSCustomObject]@{ Model = $modelName; Link = $modelLink } - } - } - } - } - - WriteLog "Parsing complete" + WriteLog "Loaded $($models.Count) Surface models from the shared Microsoft model index." ### FIND THE MODEL IN THE LIST OF MODELS $selectedModel = $models | Where-Object { $_.Model -eq $Model } diff --git a/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Microsoft.psm1 b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Microsoft.psm1 index 2151752..60b76ff 100644 --- a/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Microsoft.psm1 +++ b/FFUDevelopment/FFU.Common/FFU.Common.Drivers.Microsoft.psm1 @@ -161,12 +161,181 @@ function ConvertTo-SurfaceComparableName { # 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 { + $value = $value -replace '\s+', ' ' + + return $value.Trim().ToUpperInvariant() + } + + function ConvertTo-SurfaceHtmlText { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string]$HtmlFragment + ) + + # Normalize HTML fragments from the Learn table into plain text values. + $textValue = $HtmlFragment -replace '', ' ' + $textValue = $textValue -replace '<[^>]+>', ' ' + $textValue = [System.Net.WebUtility]::HtmlDecode($textValue) + $textValue = $textValue -replace '\s+', ' ' + + return $textValue.Trim() + } + + function ConvertTo-SurfaceDownloadCenterLink { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$LinkValue + ) + + # Normalize Learn links down to the canonical Download Center details URL. + $decodedLink = [System.Net.WebUtility]::HtmlDecode($LinkValue).Trim() + if ([string]::IsNullOrWhiteSpace($decodedLink)) { + return $null + } + + if ($decodedLink.StartsWith('/')) { + $decodedLink = "https://www.microsoft.com$decodedLink" + } + + $downloadCenterMatch = [regex]::Match( + $decodedLink, + 'https://www\.microsoft\.com(?:/en-us)?/download/details\.aspx\?id=\d+', + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase + ) + + if (-not $downloadCenterMatch.Success) { + return $null + } + + return ($downloadCenterMatch.Value -replace '/en-us/', '/') + } + + function Get-SurfaceDriverModelIndex { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$DriversFolder + ) + + $url = 'https://learn.microsoft.com/en-us/surface/manage-surface-driver-and-firmware-updates' + $minimumExpectedModelCount = 10 + + # Load the cached model list first to keep Microsoft model discovery fast. + try { + $cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder + if (@($cache.ModelIndex).Count -gt 0) { + WriteLog "Surface cache: Using cached Microsoft model list ($(@($cache.ModelIndex).Count) models)." + return @($cache.ModelIndex) + } + } + catch { + WriteLog "Surface cache: Failed to load cached Microsoft model list. Falling back to the Learn source. Error: $($_.Exception.Message)" + } + + try { + # Download the Learn article that now contains the authoritative Surface package table. + WriteLog "Surface cache: Downloading Microsoft model index 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 + + # Parse each table row and keep only Download Center package links. + $rowMatches = [regex]::Matches($html, ']*>(.*?)', [System.Text.RegularExpressions.RegexOptions]::Singleline) + $models = [System.Collections.Generic.List[pscustomobject]]::new() + $seenModelKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + + foreach ($rowMatch in $rowMatches) { + $rowContent = $rowMatch.Groups[1].Value + $cellMatches = [regex]::Matches($rowContent, ']*>\s*(.*?)\s*', [System.Text.RegularExpressions.RegexOptions]::Singleline) + if ($cellMatches.Count -lt 2) { + continue + } + + $rowLabel = ConvertTo-SurfaceHtmlText -HtmlFragment $cellMatches[0].Groups[1].Value + if ([string]::IsNullOrWhiteSpace($rowLabel) -or $rowLabel -notmatch '(?i)^Surface') { + continue + } + + $downloadCellContent = $cellMatches[1].Groups[1].Value + $linkMatches = [regex]::Matches( + $downloadCellContent, + ']+href="([^"]+)"[^>]*>(.*?)', + [System.Text.RegularExpressions.RegexOptions]::Singleline -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase + ) + + foreach ($linkMatch in $linkMatches) { + $modelName = ConvertTo-SurfaceHtmlText -HtmlFragment $linkMatch.Groups[2].Value + $modelLink = ConvertTo-SurfaceDownloadCenterLink -LinkValue $linkMatch.Groups[1].Value + + if ([string]::IsNullOrWhiteSpace($modelName) -or [string]::IsNullOrWhiteSpace($modelLink)) { + continue + } + + $modelKey = "$modelName`n$modelLink" + if (-not $seenModelKeys.Add($modelKey)) { + continue + } + + $models.Add([pscustomobject]@{ + Make = 'Microsoft' + Model = $modelName + Link = $modelLink + }) + } + } + + if ($models.Count -eq 0) { + throw "No Microsoft driver models were found in the Learn table." + } + + if ($models.Count -lt $minimumExpectedModelCount) { + WriteLog "Surface cache: Warning - Learn parsing returned only $($models.Count) Microsoft model entries." + } + else { + WriteLog "Surface cache: Parsed $($models.Count) Microsoft model entries from Learn." + } + + # Save the refreshed model list into the shared cache for both UI and CLI use. + try { + $cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder + $cache.ModelIndex = @($models) + Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder + WriteLog "Surface cache: Saved Microsoft model list to cache." + } + catch { + WriteLog "Surface cache: Failed to save Microsoft model list. Error: $($_.Exception.Message)" + } + + return @($models) + } + catch { + WriteLog "Surface cache: Failed to build Microsoft model list from Learn. Error: $($_.Exception.Message)" + + # Fall back to the last cached model list even if it is stale when the live request fails. + try { + $cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder + if (Test-Path -Path $cachePath -PathType Leaf) { + $staleCache = Get-Content -Path $cachePath -Raw | ConvertFrom-Json -ErrorAction Stop + if (@($staleCache.ModelIndex).Count -gt 0) { + WriteLog "Surface cache: Using stale Microsoft model list ($(@($staleCache.ModelIndex).Count) models) because the live Learn request failed." + return @($staleCache.ModelIndex) + } + } + } + catch { + WriteLog "Surface cache: Failed to load stale Microsoft model list fallback. Error: $($_.Exception.Message)" + } + + throw "Failed to retrieve Microsoft Surface models." + } + } + + function Get-SurfaceSystemSkuReferenceIndex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] @@ -586,6 +755,9 @@ Export-ModuleMember -Function ` Import-SurfaceDriverIndexCache, ` Save-SurfaceDriverIndexCache, ` ConvertTo-SurfaceComparableName, ` + ConvertTo-SurfaceHtmlText, ` + ConvertTo-SurfaceDownloadCenterLink, ` + Get-SurfaceDriverModelIndex, ` Get-SurfaceSystemSkuReferenceIndex, ` Get-SurfaceDownloadCenterDetails, ` Get-SurfaceSystemSkuListForMicrosoftDriver \ No newline at end of file diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 index 3fc216b..cce2a95 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Drivers.Microsoft.psm1 @@ -15,100 +15,8 @@ function Get-MicrosoftDriversModelList { [string]$DriversFolder ) - $url = "https://support.microsoft.com/en-us/surface/download-drivers-and-firmware-for-surface-09bb2e09-2a4b-cb69-0951-078a7739e120" - $models = @() - - # Load cached model list first (Source B) to keep the UI fast. - # The cache is refreshed automatically when missing or invalid. - try { - $cachePath = Get-SurfaceDriverIndexCachePath -DriversFolder $DriversFolder - if (Test-Path -Path $cachePath -PathType Leaf) { - $cacheAgeDays = ((Get-Date) - (Get-Item -Path $cachePath).LastWriteTime).TotalDays - if ($cacheAgeDays -lt 7) { - $cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder - if ($cache.ModelIndex -and $cache.ModelIndex.Count -gt 0) { - WriteLog "Surface cache: Using cached Microsoft model list ($($cache.ModelIndex.Count) models)." - return @($cache.ModelIndex) - } - } - } - } - catch { - WriteLog "Surface cache: Failed to load cached Microsoft model list. Falling back to online parse. Error: $($_.Exception.Message)" - } - - try { - WriteLog "Getting Surface driver information from $url" - $OriginalVerbosePreference = $VerbosePreference - $VerbosePreference = 'SilentlyContinue' - # Use passed-in UserAgent and Headers - $webContent = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers $Headers -UserAgent $UserAgent - $VerbosePreference = $OriginalVerbosePreference - WriteLog "Complete" - - WriteLog "Parsing web content for models and download links" - $html = $webContent.Content - $divPattern = ']*class="selectable-content-options__option-content(?: ocHidden)?"[^>]*>(.*?)' - $divMatches = [regex]::Matches($html, $divPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - foreach ($divMatch in $divMatches) { - $divContent = $divMatch.Groups[1].Value - $tablePattern = ']*>(.*?)' - $tableMatches = [regex]::Matches($divContent, $tablePattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - foreach ($tableMatch in $tableMatches) { - $tableContent = $tableMatch.Groups[1].Value - $rowPattern = ']*>(.*?)' - $rowMatches = [regex]::Matches($tableContent, $rowPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - foreach ($rowMatch in $rowMatches) { - $rowContent = $rowMatch.Groups[1].Value - $cellPattern = ']*>\s*(?:]*>)?(.*?)(?:

)?\s*' - $cellMatches = [regex]::Matches($rowContent, $cellPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - - if ($cellMatches.Count -ge 2) { - $modelName = ([System.Net.WebUtility]::HtmlDecode(($cellMatches[0].Groups[1].Value).Trim())) - $secondTdContent = $cellMatches[1].Groups[1].Value.Trim() - # $linkPattern = ']+href="([^"]+)"[^>]*>' - # Change linkPattern to match https://www.microsoft.com/download/details.aspx?id= - $linkPattern = ']+href="(https://www\.microsoft\.com/download/details\.aspx\?id=\d+)"[^>]*>' - $linkMatch = [regex]::Match($secondTdContent, $linkPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) - - if ($linkMatch.Success) { - $modelLink = $linkMatch.Groups[1].Value - } - else { - continue - } - - $models += [PSCustomObject]@{ - Make = 'Microsoft' - Model = $modelName - Link = $modelLink - } - } - } - } - } - WriteLog "Parsing complete. Found $($models.Count) models." - - # Persist model list (Source B) into the local cache for fast UI population. - try { - $cache = Import-SurfaceDriverIndexCache -DriversFolder $DriversFolder - $cache.ModelIndex = @($models) - Save-SurfaceDriverIndexCache -Cache $cache -DriversFolder $DriversFolder - WriteLog "Surface cache: Saved Microsoft model list to cache." - } - catch { - WriteLog "Surface cache: Failed to save Microsoft model list. Error: $($_.Exception.Message)" - } - - return $models - } - catch { - WriteLog "Error getting Microsoft models: $($_.Exception.Message)" - throw "Failed to retrieve Microsoft Surface models." - } + # Keep the UI signature unchanged while using the shared Learn-based source. + return @(Get-SurfaceDriverModelIndex -DriversFolder $DriversFolder) } # Function to download and extract drivers for a specific Microsoft model (Modified for ForEach-Object -Parallel) function Save-MicrosoftDriversTask {