private/Save-WinPEDriverPackageCore.ps1

#Requires -PSEdition Core

function Save-WinPEDriverPackageCore {
    <#
    .SYNOPSIS
        Selects, downloads, and optionally expands WinPE driver packages
 
    .DESCRIPTION
        Resolves WinPE driver packages from Get-WinPEDriverPackageCore and delegates each
        package to its vendor-specific save function. By default, all packages matching
        Name, Architecture, and Wi-Fi filters are processed without a picker.
 
        Interactive mode displays package status in Out-GridView and processes only the
        selected packages that require download or expansion. The function requires an
        elevated session except during -WhatIf processing. Download and cache changes
        are gated by ShouldProcess, but delegated helpers may replace or remove expanded
        driver content beneath the architecture-specific Boot-Assets driver directory.
 
    .PARAMETER Name
        Specifies one or more source names in the ByName parameter set. The argument
        completer excludes disabled sources and sources without a download or update URI.
        When omitted, all available sources are considered.
 
    .PARAMETER DriverPackage
        Specifies an OSDeployWinPEDriver.Package object in the ByPipeline parameter set.
        This parameter is mandatory, accepts pipeline input, and bypasses name filtering
        and the interactive picker.
 
    .PARAMETER Force
        Forces the delegated helper to download the package again. Additional force
        behavior, such as re-expansion, depends on the package type.
 
    .PARAMETER DownloadOnly
        Downloads the package without expanding it into the managed driver library.
 
    .PARAMETER Architecture
        Limits packages to amd64 or arm64. When omitted, both architectures are eligible.
 
    .PARAMETER SkipCatalogRefresh
        Indicates that the caller already refreshed the requested catalog entries. This
        parameter is available only in the ByName parameter set.
 
    .PARAMETER SkipWifiDrivers
        Excludes sources whose names contain wifi or wireless. This parameter is available
        only in the ByName parameter set.
 
    .PARAMETER Interactive
        Displays an Out-GridView multi-selection picker for packages that are not already
        complete. This parameter is available only in the ByName parameter set.
 
    .EXAMPLE
        PS> Save-WinPEDriverPackageCore -Name 'Dell' -Architecture amd64 -Interactive
 
        Displays matching Dell amd64 packages that require action and processes the selected packages.
 
    .INPUTS
        System.Management.Automation.PSCustomObject. Accepts OSDeployWinPEDriver.Package
        objects through DriverPackage.
 
    .OUTPUTS
        System.IO.FileInfo. Returns each package file produced by a delegated save function.
        Returns no object when no package is selected or requires action.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 0.1.0
        Date: 2026-08-28
 
        Requires an elevated Windows session for actual downloads. Out-GridView is required
        only when Interactive is specified.
 
    .LINK
        Update-OSDeployCoreDrivers
    #>

    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'Medium',
        DefaultParameterSetName = 'ByName'
    )]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(
            Position = 0,
            ParameterSetName = 'ByName',
            HelpMessage = 'Source name to download.'
        )]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            $global:OSDeployModule.WinPEDrivers.PSObject.Properties |
                Where-Object { ($_.Value.UpdateUri -or $_.Value.DownloadUri) -and -not $_.Value.Disabled -and $_.Name -like "$wordToComplete*" } |
                ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.Name) }
        })]
        [string[]]$Name,

        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'ByPipeline'
        )]
        [PSTypeName('OSDeployWinPEDriver.Package')]
        [PSCustomObject]$DriverPackage,

        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [switch]$DownloadOnly,

        [Parameter()]
        [ValidateSet('amd64', 'arm64')]
        [System.String]
        $Architecture,

        [Parameter(ParameterSetName = 'ByName')]
        [switch]$SkipCatalogRefresh,

        [Parameter(ParameterSetName = 'ByName')]
        [switch]$SkipWifiDrivers,

        [Parameter(ParameterSetName = 'ByName')]
        [switch]$Interactive
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Starting"

        if (-not $WhatIfPreference -and -not (Test-IsAdministrator)) {
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    [System.UnauthorizedAccessException]::new('Update-OSDeployCoreDrivers requires an elevated (Administrator) PowerShell session to save driver content.'),
                    'ElevationRequired',
                    [System.Management.Automation.ErrorCategory]::PermissionDenied,
                    $null
                )
            )
        }
    }

    process {
        $packages = if ($PSCmdlet.ParameterSetName -eq 'ByName') {
            $candidates = if ($Name) {
                Get-WinPEDriverPackageCore -Name $Name -SkipCatalogRefresh:$SkipCatalogRefresh
            }
            else {
                Get-WinPEDriverPackageCore -SkipCatalogRefresh:$SkipCatalogRefresh
            }

            if ($SkipWifiDrivers) {
                $candidates = $candidates | Where-Object { $_.Name -notmatch 'wifi|wireless' }
            }

            if ($Architecture) {
                $candidates = @($candidates | Where-Object {
                    [string]::IsNullOrWhiteSpace($_.Architecture) -or $_.Architecture -eq $Architecture
                })
            }

            $gridItems = $candidates | ForEach-Object {
                $pkg          = $_
                $idPrefix     = ($pkg.Name -split '-')[0]
                $downloadPath = Join-Path (Join-Path $script:OSDeployCoreDownloadsPath $idPrefix) $pkg.FileName
                if ($null -eq $pkg.Version) {
                    $pkg.Version = $pkg.PackageId
                }
                $expandDir    = Join-Path $script:OSDeployBootAssetsPath "winpedrivers-$($pkg.Architecture)" |
                                Join-Path -ChildPath "$($pkg.Name)-$($pkg.Version)"

                [PSCustomObject]@{
                    Id           = $pkg.Id
                    Architecture = $pkg.Architecture
                    ReleaseDate  = $pkg.ReleaseDate
                    Version      = $pkg.Version
                    FileName     = $pkg.FileName
                    FileSizeMB   = $pkg.FileSizeMB
                    DownloadUri  = $pkg.DownloadUri
                    ExpandedPath = $expandDir
                    Name         = "$($pkg.Name)-$($pkg.Version)"
                    Downloaded   = (Test-Path -Path $downloadPath) ? 'Yes' : 'No'
                    Expanded     = (Get-ChildItem -Path $expandDir -Recurse -File -ErrorAction SilentlyContinue |
                                       Select-Object -First 1) ? 'Yes' : 'No'
                }
            }

            if ($Interactive) {
                $upToDate    = $gridItems | Where-Object { $_.Downloaded -eq 'Yes' -and $_.Expanded -eq 'Yes' }
                $needsAction = $gridItems | Where-Object { $_.Downloaded -ne 'Yes' -or $_.Expanded -ne 'Yes' }

                foreach ($item in $upToDate) {
                    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Complete: $($item.Name)"
                }

                $downloadsPath = $script:OSDeployCoreDownloadsPath
                if (Test-Path -Path $downloadsPath) {
                    $downloadsSizeBytes = (Get-ChildItem -Path $downloadsPath -Recurse -File -ErrorAction SilentlyContinue |
                        Measure-Object -Property Length -Sum).Sum
                    $downloadsSizeGB = [math]::Round($downloadsSizeBytes / 1GB, 1)
                    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] $downloadsPath is currently using $downloadsSizeGB GB"
                }

                if (-not $needsAction) {
                    Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] All driver packages are downloaded, expanded, and up to date."
                    return
                }

                # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Select Drivers in GridView to download and expand"
                # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Downloads are saved to $($Script:OSDeployCorePath)\cache\downloads"
                # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [INFO] Expanded content is saved to the architecture-specific winpedrivers cache"
                $selected = $needsAction |
                    Select-Object -Property Id, Name, Architecture, ReleaseDate, Version, FileSizeMB, DownloadUri |
                    Out-GridView -Title "[$(Get-Date -Format s)] Select WinPE Drivers to add to $($Script:OSDeployCorePath)" -PassThru
                if (-not $selected) {
                    Write-Warning "[$(Get-Date -Format s)] No WinPE Drivers selected."
                    return
                }

                $selectedKeys = $selected | ForEach-Object { $_.Name }
                $candidates | Where-Object { "$($_.Name)-$($_.Version)" -in $selectedKeys }
            }
            else {
                if (-not $candidates) {
                    Write-Warning "[$(Get-Date -Format s)] No matching WinPE Drivers found for processing."
                    return
                }

                Write-Verbose "[$($MyInvocation.MyCommand.Name)] Processing all matching packages without Out-GridView"
                $candidates
            }
        }
        else {
            $DriverPackage
        }

        foreach ($package in $packages) {
            if ([string]::IsNullOrWhiteSpace($package.DownloadUri)) {
                Write-Warning "[$(Get-Date -Format s)] No DownloadUri for '$($package.Name)'. Skipping."
                continue
            }

            $action = "Download '$($package.FileName)'"
            if (-not $PSCmdlet.ShouldProcess($package.DownloadUri, $action)) {
                continue
            }

            $downloadedFile = $null
            try {
                $downloadedFile = switch -Wildcard ($package.Name) {
                    'intel-ethernet'              { Save-WinPEDriverPackageIntelEthernet -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    'intel-wifi'                  { Save-WinPEDriverPackageIntelWifi -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    'microsoft-windows-ethernet'  { Save-WinPEDriverPackageWindowsLofEthernet -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    'microsoft-windows-wifi'      { Save-WinPEDriverPackageWindowsLofWifi -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    'dell'                        { Save-WinPEDriverPackageDell -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    'hp'                          { Save-WinPEDriverPackageHp -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    'vmware'                      { Save-WinPEDriverPackageVMwareAmd64 -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    'vmware-arm64'                { Save-WinPEDriverPackageVMwareArm64 -DriverPackage $package -Force:$Force -DownloadOnly:$DownloadOnly -ErrorAction Stop }
                    default {
                        Write-Warning "[$(Get-Date -Format s)] No save function registered for '$($package.Name)'. Skipping."
                        $null
                    }
                }
            }
            catch {
                $isWafChallenge = $_.FullyQualifiedErrorId -like 'WafChallenge*' -or $_.Exception.Message -match 'AWS WAF challenge'
                if ($isWafChallenge) {
                    $idPrefix = ($package.Name -split '-')[0]
                    $manualPath = Join-Path (Join-Path $script:OSDeployCoreDownloadsPath $idPrefix) $package.FileName

                    $agreementUri = $null
                    $sourceConfig = $global:OSDeployModule.WinPEDrivers.PSObject.Properties[$package.Name].Value
                    if ($sourceConfig -and -not [string]::IsNullOrWhiteSpace($sourceConfig.UpdateUri)) {
                        $agreementUri = $sourceConfig.UpdateUri
                    }
                    elseif (-not [string]::IsNullOrWhiteSpace($package.DownloadUri)) {
                        $agreementUri = $package.DownloadUri
                    }

                    Write-Warning "[$(Get-Date -Format s)] $($package.Name) download skipped: $($_.Exception.Message)"
                    Write-Warning "[$(Get-Date -Format s)] Continue processing other packages. Manual $($package.Name) steps:"
                    Write-Warning "[$(Get-Date -Format s)] 1. Open this URL and accept the agreement: $agreementUri"
                    Write-Warning "[$(Get-Date -Format s)] 2. Download '$($package.FileName)' in a browser."
                    Write-Warning "[$(Get-Date -Format s)] 3. Place the file in '$manualPath'."
                    Write-Warning "[$(Get-Date -Format s)] 4. Rerun: Update-OSDeployCoreDrivers -Name '$($package.Name)'."
                }
                else {
                    Write-Warning "[$(Get-Date -Format s)] Failed to process '$($package.Name)': $($_.Exception.Message). Continuing with remaining packages."
                }
                continue
            }

            if (-not $downloadedFile) { continue }

            if ($DownloadOnly) {
                $downloadedFile
                continue
            }

            $arch            = if ([string]::IsNullOrWhiteSpace($package.Architecture)) { 'amd64' } else { $package.Architecture }
            $version         = if ([string]::IsNullOrWhiteSpace($package.Version)) { $package.PackageId } else { $package.Version }
            $jsonPath        = Join-Path $script:OSDeployBootAssetsPath "winpedrivers-$arch" |
                               Join-Path -ChildPath "$($package.Name)-$version" |
                               Join-Path -ChildPath 'package.json'

            Write-HostDateTimeDarkGray -Message "$jsonPath"

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

                $metadata = [ordered]@{
                    Name          = $package.Name
                    Architecture  = $arch
                    Id            = $package.Id
                    PackageId     = $package.PackageId
                    Version       = $package.Version
                    ReleaseDate   = $package.ReleaseDate
                    FileName      = $package.FileName
                    FileSizeMB    = $package.FileSizeMB
                    DownloadUri   = $package.DownloadUri
                    ExpandCommand = $package.ExpandCommand
                    Checksums     = $package.Checksums
                    DownloadedOn  = (Get-Date -Format 'yyyy-MM-dd')
                }

                try {
                    $metadata | ConvertTo-Json -Depth 5 |
                        Set-Content -Path $jsonPath -Encoding UTF8 -ErrorAction Stop
                    Write-Verbose "[$($MyInvocation.MyCommand.Name)] Metadata written to '$jsonPath'"
                }
                catch {
                    Write-Error "[$(Get-Date -Format s)] [$($MyInvocation.MyCommand.Name)] Failed to write package.json: $($_.Exception.Message)"
                }
            }

            $downloadedFile
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand.Name)] Complete"
    }
}