Private/Installation/Registry.ps1

function Get-VSORegistryRoot {
    param([string]$Scope, $Context)
    $base=if ($Scope -eq 'AllUsers') { $env:ProgramData } else { $env:LOCALAPPDATA }
    if (-not $base) { Throw-VSOError $Context Metadata @('scope storage') }
    return (Join-Path $base 'VicoSafeOptimizer.Module\Installations')
}

function ConvertTo-VSOFileMap {
    param($Records, [string]$Root, $Context, $Contract)
    $map=[ordered]@{}
    $allowed=@((Get-VSO7RequiredFiles)+@('LICENSE','LICENSE.txt','SHA256SUMS.txt','vso-distribution.json') | ForEach-Object { 'VSO7/'+$_ })
    if ($Contract) {
        $inventory=Get-VSOContractFiles $Contract $Context
        $allowed+=@($inventory.Keys | ForEach-Object { 'VSO7/'+$_ })
    }
    foreach ($entry in @($Records)) {
        $relative=[string](Get-VSOProperty $entry Path ''); $hash=[string](Get-VSOProperty $entry Sha256 '')
        if ($relative -cnotin $allowed -or $hash -notmatch '^[a-f0-9]{64}$' -or $map.Contains($relative)) { Throw-VSOError $Context Metadata @('file ownership') }
        $null=Join-VSOChild $Root $relative $Context; $map[$relative]=$hash
    }
    foreach ($required in @((Get-VSO7RequiredFiles)+@('SHA256SUMS.txt') | ForEach-Object { 'VSO7/'+$_ })) { if (-not $map.Contains($required)) { Throw-VSOError $Context Metadata @($required) } }
    if ($Contract) {
        if (-not $map.Contains('VSO7/vso-distribution.json')) { Throw-VSOError $Context Metadata @('distribution contract ownership') }
        foreach ($relative in $inventory.Keys) { if ($inventory[$relative].Required -and -not $map.Contains('VSO7/'+$relative)) { Throw-VSOError $Context Metadata @($relative) } }
    }
    if (-not $map.Contains('VSO7/LICENSE') -and -not $map.Contains('VSO7/LICENSE.txt')) { Throw-VSOError $Context Metadata @('license ownership') }
    if ($map['VSO7/VSO7.ps1'] -cne $map['VSO7/VSO7_7.0.0_RC_R80.ps1']) { Throw-VSOError $Context Metadata @('protected core identity') }
    return $map
}

function Read-VSOReceipt {
    param([string]$Root, $Context)
    $Root=Resolve-VSOPath $Root $Context -Installation
    $marker=Join-VSOChild $Root '.vso-manager.json' $Context
    $receipt=Read-VSOJson $marker $Context
    $id=[string](Get-VSOProperty $receipt Id '')
    if ((Get-VSOProperty $receipt Schema 0) -ne 1 -or (Get-VSOProperty $receipt Product '') -cne 'VicoSafeOptimizer' -or $id -notmatch '^[a-f0-9]{32}$' -or (Get-VSOProperty $receipt Scope '') -cnotin @('CurrentUser','AllUsers') -or (Get-VSOProperty $receipt Path '') -ine $Root -or (Get-VSOProperty $receipt Layout '') -cne 'vso7-classic-v1' -or (Get-VSOProperty $receipt ApplicationDirectory '') -cne 'VSO7' -or (Get-VSOProperty $receipt Launcher '') -cne 'VSO7/START VSO7.bat' -or (Get-VSOProperty $receipt Generation '') -ne '7' -or (Get-VSOProperty $receipt State '') -cne 'Installed') { Throw-VSOError $Context Metadata @('receipt identity') }
    $null=ConvertTo-VSOVersion ([string]$receipt.Version) $Context
    if ([string](Get-VSOProperty $receipt ArchiveSha256 '') -notmatch '^[a-f0-9]{64}$') { Throw-VSOError $Context Metadata @('archive identity') }
    if ([string](Get-VSOProperty $receipt MemorySourceSha256 '') -notmatch '^[a-f0-9]{64}$') { Throw-VSOError $Context Metadata @('memory source identity') }
    Assert-VSOUri ([uri]$receipt.AssetUri) $Context
    $null=ConvertTo-VSOFileMap $receipt.Files $Root $Context (Get-VSOProperty $receipt DistributionContract $null)
    $contract=Get-VSOProperty $receipt DistributionContract $null
    $contractPath=Join-VSOChild $Root 'VSO7/vso-distribution.json' $Context
    if ($contract -and [IO.File]::Exists($contractPath)) {
        $map=ConvertTo-VSOFileMap $receipt.Files $Root $Context $contract
        $actual=Read-VSOJson $contractPath $Context
        if ((Get-VSOHash $contractPath) -cne $map['VSO7/vso-distribution.json'] -or ($actual|ConvertTo-Json -Depth 20 -Compress) -cne ($contract|ConvertTo-Json -Depth 20 -Compress)) { Throw-VSOError $Context Metadata @('distribution contract changed') }
    }
    $backupNames=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    foreach ($backup in @((Get-VSOProperty $receipt Backups @()))) {
        if ([string]$backup -notmatch '^\.vso-txn-[a-f0-9]{32}$' -or -not $backupNames.Add([string]$backup)) { Throw-VSOError $Context Metadata @('backup identity') }
    }
    foreach ($transaction in @(Get-VSOProperty $receipt Transactions @())) {
        if ([string]$transaction -notmatch '^\.vso-txn-[a-f0-9]{32}$') { Throw-VSOError $Context Metadata @('transaction identity') }
    }
    $pinnedNames=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    foreach ($pin in @(Get-VSOProperty $receipt TransactionReceipts @())) {
        if ([string]$pin.Transaction -cnotin @(Get-VSOProperty $receipt Transactions @()) -or [string]$pin.JournalSha256 -notmatch '^[a-f0-9]{64}$' -or -not $pinnedNames.Add([string]$pin.Transaction) -or @(Compare-Object @('Transaction','JournalSha256') @($pin.PSObject.Properties.Name)).Count) { Throw-VSOError $Context Metadata @('transaction journal pin') }
    }
    $pinnedNames.Clear()
    foreach ($pin in @(Get-VSOProperty $receipt BackupReceipts @())) {
        if ([string]$pin.Transaction -cnotin @($receipt.Backups) -or [string]$pin.ReceiptSha256 -notmatch '^[a-f0-9]{64}$' -or -not $pinnedNames.Add([string]$pin.Transaction) -or @(Compare-Object @('Transaction','ReceiptSha256') @($pin.PSObject.Properties.Name)).Count) { Throw-VSOError $Context Metadata @('backup receipt pin') }
    }
    Assert-VSOProtectedDirectory $Root $Context -UserAllowed:($receipt.Scope -eq 'CurrentUser')
    return $receipt
}

