Set-StrictMode -Version Latest
|
$ErrorActionPreference = 'Stop'
|
|
function Invoke-AnaSemiL1ContractValidationV001 {
|
[CmdletBinding()]
|
param(
|
[Parameter(Mandatory = $true)][string]$ProjectRoot,
|
[Parameter(Mandatory = $true)][string]$RunnerAssemblyPath,
|
[Parameter(Mandatory = $true)][string]$ExpectedVectorsPath,
|
[Parameter(Mandatory = $true)][string]$OutputPath
|
)
|
|
function Get-HexSha256Bytes([byte[]]$Bytes) {
|
$sha = [Security.Cryptography.SHA256]::Create()
|
try { return ([BitConverter]::ToString($sha.ComputeHash($Bytes)).Replace('-', '').ToLowerInvariant()) }
|
finally { $sha.Dispose() }
|
}
|
function Get-HexSha256File([string]$Path) {
|
$stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)
|
try {
|
$sha = [Security.Cryptography.SHA256]::Create()
|
try { return ([BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-', '').ToLowerInvariant()) }
|
finally { $sha.Dispose() }
|
}
|
finally { $stream.Dispose() }
|
}
|
function Get-ArgvFingerprint([string[]]$LogicalArgv) {
|
$stream = New-Object IO.MemoryStream
|
try {
|
$domain = [Text.Encoding]::UTF8.GetBytes('ANA-SEMI-ARGV-V001')
|
$stream.Write($domain, 0, $domain.Length)
|
foreach ($token in $LogicalArgv) {
|
$stream.WriteByte(0)
|
$bytes = [Text.Encoding]::UTF8.GetBytes($token)
|
$stream.Write($bytes, 0, $bytes.Length)
|
}
|
return (Get-HexSha256Bytes $stream.ToArray())
|
}
|
finally { $stream.Dispose() }
|
}
|
function Decode-EscapedBytes([string]$Escaped) {
|
$stream = New-Object IO.MemoryStream
|
try {
|
for ($index = 0; $index -lt $Escaped.Length; $index++) {
|
$ch = $Escaped[$index]
|
if ($ch -ne '\') {
|
$stream.WriteByte([byte][char]$ch)
|
continue
|
}
|
if ($index + 1 -ge $Escaped.Length) { throw 'ESCAPE_EOF' }
|
$index++
|
switch -CaseSensitive ($Escaped[$index]) {
|
't' { $stream.WriteByte(9) }
|
'n' { $stream.WriteByte(10) }
|
'\' { $stream.WriteByte(92) }
|
default { throw 'ESCAPE_INVALID' }
|
}
|
}
|
return $stream.ToArray()
|
}
|
finally { $stream.Dispose() }
|
}
|
function Get-ContractStopCode($ErrorRecord) {
|
$cursor = $ErrorRecord.Exception
|
while ($null -ne $cursor) {
|
$property = $cursor.GetType().GetProperty('StopCode')
|
if ($null -ne $property) { return [string]$property.GetValue($cursor, $null) }
|
$cursor = $cursor.InnerException
|
}
|
return $null
|
}
|
function Escape-JsonString([string]$Value) {
|
$builder = New-Object Text.StringBuilder
|
[void]$builder.Append('"')
|
foreach ($ch in $Value.ToCharArray()) {
|
$code = [int][char]$ch
|
switch ($code) {
|
8 { [void]$builder.Append('\b'); continue }
|
9 { [void]$builder.Append('\t'); continue }
|
10 { [void]$builder.Append('\n'); continue }
|
12 { [void]$builder.Append('\f'); continue }
|
13 { [void]$builder.Append('\r'); continue }
|
34 { [void]$builder.Append('\"'); continue }
|
92 { [void]$builder.Append('\\'); continue }
|
}
|
if ($code -lt 32) { [void]$builder.Append(('\u{0:x4}' -f $code)) }
|
else { [void]$builder.Append($ch) }
|
}
|
[void]$builder.Append('"')
|
return $builder.ToString()
|
}
|
function ConvertTo-JcsValue($Value) {
|
if ($null -eq $Value) { return 'null' }
|
if ($Value -is [string]) { return (Escape-JsonString $Value) }
|
if ($Value -is [bool]) { if ($Value) { return 'true' }; return 'false' }
|
if ($Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or $Value -is [uint16] -or
|
$Value -is [int32] -or $Value -is [uint32] -or $Value -is [int64] -or $Value -is [uint64]) {
|
return ([Convert]::ToString($Value, [Globalization.CultureInfo]::InvariantCulture))
|
}
|
if ($Value -is [Collections.IDictionary]) {
|
$keys = @($Value.Keys | ForEach-Object { [string]$_ })
|
[Array]::Sort($keys, [StringComparer]::Ordinal)
|
$pairs = New-Object Collections.Generic.List[string]
|
foreach ($key in $keys) { $pairs.Add((Escape-JsonString $key) + ':' + (ConvertTo-JcsValue $Value[$key])) }
|
return '{' + [string]::Join(',', $pairs.ToArray()) + '}'
|
}
|
if ($Value -is [Collections.IEnumerable]) {
|
$items = New-Object Collections.Generic.List[string]
|
foreach ($item in $Value) { $items.Add((ConvertTo-JcsValue $item)) }
|
return '[' + [string]::Join(',', $items.ToArray()) + ']'
|
}
|
$dictionary = [ordered]@{}
|
foreach ($property in $Value.PSObject.Properties) { $dictionary[$property.Name] = $property.Value }
|
return (ConvertTo-JcsValue $dictionary)
|
}
|
function Write-JcsCreateNew([string]$Path, $Value) {
|
$bytes = (New-Object Text.UTF8Encoding($false)).GetBytes((ConvertTo-JcsValue $Value))
|
$stream = New-Object IO.FileStream($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
try { $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) }
|
finally { $stream.Dispose() }
|
}
|
function New-Check([string]$Id, [string]$Expected, [string]$Actual, [string]$Result, $StopCode) {
|
return [ordered]@{ check_id = $Id; expected = $Expected; actual = $Actual; result = $Result; stop_code = $StopCode }
|
}
|
|
$vectors = [IO.File]::ReadAllText($ExpectedVectorsPath, [Text.Encoding]::UTF8) | ConvertFrom-Json
|
$checks = New-Object Collections.Generic.List[object]
|
$spec = @(
|
[pscustomobject]@{ Id = 'ARGV_QUOTING'; Stop = 'STOP_L1_ARGV_QUOTING'; Action = {
|
$actual = [AnaSemi.WindowsCommandLineV006]::QuoteAndJoin([string[]]@('plain','space value',''))
|
if ($actual -cne 'plain "space value" ""') { throw ('QUOTE:' + $actual) }
|
}},
|
[pscustomobject]@{ Id = 'ARGV_FINGERPRINT'; Stop = 'STOP_L1_ARGV_FINGERPRINT'; Action = {
|
$sample = [string[]]@('one','two words','three')
|
if ([AnaSemi.WindowsCommandLineV006]::Fingerprint($sample) -cne (Get-ArgvFingerprint $sample)) { throw 'FINGERPRINT' }
|
}},
|
[pscustomobject]@{ Id = 'ENVELOPE_EXTRA'; Stop = 'STOP_L1_ENVELOPE_EXTRA'; Action = {
|
$caught = $null
|
try { [void][AnaSemi.ProcessResultEnvelopeValidatorV006]::ValidateSingle([object[]]@('sentinel','second')) }
|
catch { $caught = Get-ContractStopCode $_ }
|
if ($caught -cne 'STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT') { throw ('ACTUAL:' + $caught) }
|
}},
|
[pscustomobject]@{ Id = 'ENVELOPE_MISSING'; Stop = 'STOP_L1_ENVELOPE_MISSING'; Action = {
|
$bad = [pscustomobject]@{ schema_id = 'ANA-SEMI-NATIVE-PROCESS-RESULT-V005' }
|
$caught = $null
|
try { [void][AnaSemi.ProcessResultEnvelopeValidatorV006]::ValidateSingle([object[]]@($bad)) }
|
catch { $caught = Get-ContractStopCode $_ }
|
if ($caught -cne 'STOP_PROCESS_RESULT_COUNT_TYPE_OR_PROPERTY_DRIFT') { throw ('ACTUAL:' + $caught) }
|
}},
|
[pscustomobject]@{ Id = 'RESULT_STATE_MATRIX'; Stop = 'STOP_L1_RESULT_STATE_MATRIX'; Action = {
|
if ([string]::Join('|', @($vectors.property_contract.wrapper_status)) -cne 'RETURNED|PROCESS_START_FAILED|TIMED_OUT|COPY_FAILED') { throw 'WRAPPER' }
|
if ([string]::Join('|', @($vectors.property_contract.child_liveness)) -cne 'EXITED|NOT_STARTED') { throw 'LIVENESS' }
|
if ([uint16]$vectors.property_contract.property_count -ne 22 -or @($vectors.property_contract.property_names).Count -ne 22) { throw 'PROPERTY' }
|
}},
|
[pscustomobject]@{ Id = 'RAW_PREIMAGE'; Stop = 'STOP_L1_RAW_PREIMAGE'; Action = {
|
foreach ($property in $vectors.raw_preimages.PSObject.Properties) {
|
$item = $property.Value
|
$stdout = Decode-EscapedBytes ([string]$item.stdout_escaped)
|
$stderr = Decode-EscapedBytes ([string]$item.stderr_escaped)
|
if ([uint64]$stdout.Length -ne [uint64]$item.stdout_bytes -or (Get-HexSha256Bytes $stdout) -cne [string]$item.stdout_sha256) { throw ('STDOUT:' + $property.Name) }
|
if ([uint64]$stderr.Length -ne [uint64]$item.stderr_bytes -or (Get-HexSha256Bytes $stderr) -cne [string]$item.stderr_sha256) { throw ('STDERR:' + $property.Name) }
|
}
|
}},
|
[pscustomobject]@{ Id = 'REAL_13_TARGET_CONTRACT'; Stop = 'STOP_L1_REAL_13_TARGET_CONTRACT'; Action = {
|
$ids = 1..13 | ForEach-Object { 'T{0:d2}' -f $_ }
|
if (@($ids | Select-Object -Unique).Count -ne 13) { throw 'TARGET_IDS' }
|
$statuses = @('INPUT_READY','PROCESS_OUTPUT_PASS','PROCESS_OUTPUT_FAIL','EMPTY_STDERR_PASS','NOT_RUN_UPSTREAM_STOP','RECEIPT_FINALIZED','MANIFEST_SELF_EXTERNAL_HASH','PROCESS_RESULT_READY','PROCESS_RESULT_CONTRACT_FAILED','PROCESS_RESULT_SIDECAR_FAILED')
|
if (@($statuses | Select-Object -Unique).Count -ne 10) { throw 'STATUS_ENUM' }
|
}},
|
[pscustomobject]@{ Id = 'SYNTHETIC_30_TARGET_CONTRACT'; Stop = 'STOP_L1_SYNTHETIC_30_TARGET_CONTRACT'; Action = {
|
$rows = @($vectors.manifest_contract.rows)
|
if ($rows.Count -ne 30 -or [uint16]$vectors.manifest_contract.target_count -ne 30) { throw 'COUNT' }
|
for ($index = 0; $index -lt 30; $index++) { if ($rows[$index].target_id -cne ('ST{0:d2}' -f ($index + 1))) { throw 'ORDER' } }
|
if (@($rows.path | Select-Object -Unique).Count -ne 30) { throw 'PATH_UNIQUE' }
|
$expectedCounts = @{ DESCRIPTOR_READY = 3; FIXTURE_READY = 1; RAW_READY = 12; SIDECAR_READY = 5; PROBE_PASS = 4; SENTINEL_UNCHANGED = 2; SUMMARY_PASS = 1; RECEIPT_FINALIZED = 1; MANIFEST_SELF_EXTERNAL_HASH = 1 }
|
foreach ($key in $expectedCounts.Keys) { if (@($rows | Where-Object { $_.status -ceq $key }).Count -ne $expectedCounts[$key]) { throw ('STATUS:' + $key) } }
|
}},
|
[pscustomobject]@{ Id = 'CREATE_NEW_DECISION'; Stop = 'STOP_L1_CREATE_NEW_DECISION'; Action = {
|
function Get-Decision([bool]$Exists, [bool]$EscapesRoot) { if ($Exists -or $EscapesRoot) { return 'STOP_TARGET_ALREADY_EXISTS' }; return 'ALLOW_CREATE_NEW' }
|
if ((Get-Decision $false $false) -cne 'ALLOW_CREATE_NEW') { throw 'ABSENT' }
|
if ((Get-Decision $true $false) -cne 'STOP_TARGET_ALREADY_EXISTS') { throw 'EXISTS' }
|
if ((Get-Decision $false $true) -cne 'STOP_TARGET_ALREADY_EXISTS') { throw 'ESCAPE' }
|
}}
|
)
|
|
$firstFailure = $null
|
foreach ($item in $spec) {
|
if ($null -ne $firstFailure) {
|
$checks.Add((New-Check $item.Id 'PASS' 'NOT_RUN_UPSTREAM_STOP' 'NOT_RUN' 'STOP_UPSTREAM_NOT_RUN'))
|
continue
|
}
|
try {
|
[void](& $item.Action)
|
$checks.Add((New-Check $item.Id 'PASS' 'PASS' 'PASS' $null))
|
}
|
catch {
|
$firstFailure = $item.Stop
|
$checks.Add((New-Check $item.Id 'PASS' ('FAIL:' + $_.Exception.Message) 'FAIL' $firstFailure))
|
}
|
}
|
|
$sourceHashes = [ordered]@{}
|
$sourcePaths = @('dev/ana-dev/AnaSemi.NativeProcessRunnerV006.cs','dev/ana-dev/Invoke-AnaSemiWrapperSelftestV006.ps1','dev/ana-dev/synthetic-child.ps1','dev/ana-dev/test/Test-AnaSemiNativeProcessRunnerV006.Static.ps1','dev/ana-dev/test/Test-InvokeAnaSemiWrapperSelftestV006.Static.ps1','dev/ana-dev/test/V007ExpectedVectors.json','dev/ana-dev/test/Invoke-AnaSemiImplementationValidationV001.ps1')
|
for ($index = 0; $index -lt $sourcePaths.Count; $index++) { $sourceHashes[('I{0:d2}' -f ($index + 1))] = Get-HexSha256File (Join-Path $ProjectRoot $sourcePaths[$index]) }
|
$stagingPath = Join-Path (Split-Path (Split-Path $OutputPath -Parent) -Parent) 'staging/AnaSemi.NativeProcessRunnerV006.dll'
|
$stagingInfo = Get-Item -LiteralPath $stagingPath
|
$publishedInfo = Get-Item -LiteralPath $RunnerAssemblyPath
|
$publishedHash = Get-HexSha256File $RunnerAssemblyPath
|
$stagingHash = Get-HexSha256File $stagingPath
|
$assembly = @([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -ceq 'AnaSemi.NativeProcessRunnerV006' })[0]
|
$apiParts = @('ANA-SEMI-PUBLIC-API-V003',$assembly.FullName,'AnaSemi.NativeProcessResultV006','AnaSemi.NativeProcessContractExceptionV006','AnaSemi.WindowsCommandLineV006.QuoteAndJoin(System.String[])','AnaSemi.WindowsCommandLineV006.Fingerprint(System.String[])','AnaSemi.NativeProcessRunnerV006.Run(System.String,System.String[],System.String,System.String,System.Int32,System.Int32,System.String)','AnaSemi.ProcessResultEnvelopeValidatorV006.ValidateSingle(System.Object[])','AnaSemi.ProcessResultSidecarCsvV006.WriteCreateNew(AnaSemi.NativeProcessResultV006,System.String)')
|
$apiParts += @('schema_id:System.String','process_kind:System.String','process_started:System.Boolean','process_id:System.Nullable`1[System.UInt32]','argv_count:System.UInt16','argv_fingerprint:System.String','command_line_sha256:System.String','roundtrip_argv_fingerprint:System.String','started_at:System.Nullable`1[System.DateTimeOffset]','finished_at:System.DateTimeOffset','exit_code:System.Nullable`1[System.Int32]','timed_out:System.Boolean','copy_state:System.String','child_liveness:System.String','stdout_path:System.String','stdout_bytes:System.UInt64','stdout_sha256:System.String','stderr_path:System.String','stderr_bytes:System.UInt64','stderr_sha256:System.String','result_contract_version:System.String','wrapper_status:System.String')
|
$publicApiHash = Get-HexSha256Bytes ([Text.Encoding]::UTF8.GetBytes([string]::Join([char]0, $apiParts)))
|
$passCount = @($checks | Where-Object { $_.result -ceq 'PASS' }).Count
|
$failCount = @($checks | Where-Object { $_.result -ceq 'FAIL' }).Count
|
$notRunCount = @($checks | Where-Object { $_.result -ceq 'NOT_RUN' }).Count
|
$result = [ordered]@{
|
assembly = [ordered]@{ full_name = $assembly.FullName; public_api_sha256 = $publicApiHash; published_bytes = [uint64]$publishedInfo.Length; published_path = $RunnerAssemblyPath; published_sha256 = $publishedHash; staging_bytes = [uint64]$stagingInfo.Length; staging_path = $stagingPath; staging_sha256 = $stagingHash }
|
check_count = [uint16]9
|
checks = $checks.ToArray()
|
credential_count = [uint16]0
|
database_count = [uint16]0
|
external_process_count = [uint16]0
|
fail_count = [uint16]$failCount
|
network_count = [uint16]0
|
not_run_count = [uint16]$notRunCount
|
pass_count = [uint16]$passCount
|
run_id = 'RUN-DEV-ANA-SEMI-ROOT-PREFLIGHT-V007-IMPLEMENTATION-20260724-001'
|
schema_id = 'ANA-SEMI-IMPLEMENTATION-L1-JCS-V002'
|
source_hashes = $sourceHashes
|
status = if ($failCount -eq 0) { 'PASS' } else { 'FAIL' }
|
terminal_stop_code = $firstFailure
|
toolchain = [ordered]@{
|
csc = [ordered]@{ bytes = [uint64]2569696; path = 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe'; sha256 = 'aae1db57f898ca8bda18590c56f86ced6d0ead80c22033b76fdfe9119706f116'; version = '4.8.9232.0 built by: NET481REL1LAST_C' }
|
powershell = [ordered]@{ bytes = [uint64]455680; path = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'; sha256 = '9785001b0dcf755eddb8af294a373c0b87b2498660f724e76c4d53f9c217c7a3'; version = '10.0.19041.1 (WinBuild.160101.0800)' }
|
}
|
}
|
Write-JcsCreateNew -Path $OutputPath -Value $result
|
if ($null -ne $firstFailure) { throw $firstFailure }
|
}
|