v0.8.26: harden control-byte sanitize across the tool suite + ssh-helper traps

Shared _sanitize_ctl (unconditional, nc-document) and _sanitize_ctl_tty
(strips only when stdout is a terminal) now live in cygwin-safe.sh. nc-msgs,
nc-parse, and the hl7-* tools route stdout through the tty-gated variant, so a
terminal is protected from raw HL7/NetConfig control bytes while pipes and
redirects stay byte-exact (the 0x1c framing route_test needs is preserved).
Exit codes propagate via PIPESTATUS. ssh-helper _read_hidden installs its
restore trap before stty -echo on every path and saves/restores the prior trap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-05-28 16:35:06 -07:00
parent 9289352454
commit 111be2c744
13 changed files with 253 additions and 38 deletions

View File

@ -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 Versioning is loose-semver; bumps trigger the in-process self-update on every
running client via `LARRY_BASE_URL` + `MANIFEST`. 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 ## v0.8.25 — 2026-05-28
**★ FIX: terminal line-editing corruption after `larry tools …` runs (Bryan, **★ FIX: terminal line-editing corruption after `larry tools …` runs (Bryan,

View File

@ -23,16 +23,16 @@
# scripts/make-manifest.sh and bump VERSION. # scripts/make-manifest.sh and bump VERSION.
# Top-level scripts # Top-level scripts
larry.sh dd9fdba88d20c4472826ef9355a7a0aa3d6bcf1d9d040aff9f07a2bb1351a287 larry.sh 7fccca0d10a0a742d66efd21da703d780c8359411995cf69925123575b14321c
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423 install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423
# Metadata # Metadata
VERSION 7a2e873644ed9f1015114f43072f374eb0e1716bb1a97e73daa1337474d3e1fd VERSION 6520a3a0746d8a2969ca4c76db2109929b36882541fcdbe3fb6de1718903d97f
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
CHANGELOG.md c659506dea3dea5b1cc53ef41f978219d6e9894b4fc4a05df07df660a56f79be CHANGELOG.md 0d7a88d389d6723ee2dd289e5d143d4ada8f232ca0df43c392be9bca856f70b6
# Agent personas (system-prompt overlays) # Agent personas (system-prompt overlays)
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
@ -41,7 +41,7 @@ agents/cloverleaf-cheatsheet.md cd62d57e7ca067b42f1db2dc75a48f1474ae4b742a560250
agents/regress.md bb05ed1439b1e35d6e9799e32d683bfab166472c72115c1f02757e227c74e42f agents/regress.md bb05ed1439b1e35d6e9799e32d683bfab166472c72115c1f02757e227c74e42f
# Cygwin/MobaXterm CR-taint defense primitives (sourced by every tool) # 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- # 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 # 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 lib/oauth.sh 04a93376f88fe53cc1c86a5dbe577735c60375dadd4f2fda55b921ef3cddf22b
# Secure SSH with ControlMaster (password hidden from Larry-the-LLM) # 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, # 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 # 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 lib/journal.sh 11c62a2d47b6b67a2f423fd8b86c454126df18d2dc3e150233bbd08293e39fe7
# HL7 utilities # HL7 utilities
lib/hl7-sanitize.sh 6c7d068e0f8538683074c11cf3350868021e9c0f1823f26bf83afdc285d5dc75 lib/hl7-sanitize.sh c0ea35d28c32dcbb1476835a6e58c2ecdbd04f0a479b889675724fc564f4205f
lib/hl7-desanitize.sh d43e29eefde170cdee64b31383d32ccc995773eec9ccad26a18d4cf2270e58f5 lib/hl7-desanitize.sh 2e5462a61ab1e8bd3fefb956bace8ca1ae33397a09024cbe766fa55c37a5aad6
lib/hl7-diff.sh 162ad0e2ed2cd0e57f395ed53c4b3aa0d8f094ee08fa648f4724e0bda176f464 lib/hl7-diff.sh 66985afb3073340f1c12b0d7b39f41a5d8df68dfebc89c55190d6915f6077e86
lib/hl7-field.sh e70b032b6f3d7056fe77a564dafb1025c0feae4eaf596fb7cf315893442c1d42 lib/hl7-field.sh a640f7cbd9521dc96171ee1dbdf909170262101a1d7a433f6f0ce2bea8d42b02
lib/hl7-schema.sh 2ba4057a214867ff4950f10057ee4ffd7149e1a82ba94b07b6857d77bf10d75f lib/hl7-schema.sh 2ba4057a214867ff4950f10057ee4ffd7149e1a82ba94b07b6857d77bf10d75f
# v0.8.2: Microsoft Presidio sidecar (optional, opt-in install). # 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-smat-diff.sh ac003954701ea6b7f4aa1f6941f8536af5b5cdfbb75e306789753d453f06800e
lib/nc-create-thread.sh 5a9d5407c117183cad831d6b95f0e785b1b806f5ccc67f803c12b3695882b5b7 lib/nc-create-thread.sh 5a9d5407c117183cad831d6b95f0e785b1b806f5ccc67f803c12b3695882b5b7
lib/nc-tclgen.sh dc95f523d543192fc7b3ae204107ce67ebb9b7e5184fa0642a1af2e2454d3241 lib/nc-tclgen.sh dc95f523d543192fc7b3ae204107ce67ebb9b7e5184fa0642a1af2e2454d3241
lib/nc-parse.sh ab06df8264983a9c490af25bf20e1551a91e68b45a9ec24c6cb0fce1f1b9dd69 lib/nc-parse.sh 3419b3f8d0cfdaf767f91551d6e2441d0743d80bd31515ffa61c769db1542c2f
lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b
lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79 lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79
lib/nc-msgs.sh 729e2d6c9159e83fa177fc6b982e48ed8453a9743477cc90afdd3cd4ec7e620c lib/nc-msgs.sh 20517922d1153ec7827c833987497fb305d087b579911d1b9067d65ae156a19f
lib/nc-document.sh 13aef8f6335ee63966fdd74678c506fb4ed7ed749f750e7daad142258e518f41 lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880
lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324 lib/nc-diff-interface.sh 6b64ec3070a3d75d1d79632c9aeb357177fdbcc77c474aa78e6f6929fda1a324
lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f lib/nc-find.sh 8c79e0acad7de56e4e1f12d61e071a4b98c4e2310a1f7fb183697df521215e3f
lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a lib/nc-insert-protocol.sh ad1fa0bafbf4fdfb12bad20f9c22c3eed519f8846774331e26aa9becd6f8898a

View File

@ -1 +1 @@
0.8.25 0.8.26

View File

@ -78,7 +78,7 @@ set -o pipefail
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Config # Config
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.8.25" LARRY_VERSION="0.8.26"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────

View File

@ -130,3 +130,50 @@ read_clean() {
printf -v "$_var" '%s' "$_raw" printf -v "$_var" '%s' "$_raw"
return $_rc 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
}

View File

@ -21,6 +21,19 @@ set -o pipefail
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
DEFAULT_TABLE="$LARRY_HOME/sanitize/lookup.tsv" 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; } die() { printf 'hl7-desanitize: %s\n' "$*" >&2; exit 1; }
table="$DEFAULT_TABLE" table="$DEFAULT_TABLE"
@ -41,8 +54,8 @@ done
[ -f "$table" ] || die "no lookup table at $table (sanitize first?)" [ -f "$table" ] || die "no lookup table at $table (sanitize first?)"
if [ -n "$single_token" ]; then 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" 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 $? exit "${PIPESTATUS[0]}"
fi fi
# Build sed expression set from lookup table # Build sed expression set from lookup table
@ -80,7 +93,9 @@ NR == FNR {
' '
if [ -n "$input_file" ]; then 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 else
awk -F'\t' "$awk_script" "$table" /dev/stdin awk -F'\t' "$awk_script" "$table" /dev/stdin | _sanitize_ctl_tty
exit "${PIPESTATUS[0]}"
fi fi

View File

@ -28,6 +28,18 @@ set -o pipefail
NC_SELF="$0" NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)" 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; } die() { printf 'hl7-diff: %s\n' "$*" >&2; exit 2; }
IGNORE="MSH.7" IGNORE="MSH.7"
@ -89,6 +101,8 @@ trap 'rm -f "$TMP_L" "$TMP_R"' EXIT
split_messages "$LEFT" "$TMP_L" split_messages "$LEFT" "$TMP_L"
split_messages "$RIGHT" "$TMP_R" 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" \ awk -v IGNORE="$IGNORE" -v INCLUDE="$INCLUDE" -v FMT="$FORMAT" \
-v LFILE="$LEFT" -v RFILE="$RIGHT" ' -v LFILE="$LEFT" -v RFILE="$RIGHT" '
function ignored(seg, field, comp, subc, key, key2) { 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 if (FMT == "text") printf "\n%d total field difference(s)\n", DIFF_COUNT
exit (DIFF_COUNT > 0 ? 1 : 0) exit (DIFF_COUNT > 0 ? 1 : 0)
} }
' RS=$'\x1e' "$TMP_L" "$TMP_R" ' RS=$'\x1e' "$TMP_L" "$TMP_R" | _sanitize_ctl_tty
exit "${PIPESTATUS[0]}"