function Register-VSOReceipt {
    param($Receipt, $Context)
    $root=Get-VSORegistryRoot $Receipt.Scope $Context
    $parent=[IO.Path]::GetDirectoryName($root)
    New-VSOSecureDirectory $parent $Receipt.Scope $Context
    New-VSOSecureDirectory $root $Receipt.Scope $Context
    $marker=Join-VSOChild $Receipt.Path '.vso-manager.json' $Context
    $entry=[ordered]@{ Schema=1; Product='VicoSafeOptimizer'; Id=$Receipt.Id; Path=$Receipt.Path; Scope=$Receipt.Scope; ReceiptSha256=(Get-VSOHash $marker) }
    Write-VSOJson (Join-VSOChild $root ($Receipt.Id+'.json') $Context) $entry $Context
}

function Test-VSOInstalledFiles {
    param($Receipt, $Context)
    $map=ConvertTo-VSOFileMap $Receipt.Files $Receipt.Path $Context (Get-VSOProperty $Receipt DistributionContract $null)
    $index=0
    foreach ($relative in $map.Keys) {
        $file=Join-VSOChild $Receipt.Path $relative $Context
        if (-not [IO.File]::Exists($file) -or (Get-VSOHash $file) -cne $map[$relative]) { Throw-VSOError $Context Corrupt @($relative) }
        $index++; Write-VSOProgress $Context Verify $index $map.Count
    }
}

function Get-VSOIndexEntries {
    param([string]$Scope,$Context)
    $root=Get-VSORegistryRoot $Scope $Context
    if (-not [IO.Directory]::Exists($root)) { return }
    Assert-VSONoReparse $root $Context
    Assert-VSOProtectedDirectory $root $Context -UserAllowed:($Scope -eq 'CurrentUser')
    foreach ($file in @(Get-ChildItem -LiteralPath $root -Filter '*.json' -File -Force -ErrorAction Stop)) {
        if ($file.BaseName -notmatch '^[a-f0-9]{32}$') { Throw-VSOError $Context Metadata @('index filename') }
        $entry=Read-VSOJson $file.FullName $Context
        if ((Get-VSOProperty $entry Schema 0) -ne 1 -or (Get-VSOProperty $entry Product '') -cne 'VicoSafeOptimizer' -or (Get-VSOProperty $entry Id '') -cne $file.BaseName -or (Get-VSOProperty $entry Scope '') -cne $Scope -or [string](Get-VSOProperty $entry ReceiptSha256 '') -notmatch '^[a-f0-9]{64}$') { Throw-VSOError $Context Metadata @('index identity') }
        $candidate=Resolve-VSOPath ([string]$entry.Path) $Context -Installation
        [pscustomobject]@{Path=$candidate;Index=$entry;Scope=$Scope}
    }
}

