"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 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;