Support/ExecutionCore/Eigenverft.Manifested.Package.ExecutionCore.Registry.ps1

<#
    Eigenverft.Manifested.Package.ExecutionEngine.Registry
#>


function Test-RegistryPathExists {
<#
.SYNOPSIS
Returns whether a registry path exists.
 
.DESCRIPTION
Uses the PowerShell registry provider to test for the existence of a registry
path. Failures are treated as non-existent so callers can decide how to handle
missing or unreadable paths.
 
.EXAMPLE
Test-RegistryPathExists -Path 'HKLM:\SOFTWARE\Vendor\Product'
#>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    try {
        return (Test-Path -LiteralPath $Path)
    }
    catch {
        return $false
    }
}

function Get-RegistryValueData {
<#
.SYNOPSIS
Reads a value from a registry path.
 
.DESCRIPTION
Returns the read result in a normalized object so callers can distinguish
between missing paths, read failures, and successful value reads without
embedding registry-provider mechanics in higher-level workflows.
 
.EXAMPLE
Get-RegistryValueData -Path 'HKLM:\SOFTWARE\Vendor\Product' -ValueName 'Version'
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowNull()]
        [string]$ValueName
    )

    if (-not (Test-RegistryPathExists -Path $Path)) {
        return [pscustomobject]@{
            Path          = $Path
            ValueName     = $ValueName
            Value         = $null
            Exists        = $false
            ReadSucceeded = $false
            Status        = 'Missing'
        }
    }

    if ([string]::IsNullOrWhiteSpace($ValueName)) {
        return [pscustomobject]@{
            Path          = $Path
            ValueName     = $null
            Value         = $null
            Exists        = $true
            ReadSucceeded = $true
            Status        = 'Ready'
        }
    }

    try {
        $properties = Get-ItemProperty -LiteralPath $Path -Name $ValueName -ErrorAction Stop
        return [pscustomobject]@{
            Path          = $Path
            ValueName     = $ValueName
            Value         = $properties.$ValueName
            Exists        = $true
            ReadSucceeded = $true
            Status        = 'Ready'
        }
    }
    catch {
        return [pscustomobject]@{
            Path          = $Path
            ValueName     = $ValueName
            Value         = $null
            Exists        = $true
            ReadSucceeded = $false
            Status        = 'Failed'
        }
    }
}

function Resolve-RegistryValueFromPaths {
<#
.SYNOPSIS
Finds the first usable registry path from a candidate list.
 
.DESCRIPTION
Evaluates registry candidate paths in order and returns the first successful
match. If no candidate succeeds, the last failed candidate is preserved so
callers can surface a meaningful path in diagnostics.
 
.EXAMPLE
Resolve-RegistryValueFromPaths -Paths @('HKLM:\A', 'HKLM:\B') -ValueName 'Version'
#>

    [CmdletBinding()]
    param(
        [AllowEmptyCollection()]
        [string[]]$Paths,

        [AllowNull()]
        [string]$ValueName
    )

    $candidatePaths = @($Paths | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
    $result = [pscustomobject]@{
        Path         = if ($candidatePaths.Count -gt 0) { $candidatePaths[0] } else { $null }
        Paths        = @($candidatePaths)
        ValueName    = $ValueName
        ActualValue  = $null
        Status       = 'Missing'
    }

    foreach ($candidatePath in $candidatePaths) {
        $candidateResult = Get-RegistryValueData -Path $candidatePath -ValueName $ValueName
        if ($candidateResult.Status -eq 'Missing') {
            continue
        }

        $result.Path = $candidateResult.Path
        $result.ActualValue = $candidateResult.Value
        $result.Status = $candidateResult.Status

        if ($candidateResult.Status -eq 'Ready') {
            break
        }
    }

    return $result
}

function Get-WindowsUninstallRegistryEntry {
<#
.SYNOPSIS
Reads one Windows uninstall registry entry.
 
.DESCRIPTION
Returns a normalized uninstall-entry object from a concrete registry path. This
helper intentionally reads a direct key only; scanning uninstall roots belongs
in a separate helper when a package needs that behavior.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $result = [ordered]@{
        Path                 = $Path
        KeyName              = $null
        Exists               = $false
        ReadSucceeded        = $false
        Status               = 'Missing'
        DisplayName          = $null
        DisplayVersion       = $null
        Publisher            = $null
        InstallLocation      = $null
        DisplayIcon          = $null
        UninstallString      = $null
        QuietUninstallString = $null
    }

    if (-not (Test-RegistryPathExists -Path $Path)) {
        return [pscustomobject]$result
    }

    $result.Exists = $true
    try {
        $result.KeyName = Split-Path -Leaf $Path
    }
    catch {
        $result.KeyName = $null
    }

    try {
        $properties = Get-ItemProperty -LiteralPath $Path -ErrorAction Stop
        $result.ReadSucceeded = $true
        $result.Status = 'Ready'
        foreach ($propertyName in @('DisplayName', 'DisplayVersion', 'Publisher', 'InstallLocation', 'DisplayIcon', 'UninstallString', 'QuietUninstallString')) {
            if ($properties.PSObject.Properties[$propertyName]) {
                $result[$propertyName] = $properties.$propertyName
            }
        }
    }
    catch {
        $result.Status = 'Failed'
    }

    return [pscustomobject]$result
}

function Get-WindowsUninstallRegistryEntries {
<#
.SYNOPSIS
Enumerates Windows uninstall registry entries below one or more root keys.
 
.DESCRIPTION
Reads direct child uninstall entries from roots such as
HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall. Missing roots and
unreadable children are skipped so callers can layer package-specific matching
rules without duplicating registry-provider mechanics.
#>

    [CmdletBinding()]
    param(
        [AllowEmptyCollection()]
        [string[]]$RootPaths
    )

    $entries = New-Object System.Collections.Generic.List[object]
    foreach ($rootPath in @($RootPaths | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })) {
        if (-not (Test-RegistryPathExists -Path $rootPath)) {
            continue
        }

        try {
            foreach ($child in @(Get-ChildItem -LiteralPath $rootPath -ErrorAction Stop)) {
                $childPath = if ($child.PSObject.Properties['PSPath'] -and -not [string]::IsNullOrWhiteSpace([string]$child.PSPath)) {
                    [string]$child.PSPath
                }
                else {
                    [string]$child.Name
                }
                $entry = Get-WindowsUninstallRegistryEntry -Path $childPath
                if ($entry -and [string]::Equals([string]$entry.Status, 'Ready', [System.StringComparison]::OrdinalIgnoreCase)) {
                    $entries.Add($entry) | Out-Null
                }
            }
        }
        catch {
            continue
        }
    }

    return @($entries.ToArray())
}