function Get-VSOInstallationsInternal {
    param($Context, [string]$Path, [string]$Scope, [string]$Version)
    $candidates=[Collections.Generic.List[object]]::new(); $seen=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
    $scopes=if ($Scope) { @($Scope) } else { @('CurrentUser','AllUsers') }
    if ($Path) {
        $canonical=Resolve-VSOPath $Path $Context -Installation
        $registeredMatches=@(foreach ($scopeName in $scopes) { Get-VSOIndexEntries $scopeName $Context | Where-Object Path -EQ $canonical })
        if ($registeredMatches.Count -gt 1) { Throw-VSOError $Context Metadata @('duplicate path registrations') }
        if ($registeredMatches.Count -eq 1) { $candidates.Add($registeredMatches[0]) }
        else { $candidates.Add([pscustomobject]@{Path=$canonical;Index=$null;Scope=$null}) }
    }
    else {
        foreach ($scopeName in $scopes) {
            foreach ($entry in @(Get-VSOIndexEntries $scopeName $Context)) { $candidates.Add($entry) }
            $default=Get-VSODefaultPath $scopeName $Context
            if ([IO.Directory]::Exists($default)) { $candidates.Add([pscustomobject]@{ Path=$default; Index=$null; Scope=$scopeName }) }
        }
    }
    foreach ($candidate in $candidates) {
        if (-not $seen.Add($candidate.Path)) { continue }
        $record=[ordered]@{ PSTypeName='VSO.Installation'; Id=$(if ($candidate.Index) { $candidate.Index.Id } else { $null }); Version='Unknown'; Generation='7'; Path=$candidate.Path; ApplicationPath=(Join-Path $candidate.Path 'VSO7'); Scope=$candidate.Scope; Managed=($null -ne $candidate.Index); State='Missing'; StateDisplay=$Context.Text.Missing; Launcher=$null; Receipt=$null; Diagnostic=$null }
        if ([IO.Directory]::Exists($candidate.Path)) {
            try {
                $marker=Join-VSOChild $candidate.Path '.vso-manager.json' $Context
                if ([IO.File]::Exists($marker)) {
                    $receipt=Read-VSOReceipt $candidate.Path $Context
                    if ($candidate.Index -and ($candidate.Index.Id -cne $receipt.Id -or $candidate.Index.Scope -cne $receipt.Scope -or $candidate.Index.ReceiptSha256 -cne (Get-VSOHash $marker))) { Throw-VSOError $Context Metadata @('index does not match receipt') }
                    $record.Id=$receipt.Id; $record.Version=$receipt.Version; $record.Generation=$receipt.Generation; $record.Scope=$receipt.Scope; $record.Managed=$true; $record.Receipt=$receipt; $record.Launcher=$receipt.Launcher
                    Test-VSOInstalledFiles $receipt $Context
                    $record.State='Ready'; $record.StateDisplay=$Context.Text.Ready
                } else {
                    $app=if ([IO.File]::Exists((Join-Path $record.ApplicationPath 'START VSO7.bat'))) { $record.ApplicationPath } else { $candidate.Path }
                    $distribution=Get-VSODistribution $app $Context
                    $record.ApplicationPath=$app
                    $record.Version=$distribution.Version; $record.Launcher=$distribution.Launcher; $record.State='Unregistered'; $record.StateDisplay=$Context.Text.ManualState
                }
            } catch { $record.State='Modified'; $record.StateDisplay=$Context.Text.Modified; $record.Diagnostic=$_.Exception.Message }
            finally { Write-VSOProgress $Context Verify -Completed }
        }
        if ($Scope -and $record.Scope -and $record.Scope -ne $Scope) { continue }
        if ($Version -and $record.Version -ne (ConvertTo-VSOVersion $Version $Context).Text) { continue }
        [pscustomobject]$record
    }
}

function Select-VSOInstallation {
    param($Context, [string]$Path, [string]$Version, [string]$Scope, [switch]$AllowDamaged, [switch]$AllowManual, [switch]$AllowMissing)
    $items=@(Get-VSOInstallationsInternal $Context $Path $Scope $Version)
    if ($items.Count -eq 0) { Throw-VSOError $Context NotFound }
    if ($items.Count -gt 1) {
        if (-not (Test-VSOInteractive $Context) -or $items.Count -gt 20) { Throw-VSOError $Context Multiple }
        $options=@(); $letters='ABDEFGHIJKLMNOPQRSTUV'
        for ($i=0; $i -lt $items.Count; $i++) { $options+=('&'+$letters[$i]+': '+$items[$i].Version+' | '+$items[$i].Scope+' | '+$items[$i].Path+' | '+$items[$i].StateDisplay) }
        $options+=$Context.Text.CancelChoice
        $selected=Request-VSOChoice $Context $Context.Text.ScopeTitle $Context.Text.Selection $options
        if ($selected -eq $items.Count) { Throw-VSOError $Context Cancelled }; $item=$items[$selected]
    } else { $item=$items[0] }
    if ($item.State -eq 'Missing' -and -not $AllowMissing) { Throw-VSOError $Context NotFound }
    if (-not $item.Managed -and -not $AllowManual) { Throw-VSOError $Context Manual }
    if ($item.State -eq 'Modified' -and -not $AllowDamaged) { Throw-VSOError $Context Corrupt @($item.Diagnostic) }
    return $item
}