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' }
    $referenceDirectory = Join-Path $CatalogDirectory 'Reference'
    $versions = @($(if ($OperatingSystem.Family -eq 'win11') { '11.0' } else { '10.0' }) + '.' + $OperatingSystem.Version.ToLowerInvariant())
    if ($OperatingSystem.Family -eq 'win11' -and $OperatingSystem.Version -eq '25H2') { $versions += '11.0.24h2' }
    $xmlPath = $null
    $expandPath = Join-Path $env:SystemRoot 'System32\expand.exe'

    function Test-HPUpdateCabSignature {
        param([Parameter(Mandatory)][string]$Path)

        try {
            $stream = [System.IO.File]::OpenRead($Path)
            try {
                if ($stream.Length -lt 4) { return $false }
                $buffer = [byte[]]::new(4)
                $null = $stream.Read($buffer, 0, 4)
                return ([System.Text.Encoding]::ASCII.GetString($buffer) -eq 'MSCF')
            }
            finally {
                $stream.Dispose()
            }
        }
        catch {
            return $false
        }
    }

    function Get-HPUpdateExtractedXmlPath {
        param([Parameter(Mandatory)][string]$Directory)

        $xmlPathByName = @(Get-ChildItem -LiteralPath $Directory -Filter '*.xml' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName)
        if ($xmlPathByName) { return $xmlPathByName }

        foreach ($candidate in @(Get-ChildItem -LiteralPath $Directory -File -Recurse -ErrorAction SilentlyContinue)) {
            try {
                $document = [System.Xml.XmlDocument]::new()
                $document.XmlResolver = $null
                $document.Load($candidate.FullName)
                if ($document.DocumentElement -and $document.DocumentElement.LocalName -eq 'ImagePal') { return $candidate.FullName }
            }
            catch [System.Xml.XmlException] { }
        }
        $null
    }

    foreach ($version in $versions) {
        $cabReferenceName = "${Platform}_${architecture}_${version}"
        $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 ($xmlPath) { break }

        $null = New-Item -Path $referenceDirectory -ItemType Directory -Force
        try {
            Invoke-WebRequest -Uri "https://hpia.hpcloud.hp.com/ref/$Platform/$cabReferenceName.cab" -OutFile $cabPath -UseBasicParsing -ErrorAction Stop
        }
        catch {
            $statusCode = $null
            if ($_.Exception.Response -and $_.Exception.Response.StatusCode) {
                $statusCode = [int]$_.Exception.Response.StatusCode
            }
            if ($version -eq '11.0.25h2' -and $statusCode -eq 404) { continue }
            throw
        }

        if (-not (Test-Path -LiteralPath $cabPath -PathType Leaf)) { continue }
        $cabItem = Get-Item -LiteralPath $cabPath -ErrorAction SilentlyContinue
        if (-not $cabItem -or $cabItem.Length -le 0) { continue }

        Remove-Item -LiteralPath $extractDirectory -Recurse -Force -ErrorAction SilentlyContinue
        $null = New-Item -Path $extractDirectory -ItemType Directory -Force
        $queue = [System.Collections.Generic.Queue[string]]::new()
        $queue.Enqueue($cabPath)
        $depth = 0
        while ($queue.Count -and -not $xmlPath -and $depth -lt 4) {
            $depth++
            $currentCabPath = $queue.Dequeue()
            $stageDirectory = Join-Path $extractDirectory "stage$depth"
            Remove-Item -LiteralPath $stageDirectory -Recurse -Force -ErrorAction SilentlyContinue
            $null = New-Item -Path $stageDirectory -ItemType Directory -Force

            $expandOutput = & $expandPath $currentCabPath '-F:*' $stageDirectory 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-Verbose "Could not extract '$currentCabPath' (expand.exe exit code $LASTEXITCODE). $($expandOutput -join ' ')"
                continue
            }

            $xmlPath = @(Get-HPUpdateExtractedXmlPath -Directory $stageDirectory)
            if ($xmlPath) { break }

            foreach ($innerCab in @(Get-ChildItem -LiteralPath $stageDirectory -Filter '*.cab' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)) {
                if ($innerCab -and $innerCab -ne $currentCabPath -and (Test-HPUpdateCabSignature -Path $innerCab)) { $queue.Enqueue($innerCab) }
            }
        }

        if ($xmlPath) { break }
    }

    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)

    $resolvedPath = $Path
    $tokenPaths = [ordered]@{
        '<WINSYSDIR>' = (Join-Path $env:windir 'System32')
        '<DRIVERS>' = (Join-Path $env:windir 'System32\drivers')
        '<WINDIR>' = $env:windir
        '<WINDISK>' = [IO.Path]::GetPathRoot($env:windir).TrimEnd('\')
        '<SYSTEMDRIVE>' = $env:SystemDrive
        '<PROGRAMFILESDIRX86>' = ${env:ProgramFiles(x86)}
        '<PROGRAMFILESDIR>' = $env:ProgramFiles
        '<PROGRAMFILES>' = $env:ProgramFiles
        '<COMMONPROGRAMFILES>' = $env:CommonProgramFiles
        '<PROGRAMDATA>' = $env:ProgramData
    }
    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-HPUpdateDeviceMatches {
    param(
        [Parameter(Mandatory)][psobject]$Softpaq,
        [object[]]$DriverInventory = @(),
        [System.Xml.XmlDocument]$ReferenceCatalog
    )

    $deviceMatches = [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 = if ($ReferenceCatalog) { @($ReferenceCatalog.SelectNodes('ImagePal/Devices/Device') | Where-Object { ([string]$_.DeviceID).IndexOf($deviceId, [StringComparison]::OrdinalIgnoreCase) -ge 0 } | Select-Object -First 1) } else { $null }
        $deviceMatches.Add([pscustomobject]@{
            DeviceName = [string]$installedDevice.DeviceName
            DeviceId = [string]$installedDevice.DeviceID
            InstalledVersion = [string]$installedDevice.DriverVersion
            ExpectedVersion = if ($referenceDevice) { [string]$referenceDevice.DriverVersion } else { '' }
            ReferenceMatched = [bool]$referenceDevice
        })
    }
    @($deviceMatches)
}

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 = @(Get-HPUpdateDeviceMatches -Softpaq $Softpaq -DriverInventory $DriverInventory -ReferenceCatalog $ReferenceCatalog)

    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 }
}