View File

@ -23,6 +23,21 @@
# Exit codes: 0 = found (any number of values printed), 2 = bad path, 3 = not found. # Exit codes: 0 = found (any number of values printed), 2 = bad path, 3 = not found.
set -u 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; } usage() { sed -n '2,20p' "$0"; exit 0; }
PATH_SPEC="${1:-}" PATH_SPEC="${1:-}"
@ -209,3 +224,5 @@ if [ -n "$FIELD_VAL" ]; then
} }
' '
fi fi
} | _sanitize_ctl_tty
exit "${PIPESTATUS[0]}"

View File

@ -29,10 +29,23 @@
set -o pipefail set -o pipefail
NC_SELF="$0" NC_SELF="$0"
NC_LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
TABLE_DIR="$LARRY_HOME/sanitize" TABLE_DIR="$LARRY_HOME/sanitize"
DEFAULT_TABLE="$TABLE_DIR/lookup.tsv" 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; } die() { printf 'hl7-sanitize: %s\n' "$*" >&2; exit 1; }
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -455,6 +468,12 @@ AWK_END
# Dispatch # Dispatch
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
SUB="${1:-}" 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 case "$SUB" in
show-rules) shift; cmd_show_rules ;; show-rules) shift; cmd_show_rules ;;
show-table) shift; cmd_show_table "$@" ;; show-table) shift; cmd_show_table "$@" ;;
@ -514,3 +533,5 @@ case "$SUB" in
do_sanitize "$input_file" "$rules" "$table" "$strict" "$update_table" do_sanitize "$input_file" "$rules" "$table" "$strict" "$update_table"
;; ;;
esac esac
} | _sanitize_ctl_tty
exit "${PIPESTATUS[0]}"

