Public/Dell.Client.Update/Private/Get-DellModelCatalog.ps1

function Get-DellSystemId {
    param([Parameter(Mandatory)][object]$ComputerSystem)

    $sku = ([string]$ComputerSystem.SystemSKUNumber).Trim()
    if ($sku -match '^(?:0x)?(?<hex>[0-9A-Fa-f]{4})$') {
        return $Matches.hex.ToUpperInvariant()
    }
    throw "Dell SystemSKUNumber '$sku' is not a four-character system ID."
}

function Get-DellExtractedXmlFile {
    param(
        [Parameter(Mandatory)][string]$Directory,
        [Parameter(Mandatory)][string]$ExpectedRoot
    )

    $xmlFiles = [System.Collections.Generic.List[IO.FileInfo]]::new()
    foreach ($file in (Get-ChildItem -LiteralPath $Directory -File -ErrorAction Stop)) {
        try {
            $document = [System.Xml.XmlDocument]::new()
            $document.XmlResolver = $null
            $document.Load($file.FullName)
            if ($document.DocumentElement.LocalName -eq $ExpectedRoot) { $xmlFiles.Add($file) }
        }
        catch [System.Xml.XmlException] { }
    }
    if ($xmlFiles.Count -ne 1) {
        throw "Expected exactly one '$ExpectedRoot' XML document in '$Directory'; found $($xmlFiles.Count)."
    }
    return $xmlFiles[0]
}

function Get-DellModelCatalogXml {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$SystemId,
        [Parameter(Mandatory)][string]$WorkingDirectory,
        [string]$DownloadDirectory = (Get-DellPSUpdatePath -Name Downloads),
        [switch]$UseCachedCatalog,
        [uri]$CatalogIndexUrl = 'https://downloads.dell.com/catalog/CatalogIndexPC.cab'
    )

    $null = New-Item -Path $WorkingDirectory -ItemType Directory -Force
    $null = New-Item -Path $DownloadDirectory -ItemType Directory -Force
    $indexCabPath = Join-Path $DownloadDirectory 'CatalogIndexPC.cab'
    $indexXmlPath = Join-Path $WorkingDirectory 'CatalogIndexPC.xml'
    $indexExtractDirectory = Join-Path $WorkingDirectory 'CatalogIndex'
    $modelXmlPath = Join-Path $WorkingDirectory "Model_$SystemId.xml"
    $expandPath = Join-Path $env:SystemRoot 'System32\expand.exe'

    if ($UseCachedCatalog -and (Test-Path -LiteralPath $modelXmlPath -PathType Leaf)) {
        Write-Host "Using cached Dell model catalog XML: $modelXmlPath"
        return $modelXmlPath
    }

    Write-Host "Downloading Dell catalog index from $CatalogIndexUrl ..."
    Invoke-DellDownload -Source $CatalogIndexUrl -Destination $indexCabPath
    Remove-Item -LiteralPath $indexXmlPath -Force -ErrorAction SilentlyContinue
    Remove-Item -LiteralPath $indexExtractDirectory -Recurse -Force -ErrorAction SilentlyContinue
    $null = New-Item -Path $indexExtractDirectory -ItemType Directory -Force
    $expandOutput = & $expandPath $indexCabPath '-F:*' $indexExtractDirectory 2>&1
    if ($LASTEXITCODE -ne 0) {
        throw "Could not extract Dell catalog index. expand.exe exit code: $LASTEXITCODE. $($expandOutput -join ' ')"
    }
    $extractedIndex = Get-DellExtractedXmlFile -Directory $indexExtractDirectory -ExpectedRoot 'ManifestIndex'
    Copy-Item -LiteralPath $extractedIndex.FullName -Destination $indexXmlPath -Force
    Remove-Item -LiteralPath $indexExtractDirectory -Recurse -Force

    $indexDocument = [System.Xml.XmlDocument]::new()
    $indexDocument.XmlResolver = $null
    $indexDocument.Load($indexXmlPath)
    $manifest = $indexDocument.SelectSingleNode("//*[local-name()='GroupManifest'][.//*[local-name()='Model' and translate(@systemID, 'abcdef', 'ABCDEF')='$SystemId']]")
    if (-not $manifest) { throw "Dell model catalog index does not contain system ID '$SystemId'." }

    $manifestInformation = $manifest.SelectSingleNode("./*[local-name()='ManifestInformation']")
    if (-not $manifestInformation) { throw "Dell model catalog entry for system ID '$SystemId' has no manifest information." }
    $relativePath = $manifestInformation.GetAttribute('path')
    if ([string]::IsNullOrWhiteSpace($relativePath) -or $relativePath -match '(^[\\/]|\.\.)') {
        throw "Dell model catalog path '$relativePath' is invalid."
    }

    $baseLocation = $indexDocument.DocumentElement.GetAttribute('baseLocation')
    if ([string]::IsNullOrWhiteSpace($baseLocation)) { $baseLocation = 'downloads.dell.com' }
    if ($baseLocation -ine 'downloads.dell.com') { throw "Dell model catalog base location '$baseLocation' is not trusted." }
    $modelCatalogUri = [uri]::new("https://$baseLocation/$relativePath")
    $modelCabPath = Join-Path $DownloadDirectory ([IO.Path]::GetFileName($modelCatalogUri.AbsolutePath))

    Write-Host "Downloading Dell model catalog for system ID $SystemId ..."
    Invoke-DellDownload -Source $modelCatalogUri -Destination $modelCabPath
    $sha256Node = $manifest.SelectSingleNode(".//*[local-name()='Hash' and translate(@algorithm, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')='SHA256']")
    if (-not $sha256Node -or [string]::IsNullOrWhiteSpace($sha256Node.InnerText)) {
        throw "Dell model catalog index does not provide a SHA-256 digest for '$modelCatalogUri'."
    }
    $actualHash = (Get-FileHash -LiteralPath $modelCabPath -Algorithm SHA256).Hash
    if ($actualHash -ine $sha256Node.InnerText.Trim()) {
        Remove-Item -LiteralPath $modelCabPath -Force -ErrorAction SilentlyContinue
        throw "Dell model catalog SHA-256 validation failed for '$modelCatalogUri'."
    }

    $extractDirectory = Join-Path $WorkingDirectory "Model_$SystemId"
    Remove-Item -LiteralPath $extractDirectory -Recurse -Force -ErrorAction SilentlyContinue
    $null = New-Item -Path $extractDirectory -ItemType Directory -Force
    $expandOutput = & $expandPath $modelCabPath '-F:*' $extractDirectory 2>&1
    if ($LASTEXITCODE -ne 0) {
        throw "Could not extract Dell model catalog XML. expand.exe exit code: $LASTEXITCODE. $($expandOutput -join ' ')"
    }
    $extractedXml = Get-DellExtractedXmlFile -Directory $extractDirectory -ExpectedRoot 'Manifest'

    Copy-Item -LiteralPath $extractedXml.FullName -Destination $modelXmlPath -Force
    Remove-Item -LiteralPath $extractDirectory -Recurse -Force
    return $modelXmlPath
}