private/Test-MachineSatisfiesDependency.ps1

function Test-MachineSatisfiesDependency {
    [CmdletBinding()]
    [OutputType('System.Int32')]
    Param (
        [ValidateNotNullOrEmpty()]
        [System.Xml.XmlElement]$Dependency,
        [Parameter( Mandatory = $true )]
        [string]$PackagePath,
        [int]$DebugIndent = 0,
        [switch]$FailInboxDrivers
    )

    # 0 SUCCESS, Dependency is met
    # -1 FAILRE, Dependency is not met
    # -2 Unknown dependency kind - status uncertain

    switch ($Dependency.SchemaInfo.Name) {
        '_Bios' {
            Write-Debug "$('- ' * $DebugIndent)[ Got: $($CachedHardwareTable['_Bios']) ]"
            foreach ($entry in $Dependency.Level) {
                if ($CachedHardwareTable['_Bios'] -like "$entry*") {
                    return 0
                }
            }
            return -1
        }
        '_CPUAddressWidth' {
            Write-Debug "$('- ' * $DebugIndent)[ Got: $($CachedHardwareTable['_CPUAddressWidth']), Expected: $($dependency.AddressWidth) ]"
            if ($CachedHardwareTable['_CPUAddressWidth'] -like "$($Dependency.AddressWidth)*") {
                return 0
            } else {
                return -1
            }
        }
        '_Driver' {
            [array]$SupportedDriverNodes = 'HardwareID', 'Version', 'Date', 'File'
            [array]$DriverChildNodes = $Dependency.ChildNodes.SchemaInfo.Name
            if (-not (Compare-Array $DriverChildNodes -in $SupportedDriverNodes)) {
                Write-Debug "$('- ' * $DebugIndent)_Driver node contained unknown element - skipping checks"
                return -2
            }

            if ($DriverChildNodes -contains 'HardwareID') {
                [System.Collections.Generic.List[object]]$DevicesMatchedExact = [System.Collections.Generic.List[object]]::new()
                [System.Collections.Generic.List[object]]$DevicesMatchedWildcard = [System.Collections.Generic.List[object]]::new()
                [System.Collections.Generic.List[object]]$DevicesToTest = [System.Collections.Generic.List[object]]::new()

                :NextDevice foreach ($DeviceInMachine in $CachedHardwareTable['_PnPID']) {
                    [bool]$DeviceHwIdWildcardMatched = $false

                    foreach ($HardwareInMachine in $DeviceInMachine.HardwareID) {
                        # A _Driver node can have multiple 'HardwareID' child nodes, e.g. https://download.lenovo.com/pccbbs/mobiles/r1kwq15w_2_.xml
                        foreach ($HardwareID in $Dependency.HardwareID.'#cdata-section') {
                            # Matching with wildcards may have been a mistake, some HardwareIDs just contain a * (star).
                            # Try exact equal matches first and fall back to wildcard only when needed. I want to see how often that happens.
                            if ($HardwareInMachine -eq "$HardwareID") {
                                Write-Debug "$('- ' * $DebugIndent)Matched device '$HardwareInMachine' with required '$HardwareID' (EXACT)"
                                $DevicesMatchedExact.Add($DeviceInMachine)
                                continue NextDevice
                            }
                            # Lenovo HardwareIDs can contain wildcards (*) so we have to compare with "-like"
                            if ($HardwareInMachine -like "*$HardwareID*") {
                                Write-Debug "$('- ' * $DebugIndent)Matched device '$HardwareInMachine' with required '$HardwareID' (WILDCARD)"
                                $DeviceHwIdWildcardMatched = $true
                            }
                        }
                    }

                    # To preserve the old behavior whilst fully testing the new, do add devices that were only matched via wildcards
                    if ($DeviceHwIdWildcardMatched) {
                        Write-Debug "$('- ' * $DebugIndent)Adding device - HardwareIDs matched only when using wildcards"
                        $DevicesMatchedWildcard.Add($DeviceInMachine)
                    }
                }

                Write-Debug "$('- ' * $DebugIndent)Matched devices: $($DevicesMatchedExact.Count) exact, $($DevicesMatchedWildcard.Count) wildcard"
                $DevicesToTest = if ($DevicesMatchedExact) {
                    $DevicesMatchedExact
                } else {
                    $DevicesMatchedWildcard
                }

                if ($DevicesToTest.Count -ge 1) {
                    $TestResults = [System.Collections.Generic.List[bool]]::new()
                    foreach ($Device in $DevicesToTest) {
                        Write-Debug "$('- ' * $DebugIndent)Testing $($Device.DeviceId)"
                        # First, check if there is a driver installed for the device at all before proceeding (issue#24)
                        if ($Device.Problem -eq 'CM_PROB_FAILED_INSTALL') {
                            [string]$HexDeviceProblemStatus = '0x{0:X8}' -f (Get-PnpDeviceProperty -InputObject $Device -KeyName 'DEVPKEY_Device_ProblemStatus').Data
                            Write-Debug "$('- ' * $DebugIndent)Device '$($Device.InstanceId)' does not have any driver (ProblemStatus: $HexDeviceProblemStatus)"
                            return -1
                        }

                        if ($FailInboxDrivers) {
                            # This approach of identifying 'inbox' drivers seems to produce the most matching SeverityOverride results.
                            # Some alternatives tested were DEVPKEY_Device_GenericDriverInstalled and Get-AuthenticodeSignature .IsOSBinary property.
                            [bool]$DriverIsInbox = (
                                (Get-PnpDeviceProperty -InputObject $Device -KeyName 'DEVPKEY_Device_DriverProvider').Data -eq 'Microsoft' -and
                                (Get-PnpDeviceProperty -InputObject $Device -KeyName 'DEVPKEY_Device_DriverInfPath').Data -notmatch '^oem\d+\.inf$'
                            )
                            if ($DriverIsInbox) {
                                Write-Debug "$('- ' * $DebugIndent)Failed because device is using an inbox driver"
                                return -1
                            }
                        }

                        $icmParams = @{
                            'InputObject' = $Device
                            'MethodName'  = 'GetDeviceProperties'
                            'Arguments'   = @{'devicePropertyKeys' = @('DEVPKEY_Device_DriverVersion')}
                            'Verbose'     = $false
                            'ErrorAction' = 'SilentlyContinue'
                        }

                        $DriverVersionObject = Invoke-CimMethod @icmParams | Select-Object -ExpandProperty deviceProperties
                        if (-not $DriverVersionObject) {
                            # Fall back to the much slower Get-PnpDeviceProperty cmdlet in cases where GetDeviceProperties fails (e.g. disconnected "phantom" devices)
                            $DriverVersionObject = Get-PnpDeviceProperty -InputObject $Device -KeyName 'DEVPKEY_Device_DriverVersion'
                        }
                        $DriverVersion = $DriverVersionObject.Data

                        $icmParams = @{
                            'InputObject' = $Device
                            'MethodName'  = 'GetDeviceProperties'
                            'Arguments'   = @{'devicePropertyKeys' = @('DEVPKEY_Device_DriverDate')}
                            'Verbose'     = $false
                            'ErrorAction' = 'SilentlyContinue'
                        }

                        $DriverDateObject = Invoke-CimMethod @icmParams | Select-Object -ExpandProperty deviceProperties
                        if (-not $DriverDateObject) {
                            # Fall back to the much slower Get-PnpDeviceProperty cmdlet in cases where GetDeviceProperties fails (e.g. disconnected "phantom" devices)
                            $DriverDateObject = Get-PnpDeviceProperty -InputObject $Device -KeyName 'DEVPKEY_Device_DriverDate'
                        }
                        $DriverDate = $DriverDateObject.Data

                        # Documentation for this: https://docs.microsoft.com/en-us/windows-hardware/drivers/install/identifier-score--windows-vista-and-later-
                        # To be clear, this is a 'pretty good / best effort' approach, but it can detect false positives or miss generic drivers.
                        # AFAIK it is not possible to detect with 100% certainty that a driver is generic/inbox and even if - it's not always a problem.
                        # So this information should only be used for informaing the user or as an aid in making non-critical decisions,
                        # do not rely on this detection/boolean to be accurate!
                        [UInt32]$DriverRank = (Get-PnpDeviceProperty -InputObject $Device -KeyName 'DEVPKEY_Device_DriverRank').Data
                        [byte]$DriverMatchTypeScore = $DriverRank -shr 12 -band 0xF
                        Write-Debug "$('- ' * $DebugIndent)Device '$($Device.Name)' DriverRank is 0x$('{0:X8}' -f $DriverRank)"
                        if ($DriverMatchTypeScore -ge 2) {
                            Write-Verbose "Device '$($Device.Name)' may currently be using a generic or inbox driver"
                        }

                        if ($DriverChildNodes -contains 'Date') {
                            Write-Debug "$('- ' * $DebugIndent)Trying to match driver based on Date"
                            $LenovoDate = [DateTime]::new(0)
                            [bool]$LenovoDateIsValid = [DateTime]::TryParseExact(
                                $Dependency.Date,
                                'yyyy-MM-dd',
                                [CultureInfo]::InvariantCulture,
                                [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
                                [ref]$LenovoDate
                            )
                            if ($LenovoDateIsValid) {
                                if ($DriverDate) {
                                    # WMI and therefore CIM stores datetime values in a DMTF string format.
                                    # When these are converted to DateTime objects, they are always converted to the local timezone, aka an offset
                                    # is "artificially" added. For driver dates, this can lead to GitHub#33 where the offset is enough to change the date,
                                    # which leads to false driver results. We have to remove the offset by converting the DateTime of Kind 'Local' back to UTC.
                                    # See GitHub#33 and https://docs.microsoft.com/en-us/dotnet/api/system.management.managementdatetimeconverter.todatetime?view=netframework-4.8#remarks
                                    $DriverDate = $DriverDate.ToUniversalTime().Date
                                    Write-Debug "$('- ' * $DebugIndent)[Got: $DriverDate, Expected: $LenovoDate]"
                                    if ($DriverDate -ge $LenovoDate) {
                                        Write-Debug "$('- ' * $DebugIndent)Passed DriverDate test"
                                        $TestResults.Add($true)
                                    } else {
                                        Write-Debug "$('- ' * $DebugIndent)Failed DriverDate test"
                                        $TestResults.Add($false)
                                    }
                                } else {
                                    Write-Verbose "Device '$($Device.InstanceId)' does not report its driver date"
                                }
                            } else {
                                Write-Verbose "Got unsupported date format from Lenovo: '$($Dependency.Date)' (expected yyyy-MM-dd)"
                            }
                        }

                        if ($DriverChildNodes -contains 'Version') {
                            Write-Debug "$('- ' * $DebugIndent)Trying to match driver based on Version"
                            # Not all drivers tell us their versions via the OS API. I think later I can try to parse the INIs as an alternative, but it would get tricky
                            if ($DriverVersion) {
                                Write-Debug "$('- ' * $DebugIndent)[Got: $DriverVersion, Expected: $($Dependency.Version)]"
                                if ((Test-VersionPattern -LenovoString $Dependency.Version -SystemString $DriverVersion) -eq 0) {
                                    Write-Debug "$('- ' * $DebugIndent)Passed DriverVersion test"
                                    $TestResults.Add($true)
                                } else {
                                    Write-Debug "$('- ' * $DebugIndent)Failed DriverVersion test"
                                    $TestResults.Add($false)
                                }
                            } else {
                                Write-Verbose "Device '$($Device.InstanceId)' does not report its driver version"
                            }
                        }
                    }

                    # If all HardwareID-tests were successful, return SUCCESS
                    if (-not ($TestResults -contains $false)) {
                        return 0 #SUCCESS
                    }

                    # If one or more HardwareID-tests were completed but failed (e.g. Date) continue in case there are further tests like FileVersion
                }
            }

            if (Compare-Array @('File', 'Version') -in $DriverChildNodes) {
                # This may not be 100% yet as Lenovo sometimes uses some non-system environment variables in their file paths
                [string]$Path = Resolve-CmdVariable -String $Dependency.File -ExtraVariables @{'WINDOWS' = $env:SystemRoot}
                if (Test-Path -LiteralPath $Path -PathType Leaf) {
                    $filProductVersion = (Get-Item -LiteralPath $Path).VersionInfo.ProductVersion
                    $FileVersionCompare = Test-VersionPattern -LenovoString $Dependency.Version -SystemString $filProductVersion
                    if ($FileVersionCompare -eq -2) {
                        Write-Debug "$('- ' * $DebugIndent)Got unsupported with ProductVersion, trying comparison with FileVersion"
                        $filFileVersion = (Get-Item -LiteralPath $Path).VersionInfo.FileVersion
                        return (Test-VersionPattern -LenovoString $Dependency.Version -SystemString $filFileVersion)
                    } else {
                        return $FileVersionCompare
                    }
                } else {
                    Write-Debug "$('- ' * $DebugIndent)The file '$Path' was not found."
                    return -1
                }
            }

            # If we have not hit a success condition before the end, return with failure
            return -1
        }
        '_EmbeddedControllerVersion' {
            if ($CachedHardwareTable['_EmbeddedControllerVersion']) {
                if ($CachedHardwareTable['_EmbeddedControllerVersion'] -eq '255.255') {
                    Write-Warning "This computers EC firmware is not upgradable but is being used to evaluate a package"
                }
                return (Test-VersionPattern -LenovoString $Dependency.Version -SystemString $CachedHardwareTable['_EmbeddedControllerVersion'])
            }
            return -1
        }
        '_ExternalDetection' {
            $externalDetection = Invoke-PackageCommand -Command $Dependency.'#text' -Path $PackagePath -RuntimeLimit $script:LSUClientConfiguration.MaxExternalDetectionRuntime
            if ($externalDetection.Err) {
                Write-Debug "$('- ' * $DebugIndent)[ External process did not run properly: $($externalDetection.Err) ]"
                return -1
            } else {
                Write-Debug "$('- ' * $DebugIndent)[ Got ExitCode: $($externalDetection.Info.ExitCode), Expected: $($Dependency.rc) ]"
                if ($externalDetection.Info.ExitCode -in ($Dependency.rc -split ',')) {
                    return 0
                } else {
                    return -1
                }
            }
        }
        '_FileExists' {
            # This may not be 100% yet as Lenovo sometimes uses some non-system environment variables in their file paths
            [string]$Path = Resolve-CmdVariable -String $Dependency.'#text' -ExtraVariables @{'WINDOWS' = $env:SystemRoot}
            if (Test-Path -LiteralPath $Path -PathType Leaf) {
                return 0
            } else {
                return -1
            }
        }
        '_FileVersion' {
            # This may not be 100% yet as Lenovo sometimes uses some non-system environment variables in their file paths
            [string]$Path = Resolve-CmdVariable -String $Dependency.File -ExtraVariables @{'WINDOWS' = $env:SystemRoot}
            if (Test-Path -LiteralPath $Path -PathType Leaf) {
                $filProductVersion = (Get-Item -LiteralPath $Path).VersionInfo.ProductVersion
                $FileVersionCompare = Test-VersionPattern -LenovoString $Dependency.Version -SystemString $filProductVersion
                if ($FileVersionCompare -eq -2) {
                    Write-Debug "$('- ' * $DebugIndent)Got unsupported with ProductVersion, trying comparison with FileVersion"
                    $filFileVersion = (Get-Item -LiteralPath $Path).VersionInfo.FileVersion
                    return (Test-VersionPattern -LenovoString $Dependency.Version -SystemString $filFileVersion)
                } else {
                    return $FileVersionCompare
                }
            } else {
                Write-Debug "$('- ' * $DebugIndent)The file '$Path' was not found."
                return -1
            }
        }
        '_Firmware' {
            # https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/query-version-and-status-ps1-script?view=windows-11
            # Dependency.Version can also have a hex2dec attribute (True/False) and depending on whether it exists PowerShell deserializes
            # the XML differently (.Version can be string or XmlElement). Using SelectNode is consistent.
            [string]$LenovoVersion    = $Dependency.SelectSingleNode('Version').'#text'
            [bool]$LenovoVersionIsHex = $Dependency.SelectSingleNode('Version').GetAttribute('hex2dec') -eq 'True'
            foreach ($PnpDevice in $CachedHardwareTable['_PnPID']) {
                foreach ($entry in $Dependency.HardwareIDs) {
                    # Only exact HardwareID matches will be found (no wildcards)
                    if ($entry.'#cdata-section' -in $PnpDevice.HardwareID) {
                        [string]$PnpDeviceFirmwareRev = $PnpDevice.HardwareID[0].Substring($PnpDevice.HardwareID[0].IndexOf('&REV_') + 5)
                        Write-Debug "$('- ' * $DebugIndent)[ Got: ${PnpDeviceFirmwareRev}, Expected: ${LenovoVersion} (IsHex: ${LenovoVersionIsHex}) ]"
                        if ($LenovoVersionIsHex) {
                            # If hex2dec is explicitly set to True only interpret the LenovoVersion as hexadecimal.
                            # This is important because any decimal-looking number can also be interpreted in hex but has a very different value there.
                            return (Test-VersionPattern -LenovoString $LenovoVersion -SystemString $PnpDeviceFirmwareRev -SystemStringFormat Hex -LenovoStringFormat Hex)
                        } else {
                            # Some packages, like https://download.lenovo.com/pccbbs/mobiles/n2wrg16w_v2_2_.xml, use hexadecimal versions but set hex2dec=False.
                            # We leave LenovoStringFormat set to the default 'Auto' to try and support that case as best as possible with fallback logic
                            return (Test-VersionPattern -LenovoString $LenovoVersion -SystemString $PnpDeviceFirmwareRev -SystemStringFormat Hex)
                        }
                    }
                }
            }
            return -1 # HardwareID not in system - fail
        }
        '_OS' {
            foreach ($entry in $Dependency.OS) {
                if ("$entry" -like "WIN$($CachedHardwareTable['_OS'])*") {
                    return 0
                }
            }
            return -1
        }
        '_OSLang' {
            if ($Dependency.Lang -eq [CultureInfo]::CurrentUICulture.ThreeLetterWindowsLanguageName) {
                return 0
            } else {
                return -1
            }
        }
        '_PnPID' {
            foreach ($HardwareID in $CachedHardwareTable['_PnPID'].HardwareID) {
                if ($HardwareID -like "*$($Dependency.'#cdata-section')*") {
                    return 0
                }
            }
            return -1
        }
        '_RegistryKey' {
            if ($Dependency.Key) {
                if (Test-Path -LiteralPath ('Microsoft.PowerShell.Core\Registry::{0}' -f $Dependency.Key) -PathType Container) {
                    return 0
                }
            }
            return -1
        }
        '_RegistryKeyValue' {
            if ($Dependency.type -ne 'REG_SZ') {
                return -2
            }

            if (Test-Path -LiteralPath ('Microsoft.PowerShell.Core\Registry::{0}' -f $Dependency.Key) -PathType Container) {
                try {
                    $regVersion = Get-ItemPropertyValue -LiteralPath ('Microsoft.PowerShell.Core\Registry::{0}' -f $Dependency.Key) -Name $Dependency.KeyName -ErrorAction Stop
                }
                catch {
                    return -1
                }

                [string]$DependencyVersion = if ($Dependency.KeyValue) {
                    $Dependency.KeyValue
                } elseif ($Dependency.Version) {
                    $Dependency.Version
                } else {
                    Write-Verbose "Could not get LenovoString from _RegistryKeyValue dependency node"
                    return -2
                }

                return (Test-VersionPattern -LenovoString $DependencyVersion -SystemString $regVersion)
            } else {
                return -1
            }
        }
        '_WindowsBuildVersion' {
            # A _WindowsBuildVersion test can specify multiple Build Versions, see issue #42
            [array]$TestResults = foreach ($DependencyVersion in $Dependency.Version) {
                Write-Debug "$('- ' * $DebugIndent)[ Got: $($CachedHardwareTable['_WindowsBuildVersion']), Expected: $DependencyVersion ]"
                Test-VersionPattern -LenovoString $DependencyVersion -SystemString $CachedHardwareTable['_WindowsBuildVersion']
            }

            # If we had a clear success match, return success overall.
            # If we had no clear successes, but an unsupported case, return
            # -2 for unsupported so the calling function can evaluate that.
            # Otherwise return -1 to indicate failure (no matches).
            if ($TestResults -contains 0) {
                return 0
            } elseif ($TestResults -contains -2) {
                return -2
            } else {
                return -1
            }
        }
        default {
            Write-Verbose "Unsupported dependency encountered: $_"
            return -2
        }
    }

    return -2
}