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

            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)
                        }
                        else {
                            $rulePassed = $true
                        }
                    }
                }
                '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
                        }
                        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
                        }
                    }
                }
                '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
                            }
                        }
                    }
                }
                default {
                    $rulePassed = $false
                }
            }

            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
}