Adding upstream version 0.1.10+dfsg.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
157f539082
commit
4d3e0bf859
42 changed files with 10556 additions and 0 deletions
124
src/encode.js
Normal file
124
src/encode.js
Normal 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) {
|
||||
//Can’t bit-shift because that runs into JS’s 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
33
src/text.js
Normal 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
143
src/zcrc.js
Normal 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
240
src/zdle.js
Normal file
|
@ -0,0 +1,240 @@
|
|||
"use strict";
|
||||
|
||||
var Zmodem = module.exports;
|
||||
|
||||
Object.assign(
|
||||
Zmodem,
|
||||
require("./zmlib")
|
||||
);
|
||||
|
||||
//encode() variables - declare them here so we don’t
|
||||
//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 don’t 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 isn’t 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 isn’t 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) {
|
||||
|
||||
//Don’t 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 doesn’t 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
47
src/zerror.js
Normal 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
763
src/zheader.js
Normal 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 {
|
||||
|
||||
//lrzsz’s “sz” command sends a random (?) CR/0x0d byte
|
||||
//after ZEOF. Let’s accommodate 0x0a, 0x0d, 0x8a, and 0x8d.
|
||||
//
|
||||
//Also, when you skip a file, sz outputs a message about it.
|
||||
//
|
||||
//It appears that we’re supposed to ignore anything until
|
||||
//[ ZPAD, ZDLE ] when we’re looking for a header.
|
||||
|
||||
/**
|
||||
* Weed out the leading bytes that aren’t 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 there’s no escaping of the output it’s possible
|
||||
//that the garbage could trip us up, e.g., by having a filename
|
||||
//be a legit ZMODEM header. But that’s 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 it’s 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.
|
||||
|
||||
//We’re assuming the length of the header is 4 in
|
||||
//this logic … but ZMODEM isn’t 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 don’t trim any more.
|
||||
break TRIM_LOOP;
|
||||
}
|
||||
|
||||
//Otherwise, we’ll 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 we’ll throw away the parser.
|
||||
//It’s 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 aren’t 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 Mattheij’s 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 don’t 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 what’s 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 don’t see it used in lrzsz or syncterm
|
||||
escape_8th_bit() {
|
||||
return !!( this._bytes4[3] & ZSINIT_FLAG.ESC8 );
|
||||
}
|
||||
}
|
||||
|
||||
//Thus far it doesn’t seem we really need this header except to respond
|
||||
//to ZSINIT, which doesn’t 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”. It’s 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 shouldn’t 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 doesn’t 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
102
src/zmlib.js
Normal 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 - ZMODEM’s 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 appears—or, -1 if “needle” doesn’t 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
4
src/zmodem.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
Object.assign(
|
||||
module.exports,
|
||||
require("./zsentry"),
|
||||
);
|
182
src/zmodem_browser.js
Normal file
182
src/zmodem_browser.js
Normal 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 it’s 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 shouldn’t happen … so let’s
|
||||
//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) don’t.
|
||||
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 we’re done, or with another file-send promise
|
||||
//if there’s 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 element’s `click()`
|
||||
* method. The element is removed immediately after.
|
||||
*
|
||||
* @param {Array} packets - Same as the first argument to [Blob’s 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;
|
||||
//I’d 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
394
src/zsentry.js
Normal 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'
|
||||
//ZRQINIT’s next byte will be '0'; ZRINIT’s 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 Sentry’s 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 Sentry’s `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 doesn’t end in error. That’s
|
||||
//possible if we’re the sender, we accept the session,
|
||||
//then we just send a close(), but it doesn’t seem to be
|
||||
//possible for a receiver. Thus, let’s just leave it so
|
||||
//it’s 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 session’s 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
|
||||
* it’s 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 wouldn’t 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. That’ll be one less step
|
||||
* for the user, but an unaccustomed user might find that a bit
|
||||
* confusing. It’s also then possible to have a “false positive”:
|
||||
* a text stream that contains a ZMODEM initialization string but
|
||||
* isn’t, 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 user’s chance to say, “yeah, I know those
|
||||
* bytes look like ZMODEM, but they’re not. So back off!”
|
||||
*
|
||||
* If you .confirm(), the Session object is returned, and
|
||||
* further input that goes to the Sentry’s .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 it’s 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 we’ve 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 won’t 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 sentry’s 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. They’re 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;
|
||||
|
||||
//Don’t 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 don’t 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
1677
src/zsession.js
Normal file
File diff suppressed because it is too large
Load diff
241
src/zsubpacket.js
Normal file
241
src/zsubpacket.js
Normal 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 payload’s 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 won’t need the Subpacket anymore, but
|
||||
* just be careful.
|
||||
*
|
||||
* @returns {number[]} The subpacket’s 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, we’ll 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 they’re 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 shouldn’t need to do this, but just for good measure.
|
||||
//I suppose it’s 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
130
src/zvalidation.js
Normal 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 it’s
|
||||
* 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 doesn’t 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;
|
||||
},
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue