DellWinPEDrivers.psm1

Set-StrictMode -Version Latest

$script:DellCatalogUrl = 'https://downloads.dell.com/catalog/DriverPackCatalog.cab'

function Test-DellWindows {
    [CmdletBinding()]
    param()

    if ($PSVersionTable.PSVersion.Major -lt 6) {
        return $true
    }

    return [bool]$IsWindows
}

function Get-DellCatalogDocument {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string] $CatalogPath
    )

    if ($CatalogPath) {
        if (-not (Test-Path -LiteralPath $CatalogPath -PathType Leaf)) {
            throw "CatalogPath '$CatalogPath' does not exist."
        }

        if ([System.IO.Path]::GetExtension($CatalogPath) -ieq '.xml') {
            [System.Xml.XmlDocument] $document = New-Object System.Xml.XmlDocument
            $document.Load($CatalogPath)
            return ,$document
        }

        throw "CatalogPath must point to an extracted DriverPackCatalog.xml file."
    }

    if (-not (Test-DellWindows)) {
        throw 'Downloading Dell''s CAB catalog requires Windows because expand.exe is used to extract DriverPackCatalog.xml. Supply -CatalogPath with an extracted XML file on non-Windows systems.'
    }

    $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('DellWinPEDrivers-' + [guid]::NewGuid().ToString('N'))
    $cabPath = Join-Path $tempRoot 'DriverPackCatalog.cab'
    $xmlPath = Join-Path $tempRoot 'DriverPackCatalog.xml'

    New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null

    try {
        $null = Invoke-WebRequest -Uri $script:DellCatalogUrl -OutFile $cabPath -UseBasicParsing

        $expand = Join-Path $env:SystemRoot 'System32\expand.exe'
        if (-not (Test-Path -LiteralPath $expand)) {
            throw 'expand.exe was not found.'
        }

        & $expand $cabPath $xmlPath | Out-Null
        $expandExitCode = $LASTEXITCODE
        if ($expandExitCode -ne 0 -or -not (Test-Path -LiteralPath $xmlPath)) {
            throw "Failed to extract DriverPackCatalog.xml from Dell catalog CAB. expand.exe exit code: $expandExitCode"
        }

        [System.Xml.XmlDocument] $document = New-Object System.Xml.XmlDocument
        $document.Load($xmlPath)
        return ,$document
    }
    finally {
        if (Test-Path -LiteralPath $tempRoot) {
            Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

function ConvertFrom-DellWinPEPackageNode {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Xml.XmlElement] $Node,

        [Parameter(Mandatory)]
        [string] $BaseLocation
    )

    $operatingSystems = @($Node.SelectNodes("./*[local-name()='SupportedOperatingSystems']/*[local-name()='OperatingSystem']"))
    if ($operatingSystems.Count -eq 0) {
        $operatingSystems = @($null)
    }

    $displayName = $null
    $displayNodes = @($Node.SelectNodes("./*[local-name()='Name']/*[local-name()='Display']"))
    if ($displayNodes.Count -gt 0) {
        $display = $displayNodes | Where-Object { $_.GetAttribute('lang') -eq 'en' } | Select-Object -First 1
        if (-not $display) {
            $display = $displayNodes | Select-Object -First 1
        }
        if ($display) {
            $displayName = [string]$display.InnerText
        }
    }

    $releaseDate = $null
    $dateTimeValue = [string]$Node.GetAttribute('dateTime')
    if ($dateTimeValue) {
        [datetime]$parsedDate = [datetime]::MinValue
        if ([datetime]::TryParse($dateTimeValue, [ref]$parsedDate)) {
            $releaseDate = $parsedDate
        }
    }

    $packagePath = ([string]$Node.GetAttribute('path')).TrimStart('/')
    $base = $BaseLocation.Trim().TrimEnd('/')
    if ($base -notmatch '^https?://') {
        $base = 'https://' + $base
    }
    $downloadUrl = if ($packagePath) { "$base/$packagePath" } else { $null }

    $winPEVersion = $null
    $versionSource = if ($displayName) { $displayName } else { [string]$Node.GetAttribute('path') }
    if ($versionSource -match '(?i)WinPE\s*(?<Version>\d+(?:\.\d+)?)') {
        $winPEVersion = $Matches.Version
    }

    foreach ($os in $operatingSystems) {
        [pscustomobject]@{
            PSTypeName      = 'DellWinPEDrivers.Package'
            Name            = $displayName
            Type            = [string]$Node.GetAttribute('type')
            WinPEVersion    = $winPEVersion
            DellVersion     = [string]$Node.GetAttribute('dellVersion')
            ReleaseId       = [string]$Node.GetAttribute('releaseID')
            ReleaseDate     = $releaseDate
            Path            = [string]$Node.GetAttribute('path')
            DownloadUrl     = $downloadUrl
            Architecture    = if ($os) { [string]$os.GetAttribute('osArch') } else { $null }
            OSVendor        = if ($os) { [string]$os.GetAttribute('osVendor') } else { $null }
            MajorVersion    = if ($os) { [string]$os.GetAttribute('majorVersion') } else { $null }
            MinorVersion    = if ($os) { [string]$os.GetAttribute('minorVersion') } else { $null }
            SPMajorVersion  = if ($os) { [string]$os.GetAttribute('spMajorVersion') } else { $null }
            SPMinorVersion  = if ($os) { [string]$os.GetAttribute('spMinorVersion') } else { $null }
        }
    }
}

