1
0
Fork 0

Adding upstream version 0.1.10+dfsg.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-04-22 16:48:36 +02:00
parent 157f539082
commit 4d3e0bf859
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
42 changed files with 10556 additions and 0 deletions

124
src/encode.js Normal file
View file

@ -0,0 +1,124 @@
"use strict";
var Zmodem = module.exports;
const HEX_DIGITS = [ 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102 ];
const HEX_OCTET_VALUE = {};
for (var hd=0; hd<HEX_DIGITS.length; hd++) {
HEX_OCTET_VALUE[ HEX_DIGITS[hd] ] = hd;
}
/**
* General, non-ZMODEM-specific encoding logic.
*
* @exports ENCODELIB
*/
Zmodem.ENCODELIB = {
/**
* Return an array with the given number as 2 big-endian bytes.
*
* @param {number} number - The number to encode.
*
* @returns {number[]} The octet values.
*/
pack_u16_be: function pack_u16_be(number) {
if (number > 0xffff) throw( "Number cannot exceed 16 bits: " + number )
return [ number >> 8, number & 0xff ];
},
/**
* Return an array with the given number as 4 little-endian bytes.
*
* @param {number} number - The number to encode.
*
* @returns {number[]} The octet values.
*/
pack_u32_le: function pack_u32_le(number) {
//Cant bit-shift because that runs into JSs bit-shift problem.
//(See _updcrc32() for an example.)
var high_bytes = number / 65536; //fraction is ok
//a little-endian 4-byte sequence
return [
number & 0xff,
(number & 65535) >> 8,
high_bytes & 0xff,
high_bytes >> 8,
];
},
/**
* The inverse of pack_u16_be() - i.e., take in 2 octet values
* and parse them as an unsigned, 2-byte big-endian number.
*
* @param {number[]} octets - The octet values (2 of them).
*
* @returns {number} The decoded number.
*/
unpack_u16_be: function unpack_u16_be(bytes_arr) {
return (bytes_arr[0] << 8) + bytes_arr[1];
},
/**
* The inverse of pack_u32_le() - i.e., take in a 4-byte sequence
* and parse it as an unsigned, 4-byte little-endian number.
*
* @param {number[]} octets - The octet values (4 of them).
*
* @returns {number} The decoded number.
*/
unpack_u32_le: function unpack_u32_le(octets) {
//<sigh> … (254 << 24 is -33554432, according to JavaScript)
return octets[0] + (octets[1] << 8) + (octets[2] << 16) + (octets[3] * 16777216);
},
/**
* Encode a series of octet values to be the octet values that
* correspond to the ASCII hex characters for each octet. The
* returned array is suitable for use as binary data.
*
* For example:
*
* Original Hex Returned
* 254 fe 102, 101
* 12 0c 48, 99
* 129 81 56, 49
*
* @param {number[]} octets - The original octet values.
*
* @returns {number[]} The octet values that correspond to an ASCII
* representation of the given octets.
*/
octets_to_hex: function octets_to_hex(octets) {
var hex = [];
for (var o=0; o<octets.length; o++) {
hex.push(
HEX_DIGITS[ octets[o] >> 4 ],
HEX_DIGITS[ octets[o] & 0x0f ]
);
}
return hex;
},
/**
* The inverse of octets_to_hex(): takes an array
* of hex octet pairs and returns their octet values.
*
* @param {number[]} hex_octets - The hex octet values.
*
* @returns {number[]} The parsed octet values.
*/
parse_hex_octets: function parse_hex_octets(hex_octets) {
var octets = new Array(hex_octets.length / 2);
for (var i=0; i<octets.length; i++) {
octets[i] = (HEX_OCTET_VALUE[ hex_octets[2 * i] ] << 4) + HEX_OCTET_VALUE[ hex_octets[1 + 2 * i] ];
}
return octets;
},
};

33
src/text.js Normal file
View file

@ -0,0 +1,33 @@
class _my_TextEncoder {
encode(text) {
text = unescape(encodeURIComponent(text));
var bytes = new Array( text.length );
for (var b = 0; b < text.length; b++) {
bytes[b] = text.charCodeAt(b);
}
return new Uint8Array(bytes);
}
}
class _my_TextDecoder {
decode(bytes) {
return decodeURIComponent( escape( String.fromCharCode.apply(String, bytes) ) );
}
}
var Zmodem = module.exports;
/**
* A limited-use compatibility shim for TextEncoder and TextDecoder.
* Useful because both Edge and node.js still lack support for these
* as of October 2017.
*
* @exports Text
*/
Zmodem.Text = {
Encoder: (typeof TextEncoder !== "undefined") ? TextEncoder : _my_TextEncoder,
Decoder: (typeof TextDecoder !== "undefined") ? TextDecoder : _my_TextDecoder,
};

143
src/zcrc.js Normal file
View file

@ -0,0 +1,143 @@
"use strict";
const CRC32_MOD = require('crc-32');
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zerror"),
require("./encode")
);
//----------------------------------------------------------------------
// BEGIN adapted from crc-js by Johannes Rudolph
var _crctab;
const
crc_width = 16,
crc_polynomial = 0x1021,
crc_castmask = 0xffff,
crc_msbmask = 1 << (crc_width - 1)
;
function _compute_crctab() {
_crctab = new Array(256);
var divident_shift = crc_width - 8;
for (var divident = 0; divident < 256; divident++) {
var currByte = (divident << divident_shift) & crc_castmask;
for (var bit = 0; bit < 8; bit++) {
if ((currByte & crc_msbmask) !== 0) {
currByte <<= 1;
currByte ^= crc_polynomial;
}
else {
currByte <<= 1;
}
}
_crctab[divident] = (currByte & crc_castmask);
}
}
// END adapted from crc-js by Johannes Rudolph
//----------------------------------------------------------------------
function _updcrc(cp, crc) {
if (!_crctab) _compute_crctab();
return(
_crctab[((crc >> 8) & 255)]
^ ((255 & crc) << 8)
^ cp
);
}
function __verify(expect, got) {
var err;
if ( expect.join() !== got.join() ) {
throw new Zmodem.Error("crc", got, expect);
}
}
//TODO: use external implementation(s)
Zmodem.CRC = {
//https://www.lammertbies.nl/comm/info/crc-calculation.html
//CRC-CCITT (XModem)
/**
* Deduce a given set of octet values CRC16, as per the CRC16
* variant that ZMODEM uses (CRC-CCITT/XModem).
*
* @param {Array} octets - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @returns {Array} crc - The CRC, expressed as an array of octet values.
*/
crc16: function crc16(octet_nums) {
var crc = octet_nums[0];
for (var b=1; b<octet_nums.length; b++) {
crc = _updcrc( octet_nums[b], crc );
}
crc = _updcrc( 0, _updcrc(0, crc) );
//a big-endian 2-byte sequence
return Zmodem.ENCODELIB.pack_u16_be(crc);
},
/**
* Deduce a given set of octet values CRC32.
*
* @param {Array} octets - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @returns {Array} crc - The CRC, expressed as an array of octet values.
*/
crc32: function crc32(octet_nums) {
return Zmodem.ENCODELIB.pack_u32_le(
CRC32_MOD.buf(octet_nums) >>> 0 //bit-shift to get unsigned
);
},
/**
* Verify a given set of octet values CRC16.
* An exception is thrown on failure.
*
* @param {Array} bytes_arr - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {Array} crc - The CRC to check against, expressed as
* an array of octet values.
*/
verify16: function verify16(bytes_arr, got) {
return __verify( this.crc16(bytes_arr), got );
},
/**
* Verify a given set of octet values CRC32.
* An exception is thrown on failure.
*
* @param {Array} bytes_arr - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {Array} crc - The CRC to check against, expressed as
* an array of octet values.
*/
verify32: function verify32(bytes_arr, crc) {
try {
__verify( this.crc32(bytes_arr), crc );
}
catch(err) {
err.input = bytes_arr.slice(0);
throw err;
}
},
};

240
src/zdle.js Normal file
View file

