awehttam / zmodem-php
Pure-PHP ZMODEM (sz/rz) file transfer protocol implementation for telnet/SSH-style byte streams.
Requires
- php: >=8.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A pure-PHP implementation of the ZMODEM (sz/rz) file transfer protocol,
extracted from BinktermPHP's
telnet/SSH terminal server.
Implements both the sender (sz — server sends a file to the client) and
receiver (rz — server receives a file from the client) sides of the
protocol, as documented in ZMODEM.DOC by Chuck Forsberg (1988), against any
PHP stream resource — telnet sockets, SSH channel streams, etc.
Installation
composer require awehttam/zmodem-php
Usage
use Zmodem\ZmodemTransfer; // Send a file to the connected client ($conn is a PHP stream resource, // e.g. from stream_socket_accept() or an SSH channel stream). ZmodemTransfer::send($conn, '/path/to/file.zip', 'file.zip'); // Receive a file uploaded by the client into a destination directory. $savedPath = ZmodemTransfer::receive($conn, '/path/to/uploads');
Pass $escapeTelnetIac = true as the trailing argument on plain Telnet
connections so that 0xFF bytes in binary file data are doubled per the
Telnet IAC escaping rule. Omit it (or pass false) for SSH or raw socket
connections.
ZmodemTransfer::send($conn, $path, $name, escapeTelnetIac: true); ZmodemTransfer::receive($conn, $destDir, escapeTelnetIac: true);
CLI Tools
sz.php and rz.php in the repo root are standalone command-line tools
modeled on the classic Unix sz/rz utilities. They communicate over
stdin/stdout, so they're meant to be run with stdin/stdout connected to the
remote ZMODEM peer — e.g. over a serial line, a telnet/SSH session, or from
a terminal emulator with ZMODEM autodetect (in which case the emulator
intercepts the ZMODEM stream and drives its own file dialog).
Internally they wrap STDIN (read) and STDOUT (write) into a single
bidirectional resource via Zmodem\StdioDuplexStream
(src/StdioDuplexStream.php), since send()/receive() operate on one
$conn used for both directions.
# Send a file php sz.php [--iac] <file> # Receive a file into a directory (default: current directory) php rz.php [--iac] [dest-dir]
Pass --iac only on plain-telnet links (see the IAC escaping note above).
Configuration
Configuration is read via Zmodem\Config::env(), which checks process
environment variables by default:
| Variable | Default | Purpose |
|---|---|---|
ZMODEM_DEBUG |
false |
Verbose frame-level protocol logging |
ZMODEM_LOG_FILE |
(auto) | Log file path (defaults to zdebug.log in the project root) |
To inject configuration values programmatically instead of via environment variables (e.g. from a host application's own config store):
use Zmodem\Config; Config::set('ZMODEM_DEBUG', 'true'); Config::set('ZMODEM_LOG_FILE', '/var/log/myapp/zmodem.log');
Development
Project layout
src/
ZmodemTransfer.php Protocol implementation (send/receive, framing, CRC, I/O)
Config.php Environment/config resolver (getenv() + runtime overrides)
StdioDuplexStream.php stdin/stdout → single bidirectional stream adapter
sz.php CLI sender built on ZmodemTransfer::send()
rz.php CLI receiver built on ZmodemTransfer::receive()
zdebug.log Default log file (debug/info logging), created on demand
There's no build step — this is a plain PSR-4 library (Zmodem\ → src/).
Clone the repo and either composer require it as a path repository in a
consuming project, or require the two files in src/ directly (see
sz.php/rz.php for an example that does this without Composer at all).
How send()/receive() work
Both methods take a single PHP stream resource ($conn) representing an
already-connected, full-duplex link to the remote peer — a telnet socket, an
SSH channel stream, or an adapter like StdioDuplexStream. The library does
not open connections itself; it only reads and writes framed ZMODEM data on
whatever resource it's given via fread()/fwrite()/stream_select().
The core protocol state machine lives entirely in ZmodemTransfer.php:
- Header building/parsing (
sendHexHeader/sendBinHeader,parseHexHeader/parseBinHeader) - Data subpacket framing with ZDLE escaping and CRC-16 (
sendDataSubpacket,receiveFileData) - The
send()/receive()methods drive the frame exchange (ZRQINIT → ZRINIT → ZFILE → ZRPOS → ZDATA/ZEOF → ZFIN) and handle retries/resync against real-world client quirks (documented inline where non-obvious).
Debugging
Set ZMODEM_DEBUG=true (or Config::set('ZMODEM_DEBUG', 'true')) to get
frame-level logging of every header sent/received, written to
ZMODEM_LOG_FILE (default zdebug.log in the project root). This is the fastest way
to diagnose interop issues with a specific terminal client — compare the
logged frame sequence against ZMODEM.DOC.
Testing changes
There's no automated test suite yet. The most direct way to exercise a
change is a loopback transfer using the CLI tools — bridge sz.php's
stdout/stdin to rz.php's stdin/stdout (e.g. via two proc_open() calls
piped together, or two real terminals/PTYs) and diff the received file
against the source. When testing against real terminal emulators (SecureCRT,
SyncTERM, ZOC, etc.), enable ZMODEM_DEBUG on both ends if possible to
compare frame traces.
License
MIT