param(
[Parameter(Mandatory = $true)][string]$PackagePath,
[string]$QemuPath,
[switch]$UseWorkspaceLauncher,
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
$packageRoot = (Resolve-Path -LiteralPath $PackagePath).Path
if (!$QemuPath) { $QemuPath = Join-Path $taskRoot '.tools/qemu/qemu-system-x86_64.exe' }
$caseRoot = Join-Path $taskRoot ('build/CSharpBoot/PackagedLauncher-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
$layout = Join-Path $caseRoot 'PackageLayout'
[void][IO.Directory]::CreateDirectory($layout)
# Preserve the actual generated wrapper and top-level JSON array. Only this private
# launcher layout is mutated; the release directory and existing proofs stay intact.
$relativePaths = @('Start.ps1','System.img','Tools/OpenCSharp.ps1','Tools/CSharpBoot/ReadHostDisplay.ps1',
'Validation/Boot.json','Validation/Receipts/Display.json','Validation/ReceiptFiles.json',
'Validation/Dependencies.json','Firmware/edk2-x86_64-code.fd','Firmware/edk2-i386-vars.fd')
$sourceHashes = @{}
foreach ($relative in $relativePaths) {
$source = Join-Path $packageRoot $relative
$destination = Join-Path $layout $relative
$sourceHashes[$relative] = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToLowerInvariant()
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination))
Copy-Item -LiteralPath $source -Destination $destination
}
if ($UseWorkspaceLauncher) { Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/OpenCSharp.ps1') -Destination (Join-Path $layout 'Tools/OpenCSharp.ps1') }
$start = Join-Path $layout 'Start.ps1'
$manifestPath = Join-Path $layout 'Validation/ReceiptFiles.json'
$comparisonPath = Join-Path $layout 'Validation/Receipts/Display.json'
$manifestBytes = [IO.File]::ReadAllBytes($manifestPath)
$comparisonBytes = [IO.File]::ReadAllBytes($comparisonPath)
$manifest = Get-Content -Raw -Encoding UTF8 -LiteralPath $manifestPath | ConvertFrom-Json
$entries = @($manifest | Where-Object { $_.Name -eq 'Display' -and $_.PackagedPath -eq 'Validation/Receipts/Display.json' })
if ($manifest.Count -lt 2 -or $entries.Count -ne 1 -or $entries[0] -is [Array]) { throw 'The actual package must contain a multi-entry receipt array and one scalar Display entry.' }
$expectedComparisonHash = (Get-FileHash -LiteralPath $comparisonPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($entries[0].Sha256 -ne $expectedComparisonHash) { throw 'Source package Display receipt hash is invalid.' }
$planPath = & $start -QemuPath $QemuPath -PlanOnly
$plan = Get-Content -Raw -Encoding UTF8 -LiteralPath $planPath | ConvertFrom-Json
if ($plan.ProcessStarted -ne $false -or $plan.AutomaticInput -ne $false -or $plan.PhysicalDiskAccess -ne $false -or
$plan.Display.Acceleration -ne 'tcg' -or $plan.Display.VerifiedTcgComparisonImageMatch -ne $true -or
$plan.Display.VerifiedTcgReceiptSha256 -ne $expectedComparisonHash -or
$plan.Display.VerifiedTcgBootResult -ne $comparisonPath) { throw 'Packaged default auto did not discover and verify the matching TCG receipt.' }
$checks = @('actual generated Start wrapper auto discovers scalar Display from the multi-entry package manifest')
function Assert-Rejected([string]$name, [string]$expectedMessage) {
$rejected = $false
try { & $start -QemuPath $QemuPath -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match $expectedMessage) { $rejected = $true } else { throw } }
if (!$rejected) { throw ('Packaged launcher accepted ' + $name) }
}
try {
# Appending whitespace preserves valid JSON but invalidates the manifest hash.
[IO.File]::WriteAllBytes($comparisonPath, [byte[]]($comparisonBytes + [byte]32))
Assert-Rejected 'a changed Display receipt' 'manifest verification'
$checks += 'packaged auto rejects changed Display receipt bytes before creating a session'
[IO.File]::WriteAllBytes($comparisonPath, $comparisonBytes)
$duplicate = @($manifest) + @($entries[0])
$duplicate | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
Assert-Rejected 'duplicate Display entries' 'manifest verification'
$checks += 'packaged auto rejects duplicate Display entries'
[IO.File]::WriteAllBytes($manifestPath, $manifestBytes)
$missing = @($manifest | Where-Object { $_.Name -ne 'Display' })
$missing | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
Assert-Rejected 'a missing Display entry' 'manifest verification'
$checks += 'packaged auto rejects a manifest without Display'
[IO.File]::WriteAllBytes($manifestPath, $manifestBytes)
$wrong = Get-Content -Raw -Encoding UTF8 -LiteralPath $comparisonPath | ConvertFrom-Json
$wrong.ImageSha256 = '0' * 64
$wrong | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $comparisonPath -Encoding UTF8
$entries[0].Sha256 = (Get-FileHash -LiteralPath $comparisonPath -Algorithm SHA256).Hash.ToLowerInvariant()
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
Assert-Rejected 'a different-image receipt with a valid manifest hash' 'identical image'
$checks += 'packaged auto rejects another image even when its manifest hash matches'
} finally {
[IO.File]::WriteAllBytes($comparisonPath, $comparisonBytes)
[IO.File]::WriteAllBytes($manifestPath, $manifestBytes)
}
foreach ($relative in $relativePaths) {
if ((Get-FileHash -LiteralPath (Join-Path $packageRoot $relative) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $sourceHashes[$relative]) { throw 'Regression test modified the source package.' }
}
$checks += 'source package files remain unchanged and no GUI or physical-media operation was requested'
$result = [ordered]@{Kind='PackagedLauncherManifestRegression';Passed=$true;PowerShellVersion=$PSVersionTable.PSVersion.ToString();
PackagePath=$packageRoot;LayoutPath=$layout;LayoutOnly=$true;WorkspaceLauncherOverride=[bool]$UseWorkspaceLauncher;
ImageSha256=$plan.ImageSha256;ManifestEntries=$manifest.Count;MatchedEntryIsScalar=$true;VerifiedTcgReceiptSha256=$expectedComparisonHash;
LauncherSha256=(Get-FileHash -LiteralPath (Join-Path $layout 'Tools/OpenCSharp.ps1') -Algorithm SHA256).Hash.ToLowerInvariant();
StartWrapperSha256=$sourceHashes['Start.ps1'];PlanPath=$planPath;Checks=$checks;SourcePackageUnchanged=$true;
ProcessStarted=$false;AutomaticInput=$false;PhysicalDiskAccess=$false;GtkStartupMeasured=$false}
if (!$OutputPath) { $OutputPath = Join-Path $caseRoot 'Result.json' }
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($OutputPath)))
$result | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $OutputPath -Encoding UTF8
Write-Output ([IO.Path]::GetFullPath($OutputPath))
param(
[Parameter(Mandatory = $true)][string]$PackagePath,
[string]$QemuPath,
[switch]$UseWorkspaceLauncher,
[string]$OutputPath
)
$ErrorActionPreference = 'Stop'
$taskRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
$packageRoot = (Resolve-Path -LiteralPath $PackagePath).Path
if (!$QemuPath) { $QemuPath = Join-Path $taskRoot '.tools/qemu/qemu-system-x86_64.exe' }
$caseRoot = Join-Path $taskRoot ('build/CSharpBoot/PackagedLauncher-' + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
$layout = Join-Path $caseRoot 'PackageLayout'
[void][IO.Directory]::CreateDirectory($layout)
# Preserve the actual generated wrapper and top-level JSON array. Only this private
# launcher layout is mutated; the release directory and existing proofs stay intact.
$relativePaths = @('Start.ps1','System.img','Tools/OpenCSharp.ps1','Tools/CSharpBoot/ReadHostDisplay.ps1',
'Validation/Boot.json','Validation/Receipts/Display.json','Validation/ReceiptFiles.json',
'Validation/Dependencies.json','Firmware/edk2-x86_64-code.fd','Firmware/edk2-i386-vars.fd')
$sourceHashes = @{}
foreach ($relative in $relativePaths) {
$source = Join-Path $packageRoot $relative
$destination = Join-Path $layout $relative
$sourceHashes[$relative] = (Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToLowerInvariant()
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination))
Copy-Item -LiteralPath $source -Destination $destination
}
if ($UseWorkspaceLauncher) { Copy-Item -LiteralPath (Join-Path $taskRoot 'Tools/OpenCSharp.ps1') -Destination (Join-Path $layout 'Tools/OpenCSharp.ps1') }
$start = Join-Path $layout 'Start.ps1'
$manifestPath = Join-Path $layout 'Validation/ReceiptFiles.json'
$comparisonPath = Join-Path $layout 'Validation/Receipts/Display.json'
$manifestBytes = [IO.File]::ReadAllBytes($manifestPath)
$comparisonBytes = [IO.File]::ReadAllBytes($comparisonPath)
$manifest = Get-Content -Raw -Encoding UTF8 -LiteralPath $manifestPath | ConvertFrom-Json
$entries = @($manifest | Where-Object { $_.Name -eq 'Display' -and $_.PackagedPath -eq 'Validation/Receipts/Display.json' })
if ($manifest.Count -lt 2 -or $entries.Count -ne 1 -or $entries[0] -is [Array]) { throw 'The actual package must contain a multi-entry receipt array and one scalar Display entry.' }
$expectedComparisonHash = (Get-FileHash -LiteralPath $comparisonPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($entries[0].Sha256 -ne $expectedComparisonHash) { throw 'Source package Display receipt hash is invalid.' }
$planPath = & $start -QemuPath $QemuPath -PlanOnly
$plan = Get-Content -Raw -Encoding UTF8 -LiteralPath $planPath | ConvertFrom-Json
if ($plan.ProcessStarted -ne $false -or $plan.AutomaticInput -ne $false -or $plan.PhysicalDiskAccess -ne $false -or
$plan.Display.Acceleration -ne 'tcg' -or $plan.Display.VerifiedTcgComparisonImageMatch -ne $true -or
$plan.Display.VerifiedTcgReceiptSha256 -ne $expectedComparisonHash -or
$plan.Display.VerifiedTcgBootResult -ne $comparisonPath) { throw 'Packaged default auto did not discover and verify the matching TCG receipt.' }
$checks = @('actual generated Start wrapper auto discovers scalar Display from the multi-entry package manifest')
function Assert-Rejected([string]$name, [string]$expectedMessage) {
$rejected = $false
try { & $start -QemuPath $QemuPath -PlanOnly | Out-Null }
catch { if ($_.Exception.Message -match $expectedMessage) { $rejected = $true } else { throw } }
if (!$rejected) { throw ('Packaged launcher accepted ' + $name) }
}
try {
# Appending whitespace preserves valid JSON but invalidates the manifest hash.
[IO.File]::WriteAllBytes($comparisonPath, [byte[]]($comparisonBytes + [byte]32))
Assert-Rejected 'a changed Display receipt' 'manifest verification'
$checks += 'packaged auto rejects changed Display receipt bytes before creating a session'
[IO.File]::WriteAllBytes($comparisonPath, $comparisonBytes)
$duplicate = @($manifest) + @($entries[0])
$duplicate | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
Assert-Rejected 'duplicate Display entries' 'manifest verification'
$checks += 'packaged auto rejects duplicate Display entries'
[IO.File]::WriteAllBytes($manifestPath, $manifestBytes)
$missing = @($manifest | Where-Object { $_.Name -ne 'Display' })
$missing | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
Assert-Rejected 'a missing Display entry' 'manifest verification'
$checks += 'packaged auto rejects a manifest without Display'
[IO.File]::WriteAllBytes($manifestPath, $manifestBytes)
$wrong = Get-Content -Raw -Encoding UTF8 -LiteralPath $comparisonPath | ConvertFrom-Json
$wrong.ImageSha256 = '0' * 64
$wrong | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $comparisonPath -Encoding UTF8
$entries[0].Sha256 = (Get-FileHash -LiteralPath $comparisonPath -Algorithm SHA256).Hash.ToLowerInvariant()
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
Assert-Rejected 'a different-image receipt with a valid manifest hash' 'identical image'
$checks += 'packaged auto rejects another image even when its manifest hash matches'
} finally {
[IO.File]::WriteAllBytes($comparisonPath, $comparisonBytes)
[IO.File]::WriteAllBytes($manifestPath, $manifestBytes)
}
foreach ($relative in $relativePaths) {
if ((Get-FileHash -LiteralPath (Join-Path $packageRoot $relative) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $sourceHashes[$relative]) { throw 'Regression test modified the source package.' }
}
$checks += 'source package files remain unchanged and no GUI or physical-media operation was requested'
$result = [ordered]@{Kind='PackagedLauncherManifestRegression';Passed=$true;PowerShellVersion=$PSVersionTable.PSVersion.ToString();
PackagePath=$packageRoot;LayoutPath=$layout;LayoutOnly=$true;WorkspaceLauncherOverride=[bool]$UseWorkspaceLauncher;
ImageSha256=$plan.ImageSha256;ManifestEntries=$manifest.Count;MatchedEntryIsScalar=$true;VerifiedTcgReceiptSha256=$expectedComparisonHash;
LauncherSha256=(Get-FileHash -LiteralPath (Join-Path $layout 'Tools/OpenCSharp.ps1') -Algorithm SHA256).Hash.ToLowerInvariant();
StartWrapperSha256=$sourceHashes['Start.ps1'];PlanPath=$planPath;Checks=$checks;SourcePackageUnchanged=$true;
ProcessStarted=$false;AutomaticInput=$false;PhysicalDiskAccess=$false;GtkStartupMeasured=$false}
if (!$OutputPath) { $OutputPath = Join-Path $caseRoot 'Result.json' }
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($OutputPath)))
$result | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $OutputPath -Encoding UTF8
Write-Output ([IO.Path]::GetFullPath($OutputPath))