@ -0,0 +1,240 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zmlib")
);
//encode() variables - declare them here so we dont
//create them in the function.
var encode_cur, encode_todo;
const ZDLE = Zmodem.ZMLIB.ZDLE;
/**
* Class that handles ZDLE encoding and decoding.
* Encoding is subject to a given configuration--specifically, whether
* we want to escape all control characters. Decoding is static; however
* a given string is encoded we can always decode it.
*/
Zmodem.ZDLE = class ZmodemZDLE {
/**
* Create a ZDLE encoder.
*
* @param {object} [config] - The initial configuration.
* @param {object} config.escape_ctrl_chars - Whether the ZDLE encoder
* should escape control characters.
*/
constructor(config) {
this._config = {};
if (config) {
this.set_escape_ctrl_chars(!!config.escape_ctrl_chars);
}
}
/**
* Enable or disable control-character escaping.
* You should probably enable this for sender sessions.
*
* @param {boolean} value - Whether to enable (true) or disable (false).
*/
set_escape_ctrl_chars(value) {
if (typeof value !== "boolean") throw "need boolean!";
if (value !== this._config.escape_ctrl_chars) {
this._config.escape_ctrl_chars = value;
this._setup_zdle_table();
}
}
/**
* Whether or not control-character escaping is enabled.
*
* @return {boolean} Whether the escaping is on (true) or off (false).
*/
escapes_ctrl_chars() {
return !!this._config.escape_ctrl_chars;
}
//I dont know of any Zmodem implementations that use ZESC8
//(“escape_8th_bit”)??
/*
ZMODEM software escapes ZDLE, 020, 0220, 021, 0221, 023, and 0223. If
preceded by 0100 or 0300 (@), 015 and 0215 are also escaped to protect the
Telenet command escape CR-@-CR.
*/
/**
* Encode an array of octet values and return it.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array, transformed. This is the
* same object that is passed in.
*/
encode(octets) {
//NB: Performance matters here!
if (!this._zdle_table) throw "No ZDLE encode table configured!";
var zdle_table = this._zdle_table;
var last_code = this._lastcode;
var arrbuf = new ArrayBuffer( 2 * octets.length );
var arrbuf_uint8 = new Uint8Array(arrbuf);
var escctl_yn = this._config.escape_ctrl_chars;
var arrbuf_i = 0;
for (encode_cur=0; encode_cur<octets.length; encode_cur++) {
encode_todo = zdle_table[octets[encode_cur]];
if (!encode_todo) {
console.trace();
console.error("bad encode() call:", JSON.stringify(octets));
this._lastcode = last_code;
throw( "Invalid octet: " + octets[encode_cur] );
}
last_code = octets[encode_cur];
if (encode_todo === 1) {
//Do nothing; we append last_code below.
}
//0x40 = '@'; i.e., only escape if the last
//octet was '@'.
else if (escctl_yn || (encode_todo === 2) || ((last_code & 0x7f) === 0x40)) {
arrbuf_uint8[arrbuf_i] = ZDLE;
arrbuf_i++;
last_code ^= 0x40; //0100
}
arrbuf_uint8[arrbuf_i] = last_code;
arrbuf_i++;
}
this._lastcode = last_code;
octets.splice(0);
octets.push.apply(octets, new Uint8Array( arrbuf, 0, arrbuf_i ));
return octets;
}
/**
* Decode an array of octet values and return it.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array.
* This is the same object that is passed in.
*/
static decode(octets) {
for (var o=octets.length-1; o>=0; o--) {
if (octets[o] === ZDLE) {
octets.splice( o, 2, octets[o+1] - 64 );
}
}
return octets;
}
/**
* Remove, ZDLE-decode, and return bytes from the passed-in array.
* If the requested number of ZDLE-encoded bytes isnt available,
* then the passed-in array is unmodified (and the return is undefined).
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @param {number} offset - The number of (undecoded) bytes to skip
* at the beginning of the octets array.
*
* @param {number} count - The number of bytes (octet values) to return.
*
* @returns {number[]|undefined} An array with the requested number of
* decoded octet values, or undefined if that number of decoded
* octets isnt available (given the passed-in offset).
*/
static splice(octets, offset, count) {
var so_far = 0;
if (!offset) offset = 0;
for (var i = offset; i<octets.length && so_far<count; i++) {
so_far++;
if (octets[i] === ZDLE) i++;
}
if (so_far === count) {
//Dont accept trailing ZDLE. This check works
//because of the i++ logic above.
if (octets.length === (i - 1)) return;
octets.splice(0, offset);
return ZmodemZDLE.decode( octets.splice(0, i - offset) );
}
return;
}
_setup_zdle_table() {
var zsendline_tab = new Array(256);
for (var i=0; i<zsendline_tab.length; i++) {
//1 = never escape
//2 = always escape
//3 = escape only if the previous byte was '@'
//Never escape characters from 0x20 (32) to 0x7f (127).
//This is the range of printable characters, plus DEL.
//I guess ZMODEM doesnt consider DEL to be a control character?
if ( i & 0x60 ) {
zsendline_tab[i] = 1;
}
else {
switch(i) {
case ZDLE: //NB: no (ZDLE | 0x80)
case Zmodem.ZMLIB.XOFF:
case Zmodem.ZMLIB.XON:
case (Zmodem.ZMLIB.XOFF | 0x80):
case (Zmodem.ZMLIB.XON | 0x80):
zsendline_tab[i] = 2;
break;
case 0x10: // 020
case 0x90: // 0220
zsendline_tab[i] = this._config.turbo_escape ? 1 : 2;
break;
case 0x0d: // 015
case 0x8d: // 0215
zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : !this._config.turbo_escape ? 3 : 1;
break;
default:
zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : 1;
}
}
}
this._zdle_table = zsendline_tab;
}
}

47
src/zerror.js Normal file
View file

@ -0,0 +1,47 @@
"use strict";
var Zmodem = module.exports;
function _crc_message(got, expected) {
this.got = got.slice(0);
this.expected = expected.slice(0);
return "CRC check failed! (got: " + got.join() + "; expected: " + expected.join() + ")";
}
function _pass(val) { return val }
const TYPE_MESSAGE = {
aborted: "Session aborted",
peer_aborted: "Peer aborted session",
already_aborted: "Session already aborted",
crc: _crc_message,
validation: _pass,
};
function _generate_message(type) {
const msg = TYPE_MESSAGE[type];
switch (typeof msg) {
case "string":
return msg;
case "function":
var args_after_type = [].slice.call(arguments).slice(1);
return msg.apply(this, args_after_type);
}
return null;
}
Zmodem.Error = class ZmodemError extends Error {
constructor(msg_or_type) {
super();
var generated = _generate_message.apply(this, arguments);
if (generated) {
this.type = msg_or_type;
this.message = generated;
}
else {
this.message = msg_or_type;
}
}
};

763
src/zheader.js Normal file
View file

