Browse Source

punk-runtime 'info' action: embedded record vs sidecar identity report (G-117)

Both payloads + rewrapped bin/punk-runtime.cmd (roundtrip pin green 9/0/1skip).
Embedded read WITHOUT executing the target: bash uses unzip's central-directory
read; ps1 uses .NET System.IO.Compression with a manual EOCD scan + prepend
offset fixup (.NET does not apply the self-extractor fixup - the pure-zip tail
is copied to a temp file and read there; delta 0 = plain zip also fine). The
cooperative probe (piping a reader into the EXECUTED target) is printed as the
documented fallback when no zip reader exists, execution caveat stated.
Report: field-by-field embedded/sidecar columns, !DISAGREE flags on shared-field
mismatches, file sha1 checked against the sidecar (!SHA1-MISMATCH), stray
renamed copies identified embedded-only, pre-v1 artifacts report sidecar-only
(schema-tolerant parsing; new Get-PunkRuntimeRecordFields helper). Verified on
the emitted v1 r2 artifacts: sidecar-agree case, renamed-stray case, pre-v1
case - ps1/bash output parity, wrapped cmd smoke under Windows PowerShell 5.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 6 days ago
parent
commit
c23c854eeb
  1. 296
      bin/punk-runtime.cmd
  2. 104
      src/scriptapps/bin/punk-runtime.bash
  3. 192
      src/scriptapps/bin/punk-runtime.ps1

296
bin/punk-runtime.cmd

