diff --git a/bin/punk-runtime.cmd b/bin/punk-runtime.cmd index d548299d..8c8f2834 100755 --- a/bin/punk-runtime.cmd +++ b/bin/punk-runtime.cmd @@ -1506,11 +1506,12 @@ metadata_summary() { #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. show_usage() { - echo "Usage: $0 {fetch|list|use|run|platforms|help}" + echo "Usage: $0 {fetch|list|use|run|info|platforms|help}" echo " fetch ?? ?-platform

? download+verify a runtime from punkbin" echo " list ?-remote? ?-platform

? installed (or server) runtimes + metadata" echo " use ?-platform

? select active / materialize -r artifact" echo " run ?args...? launch the active local-platform runtime" + echo " info ?-platform

? identity report: embedded record vs sidecar toml" echo " platforms ?-remote? local platform folders / platforms the server serves" echo " help full help (options, env vars, examples)" } @@ -1549,6 +1550,17 @@ show_help() { echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" echo " installed candidate. Takes no -platform (foreign binaries are" echo " not runnable here)." + echo " info ?-platform

?" + 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 " List local platform folders under bin/runtime/. With -remote," 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" 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 ?-platform

?" + 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") show_help ;; @@ -2401,11 +2503,12 @@ function Get-PunkRuntimeCandidates { #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. 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 ?? ?-platform

? download+verify a runtime from punkbin" write-host " list ?-remote? ?-platform

? installed (or server) runtimes + metadata" write-host " use ?-platform

? select active / materialize -r artifact" write-host " run ?args...? launch the active local-platform runtime" + write-host " info ?-platform

? identity report: embedded record vs sidecar toml" write-host " platforms ?-remote? local platform folders / platforms the server serves" 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 " installed candidate. Takes no -platform (foreign binaries are" write-host " not runnable here)." + write-host " info ?-platform

?" + 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 " List local platform folders under bin/runtime/. With -remote," write-host " list the platforms the artifact server serves - read from the" @@ -2588,6 +2701,102 @@ function Get-PunkRuntimeMetadataSummary { } 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 { [CmdletBinding()] @@ -2622,7 +2831,7 @@ function psmain { $paramDictionary.Add('remote', $dynParam1) $paramDictionary.Add('platform', $dynParam2) return $paramDictionary - } elseif ($action -eq 'fetch' -or $action -eq 'use') { + } elseif ($action -eq 'fetch' -or $action -eq 'use' -or $action -eq 'info') { #GetDynamicParamDictionary ParameterDefinitions $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "fetchruntime" @@ -2695,7 +2904,7 @@ function psmain { 'action' { write-host "got action " $PSBoundParameters.action 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)) { write-host "action '$action' not understood. Known_actions: $known_actions" 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 ?-platform

?" + 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' { #enumerate platform folders: local (bin/runtime/*) and, with #-remote, the server's platforms.txt discovery manifest (raw-file diff --git a/src/scriptapps/bin/punk-runtime.bash b/src/scriptapps/bin/punk-runtime.bash index 83015060..eac1c65f 100644 --- a/src/scriptapps/bin/punk-runtime.bash +++ b/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). #Keep in sync with the ps1 payload's Show-PunkRuntimeHelp/Show-PunkRuntimeUsage. show_usage() { - echo "Usage: $0 {fetch|list|use|run|platforms|help}" + echo "Usage: $0 {fetch|list|use|run|info|platforms|help}" echo " fetch ?? ?-platform

? download+verify a runtime from punkbin" echo " list ?-remote? ?-platform

? installed (or server) runtimes + metadata" echo " use ?-platform

? select active / materialize -r artifact" echo " run ?args...? launch the active local-platform runtime" + echo " info ?-platform

? identity report: embedded record vs sidecar toml" echo " platforms ?-remote? local platform folders / platforms the server serves" echo " help full help (options, env vars, examples)" } @@ -269,6 +270,17 @@ show_help() { echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole" echo " installed candidate. Takes no -platform (foreign binaries are" echo " not runnable here)." + echo " info ?-platform

?" + 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 " List local platform folders under bin/runtime/. With -remote," 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" 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 ?-platform

?" + 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") show_help ;; diff --git a/src/scriptapps/bin/punk-runtime.ps1 b/src/scriptapps/bin/punk-runtime.ps1 index ddcc9d5d..84a95aa7 100644 --- a/src/scriptapps/bin/punk-runtime.ps1 +++ b/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). #Keep in sync with the bash payload's show_help/show_usage. 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 ?? ?-platform

? download+verify a runtime from punkbin" write-host " list ?-remote? ?-platform

? installed (or server) runtimes + metadata" write-host " use ?-platform

? select active / materialize -r artifact" write-host " run ?args...? launch the active local-platform runtime" + write-host " info ?-platform

? identity report: embedded record vs sidecar toml" write-host " platforms ?-remote? local platform folders / platforms the server serves" 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 " installed candidate. Takes no -platform (foreign binaries are" write-host " not runnable here)." + write-host " info ?-platform

?" + 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 " List local platform folders under bin/runtime/. With -remote," write-host " list the platforms the artifact server serves - read from the" @@ -274,6 +285,102 @@ function Get-PunkRuntimeMetadataSummary { } 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 { [CmdletBinding()] @@ -308,7 +415,7 @@ function psmain { $paramDictionary.Add('remote', $dynParam1) $paramDictionary.Add('platform', $dynParam2) return $paramDictionary - } elseif ($action -eq 'fetch' -or $action -eq 'use') { + } elseif ($action -eq 'fetch' -or $action -eq 'use' -or $action -eq 'info') { #GetDynamicParamDictionary ParameterDefinitions $parameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "fetchruntime" @@ -381,7 +488,7 @@ function psmain { 'action' { write-host "got action " $PSBoundParameters.action 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)) { write-host "action '$action' not understood. Known_actions: $known_actions" 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 ?-platform

?" + 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' { #enumerate platform folders: local (bin/runtime/*) and, with #-remote, the server's platforms.txt discovery manifest (raw-file