• docs/v322_new.md src/sbbs3/zmodem.c

    From Rob Swindell (on Debian Linux)@1:103/705 to Git commit to main/sbbs/master on Tue Aug 25 01:12:30 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/fd038d9dd3cba68b4d95d026
    Modified Files:
    docs/v322_new.md src/sbbs3/zmodem.c
    Log Message:
    zmodem: escape CR unconditionally when ESCCTL is negotiated

    A sender with ESCCTL negotiated escaped every control character
    except carriage return, which it routed to a conditional rule --
    escape only when the previous byte sent was '@' -- and otherwise put
    on the wire raw. A receiver that requested ESCCTL discards unescaped
    control characters by definition, so it discarded those CRs, every
    subpacket carrying one failed its CRC, and the transfer never
    advanced: the sender ZRPOSed to zero until it spent its error budget
    and gave up. With random data that is any subpacket at all. A
    download to any receiver requesting ESCCTL was therefore impossible,
    not merely slow.

    lrzsz settles what the rule should be. In zsendline_init() (zm.c) the
    table entry for 015/0215 is:

    if (Zctlesc) tab = 1; /* always escape */
    else if (!turbo_escape) tab = 2; /* the conditional '@' rule */
    else tab = 0;

    So the conditional rule is the NON-ESCCTL case, and ESCCTL escapes CR
    like any other control character. This engine had the two swapped:
    the conditional rule was reachable only with ESCCTL on, where it is
    wrong, and never reachable with ESCCTL off, where it would be right.
    Since it could not fire correctly in either state, drop it -- fold CR
    into the control class and let the escape mask decide, which is
    exactly lrzsz's behaviour for both states. The '@' rule can be added
    properly later if wanted; the last_sent field it read is left in
    place, being public.

    The unescaped path is provably untouched: without ESCCTL neither the
    old CR class nor the new one is in the active mask, so CR is a normal
    byte either way. Confirmed byte-for-byte -- a 4 MB send still puts
    4,311,816 bytes on the wire, 256 MB still receives at 484.99 MB/s and
    sends at 206.24 MB/s, and the 3e-6 corruption gate still passes 5 of 5 receiving and 3 of 3 sending.

    With the fix, ESCCTL interoperates in every direction: sexyz to lrz,
    lrz to sexyz, and sexyz to itself all verify byte-identical. The
    strongest evidence it is now correct rather than merely accepted is
    the wire size -- sexyz puts 5,245,034 bytes on the wire where lsz puts 5,245,030 for the same file, so the two escape the same byte set.
    Random data exercises every one of the 256 values, so nothing is
    escaped by luck.

    Under injected errors the escaped path is a little less robust than
    the plain one: 4 of 5 at 3e-6 and 5 of 5 at 1e-6, against 5 of 5 for
    the unescaped path at 3e-6. It carries 25 % more bytes and so takes
    25 % more corruption at a given per-byte rate. No lrzsz baseline
    exists for that comparison -- lsz -e to lrz -e does not survive the
    harness's error injection at all, dying inside 0.04 s on every
    attempt.
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)