cloverleaf-larry/lib/nc-make-jump.sh
bj 9a2ed47785 v0.9.2: fix F-1/F-2/F-3/F-5 — regression false-PASS, PHI leak, jump guard, MRN match
F-1 (HIGH — blocks regression): hl7-diff --format count always returned 0
because the early-exit in END fired before the diff loop ran. Fix: remove
the early exit; suppress per-diff printf in emit() for count mode; emit
DIFF_COUNT after the loop. count/text/tsv all agree (13 diffs on fixture,
0 on identical pair, exit codes correct). Ref: lib/hl7-diff.sh.

F-5 (MEDIUM — PHI leak): hl7-sanitize silently passed LF-delimited HL7
through as cleartext (awk RS="\r" never split on LF). Fix: detect CR
absence via python3 binary read; normalise LF/CRLF→CR via `tr` before
the awk pass. Both file and stdin paths handled. CR path is a zero-overhead
passthrough. Before: 0 tokens, cleartext PHI. After: 6 tokens, all PID
fields replaced with [[MRN_0001]] etc. Ref: lib/hl7-sanitize.sh.

F-2 (MEDIUM): nc-make-jump emitted { PORT {} } for file/ICL inbounds
because the guard only tested for empty ORIG_PORT; protocol-nested returns
the literal "{}" for empty blocks. Fix: case guard rejects empty, "{}", and
any non-numeric value with a clear "is it a TCP listener?" error (exit 1).
TCP inbounds (numeric PORT) still generate correctly. Ref: lib/nc-make-jump.sh.

F-3 (MEDIUM — manual marquee example): nc-msgs mrn=<bare> returned 0 on
real Epic MRNs stored as "5720501458^^^MRN". Fix: in field_matches "="
operator, when expected has no ^ and the stored repetition does, compare
component-1 (text before first ^). Full-componented and mrn.1= paths
unchanged. Fixture: bare mrn=5720501458 now matches 2/3 messages correctly.
Ref: lib/nc-msgs.sh.

All four files pass bash -n. MANIFEST regenerated (54 entries, --check=0).
Tested against synthetic fixtures on .135 (no live engine required for these
logic bugs). Work-box re-verify commands in audit §4-B.

Co-Authored-By: Clover (claude-sonnet-4-6) <noreply@anthropic.com>
2026-06-08 10:52:57 -07:00