@ -0,0 +1,763 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./encode"),
require("./zdle"),
require("./zmlib"),
require("./zcrc"),
require("./zerror")
);
const ZPAD = '*'.charCodeAt(0),
ZBIN = 'A'.charCodeAt(0),
ZHEX = 'B'.charCodeAt(0),
ZBIN32 = 'C'.charCodeAt(0)
;
//NB: lrzsz uses \x8a rather than \x0a where the specs
//say to use LF. For simplicity, we avoid that and just use
//the 7-bit LF character.
const HEX_HEADER_CRLF = [ 0x0d, 0x0a ];
const HEX_HEADER_CRLF_XON = HEX_HEADER_CRLF.slice(0).concat( [Zmodem.ZMLIB.XON] );
//These are more or less duplicated by the logic in trim_leading_garbage().
//
//"**" + ZDLE_CHAR + "B"
const HEX_HEADER_PREFIX = [ ZPAD, ZPAD, Zmodem.ZMLIB.ZDLE, ZHEX ];
const BINARY16_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN ];
const BINARY32_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN32 ];
/** Class that represents a ZMODEM header. */
Zmodem.Header = class ZmodemHeader {
//lrzszs “sz” command sends a random (?) CR/0x0d byte
//after ZEOF. Lets accommodate 0x0a, 0x0d, 0x8a, and 0x8d.
//
//Also, when you skip a file, sz outputs a message about it.
//
//It appears that were supposed to ignore anything until
//[ ZPAD, ZDLE ] when were looking for a header.
/**
* Weed out the leading bytes that arent valid to start a ZMODEM header.
*
* @param {number[]} ibuffer - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The octet values that were removed from the start
* of ibuffer. Order is preserved.
*/
static trim_leading_garbage(ibuffer) {
//Since theres no escaping of the output its possible
//that the garbage could trip us up, e.g., by having a filename
//be a legit ZMODEM header. But thats pretty unlikely.
//Everything up to the first ZPAD: garbage
//If first ZPAD has asterisk + ZDLE
var garbage = [];
var discard_all, parser, next_ZPAD_at_least = 0;
TRIM_LOOP:
while (ibuffer.length && !parser) {
var first_ZPAD = ibuffer.indexOf(ZPAD);
//No ZPAD? Then we purge the input buffer cuz its all garbage.
if (first_ZPAD === -1) {
discard_all = true;
break TRIM_LOOP;
}
else {
garbage.push.apply( garbage, ibuffer.splice(0, first_ZPAD) );
//buffer has only an asterisk … gotta see about more
if (ibuffer.length < 2) {
break TRIM_LOOP;
}
else if (ibuffer[1] === ZPAD) {
//Two leading ZPADs should be a hex header.
//Were assuming the length of the header is 4 in
//this logic … but ZMODEM isnt likely to change, so.
if (ibuffer.length < HEX_HEADER_PREFIX.length) {
if (ibuffer.join() === HEX_HEADER_PREFIX.slice(0, ibuffer.length).join()) {
//We have an incomplete fragment that matches
//HEX_HEADER_PREFIX. So dont trim any more.
break TRIM_LOOP;
}
//Otherwise, well discard one.
}
else if ((ibuffer[2] === HEX_HEADER_PREFIX[2]) && (ibuffer[3] === HEX_HEADER_PREFIX[3])) {
parser = _parse_hex;
}
}
else if (ibuffer[1] === Zmodem.ZMLIB.ZDLE) {
//ZPAD + ZDLE should be a binary header.
if (ibuffer.length < BINARY16_HEADER_PREFIX.length) {
break TRIM_LOOP;
}
if (ibuffer[2] === BINARY16_HEADER_PREFIX[2]) {
parser = _parse_binary16;
}
else if (ibuffer[2] === BINARY32_HEADER_PREFIX[2]) {
parser = _parse_binary32;
}
}
if (!parser) {
garbage.push( ibuffer.shift() );
}
}
}
if (discard_all) {
garbage.push.apply( garbage, ibuffer.splice(0) );
}
//For now well throw away the parser.
//Its not hard for parse() to discern anyway.
return garbage;
}
/**
* Parse out a Header object from a given array of octet values.
*
* An exception is thrown if the given bytes are definitively invalid
* as header values.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Header|undefined} An instance of the appropriate Header
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid header here or not.
*/
static parse(octets) {
var hdr;
if (octets[1] === ZPAD) {
hdr = _parse_hex(octets);
return hdr && [ hdr, 16 ];
}
else if (octets[2] === ZBIN) {
hdr = _parse_binary16(octets, 3);
return hdr && [ hdr, 16 ];
}
else if (octets[2] === ZBIN32) {
hdr = _parse_binary32(octets);
return hdr && [ hdr, 32 ];
}
if (octets.length < 3) return;
throw( "Unrecognized/unsupported octets: " + octets.join() );
}
/**
* Build a Header subclass given a name and arguments.
*
* @param {string} name - The header type name, e.g., ZRINIT.
*
* @param {...*} args - The arguments to pass to the appropriate
* subclass constructor. These arent documented currently
* but are pretty easy to glean from the code.
*
* @returns {Header} An instance of the appropriate Header subclass.
*/
static build(name /*, args */) {
var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
//TODO: make this better
var Ctr = FRAME_NAME_CREATOR[name];
if (!Ctr) throw("No frame class “" + name + "” is defined!");
args.shift();
//Plegh!
//https://stackoverflow.com/questions/33193310/constr-applythis-args-in-es6-classes
var hdr = new (Ctr.bind.apply(Ctr, [null].concat(args)));
return hdr;
}
/**
* Return the octet values array that represents the object
* in ZMODEM hex encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_hex() {
var to_crc = this._crc_bytes();
return HEX_HEADER_PREFIX.concat(
Zmodem.ENCODELIB.octets_to_hex( to_crc.concat( Zmodem.CRC.crc16(to_crc) ) ),
this._hex_header_ending
);
}
/**
* Return the octet values array that represents the object
* in ZMODEM binary encoding with a 16-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for
* ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_binary16(zencoder) {
return this._to_binary(zencoder, BINARY16_HEADER_PREFIX, Zmodem.CRC.crc16);
}
/**
* Return the octet values array that represents the object
* in ZMODEM binary encoding with a 32-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for
* ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_binary32(zencoder) {
return this._to_binary(zencoder, BINARY32_HEADER_PREFIX, Zmodem.CRC.crc32);
}
//This is never called directly, but only as super().
constructor() {
if (!this._bytes4) {
this._bytes4 = [0, 0, 0, 0];
}
}
_to_binary(zencoder, prefix, crc_func) {
var to_crc = this._crc_bytes();
//Both the 4-byte payload and the CRC bytes are ZDLE-encoded.
var octets = prefix.concat(
zencoder.encode( to_crc.concat( crc_func(to_crc) ) )
);
return octets;
}
_crc_bytes() {
return [ this.TYPENUM ].concat(this._bytes4);
}
}
Zmodem.Header.prototype._hex_header_ending = HEX_HEADER_CRLF_XON;
class ZRQINIT_HEADER extends Zmodem.Header {};
//----------------------------------------------------------------------
const ZRINIT_FLAG = {
//----------------------------------------------------------------------
// Bit Masks for ZRINIT flags byte ZF0
//----------------------------------------------------------------------
CANFDX: 0x01, // Rx can send and receive true FDX
CANOVIO: 0x02, // Rx can receive data during disk I/O
CANBRK: 0x04, // Rx can send a break signal
CANCRY: 0x08, // Receiver can decrypt -- nothing does this
CANLZW: 0x10, // Receiver can uncompress -- nothing does this
CANFC32: 0x20, // Receiver can use 32 bit Frame Check
ESCCTL: 0x40, // Receiver expects ctl chars to be escaped
ESC8: 0x80, // Receiver expects 8th bit to be escaped
};
function _get_ZRINIT_flag_num(fl) {
if (!ZRINIT_FLAG[fl]) {
throw new Zmodem.Error("Invalid ZRINIT flag: " + fl);
}
return ZRINIT_FLAG[fl];
}
class ZRINIT_HEADER extends Zmodem.Header {
constructor(flags_arr, bufsize) {
super();
var flags_num = 0;
if (!bufsize) bufsize = 0;
flags_arr.forEach( function(fl) {
flags_num |= _get_ZRINIT_flag_num(fl);
} );
this._bytes4 = [
bufsize & 0xff,
bufsize >> 8,
0,
flags_num,
];
}
//undefined if nonstop I/O is allowed
get_buffer_size() {
return Zmodem.ENCODELIB.unpack_u16_be( this._bytes4.slice(0, 2) ) || undefined;
}
//Unimplemented:
// can_decrypt
// can_decompress
//----------------------------------------------------------------------
//function names taken from Jacques Mattheijs implementation,
//as used in syncterm.
can_full_duplex() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFDX );
}
can_overlap_io() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANOVIO );
}
can_break() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANBRK );
}
can_fcs_32() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFC32 );
}
escape_ctrl_chars() {
return !!( this._bytes4[3] & ZRINIT_FLAG.ESCCTL );
}
//Is this used? I dont see it used in lrzsz or syncterm
//Looks like it was a “foreseen” feature that Forsberg
//never implemented. (The need for it went away, maybe?)
escape_8th_bit() {
return !!( this._bytes4[3] & ZRINIT_FLAG.ESC8 );
}
};
//----------------------------------------------------------------------
//Since context makes clear whats going on, we use these
//rather than the T-prefixed constants in the specification.
const ZSINIT_FLAG = {
ESCCTL: 0x40, // Transmitter will escape ctl chars
ESC8: 0x80, // Transmitter will escape 8th bit
};
function _get_ZSINIT_flag_num(fl) {
if (!ZSINIT_FLAG[fl]) {
throw("Invalid ZSINIT flag: " + fl);
}
return ZSINIT_FLAG[fl];
}
class ZSINIT_HEADER extends Zmodem.Header {
constructor( flags_arr, attn_seq_arr ) {
super();
var flags_num = 0;
flags_arr.forEach( function(fl) {
flags_num |= _get_ZSINIT_flag_num(fl);
} );
this._bytes4 = [ 0, 0, 0, flags_num ];
if (attn_seq_arr) {
if (attn_seq_arr.length > 31) {
throw("Attn sequence must be <= 31 bytes");
}
if (attn_seq_arr.some( function(num) { return num > 255 } )) {
throw("Attn sequence (" + attn_seq_arr + ") must be <256");
}
this._data = attn_seq_arr.concat([0]);
}
}
escape_ctrl_chars() {
return !!( this._bytes4[3] & ZSINIT_FLAG.ESCCTL );
}
//Is this used? I dont see it used in lrzsz or syncterm
escape_8th_bit() {
return !!( this._bytes4[3] & ZSINIT_FLAG.ESC8 );
}
}
//Thus far it doesnt seem we really need this header except to respond
//to ZSINIT, which doesnt require a payload.
class ZACK_HEADER extends Zmodem.Header {
constructor(payload4) {
super();
if (payload4) {
this._bytes4 = payload4.slice();
}
}
}
ZACK_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
//----------------------------------------------------------------------
const ZFILE_VALUES = {
//ZF3 (i.e., first byte)
extended: {
sparse: 0x40, //ZXSPARS
},
//ZF2
transport: [
undefined,
"compress", //ZTLZW
"encrypt", //ZTCRYPT
"rle", //ZTRLE
],
//ZF1
management: [
undefined,
"newer_or_longer", //ZF1_ZMNEWL
"crc", //ZF1_ZMCRC
"append", //ZF1_ZMAPND
"clobber", //ZF1_ZMCLOB
"newer", //ZF1_ZMNEW
"mtime_or_length", //ZF1_ZMNEW
"protect", //ZF1_ZMPROT
"rename", //ZF1_ZMPROT
],
//ZF0 (i.e., last byte)
conversion: [
undefined,
"binary", //ZCBIN
"text", //ZCNL
"resume", //ZCRESUM
],
};
const ZFILE_ORDER = ["extended", "transport", "management", "conversion"];
const ZMSKNOLOC = 0x80,
MANAGEMENT_MASK = 0x1f,
ZXSPARS = 0x40
;
class ZFILE_HEADER extends Zmodem.Header {
//TODO: allow options on instantiation
get_options() {
var opts = {
sparse: !!(this._bytes4[0] & ZXSPARS),
};
var bytes_copy = this._bytes4.slice(0);
ZFILE_ORDER.forEach( function(key, i) {
if (ZFILE_VALUES[key] instanceof Array) {
if (key === "management") {
opts.skip_if_absent = !!(bytes_copy[i] & ZMSKNOLOC);
bytes_copy[i] &= MANAGEMENT_MASK;
}
opts[key] = ZFILE_VALUES[key][ bytes_copy[i] ];
}
else {
for (var extkey in ZFILE_VALUES[key]) {
opts[extkey] = !!(bytes_copy[i] & ZFILE_VALUES[key][extkey]);
if (opts[extkey]) {
bytes_copy[i] ^= ZFILE_VALUES[key][extkey]
}
}
}
if (!opts[key] && bytes_copy[i]) {
opts[key] = "unknown:" + bytes_copy[i];
}
} );
return opts;
}
}
//----------------------------------------------------------------------
//Empty headers - in addition to ZRQINIT
class ZSKIP_HEADER extends Zmodem.Header {}
//No need for ZNAK
class ZABORT_HEADER extends Zmodem.Header {}
class ZFIN_HEADER extends Zmodem.Header {}
class ZFERR_HEADER extends Zmodem.Header {}
ZFIN_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
class ZOffsetHeader extends Zmodem.Header {
constructor(offset) {
super();
this._bytes4 = Zmodem.ENCODELIB.pack_u32_le(offset);
}
get_offset() {
return Zmodem.ENCODELIB.unpack_u32_le(this._bytes4);
}
}
class ZRPOS_HEADER extends ZOffsetHeader {};
class ZDATA_HEADER extends ZOffsetHeader {};
class ZEOF_HEADER extends ZOffsetHeader {};
//As request, receiver creates.
/* UNIMPLEMENTED FOR NOW
class ZCRC_HEADER extends ZHeader {
constructor(crc_le_bytes) {
super();
if (crc_le_bytes) { //response, sender creates
this._bytes4 = crc_le_bytes;
}
}
}
*/
//No ZCHALLENGE implementation
//class ZCOMPL_HEADER extends ZHeader {}
//class ZCAN_HEADER extends Zmodem.Header {}
//As described, this header represents an information disclosure.
//It could be interpreted, I suppose, merely as “this is how much space
//I have FOR YOU.”
//TODO: implement if needed/requested
//class ZFREECNT_HEADER extends ZmodemHeader {}
//----------------------------------------------------------------------
const FRAME_CLASS_TYPES = [
[ ZRQINIT_HEADER, "ZRQINIT" ],
[ ZRINIT_HEADER, "ZRINIT" ],
[ ZSINIT_HEADER, "ZSINIT" ],
[ ZACK_HEADER, "ZACK" ],
[ ZFILE_HEADER, "ZFILE" ],
[ ZSKIP_HEADER, "ZSKIP" ],
undefined, // [ ZNAK_HEADER, "ZNAK" ],
[ ZABORT_HEADER, "ZABORT" ],
[ ZFIN_HEADER, "ZFIN" ],
[ ZRPOS_HEADER, "ZRPOS" ],
[ ZDATA_HEADER, "ZDATA" ],
[ ZEOF_HEADER, "ZEOF" ],
[ ZFERR_HEADER, "ZFERR" ], //see note
undefined, //[ ZCRC_HEADER, "ZCRC" ],
undefined, //[ ZCHALLENGE_HEADER, "ZCHALLENGE" ],
undefined, //[ ZCOMPL_HEADER, "ZCOMPL" ],
undefined, //[ ZCAN_HEADER, "ZCAN" ],
undefined, //[ ZFREECNT_HEADER, "ZFREECNT" ],
undefined, //[ ZCOMMAND_HEADER, "ZCOMMAND" ],
undefined, //[ ZSTDERR_HEADER, "ZSTDERR" ],
];
/*
ZFERR is described as error in reading or writing file. Its really
not a good idea from a security angle for the endpoint to expose this
information. We should parse this and handle it as ZABORT but never send it.
Likewise with ZFREECNT: the sender shouldnt ask how much space is left
on the other box; rather, the receiver should decide what to do with the
file size as the sender reports it.
*/
var FRAME_NAME_CREATOR = {};
for (var fc=0; fc<FRAME_CLASS_TYPES.length; fc++) {
if (!FRAME_CLASS_TYPES[fc]) continue;
FRAME_NAME_CREATOR[ FRAME_CLASS_TYPES[fc][1] ] = FRAME_CLASS_TYPES[fc][0];
Object.assign(
FRAME_CLASS_TYPES[fc][0].prototype,
{
TYPENUM: fc,
NAME: FRAME_CLASS_TYPES[fc][1],
}
);
}
//----------------------------------------------------------------------
const CREATORS = [
ZRQINIT_HEADER,
ZRINIT_HEADER,
ZSINIT_HEADER,
ZACK_HEADER,
ZFILE_HEADER,
ZSKIP_HEADER,
'ZNAK',
ZABORT_HEADER,
ZFIN_HEADER,
ZRPOS_HEADER,
ZDATA_HEADER,
ZEOF_HEADER,
ZFERR_HEADER,
'ZCRC', //ZCRC_HEADER, -- leaving unimplemented?
'ZCHALLENGE',
'ZCOMPL',
'ZCAN',
'ZFREECNT', // ZFREECNT_HEADER,
'ZCOMMAND',
'ZSTDERR',
];
function _get_blank_header(typenum) {
var creator = CREATORS[typenum];
if (typeof(creator) === "string") {
throw( "Received unsupported header: " + creator );
}
/*
if (creator === ZCRC_HEADER) {
return new creator([0, 0, 0, 0]);
}
*/
return _get_blank_header_from_constructor(creator);
}
//referenced outside TODO
function _get_blank_header_from_constructor(creator) {
if (creator.prototype instanceof ZOffsetHeader) {
return new creator(0);
}
return new creator([]);
}
function _parse_binary16(bytes_arr) {
//The max length of a ZDLE-encoded binary header w/ 16-bit CRC is:
// 3 initial bytes, NOT ZDLE-encoded
// 2 typenum bytes (1 decoded)
// 8 data bytes (4 decoded)
// 4 CRC bytes (2 decoded)
//A 16-bit payload has 7 ZDLE-encoded octets.
//The ZDLE-encoded octets follow the initial prefix.
var zdle_decoded = Zmodem.ZDLE.splice( bytes_arr, BINARY16_HEADER_PREFIX.length, 7 );
return zdle_decoded && _parse_non_zdle_binary16(zdle_decoded);
}
function _parse_non_zdle_binary16(decoded) {
Zmodem.CRC.verify16(
decoded.slice(0, 5),
decoded.slice(5)
);
var typenum = decoded[0];
var hdr = _get_blank_header(typenum);
hdr._bytes4 = decoded.slice( 1, 5 );
return hdr;
}
function _parse_binary32(bytes_arr) {
//Same deal as with 16-bit CRC except there are two more
//potentially ZDLE-encoded bytes, for a total of 9.
var zdle_decoded = Zmodem.ZDLE.splice(
bytes_arr, //omit the leading "*", ZDLE, and "C"
BINARY32_HEADER_PREFIX.length,
9
);
if (!zdle_decoded) return;
Zmodem.CRC.verify32(
zdle_decoded.slice(0, 5),
zdle_decoded.slice(5)
);
var typenum = zdle_decoded[0];
var hdr = _get_blank_header(typenum);
hdr._bytes4 = zdle_decoded.slice( 1, 5 );
return hdr;
}
function _parse_hex(bytes_arr) {
//A hex header always has:
// 4 bytes for the ** . ZDLE . 'B'
// 2 hex bytes for the header type
// 8 hex bytes for the header content
// 4 hex bytes for the CRC
// 1-2 bytes for (CR/)LF
// (...and at this point the trailing XON is already stripped)
//
//----------------------------------------------------------------------
//A carriage return and line feed are sent with HEX headers. The
//receive routine expects to see at least one of these characters, two
//if the first is CR.
//----------------------------------------------------------------------
//
//^^ I guess it can be either CR/LF or just LF … though those two
//sentences appear to be saying contradictory things.
var lf_pos = bytes_arr.indexOf( 0x8a ); //lrzsz sends this
if (-1 === lf_pos) {
lf_pos = bytes_arr.indexOf( 0x0a );
}
var hdr_err, hex_bytes;
if (-1 === lf_pos) {
if (bytes_arr.length > 11) {
hdr_err = "Invalid hex header - no LF detected within 12 bytes!";
}
//incomplete header
return;
}
else {
hex_bytes = bytes_arr.splice( 0, lf_pos );
//Trim off the LF
bytes_arr.shift();
if ( hex_bytes.length === 19 ) {
//NB: The spec says CR but seems to treat high-bit variants
//of control characters the same as the regulars; should we
//also allow 0x8d?
var preceding = hex_bytes.pop();
if ( preceding !== 0x0d && preceding !== 0x8d ) {
hdr_err = "Invalid hex header: (CR/)LF doesnt have CR!";
}
}
else if ( hex_bytes.length !== 18 ) {
hdr_err = "Invalid hex header: invalid number of bytes before LF!";
}
}
if (hdr_err) {
hdr_err += " (" + hex_bytes.length + " bytes: " + hex_bytes.join() + ")";
throw hdr_err;
}
hex_bytes.splice(0, 4);
//Should be 7 bytes ultimately:
// 1 for typenum
// 4 for header data
// 2 for CRC
var octets = Zmodem.ENCODELIB.parse_hex_octets(hex_bytes);
return _parse_non_zdle_binary16(octets);
}
Zmodem.Header.parse_hex = _parse_hex;

102
src/zmlib.js Normal file
View file

@ -0,0 +1,102 @@
"use strict";
var Zmodem = module.exports;
const
ZDLE = 0x18,
XON = 0x11,
XOFF = 0x13,
XON_HIGH = 0x80 | XON,
XOFF_HIGH = 0x80 | XOFF,
CAN = 0x18 //NB: same character as ZDLE
;
/**
* Tools and constants that are useful for ZMODEM.
*
* @exports ZMLIB
*/
Zmodem.ZMLIB = {
/**
* @property {number} The ZDLE constant, which ZMODEM uses for escaping
*/
ZDLE: ZDLE,
/**
* @property {number} XON - ASCII XON
*/
XON: XON,
/**
* @property {number} XOFF - ASCII XOFF
*/
XOFF: XOFF,
/**
* @property {number[]} ABORT_SEQUENCE - ZMODEMs abort sequence
*/
ABORT_SEQUENCE: [ CAN, CAN, CAN, CAN, CAN ],
/**
* Remove octet values from the given array that ZMODEM always ignores.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array. This is the same object that is
* passed in.
*/
strip_ignored_bytes: function strip_ignored_bytes(octets) {
for (var o=octets.length-1; o>=0; o--) {
switch (octets[o]) {
case XON:
case XON_HIGH:
case XOFF:
case XOFF_HIGH:
octets.splice(o, 1);
continue;
}
}
return octets;
},
/**
* Like Array.prototype.indexOf, but searches for a subarray
* rather than just a particular value.
*
* @param {Array} haystack - The array to search, i.e., the bigger.
*
* @param {Array} needle - The array whose values to find,
* i.e., the smaller.
*
* @returns {number} The position in haystack where needle
* first appearsor, -1 if needle doesnt appear anywhere
* in haystack.
*/
find_subarray: function find_subarray(haystack, needle) {
var h=0, n;
var start = Date.now();
HAYSTACK:
while (h !== -1) {
h = haystack.indexOf( needle[0], h );
if (h === -1) break HAYSTACK;
for (n=1; n<needle.length; n++) {
if (haystack[h + n] !== needle[n]) {
h++;
continue HAYSTACK;
}
}
return h;
}
return -1;
},
};

4
src/zmodem.js Normal file
View file

@ -0,0 +1,4 @@
Object.assign(
module.exports,
require("./zsentry"),
);

182
src/zmodem_browser.js Normal file
View file

@ -0,0 +1,182 @@
"use strict";
var Zmodem = module.exports;
//TODO: Make this usable without require.js or what not.
window.Zmodem = Zmodem;
Object.assign(
Zmodem,
require("./zmodem")
);
function _check_aborted(session) {
if (session.aborted()) {
throw new Zmodem.Error("aborted");
}
}
/** Browser-specific tools
*
* @exports Browser
*/
Zmodem.Browser = {
/**
* Send a batch of files in sequence. The session is left open
* afterward, which allows for more files to be sent if desired.
*
* @param {Zmodem.Session} session - The send session
*
* @param {FileList|Array} files - A list of File objects
*
* @param {Object} [options]
* @param {Function} [options.on_offer_response] - Called when an
* offer response arrives. Arguments are:
*
* - (File) - The File object that corresponds to the offer.
* - (Transfer|undefined) - If the receiver accepts the offer, then
* this is a Transfer object; otherwise its undefined.
*
* @param {Function} [options.on_progress] - Called immediately
* after a chunk of a file is sent. Arguments are:
*
* - (File) - The File object that corresponds to the file.
* - (Transfer) - The Transfer object for the current transfer.
* - (Uint8Array) - The chunk of data that was just loaded from disk
* and sent to the receiver.
*
* @param {Function} [options.on_file_complete] - Called immediately
* after the last file packet is sent. Arguments are:
*
* - (File) - The File object that corresponds to the file.
* - (Transfer) - The Transfer object for the now-completed transfer.
*
* @return {Promise} A Promise that fulfills when the batch is done.
* Note that skipped files are not considered an error condition.
*/
send_files: function send_files(session, files, options) {
if (!options) options = {};
//Populate the batch in reverse order to simplify sending
//the remaining files/bytes components.
var batch = [];
var total_size = 0;
for (var f=files.length - 1; f>=0; f--) {
var fobj = files[f];
total_size += fobj.size;
batch[f] = {
obj: fobj,
name: fobj.name,
size: fobj.size,
mtime: new Date(fobj.lastModified),
files_remaining: files.length - f,
bytes_remaining: total_size,
};
}
var file_idx = 0;
function promise_callback() {
var cur_b = batch[file_idx];
if (!cur_b) {
return Promise.resolve(); //batch done!
}
file_idx++;
return session.send_offer(cur_b).then( function after_send_offer(xfer) {
if (options.on_offer_response) {
options.on_offer_response(cur_b.obj, xfer);
}
if (xfer === undefined) {
return promise_callback(); //skipped
}
return new Promise( function(res) {
var reader = new FileReader();
//This really shouldnt happen … so lets
//blow up if it does.
reader.onerror = function reader_onerror(e) {
console.error("file read error", e);
throw("File read error: " + e);
};
var piece;
reader.onprogress = function reader_onprogress(e) {
//Some browsers (e.g., Chrome) give partial returns,
//while others (e.g., Firefox) dont.
if (e.target.result) {
piece = new Uint8Array(e.target.result, xfer.get_offset())
_check_aborted(session);
xfer.send(piece);
if (options.on_progress) {
options.on_progress(cur_b.obj, xfer, piece);
}
}
};
reader.onload = function reader_onload(e) {
piece = new Uint8Array(e.target.result, xfer, piece)
_check_aborted(session);
xfer.end(piece).then( function() {
if (options.on_progress && piece.length) {
options.on_progress(cur_b.obj, xfer, piece);
}
if (options.on_file_complete) {
options.on_file_complete(cur_b.obj, xfer);
}
//Resolve the current file-send promise with
//another promise. That promise resolves immediately
//if were done, or with another file-send promise
//if theres more to send.
res( promise_callback() );
} );
};
reader.readAsArrayBuffer(cur_b.obj);
} );
} );
}
return promise_callback();
},
/**
* Prompt a user to save the given packets as a file by injecting an
* `<a>` element (with `display: none` styling) into the page and
* calling the elements `click()`
* method. The element is removed immediately after.
*
* @param {Array} packets - Same as the first argument to [Blobs constructor](https://developer.mozilla.org/en-US/docs/Web/API/Blob).
* @param {string} name - The name to give the file.
*/
save_to_disk: function save_to_disk(packets, name) {
var blob = new Blob(packets);
var url = URL.createObjectURL(blob);
var el = document.createElement("a");
el.style.display = "none";
el.href = url;
el.download = name;
document.body.appendChild(el);
//It seems like a security problem that this actually works;
//Id think there would need to be some confirmation before
//a browser could save arbitrarily many bytes onto the disk.
//But, hey.
el.click();
document.body.removeChild(el);
},
};

394
src/zsentry.js Normal file
View file

@ -0,0 +1,394 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zmlib"),
require("./zsession")
);
const
MIN_ZM_HEX_START_LENGTH = 20,
MAX_ZM_HEX_START_LENGTH = 21,
// **, ZDLE, 'B0'
//ZRQINITs next byte will be '0'; ZRINITs will be '1'.
COMMON_ZM_HEX_START = [ 42, 42, 24, 66, 48 ],
SENTRY_CONSTRUCTOR_REQUIRED_ARGS = [
"to_terminal",
"on_detect",
"on_retract",
"sender",
],
ASTERISK = 42
;
/**
* An instance of this object is passed to the Sentrys on_detect
* callback each time the Sentry object sees what looks like the
* start of a ZMODEM session.
*
* Note that it is possible for a detection to be retracted
* if the Sentry consumes bytes afterward that are not ZMODEM.
* When this happens, the Sentrys `retract` event will fire,
* after which the Detection object is no longer usable.
*/
class Detection {
/**
* Not called directly.
*/
constructor(session_type, accepter, denier, checker) {
//confirm() - user confirms that ZMODEM is desired
this._confirmer = accepter;
//deny() - user declines ZMODEM; send abort sequence
//
//TODO: It might be ideal to forgo the session “peaceably”,
//i.e., such that the peer doesnt end in error. Thats
//possible if were the sender, we accept the session,
//then we just send a close(), but it doesnt seem to be
//possible for a receiver. Thus, lets just leave it so
//its at least consistent (and simpler, too).
this._denier = denier;
this._is_valid = checker;
this._session_type = session_type;
}
/**
* Confirm that the detected ZMODEM sequence indicates the
* start of a ZMODEM session.
*
* @return {Session} The ZMODEM Session object (i.e., either a
* Send or Receive instance).
*/
confirm() {
return this._confirmer.apply(this, arguments);
}
/**
* Tell the Sentry that the detected bytes sequence is
* **NOT** intended to be the start of a ZMODEM session.
*/
deny() {
return this._denier.apply(this, arguments);
}
/**
* Tells whether the Detection is still valid; i.e., whether
* the Sentry has `consume()`d bytes that invalidate the
* Detection.
*
* @returns {boolean} Whether the Detection is valid.
*/
is_valid() {
return this._is_valid.apply(this, arguments);
}
/**
* Gives the sessions role.
*
* @returns {string} One of:
* - `receive`
* - `send`
*/
get_session_role() { return this._session_type }
}
/**
* Class that parses an input stream for the beginning of a
* ZMODEM session. We look for the tell-tale signs
* of a ZMODEM transfer and allow the client to determine whether
* its really ZMODEM or not.
*
* This is the mother class for zmodem.js;
* all other class instances are created, directly or indirectly,
* by an instance of this class.
*
* This logic is not unlikely to need tweaking, and it can never
* be fully bulletproof; if it could be bulletproof it would be
* simpler since there wouldnt need to be the .confirm()/.deny()
* step.
*
* One thing you could do to make things a bit simpler *is* just
* to make that assumption for your users--i.e., to .confirm()
* Detection objects automatically. Thatll be one less step
* for the user, but an unaccustomed user might find that a bit
* confusing. Its also then possible to have a false positive:
* a text stream that contains a ZMODEM initialization string but
* isnt, in fact, meant to start a ZMODEM session.
*
* Workflow:
* - parse all input with .consume(). As long as nothing looks
* like ZMODEM, all the traffic will go to to_terminal().
*
* - when a tell-tale sequence of bytes arrives, we create a
* Detection object and pass it to the on_detect handler.
*
* - Either .confirm() or .deny() with the Detection object.
* This is the users chance to say, yeah, I know those
* bytes look like ZMODEM, but theyre not. So back off!
*
* If you .confirm(), the Session object is returned, and
* further input that goes to the Sentrys .consume() will
* go to the (now-active) Session object.
*
* - Sometimes additional traffic arrives that makes it apparent
* that no ZMODEM session is intended to start; in this case,
* the Sentry marks the Detection as stale and calls the
* `on_retract` handler. Any attempt from here to .confirm()
* on the Detection object will prompt an exception.
*
* (This retraction behavior will only happen prior to
* .confirm() or .deny() being called on the Detection object.
* Beyond that point, either the Session has to deal with the
* garbage, or its back to the terminal anyway.
*
* - Once the Session object is done, the Sentry will again send
* all traffic to to_terminal().
*/
Zmodem.Sentry = class ZmodemSentry {
/**
* Invoked directly. Creates a new Sentry that inspects all
* traffic before it goes to the terminal.
*
* @param {Object} options - The Sentry parameters
*
* @param {Function} options.to_terminal - Handler that sends
* traffic to the terminal object. Receives an iterable object
* (e.g., an Array) that contains octet numbers.
*
* @param {Function} options.on_detect - Handler for new
* detection events. Receives a new Detection object.
*
* @param {Function} options.on_retract - Handler for retraction
* events. Receives no input.
*
* @param {Function} options.sender - Handler that sends traffic to
* the peer. If, for example, your application uses WebSocket to talk
* to the peer, use this to send data to the WebSocket instance.
*/
constructor(options) {
if (!options) throw "Need options!";
var sentry = this;
SENTRY_CONSTRUCTOR_REQUIRED_ARGS.forEach( function(arg) {
if (!options[arg]) {
throw "Need “" + arg + "”!";
}
sentry["_" + arg] = options[arg];
} );
this._cache = [];
}
_after_session_end() {
this._zsession = null;
}
/**
* Consumes a piece of input:
*
* - If there is no active or pending ZMODEM session, the text is
* all output. (This is regardless of whether weve got a new
* Detection.)
*
* - If there is no active ZMODEM session and the input **ends** with
* a ZRINIT or ZRQINIT, then a new Detection object is created,
* and it is passed to the on_detect function.
* If there was another pending Detection object, it is retracted.
*
* - If there is no active ZMODEM session and the input does NOT end
* with a ZRINIT or ZRQINIT, then any pending Detection object is
* retracted.
*
* - If there is an active ZMODEM session, the input is passed to it.
* Any non-ZMODEM data (i.e., garbage) parsed from the input
* is sent to output.
* If the ZMODEM session ends, any post-ZMODEM part of the input
* is sent to output.
*
* @param {number[] | ArrayBuffer} input - Octets to parse as input.
*/
consume(input) {
if (!(input instanceof Array)) {
input = Array.prototype.slice.call( new Uint8Array(input) );
}
if (this._zsession) {
var session_before_consume = this._zsession;
session_before_consume.consume(input);
if (session_before_consume.has_ended()) {
if (session_before_consume.type === "receive") {
input = session_before_consume.get_trailing_bytes();
}
else {
input = [];
}
}
else return;
}
var new_session = this._parse(input);
var to_terminal = input;
if (new_session) {
let replacement_detect = !!this._parsed_session;
if (replacement_detect) {
//no terminal output if the new session is of the
//same type as the old
if (this._parsed_session.type === new_session.type) {
to_terminal = [];
}
this._on_retract();
}
this._parsed_session = new_session;
var sentry = this;
function checker() {
return sentry._parsed_session === new_session;
}
//This runs with the Sentry object as the context.
function accepter() {
if (!this.is_valid()) {
throw "Stale ZMODEM session!";
}
new_session.on("garbage", sentry._to_terminal);
new_session.on(
"session_end",
sentry._after_session_end.bind(sentry)
);
new_session.set_sender(sentry._sender);
delete sentry._parsed_session;
return sentry._zsession = new_session;
};
function denier() {
if (!this.is_valid()) return;
};
this._on_detect( new Detection(
new_session.type,
accepter,
this._send_abort.bind(this),
checker
) );
}
else {
/*
if (this._parsed_session) {
this._session_stale_because = 'Non-ZMODEM output received after ZMODEM initialization.';
}
*/
var expired_session = this._parsed_session;
this._parsed_session = null;
if (expired_session) {
//If we got a single “C” after parsing a session,
//that means our peer is trying to downgrade to YMODEM.
//That wont work, so we just send the ABORT_SEQUENCE
//right away.
if (to_terminal.length === 1 && to_terminal[0] === 67) {
this._send_abort();
}
this._on_retract();
}
}
this._to_terminal(to_terminal);
}
/**
* @return {Session|null} The sentrys current Session object, or
* null if there is none.
*/
get_confirmed_session() {
return this._zsession || null;
}
_send_abort() {
this._sender( Zmodem.ZMLIB.ABORT_SEQUENCE );
}
/**
* Parse an input stream and decide how much of it goes to the
* terminal or to a new Session object.
*
* This will accommodate input strings that are fragmented
* across calls to this function; e.g., if you send the first
* two bytes at the end of one parse() call then send the rest
* at the beginning of the next, parse() will recognize it as
* the beginning of a ZMODEM session.
*
* In order to keep from blocking any actual useful data to the
* terminal in real-time, this will send on the initial
* ZRINIT/ZRQINIT bytes to the terminal. Theyre meant to go to the
* terminal anyway, so that should be fine.
*
* @private
*
* @param {Array|Uint8Array} array_like - The input bytes.
* Each member should be a number between 0 and 255 (inclusive).
*
* @return {Array} A two-member list:
* 0) the bytes that should be printed on the terminal
* 1) the created Session object (if any)
*/
_parse(array_like) {
var cache = this._cache;
cache.push.apply( cache, array_like );
while (true) {
let common_hex_at = Zmodem.ZMLIB.find_subarray( cache, COMMON_ZM_HEX_START );
if (-1 === common_hex_at) break;
let before_common_hex = cache.splice(0, common_hex_at);
let zsession;
try {
zsession = Zmodem.Session.parse(cache);
} catch(err) { //ignore errors
//console.log(err);
}
if (!zsession) break;
//Dont need to parse the trailing XON.
if ((cache.length === 1) && (cache[0] === Zmodem.ZMLIB.XON)) {
cache.shift();
}
//If there are still bytes in the cache,
//then we dont have a ZMODEM session. This logic depends
//on the sender only sending one initial header.
return cache.length ? null : zsession;
}
cache.splice( MAX_ZM_HEX_START_LENGTH );
return null;
}
}

