Search by

awehttam / zmodem-php

awehttam

Pure-PHP ZMODEM (sz/rz) file transfer protocol implementation for telnet/SSH-style byte streams.

Package info

github.com/awehttam/zmodem-php

pkg:composer/awehttam/zmodem-php

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-27 05:56 UTC

This package is auto-updated.

Last update: 2026-08-27 06:11:53 UTC


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