View File

@ -98,6 +98,20 @@ LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
NCP="$LIB_DIR/nc-parse.sh" NCP="$LIB_DIR/nc-parse.sh"
NCPATHS="$LIB_DIR/nc-paths.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; } die() { printf 'nc-document: %s\n' "$*" >&2; exit 1; }
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -250,27 +264,20 @@ _locate_thread() {
# Output sink # 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 # 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 # 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 # 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 # (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), # different mode and wrecks line editing (backspace prints ^H, arrows die),
# forcing `stty sane`/`reset` to recover. We neutralise it at the single output # forcing `stty sane`/`reset` to recover.
# choke-point so EVERY emitted byte is clean, whether bound for stdout or --out.
# #
# Strip set (octal, POSIX `tr` ranges — portable to AIX/Linux/BSD/Cygwin): # We neutralise it at the single output choke-point UNCONDITIONALLY — even when
# \001-\010 SOH..BS (drops BS ^H, the literal-backspace culprit) # redirected to a .txt for OneNote, this is a human-readable document and the
# [keep \011 TAB, \012 LF] # user does NOT want control bytes. So out_target() calls `_sanitize_ctl` (the
# \013\014 VT, FF # always-strip variant from lib/cygwin-safe.sh), NOT the tty-gated variant the
# [keep \015 CR — legit in MobaXterm/Windows-tainted content] # raw-data tools use. The full strip-set rationale lives with the definition in
# \016-\037 SO..US (drops ESC 0x1B, the mode-flip culprit) # lib/cygwin-safe.sh; behavior here is identical to v0.8.25.
# \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
}
out_target() { out_target() {
if [ -n "$OUT" ]; then if [ -n "$OUT" ]; then
mkdir -p "$(dirname "$OUT")" 2>/dev/null mkdir -p "$(dirname "$OUT")" 2>/dev/null

View File

@ -44,6 +44,9 @@ if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
. "$LIB_DIR/cygwin-safe.sh" . "$LIB_DIR/cygwin-safe.sh"
else else
coerce_int() { local r="${1:-}" d="${2:-0}" c; c=$(printf '%s' "$r" | tr -cd '0-9'); printf '%s' "${c:-$d}"; } 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 fi
die() { printf 'nc-msgs: %s\n' "$*" >&2; exit 1; } die() { printf 'nc-msgs: %s\n' "$*" >&2; exit 1; }
@ -416,6 +419,13 @@ match_filters() {
} }
# Emit # 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 case "$FORMAT" in
count) count)
# Apply filter if ANY group has entries # Apply filter if ANY group has entries
@ -559,3 +569,7 @@ case "$FORMAT" in
printf ']\n' printf ']\n'
;; ;;
esac 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]}"

