Private/Core.ps1

function Invoke-SurfaceWebRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Uri,
        [ValidateSet('GET','HEAD')][string]$Method = 'GET',
        [int]$TimeoutSec = 90
    )

    $headers = @{
        'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/153 Safari/537.36 SurfaceWinPEDrivers/1.0'
        'Accept-Language' = 'en-US,en;q=0.9'
    }

    $params = @{
        Uri                = $Uri
        Method             = $Method
        Headers            = $headers
        TimeoutSec         = $TimeoutSec
        MaximumRedirection = 10
        ErrorAction        = 'Stop'
    }

    if ($PSVersionTable.PSVersion.Major -lt 6) {
        $params.UseBasicParsing = $true
    }

    Invoke-WebRequest @params
}

function ConvertFrom-SurfaceHtmlText {
    param([AllowNull()][string]$Text)

    if ([string]::IsNullOrWhiteSpace($Text)) { return '' }

    $value = $Text -replace '(?is)<br\s*/?>', "`n"
    $value = $value -replace '(?is)<[^>]+>', ''
    $value = [System.Net.WebUtility]::HtmlDecode($value)
    return $value.Trim()
}

function Get-SurfaceModelKey {
    param([Parameter(Mandatory)][string]$Name)

    $value = [System.Net.WebUtility]::HtmlDecode($Name).Trim()
    $value = $value -replace '[\u2012\u2013\u2014\u2212]', '-'

    # Microsoft sometimes combines the Snapdragon and 5G variants in the WinPE heading,
    # while the download catalog exposes a single cumulative package for the generation.
    if ($value -match '^(?<first>Surface\s+(?:Laptop|Pro)\s+\d+)\s+and\s+Surface\s+.+?\s*-\s*(?<cpu>Intel|AMD|Snapdragon)\s*$') {
        $value = '{0} {1}' -f $Matches.first, $Matches.cpu
    }

    $value = $value -replace '(?i)\bfor\s+business\b', ' '
    $value = $value -replace '(?i)\bwith\s+(intel|amd|snapdragon)\s+processor\b', ' $1 '
    $value = $value -replace '(?i)\((intel|amd|snapdragon)\)', ' $1 '
    $value = $value -replace '(?i)\b(\d+)(?:st|nd|rd|th)\s+edition\b', ' $1 '
    $value = $value -replace '(?i)\b1st\s+gen(?:eration)?\b', ' 1 '
    $value = $value -replace '(?i)\bedition\b', ' '
    $value = $value -replace '(?i)\bprocessor\b', ' '
    $value = $value -replace '(?i)\bwith\b', ' '
    $value = $value -replace '[^A-Za-z0-9]+', ' '
    $value = ($value -replace '\s+', ' ').Trim().ToLowerInvariant()

    if ($value -eq 'surface laptop') { $value = 'surface laptop 1' }

    return $value
}

function Get-SurfaceArchitectureFromName {
    param([Parameter(Mandatory)][string]$Name)

    if ($Name -match '(?i)Snapdragon|\bARM(?:64)?\b') { return 'ARM64' }
    return 'x64'
}

function Get-SurfaceCpuVendorFromName {
    param([Parameter(Mandatory)][string]$Name)

    if ($Name -match '(?i)Snapdragon|\bARM(?:64)?\b') { return 'Snapdragon' }
    if ($Name -match '(?i)\bAMD\b') { return 'AMD' }
    return 'Intel'
}

function Get-SurfaceDiscovery {
    [CmdletBinding()]
    param()

    $winpe = @(Get-SurfaceWinPEImportCatalog)
    $downloads = @(Get-SurfaceDriverDownloadCatalog)

    foreach ($entry in $winpe) {
        $matches = @($downloads | Where-Object ModelKey -eq $entry.ModelKey)
        $status = if ($matches.Count -eq 1) { 'Matched' } elseif ($matches.Count -eq 0) { 'Unmatched' } else { 'Ambiguous' }
        $match = if ($matches.Count -eq 1) { $matches[0] } else { $null }
        $importFolderSource = if ($entry.PSObject.Properties['ImportFolderSource']) {
            [string]$entry.ImportFolderSource
        }
        else {
            'PrimaryImportFolders'
        }

        $obj = [pscustomobject]@{
            PSTypeName             = 'SurfaceWinPEDrivers.Model'
            Model                  = $entry.Model
            ModelKey               = $entry.ModelKey
            Architecture           = $entry.Architecture
            CpuVendor              = $entry.CpuVendor
            ImportFolders          = @($entry.ImportFolders)
            ImportFolderCount      = $entry.ImportFolderCount
            ImportFolderSource     = $importFolderSource
            RequiredPackages       = @($entry.RequiredPackages)
            RequiredPackageCount   = $entry.RequiredPackageCount
            DownloadModel          = if ($match) { $match.Model } else { $null }
            DownloadCenterId       = if ($match) { $match.DownloadCenterId } else { $null }
            DetailsUrl             = if ($match) { $match.DetailsUrl } else { $null }
            Status                 = $status
            MatchCandidates        = @($matches | ForEach-Object Model)
            WinPESourceUrl         = $entry.SourceUrl
            DriverCatalogSourceUrl = $script:DriverCatalogUrl
        }
        $obj.PSObject.TypeNames.Insert(0, 'SurfaceWinPEDrivers.Model')
        $obj
    }
}