433 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
# nc-make-jump.sh — generate the 3-thread jump pattern for cross-environment
# data replay during a Cloverleaf migration. Matches Bryan's house pattern
# (linux_<tag>_out / windows_<tag>_in / windows_<tag>_out).
#
# Topology:
#
# OLD env (e.g. windows) NEW env (linux)
# ── adt site (existing) ── ── server_jump site (new) ── ── adt site (cloned, unchanged) ──
# ┌─────────────────────┐ ┌────────────────────────────┐ ┌─────────────────────────┐
# │ <existing inbound> │ │ windows_<tag>_in (NEW) │ │ <existing inbound> │
# │ ├─→ existing dests │ │ tcpip-server, ISSERVER=1 │ │ listens on ORIG_PORT │
# │ └─→ linux_<tag>_ │ ──TCP──→ │ PORT = jump_port │ │ (UNCHANGED — no route │
# │ out (NEW) │ │ │ internal route │ │ changes here) │
# │ tcpip-client │ │ ▼ │ │ │
# │ → jump_port │ │ windows_<tag>_out (NEW) │ │ ▲ │
# └─────────────────────┘ │ tcpip-client │ │ │ │
# │ HOST=127.0.0.1 │ │ │ TCP localhost │
# │ PORT=ORIG_PORT ─────────┼───────┘ │ ──────────────────── │
# └────────────────────────────┘ │
# │
# (so OLD's existing inbound gets ONE new route; NEW's existing inbound is UNTOUCHED)
#
# Three new protocol blocks:
# 1. `linux_<tag>_out` → add to OLD env NetConfig (same process as original inbound).
# 2. `windows_<tag>_in` → add to NEW env server_jump/NetConfig.
# 3. `windows_<tag>_out` → add to NEW env server_jump/NetConfig.
#
# Plus one route-add snippet for OLD's existing inbound's DATAXLATE block.
#
# Tag = inbound thread name (auto-derived per Bryan's preference).
#
# Usage:
# nc-make-jump.sh <netconfig> --inbound NAME --new-host HOST --jump-port PORT
# [--inbound-host HOST] # default 127.0.0.1
# [--process-jump PROC] # process for NEW-side threads, default server_jump
# [--encoding ENC] # default = ENCODING from existing inbound
# [--out-prefix PREFIX] # write files instead of stdout
set -u
set -o pipefail
NC_SELF="$0"
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
NCP="$LIB_DIR/nc-parse.sh"
die() { printf 'nc-make-jump: %s\n' "$*" >&2; exit 1; }
NC=""
INBOUND=""
NEW_HOST=""
JUMP_PORT=""
INBOUND_HOST="127.0.0.1"
PROC_JUMP="server_jump"
ENC_OVERRIDE=""
OUT_PREFIX=""
while [ $# -gt 0 ]; do
case "$1" in
--inbound) shift; INBOUND="$1" ;;
--new-host) shift; NEW_HOST="$1" ;;
--jump-port) shift; JUMP_PORT="$1" ;;
--inbound-host) shift; INBOUND_HOST="$1" ;;
--process-jump) shift; PROC_JUMP="$1" ;;
--encoding) shift; ENC_OVERRIDE="$1" ;;
--out-prefix) shift; OUT_PREFIX="$1" ;;
-h|--help) sed -n '2,50p' "$NC_SELF"; exit 0 ;;
-*) die "unknown flag: $1" ;;
*) [ -z "$NC" ] && NC="$1" || die "extra arg: $1" ;;
esac
shift
done
[ -n "$NC" ] || die "usage: see --help"
[ -n "$INBOUND" ] || die "missing --inbound"
[ -n "$NEW_HOST" ] || die "missing --new-host"
[ -n "$JUMP_PORT" ] || die "missing --jump-port"
[ -f "$NC" ] || die "not a file: $NC"
# Read fields from the existing inbound
T_PROCESS=$("$NCP" protocol-field "$NC" "$INBOUND" PROCESSNAME 2>/dev/null | head -1)
[ -n "$T_PROCESS" ] || die "no such protocol in $NC: $INBOUND"
T_ENC=$("$NCP" protocol-field "$NC" "$INBOUND" ENCODING 2>/dev/null | head -1)
[ -z "$T_ENC" ] && T_ENC="ASCII"
ENC="${ENC_OVERRIDE:-$T_ENC}"
ORIG_PORT=$("$NCP" protocol-nested "$NC" "$INBOUND" PROTOCOL.PORT 2>/dev/null | head -1)
# F-2 fix (2026-06-08): protocol-nested returns the literal string "{}" for
# file/ICL inbounds whose PORT block is empty ({ PORT {} }). The old guard
# only tested for empty string, so "{}" (non-empty) slipped through and the
# generated thread carried "{ PORT {} }" — a broken TCP client with no port.
# Treat empty, "{}", and any non-numeric value as "no port" and die clearly.
case "$ORIG_PORT" in
''|'{}'|*[!0-9]*)
die "could not read a numeric PROTOCOL.PORT for inbound '$INBOUND' (got: '${ORIG_PORT:-<empty>}'). Is it a TCP listener? File/ICL inbounds do not use this jump pattern." ;;
esac
# tag = the inbound name itself (Bryan's "auto-derived" preference)
TAG="$INBOUND"
OLD_OUT_NAME="linux_${TAG}_out"
NEW_IN_NAME="windows_${TAG}_in"
NEW_OUT_NAME="windows_${TAG}_out"
# ─────────────────────────────────────────────────────────────────────────────
# Common helpers — emit the standard set of fields that every protocol block
# carries. Differences between the three threads are isolated below.
# ─────────────────────────────────────────────────────────────────────────────
emit_dataformat_passthrough() {
cat <<'EOF'
{ DATAFORMAT {
{ FRLTYPE offlen }
{ OFFLEN { { LEN 0 } { OFF 0 } } }
{ TYPE frl }
} }
EOF
}
emit_edibatch_empty() {
cat <<'EOF'
{ EDIBATCH {
{ IN_DATA { { TYPE {} } { VERSION {} } } }
{ OUT_DATA { { HEADER {} } { TRIGGER { { COUNT {} } { SCHEDULER {} } { TIMER {} } } } { TYPE {} } { VERSION {} } } }
} }
EOF
}
emit_errdbtps_default() {
cat <<'EOF'
{ ERRDBTPS {
{ ERRTPSPROCS { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
{ RETRIES -1 }
} }
EOF
}
emit_proc_blocks_empty() {
cat <<'EOF'
{ RECVCONTROL { { ACKCONTROL { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } } { EOMSG {} } { HOLDMSGS 0 } { MSGPRIO 5120 } } }
{ SAVECONTROL { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
{ TPS_INBOUND { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
{ TPS_OUTBOUND { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
{ TRACING 0 }
EOF
}
emit_inner_protocol_tcp_client() {
local host="$1" port="$2"
cat <<EOF
{ PROTOCOL {
{ CA_FILE {} }
{ CA_PATH {} }
{ CERT_FILE {} }
{ CIPHERSUITES {} }
{ CLOSE 0 }
{ CONTROLMSGS 0 }
{ COPYCLIENTIPP 0 }
{ DELAYCONNECT 0 }
{ ENCODE_FILL {} }
{ ENCODE_INCLUSIVE 1 }
{ ENCODE_ISNATIVE 0 }
{ ENCODE_JUST r }
{ ENCODE_LEN 4 }
{ ENCODE_TYPE encapsulated }
{ HOST ${host} }
{ IPV4_V6_DUAL 0 }
{ IS_SSL 0 }
{ ISMULTI 0 }
{ ISSERVER 0 }
{ LOCAL_IP {} }
{ MAXCLIENT 0 }
{ MAXOBQD 0 }
{ MAXPREXLTQD 0 }
{ MLP_ERROR RESET }
{ MLP_MODE MLP }
{ MLP_TIMEOUT 30 }
{ MODE {} }
{ PASSWORD {} }
{ PORT ${port} }
{ PRIVATE_KEY {} }
{ RECONNECT 1 }
{ REOPEN 5 }
{ SSL_PROTOCOL All }
{ TCP_CONNECTION_TIMEOUT {} }
{ TYPE tcpip }
{ WRITEZERO 0 }
} }
EOF
}
emit_inner_protocol_tcp_server() {
local port="$1"
cat <<EOF
{ PROTOCOL {
{ CA_FILE {} }
{ CA_PATH {} }
{ CERT_FILE {} }
{ CIPHERSUITES {} }
{ CLOSE 0 }
{ CONTROLMSGS 0 }
{ COPYCLIENTIPP 0 }
{ DELAYCONNECT 0 }
{ ENCODE_FILL {} }
{ ENCODE_INCLUSIVE 1 }
{ ENCODE_ISNATIVE 0 }
{ ENCODE_JUST r }
{ ENCODE_LEN 4 }
{ ENCODE_TYPE encapsulated }
{ HOST {} }
{ IPV4_V6_DUAL 0 }
{ IS_SSL 0 }
{ ISMULTI 0 }
{ ISSERVER 1 }
{ LOCAL_IP {} }
{ MAXCLIENT 0 }
{ MAXOBQD 0 }
{ MAXPREXLTQD 0 }
{ MLP_ERROR RESET }
{ MLP_MODE MLP }
{ MLP_TIMEOUT 30 }
{ MODE {} }
{ PASSWORD {} }
{ PORT ${port} }
{ PRIVATE_KEY {} }
{ RECONNECT 1 }
{ REOPEN 5 }
{ SSL_PROTOCOL All }
{ TCP_CONNECTION_TIMEOUT {} }
{ TYPE tcpip }
{ WRITEZERO 0 }
} }
EOF
}
# ─────────────────────────────────────────────────────────────────────────────
# Thread 1 — OLD env: linux_<tag>_out
# Outbound TCP client, same process as the existing inbound.
# No DATAXLATE (pass-through). Receives data via the route-add on the original inbound.
# ─────────────────────────────────────────────────────────────────────────────
emit_old_out() {
printf 'protocol %s {\n' "$OLD_OUT_NAME"
cat <<EOF
{ AUTOSTART 1 }
{ BITMAP {} }
{ COORDS {0 0} }
EOF
emit_dataformat_passthrough
printf ' { DATAXLATE {\n\n } }\n'
emit_edibatch_empty
cat <<EOF
{ ENCODING ${ENC} }
{ ENCODING_BOM_IB 0 }
{ ENCODING_BOM_OB 0 }
{ ENCODING_HL7 0 }
{ ENCODING_XML 0 }
{ EOCONFIG {} }
EOF
emit_errdbtps_default
cat <<EOF
{ GROUPS {server_jump OB_jump} }
{ HOSTDOWN 0 }
{ ICLSERVERPORT {} }
{ KEEPMSGONDISK 0 }
{ META {} }
{ OBWORKASIB 0 }
{ OUTBOUNDONLY 1 }
{ PROCESSNAME ${T_PROCESS} }
EOF
emit_inner_protocol_tcp_client "$NEW_HOST" "$JUMP_PORT"
emit_proc_blocks_empty
printf '}\n'
}
# ─────────────────────────────────────────────────────────────────────────────
# Thread 2 — NEW env, server_jump site: windows_<tag>_in
# Inbound TCP server. Routes internally to windows_<tag>_out (same site).
# ─────────────────────────────────────────────────────────────────────────────
emit_new_in() {
printf 'protocol %s {\n' "$NEW_IN_NAME"
cat <<EOF
{ AUTOSTART 1 }
{ BITMAP {} }
{ COORDS {0 0} }
EOF
emit_dataformat_passthrough
cat <<EOF
{ DATAXLATE {
{
{ CACHEMSG 0 }
{ DEL_ON_ERR_ROUTE 0 }
{ ROUTE_DETAILS {
{
{ DEST ${NEW_OUT_NAME} }
{ PROCS { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
{ TYPE raw }
}
} }
{ ROUTE_ENABLED 1 }
{ TRXID .* }
{ WILDCARD ON }
}
} }
EOF
emit_edibatch_empty
cat <<EOF
{ ENCODING ${ENC} }
{ ENCODING_BOM_IB 0 }
{ ENCODING_BOM_OB 0 }
{ ENCODING_HL7 0 }
{ ENCODING_XML 0 }
{ EOCONFIG {} }
EOF
emit_errdbtps_default
cat <<EOF
{ GROUPS {server_jump IB_jump} }
{ HOSTDOWN 0 }
{ ICLSERVERPORT {} }
{ KEEPMSGONDISK 0 }
{ META {} }
{ OBWORKASIB 0 }
{ OUTBOUNDONLY 0 }
{ PROCESSNAME ${PROC_JUMP} }
EOF
emit_inner_protocol_tcp_server "$JUMP_PORT"
emit_proc_blocks_empty
printf '}\n'
}
# ─────────────────────────────────────────────────────────────────────────────
# Thread 3 — NEW env, server_jump site: windows_<tag>_out
# Outbound TCP client, connects to localhost on the ORIGINAL inbound port.
# Receives via internal route from windows_<tag>_in.
# ─────────────────────────────────────────────────────────────────────────────
emit_new_out() {
printf 'protocol %s {\n' "$NEW_OUT_NAME"
cat <<EOF
{ AUTOSTART 1 }
{ BITMAP {} }
{ COORDS {0 0} }
EOF
emit_dataformat_passthrough
printf ' { DATAXLATE {\n\n } }\n'
emit_edibatch_empty
cat <<EOF
{ ENCODING ${ENC} }
{ ENCODING_BOM_IB 0 }
{ ENCODING_BOM_OB 0 }
{ ENCODING_HL7 0 }
{ ENCODING_XML 0 }
{ EOCONFIG {} }
EOF
emit_errdbtps_default
cat <<EOF
{ GROUPS {server_jump OB_jump} }
{ HOSTDOWN 0 }
{ ICLSERVERPORT {} }
{ KEEPMSGONDISK 0 }
{ META {} }
{ OBWORKASIB 0 }
{ OUTBOUNDONLY 1 }
{ PROCESSNAME ${PROC_JUMP} }
EOF
emit_inner_protocol_tcp_client "$INBOUND_HOST" "$ORIG_PORT"
emit_proc_blocks_empty
printf '}\n'
}
# ─────────────────────────────────────────────────────────────────────────────
# Route-add snippet — to splice into the OLD env existing inbound's DATAXLATE block.
# ─────────────────────────────────────────────────────────────────────────────
emit_route_add() {
cat <<EOF
{
{ CACHEMSG 0 }
{ DEL_ON_ERR_ROUTE 0 }
{ ROUTE_DETAILS {
{
{ DEST ${OLD_OUT_NAME} }
{ PROCS { { ARGS {} } { PROCS {} } { PROCSCONTROL {} } } }
{ TYPE raw }
}
} }
{ ROUTE_ENABLED 1 }
{ TRXID .* }
{ WILDCARD ON }
}
EOF
}
# ─────────────────────────────────────────────────────────────────────────────
# Output
# ─────────────────────────────────────────────────────────────────────────────
if [ -n "$OUT_PREFIX" ]; then
emit_old_out > "${OUT_PREFIX}.old_out.tcl"
emit_new_in > "${OUT_PREFIX}.new_in.tcl"
emit_new_out > "${OUT_PREFIX}.new_out.tcl"
emit_route_add > "${OUT_PREFIX}.route_add.tcl"
printf '%s\n%s\n%s\n%s\n' \
"${OUT_PREFIX}.old_out.tcl" \
"${OUT_PREFIX}.new_in.tcl" \
"${OUT_PREFIX}.new_out.tcl" \
"${OUT_PREFIX}.route_add.tcl"
else
cat <<EOF
# ═════════════════════════════════════════════════════════════════════════════
# Jump-thread set for inbound '${INBOUND}'
# tag (auto) = ${TAG}
# OLD process = ${T_PROCESS}
# NEW process = ${PROC_JUMP}
# ENCODING = ${ENC}
# ORIG inbound port = ${ORIG_PORT} (used by windows_${TAG}_out → 127.0.0.1)
# JUMP TCP port = ${JUMP_PORT} (OLD → NEW, used by linux_${TAG}_out and windows_${TAG}_in)
# NEW linux host = ${NEW_HOST}
# NEW-side dest = ${INBOUND_HOST}:${ORIG_PORT}
# ═════════════════════════════════════════════════════════════════════════════
# ── ① OLD env — add to NetConfig (process ${T_PROCESS}) ──
EOF
emit_old_out
cat <<EOF
# ── ② NEW env — add to server_jump/NetConfig (process ${PROC_JUMP}) ──
EOF
emit_new_in
echo ""
emit_new_out
cat <<EOF
# ── ③ ROUTE_ADD snippet — splice into OLD inbound '${INBOUND}' DATAXLATE block ──
EOF
emit_route_add
fi