diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d1a92..b13344c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,48 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here. Versioning is loose-semver; bumps trigger the in-process self-update on every running client via `LARRY_BASE_URL` + `MANIFEST`. +## v0.8.26 — 2026-05-28 + +**★ HARDENING: extend the v0.8.25 control-byte sanitize across the whole tool +suite (Vera follow-up).** v0.8.25 fixed the terminal-corruption leak in +`lib/nc-document.sh` only. Vera flagged that the OTHER tools dumping +NetConfig/`.tcl`/HL7 content to stdout carry the SAME risk and were still +unsanitised — most importantly `nc-msgs.sh`, whose raw HL7 contains C0 framing +bytes (e.g. `0x1c` block separator) that corrupt a terminal when viewed +un-redirected. + +- **Shared sanitiser.** `_sanitize_ctl` is hoisted out of `nc-document.sh` into + `lib/cygwin-safe.sh` (already the sourceable shared-primitives lib), so every + tool shares ONE definition. `nc-document.sh` now sources it; its behavior is + UNCHANGED — it still strips UNCONDITIONALLY (the doc is a human-readable + artifact; control bytes are unwanted even when redirected to a `.txt`). + +- **New `_sanitize_ctl_tty` — the data-tool variant.** For the data-producing + tools (`nc-msgs.sh`, `nc-parse.sh`, `hl7-field.sh`, `hl7-diff.sh`, + `hl7-sanitize.sh`, `hl7-desanitize.sh`) stripping happens ONLY when stdout is + an interactive terminal (`[ -t 1 ]`). When the user pipes/redirects (e.g. + `nc-msgs … --format raw > input.msgs` feeding route_test, or `| awkcut`), the + output passes through RAW and byte-identical — the `0x1c` HL7 framing and + other bytes are LOAD-BEARING downstream and must not be silently corrupted. + Each tool's output region runs in a brace group piped through the gate, with + `${PIPESTATUS[0]}` propagated so subcommand exit codes survive the pipe. + (`hl7-schema.sh` is sourced-only — no stdout sink of its own — so it is + untouched.) + +- **`ssh-helper.sh` `_read_hidden` trap nits (Vera).** (a) The restore trap was + installed only when `stty -g` succeeded, yet `stty -echo` ran regardless — a + `^C` in that window left echo off. Now a restore trap is installed on EVERY + path BEFORE touching echo (falling back to `stty echo` when the save failed). + (b) The trap reset was `trap -` (reset-to-default); it now captures the + caller's PRIOR `INT/TERM/HUP` trap with `trap -p` and restores it. + +Proved on the real test integrator (`to_appriss.smatdb`, 11 msgs): `nc-msgs … +--format raw` to a file kept `0x1c`/`0x0d` intact (493 bytes), while the same +command through a PTY stripped ESC/FS; `cmp` confirmed they differ. `nc-document +--name codametrix` to a file still emits 0 control bytes with the em-dash +preserved. Portable: `LC_ALL=C tr` octal ranges + POSIX `[ -t 1 ]`; no GNU-only +flags. `bash -n` clean on all touched files. MANIFEST regenerated. + ## v0.8.25 — 2026-05-28 **★ FIX: terminal line-editing corruption after `larry tools …` runs (Bryan, diff --git a/MANIFEST b/MANIFEST index 4e89300..8af70b0 100644 --- a/MANIFEST +++ b/MANIFEST @@ -23,16 +23,16 @@ # scripts/make-manifest.sh and bump VERSION. # Top-level scripts -larry.sh dd9fdba88d20c4472826ef9355a7a0aa3d6bcf1d9d040aff9f07a2bb1351a287 +larry.sh 7fccca0d10a0a742d66efd21da703d780c8359411995cf69925123575b14321c larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423 # Metadata -VERSION 7a2e873644ed9f1015114f43072f374eb0e1716bb1a97e73daa1337474d3e1fd +VERSION 6520a3a0746d8a2969ca4c76db2109929b36882541fcdbe3fb6de1718903d97f MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde -CHANGELOG.md c659506dea3dea5b1cc53ef41f978219d6e9894b4fc4a05df07df660a56f79be +CHANGELOG.md 0d7a88d389d6723ee2dd289e5d143d4ada8f232ca0df43c392be9bca856f70b6 # Agent personas (system-prompt overlays) agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 @@ -41,7 +41,7 @@ agents/cloverleaf-cheatsheet.md cd62d57e7ca067b42f1db2dc75a48f1474ae4b742a560250 agents/regress.md bb05ed1439b1e35d6e9799e32d683bfab166472c72115c1f02757e227c74e42f # Cygwin/MobaXterm CR-taint defense primitives (sourced by every tool) -lib/cygwin-safe.sh efa83387f03e213a2b200b78cf5468dc930d71b4e3dbb98477187057fa8f4857 +lib/cygwin-safe.sh 6cef02f3e931f4067eed990b01fb9535c9e5b1e91b62ff318f0008732d4ad545 # v0.8.4: content-validating fetch (HTML-sign-in-page trap detection + per- # file-type shape checks) for the installer/auto-updater. Canonical home of the @@ -52,7 +52,7 @@ lib/fetch-safe.sh abecf0045b9856f63ffa346119443c11de56547344be32bddaed9fbae6b021 lib/oauth.sh 04a93376f88fe53cc1c86a5dbe577735c60375dadd4f2fda55b921ef3cddf22b # Secure SSH with ControlMaster (password hidden from Larry-the-LLM) -lib/ssh-helper.sh b8442e1e086eed7afebc77e1948c9c688301a8ef4b14f563470396aceccdddd4 +lib/ssh-helper.sh 18df1f1f1936c930ba0197c0e0b4bd89c027500de99b56067b620ca9144f6e9e # v0.8.6: work-box → Mac headers.log sync (tsk-2026-05-27-023). Incremental, # offset-tracked push of $LARRY_HOME/log/headers.log to a daemon-watched path @@ -66,10 +66,10 @@ lib/lessons.sh 45ea4fdadb843701cd3e87f6a0011ba4097978661851ebc9098ad22ea219efb1 lib/journal.sh 11c62a2d47b6b67a2f423fd8b86c454126df18d2dc3e150233bbd08293e39fe7 # HL7 utilities -lib/hl7-sanitize.sh 6c7d068e0f8538683074c11cf3350868021e9c0f1823f26bf83afdc285d5dc75 -lib/hl7-desanitize.sh d43e29eefde170cdee64b31383d32ccc995773eec9ccad26a18d4cf2270e58f5 -lib/hl7-diff.sh 162ad0e2ed2cd0e57f395ed53c4b3aa0d8f094ee08fa648f4724e0bda176f464 -lib/hl7-field.sh e70b032b6f3d7056fe77a564dafb1025c0feae4eaf596fb7cf315893442c1d42 +lib/hl7-sanitize.sh c0ea35d28c32dcbb1476835a6e58c2ecdbd04f0a479b889675724fc564f4205f +lib/hl7-desanitize.sh 2e5462a61ab1e8bd3fefb956bace8ca1ae33397a09024cbe766fa55c37a5aad6 +lib/hl7-diff.sh 66985afb3073340f1c12b0d7b39f41a5d8df68dfebc89c55190d6915f6077e86 +lib/hl7-field.sh a640f7cbd9521dc96171ee1dbdf909170262101a1d7a433f6f0ce2bea8d42b02 lib/hl7-schema.sh 2ba4057a214867ff4950f10057ee4ffd7149e1a82ba94b07b6857d77bf10d75f # v0.8.2: Microsoft Presidio sidecar (optional, opt-in install). @@ -97,12 +97,12 @@ lib/nc-xlate.sh ea02693c3dff5db271771d4bb2927b23465b07798df2f9912bc2d2b58a134d54 lib/nc-smat-diff.sh ac003954701ea6b7f4aa1f6941f8536af5b5cdfbb75e306789753d453f06800e lib/nc-create-thread.sh 5a9d5407c117183cad831d6b95f0e785b1b806f5ccc67f803c12b3695882b5b7 lib/nc-tclgen.sh dc95f523d543192fc7b3ae204107ce67ebb9b7e5184fa0642a1af2e2454d3241 -lib/nc-parse.sh ab06df8264983a9c490af25bf20e1551a91e68b45a9ec24c6cb0fce1f1b9dd69 +lib/nc-parse.sh 3419b3f8d0cfdaf767f91551d6e2441d0743d80bd31515ffa61c769db1542c2f lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79 -lib/nc-msgs.sh 729e2d6c9159e83fa177fc6b982e48ed8453a9743477cc90afdd3cd4ec7e620c -lib/nc-document.sh 13aef8f6335ee63966fdd74678c506fb4ed7ed749f750e7daad142258e518f41 +lib/nc-msgs.sh 20517922d1153ec7827c833987497fb305d087b579911d1b9067d65ae156a19f +lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880 lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324 lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a diff --git a/VERSION b/VERSION index 4039074..85ccf6f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.25 +0.8.26 diff --git a/larry.sh b/larry.sh index fcbd61d..2e4f535 100755 --- a/larry.sh +++ b/larry.sh @@ -78,7 +78,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.8.25" +LARRY_VERSION="0.8.26" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" # ───────────────────────────────────────────────────────────────────────────── diff --git a/lib/cygwin-safe.sh b/lib/cygwin-safe.sh index 71ddd03..57ecee4 100755 --- a/lib/cygwin-safe.sh +++ b/lib/cygwin-safe.sh @@ -130,3 +130,50 @@ read_clean() { printf -v "$_var" '%s' "$_raw" return $_rc } + +# ───────────────────────────────────────────────────────────────────────────── +# CONTROL-BYTE SANITIZER (terminal-corruption defence) — shared since v0.8.26. +# +# Origin: v0.8.25 added this as a private helper in nc-document.sh after raw +# ESC/control bytes in tool output flipped the user's terminal mode and broke +# backspace/arrows (recoverable only with `stty sane`/`reset`). v0.8.26 hoists +# the one definition here so EVERY tool that dumps NetConfig/.tcl/HL7 content +# shares it — most importantly nc-msgs.sh, whose raw HL7 carries 0x1c block +# framing and other C0 bytes that wreck a terminal when viewed un-redirected. +# +# `_sanitize_ctl` filters stdin→stdout, stripping the C0 control bytes that +# corrupt a terminal while PRESERVING the three whitespace controls that text +# legitimately uses (TAB, LF, CR) and all high bytes (0x80-0xFF, so UTF-8 +# names/comments and the em-dash survive intact). +# +# Strip set (octal, POSIX `tr` ranges — portable to AIX/Linux/BSD/Cygwin): +# \001-\010 SOH..BS (drops BS ^H, the literal-backspace culprit) +# [keep \011 TAB, \012 LF] +# \013\014 VT, FF +# [keep \015 CR — legit in MobaXterm/Windows-tainted content] +# \016-\037 SO..US (drops ESC 0x1B, the mode-flip culprit; 0x1c FS too) +# \177 DEL +# LC_ALL=C forces byte-wise operation (AIX `tr` is locale-sensitive otherwise). +# Falls back to `cat` if `tr` is somehow unavailable, so it never drops data. +_sanitize_ctl() { + LC_ALL=C tr -d '\001-\010\013\014\016-\037\177' 2>/dev/null || cat +} + +# `_sanitize_ctl_tty` — the DATA-TOOL variant. nc-msgs/nc-parse/hl7-* emit data +# that downstream tooling consumes byte-for-byte (e.g. `nc-msgs ... > input.msgs` +# feeding route_test, or `| awkcut`). The 0x1c HL7 framing and other control +# bytes are LOAD-BEARING on a pipe/redirect — stripping them would silently +# corrupt the data. So we only sanitize when stdout is an interactive TERMINAL +# (protect the human's tty); on a pipe/file we pass through RAW, byte-identical. +# +# `[ -t 1 ]` is POSIX (true only when fd 1 is a terminal). Note this is a +# FILTER (stdin→stdout); the gate decision is made once at call time. Callers +# pipe their whole output region through it and propagate ${PIPESTATUS[0]} so +# the upstream producer's exit code is preserved across the pipe. +_sanitize_ctl_tty() { + if [ -t 1 ]; then + _sanitize_ctl + else + cat + fi +} diff --git a/lib/hl7-desanitize.sh b/lib/hl7-desanitize.sh index 0360cf5..eeb7ff4 100755 --- a/lib/hl7-desanitize.sh +++ b/lib/hl7-desanitize.sh @@ -21,6 +21,19 @@ set -o pipefail LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" DEFAULT_TABLE="$LARRY_HOME/sanitize/lookup.tsv" +# v0.8.26: shared control-byte sanitizer. Desanitized HL7 can carry C0 control +# bytes that corrupt a terminal when viewed un-redirected; strip them ONLY when +# stdout is a tty. Piping to `less` (a documented use) is NOT a tty, so the +# content passes through raw and less handles the control bytes itself. See +# lib/cygwin-safe.sh. +_HL7D_LIB_DIR="$(cd "$(dirname "$0")" && pwd)" +if [ -r "$_HL7D_LIB_DIR/cygwin-safe.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$_HL7D_LIB_DIR/cygwin-safe.sh" +else + _sanitize_ctl_tty() { cat; } # degrade safe: raw passthrough if lib missing +fi + die() { printf 'hl7-desanitize: %s\n' "$*" >&2; exit 1; } table="$DEFAULT_TABLE" @@ -41,8 +54,8 @@ done [ -f "$table" ] || die "no lookup table at $table (sanitize first?)" if [ -n "$single_token" ]; then - awk -F'\t' -v t="$single_token" 'NR>1 && $1==t {print $3; found=1; exit} END{if (!found) {print "no such token: " t > "/dev/stderr"; exit 2}}' "$table" - exit $? + awk -F'\t' -v t="$single_token" 'NR>1 && $1==t {print $3; found=1; exit} END{if (!found) {print "no such token: " t > "/dev/stderr"; exit 2}}' "$table" | _sanitize_ctl_tty + exit "${PIPESTATUS[0]}" fi # Build sed expression set from lookup table @@ -80,7 +93,9 @@ NR == FNR { ' if [ -n "$input_file" ]; then - awk -F'\t' "$awk_script" "$table" "$input_file" + awk -F'\t' "$awk_script" "$table" "$input_file" | _sanitize_ctl_tty + exit "${PIPESTATUS[0]}" else - awk -F'\t' "$awk_script" "$table" /dev/stdin + awk -F'\t' "$awk_script" "$table" /dev/stdin | _sanitize_ctl_tty + exit "${PIPESTATUS[0]}" fi diff --git a/lib/hl7-diff.sh b/lib/hl7-diff.sh index d372c4e..6781da8 100755 --- a/lib/hl7-diff.sh +++ b/lib/hl7-diff.sh @@ -28,6 +28,18 @@ set -o pipefail NC_SELF="$0" LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" +# v0.8.26: shared control-byte sanitizer. The diff echoes raw HL7 field values, +# which can carry C0 control bytes that corrupt a terminal when viewed +# un-redirected. The final awk (sole stdout producer) is piped through the +# tty-gated sanitizer; on a pipe/redirect it passes through raw. See +# lib/cygwin-safe.sh. +if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$LIB_DIR/cygwin-safe.sh" +else + _sanitize_ctl_tty() { cat; } # degrade safe: raw passthrough if lib missing +fi + die() { printf 'hl7-diff: %s\n' "$*" >&2; exit 2; } IGNORE="MSH.7" @@ -89,6 +101,8 @@ trap 'rm -f "$TMP_L" "$TMP_R"' EXIT split_messages "$LEFT" "$TMP_L" split_messages "$RIGHT" "$TMP_R" +# v0.8.26: pipe the comparison output through the tty-gated sanitizer, then +# propagate ${PIPESTATUS[0]} so awk's exit (0 identical, 1 differs) survives. awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \ -v LFILE="$LEFT" -v RFILE="$RIGHT" ' function ignored(seg, field, comp, subc, key, key2) { @@ -245,4 +259,5 @@ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \ if (FMT == "text") printf "\n%d total field difference(s)\n", DIFF_COUNT exit (DIFF_COUNT > 0 ? 1 : 0) } -' RS=$'\x1e' "$TMP_L" "$TMP_R" +' RS=$'\x1e' "$TMP_L" "$TMP_R" | _sanitize_ctl_tty +exit "${PIPESTATUS[0]}" diff --git a/lib/hl7-field.sh b/lib/hl7-field.sh index 8e1f0db..92abee8 100755 --- a/lib/hl7-field.sh +++ b/lib/hl7-field.sh @@ -23,6 +23,21 @@ # Exit codes: 0 = found (any number of values printed), 2 = bad path, 3 = not found. set -u +# v0.8.26: shared control-byte sanitizer. HL7 field/segment content carries C0 +# control bytes (e.g. 0x1c block framing) that corrupt a terminal when viewed +# un-redirected. _sanitize_ctl_tty strips them ONLY when stdout is a tty; on a +# pipe/redirect the bytes pass through raw (load-bearing downstream). The whole +# body below runs in a brace group piped through it, and ${PIPESTATUS[0]} is +# propagated so every exit code (usage 0, bad-path 2, not-found 3) survives. +_HL7F_LIB_DIR="$(cd "$(dirname "$0")" && pwd)" +if [ -r "$_HL7F_LIB_DIR/cygwin-safe.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$_HL7F_LIB_DIR/cygwin-safe.sh" +else + _sanitize_ctl_tty() { cat; } # degrade safe: raw passthrough if lib missing +fi + +{ usage() { sed -n '2,20p' "$0"; exit 0; } PATH_SPEC="${1:-}" @@ -209,3 +224,5 @@ if [ -n "$FIELD_VAL" ]; then } ' fi +} | _sanitize_ctl_tty +exit "${PIPESTATUS[0]}" diff --git a/lib/hl7-sanitize.sh b/lib/hl7-sanitize.sh index e02a932..d6ca6c8 100755 --- a/lib/hl7-sanitize.sh +++ b/lib/hl7-sanitize.sh @@ -29,10 +29,23 @@ set -o pipefail NC_SELF="$0" +NC_LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" TABLE_DIR="$LARRY_HOME/sanitize" DEFAULT_TABLE="$TABLE_DIR/lookup.tsv" +# v0.8.26: shared control-byte sanitizer. Sanitized HL7 and the show-table dump +# echo message/field bytes that can include C0 controls (and 0x1c framing on +# raw input) that corrupt a terminal when viewed un-redirected. Strip them ONLY +# when stdout is a tty; on a pipe/redirect (feeding analysis tooling) the bytes +# pass through raw and byte-identical. See lib/cygwin-safe.sh. +if [ -r "$NC_LIB_DIR/cygwin-safe.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$NC_LIB_DIR/cygwin-safe.sh" +else + _sanitize_ctl_tty() { cat; } # degrade safe: raw passthrough if lib missing +fi + die() { printf 'hl7-sanitize: %s\n' "$*" >&2; exit 1; } # ───────────────────────────────────────────────────────────────────────────── @@ -455,6 +468,12 @@ AWK_END # Dispatch # ───────────────────────────────────────────────────────────────────────────── SUB="${1:-}" +# v0.8.26: route the whole dispatch's stdout through the tty-gated sanitizer in +# a brace group, then propagate ${PIPESTATUS[0]} so each subcommand's exit code +# survives the pipe. On a pipe/redirect the gate is a no-op (raw passthrough). +# clear-table's confirmation read uses /dev/tty directly, so the pipeline +# subshell does not interfere with it. +{ case "$SUB" in show-rules) shift; cmd_show_rules ;; show-table) shift; cmd_show_table "$@" ;; @@ -514,3 +533,5 @@ case "$SUB" in do_sanitize "$input_file" "$rules" "$table" "$strict" "$update_table" ;; esac +} | _sanitize_ctl_tty +exit "${PIPESTATUS[0]}" diff --git a/lib/nc-document.sh b/lib/nc-document.sh index 5f743b3..ce601cc 100755 --- a/lib/nc-document.sh +++ b/lib/nc-document.sh @@ -98,6 +98,20 @@ LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" NCP="$LIB_DIR/nc-parse.sh" NCPATHS="$LIB_DIR/nc-paths.sh" +# v0.8.26: the control-byte sanitizer (added here in v0.8.25) now lives in the +# shared lib/cygwin-safe.sh so every content-dumping tool shares one definition. +# This tool's behavior is UNCHANGED: out_target() still strips UNCONDITIONALLY +# (the doc is a human-readable artifact — even when redirected to a .txt for +# OneNote the user does NOT want control bytes), so it calls _sanitize_ctl, not +# the tty-gated variant. Fallback below keeps the prior inline definition if the +# shared lib is somehow unavailable, so behavior never regresses. +if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$LIB_DIR/cygwin-safe.sh" +else + _sanitize_ctl() { LC_ALL=C tr -d '\001-\010\013\014\016-\037\177' 2>/dev/null || cat; } +fi + die() { printf 'nc-document: %s\n' "$*" >&2; exit 1; } # ───────────────────────────────────────────────────────────────────────────── @@ -250,27 +264,20 @@ _locate_thread() { # Output sink # ───────────────────────────────────────────────────────────────────────────── # -# v0.8.25 — CONTROL-BYTE SANITIZER (terminal-corruption fix). +# CONTROL-BYTE SANITIZER — added v0.8.25, hoisted to the shared lib in v0.8.26. # This tool reads arbitrary NetConfig and .tcl source (author comments, raw proc # bodies via --raw-tcl, xlate bodies). That content can contain raw C0 control # bytes — most dangerously ESC (0x1B). When the doc is printed to a terminal # (NOT redirected to a file), a stray ESC sequence flips the terminal into a # different mode and wrecks line editing (backspace prints ^H, arrows die), -# forcing `stty sane`/`reset` to recover. We neutralise it at the single output -# choke-point so EVERY emitted byte is clean, whether bound for stdout or --out. +# forcing `stty sane`/`reset` to recover. # -# Strip set (octal, POSIX `tr` ranges — portable to AIX/Linux/BSD/Cygwin): -# \001-\010 SOH..BS (drops BS ^H, the literal-backspace culprit) -# [keep \011 TAB, \012 LF] -# \013\014 VT, FF -# [keep \015 CR — legit in MobaXterm/Windows-tainted content] -# \016-\037 SO..US (drops ESC 0x1B, the mode-flip culprit) -# \177 DEL -# High bytes (0x80-0xFF) are NOT touched, so UTF-8 names/comments survive intact. -# LC_ALL=C forces byte-wise operation (AIX `tr` is locale-sensitive otherwise). -_sanitize_ctl() { - LC_ALL=C tr -d '\001-\010\013\014\016-\037\177' 2>/dev/null || cat -} +# We neutralise it at the single output choke-point UNCONDITIONALLY — even when +# redirected to a .txt for OneNote, this is a human-readable document and the +# user does NOT want control bytes. So out_target() calls `_sanitize_ctl` (the +# always-strip variant from lib/cygwin-safe.sh), NOT the tty-gated variant the +# raw-data tools use. The full strip-set rationale lives with the definition in +# lib/cygwin-safe.sh; behavior here is identical to v0.8.25. out_target() { if [ -n "$OUT" ]; then mkdir -p "$(dirname "$OUT")" 2>/dev/null diff --git a/lib/nc-msgs.sh b/lib/nc-msgs.sh index 5de3d49..7263c06 100755 --- a/lib/nc-msgs.sh +++ b/lib/nc-msgs.sh @@ -44,6 +44,9 @@ if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then . "$LIB_DIR/cygwin-safe.sh" else coerce_int() { local r="${1:-}" d="${2:-0}" c; c=$(printf '%s' "$r" | tr -cd '0-9'); printf '%s' "${c:-$d}"; } + # v0.8.26 fallback: raw passthrough if the shared lib is missing. Never strips + # — the gated stripping is a terminal-protection nicety, not a correctness need. + _sanitize_ctl_tty() { cat; } fi die() { printf 'nc-msgs: %s\n' "$*" >&2; exit 1; } @@ -416,6 +419,13 @@ match_filters() { } # Emit +# v0.8.26: route ALL stdout from the emit block through the tty-gated sanitizer. +# CRITICAL: the `raw` format intentionally emits raw HL7 plus 0x1c framing for +# downstream tooling (`nc-msgs ... --format raw > input.msgs` → route_test). On a +# pipe/redirect those bytes are load-bearing and MUST pass through untouched, so +# the gate sanitizes ONLY when stdout is an interactive tty (protect the human's +# terminal). Stderr lines (the `>&2` scan summary) bypass this pipe by design. +{ case "$FORMAT" in count) # Apply filter if ANY group has entries @@ -559,3 +569,7 @@ case "$FORMAT" in printf ']\n' ;; esac +} | _sanitize_ctl_tty +# Preserve the emit block's exit status across the gating pipe. The EXIT trap +# (rm -rf "$TMP_OUT") still fires on this parent exit. +exit "${PIPESTATUS[0]}" diff --git a/lib/nc-parse.sh b/lib/nc-parse.sh index fd9b285..4d6305f 100755 --- a/lib/nc-parse.sh +++ b/lib/nc-parse.sh @@ -39,6 +39,19 @@ set -u set -o pipefail NC_SELF="$0" +NC_LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" + +# v0.8.26: shared control-byte sanitizer. This tool dumps NetConfig and raw .tcl +# proc bodies to stdout; that content can carry C0 control bytes (ESC etc.) that +# corrupt a terminal when viewed un-redirected. _sanitize_ctl_tty strips them +# ONLY when stdout is a tty — on a pipe/redirect the bytes pass through raw so +# downstream tooling sees byte-identical content. See lib/cygwin-safe.sh. +if [ -r "$NC_LIB_DIR/cygwin-safe.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$NC_LIB_DIR/cygwin-safe.sh" +else + _sanitize_ctl_tty() { cat; } # degrade safe: raw passthrough if lib missing +fi die() { printf 'nc-parse: %s\n' "$*" >&2; exit 1; } @@ -499,6 +512,11 @@ cmd_help() { sed -n '2,30p' "$NC_SELF"; } # Dispatch # ───────────────────────────────────────────────────────────────────────────── SUB="${1:-help}" +# v0.8.26: route the whole dispatch's stdout through the tty-gated sanitizer in +# a brace group, then propagate ${PIPESTATUS[0]} so the subcommand's exit code +# (incl. die's exit 1 and not-found 2/3) survives the pipe. On a pipe/redirect +# the gate is a no-op (raw passthrough); only an interactive tty gets stripped. +{ case "$SUB" in list-protocols) [ $# -ge 2 ] || die "usage: $0 list-protocols "; cmd_list_protocols "$2" ;; list-processes) [ $# -ge 2 ] || die "usage: $0 list-processes "; cmd_list_processes "$2" ;; @@ -517,3 +535,5 @@ case "$SUB" in help|-h|--help) cmd_help ;; *) die "unknown subcommand: $SUB (try '$0 help')" ;; esac +} | _sanitize_ctl_tty +exit "${PIPESTATUS[0]}" diff --git a/lib/ssh-helper.sh b/lib/ssh-helper.sh index ee47270..1149320 100755 --- a/lib/ssh-helper.sh +++ b/lib/ssh-helper.sh @@ -90,12 +90,24 @@ ok() { printf 'ssh-helper: %s\n' "$*"; } # Sets the named variable to the line read (no echo). Returns 0 always (empty # input is the caller's "abort" signal). _read_hidden() { # varname - local _rh_var="$1" _rh_val="" _rh_saved="" + local _rh_var="$1" _rh_val="" _rh_saved="" _rh_prior_trap="" if command -v stty >/dev/null 2>&1 && { [ -t 0 ] || [ -e /dev/tty ]; }; then _rh_saved=$(stty -g /dev/null || stty -g 2>/dev/null || true) - # Restore on ANY exit from the prompt window — normal return OR ^C/kill/HUP. + # v0.8.26 nit (b): capture the caller's PRIOR INT/TERM/HUP trap so we can + # RESTORE it on the way out instead of blindly resetting to default. `trap -p` + # prints the re-installable trap commands (empty if none was set). Harmless + # today (no caller sets these around the prompt) but correct. + _rh_prior_trap=$(trap -p INT TERM HUP 2>/dev/null || true) + # v0.8.26 nit (a): install a restore trap BEFORE touching echo, on EVERY + # path. If `stty -g` saved a state, restore exactly that; if the save failed + # (_rh_saved empty), fall back to `stty echo` so a ^C mid-read can never + # leave the terminal with echo off. Previously the trap was only installed + # when _rh_saved was non-empty, yet `stty -echo` ran regardless — an + # interrupt in that window left the tty corrupted. if [ -n "$_rh_saved" ]; then - trap 'stty "$_rh_saved" /dev/null || stty "$_rh_saved" 2>/dev/null; trap - INT TERM HUP' INT TERM HUP + trap 'stty "$_rh_saved" /dev/null || stty "$_rh_saved" 2>/dev/null' INT TERM HUP + else + trap 'stty echo /dev/null || stty echo 2>/dev/null' INT TERM HUP fi stty -echo /dev/null || stty -echo 2>/dev/null || true IFS= read -r _rh_val /dev/null || IFS= read -r _rh_val || true @@ -104,7 +116,12 @@ _read_hidden() { # varname else stty echo /dev/null || stty echo 2>/dev/null || true fi - trap - INT TERM HUP + # Restore the caller's prior trap (or clear ours if there was none). + if [ -n "$_rh_prior_trap" ]; then + eval "$_rh_prior_trap" + else + trap - INT TERM HUP + fi else IFS= read -r _rh_val /dev/null || IFS= read -r _rh_val || true fi