function Convert-DateTextToIsoUtc {
    param([AllowNull()][string]$DateText)
    if ([string]::IsNullOrWhiteSpace($DateText)) { return $null }

    try { $dt = [datetime]::Parse($DateText, [System.Globalization.CultureInfo]::InvariantCulture) }
    catch {
        try { $dt = [datetime]::Parse($DateText) }
        catch { return $null }
    }

    if ($dt.Kind -eq [System.DateTimeKind]::Unspecified) {
        $dt = [datetime]::SpecifyKind($dt, [System.DateTimeKind]::Local)
    }
    return $dt.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
}

function Get-DlcDetailsObjectFromHtml {
    param([Parameter(Mandatory)][string]$Html)

    $match = [regex]::Match($Html, '(?is)window\.__DLCDetails__\s*=\s*(?<json>\{.*?\})\s*;?\s*</script>')
    if (-not $match.Success) {
        throw 'window.__DLCDetails__ was not found on the Microsoft Download Center page.'
    }

    try { return ($match.Groups['json'].Value | ConvertFrom-Json) }
    catch { throw "Failed to parse Microsoft Download Center metadata: $($_.Exception.Message)" }
}

function Get-SurfaceDownloadCenterData {
    [CmdletBinding()]
    param([Parameter(Mandatory)][int]$DownloadCenterId)

    $detailsUrl = "https://www.microsoft.com/en-us/download/details.aspx?id=$DownloadCenterId"
    $response = Invoke-SurfaceWebRequest -Uri $detailsUrl
    $dlc = Get-DlcDetailsObjectFromHtml -Html ([string]$response.Content)
    $view = $dlc.dlcDetailsView
    if (-not $view) { throw "dlcDetailsView is missing for Download Center id $DownloadCenterId." }

    [pscustomobject]@{
        DetailsUrl = $detailsUrl
        Files      = @($view.downloadFile)
    }
}

function Test-SurfaceUri {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Uri,
        [int]$TimeoutSec = 30
    )

    $handler = [System.Net.Http.HttpClientHandler]::new()
    $handler.AllowAutoRedirect = $true
    $client = [System.Net.Http.HttpClient]::new($handler)
    $client.Timeout = [timespan]::FromSeconds($TimeoutSec)
    $client.DefaultRequestHeaders.UserAgent.ParseAdd('SurfaceWinPEDrivers/1.0')

    try {
        $request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Head, $Uri)
        $response = $client.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()

        if (-not $response.IsSuccessStatusCode -and [int]$response.StatusCode -in 403,405) {
            $response.Dispose()
            $request.Dispose()
            $request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $Uri)
            $request.Headers.Range = [System.Net.Http.Headers.RangeHeaderValue]::new(0, 0)
            $response = $client.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
        }

        [pscustomobject]@{
            Uri           = $Uri
            Success       = [bool]$response.IsSuccessStatusCode
            StatusCode    = [int]$response.StatusCode
            ContentLength = if ($response.Content.Headers.ContentLength) { [int64]$response.Content.Headers.ContentLength } else { $null }
            ContentType   = if ($response.Content.Headers.ContentType) { [string]$response.Content.Headers.ContentType.MediaType } else { $null }
            Error         = $null
        }
    }
    catch {
        [pscustomobject]@{
            Uri           = $Uri
            Success       = $false
            StatusCode    = $null
            ContentLength = $null
            ContentType   = $null
            Error         = $_.Exception.Message
        }
    }
    finally {
        if ($response) { $response.Dispose() }
        if ($request) { $request.Dispose() }
        $client.Dispose()
        $handler.Dispose()
    }
}

function Get-SurfaceConfigurationHash {
    param(
        [Parameter(Mandatory)][string[]]$ImportFolders,
        [object[]]$RequiredPackages = @()
    )

    $packageLines = @($RequiredPackages | ForEach-Object { '{0}|{1}' -f $_.Folder, $_.DownloadUrl })
    $text = (@($ImportFolders | Sort-Object) + @($packageLines | Sort-Object)) -join "`n"
    $bytes = [Text.Encoding]::UTF8.GetBytes($text)
    $sha = [Security.Cryptography.SHA256]::Create()
    try { return ([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-', '').ToLowerInvariant() }
    finally { $sha.Dispose() }
}