function Get-DellWinPEDriverPackInfo {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('x64', 'x86')]
        [string] $Architecture,

        [Parameter()]
        [string] $WinPEVersion,

        [Parameter()]
        [string] $MajorVersion,

        [Parameter()]
        [string] $MinorVersion,

        [Parameter()]
        [string] $CatalogPath
    )

    $catalog = Get-DellCatalogDocument -CatalogPath $CatalogPath

    if ($catalog -is [System.Xml.XmlDocument]) {
        $manifest = $catalog.DocumentElement
    }
    elseif ($catalog -is [System.Xml.XmlElement]) {
        $manifest = $catalog
    }
    else {
        throw "Unexpected Dell catalog object type: $($catalog.GetType().FullName)"
    }

    if (-not $manifest -or $manifest.LocalName -ne 'DriverPackManifest') {
        throw 'The supplied XML is not a Dell DriverPackCatalog document.'
    }

    $baseLocation = [string]$manifest.GetAttribute('baseLocation')
    if ([string]::IsNullOrWhiteSpace($baseLocation)) {
        throw 'Dell catalog does not contain DriverPackManifest.baseLocation.'
    }

    $packageNodes = @($manifest.SelectNodes("./*[local-name()='DriverPackage']"))

    $packages = foreach ($node in $packageNodes) {
        if ([string]$node.GetAttribute('type') -ine 'WinPE') {
            continue
        }

        ConvertFrom-DellWinPEPackageNode -Node $node -BaseLocation $baseLocation
    }

    if ($Architecture) {
        $packages = @($packages | Where-Object Architecture -EQ $Architecture)
    }
    if ($WinPEVersion) {
        $packages = @($packages | Where-Object WinPEVersion -EQ $WinPEVersion)
    }
    if ($MajorVersion) {
        $packages = @($packages | Where-Object MajorVersion -EQ $MajorVersion)
    }
    if ($MinorVersion) {
        $packages = @($packages | Where-Object MinorVersion -EQ $MinorVersion)
    }

    $packages | Sort-Object ReleaseDate, DellVersion -Descending
}

function Select-DellWinPEDriverPack {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]] $Package
    )

    $candidate = @($Package | Sort-Object ReleaseDate, DellVersion -Descending | Select-Object -First 1)
    if ($candidate.Count -eq 0) {
        throw 'No Dell WinPE driver pack matched the supplied filters.'
    }

    $candidate[0]
}

function New-DellWinPEManifest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter()]
        [ValidateSet('x64', 'x86')]
        [string] $Architecture,

        [Parameter()]
        [string] $WinPEVersion,

        [Parameter()]
        [string] $MajorVersion,

        [Parameter()]
        [string] $MinorVersion,

        [Parameter()]
        [string] $CatalogPath
    )

    $getParams = @{}
    if ($PSBoundParameters.ContainsKey('Architecture')) { $getParams.Architecture = $Architecture }
    if ($PSBoundParameters.ContainsKey('WinPEVersion')) { $getParams.WinPEVersion = $WinPEVersion }
    if ($PSBoundParameters.ContainsKey('MajorVersion')) { $getParams.MajorVersion = $MajorVersion }
    if ($PSBoundParameters.ContainsKey('MinorVersion')) { $getParams.MinorVersion = $MinorVersion }
    if ($PSBoundParameters.ContainsKey('CatalogPath')) { $getParams.CatalogPath = $CatalogPath }

    $packages = @(Get-DellWinPEDriverPackInfo @getParams -ErrorAction Stop)

    $manifest = [ordered]@{
        SchemaVersion = 1
        GeneratedAtUtc = [datetime]::UtcNow.ToString('o')
        Manufacturer = 'Dell'
        CatalogUrl = $script:DellCatalogUrl
        Packages = @($packages | ForEach-Object {
            [ordered]@{
                Name = $_.Name
                Type = $_.Type
                WinPEVersion = $_.WinPEVersion
                DellVersion = $_.DellVersion
                ReleaseId = $_.ReleaseId
                ReleaseDate = if ($_.ReleaseDate) { $_.ReleaseDate.ToString('o') } else { $null }
                Architecture = $_.Architecture
                MajorVersion = $_.MajorVersion
                MinorVersion = $_.MinorVersion
                DownloadUrl = $_.DownloadUrl
                Path = $_.Path
            }
        })
    }

    $parent = Split-Path -Parent $Path
    if ($parent -and -not (Test-Path -LiteralPath $parent)) {
        New-Item -ItemType Directory -Path $parent -Force | Out-Null
    }

    $manifest | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $Path -Encoding UTF8
    Get-Item -LiteralPath $Path
}

