mirror of
https://github.com/rbalsleyMSFT/FFU.git
synced 2026-06-14 02:09:35 -06:00
895728ebe8
Use DeviceGroup OperatingSystem osArch when selecting Dell client driver packages, falling back to SupportedOperatingSystems when DeviceGroup metadata is absent. This keeps existing server catalog behavior unchanged.
317 lines
14 KiB
PowerShell
317 lines
14 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Common Dell driver helpers (catalog index, model listing, latest package selection).
|
||
#>
|
||
|
||
function Convert-DellVendorVersion {
|
||
param([Parameter(Mandatory=$true)][string]$VendorVersion)
|
||
$segments = $VendorVersion.Split('.') | ForEach-Object {
|
||
if ($_ -match '^\d+$') { [int]$_ } else { 0 }
|
||
}
|
||
return ,$segments
|
||
}
|
||
|
||
function Compare-DellVendorVersion {
|
||
param(
|
||
[int[]]$Left,
|
||
[int[]]$Right
|
||
)
|
||
$len = [Math]::Max($Left.Length,$Right.Length)
|
||
for ($i=0; $i -lt $len; $i++) {
|
||
$l = if ($i -lt $Left.Length) { $Left[$i] } else { 0 }
|
||
$r = if ($i -lt $Right.Length) { $Right[$i] } else { 0 }
|
||
if ($l -gt $r) { return 1 }
|
||
if ($l -lt $r) { return -1 }
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function Test-DellDriverComponentOsArch {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory=$true)][System.Xml.XmlElement]$Component,
|
||
[Parameter(Mandatory=$true)][ValidateSet('x64', 'x86', 'ARM64')][string]$WindowsArch
|
||
)
|
||
|
||
$deviceGroupOsNodes = @($Component.SelectNodes("*[local-name()='DeviceGroup']/*[local-name()='OperatingSystem']"))
|
||
if ($deviceGroupOsNodes.Count -gt 0) {
|
||
$validDeviceGroupOs = $deviceGroupOsNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
|
||
return $null -ne $validDeviceGroupOs
|
||
}
|
||
|
||
$supportedOsNodes = @($Component.SelectNodes("*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']"))
|
||
if ($supportedOsNodes.Count -eq 0) { return $false }
|
||
|
||
$validSupportedOs = $supportedOsNodes | Where-Object { $_.GetAttribute('osArch') -eq $WindowsArch } | Select-Object -First 1
|
||
return $null -ne $validSupportedOs
|
||
}
|
||
|
||
function Get-DellCatalogIndex {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory=$true)][string]$DriversFolder
|
||
)
|
||
|
||
$dellFolder = Join-Path $DriversFolder 'Dell'
|
||
if (-not (Test-Path $dellFolder)) { New-Item -Path $dellFolder -ItemType Directory -Force | Out-Null }
|
||
$cabPath = Join-Path $dellFolder 'CatalogIndexPC.cab'
|
||
$xmlPath = Join-Path $dellFolder 'CatalogIndexPC.xml'
|
||
$url = 'https://downloads.dell.com/catalog/CatalogIndexPC.cab'
|
||
|
||
$need = $true
|
||
if (Test-Path $xmlPath) {
|
||
$ageDays = ((Get-Date) - (Get-Item $xmlPath).CreationTime).TotalDays
|
||
if ($ageDays -lt 7) { $need = $false }
|
||
}
|
||
|
||
if ($need) {
|
||
if (Test-Path $cabPath) { Remove-Item $cabPath -Force -ErrorAction SilentlyContinue }
|
||
if (Test-Path $xmlPath) { Remove-Item $xmlPath -Force -ErrorAction SilentlyContinue }
|
||
Start-BitsTransferWithRetry -Source $url -Destination $cabPath
|
||
Invoke-Process -FilePath Expand.exe -ArgumentList """$cabPath"" ""$xmlPath""" | Out-Null
|
||
Remove-Item $cabPath -Force -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
if (-not (Test-Path $xmlPath)) { throw "Dell CatalogIndexPC XML missing: $xmlPath" }
|
||
return $xmlPath
|
||
}
|
||
|
||
function Get-DellClientModels {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory=$true)][string]$CatalogIndexXmlPath
|
||
)
|
||
|
||
$settings = New-Object System.Xml.XmlReaderSettings
|
||
$settings.IgnoreWhitespace = $true
|
||
$settings.IgnoreComments = $true
|
||
$reader = [System.Xml.XmlReader]::Create($CatalogIndexXmlPath,$settings)
|
||
|
||
$models = [System.Collections.Generic.List[pscustomobject]]::new()
|
||
try {
|
||
while ($reader.Read()) {
|
||
if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq 'GroupManifest') {
|
||
# Read subtree to pick out brand/model/systemID + path
|
||
$sub = $reader.ReadSubtree()
|
||
$doc = New-Object System.Xml.XmlDocument
|
||
$doc.Load($sub)
|
||
$sub.Dispose()
|
||
|
||
# Use local-name() to ignore namespaces
|
||
$brandNode = $doc.SelectSingleNode("//*[local-name()='SupportedSystems']/*[local-name()='Brand']")
|
||
if (-not $brandNode) { continue }
|
||
$brandDisplay = ($brandNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim()
|
||
$modelNode = $brandNode.SelectSingleNode("*[local-name()='Model']")
|
||
if (-not $modelNode) { continue }
|
||
$modelNumber = ($modelNode.SelectSingleNode("*[local-name()='Display']")?.InnerText).Trim()
|
||
$systemId = $modelNode.GetAttribute('systemID')
|
||
$manifestInfo = $doc.SelectSingleNode("//*[local-name()='ManifestInformation']")
|
||
if (-not $manifestInfo) { continue }
|
||
$pathAttr = $manifestInfo.GetAttribute('path')
|
||
if (-not $pathAttr) { continue }
|
||
$cabUrl = 'https://downloads.dell.com/' + $pathAttr
|
||
# Normalize model display using GroupManifest Display CDATA if available (strip 'PDK Catalog for')
|
||
$gmDisplayNode = $doc.SelectSingleNode("/*[local-name()='GroupManifest']/*[local-name()='Display']")
|
||
$modelFull = $null
|
||
if ($gmDisplayNode -and $gmDisplayNode.InnerText) {
|
||
$rawDisplay = $gmDisplayNode.InnerText.Trim()
|
||
$modelFull = ($rawDisplay -replace '^\s*PDK Catalog for\s+','').Trim()
|
||
}
|
||
if ([string]::IsNullOrWhiteSpace($modelFull)) {
|
||
# Fallback: assemble from brand/model nodes (legacy heuristic)
|
||
$prefixedModelNumber = $modelNumber
|
||
if ($modelNumber -and $brandDisplay) {
|
||
if ($modelNumber.StartsWith($brandDisplay,[System.StringComparison]::OrdinalIgnoreCase)) {
|
||
$prefixedModelNumber = $modelNumber
|
||
}
|
||
else {
|
||
$prefixedModelNumber = "$brandDisplay $modelNumber"
|
||
}
|
||
}
|
||
elseif ($brandDisplay -and -not $modelNumber) {
|
||
$prefixedModelNumber = $brandDisplay
|
||
}
|
||
$modelFull = $prefixedModelNumber
|
||
}
|
||
$modelDisplay = "$modelFull ($systemId)"
|
||
$models.Add([pscustomobject]@{
|
||
Brand = $brandDisplay
|
||
ModelNumber = $modelNumber
|
||
SystemId = $systemId
|
||
CabRelativePath = $pathAttr
|
||
CabUrl = $cabUrl
|
||
ModelDisplay = $modelDisplay
|
||
})
|
||
}
|
||
}
|
||
}
|
||
finally {
|
||
$reader.Dispose()
|
||
}
|
||
return $models
|
||
}
|
||
|
||
function Get-DellLatestDriverPackages {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory=$true)][string]$ModelXmlPath,
|
||
[Parameter(Mandatory=$true)][string]$WindowsArch,
|
||
[Parameter(Mandatory=$true)][int]$WindowsRelease
|
||
)
|
||
|
||
if (-not (Test-Path $ModelXmlPath)) { throw "Model XML not found: $ModelXmlPath" }
|
||
|
||
$xml = [xml](Get-Content -Path $ModelXmlPath -Raw)
|
||
|
||
# Collect all SoftwareComponent nodes
|
||
$components = $xml.SelectNodes("//*[local-name()='SoftwareComponent']")
|
||
if (-not $components) { return @() }
|
||
|
||
$rawPackages = [System.Collections.Generic.List[pscustomobject]]::new()
|
||
|
||
foreach ($comp in $components) {
|
||
$ctype = $comp.SelectSingleNode("*[local-name()='ComponentType']")
|
||
if (-not $ctype) { continue }
|
||
if ($ctype.GetAttribute('value') -ne 'DRVR') { continue }
|
||
|
||
# OS filtering (arch only – release filtering intentionally minimal for now)
|
||
if (-not (Test-DellDriverComponentOsArch -Component $comp -WindowsArch $WindowsArch)) { continue }
|
||
|
||
$path = $comp.GetAttribute('path')
|
||
if (-not $path) { continue }
|
||
|
||
$downloadUrl = "https://downloads.dell.com/$path"
|
||
$fileName = [IO.Path]::GetFileName($path)
|
||
$vendorVersion = $comp.GetAttribute('vendorVersion')
|
||
$versionArr = if ($vendorVersion) { Convert-DellVendorVersion $vendorVersion } else { @(0) }
|
||
$dateTimeAttr = $comp.GetAttribute('dateTime')
|
||
$dt = Get-Date
|
||
if ($dateTimeAttr) {
|
||
try { $dt = [DateTime]::Parse($dateTimeAttr) } catch { }
|
||
}
|
||
|
||
$categoryNode = $comp.SelectSingleNode("*[local-name()='Category']/*[local-name()='Display']")
|
||
$category = if ($categoryNode) { $categoryNode.InnerText.Trim() } else { 'Uncategorized' }
|
||
|
||
# Collect componentIDs (SupportedDevices + SupportedDCHDevices)
|
||
$compIds = [System.Collections.Generic.List[string]]::new()
|
||
$devNodes = @($comp.SelectNodes(".//*[local-name()='Device']"))
|
||
foreach ($dn in $devNodes) {
|
||
$id = $dn.GetAttribute('componentID')
|
||
if ($id) { [void]$compIds.Add($id) }
|
||
}
|
||
if ($compIds.Count -eq 0) { continue }
|
||
|
||
# Build a deterministic sortable key: zero-pad each numeric segment to 6 digits
|
||
$versionSortable = ($versionArr | ForEach-Object { $_.ToString('D6') }) -join '-'
|
||
|
||
# Capture a human‑readable driver name (preserve spaces like HP/Lenovo; remove only illegal path chars and extra whitespace)
|
||
$displayNode = $comp.SelectSingleNode("*[local-name()='Name']/*[local-name()='Display']")
|
||
$nameRaw = if ($displayNode) { $displayNode.InnerText.Trim() } else { $fileName }
|
||
# Remove characters not suitable for display (and disallowed in file names) but keep spaces
|
||
$nameDisplay = $nameRaw -replace '[\\\/:\*\?\"\<\>\|]', ' ' -replace '[,]', '-'
|
||
# Collapse multiple spaces to single
|
||
$nameDisplay = ($nameDisplay -replace '\s+', ' ').Trim()
|
||
|
||
$rawPackages.Add([pscustomobject]@{
|
||
Path = $path
|
||
DownloadUrl = $downloadUrl
|
||
FileName = $fileName
|
||
Name = $nameDisplay
|
||
Category = $category
|
||
VendorVersion = $vendorVersion
|
||
VersionArray = $versionArr
|
||
VersionSortable = $versionSortable
|
||
DateTime = $dt
|
||
ComponentIds = $compIds
|
||
})
|
||
}
|
||
|
||
if ($rawPackages.Count -eq 0) { return @() }
|
||
|
||
# Sort newest first by VersionSortable (lexicographic works due to zero padding) then DateTime
|
||
$sorted = $rawPackages | Sort-Object -Property @{ Expression = { $_.VersionSortable }; Descending = $true }, @{ Expression = { $_.DateTime }; Descending = $true }
|
||
|
||
$chosen = [System.Collections.Generic.List[pscustomobject]]::new()
|
||
$assignedIds = [System.Collections.Generic.HashSet[string]]::new()
|
||
|
||
foreach ($pkg in $sorted) {
|
||
$hasOverlap = $false
|
||
foreach ($cid in $pkg.ComponentIds) {
|
||
if ($assignedIds.Contains($cid)) { $hasOverlap = $true; break }
|
||
}
|
||
if ($hasOverlap) {
|
||
WriteLog "Get-DellLatestDriverPackages: Skipping superseded package $($pkg.FileName) (shared componentID with newer package)."
|
||
continue
|
||
}
|
||
|
||
foreach ($cid in $pkg.ComponentIds) { [void]$assignedIds.Add($cid) }
|
||
|
||
$chosen.Add([pscustomobject]@{
|
||
Path = $pkg.Path
|
||
DownloadUrl = $pkg.DownloadUrl
|
||
DriverFileName = $pkg.FileName
|
||
Name = $pkg.Name
|
||
Category = $pkg.Category
|
||
VendorVersion = $pkg.VendorVersion
|
||
DateTime = $pkg.DateTime
|
||
ComponentIds = $pkg.ComponentIds
|
||
})
|
||
}
|
||
|
||
if ($chosen.Count -eq 0) {
|
||
WriteLog "Get-DellLatestDriverPackages: No qualifying driver packages after supersedence."
|
||
return @()
|
||
}
|
||
|
||
WriteLog ("Get-DellLatestDriverPackages: Selected {0} package(s) after supersedence." -f $chosen.Count)
|
||
return $chosen
|
||
}
|
||
|
||
# Resolve a Dell per‑model CabUrl when missing by inspecting CatalogIndexPC
|
||
function Resolve-DellCabUrlFromModel {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory = $true)][string]$DriversFolder,
|
||
[Parameter()][string]$ModelDisplay,
|
||
[Parameter()][string]$SystemId
|
||
)
|
||
|
||
if ([string]::IsNullOrWhiteSpace($SystemId) -and -not [string]::IsNullOrWhiteSpace($ModelDisplay)) {
|
||
# Try to parse the trailing (XXXX) token (SystemId)
|
||
if ($ModelDisplay -match '\(([0-9A-Fa-f]{4})\)\s*$') {
|
||
$SystemId = $matches[1].ToUpperInvariant()
|
||
}
|
||
}
|
||
|
||
if ([string]::IsNullOrWhiteSpace($SystemId)) {
|
||
WriteLog "Resolve-DellCabUrlFromModel: No SystemId could be determined from '$ModelDisplay'."
|
||
return $null
|
||
}
|
||
|
||
try {
|
||
$indexXml = Get-DellCatalogIndex -DriversFolder $DriversFolder
|
||
# Reuse existing model parsing to avoid duplicating streaming logic
|
||
$allModels = Get-DellClientModels -CatalogIndexXmlPath $indexXml
|
||
$match = $allModels | Where-Object { $_.SystemId -eq $SystemId } | Select-Object -First 1
|
||
if ($null -eq $match) {
|
||
WriteLog "Resolve-DellCabUrlFromModel: SystemId '$SystemId' not found in CatalogIndexPC.xml."
|
||
return $null
|
||
}
|
||
WriteLog "Resolve-DellCabUrlFromModel: Resolved CabUrl for '$($match.ModelDisplay)' -> $($match.CabUrl)"
|
||
return [pscustomobject]@{
|
||
Brand = $match.Brand
|
||
ModelNumber = $match.ModelNumber
|
||
SystemId = $match.SystemId
|
||
CabRelativePath = $match.CabRelativePath
|
||
CabUrl = $match.CabUrl
|
||
ModelDisplay = $match.ModelDisplay
|
||
}
|
||
}
|
||
catch {
|
||
WriteLog "Resolve-DellCabUrlFromModel: Failure resolving CabUrl for '$ModelDisplay' / SystemId '$SystemId' : $($_.Exception.Message)"
|
||
return $null
|
||
}
|
||
}
|
||
|
||
Export-ModuleMember -Function Convert-DellVendorVersion,Compare-DellVendorVersion,Get-DellCatalogIndex,Get-DellClientModels,Get-DellLatestDriverPackages,Resolve-DellCabUrlFromModel |