@masx200/zmodem.js
Version:
ZMODEM file transfers in JavaScript
255 lines (219 loc) • 7.77 kB
JavaScript
;
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,
};