function Save-DellWinPEDriverPack {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter()]
        [ValidateSet('x64', 'x86')]
        [string] $Architecture = 'x64',

        [Parameter()]
        [string] $WinPEVersion,

        [Parameter()]
        [string] $MajorVersion,

        [Parameter()]
        [string] $MinorVersion,

        [Parameter()]
        [string] $CatalogPath,

        [Parameter()]
        [switch] $Force,

        [Parameter()]
        [switch] $PassThru
    )

    if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) {
        throw 'Save-DellWinPEDriverPack requires Windows because Dell driver CABs are extracted with expand.exe.'
    }

    $getParams = @{
        Architecture = $Architecture
    }
    if ($PSBoundParameters.ContainsKey('WinPEVersion')) { $getParams.WinPEVersion = $WinPEVersion }
    if ($PSBoundParameters.ContainsKey('MajorVersion')) { $getParams.MajorVersion = $MajorVersion }
    if ($PSBoundParameters.ContainsKey('MinorVersion')) { $getParams.MinorVersion = $MinorVersion }
    if ($PSBoundParameters.ContainsKey('CatalogPath')) { $getParams.CatalogPath = $CatalogPath }

    $packages = @(Get-DellWinPEDriverPackInfo @getParams -ErrorAction Stop)
    $selected = Select-DellWinPEDriverPack -Package $packages

    if ([string]::IsNullOrWhiteSpace($selected.DownloadUrl)) {
        throw 'Selected Dell WinPE driver pack does not contain a download URL.'
    }

    $safeName = @(
        'WinPE'
        if ($selected.WinPEVersion) { $selected.WinPEVersion }
        if ($selected.DellVersion) { $selected.DellVersion }
        if ($selected.Architecture) { $selected.Architecture }
    ) -join '-'
    $safeName = $safeName -replace '[^A-Za-z0-9._-]', '_'

    $destination = Join-Path $Path $safeName
    $metadataPath = Join-Path $destination '.dellwinpe.json'

    if ((Test-Path -LiteralPath $metadataPath) -and -not $Force) {
        $existing = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json
        if ($existing.ReleaseId -eq $selected.ReleaseId -and $existing.DellVersion -eq $selected.DellVersion) {
            if ($PassThru) {
                return [pscustomobject]@{
                    Status = 'Current'
                    Path = $destination
                    Package = $selected
                }
            }
            return
        }
    }

    if (-not $PSCmdlet.ShouldProcess($destination, "Download and extract Dell WinPE driver pack $($selected.DellVersion)")) {
        return
    }

    $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('DellWinPEDrivers-' + [guid]::NewGuid().ToString('N'))
    $cabPath = Join-Path $tempRoot ([System.IO.Path]::GetFileName(([uri]$selected.DownloadUrl).AbsolutePath))

    New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null

    try {
        $null = Invoke-WebRequest -Uri $selected.DownloadUrl -OutFile $cabPath -UseBasicParsing

        if (Test-Path -LiteralPath $destination) {
            if ($Force) {
                Remove-Item -LiteralPath $destination -Recurse -Force
            }
            else {
                throw "Destination '$destination' already exists. Use -Force to rebuild it."
            }
        }

        New-Item -ItemType Directory -Path $destination -Force | Out-Null

        $expand = Join-Path $env:SystemRoot 'System32\expand.exe'
        if (-not (Test-Path -LiteralPath $expand)) {
            throw 'expand.exe was not found.'
        }

        & $expand $cabPath '-F:*' $destination | Out-Null
        $expandExitCode = $LASTEXITCODE
        if ($expandExitCode -ne 0) {
            throw "Failed to extract Dell WinPE driver CAB. expand.exe exit code: $expandExitCode"
        }

        $metadata = [ordered]@{
            SchemaVersion = 1
            Manufacturer = 'Dell'
            Name = $selected.Name
            WinPEVersion = $selected.WinPEVersion
            DellVersion = $selected.DellVersion
            ReleaseId = $selected.ReleaseId
            ReleaseDate = if ($selected.ReleaseDate) { $selected.ReleaseDate.ToString('o') } else { $null }
            Architecture = $selected.Architecture
            MajorVersion = $selected.MajorVersion
            MinorVersion = $selected.MinorVersion
            DownloadUrl = $selected.DownloadUrl
            SourceCatalog = $script:DellCatalogUrl
            SavedAtUtc = [datetime]::UtcNow.ToString('o')
        }

        $metadata | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $metadataPath -Encoding UTF8

        if ($PassThru) {
            [pscustomobject]@{
                Status = 'Saved'
                Path = $destination
                Package = $selected
            }
        }
    }
    catch {
        if (Test-Path -LiteralPath $destination) {
            Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction SilentlyContinue
        }
        throw
    }
    finally {
        if (Test-Path -LiteralPath $tempRoot) {
            Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

Export-ModuleMember -Function @(
    'Get-DellWinPEDriverPackInfo'
    'Save-DellWinPEDriverPack'
    'New-DellWinPEManifest'
)