1677
src/zsession.js Normal file

File diff suppressed because it is too large Load diff

241
src/zsubpacket.js Normal file
View file

@ -0,0 +1,241 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zcrc"),
require("./zdle"),
require("./zmlib"),
require("./zerror")
);
const
ZCRCE = 0x68, // 'h', 104, frame ends, header packet follows
ZCRCG = 0x69, // 'i', 105, frame continues nonstop
ZCRCQ = 0x6a, // 'j', 106, frame continues, ZACK expected
ZCRCW = 0x6b // 'k', 107, frame ends, ZACK expected
;
var SUBPACKET_BUILDER;
/** Class that represents a ZMODEM data subpacket. */
Zmodem.Subpacket = class ZmodemSubpacket {
/**
* Build a Subpacket subclass given a payload and frame end string.
*
* @param {Array} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {string} frameend - One of:
* - `no_end_no_ack`
* - `end_no_ack`
* - `no_end_ack` (unused currently)
* - `end_ack`
*
* @returns {Subpacket} An instance of the appropriate Subpacket subclass.
*/
static build(octets, frameend) {
//TODO: make this better
var Ctr = SUBPACKET_BUILDER[frameend];
if (!Ctr) {
throw("No subpacket type “" + frameend + "” is defined! Try one of: " + Object.keys(SUBPACKET_BUILDER).join(", "));
}
return new Ctr(octets);
}
/**
* Return the octet values array that represents the object
* encoded with a 16-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
encode16(zencoder) {
return this._encode( zencoder, Zmodem.CRC.crc16 );
}
/**
* Return the octet values array that represents the object
* encoded with a 32-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
encode32(zencoder) {
return this._encode( zencoder, Zmodem.CRC.crc32 );
}
/**
* Return the subpacket payloads octet values.
*
* NOTE: For speed, this returns the actual data in the subpacket;
* if you mutate this return value, you alter the Subpacket object
* internals. This is OK if you wont need the Subpacket anymore, but
* just be careful.
*
* @returns {number[]} The subpackets payload, represented as an
* array of octet values. **DO NOT ALTER THIS ARRAY** unless you
* no longer need the Subpacket.
*/
get_payload() { return this._payload }
/**
* Parse out a Subpacket object from a given array of octet values,
* assuming a 16-bit CRC.
*
* An exception is thrown if the given bytes are definitively invalid
* as subpacket values with 16-bit CRC.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Subpacket|undefined} An instance of the appropriate Subpacket
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid subpacket here or not.
*/
static parse16(octets) {
return ZmodemSubpacket._parse(octets, 2);
}
//parse32 test:
//[102, 105, 108, 101, 110, 97, 109, 101, 119, 105, 116, 104, 115, 112, 97, 99, 101, 115, 0, 49, 55, 49, 51, 49, 52, 50, 52, 51, 50, 49, 55, 50, 49, 48, 48, 54, 52, 52, 48, 49, 49, 55, 0, 43, 8, 63, 115, 23, 17]
/**
* Same as parse16(), but assuming a 32-bit CRC.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Subpacket|undefined} An instance of the appropriate Subpacket
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid subpacket here or not.
*/
static parse32(octets) {
return ZmodemSubpacket._parse(octets, 4);
}
/**
* Not used directly.
*/
constructor(payload) {
this._payload = payload;
}
_encode(zencoder, crc_func) {
return zencoder.encode( this._payload.slice(0) ).concat(
[ Zmodem.ZMLIB.ZDLE, this._frameend_num ],
zencoder.encode( crc_func( this._payload.concat(this._frameend_num) ) )
);
}
//Because of ZDLE encoding, well never see any of the frame-end octets
//in a stream except as the ends of data payloads.
static _parse(bytes_arr, crc_len) {
var end_at;
var creator;
//These have to be written in decimal since theyre lookup keys.
var _frame_ends_lookup = {
104: ZEndNoAckSubpacket,
105: ZNoEndNoAckSubpacket,
106: ZNoEndAckSubpacket,
107: ZEndAckSubpacket,
};
var zdle_at = 0;
while (zdle_at < bytes_arr.length) {
zdle_at = bytes_arr.indexOf( Zmodem.ZMLIB.ZDLE, zdle_at );
if (zdle_at === -1) return;
var after_zdle = bytes_arr[ zdle_at + 1 ];
creator = _frame_ends_lookup[ after_zdle ];
if (creator) {
end_at = zdle_at + 1;
break;
}
zdle_at++;
}
if (!creator) return;
var frameend_num = bytes_arr[end_at];
//sanity check
if (bytes_arr[end_at - 1] !== Zmodem.ZMLIB.ZDLE) {
throw( "Byte before frame end should be ZDLE, not " + bytes_arr[end_at - 1] );
}
var zdle_encoded_payload = bytes_arr.splice( 0, end_at - 1 );
var got_crc = Zmodem.ZDLE.splice( bytes_arr, 2, crc_len );
if (!got_crc) {
//got payload but no CRC yet .. should be rare!
//We have to put the ZDLE-encoded payload back before returning.
bytes_arr.unshift.apply(bytes_arr, zdle_encoded_payload);
return;
}
var payload = Zmodem.ZDLE.decode(zdle_encoded_payload);
//We really shouldnt need to do this, but just for good measure.
//I suppose its conceivable this may run over UDP or something?
Zmodem.CRC[ (crc_len === 2) ? "verify16" : "verify32" ](
payload.concat( [frameend_num] ),
got_crc
);
return new creator(payload, got_crc);
}
}
class ZEndSubpacketBase extends Zmodem.Subpacket {
frame_end() { return true }
}
class ZNoEndSubpacketBase extends Zmodem.Subpacket {
frame_end() { return false }
}
//Used for end-of-file.
class ZEndNoAckSubpacket extends ZEndSubpacketBase {
ack_expected() { return false }
}
ZEndNoAckSubpacket.prototype._frameend_num = ZCRCE;
//Used for ZFILE and ZSINIT payloads.
class ZEndAckSubpacket extends ZEndSubpacketBase {
ack_expected() { return true }
}
ZEndAckSubpacket.prototype._frameend_num = ZCRCW;
//Used for ZDATA, prior to end-of-file.
class ZNoEndNoAckSubpacket extends ZNoEndSubpacketBase {
ack_expected() { return false }
}
ZNoEndNoAckSubpacket.prototype._frameend_num = ZCRCG;
//only used if receiver can full-duplex
class ZNoEndAckSubpacket extends ZNoEndSubpacketBase {
ack_expected() { return true }
}
ZNoEndAckSubpacket.prototype._frameend_num = ZCRCQ;
SUBPACKET_BUILDER = {
end_no_ack: ZEndNoAckSubpacket,
end_ack: ZEndAckSubpacket,
no_end_no_ack: ZNoEndNoAckSubpacket,
no_end_ack: ZNoEndAckSubpacket,
};