@ -1506,11 +1506,12 @@ metadata_summary() {
#operator help (the 'help' action; the no-args case shows show_usage only). #operator help (the 'help' action; the no-args case shows show_usage only).
#Keep in sync with the ps1 payload's Show-PunkRuntimeHelp/Show-PunkRuntimeUsage. #Keep in sync with the ps1 payload's Show-PunkRuntimeHelp/Show-PunkRuntimeUsage.
show_usage() { show_usage() {
echo "Usage: $0 {fetch|list|use|run|platforms|help}" echo "Usage: $0 {fetch|list|use|run|info|platforms|help}"
echo " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin" echo " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin"
echo " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata" echo " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata"
echo " use <name> ?-platform <p>? select active / materialize -r<N> artifact" echo " use <name> ?-platform <p>? select active / materialize -r<N> artifact"
echo " run ?args...? launch the active local-platform runtime" echo " run ?args...? launch the active local-platform runtime"
echo " info <name-or-path> ?-platform <p>? identity report: embedded record vs sidecar toml"
echo " platforms ?-remote? local platform folders / platforms the server serves" echo " platforms ?-remote? local platform folders / platforms the server serves"
echo " help full help (options, env vars, examples)" echo " help full help (options, env vars, examples)"
} }
@ -1549,6 +1550,17 @@ show_help() {
echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole"
echo " installed candidate. Takes no -platform (foreign binaries are" echo " installed candidate. Takes no -platform (foreign binaries are"
echo " not runnable here)." echo " not runnable here)."
echo " info <name-or-path> ?-platform <p>?"
echo " Identity report for a runtime file (G-117 schema v1): the record"
echo " EMBEDDED in its attached image and the sidecar toml side by side,"
echo " with disagreements flagged and the file's sha1 checked against"
echo " the sidecar. The embedded read uses a zip central-directory read"
echo " of the exe-appended archive (unzip) - the target is NEVER"
echo " executed, so it is safe on stray/untrusted binaries; without"
echo " unzip the printed fallback is a cooperative probe that EXECUTES"
echo " the target (only for binaries you already trust). Works for a"
echo " bare renamed copy with no sidecar (embedded-only report);"
echo " pre-v1 artifacts without an embedded record report sidecar-only."
echo " platforms ?-remote?" echo " platforms ?-remote?"
echo " List local platform folders under bin/runtime/. With -remote," echo " List local platform folders under bin/runtime/. With -remote,"
echo " list the platforms the artifact server serves - read from the" echo " list the platforms the artifact server serves - read from the"
@ -2000,6 +2012,96 @@ case "$action" in
echo "Canonical platform names: 'help platforms' in the punk shell" echo "Canonical platform names: 'help platforms' in the punk shell"
fi fi
;; ;;
"info")
#G-117: identity report - the sidecar toml and the record EMBEDDED in the
#artifact's attached image, side by side, disagreements flagged. Embedded
#read = zip central-directory read of the exe-appended archive via unzip
#(the target is NEVER executed - identifying an untrusted stray binary by
#running it is exactly what this action avoids). Hosts without unzip get
#the cooperative-probe fallback COMMAND printed instead (that one EXECUTES
#the target - only for binaries already trusted).
iname="${1:-}"
if [[ -z "$iname" ]]; then
echo "usage: $0 info <runtime-name-or-path> ?-platform <p>?"
exit 1
fi
if [[ -f "$iname" ]]; then
tfile="$iname"
elif [[ -f "$archdir/$iname" ]]; then
tfile="$archdir/$iname"
else
echo "punk-runtime info: no such file: '$iname' (checked as a path and in $archdir)"
exit 1
fi
tdir=$(dirname "$tfile"); tbase=$(basename "$tfile")
sidecar="$tdir/$(rootname_of "$tbase").toml"
embedded=""
haveunzip=0
if command -v unzip >/dev/null 2>&1; then
haveunzip=1
embedded=$(unzip -p "$tfile" punkbin-artifact.toml 2>/dev/null || true)
fi
localsha1=$(lcase "$(sha1_of "$tfile")")
echo "-----------------------------------------------------------------------"
echo "punk-runtime info: $tbase"
echo " file: $tfile"
[[ -n "$localsha1" ]] && echo " sha1: $localsha1 (computed from the file)"
if [[ -f "$sidecar" ]]; then
echo " sidecar: $sidecar"
else
echo " sidecar: (none beside the file - stray/renamed copy?)"
fi
if [[ $haveunzip -eq 0 ]]; then
echo " embedded: (cannot read - no 'unzip' on this host)"
echo " fallback - a cooperative probe that EXECUTES the target (only run"
echo " this against a binary you already trust):"
echo " echo 'catch {puts [read [open //zipfs:/app/punkbin-artifact.toml]]};exit' | $tfile"
elif [[ -z "$embedded" ]]; then
echo " embedded: (no punkbin-artifact.toml in the attached image - pre-v1"
echo " artifact, or not a punkshell family runtime)"
fi
echo "-----------------------------------------------------------------------"
if [[ -n "$embedded" || -f "$sidecar" ]]; then
printf ' %-20s %-36s %s\n' "field" "embedded" "sidecar"
disagreements=""
for k in schema name class variant working_name revision target build_id origin packager project project_url license build_host_platform tcl_patchlevel sha1 size built; do
ev=""; sv=""
if [[ -n "$embedded" ]]; then
ev=$(printf '%s\n' "$embedded" | sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' | head -n 1)
[[ -z "$ev" ]] && ev=$(printf '%s\n' "$embedded" | sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*\([0-9][0-9]*\|true\|false\).*/\1/p' | head -n 1)
fi
if [[ -f "$sidecar" ]]; then
sv=$(sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$sidecar" | head -n 1)
[[ -z "$sv" ]] && sv=$(sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*\([0-9][0-9]*\|true\|false\).*/\1/p' "$sidecar" | head -n 1)
fi
[[ -z "$ev" && -z "$sv" ]] && continue
printf ' %-20s %-36s %s\n' "$k" "${ev:--}" "${sv:--}"
#schema absent on one side is pre-v1 tolerance, not a disagreement
if [[ -n "$ev" && -n "$sv" && "$ev" != "$sv" ]]; then
disagreements="$disagreements $k"
fi
done
echo "-----------------------------------------------------------------------"
if [[ -n "$disagreements" ]]; then
echo " !DISAGREE embedded-vs-sidecar on:$disagreements"
echo " (the sidecar + sha1sums are the integrity authority; a mismatched pair"
echo " means this sidecar belongs to a different assembly than this binary)"
elif [[ -n "$embedded" && -f "$sidecar" ]]; then
echo " embedded and sidecar records agree on all shared fields"
fi
if [[ -f "$sidecar" && -n "$localsha1" ]]; then
scsha1=$(lcase "$(sed -n 's/^[[:space:]]*sha1[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$sidecar" | head -n 1)")
if [[ -n "$scsha1" ]]; then
if [[ "$scsha1" == "$localsha1" ]]; then
echo " sha1: file matches the sidecar record"
else
echo " !SHA1-MISMATCH: file does not match the sidecar record (modified or"
echo " different assembly; for a materialized working copy compare build_id)"
fi
fi
fi
fi
;;
"help") "help")
show_help show_help
;; ;;
@ -2401,11 +2503,12 @@ function Get-PunkRuntimeCandidates {
#operator help (the 'help' action; the no-args case shows the Usage block only). #operator help (the 'help' action; the no-args case shows the Usage block only).
#Keep in sync with the bash payload's show_help/show_usage. #Keep in sync with the bash payload's show_help/show_usage.
function Show-PunkRuntimeUsage { function Show-PunkRuntimeUsage {
write-host "Usage: punk-runtime.cmd {fetch|list|use|run|platforms|help}" write-host "Usage: punk-runtime.cmd {fetch|list|use|run|info|platforms|help}"
write-host " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin" write-host " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin"
write-host " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata" write-host " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata"
write-host " use <name> ?-platform <p>? select active / materialize -r<N> artifact" write-host " use <name> ?-platform <p>? select active / materialize -r<N> artifact"
write-host " run ?args...? launch the active local-platform runtime" write-host " run ?args...? launch the active local-platform runtime"
write-host " info <name-or-path> ?-platform <p>? identity report: embedded record vs sidecar toml"
write-host " platforms ?-remote? local platform folders / platforms the server serves" write-host " platforms ?-remote? local platform folders / platforms the server serves"
write-host " help full help (options, env vars, examples)" write-host " help full help (options, env vars, examples)"
} }
@ -2444,6 +2547,16 @@ function Show-PunkRuntimeHelp {
write-host " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" write-host " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole"
write-host " installed candidate. Takes no -platform (foreign binaries are" write-host " installed candidate. Takes no -platform (foreign binaries are"
write-host " not runnable here)." write-host " not runnable here)."
write-host " info <name-or-path> ?-platform <p>?"
write-host " Identity report for a runtime file (G-117 schema v1): the record"
write-host " EMBEDDED in its attached image and the sidecar toml side by side,"
write-host " with disagreements flagged and the file's sha1 checked against"
write-host " the sidecar. The embedded read uses a zip central-directory read"
write-host " of the exe-appended archive (.NET System.IO.Compression) - the"
write-host " target is NEVER executed, so it is safe on stray/untrusted"
write-host " binaries. Works for a bare renamed copy with no sidecar"
write-host " (embedded-only report); pre-v1 artifacts without an embedded"
write-host " record report sidecar-only."
write-host " platforms ?-remote?" write-host " platforms ?-remote?"
write-host " List local platform folders under bin/runtime/. With -remote," write-host " List local platform folders under bin/runtime/. With -remote,"
write-host " list the platforms the artifact server serves - read from the" write-host " list the platforms the artifact server serves - read from the"
@ -2588,6 +2701,102 @@ function Get-PunkRuntimeMetadataSummary {
} }
return "[" + ($parts -join " ") + "]" return "[" + ($parts -join " ") + "]"
} }
#G-117 record parsing (schema-tolerant): quoted-string / bare integer / true|false
#value lines only; '#' comments and unknown fields are ignored by construction, and
#an absent schema line simply yields no 'schema' key (pre-v1 records keep working).
function Get-PunkRuntimeRecordFields {
param([string[]] $recordlines)
$fields = @{}
foreach ($line in $recordlines) {
$m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"(.*)"\s*$')
if ($m.Success) {
if (-not $fields.ContainsKey($m.Groups[1].Value)) { $fields[$m.Groups[1].Value] = $m.Groups[2].Value }
continue
}
$m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([0-9]+|true|false)\s*$')
if ($m.Success) {
if (-not $fields.ContainsKey($m.Groups[1].Value)) { $fields[$m.Groups[1].Value] = $m.Groups[2].Value }
}
}
return $fields
}
#G-117 embedded record: zip CENTRAL-DIRECTORY read of the exe-appended archive via
#.NET System.IO.Compression (offsets resolve from the end-of-central-directory
#record, so the prepended executable data is no obstacle). The target is NEVER
#executed - identifying an untrusted stray binary by running it is exactly what an
#identity mechanism should avoid; the cooperative probe (piping a reader script
#into the EXECUTED target) is the documented fallback for hosts without a zip
#reader, with that execution caveat. Returns the record text, or $null when the
#archive has no punkbin-artifact.toml (pre-v1 artifact / not a family runtime).
function Get-PunkRuntimeEmbeddedRecord {
param([string] $Path)
#.NET's ZipArchive does NOT apply the self-extractor offset fixup Info-ZIP
#applies: an exe-appended archive's central-directory offsets are relative to
#the ZIP START, not the file start, and OpenRead on the exe throws. Locate the
#end-of-central-directory record ourselves, compute the prepend length (actual
#CD position minus recorded offset), copy the pure-zip tail to a temp file and
#let .NET read that (deflate/stored handling for free). Works for plain zips
#too (delta 0). No zip64 handling - family images are far below those limits.
try { Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop } catch { }
$fullpath = (Resolve-Path -LiteralPath $Path).ProviderPath
$tmpzip = $null
$zip = $null
try {
$fs = [System.IO.File]::OpenRead($fullpath)
try {
$flen = $fs.Length
if ($flen -lt 22) { return $null }
#EOCD signature scan in the trailing 64KB+22 (max comment length)
$scanlen = [Math]::Min($flen, 65557 + 22)
$fs.Seek($flen - $scanlen, [System.IO.SeekOrigin]::Begin) | Out-Null
$buf = New-Object byte[] $scanlen
$read = 0
while ($read -lt $scanlen) {
$n = $fs.Read($buf, $read, $scanlen - $read)
if ($n -le 0) { break }
$read += $n
}
$eocdrel = -1
for ($i = $read - 22; $i -ge 0; $i--) {
if ($buf[$i] -eq 0x50 -and $buf[$i+1] -eq 0x4b -and $buf[$i+2] -eq 0x05 -and $buf[$i+3] -eq 0x06) {
$eocdrel = $i
break
}
}
if ($eocdrel -lt 0) { return $null }
$eocdabs = ($flen - $scanlen) + $eocdrel
$cdsize = [System.BitConverter]::ToUInt32($buf, $eocdrel + 12)
$cdoffset = [System.BitConverter]::ToUInt32($buf, $eocdrel + 16)
$zipstart = $eocdabs - $cdsize - $cdoffset
if ($zipstart -lt 0) { return $null }
$tmpzip = [System.IO.Path]::GetTempFileName()
$out = [System.IO.File]::Open($tmpzip, [System.IO.FileMode]::Create)
try {
$fs.Seek($zipstart, [System.IO.SeekOrigin]::Begin) | Out-Null
$fs.CopyTo($out)
} finally {
$out.Dispose()
}
} finally {
$fs.Dispose()
}
$zip = [System.IO.Compression.ZipFile]::OpenRead($tmpzip)
foreach ($entry in $zip.Entries) {
if ($entry.FullName -eq 'punkbin-artifact.toml') {
$sr = New-Object System.IO.StreamReader($entry.Open())
try { return $sr.ReadToEnd() } finally { $sr.Dispose() }
}
}
return $null
} catch {
return $null
} finally {
if ($zip) { $zip.Dispose() }
if ($tmpzip -and (Test-Path -LiteralPath $tmpzip)) {
Remove-Item -LiteralPath $tmpzip -Force -ErrorAction SilentlyContinue
}
}
}
function psmain { function psmain {
[CmdletBinding()] [CmdletBinding()]
@ -2622,7 +2831,7 @@ function psmain {
$paramDictionary.Add('remote', $dynParam1) $paramDictionary.Add('remote', $dynParam1)
$paramDictionary.Add('platform', $dynParam2) $paramDictionary.Add('platform', $dynParam2)
return $paramDictionary return $paramDictionary
} elseif ($action -eq 'fetch' -or $action -eq 'use') { } elseif ($action -eq 'fetch' -or $action -eq 'use' -or $action -eq 'info') {
#GetDynamicParamDictionary ParameterDefinitions #GetDynamicParamDictionary ParameterDefinitions
$parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{
ParameterSetName = "fetchruntime" ParameterSetName = "fetchruntime"
@ -2695,7 +2904,7 @@ function psmain {
'action' { 'action' {
write-host "got action " $PSBoundParameters.action write-host "got action " $PSBoundParameters.action
Set-Variable -Name $_ -Value $PSBoundParameters."$_" Set-Variable -Name $_ -Value $PSBoundParameters."$_"
$known_actions = @("fetch", "list", "use", "run", "platforms", "help") $known_actions = @("fetch", "list", "use", "run", "info", "platforms", "help")
if (-not($known_actions -contains $action)) { if (-not($known_actions -contains $action)) {
write-host "action '$action' not understood. Known_actions: $known_actions" write-host "action '$action' not understood. Known_actions: $known_actions"
exit 1 exit 1
@ -3221,6 +3430,85 @@ function psmain {
} }
} }
} }
'info' {
#G-117: identity report - the sidecar toml and the record EMBEDDED in
#the artifact's attached image, side by side, disagreements flagged.
#Embedded read = zip central-directory read (the target is NEVER
#executed; see Get-PunkRuntimeEmbeddedRecord). Keep output in parity
#with the bash payload's "info" action.
$arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"]
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
$iname = ""
if ( $PSBoundParameters["runtime"].Length ) { $iname = $PSBoundParameters["runtime"] }
if (-not $iname) {
write-host "usage: punk-runtime.cmd info <runtime-name-or-path> ?-platform <p>?"
exit 1
}
$tfile = ""
if (Test-Path -Path $iname -PathType Leaf) {
$tfile = (Resolve-Path -LiteralPath $iname).ProviderPath
} elseif (Test-Path -Path (Join-Path -Path $archfolder -ChildPath $iname) -PathType Leaf) {
$tfile = (Resolve-Path -LiteralPath (Join-Path -Path $archfolder -ChildPath $iname)).ProviderPath
} else {
write-host "punk-runtime info: no such file: '$iname' (checked as a path and in $archfolder)"
exit 1
}
$tbase = Split-Path -Leaf $tfile
$tdir = Split-Path -Parent $tfile
$sidecarfile = Join-Path -Path $tdir -ChildPath ((Get-PunkRuntimeRootName $tbase) + ".toml")
$embedtext = Get-PunkRuntimeEmbeddedRecord $tfile
$localsha1 = Get-PunkFileSha1 -Path $tfile
write-host "-----------------------------------------------------------------------"
write-host "punk-runtime info: $tbase"
write-host " file: $tfile"
write-host " sha1: $localsha1 (computed from the file)"
$havesidecar = Test-Path -Path $sidecarfile -PathType Leaf
if ($havesidecar) {
write-host " sidecar: $sidecarfile"
} else {
write-host " sidecar: (none beside the file - stray/renamed copy?)"
}
if ($null -eq $embedtext) {
write-host " embedded: (no punkbin-artifact.toml in the attached image - pre-v1"
write-host " artifact, or not a punkshell family runtime)"
}
write-host "-----------------------------------------------------------------------"
if (($null -ne $embedtext) -or $havesidecar) {
$efields = @{}
if ($null -ne $embedtext) { $efields = Get-PunkRuntimeRecordFields ($embedtext -split "`r?`n") }
$sfields = @{}
if ($havesidecar) { $sfields = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $sidecarfile)) }
write-host (" {0,-20} {1,-36} {2}" -f "field", "embedded", "sidecar")
$disagreements = @()
foreach ($k in @('schema','name','class','variant','working_name','revision','target','build_id','origin','packager','project','project_url','license','build_host_platform','tcl_patchlevel','sha1','size','built')) {
$ev = ""; $sv = ""
if ($efields.ContainsKey($k)) { $ev = $efields[$k] }
if ($sfields.ContainsKey($k)) { $sv = $sfields[$k] }
if (($ev -eq "") -and ($sv -eq "")) { continue }
$evd = "-"; if ($ev -ne "") { $evd = $ev }
$svd = "-"; if ($sv -ne "") { $svd = $sv }
write-host (" {0,-20} {1,-36} {2}" -f $k, $evd, $svd)
#schema absent on one side is pre-v1 tolerance, not a disagreement
if (($ev -ne "") -and ($sv -ne "") -and ($ev -cne $sv)) { $disagreements += $k }
}
write-host "-----------------------------------------------------------------------"
if ($disagreements.Count) {
write-host " !DISAGREE embedded-vs-sidecar on: $($disagreements -join ' ')"
write-host " (the sidecar + sha1sums are the integrity authority; a mismatched pair"
write-host " means this sidecar belongs to a different assembly than this binary)"
} elseif (($null -ne $embedtext) -and $havesidecar) {
write-host " embedded and sidecar records agree on all shared fields"
}
if ($havesidecar -and $sfields.ContainsKey('sha1')) {
if ($sfields['sha1'] -eq $localsha1) {
write-host " sha1: file matches the sidecar record"
} else {
write-host " !SHA1-MISMATCH: file does not match the sidecar record (modified or"
write-host " different assembly; for a materialized working copy compare build_id)"
}
}
}
}
'platforms' { 'platforms' {
#enumerate platform folders: local (bin/runtime/*) and, with #enumerate platform folders: local (bin/runtime/*) and, with
#-remote, the server's platforms.txt discovery manifest (raw-file #-remote, the server's platforms.txt discovery manifest (raw-file

104
src/scriptapps/bin/punk-runtime.bash

@ -226,11 +226,12 @@ metadata_summary() {
#operator help (the 'help' action; the no-args case shows show_usage only). #operator help (the 'help' action; the no-args case shows show_usage only).
#Keep in sync with the ps1 payload's Show-PunkRuntimeHelp/Show-PunkRuntimeUsage. #Keep in sync with the ps1 payload's Show-PunkRuntimeHelp/Show-PunkRuntimeUsage.
show_usage() { show_usage() {
echo "Usage: $0 {fetch|list|use|run|platforms|help}" echo "Usage: $0 {fetch|list|use|run|info|platforms|help}"
echo " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin" echo " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin"
echo " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata" echo " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata"
echo " use <name> ?-platform <p>? select active / materialize -r<N> artifact" echo " use <name> ?-platform <p>? select active / materialize -r<N> artifact"
echo " run ?args...? launch the active local-platform runtime" echo " run ?args...? launch the active local-platform runtime"
echo " info <name-or-path> ?-platform <p>? identity report: embedded record vs sidecar toml"
echo " platforms ?-remote? local platform folders / platforms the server serves" echo " platforms ?-remote? local platform folders / platforms the server serves"
echo " help full help (options, env vars, examples)" echo " help full help (options, env vars, examples)"
} }
@ -269,6 +270,17 @@ show_help() {
echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole"
echo " installed candidate. Takes no -platform (foreign binaries are" echo " installed candidate. Takes no -platform (foreign binaries are"
echo " not runnable here)." echo " not runnable here)."
echo " info <name-or-path> ?-platform <p>?"
echo " Identity report for a runtime file (G-117 schema v1): the record"
echo " EMBEDDED in its attached image and the sidecar toml side by side,"
echo " with disagreements flagged and the file's sha1 checked against"
echo " the sidecar. The embedded read uses a zip central-directory read"
echo " of the exe-appended archive (unzip) - the target is NEVER"
echo " executed, so it is safe on stray/untrusted binaries; without"
echo " unzip the printed fallback is a cooperative probe that EXECUTES"
echo " the target (only for binaries you already trust). Works for a"
echo " bare renamed copy with no sidecar (embedded-only report);"
echo " pre-v1 artifacts without an embedded record report sidecar-only."
echo " platforms ?-remote?" echo " platforms ?-remote?"
echo " List local platform folders under bin/runtime/. With -remote," echo " List local platform folders under bin/runtime/. With -remote,"
echo " list the platforms the artifact server serves - read from the" echo " list the platforms the artifact server serves - read from the"
@ -720,6 +732,96 @@ case "$action" in
echo "Canonical platform names: 'help platforms' in the punk shell" echo "Canonical platform names: 'help platforms' in the punk shell"
fi fi
;; ;;
"info")
#G-117: identity report - the sidecar toml and the record EMBEDDED in the
#artifact's attached image, side by side, disagreements flagged. Embedded
#read = zip central-directory read of the exe-appended archive via unzip
#(the target is NEVER executed - identifying an untrusted stray binary by
#running it is exactly what this action avoids). Hosts without unzip get
#the cooperative-probe fallback COMMAND printed instead (that one EXECUTES
#the target - only for binaries already trusted).
iname="${1:-}"
if [[ -z "$iname" ]]; then
echo "usage: $0 info <runtime-name-or-path> ?-platform <p>?"
exit 1
fi
if [[ -f "$iname" ]]; then
tfile="$iname"
elif [[ -f "$archdir/$iname" ]]; then
tfile="$archdir/$iname"
else
echo "punk-runtime info: no such file: '$iname' (checked as a path and in $archdir)"
exit 1
fi
tdir=$(dirname "$tfile"); tbase=$(basename "$tfile")
sidecar="$tdir/$(rootname_of "$tbase").toml"
embedded=""
haveunzip=0
if command -v unzip >/dev/null 2>&1; then
haveunzip=1
embedded=$(unzip -p "$tfile" punkbin-artifact.toml 2>/dev/null || true)
fi
localsha1=$(lcase "$(sha1_of "$tfile")")
echo "-----------------------------------------------------------------------"
echo "punk-runtime info: $tbase"
echo " file: $tfile"
[[ -n "$localsha1" ]] && echo " sha1: $localsha1 (computed from the file)"
if [[ -f "$sidecar" ]]; then
echo " sidecar: $sidecar"
else
echo " sidecar: (none beside the file - stray/renamed copy?)"
fi
if [[ $haveunzip -eq 0 ]]; then
echo " embedded: (cannot read - no 'unzip' on this host)"
echo " fallback - a cooperative probe that EXECUTES the target (only run"
echo " this against a binary you already trust):"
echo " echo 'catch {puts [read [open //zipfs:/app/punkbin-artifact.toml]]};exit' | $tfile"
elif [[ -z "$embedded" ]]; then
echo " embedded: (no punkbin-artifact.toml in the attached image - pre-v1"
echo " artifact, or not a punkshell family runtime)"
fi
echo "-----------------------------------------------------------------------"
if [[ -n "$embedded" || -f "$sidecar" ]]; then
printf ' %-20s %-36s %s\n' "field" "embedded" "sidecar"
disagreements=""
for k in schema name class variant working_name revision target build_id origin packager project project_url license build_host_platform tcl_patchlevel sha1 size built; do
ev=""; sv=""
if [[ -n "$embedded" ]]; then
ev=$(printf '%s\n' "$embedded" | sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' | head -n 1)
[[ -z "$ev" ]] && ev=$(printf '%s\n' "$embedded" | sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*\([0-9][0-9]*\|true\|false\).*/\1/p' | head -n 1)
fi
if [[ -f "$sidecar" ]]; then
sv=$(sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$sidecar" | head -n 1)
[[ -z "$sv" ]] && sv=$(sed -n 's/^[[:space:]]*'"$k"'[[:space:]]*=[[:space:]]*\([0-9][0-9]*\|true\|false\).*/\1/p' "$sidecar" | head -n 1)
fi
[[ -z "$ev" && -z "$sv" ]] && continue
printf ' %-20s %-36s %s\n' "$k" "${ev:--}" "${sv:--}"
#schema absent on one side is pre-v1 tolerance, not a disagreement
if [[ -n "$ev" && -n "$sv" && "$ev" != "$sv" ]]; then
disagreements="$disagreements $k"
fi
done
echo "-----------------------------------------------------------------------"
if [[ -n "$disagreements" ]]; then
echo " !DISAGREE embedded-vs-sidecar on:$disagreements"
echo " (the sidecar + sha1sums are the integrity authority; a mismatched pair"
echo " means this sidecar belongs to a different assembly than this binary)"
elif [[ -n "$embedded" && -f "$sidecar" ]]; then
echo " embedded and sidecar records agree on all shared fields"
fi
if [[ -f "$sidecar" && -n "$localsha1" ]]; then
scsha1=$(lcase "$(sed -n 's/^[[:space:]]*sha1[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$sidecar" | head -n 1)")
if [[ -n "$scsha1" ]]; then
if [[ "$scsha1" == "$localsha1" ]]; then
echo " sha1: file matches the sidecar record"
else
echo " !SHA1-MISMATCH: file does not match the sidecar record (modified or"
echo " different assembly; for a materialized working copy compare build_id)"
fi
fi
fi
fi
;;
"help") "help")
show_help show_help
;; ;;

192
src/scriptapps/bin/punk-runtime.ps1

@ -87,11 +87,12 @@ function Get-PunkRuntimeCandidates {
#operator help (the 'help' action; the no-args case shows the Usage block only). #operator help (the 'help' action; the no-args case shows the Usage block only).
#Keep in sync with the bash payload's show_help/show_usage. #Keep in sync with the bash payload's show_help/show_usage.
function Show-PunkRuntimeUsage { function Show-PunkRuntimeUsage {
write-host "Usage: punk-runtime.cmd {fetch|list|use|run|platforms|help}" write-host "Usage: punk-runtime.cmd {fetch|list|use|run|info|platforms|help}"
write-host " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin" write-host " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin"
write-host " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata" write-host " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata"
write-host " use <name> ?-platform <p>? select active / materialize -r<N> artifact" write-host " use <name> ?-platform <p>? select active / materialize -r<N> artifact"
write-host " run ?args...? launch the active local-platform runtime" write-host " run ?args...? launch the active local-platform runtime"
write-host " info <name-or-path> ?-platform <p>? identity report: embedded record vs sidecar toml"
write-host " platforms ?-remote? local platform folders / platforms the server serves" write-host " platforms ?-remote? local platform folders / platforms the server serves"
write-host " help full help (options, env vars, examples)" write-host " help full help (options, env vars, examples)"
} }
@ -130,6 +131,16 @@ function Show-PunkRuntimeHelp {
write-host " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" write-host " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole"
write-host " installed candidate. Takes no -platform (foreign binaries are" write-host " installed candidate. Takes no -platform (foreign binaries are"
write-host " not runnable here)." write-host " not runnable here)."
write-host " info <name-or-path> ?-platform <p>?"
write-host " Identity report for a runtime file (G-117 schema v1): the record"
write-host " EMBEDDED in its attached image and the sidecar toml side by side,"
write-host " with disagreements flagged and the file's sha1 checked against"
write-host " the sidecar. The embedded read uses a zip central-directory read"
write-host " of the exe-appended archive (.NET System.IO.Compression) - the"
write-host " target is NEVER executed, so it is safe on stray/untrusted"
write-host " binaries. Works for a bare renamed copy with no sidecar"
write-host " (embedded-only report); pre-v1 artifacts without an embedded"
write-host " record report sidecar-only."
write-host " platforms ?-remote?" write-host " platforms ?-remote?"
write-host " List local platform folders under bin/runtime/. With -remote," write-host " List local platform folders under bin/runtime/. With -remote,"
write-host " list the platforms the artifact server serves - read from the" write-host " list the platforms the artifact server serves - read from the"
@ -274,6 +285,102 @@ function Get-PunkRuntimeMetadataSummary {
} }
return "[" + ($parts -join " ") + "]" return "[" + ($parts -join " ") + "]"
} }
#G-117 record parsing (schema-tolerant): quoted-string / bare integer / true|false
#value lines only; '#' comments and unknown fields are ignored by construction, and
#an absent schema line simply yields no 'schema' key (pre-v1 records keep working).
function Get-PunkRuntimeRecordFields {
param([string[]] $recordlines)
$fields = @{}
foreach ($line in $recordlines) {
$m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"(.*)"\s*$')
if ($m.Success) {
if (-not $fields.ContainsKey($m.Groups[1].Value)) { $fields[$m.Groups[1].Value] = $m.Groups[2].Value }
continue
}
$m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([0-9]+|true|false)\s*$')
if ($m.Success) {
if (-not $fields.ContainsKey($m.Groups[1].Value)) { $fields[$m.Groups[1].Value] = $m.Groups[2].Value }
}
}
return $fields
}
#G-117 embedded record: zip CENTRAL-DIRECTORY read of the exe-appended archive via
#.NET System.IO.Compression (offsets resolve from the end-of-central-directory
#record, so the prepended executable data is no obstacle). The target is NEVER
#executed - identifying an untrusted stray binary by running it is exactly what an
#identity mechanism should avoid; the cooperative probe (piping a reader script
#into the EXECUTED target) is the documented fallback for hosts without a zip
#reader, with that execution caveat. Returns the record text, or $null when the
#archive has no punkbin-artifact.toml (pre-v1 artifact / not a family runtime).
function Get-PunkRuntimeEmbeddedRecord {
param([string] $Path)
#.NET's ZipArchive does NOT apply the self-extractor offset fixup Info-ZIP
#applies: an exe-appended archive's central-directory offsets are relative to
#the ZIP START, not the file start, and OpenRead on the exe throws. Locate the
#end-of-central-directory record ourselves, compute the prepend length (actual
#CD position minus recorded offset), copy the pure-zip tail to a temp file and
#let .NET read that (deflate/stored handling for free). Works for plain zips
#too (delta 0). No zip64 handling - family images are far below those limits.
try { Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop } catch { }
$fullpath = (Resolve-Path -LiteralPath $Path).ProviderPath
$tmpzip = $null
$zip = $null
try {
$fs = [System.IO.File]::OpenRead($fullpath)
try {
$flen = $fs.Length
if ($flen -lt 22) { return $null }
#EOCD signature scan in the trailing 64KB+22 (max comment length)
$scanlen = [Math]::Min($flen, 65557 + 22)
$fs.Seek($flen - $scanlen, [System.IO.SeekOrigin]::Begin) | Out-Null
$buf = New-Object byte[] $scanlen
$read = 0
while ($read -lt $scanlen) {
$n = $fs.Read($buf, $read, $scanlen - $read)
if ($n -le 0) { break }
$read += $n
}
$eocdrel = -1
for ($i = $read - 22; $i -ge 0; $i--) {
if ($buf[$i] -eq 0x50 -and $buf[$i+1] -eq 0x4b -and $buf[$i+2] -eq 0x05 -and $buf[$i+3] -eq 0x06) {
$eocdrel = $i
break
}
}
if ($eocdrel -lt 0) { return $null }
$eocdabs = ($flen - $scanlen) + $eocdrel
$cdsize = [System.BitConverter]::ToUInt32($buf, $eocdrel + 12)
$cdoffset = [System.BitConverter]::ToUInt32($buf, $eocdrel + 16)
$zipstart = $eocdabs - $cdsize - $cdoffset
if ($zipstart -lt 0) { return $null }
$tmpzip = [System.IO.Path]::GetTempFileName()
$out = [System.IO.File]::Open($tmpzip, [System.IO.FileMode]::Create)
try {
$fs.Seek($zipstart, [System.IO.SeekOrigin]::Begin) | Out-Null
$fs.CopyTo($out)
} finally {
$out.Dispose()
}
} finally {
$fs.Dispose()
}
$zip = [System.IO.Compression.ZipFile]::OpenRead($tmpzip)
foreach ($entry in $zip.Entries) {
if ($entry.FullName -eq 'punkbin-artifact.toml') {
$sr = New-Object System.IO.StreamReader($entry.Open())
try { return $sr.ReadToEnd() } finally { $sr.Dispose() }
}
}
return $null
} catch {
return $null
} finally {
if ($zip) { $zip.Dispose() }
if ($tmpzip -and (Test-Path -LiteralPath $tmpzip)) {
Remove-Item -LiteralPath $tmpzip -Force -ErrorAction SilentlyContinue
}
}
}
function psmain { function psmain {
[CmdletBinding()] [CmdletBinding()]
@ -308,7 +415,7 @@ function psmain {
$paramDictionary.Add('remote', $dynParam1) $paramDictionary.Add('remote', $dynParam1)
$paramDictionary.Add('platform', $dynParam2) $paramDictionary.Add('platform', $dynParam2)
return $paramDictionary return $paramDictionary
} elseif ($action -eq 'fetch' -or $action -eq 'use') { } elseif ($action -eq 'fetch' -or $action -eq 'use' -or $action -eq 'info') {
#GetDynamicParamDictionary ParameterDefinitions #GetDynamicParamDictionary ParameterDefinitions
$parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{
ParameterSetName = "fetchruntime" ParameterSetName = "fetchruntime"
@ -381,7 +488,7 @@ function psmain {
'action' { 'action' {
write-host "got action " $PSBoundParameters.action write-host "got action " $PSBoundParameters.action
Set-Variable -Name $_ -Value $PSBoundParameters."$_" Set-Variable -Name $_ -Value $PSBoundParameters."$_"
$known_actions = @("fetch", "list", "use", "run", "platforms", "help") $known_actions = @("fetch", "list", "use", "run", "info", "platforms", "help")
if (-not($known_actions -contains $action)) { if (-not($known_actions -contains $action)) {
write-host "action '$action' not understood. Known_actions: $known_actions" write-host "action '$action' not understood. Known_actions: $known_actions"
exit 1 exit 1
@ -907,6 +1014,85 @@ function psmain {
} }
} }
} }
'info' {
#G-117: identity report - the sidecar toml and the record EMBEDDED in
#the artifact's attached image, side by side, disagreements flagged.
#Embedded read = zip central-directory read (the target is NEVER
#executed; see Get-PunkRuntimeEmbeddedRecord). Keep output in parity
#with the bash payload's "info" action.
$arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"]
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
$iname = ""
if ( $PSBoundParameters["runtime"].Length ) { $iname = $PSBoundParameters["runtime"] }
if (-not $iname) {
write-host "usage: punk-runtime.cmd info <runtime-name-or-path> ?-platform <p>?"
exit 1
}
$tfile = ""
if (Test-Path -Path $iname -PathType Leaf) {
$tfile = (Resolve-Path -LiteralPath $iname).ProviderPath
} elseif (Test-Path -Path (Join-Path -Path $archfolder -ChildPath $iname) -PathType Leaf) {
$tfile = (Resolve-Path -LiteralPath (Join-Path -Path $archfolder -ChildPath $iname)).ProviderPath
} else {
write-host "punk-runtime info: no such file: '$iname' (checked as a path and in $archfolder)"
exit 1
}
$tbase = Split-Path -Leaf $tfile
$tdir = Split-Path -Parent $tfile
$sidecarfile = Join-Path -Path $tdir -ChildPath ((Get-PunkRuntimeRootName $tbase) + ".toml")
$embedtext = Get-PunkRuntimeEmbeddedRecord $tfile
$localsha1 = Get-PunkFileSha1 -Path $tfile
write-host "-----------------------------------------------------------------------"
write-host "punk-runtime info: $tbase"
write-host " file: $tfile"
write-host " sha1: $localsha1 (computed from the file)"
$havesidecar = Test-Path -Path $sidecarfile -PathType Leaf
if ($havesidecar) {
write-host " sidecar: $sidecarfile"
} else {
write-host " sidecar: (none beside the file - stray/renamed copy?)"
}
if ($null -eq $embedtext) {
write-host " embedded: (no punkbin-artifact.toml in the attached image - pre-v1"
write-host " artifact, or not a punkshell family runtime)"
}
write-host "-----------------------------------------------------------------------"
if (($null -ne $embedtext) -or $havesidecar) {
$efields = @{}
if ($null -ne $embedtext) { $efields = Get-PunkRuntimeRecordFields ($embedtext -split "`r?`n") }
$sfields = @{}
if ($havesidecar) { $sfields = Get-PunkRuntimeRecordFields ([string[]](Get-Content -Path $sidecarfile)) }
write-host (" {0,-20} {1,-36} {2}" -f "field", "embedded", "sidecar")
$disagreements = @()
foreach ($k in @('schema','name','class','variant','working_name','revision','target','build_id','origin','packager','project','project_url','license','build_host_platform','tcl_patchlevel','sha1','size','built')) {
$ev = ""; $sv = ""
if ($efields.ContainsKey($k)) { $ev = $efields[$k] }
if ($sfields.ContainsKey($k)) { $sv = $sfields[$k] }
if (($ev -eq "") -and ($sv -eq "")) { continue }
$evd = "-"; if ($ev -ne "") { $evd = $ev }
$svd = "-"; if ($sv -ne "") { $svd = $sv }
write-host (" {0,-20} {1,-36} {2}" -f $k, $evd, $svd)
#schema absent on one side is pre-v1 tolerance, not a disagreement
if (($ev -ne "") -and ($sv -ne "") -and ($ev -cne $sv)) { $disagreements += $k }
}
write-host "-----------------------------------------------------------------------"
if ($disagreements.Count) {
write-host " !DISAGREE embedded-vs-sidecar on: $($disagreements -join ' ')"
write-host " (the sidecar + sha1sums are the integrity authority; a mismatched pair"
write-host " means this sidecar belongs to a different assembly than this binary)"
} elseif (($null -ne $embedtext) -and $havesidecar) {
write-host " embedded and sidecar records agree on all shared fields"
}
if ($havesidecar -and $sfields.ContainsKey('sha1')) {
if ($sfields['sha1'] -eq $localsha1) {
write-host " sha1: file matches the sidecar record"
} else {
write-host " !SHA1-MISMATCH: file does not match the sidecar record (modified or"
write-host " different assembly; for a materialized working copy compare build_id)"
}
}
}
}
'platforms' { 'platforms' {
#enumerate platform folders: local (bin/runtime/*) and, with #enumerate platform folders: local (bin/runtime/*) and, with
#-remote, the server's platforms.txt discovery manifest (raw-file #-remote, the server's platforms.txt discovery manifest (raw-file

Loading…
Cancel
Save