Private/Test-LocalPackageDetection.ps1

#Requires -Version 5.1
<#!
.SYNOPSIS
    Evaluates App.json DetectionRule entries on the local machine.
 
.DESCRIPTION
    Supports File, Registry, and MSI rule types and returns whether all rules
    pass, with the best available detected version.
 
.PARAMETER DefinitionObject
    Parsed App.json definition object.
 
.OUTPUTS
    PSCustomObject with:
        Succeeded : bool
        Installed : bool
        DetectedVersion : string
        Status : string
        RuleCount : int
        RulePassCount : int
        Error : string
!#>

function Test-LocalPackageDetection {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject]$DefinitionObject
    )

    $result = [PSCustomObject]@{
        Succeeded       = $false
        Installed       = $false
        DetectedVersion = ''
        Status          = ''
        RuleCount       = 0
        RulePassCount   = 0
        Error           = ''
    }

    if ($null -eq $DefinitionObject) {
        $result.Error = 'Definition object is null.'
        $result.Status = 'Detection failed'
        return $result
    }

    $rules = @($DefinitionObject.DetectionRule)
    if ($rules.Count -eq 0) {
        $result.Succeeded = $true
        $result.Status = 'No detection rules'
        return $result
    }

    $compareValues = {
        param(
            [string]$Operator,
            [string]$Left,
            [string]$Right
        )

        $op = if ([string]::IsNullOrWhiteSpace($Operator)) { 'equal' } else { $Operator.Trim() }

        $leftVersion = $null
        $rightVersion = $null
        $leftIsVersion = [version]::TryParse(($Left -replace '[^0-9\.]', '').Trim('.'), [ref]$leftVersion)
        $rightIsVersion = [version]::TryParse(($Right -replace '[^0-9\.]', '').Trim('.'), [ref]$rightVersion)

        if ($leftIsVersion -and $rightIsVersion) {
            switch -Regex ($op.ToLowerInvariant()) {
                '^equal$|^eq$' { return $leftVersion -eq $rightVersion }
                '^notequal$|^ne$' { return $leftVersion -ne $rightVersion }
                '^greaterthan$|^gt$' { return $leftVersion -gt $rightVersion }
                '^greaterthanorequal$|^greaterthanorequals$|^ge$' { return $leftVersion -ge $rightVersion }
                '^lessthan$|^lt$' { return $leftVersion -lt $rightVersion }
                '^lessthanorequal$|^lessthanorequals$|^le$' { return $leftVersion -le $rightVersion }
                default { return $leftVersion -eq $rightVersion }
            }
        }

        switch -Regex ($op.ToLowerInvariant()) {
            '^equal$|^eq$' { return [string]::Equals($Left, $Right, [System.StringComparison]::OrdinalIgnoreCase) }
            '^notequal$|^ne$' { return -not [string]::Equals($Left, $Right, [System.StringComparison]::OrdinalIgnoreCase) }
            '^contains$' { return ([string]$Left).IndexOf([string]$Right, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 }
            default { return [string]::Equals($Left, $Right, [System.StringComparison]::OrdinalIgnoreCase) }
        }
    }

    $resolveRegistryPath = {
        param(
            [string]$RawPath,
            [bool]$Check32BitOn64System
        )

        if ([string]::IsNullOrWhiteSpace($RawPath)) {
            return ''
        }

        $normalized = $RawPath.Trim()
        if ($normalized -match '^HKEY_LOCAL_MACHINE' -or $normalized -match '^HKLM') {
            $normalized = $normalized -replace '^HKEY_LOCAL_MACHINE', 'HKLM:' -replace '^HKLM', 'HKLM:'
        }
        elseif ($normalized -match '^HKEY_CURRENT_USER' -or $normalized -match '^HKCU') {
            $normalized = $normalized -replace '^HKEY_CURRENT_USER', 'HKCU:' -replace '^HKCU', 'HKCU:'
        }

        if ($Check32BitOn64System -and $normalized -like 'HKLM:*' -and $normalized -notmatch 'WOW6432Node') {
            $normalized = $normalized -replace '^HKLM:\Software', 'HKLM:\Software\WOW6432Node'
        }

        return $normalized
    }

    $detectedVersion = ''
    $ruleCount = 0
    $rulePassCount = 0

    try {
        foreach ($rule in $rules) {
            if ($null -eq $rule) { continue }
            $ruleCount++

            $ruleType = [string]$rule.Type
            $rulePassed = $false
            Write-Verbose -Message "EvergreenUI: Detection rule $ruleCount - type=$ruleType"

            switch ($ruleType) {
                'File' {
                    $basePath = [string]$rule.Path
                    $fileOrFolder = [string]$rule.FileOrFolder
                    $targetPath = if ([string]::IsNullOrWhiteSpace($fileOrFolder)) {
                        $basePath
                    }
                    else {
                        Join-Path -Path $basePath -ChildPath $fileOrFolder
                    }

                    if (-not [string]::IsNullOrWhiteSpace($targetPath) -and (Test-Path -LiteralPath $targetPath)) {
                        $method = [string]$rule.DetectionMethod
                        if ($method -eq 'Version') {
                            $fileInfo = Get-Item -LiteralPath $targetPath -ErrorAction SilentlyContinue
                            $currentVersion = if ($null -ne $fileInfo -and $null -ne $fileInfo.VersionInfo) {
                                [string]$fileInfo.VersionInfo.FileVersion
                            }
                            else {
                                ''
                            }

                            if (-not [string]::IsNullOrWhiteSpace($currentVersion)) {
                                $detectedVersion = $currentVersion
                            }

                            $rulePassed = & $compareValues -Operator ([string]$rule.Operator) -Left $currentVersion -Right ([string]$rule.VersionValue)
                            Write-Verbose -Message "EvergreenUI: File rule - path='$targetPath' detectedVersion='$currentVersion' expectedVersion='$([string]$rule.VersionValue)' passed=$rulePassed"
                        }
                        else {
                            $rulePassed = $true
                            Write-Verbose -Message "EvergreenUI: File rule (existence) - path='$targetPath' passed=$rulePassed"
                        }
                    }
                    else {
                        Write-Verbose -Message "EvergreenUI: File rule - path='$targetPath' not found; rule failed"
                    }
                }
                'Registry' {
                    $registryPath = & $resolveRegistryPath -RawPath ([string]$rule.KeyPath) -Check32BitOn64System ([string]$rule.Check32BitOn64System -ieq 'true')
                    if (-not [string]::IsNullOrWhiteSpace($registryPath) -and (Test-Path -LiteralPath $registryPath)) {
                        $method = [string]$rule.DetectionMethod
                        if ($method -eq 'Existence') {
                            $rulePassed = $true
                            Write-Verbose -Message "EvergreenUI: Registry rule (existence) - key='$registryPath' passed=$rulePassed"
                        }
                        else {
                            $valueName = [string]$rule.ValueName
                            $item = Get-ItemProperty -LiteralPath $registryPath -ErrorAction SilentlyContinue
                            $currentValue = if ($null -ne $item -and -not [string]::IsNullOrWhiteSpace($valueName)) {
                                [string]$item.$valueName
                            }
                            else {
                                ''
                            }

                            if ($method -eq 'Version' -and -not [string]::IsNullOrWhiteSpace($currentValue)) {
                                $detectedVersion = $currentValue
                            }

                            $expectedValue = if ($method -eq 'Version') { [string]$rule.DetectionValue } else { [string]$rule.DetectionValue }
                            $rulePassed = & $compareValues -Operator ([string]$rule.Operator) -Left $currentValue -Right $expectedValue
                            Write-Verbose -Message "EvergreenUI: Registry rule ($method) - key='$registryPath' value='$valueName' detected='$currentValue' expected='$expectedValue' passed=$rulePassed"
                        }
                    }
                    else {
                        Write-Verbose -Message "EvergreenUI: Registry rule - key='$registryPath' not found; rule failed"
                    }
                }
                'MSI' {
                    $productCode = [string]$rule.ProductCode
                    if (-not [string]::IsNullOrWhiteSpace($productCode)) {
                        $normalizedCode = $productCode.Trim('{}').ToUpperInvariant()
                        $uninstallKeys = @(
                            'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall',
                            'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
                        )

                        $matchedVersion = ''
                        foreach ($keyRoot in $uninstallKeys) {
                            if (-not (Test-Path -LiteralPath $keyRoot)) { continue }

                            $candidates = @(Get-ChildItem -LiteralPath $keyRoot -ErrorAction SilentlyContinue)
                            foreach ($candidate in $candidates) {
                                $leaf = [string]$candidate.PSChildName
                                if ([string]::IsNullOrWhiteSpace($leaf)) { continue }
                                if ($leaf.Trim('{}').ToUpperInvariant() -ne $normalizedCode) { continue }

                                $entry = Get-ItemProperty -LiteralPath $candidate.PSPath -ErrorAction SilentlyContinue
                                if ($null -ne $entry -and -not [string]::IsNullOrWhiteSpace([string]$entry.DisplayVersion)) {
                                    $matchedVersion = [string]$entry.DisplayVersion
                                }
                                else {
                                    $matchedVersion = 'Installed'
                                }
                                break
                            }

                            if (-not [string]::IsNullOrWhiteSpace($matchedVersion)) {
                                break
                            }
                        }

                        if (-not [string]::IsNullOrWhiteSpace($matchedVersion)) {
                            if ($matchedVersion -ne 'Installed') {
                                $detectedVersion = $matchedVersion
                            }

                            $expectedVersion = [string]$rule.ProductVersion
                            if ([string]::IsNullOrWhiteSpace($expectedVersion)) {
                                $rulePassed = $true
                            }
                            else {
                                $rulePassed = & $compareValues -Operator ([string]$rule.ProductVersionOperator) -Left $matchedVersion -Right $expectedVersion
                            }
                            Write-Verbose -Message "EvergreenUI: MSI rule - productCode='{$normalizedCode}' detectedVersion='$matchedVersion' expectedVersion='$expectedVersion' passed=$rulePassed"
                        }
                        else {
                            Write-Verbose -Message "EvergreenUI: MSI rule - productCode='{$normalizedCode}' not found in uninstall keys; rule failed"
                        }
                    }
                }
                default {
                    $rulePassed = $false
                    Write-Verbose -Message "EvergreenUI: Unknown rule type '$ruleType'; rule failed"
                }
            }

            if ($rulePassed) {
                $rulePassCount++
            }
        }

        $result.RuleCount = $ruleCount
        $result.RulePassCount = $rulePassCount
        $result.Succeeded = $true
        $result.Installed = ($ruleCount -gt 0 -and $rulePassCount -eq $ruleCount)
        $result.DetectedVersion = $detectedVersion
        if ($result.Installed) {
            $result.Status = if ([string]::IsNullOrWhiteSpace($detectedVersion)) { 'Installed' } else { "Installed ($detectedVersion)" }
        }
        else {
            $result.Status = 'Not installed'
        }
    }
    catch {
        $result.Succeeded = $false
        $result.Error = $_.Exception.Message
        $result.Status = 'Detection failed'
    }

    return $result
}