function Get-WindowsInstallerProductCodeFromText {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Text
    )

    if ([string]::IsNullOrWhiteSpace($Text)) {
        return $null
    }

    $match = [regex]::Match([string]$Text, '\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}')
    if (-not $match.Success) {
        return $null
    }

    return $match.Value.ToUpperInvariant()
}

function Get-WindowsInstallerProductCodeFromUninstallEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry
    )

    foreach ($propertyName in @('KeyName', 'Path', 'QuietUninstallString', 'UninstallString')) {
        if (-not $Entry.PSObject.Properties[$propertyName]) {
            continue
        }
        $productCode = Get-WindowsInstallerProductCodeFromText -Text ([string]$Entry.$propertyName)
        if (-not [string]::IsNullOrWhiteSpace($productCode)) {
            return $productCode
        }
    }

    return $null
}

function Get-WindowsRegistryExecutablePathFromText {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Text
    )

    if ([string]::IsNullOrWhiteSpace($Text)) {
        return $null
    }

    $candidateText = [Environment]::ExpandEnvironmentVariables(([string]$Text).Trim())
    if ($candidateText.StartsWith('"')) {
        $quotedMatch = [regex]::Match($candidateText, '^"([^"]+)"')
        if ($quotedMatch.Success) {
            return $quotedMatch.Groups[1].Value
        }
    }

    $extensionMatch = [regex]::Match($candidateText, '^(.*?\.(?:exe|msi|cmd|bat))(?=\s|,|$)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
    if ($extensionMatch.Success) {
        return ($extensionMatch.Groups[1].Value -replace ',\s*\d+$', '').Trim()
    }

    $firstToken = ($candidateText -split '\s+', 2)[0]
    return ($firstToken -replace ',\s*\d+$', '').Trim()
}

function Resolve-WindowsUninstallRegistryEntryPath {
<#
.SYNOPSIS
Resolves a filesystem path from a Windows uninstall registry entry.
 
.DESCRIPTION
Extracts common install-related paths from a normalized uninstall entry. The
helper accepts explicit source names so package logic can stay declarative and
future installer packages can add path-source strategies without changing
existing discovery flow.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry,

        [Parameter(Mandatory = $true)]
        [ValidateSet('installLocation', 'displayIcon', 'displayIconDirectory', 'uninstallString', 'uninstallStringDirectory')]
        [string]$Source
    )

    $rawPath = $null
    switch -Exact ($Source) {
        'installLocation' {
            $rawPath = if ($Entry.PSObject.Properties['InstallLocation']) { [string]$Entry.InstallLocation } else { $null }
        }
        'displayIcon' {
            $rawPath = Get-WindowsRegistryExecutablePathFromText -Text $(if ($Entry.PSObject.Properties['DisplayIcon']) { [string]$Entry.DisplayIcon } else { $null })
        }
        'displayIconDirectory' {
            $iconPath = Get-WindowsRegistryExecutablePathFromText -Text $(if ($Entry.PSObject.Properties['DisplayIcon']) { [string]$Entry.DisplayIcon } else { $null })
            $rawPath = if ([string]::IsNullOrWhiteSpace($iconPath)) { $null } else { Split-Path -Parent $iconPath }
        }
        'uninstallString' {
            $rawPath = Get-WindowsRegistryExecutablePathFromText -Text $(if ($Entry.PSObject.Properties['UninstallString']) { [string]$Entry.UninstallString } else { $null })
        }
        'uninstallStringDirectory' {
            $uninstallPath = Get-WindowsRegistryExecutablePathFromText -Text $(if ($Entry.PSObject.Properties['UninstallString']) { [string]$Entry.UninstallString } else { $null })
            $rawPath = if ([string]::IsNullOrWhiteSpace($uninstallPath)) { $null } else { Split-Path -Parent $uninstallPath }
        }
    }

    $result = [ordered]@{
        EntryPath    = if ($Entry.PSObject.Properties['Path']) { $Entry.Path } else { $null }
        Source       = $Source
        RawPath      = $rawPath
        ResolvedPath = $null
        Status       = 'Missing'
    }

    if ([string]::IsNullOrWhiteSpace($rawPath)) {
        return [pscustomobject]$result
    }

    try {
        $expandedPath = [Environment]::ExpandEnvironmentVariables([string]$rawPath)
        $result.ResolvedPath = [System.IO.Path]::GetFullPath($expandedPath)
        $result.Status = 'Ready'
    }
    catch {
        $result.ResolvedPath = $rawPath
        $result.Status = 'Failed'
    }

    return [pscustomobject]$result
}