Public/HP.Client.Update/Private/Resolve-HPUpdateState.ps1

function Get-HPUpdateReferenceCatalog {
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][psobject]$OperatingSystem,
        [string]$CatalogDirectory = (Get-HPUpdatePath -Name Catalogs)
    )

    $architecture = if ([Environment]::Is64BitOperatingSystem) { '64' } else { '32' }
    $referenceVersion = if ($OperatingSystem.Family -eq 'win11') { "11.0.$($OperatingSystem.Version)" } else { "10.0.$($OperatingSystem.Version)" }
    $cabReferenceVersion = $referenceVersion.ToLowerInvariant()
    $referenceName = "${Platform}_${architecture}_${referenceVersion}"
    $cabReferenceName = "${Platform}_${architecture}_${cabReferenceVersion}"
    $referenceDirectory = Join-Path $CatalogDirectory 'Reference'
    $cabPath = Join-Path $referenceDirectory "$cabReferenceName.cab"
    $extractDirectory = "$cabPath.dir"
    $xmlPath = @(Get-ChildItem -LiteralPath $extractDirectory -Filter '*.xml' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName)

    if (-not $xmlPath) {
        $null = New-Item -Path $referenceDirectory -ItemType Directory -Force
        Invoke-WebRequest -Uri "https://hpia.hpcloud.hp.com/ref/$Platform/$cabReferenceName.cab" -OutFile $cabPath -ErrorAction Stop
        Remove-Item -LiteralPath $extractDirectory -Recurse -Force -ErrorAction SilentlyContinue
        $null = New-Item -Path $extractDirectory -ItemType Directory -Force
        $shell = New-Object -ComObject Shell.Application
        try {
            $sourceCab = $shell.Namespace($cabPath).Items()
            $destination = $shell.Namespace($extractDirectory)
            $destination.CopyHere($sourceCab)
        }
        finally {
            [System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$shell) | Out-Null
        }
        $xmlPath = @(Get-ChildItem -LiteralPath $extractDirectory -Filter '*.xml' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName)
    }

    if (-not $xmlPath) { throw "HP reference XML was not created from '$cabPath'." }
    $catalog = [System.Xml.XmlDocument]::new()
    $catalog.XmlResolver = $null
    $catalog.Load($xmlPath)
    $catalog
}

function Get-HPUpdateDriverInventory {
    @(Get-CimInstance -ClassName Win32_PnPSignedDriver -ErrorAction Stop | Where-Object { $_.DeviceID -and $_.DriverVersion })
}

function ConvertTo-HPUpdateVersion {
    param([object]$Value)

    $match = [regex]::Match([string]$Value, '\d+(?:[.,]\d+){1,3}')
    if (-not $match.Success) { return $null }
    $version = $null
    if ([version]::TryParse(($match.Value -replace ',', '.'), [ref]$version)) { return $version }
    $null
}