View File

@ -39,6 +39,19 @@ set -u
set -o pipefail set -o pipefail
NC_SELF="$0" 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; } die() { printf 'nc-parse: %s\n' "$*" >&2; exit 1; }
@ -499,6 +512,11 @@ cmd_help() { sed -n '2,30p' "$NC_SELF"; }
# Dispatch # Dispatch
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
SUB="${1:-help}" 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 case "$SUB" in
list-protocols) [ $# -ge 2 ] || die "usage: $0 list-protocols <netconfig>"; cmd_list_protocols "$2" ;; list-protocols) [ $# -ge 2 ] || die "usage: $0 list-protocols <netconfig>"; cmd_list_protocols "$2" ;;
list-processes) [ $# -ge 2 ] || die "usage: $0 list-processes <netconfig>"; cmd_list_processes "$2" ;; list-processes) [ $# -ge 2 ] || die "usage: $0 list-processes <netconfig>"; cmd_list_processes "$2" ;;
@ -517,3 +535,5 @@ case "$SUB" in
help|-h|--help) cmd_help ;; help|-h|--help) cmd_help ;;
*) die "unknown subcommand: $SUB (try '$0 help')" ;; *) die "unknown subcommand: $SUB (try '$0 help')" ;;
esac esac
} | _sanitize_ctl_tty
exit "${PIPESTATUS[0]}"

View File

@ -90,12 +90,24 @@ ok() { printf 'ssh-helper: %s\n' "$*"; }
# Sets the named variable to the line read (no echo). Returns 0 always (empty # Sets the named variable to the line read (no echo). Returns 0 always (empty
# input is the caller's "abort" signal). # input is the caller's "abort" signal).
_read_hidden() { # varname _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 if command -v stty >/dev/null 2>&1 && { [ -t 0 ] || [ -e /dev/tty ]; }; then
_rh_saved=$(stty -g </dev/tty 2>/dev/null || stty -g 2>/dev/null || true) _rh_saved=$(stty -g </dev/tty 2>/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 if [ -n "$_rh_saved" ]; then
trap 'stty "$_rh_saved" </dev/tty 2>/dev/null || stty "$_rh_saved" 2>/dev/null; trap - INT TERM HUP' INT TERM HUP trap 'stty "$_rh_saved" </dev/tty 2>/dev/null || stty "$_rh_saved" 2>/dev/null' INT TERM HUP
else
trap 'stty echo </dev/tty 2>/dev/null || stty echo 2>/dev/null' INT TERM HUP
fi fi
stty -echo </dev/tty 2>/dev/null || stty -echo 2>/dev/null || true stty -echo </dev/tty 2>/dev/null || stty -echo 2>/dev/null || true
IFS= read -r _rh_val </dev/tty 2>/dev/null || IFS= read -r _rh_val || true IFS= read -r _rh_val </dev/tty 2>/dev/null || IFS= read -r _rh_val || true
@ -104,7 +116,12 @@ _read_hidden() { # varname
else else
stty echo </dev/tty 2>/dev/null || stty echo 2>/dev/null || true stty echo </dev/tty 2>/dev/null || stty echo 2>/dev/null || true
fi 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 else
IFS= read -r _rh_val </dev/tty 2>/dev/null || IFS= read -r _rh_val || true IFS= read -r _rh_val </dev/tty 2>/dev/null || IFS= read -r _rh_val || true
fi fi