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>
433 lines
16 KiB
Bash
Executable File
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
|