function Resolve-HPUpdateDetailPath {
    param([Parameter(Mandatory)][string]$Path)

    $programFilesX86 = ${env:ProgramFiles(x86)}
    $windowsDisk = [IO.Path]::GetPathRoot($env:windir).TrimEnd('\')
    $resolvedPath = $Path
    $tokenPaths = @{
        '<WINSYSDIR>' = (Join-Path $env:windir 'System32')
        '<WINDIR>' = $env:windir
        '<WINDISK>' = $windowsDisk
        '<DRIVERS>' = (Join-Path $env:windir 'System32\drivers')
        '<PROGRAMFILESDIR>' = $env:ProgramFiles
        '<PROGRAMFILESDIRX86>' = $programFilesX86
        '<PROGRAMFILES>' = $env:ProgramFiles
    }
    foreach ($token in $tokenPaths.Keys) {
        if ($tokenPaths[$token]) { $resolvedPath = $resolvedPath -ireplace [regex]::Escape($token), [string]$tokenPaths[$token] }
    }
    if ($resolvedPath -match '<[^>]+>') { return $null }
    $resolvedPath
}

function Get-HPUpdateDetailFileState {
    param(
        [Parameter(Mandatory)][psobject]$Softpaq,
        [Parameter(Mandatory)][psobject]$OperatingSystem,
        [Parameter(Mandatory)][hashtable]$FileVersionCache
    )

    $osCode = ('{0}_{1}' -f $OperatingSystem.Family.ToUpperInvariant().Replace('WIN', 'W'), $OperatingSystem.Version)
    $results = [System.Collections.Generic.List[object]]::new()
    foreach ($detailFile in @($Softpaq.detailFileInfo | Where-Object { $_.cvaOsCode -eq $osCode })) {
        $expectedVersion = ConvertTo-HPUpdateVersion "$($detailFile.majorVersion).$($detailFile.minorVersion).$($detailFile.majorBuild).$($detailFile.minorBuild)"
        if (-not $expectedVersion -or -not $detailFile.fileName) { continue }

        $cacheKey = [string]$detailFile.fileName
        if (-not $FileVersionCache.ContainsKey($cacheKey)) {
            $candidates = [System.Collections.Generic.List[object]]::new()
            $expandedPath = Resolve-HPUpdateDetailPath -Path ([string]$detailFile.path)
            if ($expandedPath) {
                try {
                    $directPath = Join-Path $expandedPath $detailFile.fileName
                    if (Test-Path -LiteralPath $directPath -PathType Leaf) {
                        $candidates.Add((Get-Item -LiteralPath $directPath))
                    }
                }
                catch {
                    Write-Verbose "Ignoring invalid HP detail-file path '$($detailFile.path)': $($_.Exception.Message)"
                }
            }
            if ($expandedPath -and $expandedPath -match '(?i)DriverStore\\FileRepository') {
                $driverStore = Join-Path $env:windir 'System32\DriverStore\FileRepository'
                foreach ($file in (Get-ChildItem -LiteralPath $driverStore -Filter $detailFile.fileName -File -Recurse -ErrorAction SilentlyContinue)) { $candidates.Add($file) }
            }
            $FileVersionCache[$cacheKey] = @($candidates)
        }

        $files = @($FileVersionCache[$cacheKey])
        $installedVersions = @($files | ForEach-Object { ConvertTo-HPUpdateVersion $_.VersionInfo.FileVersion } | Where-Object { $_ })
        $results.Add([pscustomobject]@{
            FileName = [string]$detailFile.fileName
            ExpectedVersion = $expectedVersion
            InstalledVersions = $installedVersions
            InstalledVersion = @($installedVersions | Sort-Object -Descending | Select-Object -First 1)[0]
            FilesFound = $files.Count
            UsesDriverBinaryPath = ([string]$detailFile.path) -match '(?i)<DRIVERS>'
        })
    }
    @($results)
}

function Get-HPUpdateWindowsUpdateVersions {
    param([Parameter(Mandatory)][psobject]$Softpaq)

    @($Softpaq.windowsUpdates | Where-Object { $_ -match '(?i)SoftwareComponent' } | ForEach-Object {
        [regex]::Matches([string]$_, '\d+(?:\.\d+){1,3}') | ForEach-Object { ConvertTo-HPUpdateVersion $_.Value }
    } | Where-Object { $_ } | Sort-Object -Descending -Unique)
}

function Resolve-HPUpdateState {
    param(
        [Parameter(Mandatory)][psobject]$Softpaq,
        [Parameter(Mandatory)][object[]]$DriverInventory,
        [Parameter(Mandatory)][System.Xml.XmlDocument]$ReferenceCatalog,
        [Parameter(Mandatory)][psobject]$OperatingSystem,
        [Parameter(Mandatory)][hashtable]$FileVersionCache
    )

    $matches = [System.Collections.Generic.List[object]]::new()
    foreach ($device in @($Softpaq.devices)) {
        $deviceId = ([string]$device.matchId).Trim().Trim('*')
        if ([string]::IsNullOrWhiteSpace($deviceId)) { continue }
        $installedDevice = @($DriverInventory | Where-Object { $_.DeviceID.IndexOf($deviceId, [StringComparison]::OrdinalIgnoreCase) -ge 0 } | Select-Object -First 1)
        if (-not $installedDevice) { continue }
        $referenceDevice = @($ReferenceCatalog.SelectNodes('ImagePal/Devices/Device') | Where-Object { ([string]$_.DeviceID).IndexOf($deviceId, [StringComparison]::OrdinalIgnoreCase) -ge 0 } | Select-Object -First 1)
        $matches.Add([pscustomobject]@{
            DeviceName = [string]$installedDevice.DeviceName
            DeviceId = [string]$installedDevice.DeviceID
            InstalledVersion = [string]$installedDevice.DriverVersion
            ExpectedVersion = if ($referenceDevice) { [string]$referenceDevice.DriverVersion } else { '' }
            ReferenceMatched = [bool]$referenceDevice
        })
    }

    if (-not $matches.Count) { return [pscustomobject]@{ IsApplicable = $false; IsInstalled = $null; WhyApplicable = 'No supported installed device was found.'; Matches = @() } }

    $driverBinaryMatches = @($matches | Where-Object {
        $installedVersion = $null; $expectedVersion = $null
        [version]::TryParse($_.InstalledVersion, [ref]$installedVersion) -and [version]::TryParse($_.ExpectedVersion, [ref]$expectedVersion) -and $installedVersion -lt $expectedVersion
    })
    $driverCurrentMatches = @($matches | Where-Object {
        $installedVersion = $null; $expectedVersion = $null
        [version]::TryParse($_.InstalledVersion, [ref]$installedVersion) -and [version]::TryParse($_.ExpectedVersion, [ref]$expectedVersion) -and $installedVersion -ge $expectedVersion
    })
    $familyKey = @($matches.DeviceId | Sort-Object -Unique) -join '|'
    $detailFiles = @(Get-HPUpdateDetailFileState -Softpaq $Softpaq -OperatingSystem $OperatingSystem -FileVersionCache $FileVersionCache)
    $outdatedFiles = @($detailFiles | Where-Object { $_.InstalledVersion -and $_.InstalledVersion -lt $_.ExpectedVersion })
    $currentFiles = @($detailFiles | Where-Object { $_.InstalledVersion -and $_.InstalledVersion -ge $_.ExpectedVersion })
    if ($currentFiles.Count) { return [pscustomobject]@{ IsApplicable = $true; IsInstalled = $true; WhyApplicable = 'Installed HP package detail files meet or exceed the catalog version.'; Matches = @($matches); PnpOutdated = [bool]$driverBinaryMatches.Count; FamilyKey = $familyKey } }

    $windowsUpdateVersions = @(Get-HPUpdateWindowsUpdateVersions -Softpaq $Softpaq)
    $hasNewerWindowsUpdateComponent = [bool]($driverCurrentMatches | Where-Object {
        $installedVersion = ConvertTo-HPUpdateVersion $_.InstalledVersion
        $installedVersion -and @($windowsUpdateVersions | Where-Object { $_ -gt $installedVersion }).Count
    } | Select-Object -First 1)
    if ($hasNewerWindowsUpdateComponent) { return [pscustomobject]@{ IsApplicable = $true; IsInstalled = $false; WhyApplicable = 'An HP Windows Update component version is newer than the active driver version.'; Matches = @($matches); PnpOutdated = $true; FamilyKey = $familyKey } }
    if ($driverCurrentMatches.Count) { return [pscustomobject]@{ IsApplicable = $true; IsInstalled = $true; WhyApplicable = 'The active PnP driver version meets or exceeds the HP reference catalog version.'; Matches = @($matches); PnpOutdated = [bool]$driverBinaryMatches.Count; FamilyKey = $familyKey } }

    $unresolvedPackageDetails = @($detailFiles | Where-Object { $_.FilesFound -eq 0 -and -not $_.UsesDriverBinaryPath })
    if ($detailFiles.Count -and $unresolvedPackageDetails.Count -eq $detailFiles.Count) { return [pscustomobject]@{ IsApplicable = $false; IsInstalled = $null; WhyApplicable = 'HP package-specific detail files are absent, so the associated PnP device alone is insufficient to establish an update requirement.'; Matches = @($matches); PnpOutdated = [bool]$driverBinaryMatches.Count; FamilyKey = $familyKey } }
    if ($outdatedFiles.Count) { return [pscustomobject]@{ IsApplicable = $true; IsInstalled = $false; WhyApplicable = 'An installed HP package detail file is older than the catalog version.'; Matches = @($matches); PnpOutdated = [bool]$driverBinaryMatches.Count; FamilyKey = $familyKey } }
    [pscustomobject]@{ IsApplicable = $true; IsInstalled = -not $driverBinaryMatches.Count; WhyApplicable = if ($driverBinaryMatches.Count) { 'Installed driver version is older than the HP reference catalog version.' } else { 'No local package detail file was found for this supported device.' }; Matches = @($matches); PnpOutdated = [bool]$driverBinaryMatches.Count; FamilyKey = $familyKey }
}