130
src/zvalidation.js Normal file
View file

@ -0,0 +1,130 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zerror")
);
const LOOKS_LIKE_ZMODEM_HEADER = /\*\x18[AC]|\*\*\x18B/;
function _validate_number(key, value) {
if (value < 0) {
throw new Zmodem.Error("validation", "“" + key + "” (" + value + ") must be nonnegative.");
}
if (value !== Math.floor(value)) {
throw new Zmodem.Error("validation", "“" + key + "” (" + value + ") must be an integer.");
}
}
/** Validation logic for zmodem.js
*
* @exports Validation
*/
Zmodem.Validation = {
/**
* Validates and normalizes a set of parameters for an offer to send.
* NOTE: This returns mtime as epoch seconds, not a Date. This is
* inconsistent with the get_details() method in Session, but its
* more useful for sending over the wire.
*
* @param {FileDetails} params - The file details. Some fairly trivial
* variances from the specification are allowed.
*
* @return {FileDetails} The parameters that should be sent. `mtime`
* will be a Date rather than a number.
*/
offer_parameters: function offer_parameters(params) {
if (!params.name) {
throw new Zmodem.Error("validation", "Need “name”!");
}
if (typeof params.name !== "string") {
throw new Zmodem.Error("validation", "“name” (" + params.name + ") must be a string!");
}
//So that we can override values as is useful
//without affecting the passed-in object.
params = Object.assign({}, params);
if (LOOKS_LIKE_ZMODEM_HEADER.test(params.name)) {
console.warn("The filename " + JSON.stringify(name) + " contains characters that look like a ZMODEM header. This could corrupt the ZMODEM session; consider renaming it so that the filename doesnt contain control characters.");
}
if (params.serial !== null && params.serial !== undefined) {
throw new Zmodem.Error("validation", "“serial” is meaningless.");
}
params.serial = null;
["size", "mode", "files_remaining", "bytes_remaining"].forEach(
function(k) {
var ok;
switch (typeof params[k]) {
case "object":
ok = (params[k] === null);
break;
case "undefined":
params[k] = null;
ok = true;
break;
case "number":
_validate_number(k, params[k]);
ok = true;
break;
}
if (!ok) {
throw new Zmodem.Error("validation", "“" + k + "” (" + params[k] + ") must be null, undefined, or a number.");
}
}
);
if (typeof params.mode === "number") {
params.mode |= 0x8000;
}
if (params.files_remaining === 0) {
throw new Zmodem.Error("validation", "“files_remaining”, if given, must be positive.");
}
var mtime_ok;
switch (typeof params.mtime) {
case "object":
mtime_ok = true;
if (params.mtime instanceof Date) {
var date_obj = params.mtime;
params.mtime = Math.floor( date_obj.getTime() / 1000 );
if (params.mtime < 0) {
throw new Zmodem.Error("validation", "“mtime” (" + date_obj + ") must not be earlier than 1970.");
}
}
else if (params.mtime !== null) {
mtime_ok = false;
}
break;
case "undefined":
params.mtime = null;
mtime_ok = true;
break;
case "number":
_validate_number("mtime", params.mtime);
mtime_ok = true;
break;
}
if (!mtime_ok) {
throw new Zmodem.Error("validation", "“mtime” (" + params.mtime + ") must be null, undefined, a Date, or a number.");
}
return params;
},
};