Conversation
- Built core ZMODEM framing, encoding, and CRC verification directly in Go - Created sender and receiver state machines to orchestrate transfers - Added custom io stream interceptor in SSH client via `--zmodem` flag - Leveraged ServiceWorker StreamHelper for memory-efficient downloads - Updated README.md documentation
…rtial stream routing - Introduced a 6-byte sliding window to correctly identify ZMODEM sequences split across arbitrary `Write()` boundaries - Ensured bytes preceding the sequence continue to terminal output, preventing visual data loss - Echo ZMODEM start sequences directly to the terminal for parity with other terminal emulators - Eliminated deadlocks by properly unlocking the filter mutex before interacting with the ZMODEM pipe Extract zmodem intercept filter to native code and add tests - Split WASM-dependent API hooks into zmodem_wasm.go - Preserved intercept logic in zmodem_filter.go for native testability - Added TestZmodemFilterSplitSignature to verify 6-byte sliding window - Added TestZmodemFilterFallback to ensure safe terminal passthrough on broken pipes Discard and warn on user input during active ZMODEM transfers - Replaced naive io.Copy with a custom read loop for terminal input - Input is actively discarded while f.active is true to prevent protocol corruption - Added a one-time terminal warning if the user attempts to type during a transfer Allow users to interactively abort ZMODEM transfers via Ctrl-C - Updated keyboard input interceptor to scan for ETX (`\x03`) - Sends the standard 5 consecutive ZCAN bytes to the remote SSH server to abort the remote process - Force-closes the local io.Pipe to immediately unblock and abort the local ZMODEM parser state machine - Updated terminal warning text to indicate Ctrl-C can be used
…NIT headers - Removed the superfluous ZRQINIT pulse at the start of Send() which forced receivers to reply with a second ZRINIT - Upgraded the Send state machine to track currentFile pointer across reads - Safely skip duplicate/queued ZRINIT headers received while actively negotiating a ZFILE transfer - Properly reset currentFile state on ZEOF or ZSKIP to pop the next file sequentially
- Removed all leading `\r\n` prefixes from print statements to eliminate double-spaced blank lines in the terminal output - Added dynamic pluralization logic to properly print '1 file'/'2 files' and '1 byte'/'2 bytes' during transfers Update CHANGELOG.md with ZMODEM feature
- frame.go: ZCAN detection now requires 5 consecutive CAN bytes per spec - receive.go: Wait for onFile callback via channel, propagate its error; use uint32 for offset to match protocol semantics - send.go: Document that ZRPOS offset is not used (always sends from 0) - zmodem_filter.go: Unexport TerminalPrinter; fix TOCTOU race by capturing pipe writer under lock; fix Ctrl-C race by setting f.active=false immediately and closing write side of pipe - zmodem_filter_test.go: Add missing copyright header - session_test.go: Remove polling loop (now unnecessary since Receive waits for onFile); remove unused time import
The cancel sequence was incorrectly sending the ZCAN frame type constant (0x10) instead of the ASCII CAN character (0x18). The standard ZMODEM cancel is 8 CAN bytes + 8 backspaces. - Added CAN constant and CancelSeq variable to the zmodem package - Updated both the Ctrl-C handler and file-picker cancel to use CancelSeq - Reset the sliding window after cancel to prevent buffered ZRINIT bytes from immediately re-triggering the interceptor
rthellend
marked this pull request as ready for review
August 28, 2026 22:01
…l-based dataReader
…filter Correctness fixes: - frame.go, data.go: cancel detection compared the byte after a ZDLE against zCAN (frame type 16) instead of the ASCII CAN byte (0x18), so the whole cancel path was dead code and a remote abort decoded as data byte 'X'. Both sites now share a checkCancel helper that counts consecutive CANs (starting at 2, since ZDLE and CAN are the same byte) and pushes back the byte that ends a short run. - send.go: an unexpected ZRPOS (a retransmission request after ZEOF, or one following a ZSKIP) dereferenced a nil currentFile and panicked, which in WASM tears down the whole terminal app. It is now ignored. - receive.go: the `continue` handling a ZEOF that arrives in place of ZDATA targeted the inner wait loop, so the following ZFILE or ZFIN aborted the session. The outer loop is now labeled. - receive.go: ZCRCW means the frame ends and the sender waits for a ZACK, not that the file ends. Only ZCRCE ends the file now; on ZCRCW the reader ACKs and crosses into the next ZDATA frame (or consumes the ZEOF header itself). - send.go: the last subpacket was detected with the declared file size, but jsutil.StreamReader never returns a non-zero count together with io.EOF, so any mismatch with File.size desynchronized the receiver. Chunks are now held back one read so ZCRCE lands on the actual end of stream. - data.go: bound subpacket buffering so a peer that never sends a frame-end marker cannot exhaust memory. Concurrency fixes: - filter.go: pipeR/pipeW were read outside the mutex, and a session goroutine cleared f.active before closing its pipe, letting a signature arriving in that window start a second session on top of the first. The pipes are now captured under the lock, and finish() closes the pipe before clearing f.active. - filter.go: the Ctrl-C handler wrote the cancel sequence to stdinW while holding f.mu. stdinW is only drained by Filter.Read, so that write can block forever and freeze all terminal output. It now happens after unlocking. Tests: protocol_test.go drives the real send/receive against scripted peers to cover the ZCRCW, empty-file, stray-ZRPOS and size-mismatch paths; unit tests cover cancel detection and the subpacket size limit. mockTerminal needed a mutex because the filter writes to the terminal from two goroutines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DNhvFpURUVmKbc8cLE87f
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
--zmodemflag