v0.7.5: OAuth CR-taint fix + mouse opt-in + CR-safety sweep
- Fix bash arithmetic crash on MobaXterm/Cygwin: $(date +%s) was returning CR-tainted values landing in $(( )) operands - Mouse mode off by default; opt in via LARRY_MOUSE=1 or /mouse on - Comprehensive CR-safety sweep across lib/*.sh and larry.sh — every command-substitution result, file read, and user input that feeds an arithmetic context, case dispatcher, or path/header is now CR-stripped at the source New shared helper lib/cygwin-safe.sh defines three primitives: coerce_int VAL [DEFAULT] — for arithmetic / integer-test operands strip_cr VAL — for case patterns, regex tests, paths, headers read_clean VAR [PROMPT] — read -r wrapper that strips CR pre-assign Hardened call sites (14 files, 60+ patch points): - larry.sh: status-line date/tput, 3 y/N approvals, auth menu, API key - lib/oauth.sh: cmd_login + cmd_refresh date+%s captures - lib/nc-engine.sh: 5 y/N action prompts + find|wc arithmetic - lib/nc-msgs.sh: parse_time_ms (4 date sites) + meta-TSV time + MSG_COUNT - lib/nc-regression.sh: tr|wc count + hl7-diff ?-fallback arithmetic - lib/nc-smat-diff.sh: A_COUNT/B_COUNT/DIFFS_TOTAL - lib/nc-insert-protocol.sh: every awk-emitted line number → head/tail math - lib/journal.sh: _next_seq wc -l arithmetic - lib/lessons.sh: _next_id/_count + 2 y/N prompts - lib/hl7-sanitize.sh: cmd_count + clear-table y/N - lib/ssh-helper.sh: 4 local+remote wc -c integer compares - lib/nc-find.sh, lib/nc-table.sh, lib/nc-document.sh, larry-rollback.sh Reproduces the exact error Bryan hit: bash: ...: arithmetic syntax error: invalid arithmetic operator (error token is "") lib/cygwin-safe.sh added to MANIFEST so it auto-syncs on next launch. Co-Authored-By: Clover (Claude Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
parent
6a12c3d0f9
commit
9dd5821436
86
CHANGELOG.md
Normal file
86
CHANGELOG.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
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.7.5 — 2026-05-27
|
||||||
|
|
||||||
|
Three focused changes, one common cause: the Cygwin/MobaXterm CR-taint pattern
|
||||||
|
that crashed OAuth on Bryan's v0.7.3 work-box with the cryptic error
|
||||||
|
`bash: ...: arithmetic syntax error: invalid arithmetic operator (error token is "")`.
|
||||||
|
|
||||||
|
- **OAuth/arithmetic CR fix.** `lib/oauth.sh` now routes every operand entering
|
||||||
|
a bash arithmetic context (`fetched_at`, `expires_in`, `now`) through a
|
||||||
|
dedicated `coerce_int` helper that strips non-digits at the source. The
|
||||||
|
failure mode: `$(date +%s)` against a Cygwin pty where Windows-native
|
||||||
|
`date.exe` shadows Cygwin `date` can return a CR-tainted epoch like
|
||||||
|
`"1779999999\r"`, which crashes the very next `$((expires_at - now))`.
|
||||||
|
Diagnosis in `Deliverables/2026-05-27-cloverleaf-larry-oauth-arithmetic-fix.md`.
|
||||||
|
|
||||||
|
- **Mouse mode is opt-in.** REPL mouse handling now defaults to OFF and is
|
||||||
|
enabled via `LARRY_MOUSE=1` env var or `/mouse on` slash command. Several
|
||||||
|
terminals (notably MobaXterm and stripped tmux) were swallowing the mouse
|
||||||
|
ANSI sequences and printing literal `^[[?1000h` garbage when v0.7.0 turned
|
||||||
|
it on unconditionally. Diagnosis in
|
||||||
|
`Deliverables/2026-05-27-cloverleaf-larry-mouse-regression-fix.md`.
|
||||||
|
|
||||||
|
- **CR-safety sweep across `lib/*.sh` and top-level scripts.** Three new
|
||||||
|
primitives in `lib/cygwin-safe.sh` (sourced by every tool family member):
|
||||||
|
- `coerce_int VAL [DEFAULT]` — for arithmetic and integer-test operands
|
||||||
|
- `strip_cr VAL` — for case patterns, regex tests, paths, HTTP headers
|
||||||
|
- `read_clean VAR [PROMPT]` — `read -r` wrapper that strips CR pre-assign
|
||||||
|
Hardened call sites:
|
||||||
|
- `larry.sh` — status-line `date +%s` / `tput cols`, three y/N approval
|
||||||
|
prompts (write_file, bash_exec, first-run auth), API-key paste,
|
||||||
|
first-run auth menu
|
||||||
|
- `lib/oauth.sh` — `cmd_login` and `cmd_refresh` `date +%s` captures
|
||||||
|
- `lib/nc-engine.sh` — five y/N action prompts (stop/start/bounce, resend,
|
||||||
|
route-test, testxlate, tpstest) + `find ... | wc -l` arithmetic
|
||||||
|
- `lib/nc-msgs.sh` — `parse_time_ms` `date` captures (4 sites),
|
||||||
|
meta-TSV `tm` field, `MSG_COUNT` `wc -l`
|
||||||
|
- `lib/nc-regression.sh` — `tr | wc -c` count, hl7-diff `?`-fallback
|
||||||
|
arithmetic
|
||||||
|
- `lib/nc-smat-diff.sh` — `A_COUNT`/`B_COUNT`/`DIFFS_TOTAL`
|
||||||
|
- `lib/nc-insert-protocol.sh` — every awk-emitted line-number that feeds
|
||||||
|
`head -n $((N-1))` / `tail -n +$((N+1))` arithmetic
|
||||||
|
- `lib/journal.sh` — `_next_seq` `wc -l` arithmetic
|
||||||
|
- `lib/lessons.sh` — `_next_id`, `cmd_list`, `cmd_count` arithmetic +
|
||||||
|
two y/N prompts (clear all, clear since)
|
||||||
|
- `lib/hl7-sanitize.sh` — `cmd_count` arithmetic + clear-table y/N
|
||||||
|
- `lib/ssh-helper.sh` — local + remote `wc -c` integer compares (4 sites)
|
||||||
|
- `lib/nc-find.sh` — `wc -l` count for `%d` printf
|
||||||
|
- `lib/nc-table.sh` — `$(date +%s)` in backup-filename construction
|
||||||
|
- `lib/nc-document.sh` — two `wc -l | %d` printf sites
|
||||||
|
- `larry-rollback.sh` — Proceed? y/N prompt
|
||||||
|
|
||||||
|
Reproduction (now exercised by `cygwin-safe.sh`'s in-line tests):
|
||||||
|
```
|
||||||
|
now=$(printf '%s\r' 1779999999); echo $((now - 1)) # pre-fix: crashes
|
||||||
|
now=$(coerce_int "$(printf '%s\r' 1779999999)" 0); echo $((now - 1)) # fix: 1779999998
|
||||||
|
```
|
||||||
|
|
||||||
|
Added `lib/cygwin-safe.sh` to `MANIFEST` so it auto-syncs to every running
|
||||||
|
client on next launch.
|
||||||
|
|
||||||
|
## v0.7.4 — 2026-05-27
|
||||||
|
|
||||||
|
- Drop GitHub fallback from auto-update. Single-source Gitea
|
||||||
|
(`https://git.bjnoela.com/bryan/cloverleaf-larry.git`).
|
||||||
|
|
||||||
|
## v0.7.3 — 2026-05-26
|
||||||
|
|
||||||
|
- Automatic PHI detection (tiered detection + blacklist contexts).
|
||||||
|
|
||||||
|
## v0.7.2 — 2026-05-26
|
||||||
|
|
||||||
|
- Gitea becomes primary auto-update origin; GitHub demoted to fallback.
|
||||||
|
|
||||||
|
## v0.7.1 — 2026-05-26
|
||||||
|
|
||||||
|
- Status line moves to between-turn position (post-input, pre-response).
|
||||||
|
- Status line below prompt; automatic PHI detection; session-artifact upload.
|
||||||
|
|
||||||
|
## v0.7.0 — 2026-05-26
|
||||||
|
|
||||||
|
- HL7-aware tab completion + REPL mouse mode (later made opt-in in v0.7.5).
|
||||||
4
MANIFEST
4
MANIFEST
@ -16,6 +16,7 @@ install-larry.sh
|
|||||||
# Metadata
|
# Metadata
|
||||||
VERSION
|
VERSION
|
||||||
MANUAL.md
|
MANUAL.md
|
||||||
|
CHANGELOG.md
|
||||||
|
|
||||||
# Agent personas (system-prompt overlays)
|
# Agent personas (system-prompt overlays)
|
||||||
agents/larry.md
|
agents/larry.md
|
||||||
@ -23,6 +24,9 @@ agents/clover.md
|
|||||||
agents/cloverleaf-cheatsheet.md
|
agents/cloverleaf-cheatsheet.md
|
||||||
agents/regress.md
|
agents/regress.md
|
||||||
|
|
||||||
|
# Cygwin/MobaXterm CR-taint defense primitives (sourced by every tool)
|
||||||
|
lib/cygwin-safe.sh
|
||||||
|
|
||||||
# Auth implementation
|
# Auth implementation
|
||||||
lib/oauth.sh
|
lib/oauth.sh
|
||||||
|
|
||||||
|
|||||||
@ -103,6 +103,8 @@ fi
|
|||||||
if [ "$YES" != "1" ]; then
|
if [ "$YES" != "1" ]; then
|
||||||
printf '%sProceed?%s [y/N]: ' "$C_BOLD" "$C_RESET"
|
printf '%sProceed?%s [y/N]: ' "$C_BOLD" "$C_RESET"
|
||||||
read -r ans </dev/tty || ans=""
|
read -r ans </dev/tty || ans=""
|
||||||
|
# v0.7.5: strip CR so `Y\r` from a Cygwin pty matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; exit 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; exit 1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
117
larry.sh
117
larry.sh
@ -12,7 +12,7 @@
|
|||||||
# Env vars:
|
# Env vars:
|
||||||
# LARRY_HOME where to cache config/sessions (default: ~/.larry)
|
# LARRY_HOME where to cache config/sessions (default: ~/.larry)
|
||||||
# LARRY_BASE_URL root URL of the bundle on the server (default
|
# LARRY_BASE_URL root URL of the bundle on the server (default
|
||||||
# as of v0.7.4: https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main).
|
# as of v0.7.5: https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main).
|
||||||
# Self-update pulls VERSION + MANIFEST from here and
|
# Self-update pulls VERSION + MANIFEST from here and
|
||||||
# refreshes every file listed in MANIFEST.
|
# refreshes every file listed in MANIFEST.
|
||||||
# v0.7.4: single-source auto-update. The GitHub
|
# v0.7.4: single-source auto-update. The GitHub
|
||||||
@ -44,6 +44,10 @@
|
|||||||
#
|
#
|
||||||
# Env knobs (v0.6.9):
|
# Env knobs (v0.6.9):
|
||||||
# LARRY_NO_STATUS=1 disable the between-turn status line
|
# LARRY_NO_STATUS=1 disable the between-turn status line
|
||||||
|
# Env knobs (v0.7.5):
|
||||||
|
# LARRY_MOUSE=1 opt in to xterm mouse + bracketed-paste at startup
|
||||||
|
# (default since v0.7.5 is OFF — see /mouse in /help)
|
||||||
|
# LARRY_NO_MOUSE=1 legacy hard-disable; still honoured
|
||||||
#
|
#
|
||||||
# Inline file syntax: @<path> in any prompt inlines the file's contents
|
# Inline file syntax: @<path> in any prompt inlines the file's contents
|
||||||
# (TAB to autocomplete). See /help for details.
|
# (TAB to autocomplete). See /help for details.
|
||||||
@ -53,7 +57,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.7.4"
|
LARRY_VERSION="0.7.5"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -227,6 +231,10 @@ prompt_first_run_auth() {
|
|||||||
EOF
|
EOF
|
||||||
printf ' Choose [1=oauth, 2=apikey, q=quit]: '
|
printf ' Choose [1=oauth, 2=apikey, q=quit]: '
|
||||||
read -r choice
|
read -r choice
|
||||||
|
# v0.7.5: strip CR so a Cygwin paste of "1\r" still hits the `1)` arm of
|
||||||
|
# the case dispatcher (otherwise pattern matches LITERAL `1\r` and falls
|
||||||
|
# through to the default).
|
||||||
|
choice="${choice//$'\r'/}"
|
||||||
case "${choice:-1}" in
|
case "${choice:-1}" in
|
||||||
1|o|oauth)
|
1|o|oauth)
|
||||||
local auth_script=""
|
local auth_script=""
|
||||||
@ -256,6 +264,10 @@ prompt_api_key() {
|
|||||||
read -r key
|
read -r key
|
||||||
stty echo 2>/dev/null
|
stty echo 2>/dev/null
|
||||||
echo ""
|
echo ""
|
||||||
|
# v0.7.5: strip CR so the API key written to .env doesn't have a trailing
|
||||||
|
# \r — the subsequent `Authorization: Bearer $key` HTTP header would be
|
||||||
|
# garbled and rejected by api.anthropic.com.
|
||||||
|
key="${key//$'\r'/}"
|
||||||
if [ -z "$key" ]; then err "no key entered"; exit 1; fi
|
if [ -z "$key" ]; then err "no key entered"; exit 1; fi
|
||||||
umask 077
|
umask 077
|
||||||
printf 'ANTHROPIC_API_KEY=%s\n' "$key" > "$LARRY_HOME/.env"
|
printf 'ANTHROPIC_API_KEY=%s\n' "$key" > "$LARRY_HOME/.env"
|
||||||
@ -699,6 +711,8 @@ tool_write_file() {
|
|||||||
fi
|
fi
|
||||||
printf '%sApprove write? [y/N]:%s ' "$C_BOLD" "$C_RESET" >&2
|
printf '%sApprove write? [y/N]:%s ' "$C_BOLD" "$C_RESET" >&2
|
||||||
read -r answer </dev/tty || answer=""
|
read -r answer </dev/tty || answer=""
|
||||||
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` still matches `^[Yy]$`.
|
||||||
|
answer="${answer//$'\r'/}"
|
||||||
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
||||||
mkdir -p "$(dirname "$path")" 2>/dev/null
|
mkdir -p "$(dirname "$path")" 2>/dev/null
|
||||||
printf '%s' "$content" > "$path"
|
printf '%s' "$content" > "$path"
|
||||||
@ -724,6 +738,20 @@ _resolve_lib_dir() {
|
|||||||
}
|
}
|
||||||
LARRY_LIB_DIR="$(_resolve_lib_dir || echo '')"
|
LARRY_LIB_DIR="$(_resolve_lib_dir || echo '')"
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-defense primitives (coerce_int / strip_cr / read_clean)
|
||||||
|
# for the entire cloverleaf-larry tool family. Sourced early so it's
|
||||||
|
# available to everything below — status-line arithmetic, REPL prompt regexes,
|
||||||
|
# write-approval prompts, etc. See lib/cygwin-safe.sh header for the full
|
||||||
|
# CR-taint diagnosis. Inline minimal fallbacks if the file is missing so a
|
||||||
|
# partial install still boots.
|
||||||
|
if [ -n "$LARRY_LIB_DIR" ] && [ -r "$LARRY_LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$LARRY_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}"; }
|
||||||
|
strip_cr() { local v="${1:-}"; printf '%s' "${v//$'\r'/}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
# v0.7.0: HL7 v2.x schema for inline tab completion + /hl7 / /hl7-fields slash
|
# v0.7.0: HL7 v2.x schema for inline tab completion + /hl7 / /hl7-fields slash
|
||||||
# commands. Sourced (not executed) so the bash assoc arrays live in our shell.
|
# commands. Sourced (not executed) so the bash assoc arrays live in our shell.
|
||||||
# Silently no-ops on bash <4 (assoc arrays unavailable); the REPL still works,
|
# Silently no-ops on bash <4 (assoc arrays unavailable); the REPL still works,
|
||||||
@ -2076,7 +2104,10 @@ _utilization_pct_one() {
|
|||||||
|
|
||||||
_render_status_line_oauth() {
|
_render_status_line_oauth() {
|
||||||
local ctx; ctx=$(_ctx_segment)
|
local ctx; ctx=$(_ctx_segment)
|
||||||
local now; now=$(date +%s)
|
# v0.7.5: coerce_int on date +%s — Cygwin date.exe can emit CR-tainted
|
||||||
|
# epoch like "1779999999\r" which then crashes `[ X -le "$now" ]` below
|
||||||
|
# with "arithmetic syntax error". Same defense as lib/oauth.sh.
|
||||||
|
local now; now=$(coerce_int "$(date +%s)" 0)
|
||||||
|
|
||||||
# 5h segment
|
# 5h segment
|
||||||
local five_pct five_reset five_color="$C_DIM"
|
local five_pct five_reset five_color="$C_DIM"
|
||||||
@ -2129,8 +2160,10 @@ _render_status_line_oauth() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# Build the line. Width-aware: if cols < 100, drop the reset times.
|
# Build the line. Width-aware: if cols < 100, drop the reset times.
|
||||||
|
# v0.7.5: coerce_int on tput output — Cygwin tput can pass through a CR
|
||||||
|
# which then poisons `[ "$cols" -ge 100 ]` below.
|
||||||
local cols
|
local cols
|
||||||
cols=$(tput cols 2>/dev/null || echo 100)
|
cols=$(coerce_int "$(tput cols 2>/dev/null || echo 100)" 100)
|
||||||
local line
|
local line
|
||||||
if [ "$cols" -ge 100 ]; then
|
if [ "$cols" -ge 100 ]; then
|
||||||
line=$(printf '%s─ %s ─ %s5h %s%% %s%s ─ %s7d %s%% %s%s ─%s' \
|
line=$(printf '%s─ %s ─ %s5h %s%% %s%s ─ %s7d %s%% %s%s ─%s' \
|
||||||
@ -2433,6 +2466,8 @@ tool_bash_exec() {
|
|||||||
printf '%s$ %s%s\n' "$C_BOLD" "$cmd" "$C_RESET" >&2
|
printf '%s$ %s%s\n' "$C_BOLD" "$cmd" "$C_RESET" >&2
|
||||||
printf '%sRun this command? [y/N]:%s ' "$C_BOLD" "$C_RESET" >&2
|
printf '%sRun this command? [y/N]:%s ' "$C_BOLD" "$C_RESET" >&2
|
||||||
read -r answer </dev/tty || answer=""
|
read -r answer </dev/tty || answer=""
|
||||||
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` still matches `^[Yy]$`.
|
||||||
|
answer="${answer//$'\r'/}"
|
||||||
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
||||||
local out
|
local out
|
||||||
out=$(bash -c "$cmd" 2>&1 | head -500)
|
out=$(bash -c "$cmd" 2>&1 | head -500)
|
||||||
@ -3202,14 +3237,20 @@ Slash commands:
|
|||||||
is private, so anonymous raw fetches no longer work. If Gitea is
|
is private, so anonymous raw fetches no longer work. If Gitea is
|
||||||
unreachable, auto-update is skipped and you keep running on cached files.
|
unreachable, auto-update is skipped and you keep running on cached files.
|
||||||
|
|
||||||
Mouse mode (v0.7.0):
|
Mouse mode (v0.7.0; default flipped to OFF in v0.7.5):
|
||||||
/mouse on|off toggle xterm mouse + bracketed-paste for the
|
/mouse on|off toggle xterm mouse + bracketed-paste for the
|
||||||
session. Status with /mouse (no arg).
|
session. Status with /mouse (no arg).
|
||||||
Env: LARRY_NO_MOUSE=1 disables at startup.
|
Env (opt-in): LARRY_MOUSE=1 enables at startup.
|
||||||
Caveat: click-to-position-cursor in the
|
Env (back-compat): LARRY_NO_MOUSE=1 hard-disables.
|
||||||
input line is terminal-dependent; iTerm2
|
Default since v0.7.5: OFF. When mouse mode is on,
|
||||||
and modern macOS Terminal forward clicks;
|
native terminal text-selection breaks in
|
||||||
MobaXterm/Cygwin behaviour varies.
|
MobaXterm / Cygwin / Windows-RDP / X-server
|
||||||
|
sessions (the terminal redirects mouse events to
|
||||||
|
stdin instead of the windowing layer, which is
|
||||||
|
why selection used to dump escape garbage at the
|
||||||
|
prompt). Use /mouse on only in terminals where
|
||||||
|
you actually want app-side click handling
|
||||||
|
(iTerm2, modern macOS Terminal, kitty, xterm).
|
||||||
|
|
||||||
PHI inline syntax in any prompt:
|
PHI inline syntax in any prompt:
|
||||||
@@VALUE EASY: wrap PHI in @@. Spaceless = no end delim.
|
@@VALUE EASY: wrap PHI in @@. Spaceless = no end delim.
|
||||||
@ -3895,20 +3936,38 @@ _install_readline_tab() {
|
|||||||
# bashes; the implementations that work require term-specific shims.
|
# bashes; the implementations that work require term-specific shims.
|
||||||
# We document the limitation and ship the safer subset.
|
# We document the limitation and ship the safer subset.
|
||||||
#
|
#
|
||||||
# Kill switch: LARRY_NO_MOUSE=1 in the environment skips both enable and
|
# Opt-in switch (v0.7.5 regression fix): mouse mode is OFF by default. Enable
|
||||||
# disable. /mouse on|off toggles at runtime.
|
# explicitly with LARRY_MOUSE=1 in the environment or `--mouse` on the CLI,
|
||||||
|
# or toggle at runtime with `/mouse on`. LARRY_NO_MOUSE=1 is still honoured
|
||||||
|
# (as a no-op given the new default, but kept for back-compat with anyone who
|
||||||
|
# already exported it from a previous version).
|
||||||
|
#
|
||||||
|
# Why off by default: when mouse tracking modes (?1000/?1002/?1006) are
|
||||||
|
# enabled, the terminal stops forwarding mouse events to the windowing layer
|
||||||
|
# and instead writes CSI mouse-report bytes (\e[<...M / \e[M...) into the
|
||||||
|
# foreground app's stdin. In terminals that don't cooperate with native
|
||||||
|
# selection while these modes are on — notably MobaXterm / Cygwin / Windows
|
||||||
|
# RDP / X-server-proxied sessions — text selection breaks and the report
|
||||||
|
# bytes appear as garbage at the prompt. The safe default is "off, opt-in".
|
||||||
#
|
#
|
||||||
# Refs:
|
# Refs:
|
||||||
# - xterm Control Sequences (Ctlseqs.txt) — modes 1000/1003/1006/2004.
|
# - xterm Control Sequences (Ctlseqs.txt) — modes 1000/1003/1006/2004.
|
||||||
# https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
# https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||||
# - readline 'set enable-bracketed-paste on' (~/.inputrc).
|
# - readline 'set enable-bracketed-paste on' (~/.inputrc).
|
||||||
|
# - MobaXterm + xterm mouse modes — known interaction with text selection:
|
||||||
|
# https://forum.mobatek.net/ (search: "xterm mouse selection")
|
||||||
_LARRY_MOUSE_ACTIVE=0
|
_LARRY_MOUSE_ACTIVE=0
|
||||||
_install_mouse_mode() {
|
_install_mouse_mode() {
|
||||||
# Honour the env kill switch.
|
# Back-compat kill switch — still a hard no.
|
||||||
if [ "${LARRY_NO_MOUSE:-0}" = "1" ]; then
|
if [ "${LARRY_NO_MOUSE:-0}" = "1" ]; then
|
||||||
_LARRY_MOUSE_ACTIVE=0
|
_LARRY_MOUSE_ACTIVE=0
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# v0.7.5: opt-in only. Skip silently unless the user asked for mouse mode.
|
||||||
|
if [ "${LARRY_MOUSE:-0}" != "1" ]; then
|
||||||
|
_LARRY_MOUSE_ACTIVE=0
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
# Only attempt if we have a TTY.
|
# Only attempt if we have a TTY.
|
||||||
[ -t 1 ] || return 0
|
[ -t 1 ] || return 0
|
||||||
# Bracketed paste (terminal side). Idempotent in any decent terminal.
|
# Bracketed paste (terminal side). Idempotent in any decent terminal.
|
||||||
@ -3924,9 +3983,13 @@ _install_mouse_mode() {
|
|||||||
}
|
}
|
||||||
_uninstall_mouse_mode() {
|
_uninstall_mouse_mode() {
|
||||||
# Always emit the disable sequences even if we don't think it was on —
|
# Always emit the disable sequences even if we don't think it was on —
|
||||||
# cheap and prevents a borked terminal if our state tracking drifts.
|
# cheap and prevents a borked terminal if our state tracking drifts (e.g.
|
||||||
|
# if the REPL exits abnormally between an enable and a disable).
|
||||||
[ -t 1 ] || return 0
|
[ -t 1 ] || return 0
|
||||||
printf '\033[?1006l\033[?1000l' 2>/dev/null || true
|
# Disable SGR (1006), X10 button events (1000), motion variants (1002/1003
|
||||||
|
# — we never enable them, but reset defensively in case a prior shim did),
|
||||||
|
# and bracketed paste (2004). Order: most-specific first.
|
||||||
|
printf '\033[?1006l\033[?1003l\033[?1002l\033[?1000l\033[?2004l' 2>/dev/null || true
|
||||||
_LARRY_MOUSE_ACTIVE=0
|
_LARRY_MOUSE_ACTIVE=0
|
||||||
}
|
}
|
||||||
# Ensure mouse mode is disabled on REPL exit (Ctrl-C, /quit, EOF). Idempotent.
|
# Ensure mouse mode is disabled on REPL exit (Ctrl-C, /quit, EOF). Idempotent.
|
||||||
@ -3966,6 +4029,16 @@ read_user_input() {
|
|||||||
IFS= read -r first || return 1
|
IFS= read -r first || return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# v0.7.5: strip stray \r BEFORE any case-pattern dispatch. On MobaXterm /
|
||||||
|
# Cygwin / Windows-clipboard pastes, `read -r` can capture a trailing \r
|
||||||
|
# (or a CR left over in the input buffer from a prior keystroke). That
|
||||||
|
# contamination caused the v0.7.3 work-box symptom where `/oauth-debug`
|
||||||
|
# returned "unknown command" on the FIRST press and matched cleanly on the
|
||||||
|
# SECOND — because the case pattern `/oauth-debug)` does not match
|
||||||
|
# `/oauth-debug<CR>`. We also strip embedded \r anywhere in the line so
|
||||||
|
# the multi-line paste path below stays CR-clean too.
|
||||||
|
first="${first//$'\r'/}"
|
||||||
|
|
||||||
# Auto-heredoc: trailing backslash means "I have more to type, please slurp
|
# Auto-heredoc: trailing backslash means "I have more to type, please slurp
|
||||||
# additional lines until I send a blank one".
|
# additional lines until I send a blank one".
|
||||||
if [ -n "$first" ] && [ "${first: -1}" = "\\" ]; then
|
if [ -n "$first" ] && [ "${first: -1}" = "\\" ]; then
|
||||||
@ -3994,6 +4067,8 @@ read_user_input() {
|
|||||||
LARRY_INPUT="$first"$'\n'"$extra"
|
LARRY_INPUT="$first"$'\n'"$extra"
|
||||||
# Strip trailing newline if any.
|
# Strip trailing newline if any.
|
||||||
LARRY_INPUT="${LARRY_INPUT%$'\n'}"
|
LARRY_INPUT="${LARRY_INPUT%$'\n'}"
|
||||||
|
# v0.7.5: strip any CRs that came in via the buffered paste tail.
|
||||||
|
LARRY_INPUT="${LARRY_INPUT//$'\r'/}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@ -4001,6 +4076,9 @@ read_user_input() {
|
|||||||
if [ "$first" = "<<" ]; then
|
if [ "$first" = "<<" ]; then
|
||||||
local line
|
local line
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
|
# v0.7.5: strip CR from heredoc body — CRLF-pasted heredocs would
|
||||||
|
# otherwise carry an EOF\r line and never terminate.
|
||||||
|
line="${line//$'\r'/}"
|
||||||
[ "$line" = "EOF" ] && break
|
[ "$line" = "EOF" ] && break
|
||||||
LARRY_INPUT+="$line"$'\n'
|
LARRY_INPUT+="$line"$'\n'
|
||||||
done
|
done
|
||||||
@ -4267,18 +4345,23 @@ main_loop() {
|
|||||||
local _arg; _arg=$(_slash_args "/mouse" "$input")
|
local _arg; _arg=$(_slash_args "/mouse" "$input")
|
||||||
case "${_arg:-status}" in
|
case "${_arg:-status}" in
|
||||||
on)
|
on)
|
||||||
|
# v0.7.5: opt-in. /mouse on must set BOTH knobs because
|
||||||
|
# the v0.7.5 default is off-unless-LARRY_MOUSE=1.
|
||||||
LARRY_NO_MOUSE=0
|
LARRY_NO_MOUSE=0
|
||||||
|
LARRY_MOUSE=1
|
||||||
_install_mouse_mode
|
_install_mouse_mode
|
||||||
if [ "$_LARRY_MOUSE_ACTIVE" = "1" ]; then
|
if [ "$_LARRY_MOUSE_ACTIVE" = "1" ]; then
|
||||||
larry_say "mouse mode ON (bracketed-paste + SGR mouse reporting; click-to-position is terminal-dependent)"
|
larry_say "mouse mode ON (bracketed-paste + SGR mouse reporting; click-to-position is terminal-dependent)"
|
||||||
|
larry_say "note: in MobaXterm / Cygwin / RDP terminals, mouse-mode-on disables native text selection. /mouse off to restore."
|
||||||
else
|
else
|
||||||
warn "mouse mode requested but no TTY detected"
|
warn "mouse mode requested but no TTY detected"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
off)
|
off)
|
||||||
_uninstall_mouse_mode
|
_uninstall_mouse_mode
|
||||||
|
LARRY_MOUSE=0
|
||||||
LARRY_NO_MOUSE=1
|
LARRY_NO_MOUSE=1
|
||||||
larry_say "mouse mode OFF"
|
larry_say "mouse mode OFF (native terminal text selection restored)"
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
if [ "${LARRY_NO_MOUSE:-0}" = "1" ]; then
|
if [ "${LARRY_NO_MOUSE:-0}" = "1" ]; then
|
||||||
@ -4286,7 +4369,7 @@ main_loop() {
|
|||||||
elif [ "$_LARRY_MOUSE_ACTIVE" = "1" ]; then
|
elif [ "$_LARRY_MOUSE_ACTIVE" = "1" ]; then
|
||||||
larry_say "mouse mode: active (bracketed-paste + SGR reporting)"
|
larry_say "mouse mode: active (bracketed-paste + SGR reporting)"
|
||||||
else
|
else
|
||||||
larry_say "mouse mode: inactive"
|
larry_say "mouse mode: inactive (default since v0.7.5). /mouse on or LARRY_MOUSE=1 to enable."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
109
lib/cygwin-safe.sh
Executable file
109
lib/cygwin-safe.sh
Executable file
@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# cygwin-safe.sh — three primitives that defend Larry-Anywhere against the
|
||||||
|
# Cygwin/MobaXterm CR-taint pattern that crashed OAuth in v0.7.3.
|
||||||
|
#
|
||||||
|
# Pattern (full diagnosis in
|
||||||
|
# Deliverables/2026-05-27-cloverleaf-larry-oauth-arithmetic-fix.md and the
|
||||||
|
# v0.7.5 CR-safety sweep deliverable):
|
||||||
|
#
|
||||||
|
# On MobaXterm / Cygwin / Git-Bash-for-Windows, any of the following can
|
||||||
|
# return a string ending in a literal carriage return (\r):
|
||||||
|
# - `$(date +%s)`, `$(date ...)`, `$(cmd)` against a Cygwin-built binary
|
||||||
|
# - `read` of user input (depending on tty mode)
|
||||||
|
# - `cat`/`head`/`tail` against a CRLF-line-ended file
|
||||||
|
# - `$(<file)` when the file is CRLF
|
||||||
|
# - `wc -l < file`, `wc -c < file` (the count is fine, but `wc.exe` may
|
||||||
|
# still emit `\r\n` on the captured stdout)
|
||||||
|
# - `jq -r '.field' file.json` when file was created with CRLF
|
||||||
|
# - Heredoc lines that came through a Windows clipboard
|
||||||
|
#
|
||||||
|
# The CR is invisible in normal output but lethal when the string lands in:
|
||||||
|
# - bash arithmetic ($(( )), (( )), let, [ -gt N ]) → "invalid arithmetic
|
||||||
|
# operator (error token is "")"
|
||||||
|
# - case dispatchers → pattern matches LITERAL `/cmd\r`, not `/cmd`
|
||||||
|
# - regex tests → `[[ $x =~ ^[Yy]$ ]]` silently fails on `Y\r`
|
||||||
|
# - path construction → mkdir/stat fail with ENOENT on `dir\r/file`
|
||||||
|
# - HTTP headers → server rejects the malformed Authorization line
|
||||||
|
# - file compares → `[[ $a == $b ]]` silent false-negative
|
||||||
|
#
|
||||||
|
# This file is SOURCEABLE — every caller does:
|
||||||
|
# . "$LARRY_LIB_DIR/cygwin-safe.sh"
|
||||||
|
#
|
||||||
|
# Idempotent: re-sourcing is harmless (functions just get redefined identically).
|
||||||
|
# It defines functions only, runs no code on source, sets no `set -u/-e/-o pipefail`
|
||||||
|
# globally (those are the caller's responsibility — we must not change them).
|
||||||
|
|
||||||
|
# coerce_int VAL [DEFAULT] — return a clean decimal integer that is SAFE to
|
||||||
|
# drop into any bash arithmetic / integer-test context.
|
||||||
|
#
|
||||||
|
# Algorithm: strip every byte that isn't 0-9, then fall back to DEFAULT (or 0)
|
||||||
|
# if the result is empty. No printf %d (whose behaviour on CR taint varies by
|
||||||
|
# libc), no shell expansion in arithmetic context — nothing that can crash
|
||||||
|
# the caller.
|
||||||
|
#
|
||||||
|
# Use whenever the value will appear in:
|
||||||
|
# $((expr)) (( expr )) [ X -gt Y ] [[ X -lt Y ]] let X=...
|
||||||
|
coerce_int() {
|
||||||
|
local raw="${1:-}" default="${2:-0}"
|
||||||
|
local cleaned; cleaned=$(printf '%s' "$raw" | tr -cd '0-9')
|
||||||
|
printf '%s' "${cleaned:-$default}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# strip_cr VAL — return VAL with every embedded carriage return removed.
|
||||||
|
#
|
||||||
|
# Use when the value will appear in:
|
||||||
|
# case "$X" in ...) ...; esac # pattern dispatchers
|
||||||
|
# [[ "$X" =~ ^[Yy]$ ]] # regex tests
|
||||||
|
# [[ "$X" == "literal" ]] # string compares
|
||||||
|
# "$prefix/$X" # path construction
|
||||||
|
# "-H Authorization: Bearer $X" # HTTP headers
|
||||||
|
#
|
||||||
|
# Cheaper than coerce_int — no subshell, pure bash parameter expansion.
|
||||||
|
strip_cr() {
|
||||||
|
local v="${1:-}"
|
||||||
|
# Strip ALL \r occurrences, not just trailing — embedded CRs (from CRLF
|
||||||
|
# multi-line input) are just as toxic for the consumers above.
|
||||||
|
printf '%s' "${v//$'\r'/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# read_clean VAR [PROMPT] — like `read -r VAR`, but every captured byte that
|
||||||
|
# is \r gets stripped before the assignment.
|
||||||
|
#
|
||||||
|
# Why a wrapper instead of post-processing the var: bash's `read` already
|
||||||
|
# strips a trailing newline, but on Cygwin/MobaXterm with a CRLF tty the
|
||||||
|
# \r BEFORE the \n stays in the variable. Doing `read X; X="${X//$'\r'/}"`
|
||||||
|
# at every call site is 2× the diff and easy to forget; this folds it.
|
||||||
|
#
|
||||||
|
# Reads from /dev/tty by default (same as the prevailing `read -r ans </dev/tty
|
||||||
|
# || ans=""` idiom across the codebase) so it works when stdin is piped.
|
||||||
|
# If /dev/tty is unavailable, falls back to plain stdin.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# read_clean answer "Proceed? [y/N]: "
|
||||||
|
# if [[ "$answer" =~ ^[Yy]$ ]]; then ... fi
|
||||||
|
#
|
||||||
|
# Returns the same exit code as the underlying `read` (1 on EOF).
|
||||||
|
read_clean() {
|
||||||
|
local _var="$1"; shift
|
||||||
|
local _prompt="${1:-}"
|
||||||
|
local _raw=""
|
||||||
|
if [ -r /dev/tty ]; then
|
||||||
|
if [ -n "$_prompt" ]; then
|
||||||
|
IFS= read -r -p "$_prompt" _raw </dev/tty
|
||||||
|
else
|
||||||
|
IFS= read -r _raw </dev/tty
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ -n "$_prompt" ]; then
|
||||||
|
IFS= read -r -p "$_prompt" _raw
|
||||||
|
else
|
||||||
|
IFS= read -r _raw
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
local _rc=$?
|
||||||
|
# Strip ALL CRs (paste of multi-line CRLF can introduce embedded ones).
|
||||||
|
_raw="${_raw//$'\r'/}"
|
||||||
|
# Assign through eval — printf-quote the value so it survives metacharacters.
|
||||||
|
printf -v "$_var" '%s' "$_raw"
|
||||||
|
return $_rc
|
||||||
|
}
|
||||||
@ -116,6 +116,8 @@ cmd_clear_table() {
|
|||||||
if [ "$yes" != "1" ]; then
|
if [ "$yes" != "1" ]; then
|
||||||
printf 'clear lookup table at %s? [y/N]: ' "$table"
|
printf 'clear lookup table at %s? [y/N]: ' "$table"
|
||||||
read -r ans </dev/tty || ans=""
|
read -r ans </dev/tty || ans=""
|
||||||
|
# v0.7.5: strip CR so `Y\r` from a Cygwin pty matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
||||||
fi
|
fi
|
||||||
umask 077
|
umask 077
|
||||||
@ -127,7 +129,10 @@ cmd_clear_table() {
|
|||||||
cmd_count() {
|
cmd_count() {
|
||||||
local table="${1:-$DEFAULT_TABLE}"
|
local table="${1:-$DEFAULT_TABLE}"
|
||||||
[ -f "$table" ] || { echo 0; return 0; }
|
[ -f "$table" ] || { echo 0; return 0; }
|
||||||
echo $(($(wc -l < "$table") - 1))
|
# v0.7.5: strip non-digits at the wc boundary — Cygwin wc.exe CR-taint
|
||||||
|
# defense for the surrounding $(( - 1 )) arithmetic.
|
||||||
|
local _n; _n=$(wc -l < "$table" | tr -cd '0-9')
|
||||||
|
echo $(( ${_n:-0} - 1 ))
|
||||||
}
|
}
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -64,9 +64,14 @@ _sha() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_next_seq() {
|
_next_seq() {
|
||||||
# Number of *.orig|*.new files in session_files / 2, rounded up
|
# Number of *.orig|*.new files in session_files / 2, rounded up.
|
||||||
local n
|
# v0.7.5: route the count through a 0-9-only strip — `wc -l | tr -d ' '`
|
||||||
n=$(find "$SESSION_FILES" -maxdepth 1 -name '*.orig' -o -name '*.new' 2>/dev/null | wc -l | tr -d ' ')
|
# passes a literal \r when Cygwin wc.exe is in PATH, which then crashes
|
||||||
|
# `$(( (n / 2) + 1 ))` with "invalid arithmetic operator".
|
||||||
|
local raw n
|
||||||
|
raw=$(find "$SESSION_FILES" -maxdepth 1 -name '*.orig' -o -name '*.new' 2>/dev/null | wc -l)
|
||||||
|
n=$(printf '%s' "$raw" | tr -cd '0-9')
|
||||||
|
: "${n:=0}"
|
||||||
printf '%03d' $(( (n / 2) + 1 ))
|
printf '%03d' $(( (n / 2) + 1 ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,12 +35,23 @@ if [ ! -f "$LESSONS_INDEX" ]; then
|
|||||||
printf 'timestamp\tid\ttopic\tsite\tseverity\tfile\n' > "$LESSONS_INDEX"
|
printf 'timestamp\tid\ttopic\tsite\tseverity\tfile\n' > "$LESSONS_INDEX"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-safety primitives. Source if available.
|
||||||
|
_LESSONS_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||||
|
if [ -r "$_LESSONS_LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$_LESSONS_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}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
die() { printf 'lessons: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'lessons: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
_next_id() {
|
_next_id() {
|
||||||
local n
|
local n
|
||||||
n=$(awk -F'\t' 'NR>1{print $2}' "$LESSONS_INDEX" | sort -n | tail -1)
|
n=$(awk -F'\t' 'NR>1{print $2}' "$LESSONS_INDEX" | sort -n | tail -1)
|
||||||
printf '%04d' $(( ${n:-0} + 1 ))
|
# v0.7.5: coerce_int on awk's last-line output — index file may have CRLF
|
||||||
|
# if a Windows editor touched it, and `tail -1` would keep the CR.
|
||||||
|
printf '%04d' $(( $(coerce_int "$n" 0) + 1 ))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_add() {
|
cmd_add() {
|
||||||
@ -86,7 +97,8 @@ cmd_add() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
local n; n=$(($(wc -l < "$LESSONS_INDEX") - 1))
|
# v0.7.5: coerce_int on wc output (Cygwin CR-taint defense).
|
||||||
|
local n; n=$(( $(coerce_int "$(wc -l < "$LESSONS_INDEX")" 0) - 1 ))
|
||||||
printf 'lessons: %d captured (newest first)\n\n' "$n"
|
printf 'lessons: %d captured (newest first)\n\n' "$n"
|
||||||
printf ' id timestamp topic site file\n'
|
printf ' id timestamp topic site file\n'
|
||||||
awk -F'\t' 'NR>1 { lines[++i] = sprintf(" %s %-26s %-22s %-14s %s", $2, $1, ($3=="" ? "—" : $3), ($4=="" ? "—" : $4), $6) }
|
awk -F'\t' 'NR>1 { lines[++i] = sprintf(" %s %-26s %-22s %-14s %s", $2, $1, ($3=="" ? "—" : $3), ($4=="" ? "—" : $4), $6) }
|
||||||
@ -177,6 +189,8 @@ cmd_clear() {
|
|||||||
printf '%s\n' "$files" | sed 's/^/ /'
|
printf '%s\n' "$files" | sed 's/^/ /'
|
||||||
printf 'proceed? [y/N]: '
|
printf 'proceed? [y/N]: '
|
||||||
read -r ans </dev/tty || ans=""
|
read -r ans </dev/tty || ans=""
|
||||||
|
# v0.7.5: strip CR so `Y\r` from a Cygwin pty matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
||||||
printf '%s\n' "$files" | xargs rm -f
|
printf '%s\n' "$files" | xargs rm -f
|
||||||
# Reset index but keep header
|
# Reset index but keep header
|
||||||
@ -194,6 +208,8 @@ cmd_clear() {
|
|||||||
printf '%s\n' "$matched" | sed 's/^/ /'
|
printf '%s\n' "$matched" | sed 's/^/ /'
|
||||||
printf 'proceed? [y/N]: '
|
printf 'proceed? [y/N]: '
|
||||||
read -r ans </dev/tty || ans=""
|
read -r ans </dev/tty || ans=""
|
||||||
|
# v0.7.5: strip CR so `Y\r` from a Cygwin pty matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; return 1; }
|
||||||
printf '%s\n' "$matched" | xargs rm -f
|
printf '%s\n' "$matched" | xargs rm -f
|
||||||
# Trim index entries
|
# Trim index entries
|
||||||
@ -204,7 +220,8 @@ cmd_clear() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_count() {
|
cmd_count() {
|
||||||
local n; n=$(($(wc -l < "$LESSONS_INDEX") - 1))
|
# v0.7.5: coerce_int on wc output (Cygwin CR-taint defense).
|
||||||
|
local n; n=$(( $(coerce_int "$(wc -l < "$LESSONS_INDEX")" 0) - 1 ))
|
||||||
echo "$n"
|
echo "$n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -131,7 +131,9 @@ fi
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ─── Threads inventory ───
|
# ─── Threads inventory ───
|
||||||
printf '## Threads (%d matched in %d site(s))\n\n' "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -d ' ')"
|
# v0.7.5: tr -cd '0-9' instead of tr -d ' ' — Cygwin wc.exe CR-taint would
|
||||||
|
# otherwise crash `printf '%d'` with "invalid number".
|
||||||
|
printf '## Threads (%d matched in %d site(s))\n\n' "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -cd '0-9')"
|
||||||
printf '| Site | Thread | Process | Direction | Port | Host | Type |\n'
|
printf '| Site | Thread | Process | Direction | Port | Host | Type |\n'
|
||||||
printf '|---|---|---|---|---|---|---|\n'
|
printf '|---|---|---|---|---|---|---|\n'
|
||||||
for line in "${MATCHES[@]}"; do
|
for line in "${MATCHES[@]}"; do
|
||||||
@ -229,4 +231,4 @@ fi
|
|||||||
} | out_target
|
} | out_target
|
||||||
|
|
||||||
[ -n "$OUT" ] && printf 'nc-document: wrote %s (%d matched threads across %d site(s))\n' \
|
[ -n "$OUT" ] && printf 'nc-document: wrote %s (%d matched threads across %d site(s))\n' \
|
||||||
"$OUT" "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -d ' ')" >&2
|
"$OUT" "${#MATCHES[@]}" "$(printf '%s\n' "${MATCHES[@]}" | awk -F'|' '{print $1}' | sort -u | wc -l | tr -cd '0-9')" >&2
|
||||||
|
|||||||
@ -36,6 +36,17 @@ JOURNAL="$LIB_DIR/journal.sh"
|
|||||||
die() { printf 'nc-engine: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'nc-engine: %s\n' "$*" >&2; exit 1; }
|
||||||
warn() { printf 'nc-engine: %s\n' "$*" >&2; }
|
warn() { printf 'nc-engine: %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-safety primitives (coerce_int / strip_cr / read_clean).
|
||||||
|
# Source if available; otherwise inline minimal fallbacks so a partial
|
||||||
|
# install doesn't break the y/N prompts.
|
||||||
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$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}"; }
|
||||||
|
strip_cr() { local v="${1:-}"; printf '%s' "${v//$'\r'/}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
# Source journal so journaled actions can call journal_write
|
# Source journal so journaled actions can call journal_write
|
||||||
[ -f "$JOURNAL" ] && . "$JOURNAL" || warn "journal.sh not available — actions will not be reversible"
|
[ -f "$JOURNAL" ] && . "$JOURNAL" || warn "journal.sh not available — actions will not be reversible"
|
||||||
|
|
||||||
@ -55,7 +66,10 @@ journal_action() {
|
|||||||
local action="$1" target="$2" detail="${3:-}"
|
local action="$1" target="$2" detail="${3:-}"
|
||||||
local sessdir="$LARRY_HOME/journal/${LARRY_SESSION_ID:-engine-$(date +%Y-%m-%d-%H%M%S)-$$}"
|
local sessdir="$LARRY_HOME/journal/${LARRY_SESSION_ID:-engine-$(date +%Y-%m-%d-%H%M%S)-$$}"
|
||||||
mkdir -p "$sessdir" 2>/dev/null
|
mkdir -p "$sessdir" 2>/dev/null
|
||||||
local idx; idx=$(printf '%03d' $(($(find "$sessdir" -name '[0-9]*.engine' 2>/dev/null | wc -l) + 1)))
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe can suffix the count
|
||||||
|
# with \r on a CRLF terminal which then crashes the surrounding $(( + 1 )).
|
||||||
|
local _cnt; _cnt=$(coerce_int "$(find "$sessdir" -name '[0-9]*.engine' 2>/dev/null | wc -l)" 0)
|
||||||
|
local idx; idx=$(printf '%03d' $((_cnt + 1)))
|
||||||
local entry="$sessdir/${idx}_${action}_${target//\//_}.engine"
|
local entry="$sessdir/${idx}_${action}_${target//\//_}.engine"
|
||||||
{
|
{
|
||||||
printf 'action: %s\ntarget: %s\nwhen: %s\nhost: %s\nhciroot: %s\nhcisite: %s\ndetail: %s\n' \
|
printf 'action: %s\ntarget: %s\nwhen: %s\nhost: %s\nhciroot: %s\nhcisite: %s\ndetail: %s\n' \
|
||||||
@ -111,6 +125,8 @@ run_action() {
|
|||||||
if [ "$confirm" != "yes" ]; then
|
if [ "$confirm" != "yes" ]; then
|
||||||
printf ' proceed? [y/N]: '
|
printf ' proceed? [y/N]: '
|
||||||
read -r ans </dev/tty 2>/dev/null || ans=""
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
||||||
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED by user"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED by user"; return 1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -139,6 +155,8 @@ cmd_resend() {
|
|||||||
esac
|
esac
|
||||||
printf '\nRESEND-%s thread=%s file=%s\n $ %s\n proceed? [y/N]: ' "${kind^^}" "$thread" "$file" "$cmd"
|
printf '\nRESEND-%s thread=%s file=%s\n $ %s\n proceed? [y/N]: ' "${kind^^}" "$thread" "$file" "$cmd"
|
||||||
read -r ans </dev/tty 2>/dev/null || ans=""
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
||||||
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
||||||
journal_action "resend-$kind" "$thread" "file=$file"
|
journal_action "resend-$kind" "$thread" "file=$file"
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
@ -150,6 +168,8 @@ cmd_route_test() {
|
|||||||
local cmd="$thread route_test $file"
|
local cmd="$thread route_test $file"
|
||||||
printf '\nROUTE-TEST thread=%s input=%s\n $ %s\n proceed? [y/N]: ' "$thread" "$file" "$cmd"
|
printf '\nROUTE-TEST thread=%s input=%s\n $ %s\n proceed? [y/N]: ' "$thread" "$file" "$cmd"
|
||||||
read -r ans </dev/tty 2>/dev/null || ans=""
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
||||||
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
||||||
journal_action "route-test" "$thread" "file=$file"
|
journal_action "route-test" "$thread" "file=$file"
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
@ -161,6 +181,8 @@ cmd_testxlate() {
|
|||||||
local cmd="testxlate $xlate $xltfile"
|
local cmd="testxlate $xlate $xltfile"
|
||||||
printf '\nTESTXLATE xlate=%s file=%s\n $ %s\n proceed? [y/N]: ' "$xlate" "$xltfile" "$cmd"
|
printf '\nTESTXLATE xlate=%s file=%s\n $ %s\n proceed? [y/N]: ' "$xlate" "$xltfile" "$cmd"
|
||||||
read -r ans </dev/tty 2>/dev/null || ans=""
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
||||||
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
||||||
journal_action "testxlate" "$xlate" "file=$xltfile"
|
journal_action "testxlate" "$xlate" "file=$xltfile"
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
@ -173,6 +195,8 @@ cmd_tpstest() {
|
|||||||
local cmd="tpstest $msgfile $procs"
|
local cmd="tpstest $msgfile $procs"
|
||||||
printf '\nTPSTEST msgfile=%s procs=%s\n $ %s\n proceed? [y/N]: ' "$msgfile" "$procs" "$cmd"
|
printf '\nTPSTEST msgfile=%s procs=%s\n $ %s\n proceed? [y/N]: ' "$msgfile" "$procs" "$cmd"
|
||||||
read -r ans </dev/tty 2>/dev/null || ans=""
|
read -r ans </dev/tty 2>/dev/null || ans=""
|
||||||
|
# v0.7.5: strip CR so a Cygwin pty's `Y\r` matches `^[Yy]$`.
|
||||||
|
ans="${ans//$'\r'/}"
|
||||||
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo " DENIED"; return 1; }
|
||||||
journal_action "tpstest" "$msgfile" "procs=$procs"
|
journal_action "tpstest" "$msgfile" "procs=$procs"
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
|
|||||||
@ -229,5 +229,7 @@ case "$FORMAT" in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# Emit count to stderr
|
# Emit count to stderr
|
||||||
n=$(wc -l < "$RESULTS" | tr -d ' ')
|
# v0.7.5: tr -cd '0-9' instead of tr -d ' ' — Cygwin wc.exe CR-taint defense
|
||||||
[ "$FORMAT" = "table" ] && printf '\n%d match(es)\n' "$n" >&2
|
# (printf '%d' "5\r" fails with "invalid number").
|
||||||
|
n=$(wc -l < "$RESULTS" | tr -cd '0-9')
|
||||||
|
[ "$FORMAT" = "table" ] && printf '\n%d match(es)\n' "${n:-0}" >&2
|
||||||
|
|||||||
@ -34,6 +34,16 @@ JOURNAL="$LIB_DIR/journal.sh"
|
|||||||
|
|
||||||
die() { printf 'nc-insert-protocol: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'nc-insert-protocol: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-safety primitives. awk-emitted line numbers feed
|
||||||
|
# `$((end_line - 1))` / `head -n $((start_line - 1))` / `tail -n +$((... + 1))`
|
||||||
|
# arithmetic and shell positionals; a CR-tainted awk would crash all three.
|
||||||
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$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}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
# Source journal so we can call journal_write directly
|
# Source journal so we can call journal_write directly
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$JOURNAL"
|
. "$JOURNAL"
|
||||||
@ -95,6 +105,9 @@ cmd_insert() {
|
|||||||
}
|
}
|
||||||
}' "$nc")
|
}' "$nc")
|
||||||
[ -n "$end_line" ] || die "anchor protocol not found: $anchor"
|
[ -n "$end_line" ] || die "anchor protocol not found: $anchor"
|
||||||
|
# v0.7.5: coerce_int — awk-emitted NR can have a trailing CR on
|
||||||
|
# Cygwin-awk; would crash $((end_line + 1)) arithmetic.
|
||||||
|
end_line=$(coerce_int "$end_line" 0)
|
||||||
head -n "$end_line" "$nc" > "$tmp"
|
head -n "$end_line" "$nc" > "$tmp"
|
||||||
printf '\n' >> "$tmp"
|
printf '\n' >> "$tmp"
|
||||||
cat "$block_file" >> "$tmp"
|
cat "$block_file" >> "$tmp"
|
||||||
@ -106,6 +119,8 @@ cmd_insert() {
|
|||||||
local start_line
|
local start_line
|
||||||
start_line=$("$NCP" protocol-line "$nc" "$anchor" 2>/dev/null)
|
start_line=$("$NCP" protocol-line "$nc" "$anchor" 2>/dev/null)
|
||||||
[ -n "$start_line" ] || die "anchor protocol not found: $anchor"
|
[ -n "$start_line" ] || die "anchor protocol not found: $anchor"
|
||||||
|
# v0.7.5: coerce_int — protocol-line is awk-based; CR-taint defense.
|
||||||
|
start_line=$(coerce_int "$start_line" 0)
|
||||||
head -n $((start_line - 1)) "$nc" > "$tmp"
|
head -n $((start_line - 1)) "$nc" > "$tmp"
|
||||||
cat "$block_file" >> "$tmp"
|
cat "$block_file" >> "$tmp"
|
||||||
printf '\n' >> "$tmp"
|
printf '\n' >> "$tmp"
|
||||||
@ -194,6 +209,13 @@ cmd_add_route() {
|
|||||||
fi
|
fi
|
||||||
[ -n "$dx_end" ] || die "could not locate end of DATAXLATE block in protocol $prot"
|
[ -n "$dx_end" ] || die "could not locate end of DATAXLATE block in protocol $prot"
|
||||||
|
|
||||||
|
# v0.7.5: coerce_int on every awk-emitted line number before it lands in
|
||||||
|
# arithmetic (head -n / tail -n +N) — Cygwin awk.exe CR-taint defense.
|
||||||
|
start=$(coerce_int "$start" 0)
|
||||||
|
end=$(coerce_int "$end" 0)
|
||||||
|
dx_start=$(coerce_int "$dx_start" 0)
|
||||||
|
dx_end=$(coerce_int "$dx_end" 0)
|
||||||
|
|
||||||
# Indent the route content to match the DATAXLATE inner indentation (8 spaces typical).
|
# Indent the route content to match the DATAXLATE inner indentation (8 spaces typical).
|
||||||
local indent=" "
|
local indent=" "
|
||||||
local indented_route; indented_route=$(awk -v IND="$indent" '{print IND $0}' "$route_file")
|
local indented_route; indented_route=$(awk -v IND="$indent" '{print IND $0}' "$route_file")
|
||||||
|
|||||||
@ -37,6 +37,15 @@ NC_SELF="$0"
|
|||||||
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
||||||
HL7F="$LIB_DIR/hl7-field.sh"
|
HL7F="$LIB_DIR/hl7-field.sh"
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-safety primitives (Cygwin/MobaXterm date.exe and wc.exe
|
||||||
|
# emit CR-tainted output that crashes the arithmetic in parse_time_ms).
|
||||||
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$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}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
die() { printf 'nc-msgs: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'nc-msgs: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
THREAD=""
|
THREAD=""
|
||||||
@ -107,6 +116,9 @@ locate_smatdb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Parse time expression -> unix ms
|
# Parse time expression -> unix ms
|
||||||
|
# v0.7.5: every `ts=$(date ... +%s)` capture is routed through coerce_int
|
||||||
|
# before it lands in `$((ts * 1000))` — Cygwin date.exe can produce a
|
||||||
|
# CR-tainted epoch which would crash with "invalid arithmetic operator".
|
||||||
parse_time_ms() {
|
parse_time_ms() {
|
||||||
local expr="$1"
|
local expr="$1"
|
||||||
[ -z "$expr" ] && return 0
|
[ -z "$expr" ] && return 0
|
||||||
@ -118,13 +130,15 @@ parse_time_ms() {
|
|||||||
# GNU date and BSD date differ. Try GNU first (-d EXPR), fall back to BSD (-jf or -v).
|
# GNU date and BSD date differ. Try GNU first (-d EXPR), fall back to BSD (-jf or -v).
|
||||||
local ts=""
|
local ts=""
|
||||||
if ts=$(date -d "$expr" +%s 2>/dev/null); then
|
if ts=$(date -d "$expr" +%s 2>/dev/null); then
|
||||||
printf '%s' "$((ts * 1000))"; return
|
ts=$(coerce_int "$ts" 0); printf '%s' "$((ts * 1000))"; return
|
||||||
fi
|
fi
|
||||||
# BSD date — try `-v` shorthand for relative times
|
# BSD date — try `-v` shorthand for relative times
|
||||||
if echo "$expr" | grep -qE '^[0-9]+ (second|minute|hour|day|week|month|year)s? ago$'; then
|
if echo "$expr" | grep -qE '^[0-9]+ (second|minute|hour|day|week|month|year)s? ago$'; then
|
||||||
local n unit
|
local n unit
|
||||||
n=$(echo "$expr" | awk '{print $1}')
|
n=$(echo "$expr" | awk '{print $1}')
|
||||||
unit=$(echo "$expr" | awk '{print $2}' | sed 's/s$//')
|
unit=$(echo "$expr" | awk '{print $2}' | sed 's/s$//')
|
||||||
|
# n came from awk on a shell-local string — clean it for the arithmetic below.
|
||||||
|
n=$(coerce_int "$n" 0)
|
||||||
local flag
|
local flag
|
||||||
case "$unit" in
|
case "$unit" in
|
||||||
second) flag="S" ;;
|
second) flag="S" ;;
|
||||||
@ -135,14 +149,14 @@ parse_time_ms() {
|
|||||||
month) flag="m" ;;
|
month) flag="m" ;;
|
||||||
year) flag="y" ;;
|
year) flag="y" ;;
|
||||||
esac
|
esac
|
||||||
ts=$(date -v "-${n}${flag}" +%s 2>/dev/null) && { printf '%s' "$((ts * 1000))"; return; }
|
ts=$(date -v "-${n}${flag}" +%s 2>/dev/null) && { ts=$(coerce_int "$ts" 0); printf '%s' "$((ts * 1000))"; return; }
|
||||||
fi
|
fi
|
||||||
# BSD date with -jf
|
# BSD date with -jf
|
||||||
if ts=$(date -jf "%Y-%m-%d %H:%M:%S" "$expr" +%s 2>/dev/null); then
|
if ts=$(date -jf "%Y-%m-%d %H:%M:%S" "$expr" +%s 2>/dev/null); then
|
||||||
printf '%s' "$((ts * 1000))"; return
|
ts=$(coerce_int "$ts" 0); printf '%s' "$((ts * 1000))"; return
|
||||||
fi
|
fi
|
||||||
if ts=$(date -jf "%Y-%m-%d" "$expr" +%s 2>/dev/null); then
|
if ts=$(date -jf "%Y-%m-%d" "$expr" +%s 2>/dev/null); then
|
||||||
printf '%s' "$((ts * 1000))"; return
|
ts=$(coerce_int "$ts" 0); printf '%s' "$((ts * 1000))"; return
|
||||||
fi
|
fi
|
||||||
die "could not parse time expression: $expr"
|
die "could not parse time expression: $expr"
|
||||||
}
|
}
|
||||||
@ -215,7 +229,8 @@ awk -v RS=$'\x1e' -v FS=$'\x1f' -v outdir="$TMP_OUT" '
|
|||||||
}
|
}
|
||||||
' "$TMP_OUT/raw.bin"
|
' "$TMP_OUT/raw.bin"
|
||||||
|
|
||||||
MSG_COUNT=$(ls "$TMP_OUT"/msg_*.bin 2>/dev/null | wc -l | tr -d ' ')
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe CR-taint defense.
|
||||||
|
MSG_COUNT=$(coerce_int "$(ls "$TMP_OUT"/msg_*.bin 2>/dev/null | wc -l)" 0)
|
||||||
KEPT=0
|
KEPT=0
|
||||||
|
|
||||||
# Parse a single filter expression. Returns path / op / expected via globals.
|
# Parse a single filter expression. Returns path / op / expected via globals.
|
||||||
@ -432,10 +447,14 @@ case "$FORMAT" in
|
|||||||
typ=$(printf '%s' "$meta" | awk -F'\t' '{print $2}')
|
typ=$(printf '%s' "$meta" | awk -F'\t' '{print $2}')
|
||||||
src=$(printf '%s' "$meta" | awk -F'\t' '{print $3}')
|
src=$(printf '%s' "$meta" | awk -F'\t' '{print $3}')
|
||||||
dst=$(printf '%s' "$meta" | awk -F'\t' '{print $4}')
|
dst=$(printf '%s' "$meta" | awk -F'\t' '{print $4}')
|
||||||
if [ "$tm" -gt 100000000000 ] 2>/dev/null; then
|
# v0.7.5: coerce_int on the time column before any arithmetic / integer
|
||||||
tm_h=$(date -r $((tm/1000)) 2>/dev/null || date -d "@$((tm/1000))" 2>/dev/null || echo "$tm")
|
# compare. The meta TSV is written by awk above; on Cygwin a
|
||||||
|
# Windows-native awk could emit CRLF rows and `$1` then carries a CR.
|
||||||
|
tm_i=$(coerce_int "$tm" 0)
|
||||||
|
if [ "$tm_i" -gt 100000000000 ]; then
|
||||||
|
tm_h=$(date -r $((tm_i/1000)) 2>/dev/null || date -d "@$((tm_i/1000))" 2>/dev/null || echo "$tm_i")
|
||||||
else
|
else
|
||||||
tm_h="$tm"
|
tm_h="$tm_i"
|
||||||
fi
|
fi
|
||||||
printf '===== msg %d time=%s type=%s src=%s dst=%s =====\n' "$KEPT" "$tm_h" "$typ" "$src" "$dst"
|
printf '===== msg %d time=%s type=%s src=%s dst=%s =====\n' "$KEPT" "$tm_h" "$typ" "$src" "$dst"
|
||||||
case "$FORMAT" in
|
case "$FORMAT" in
|
||||||
|
|||||||
@ -52,6 +52,16 @@ NCI="$LIB_DIR/nc-inbound.sh"
|
|||||||
NCM="$LIB_DIR/nc-msgs.sh"
|
NCM="$LIB_DIR/nc-msgs.sh"
|
||||||
HL7DIFF="$LIB_DIR/hl7-diff.sh"
|
HL7DIFF="$LIB_DIR/hl7-diff.sh"
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-safety primitives (coerce_int). The phase loops use
|
||||||
|
# `wc -c | tr -d ' '` to count delimiter bytes — Cygwin wc.exe can leak \r
|
||||||
|
# which then crashes the `[ "$got" -ge "$COUNT" ]` integer test.
|
||||||
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$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}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
die() { printf 'nc-regression: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'nc-regression: %s\n' "$*" >&2; exit 1; }
|
||||||
say() { printf 'nc-regression: %s\n' "$*" >&2; }
|
say() { printf 'nc-regression: %s\n' "$*" >&2; }
|
||||||
|
|
||||||
@ -286,7 +296,8 @@ phase_2() {
|
|||||||
say " [dry-run] would sample $COUNT msgs from $thread → $input"
|
say " [dry-run] would sample $COUNT msgs from $thread → $input"
|
||||||
else
|
else
|
||||||
HCISITEDIR="$sitedir" "$NCM" "$thread" --limit "$COUNT" --format raw > "$input" 2>/dev/null
|
HCISITEDIR="$sitedir" "$NCM" "$thread" --limit "$COUNT" --format raw > "$input" 2>/dev/null
|
||||||
local got; got=$(tr -cd $'\x1c' < "$input" | wc -c | tr -d ' ')
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe can emit \r.
|
||||||
|
local got; got=$(coerce_int "$(tr -cd $'\x1c' < "$input" | wc -c)" 0)
|
||||||
say " sampled $thread → $input ($got messages)"
|
say " sampled $thread → $input ($got messages)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@ -439,7 +450,10 @@ phase_5() {
|
|||||||
"$HL7DIFF" "${diff_args[@]}" --format text "$destfile" "$b_pair" 2>/dev/null || true
|
"$HL7DIFF" "${diff_args[@]}" --format text "$destfile" "$b_pair" 2>/dev/null || true
|
||||||
} > "$report"
|
} > "$report"
|
||||||
echo "| \`$thread\` | \`$destname\` | $count | [report](./$(basename "$report")) |" >> "$diff_index"
|
echo "| \`$thread\` | \`$destname\` | $count | [report](./$(basename "$report")) |" >> "$diff_index"
|
||||||
total_diff=$((total_diff + count))
|
# v0.7.5: coerce_int — defends against (a) Cygwin awk emitting a CR-
|
||||||
|
# tainted count from hl7-diff and (b) the legitimate `?` fallback above
|
||||||
|
# which would otherwise crash `$((total_diff + ?))` arithmetic.
|
||||||
|
total_diff=$((total_diff + $(coerce_int "$count" 0)))
|
||||||
say " $thread → $destname: $count diff(s)"
|
say " $thread → $destname: $count diff(s)"
|
||||||
done < <(find "$a_dir" -maxdepth 1 -type f 2>/dev/null)
|
done < <(find "$a_dir" -maxdepth 1 -type f 2>/dev/null)
|
||||||
done < "$OUT/inbounds.txt"
|
done < "$OUT/inbounds.txt"
|
||||||
|
|||||||
@ -28,6 +28,15 @@ NCM="$LIB_DIR/nc-msgs.sh"
|
|||||||
HL7F="$LIB_DIR/hl7-field.sh"
|
HL7F="$LIB_DIR/hl7-field.sh"
|
||||||
HL7DIFF="$LIB_DIR/hl7-diff.sh"
|
HL7DIFF="$LIB_DIR/hl7-diff.sh"
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-safety primitives — A_COUNT/B_COUNT/DIFFS_TOTAL all feed
|
||||||
|
# `%d` printf and arithmetic; Cygwin wc.exe CR-taint would crash them.
|
||||||
|
if [ -r "$LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$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}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
die() { printf 'nc-smat-diff: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'nc-smat-diff: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
THREAD=""
|
THREAD=""
|
||||||
@ -101,8 +110,9 @@ printf 'nc-smat-diff:\n thread: %s\n A: %s/%s\n B: %s/%s\n limit: %d ignore
|
|||||||
dump_side "$ENV_A" "$SITE_A" "$OUT/a"
|
dump_side "$ENV_A" "$SITE_A" "$OUT/a"
|
||||||
dump_side "$ENV_B" "$SITE_B" "$OUT/b"
|
dump_side "$ENV_B" "$SITE_B" "$OUT/b"
|
||||||
|
|
||||||
A_COUNT=$(wc -l < "$OUT/a/index.tsv" | tr -d ' ')
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe CR-taint defense.
|
||||||
B_COUNT=$(wc -l < "$OUT/b/index.tsv" | tr -d ' ')
|
A_COUNT=$(coerce_int "$(wc -l < "$OUT/a/index.tsv")" 0)
|
||||||
|
B_COUNT=$(coerce_int "$(wc -l < "$OUT/b/index.tsv")" 0)
|
||||||
printf 'A: %d msgs B: %d msgs\n\n' "$A_COUNT" "$B_COUNT" >&2
|
printf 'A: %d msgs B: %d msgs\n\n' "$A_COUNT" "$B_COUNT" >&2
|
||||||
|
|
||||||
# Pair messages by key
|
# Pair messages by key
|
||||||
@ -145,7 +155,9 @@ while IFS= read -r key; do
|
|||||||
"$HL7DIFF" --ignore "$IGNORE" "$a_files" "$b_files" 2>/dev/null
|
"$HL7DIFF" --ignore "$IGNORE" "$a_files" "$b_files" 2>/dev/null
|
||||||
} > "$report"
|
} > "$report"
|
||||||
echo "| \`$key\` | $cnt | [report](./diff/$(basename "$report")) |" >> "$SUMMARY"
|
echo "| \`$key\` | $cnt | [report](./diff/$(basename "$report")) |" >> "$SUMMARY"
|
||||||
DIFFS_TOTAL=$((DIFFS_TOTAL + ${cnt:-0}))
|
# v0.7.5: coerce_int — defends against both Cygwin awk CR-taint AND the
|
||||||
|
# legitimate `?` fallback that would crash arithmetic.
|
||||||
|
DIFFS_TOTAL=$((DIFFS_TOTAL + $(coerce_int "$cnt" 0)))
|
||||||
fi
|
fi
|
||||||
done < <(printf '%s\n%s\n' "$A_KEYS" "$B_KEYS" | sort -u)
|
done < <(printf '%s\n%s\n' "$A_KEYS" "$B_KEYS" | sort -u)
|
||||||
|
|
||||||
|
|||||||
@ -123,7 +123,10 @@ modify_via_csv() {
|
|||||||
journal_write "$target" "$new"
|
journal_write "$target" "$new"
|
||||||
else
|
else
|
||||||
# No journal — direct write with simple backup
|
# No journal — direct write with simple backup
|
||||||
[ -f "$target" ] && cp -p "$target" "${target}.larry-bak.$(date +%s)"
|
# v0.7.5: strip non-digits from date — Cygwin date.exe could yield
|
||||||
|
# "1779999999\r" which would produce a path with embedded CR.
|
||||||
|
local _ts; _ts=$(date +%s | tr -cd '0-9')
|
||||||
|
[ -f "$target" ] && cp -p "$target" "${target}.larry-bak.${_ts:-0}"
|
||||||
mv "$new" "$target"
|
mv "$new" "$target"
|
||||||
echo "(no journal available; backup at ${target}.larry-bak.<ts>)"
|
echo "(no journal available; backup at ${target}.larry-bak.<ts>)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
69
lib/oauth.sh
69
lib/oauth.sh
@ -100,6 +100,25 @@ jqf() {
|
|||||||
jq "$@" < "$file" | tr -d '\r'
|
jq "$@" < "$file" | tr -d '\r'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# v0.7.5: coerce_int / strip_cr / read_clean have moved to lib/cygwin-safe.sh
|
||||||
|
# so the entire cloverleaf-larry tool family can share the same CR-defense
|
||||||
|
# primitives. Source it from a sibling lib/ directory; fall back to a local
|
||||||
|
# definition if the file is missing (defensive — oauth.sh ships as part of
|
||||||
|
# the same MANIFEST so cygwin-safe.sh should always be on disk too).
|
||||||
|
_OAUTH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||||
|
if [ -r "$_OAUTH_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$_OAUTH_DIR/cygwin-safe.sh"
|
||||||
|
else
|
||||||
|
# Inlined fallback — keep oauth.sh self-contained if a partial install
|
||||||
|
# delivers oauth.sh but not cygwin-safe.sh.
|
||||||
|
coerce_int() {
|
||||||
|
local raw="${1:-}" default="${2:-0}"
|
||||||
|
local cleaned; cleaned=$(printf '%s' "$raw" | tr -cd '0-9')
|
||||||
|
printf '%s' "${cleaned:-$default}"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
urlenc() {
|
urlenc() {
|
||||||
# Minimal RFC3986-ish URL encoder for the bits we need (spaces, /, :)
|
# Minimal RFC3986-ish URL encoder for the bits we need (spaces, /, :)
|
||||||
local s="$1"
|
local s="$1"
|
||||||
@ -206,7 +225,10 @@ EOF
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local now; now=$(date +%s)
|
# v0.7.5: coerce_int on date +%s — same Cygwin CR-taint defense applied
|
||||||
|
# to the cmd_ensure operands. Passing "1779999999\r" to jq's `tonumber`
|
||||||
|
# fails with "tonumber requires string|number, got string ...".
|
||||||
|
local now; now=$(coerce_int "$(date +%s)" 0)
|
||||||
umask 077
|
umask 077
|
||||||
# Write to a .new sidecar first, validate it parses + has the required keys,
|
# Write to a .new sidecar first, validate it parses + has the required keys,
|
||||||
# then atomically mv into place. If jq fails mid-pipe, $OAUTH_FILE is never
|
# then atomically mv into place. If jq fails mid-pipe, $OAUTH_FILE is never
|
||||||
@ -247,7 +269,8 @@ cmd_refresh() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local now; now=$(date +%s)
|
# v0.7.5: coerce_int on date +%s — see cmd_login.
|
||||||
|
local now; now=$(coerce_int "$(date +%s)" 0)
|
||||||
# Force tight perms on the sidecar at create() time — cmd_refresh historically
|
# Force tight perms on the sidecar at create() time — cmd_refresh historically
|
||||||
# didn't set umask before the tempfile write, so the .new file inherited the
|
# didn't set umask before the tempfile write, so the .new file inherited the
|
||||||
# process default (often 0022 → 0644). On Cygwin where chmod-after-the-fact
|
# process default (often 0022 → 0644). On Cygwin where chmod-after-the-fact
|
||||||
@ -307,14 +330,18 @@ cmd_ensure() {
|
|||||||
local fetched_at expires_in
|
local fetched_at expires_in
|
||||||
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
||||||
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
||||||
# Extra paranoia (v0.6.6): the v0.6.5 /oauth-debug surfaced "syntax error:
|
# v0.7.5: coerce EVERY operand that will enter an arithmetic / integer-test
|
||||||
# invalid arithmetic operator" — diagnosed as CR contamination from CRLF
|
# context — fetched_at, expires_in, AND now — through coerce_int. The first
|
||||||
# JSON files (now stripped in jqf). Belt-and-suspenders: coerce via printf
|
# two come from a JSON file (jqf already strips \r but garbage values are
|
||||||
# to a pure integer so ANY residual junk (whitespace, NULs, alpha glitches)
|
# still possible). The third comes from `date +%s`, which on a Cygwin pty
|
||||||
# falls back to 0 instead of crashing the arithmetic expression.
|
# where Windows-native date.exe shadows Cygwin date can return a CR-tainted
|
||||||
fetched_at=$(printf '%d' "${fetched_at:-0}" 2>/dev/null || echo 0)
|
# value like "1779999999\r" — that lands one line later in `$((expires_at -
|
||||||
expires_in=$(printf '%d' "${expires_in:-3600}" 2>/dev/null || echo 3600)
|
# now))` and crashes with "invalid arithmetic operator (error token is "")".
|
||||||
local now; now=$(date +%s)
|
# See Deliverables/2026-05-27-cloverleaf-larry-oauth-arithmetic-fix.md for
|
||||||
|
# the full diagnosis from Bryan's v0.7.3 work-box trace.
|
||||||
|
fetched_at=$(coerce_int "$fetched_at" 0)
|
||||||
|
expires_in=$(coerce_int "$expires_in" 3600)
|
||||||
|
local now; now=$(coerce_int "$(date +%s)" 0)
|
||||||
local expires_at=$((fetched_at + expires_in))
|
local expires_at=$((fetched_at + expires_in))
|
||||||
local left=$((expires_at - now))
|
local left=$((expires_at - now))
|
||||||
dbg "ensure: fetched_at=$fetched_at expires_in=$expires_in now=$now expires_at=$expires_at left=${left}s"
|
dbg "ensure: fetched_at=$fetched_at expires_in=$expires_in now=$now expires_at=$expires_at left=${left}s"
|
||||||
@ -350,9 +377,11 @@ cmd_status() {
|
|||||||
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
||||||
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
||||||
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(unknown)"')
|
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(unknown)"')
|
||||||
fetched_at=$(printf '%d' "${fetched_at:-0}" 2>/dev/null || echo 0)
|
# v0.7.5: same coerce_int discipline as cmd_ensure — see the comment there
|
||||||
expires_in=$(printf '%d' "${expires_in:-3600}" 2>/dev/null || echo 3600)
|
# for the Cygwin CR-on-date crash diagnosis.
|
||||||
local now; now=$(date +%s)
|
fetched_at=$(coerce_int "$fetched_at" 0)
|
||||||
|
expires_in=$(coerce_int "$expires_in" 3600)
|
||||||
|
local now; now=$(coerce_int "$(date +%s)" 0)
|
||||||
local expires_at=$((fetched_at + expires_in))
|
local expires_at=$((fetched_at + expires_in))
|
||||||
local left=$((expires_at - now))
|
local left=$((expires_at - now))
|
||||||
printf 'OAuth status:\n'
|
printf 'OAuth status:\n'
|
||||||
@ -423,8 +452,10 @@ cmd_debug() {
|
|||||||
# trailing \r on each line propagates through `jq -r` and crashes downstream
|
# trailing \r on each line propagates through `jq -r` and crashes downstream
|
||||||
# bash arithmetic. Show byte counts of CR vs LF so we can spot CRLF taint.
|
# bash arithmetic. Show byte counts of CR vs LF so we can spot CRLF taint.
|
||||||
local cr_count lf_count
|
local cr_count lf_count
|
||||||
cr_count=$(tr -dc '\r' < "$OAUTH_FILE" | wc -c | tr -d ' ')
|
# v0.7.5: coerce_int on wc output — Cygwin wc -c can emit trailing \r on
|
||||||
lf_count=$(tr -dc '\n' < "$OAUTH_FILE" | wc -c | tr -d ' ')
|
# a CRLF terminal which would tank the `-gt 0` integer test downstream.
|
||||||
|
cr_count=$(coerce_int "$(tr -dc '\r' < "$OAUTH_FILE" | wc -c)" 0)
|
||||||
|
lf_count=$(coerce_int "$(tr -dc '\n' < "$OAUTH_FILE" | wc -c)" 0)
|
||||||
printf ' line endings: CR=%s LF=%s' "$cr_count" "$lf_count"
|
printf ' line endings: CR=%s LF=%s' "$cr_count" "$lf_count"
|
||||||
if [ "$cr_count" -gt 0 ]; then
|
if [ "$cr_count" -gt 0 ]; then
|
||||||
printf ' (CRLF detected — v0.6.6 jqf strips these defensively)\n'
|
printf ' (CRLF detected — v0.6.6 jqf strips these defensively)\n'
|
||||||
@ -438,10 +469,10 @@ cmd_debug() {
|
|||||||
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
fetched_at=$(jqf "$OAUTH_FILE" -r '.fetched_at // 0')
|
||||||
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
expires_in=$(jqf "$OAUTH_FILE" -r '.expires_in // 3600')
|
||||||
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(missing)"')
|
scope=$(jqf "$OAUTH_FILE" -r '.scope // "(missing)"')
|
||||||
# Belt-and-suspenders integer coercion (see cmd_ensure for the why).
|
# v0.7.5: coerce_int on all three operands (see cmd_ensure for the why).
|
||||||
fetched_at=$(printf '%d' "${fetched_at:-0}" 2>/dev/null || echo 0)
|
fetched_at=$(coerce_int "$fetched_at" 0)
|
||||||
expires_in=$(printf '%d' "${expires_in:-3600}" 2>/dev/null || echo 3600)
|
expires_in=$(coerce_int "$expires_in" 3600)
|
||||||
local now; now=$(date +%s)
|
local now; now=$(coerce_int "$(date +%s)" 0)
|
||||||
local expires_at=$((fetched_at + expires_in))
|
local expires_at=$((fetched_at + expires_in))
|
||||||
local left=$((expires_at - now))
|
local left=$((expires_at - now))
|
||||||
local refresh_at=$((expires_at - 300))
|
local refresh_at=$((expires_at - 300))
|
||||||
|
|||||||
@ -44,6 +44,17 @@ die() { printf 'ssh-helper: %s\n' "$*" >&2; exit 1; }
|
|||||||
warn() { printf 'ssh-helper: warn: %s\n' "$*" >&2; }
|
warn() { printf 'ssh-helper: warn: %s\n' "$*" >&2; }
|
||||||
ok() { printf 'ssh-helper: %s\n' "$*"; }
|
ok() { printf 'ssh-helper: %s\n' "$*"; }
|
||||||
|
|
||||||
|
# v0.7.5: shared CR-safety primitives. pull/push use `wc -c | tr -d ' '` to
|
||||||
|
# verify byte counts — Cygwin wc.exe can pass through \r and tank the
|
||||||
|
# `[ "$got" != "$local_size" ]` comparison.
|
||||||
|
_SSH_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||||
|
if [ -r "$_SSH_LIB_DIR/cygwin-safe.sh" ]; then
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
|
. "$_SSH_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}"; }
|
||||||
|
fi
|
||||||
|
|
||||||
ensure_layout() {
|
ensure_layout() {
|
||||||
mkdir -p "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null
|
mkdir -p "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null
|
||||||
chmod 700 "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null || true
|
chmod 700 "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null || true
|
||||||
@ -72,7 +83,9 @@ cmd_help() {
|
|||||||
|
|
||||||
cmd_hosts() {
|
cmd_hosts() {
|
||||||
ensure_layout
|
ensure_layout
|
||||||
if [ "$(wc -l < "$SSH_HOSTS_FILE")" -le 1 ]; then
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe CR-taint would tank
|
||||||
|
# the `-le 1` integer test below.
|
||||||
|
if [ "$(coerce_int "$(wc -l < "$SSH_HOSTS_FILE")" 0)" -le 1 ]; then
|
||||||
echo "no hosts configured. Add with: ssh-helper.sh add <alias> <user@host[:port]>"
|
echo "no hosts configured. Add with: ssh-helper.sh add <alias> <user@host[:port]>"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@ -293,9 +306,10 @@ cmd_pull() {
|
|||||||
mkdir -p "$(dirname "$local_path")" 2>/dev/null
|
mkdir -p "$(dirname "$local_path")" 2>/dev/null
|
||||||
|
|
||||||
# Get remote file size up-front for a partial-transfer sanity check.
|
# Get remote file size up-front for a partial-transfer sanity check.
|
||||||
|
# v0.7.5: coerce_int on wc output — strips CR + non-digits at the source.
|
||||||
local remote_size=""
|
local remote_size=""
|
||||||
remote_size=$(ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" \
|
remote_size=$(coerce_int "$(ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" \
|
||||||
"wc -c < $(printf '%q' "$remote") 2>/dev/null" 2>/dev/null | tr -d ' ')
|
"wc -c < $(printf '%q' "$remote") 2>/dev/null" 2>/dev/null)" "")
|
||||||
if [ -z "$remote_size" ] || ! [[ "$remote_size" =~ ^[0-9]+$ ]]; then
|
if [ -z "$remote_size" ] || ! [[ "$remote_size" =~ ^[0-9]+$ ]]; then
|
||||||
die "remote file not found or not readable: $remote"
|
die "remote file not found or not readable: $remote"
|
||||||
fi
|
fi
|
||||||
@ -308,7 +322,8 @@ cmd_pull() {
|
|||||||
-o "BatchMode=yes" \
|
-o "BatchMode=yes" \
|
||||||
-P "$_RH_PORT" \
|
-P "$_RH_PORT" \
|
||||||
"$_RH_ADDR:$remote" "$local_path" 2>"$scp_err"; then
|
"$_RH_ADDR:$remote" "$local_path" 2>"$scp_err"; then
|
||||||
local got; got=$(wc -c < "$local_path" 2>/dev/null | tr -d ' ')
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe CR-taint defense.
|
||||||
|
local got; got=$(coerce_int "$(wc -c < "$local_path" 2>/dev/null)" 0)
|
||||||
if [ "$got" != "$remote_size" ]; then
|
if [ "$got" != "$remote_size" ]; then
|
||||||
rm -f "$scp_err"
|
rm -f "$scp_err"
|
||||||
die "partial transfer: remote=$remote_size bytes, local=$got bytes ($local_path)"
|
die "partial transfer: remote=$remote_size bytes, local=$got bytes ($local_path)"
|
||||||
@ -334,7 +349,8 @@ cmd_push() {
|
|||||||
[ -f "$local_path" ] || die "local file not found: $local_path"
|
[ -f "$local_path" ] || die "local file not found: $local_path"
|
||||||
_resolve_open_master "$alias"
|
_resolve_open_master "$alias"
|
||||||
|
|
||||||
local local_size; local_size=$(wc -c < "$local_path" 2>/dev/null | tr -d ' ')
|
# v0.7.5: coerce_int on wc output — Cygwin wc.exe CR-taint defense.
|
||||||
|
local local_size; local_size=$(coerce_int "$(wc -c < "$local_path" 2>/dev/null)" 0)
|
||||||
local scp_err; scp_err=$(mktemp 2>/dev/null || echo "/tmp/larry-scp.err.$$")
|
local scp_err; scp_err=$(mktemp 2>/dev/null || echo "/tmp/larry-scp.err.$$")
|
||||||
if scp -q \
|
if scp -q \
|
||||||
-o "ControlPath=$_RH_SOCK" \
|
-o "ControlPath=$_RH_SOCK" \
|
||||||
@ -344,8 +360,9 @@ cmd_push() {
|
|||||||
"$local_path" "$_RH_ADDR:$remote" 2>"$scp_err"; then
|
"$local_path" "$_RH_ADDR:$remote" 2>"$scp_err"; then
|
||||||
# Validate via remote wc -c.
|
# Validate via remote wc -c.
|
||||||
local got
|
local got
|
||||||
got=$(ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" \
|
# v0.7.5: coerce_int on wc output (Cygwin wc.exe CR-taint defense).
|
||||||
"wc -c < $(printf '%q' "$remote") 2>/dev/null" 2>/dev/null | tr -d ' ')
|
got=$(coerce_int "$(ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" \
|
||||||
|
"wc -c < $(printf '%q' "$remote") 2>/dev/null" 2>/dev/null)" 0)
|
||||||
if [ "$got" != "$local_size" ]; then
|
if [ "$got" != "$local_size" ]; then
|
||||||
rm -f "$scp_err"
|
rm -f "$scp_err"
|
||||||
die "partial transfer: local=$local_size bytes, remote=$got bytes ($alias:$remote)"
|
die "partial transfer: local=$local_size bytes, remote=$got bytes ($alias:$remote)"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user