summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xtests/encode.js119
-rw-r--r--tests/lib/testhelp.js121
-rw-r--r--tests/lib/zmodem.js1
-rwxr-xr-xtests/text.js45
-rwxr-xr-xtests/zcrc.js113
-rwxr-xr-xtests/zdle.js41
-rw-r--r--tests/zerror.js82
-rwxr-xr-xtests/zheader.js309
-rwxr-xr-xtests/zmlib.js81
-rwxr-xr-xtests/zsentry.js226
-rwxr-xr-xtests/zsession.js312
-rwxr-xr-xtests/zsession_receive.js295
-rwxr-xr-xtests/zsession_send.js248
-rwxr-xr-xtests/zsubpacket.js62
-rw-r--r--tests/zvalidation.js227
15 files changed, 2282 insertions, 0 deletions
diff --git a/tests/encode.js b/tests/encode.js
new file mode 100755
index 0000000..615635f
--- /dev/null
+++ b/tests/encode.js
@@ -0,0 +1,119 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+
+var enclib = Zmodem.ENCODELIB;
+
+tape('round-trip: 32-bit little-endian', function(t) {
+ var times = 1000;
+
+ t.doesNotThrow(
+ () => {
+ for (var a=0; a<times; a++) {
+ var orig = Math.floor( 0xffffffff * Math.random() );
+
+ var enc = enclib.pack_u32_le(orig);
+ var roundtrip = enclib.unpack_u32_le(enc);
+
+ if (roundtrip !== orig) {
+ throw( `Orig: ${orig}, Packed: ` + JSON.stringify(enc) + `, Parsed: ${roundtrip}` );
+ }
+ }
+ },
+ `round-trip 32-bit little-endian: ${times} times`
+ );
+
+ t.end();
+} );
+
+tape('unpack_u32_le', function(t) {
+ t.equals(
+ enclib.unpack_u32_le([222,233,202,254]),
+ 4274711006,
+ 'unpack 4-byte number'
+ );
+
+ var highest = 0xffffffff;
+ t.equals(
+ enclib.unpack_u32_le([255,255,255,255]),
+ highest,
+ `highest number possible (${highest})`
+ );
+
+ t.equals(
+ enclib.unpack_u32_le([1, 0, 0, 0]),
+ 1,
+ '1'
+ );
+
+ t.end();
+});
+
+tape('unpack_u16_be', function(t) {
+ t.equals(
+ enclib.unpack_u16_be([202,254]),
+ 51966,
+ 'unpack 2-byte number'
+ );
+
+ var highest = 0xffff;
+ t.equals(
+ enclib.unpack_u16_be([255,255]),
+ highest,
+ `highest number possible (${highest})`
+ );
+
+ t.equals(
+ enclib.unpack_u16_be([0, 1]),
+ 1,
+ '1'
+ );
+
+ t.end();
+});
+
+tape('octets_to_hex', function(t) {
+ t.deepEquals(
+ enclib.octets_to_hex( [ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x0a ] ),
+ '123456789abcdef00a'.split("").map( (c) => c.charCodeAt(0) ),
+ 'hex encoding'
+ );
+
+ t.end();
+} );
+
+tape('parse_hex_octets', function(t) {
+ t.deepEquals(
+ enclib.parse_hex_octets( [ 48, 49, 102, 101 ] ),
+ [ 0x01, 0xfe ],
+ 'parse hex excoding',
+ );
+
+ t.end();
+} );
+
+tape('round-trip: 16-bit big-endian', function(t) {
+ var times = 10000;
+
+ t.doesNotThrow(
+ () => {
+ for (var a=0; a<times; a++) {
+ var orig = Math.floor( 0x10000 * Math.random() );
+
+ var enc = enclib.pack_u16_be(orig);
+ var roundtrip = enclib.unpack_u16_be(enc);
+
+ if (roundtrip !== orig) {
+ throw( `Orig: ${orig}, Packed: ` + JSON.stringify(enc) + `, Parsed: ${roundtrip}` );
+ }
+ }
+ },
+ `round-trip 16-bit big-endian: ${times} times`
+ );
+
+ t.end();
+} );
diff --git a/tests/lib/testhelp.js b/tests/lib/testhelp.js
new file mode 100644
index 0000000..ae1ab69
--- /dev/null
+++ b/tests/lib/testhelp.js
@@ -0,0 +1,121 @@
+var Zmodem = require('./zmodem');
+
+module.exports = {
+ /**
+ * Return an array with the given number of random octet values.
+ *
+ * @param {Array} count - The number of octet values to return.
+ *
+ * @returns {Array} The octet values.
+ */
+ get_random_octets(count) {
+ if (!(count > 0)) throw( "Must be positive, not " + count );
+
+ var octets = [];
+
+ //This assigns backwards both for convenience and so that
+ //the initial assignment allocates the needed size.
+ while (count) {
+ octets[count - 1] = Math.floor( Math.random() * 256 );
+ count--;
+ }
+
+ return octets;
+ },
+
+ //This is meant NOT to do UTF-8 stuff since it handles \xXX.
+ string_to_octets(string) {
+ return string.split("").map( (c) => c.charCodeAt(0) );
+ },
+
+ make_temp_dir() {
+ return require('tmp').dirSync().name;
+ },
+
+ make_temp_file(size) {
+ const fs = require('fs');
+ const tmp = require('tmp');
+
+ var tmpobj = tmp.fileSync();
+ var content = Array(size).fill("x").join("");
+ fs.writeSync( tmpobj.fd, content );
+ fs.writeSync( tmpobj.fd, "=THE_END" );
+ fs.closeSync( tmpobj.fd );
+
+ return tmpobj.name;
+ },
+
+ make_empty_temp_file() {
+ const fs = require('fs');
+ const tmp = require('tmp');
+
+ var tmpobj = tmp.fileSync();
+ fs.closeSync( tmpobj.fd );
+
+ return tmpobj.name;
+ },
+
+ exec_lrzsz_steps(t, binpath, z_args, steps) {
+ const spawn = require('child_process').spawn;
+
+ var child;
+
+ var zsession;
+ var zsentry = new Zmodem.Sentry( {
+ to_terminal: Object,
+ on_detect: (d) => { zsession = d.confirm() },
+ on_retract: console.error.bind(console),
+ sender: (d) => {
+ child.stdin.write( new Buffer(d) );
+ },
+ } );
+
+ var step = 0;
+ var inputs = [];
+
+ child = spawn(binpath, z_args);
+ console.log("child PID:", child.pid);
+
+ child.on("error", console.error.bind(console));
+
+ child.stdin.on("close", () => console.log(`# PID ${child.pid} STDIN closed`));
+ child.stdout.on("close", () => console.log(`# PID ${child.pid} STDOUT closed`));
+ child.stderr.on("close", () => console.log(`# PID ${child.pid} STDERR closed`));
+
+ //We can’t just pipe this on through because there can be lone CR
+ //bytes which screw up TAP::Harness.
+ child.stderr.on("data", (d) => {
+ d = d.toString().replace(/\r\n?/g, "\n");
+ if (d.substr(-1) !== "\n") d += "\n";
+ process.stderr.write(`STDERR: ${d}`);
+ });
+
+ child.stdout.on("data", (d) => {
+ //console.log(`STDOUT from PID ${child.pid}`, d);
+ inputs.push( Array.from(d) );
+
+ zsentry.consume( Array.from(d) );
+
+ if (zsession) {
+ if ( steps[step] ) {
+ if ( steps[step](zsession, child) ) {
+ step++;
+ }
+ }
+ else {
+ console.log(`End of task list; closing PID ${child.pid}’s STDIN`);
+ child.stdin.end();
+ }
+ }
+ });
+
+ var exit_promise = new Promise( (res, rej) => {
+ child.on("exit", (code, signal) => {
+ console.log(`# "${binpath}" exit: code ${code}, signal ${signal}`);
+ res([code, signal]);
+ } );
+ } );
+
+ return exit_promise.then( () => { return inputs } );
+ },
+};
diff --git a/tests/lib/zmodem.js b/tests/lib/zmodem.js
new file mode 100644
index 0000000..8b485b4
--- /dev/null
+++ b/tests/lib/zmodem.js
@@ -0,0 +1 @@
+module.exports = require('../../src/zmodem.js');
diff --git a/tests/text.js b/tests/text.js
new file mode 100755
index 0000000..cdb5200
--- /dev/null
+++ b/tests/text.js
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var Zmodem = require('../src/zmodem');
+
+var ZText = Zmodem.Text;
+
+const TEXTS = [
+ [ "-./", [45, 46, 47] ],
+ [ "épée", [195, 169, 112, 195, 169, 101] ],
+ [ "“words”", [226, 128, 156, 119, 111, 114, 100, 115, 226, 128, 157] ],
+ [ "🍊", [240, 159, 141, 138] ],
+ [ "🍊🍊", [240, 159, 141, 138, 240, 159, 141, 138] ],
+];
+
+tape('decoder', function(t) {
+ var decoder = new ZText.Decoder();
+
+ TEXTS.forEach( (tt) => {
+ t.is(
+ decoder.decode( new Uint8Array(tt[1]) ),
+ tt[0],
+ `decode: ${tt[1]} -> ${tt[0]}`
+ );
+ } );
+
+ t.end();
+} );
+
+tape('encoder', function(t) {
+ var encoder = new ZText.Encoder();
+
+ TEXTS.forEach( (tt) => {
+ t.deepEquals(
+ encoder.encode(tt[0]),
+ new Uint8Array( tt[1] ),
+ `encode: ${tt[0]} -> ${tt[1]}`
+ );
+ } );
+
+ t.end();
+} );
diff --git a/tests/zcrc.js b/tests/zcrc.js
new file mode 100755
index 0000000..0698fa4
--- /dev/null
+++ b/tests/zcrc.js
@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var Zmodem = Object.assign(
+ {},
+ require('../src/zcrc')
+);
+
+var zcrc = Zmodem.CRC;
+
+tape('crc16', function(t) {
+ t.deepEqual(
+ zcrc.crc16( [ 0x0d, 0x0a ] ),
+ [ 0xd7, 0x16 ],
+ 'crc16 - first test'
+ );
+
+ t.deepEqual(
+ zcrc.crc16( [ 0x11, 0x17, 0, 0, 0 ] ),
+ [ 0xe4, 0x81 ],
+ 'crc16 - second test'
+ );
+
+ t.end();
+} );
+
+tape('verify16', function(t) {
+ t.doesNotThrow(
+ () => zcrc.verify16( [ 0x0d, 0x0a ], [ 0xd7, 0x16 ] ),
+ 'verify16 - no throw on good'
+ );
+
+ var err;
+ try { zcrc.verify16( [ 0x0d, 0x0a ], [ 0xd7, 16 ] ) }
+ catch(e) { err = e };
+
+ t.ok(
+ /215,16.*215,22/.test(err.message),
+ 'verify16 - throw on bad (message)'
+ );
+
+ t.ok(
+ err instanceof Zmodem.Error,
+ 'verify16 - typed error'
+ );
+
+ t.ok(
+ err.type,
+ 'verify16 - error type'
+ );
+
+ t.end();
+} );
+
+//----------------------------------------------------------------------
+// The crc32 logic is unused for now, but some misbehaving ZMODEM
+// implementation might send CRC32 regardless of that we don’t
+// advertise it.
+//----------------------------------------------------------------------
+
+tape('crc32', function(t) {
+ const tests = [
+ [ [ 4, 0, 0, 0, 0 ], [ 0xdd, 0x51, 0xa2, 0x33 ] ],
+ [ [ 11, 17, 0, 0, 0 ], [ 0xf6, 0xf6, 0x57, 0x59 ] ],
+ [ [ 3, 0, 0, 0, 0 ], [ 205, 141, 130, 129 ] ],
+ ];
+// } [ 3, 0, 0, 0, 0 ] [ 205, 141, 131, -127 ]
+//2172816845
+//crc32 [ 3, 0, 0, 0, 0 ] -2122150451
+
+ tests.forEach( (cur_t) => {
+ let [ input, output ] = cur_t;
+
+ t.deepEqual(
+ zcrc.crc32(input),
+ output,
+ "crc32: " + input.join(", ")
+ );
+ } );
+
+ t.end();
+} );
+
+tape('verify32', function(t) {
+ t.doesNotThrow(
+ () => zcrc.verify32( [ 4, 0, 0, 0, 0 ], [ 0xdd, 0x51, 0xa2, 0x33 ] ),
+ 'verify32 - no throw on good'
+ );
+
+ var err;
+ try { zcrc.verify32( [ 4, 0, 0, 0, 0 ], [ 1,2,3,4 ] ) }
+ catch(e) { err = e };
+
+ t.ok(
+ /1,2,3,4.*221,81,162,51/.test(err.message),
+ 'verify32 - throw on bad (message)'
+ );
+
+ t.ok(
+ err instanceof Zmodem.Error,
+ 'verify32 - typed error'
+ );
+
+ t.ok(
+ err.type,
+ 'verify32 - error type'
+ );
+
+ t.end();
+} );
diff --git a/tests/zdle.js b/tests/zdle.js
new file mode 100755
index 0000000..9fc49b7
--- /dev/null
+++ b/tests/zdle.js
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+const helper = require('./lib/testhelp');
+
+var zmlib = Zmodem.ZMLIB;
+var ZDLE = Zmodem.ZDLE;
+
+tape('round-trip', function(t) {
+ var zdle = new ZDLE( { escape_ctrl_chars: true } );
+
+ var times = 1000;
+
+ t.doesNotThrow(
+ () => {
+ for (let a of Array(times)) {
+ var orig = helper.get_random_octets(38);
+ var enc = zdle.encode( orig.slice(0) );
+ var dec = ZDLE.decode( enc.slice(0) );
+
+ var orig_j = orig.join();
+ var dec_j = dec.join();
+
+ if (orig_j !== dec_j) {
+ console.error("Original", orig.join());
+ console.error("Encoded", enc.join());
+ console.error("Decoded", dec.join());
+
+ throw 'mismatch';
+ }
+ }
+ },
+ `round-trip`
+ );
+
+ t.end();
+} );
diff --git a/tests/zerror.js b/tests/zerror.js
new file mode 100644
index 0000000..f7961e6
--- /dev/null
+++ b/tests/zerror.js
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+
+"use strict";
+
+global.Zmodem = require('./lib/zmodem');
+
+const tape = require('blue-tape'),
+ TYPE_CHECKS = {
+ aborted: [ [] ],
+ peer_aborted: [],
+ already_aborted: [],
+ crc: [
+ [ [ 1, 2 ], [ 3, 4 ] ],
+ (t, err) => {
+ t.ok(
+ /1,2/.test(err.message),
+ '"got" values are in the message'
+ );
+ t.ok(
+ /3,4/.test(err.message),
+ '"expected" values are in the message'
+ );
+ t.ok(
+ /CRC/i.test(err.message),
+ '"CRC" is in the message'
+ );
+ },
+ ],
+ validation: [
+ [ "some string" ],
+ (t, err) => {
+ t.is(
+ err.message,
+ "some string",
+ 'message is given value'
+ );
+ },
+ ],
+ }
+;
+
+tape("typed", (t) => {
+ let Ctr = Zmodem.Error;
+
+ for (let type in TYPE_CHECKS) {
+ let args = [type].concat( TYPE_CHECKS[type][0] );
+
+ //https://stackoverflow.com/questions/33193310/constr-applythis-args-in-es6-classes
+ var err = new (Ctr.bind.apply(Ctr, [null].concat(args)));
+
+ t.ok(
+ (err instanceof Zmodem.Error),
+ `${type} type isa ZmodemError`
+ );
+ t.ok(
+ !!err.message.length,
+ `${type}: message has length`
+ );
+
+ if ( TYPE_CHECKS[type][1] ) {
+ TYPE_CHECKS[type][1](t, err);
+ }
+ }
+
+ t.end();
+});
+
+tape("generic", (t) => {
+ let err = new Zmodem.Error("Van Gogh was a guy.");
+
+ t.ok(
+ (err instanceof Zmodem.Error),
+ `generic isa ZmodemError`
+ );
+ t.is(
+ err.message,
+ "Van Gogh was a guy.",
+ "passthrough of string"
+ );
+
+ t.end();
+});
diff --git a/tests/zheader.js b/tests/zheader.js
new file mode 100755
index 0000000..b462da6
--- /dev/null
+++ b/tests/zheader.js
@@ -0,0 +1,309 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var testhelp = require('./lib/testhelp');
+
+global.Zmodem = require('./lib/zmodem');
+
+var zdle = new Zmodem.ZDLE( { escape_ctrl_chars: true } );
+
+tape('trim_leading_garbage', function(t) {
+ var header = Zmodem.Header.build('ZACK');
+
+ var header_octets = new Map( [
+ [ "hex", header.to_hex(), ],
+ [ "b16", header.to_binary16(zdle), ],
+ [ "b32", header.to_binary32(zdle), ],
+ ] );
+
+ var leading_garbage = [
+ "",
+ " ",
+ "\n\n",
+ "\r\n\r\n",
+ "*",
+ "**",
+ "*\x18",
+ "*\x18D",
+ "**\x18",
+ ];
+
+ leading_garbage.forEach( (garbage) => {
+ let garbage_json = JSON.stringify(garbage);
+ let garbage_octets = testhelp.string_to_octets( garbage );
+
+ for ( let [label, hdr_octets] of header_octets ) {
+ var input = garbage_octets.slice(0).concat( hdr_octets );
+ var trimmed = Zmodem.Header.trim_leading_garbage(input);
+
+ t.deepEquals(trimmed, garbage_octets, `${garbage_json} + ${label}: garbage trimmed`);
+ t.deepEquals(input, hdr_octets, `… leaving the header`);
+ }
+ } );
+
+ //----------------------------------------------------------------------
+
+ //input, number of bytes trimmed
+ var partial_trims = [
+ [ "*", 0 ],
+ [ "**", 0 ],
+ [ "***", 1 ],
+ [ "*\x18**", 2 ],
+ [ "*\x18*\x18", 2 ],
+ [ "*\x18*\x18**", 4 ],
+ [ "*\x18*\x18*\x18", 4 ],
+ ];
+
+ partial_trims.forEach( (cur) => {
+ let [ input, trimmed_count ] = cur;
+
+ let input_json = JSON.stringify(input);
+
+ let input_octets = testhelp.string_to_octets(input);
+
+ let garbage = Zmodem.Header.trim_leading_garbage(input_octets.slice(0));
+
+ t.deepEquals(
+ garbage,
+ input_octets.slice(0, trimmed_count),
+ `${input_json}: trim first ${trimmed_count} byte(s)`
+ );
+ } );
+
+ t.end();
+});
+
+//Test that we parse a trailing 0x8a, since we ourselves follow the
+//documentation and put a plain LF (0x0a).
+tape('parse_hex', function(t) {
+ var octets = testhelp.string_to_octets( "**\x18B0901020304a57f\x0d\x8a" );
+
+ var parsed = Zmodem.Header.parse( octets );
+
+ t.is( parsed[1], 16, 'CRC size' );
+
+ t.is(
+ parsed[0].NAME,
+ 'ZRPOS',
+ 'parsed NAME'
+ );
+
+ t.is(
+ parsed[0].TYPENUM,
+ 9,
+ 'parsed TYPENUM'
+ );
+
+ t.is(
+ parsed[0].get_offset(),
+ 0x04030201, //it’s little-endian
+ 'parsed offset'
+ );
+
+ t.end();
+} );
+
+tape('round-trip, empty headers', function(t) {
+ ["ZRQINIT", "ZSKIP", "ZABORT", "ZFIN", "ZFERR"].forEach( (n) => {
+ var orig = Zmodem.Header.build(n);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ for ( const [ enc, h ] of rounds ) {
+ let [ parsed, crclen ] = Zmodem.Header.parse(h);
+
+ t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );
+
+ //Here’s where we test the CRC length in the response.
+ t.is(
+ crclen,
+ /32/.test(enc) ? 32 : 16,
+ `${n}, ${enc}: CRC length`,
+ );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('round-trip, offset headers', function(t) {
+ ["ZRPOS", "ZDATA", "ZEOF"].forEach( (n) => {
+ var orig = Zmodem.Header.build(n, 12345);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ for ( const [ enc, h ] of rounds ) {
+ //Here’s where we test that parse() leaves in trailing bytes.
+ let extra = [99, 99, 99];
+ let bytes_with_extra = h.slice().concat(extra);
+
+ let parsed = Zmodem.Header.parse(bytes_with_extra)[0];
+
+ t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );
+ t.is( parsed.get_offset(), orig.get_offset(), `${n}, ${enc}: get_offset()` );
+
+ let expected = extra.slice(0);
+ if (enc === "to_hex") {
+ expected.splice( 0, 0, Zmodem.ZMLIB.XON );
+ }
+
+ t.deepEquals(
+ bytes_with_extra,
+ expected,
+ `${enc}: parse() leaves in trailing bytes`,
+ );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('round-trip, ZSINIT', function(t) {
+ var opts = [
+ [],
+ ["ESCCTL"],
+ ];
+
+ opts.forEach( (args) => {
+ var orig = Zmodem.Header.build("ZSINIT", args);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ var args_str = JSON.stringify(args);
+
+ for ( const [ enc, h ] of rounds ) {
+ let parsed = Zmodem.Header.parse(h)[0];
+
+ t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );
+
+ t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
+ t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('round-trip, ZRINIT', function(t) {
+ var opts = [];
+
+ [ [], ["CANFDX"] ].forEach( (canfdx) => {
+ [ [], ["CANOVIO"] ].forEach( (canovio) => {
+ [ [], ["CANBRK"] ].forEach( (canbrk) => {
+ [ [], ["CANFC32"] ].forEach( (canfc32) => {
+ [ [], ["ESCCTL"] ].forEach( (escctl) => {
+ opts.push( [
+ ...canfdx,
+ ...canovio,
+ ...canbrk,
+ ...canfc32,
+ ...escctl,
+ ] );
+ } );
+ } );
+ } );
+ } );
+ } );
+
+ opts.forEach( (args) => {
+ var orig = Zmodem.Header.build("ZRINIT", args);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ var args_str = JSON.stringify(args);
+
+ for ( const [ enc, h ] of rounds ) {
+ let parsed = Zmodem.Header.parse(h)[0];
+
+ t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );
+
+ t.is( parsed.can_full_duplex(), orig.can_full_duplex(), `opts ${args_str}: ${enc}: can_full_duplex()` );
+ t.is( parsed.can_overlap_io(), orig.can_overlap_io(), `opts ${args_str}: ${enc}: can_overlap_io()` );
+ t.is( parsed.can_break(), orig.can_break(), `opts ${args_str}: ${enc}: can_break()` );
+ t.is( parsed.can_fcs_32(), orig.can_fcs_32(), `opts ${args_str}: ${enc}: can_fcs_32()` );
+ t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
+ t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('hex_final_XON', function(t) {
+ var hex_ZFIN = Zmodem.Header.build("ZFIN").to_hex();
+
+ t.notEquals(
+ hex_ZFIN.slice(-1)[0],
+ Zmodem.ZMLIB.XON,
+ 'ZFIN hex does NOT end with XON',
+ );
+
+ var hex_ZACK = Zmodem.Header.build("ZACK").to_hex();
+
+ t.notEquals(
+ hex_ZACK.slice(-1)[0],
+ Zmodem.ZMLIB.XON,
+ 'ZACK hex does NOT end with XON',
+ );
+
+ var headers = [
+ "ZRQINIT",
+ Zmodem.Header.build("ZRINIT", []),
+ Zmodem.Header.build("ZSINIT", []),
+ "ZRPOS",
+ "ZABORT",
+ "ZFERR",
+ ];
+
+ //These are the only headers we expect to send as hex … right?
+ headers.forEach( hdr => {
+ if (typeof hdr === "string") hdr = Zmodem.Header.build(hdr);
+
+ t.is(
+ hdr.to_hex().slice(-1)[0],
+ Zmodem.ZMLIB.XON,
+ `${hdr.NAME} hex ends with XON`
+ );
+ } );
+
+ t.end();
+} );
diff --git a/tests/zmlib.js b/tests/zmlib.js
new file mode 100755
index 0000000..e9dfc11
--- /dev/null
+++ b/tests/zmlib.js
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+
+var zmlib = Zmodem.ZMLIB;
+
+tape('constants', function(t) {
+ t.equal(typeof zmlib.ZDLE, "number", 'ZDLE');
+ t.equal(typeof zmlib.XON, "number", 'XON');
+ t.equal(typeof zmlib.XOFF, "number", 'XOFF');
+ t.end();
+} );
+
+tape('strip_ignored_bytes', function(t) {
+ var input = [ zmlib.XOFF, 12, 45, 76, zmlib.XON, 22, zmlib.XOFF, 32, zmlib.XON | 0x80, 0, zmlib.XOFF | 0x80, 255, zmlib.XON ];
+ var should_be = [ 12, 45, 76, 22, 32, 0, 255 ];
+
+ var input_copy = input.slice(0);
+
+ var out = zmlib.strip_ignored_bytes(input_copy);
+
+ t.deepEqual( out, should_be, 'intended bytes are stripped' );
+ t.equal( out, input_copy, 'output is the mutated input' );
+
+ t.end();
+} );
+
+/*
+tape('get_random_octets', function(t) {
+ t.equal(
+ zmlib.get_random_octets(42).length,
+ 42,
+ 'length is correct'
+ );
+
+ t.equal(
+ typeof zmlib.get_random_octets(42)[0],
+ "number",
+ 'type is correct'
+ );
+
+ t.ok(
+ zmlib.get_random_octets(999999).every( (i) => i>=0 && i<=255 ),
+ 'values are all octet values'
+ );
+
+ t.end();
+} );
+*/
+
+tape('find_subarray', function(t) {
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [43, 77]),
+ 2,
+ 'finds at end'
+ );
+
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [12, 56]),
+ 0,
+ 'finds at begin'
+ );
+
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [56, 43]),
+ 1,
+ 'finds in the middle'
+ );
+
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [56, 43, 43]),
+ -1,
+ 'non-find'
+ );
+
+ t.end();
+} );
diff --git a/tests/zsentry.js b/tests/zsentry.js
new file mode 100755
index 0000000..d97f486
--- /dev/null
+++ b/tests/zsentry.js
@@ -0,0 +1,226 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var helper = require('./lib/testhelp');
+
+global.Zmodem = require('./lib/zmodem');
+
+var ZSentry = Zmodem.Sentry;
+
+function _generate_tester() {
+ var tester = {
+ reset() {
+ this.to_terminal = [];
+ this.to_server = [];
+ this.retracted = 0;
+ }
+ };
+
+ tester.sentry = new ZSentry( {
+ to_terminal(octets) { tester.to_terminal.push.apply( tester.to_terminal, octets ) },
+ on_detect(z) { tester.detected = z; },
+ on_retract(z) { tester.retracted++; },
+ sender(octets) { tester.to_server.push.apply( tester.to_server, octets ) },
+ } );
+
+ tester.reset();
+
+ return tester;
+}
+
+tape('user says deny() to detection', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("hey**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.is( typeof tester.detected, "object", 'There is a session after ZRQINIT' );
+
+ var sent_before = tester.to_server.length;
+
+ tester.detected.deny();
+
+ t.deepEqual(
+ tester.to_server.slice(-Zmodem.ZMLIB.ABORT_SEQUENCE.length),
+ Zmodem.ZMLIB.ABORT_SEQUENCE,
+ 'deny() sends abort sequence to server',
+ );
+
+ t.end();
+} );
+
+tape('retraction because of non-ZMODEM', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("hey**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.is( typeof tester.detected, "object", 'There is a session after ZRQINIT' );
+
+ tester.sentry.consume([ 0x20, 0x21, 0x22 ]);
+
+ t.is( tester.retracted, 1, 'retraction since we got non-ZMODEM input' );
+
+ t.end();
+} );
+
+tape('retraction because of YMODEM downgrade', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.deepEquals( tester.to_server, [], 'nothing sent to server before' );
+
+ tester.sentry.consume( helper.string_to_octets("C") );
+
+ t.deepEquals( tester.to_server, Zmodem.ZMLIB.ABORT_SEQUENCE, 'abort sent to server' );
+
+ t.end();
+} );
+
+tape('replacement ZMODEM is not of same type', (t) => {
+ var tester = _generate_tester();
+
+ var zrqinit = helper.string_to_octets("**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(zrqinit);
+
+ var before = tester.to_terminal.length;
+
+ var zrinit = helper.string_to_octets("**\x18B0100000000aa51\x0d\x0a\x11");
+ tester.sentry.consume(zrinit);
+
+ t.notEqual(
+ tester.to_terminal.length,
+ before,
+ 'output to terminal when replacement session is of different type'
+ );
+
+ t.end();
+} );
+
+tape('retraction because of duplicate ZMODEM, and confirm()', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.is( typeof tester.detected, "object", 'There is a detection after ZRQINIT' );
+
+ var first_detected = tester.detected;
+ t.is( first_detected.is_valid(), true, 'detection is valid' );
+
+ tester.reset();
+
+ tester.sentry.consume(makes_offer);
+
+ t.is( tester.retracted, 1, 'retraction since we got non-ZMODEM input' );
+ t.deepEquals( tester.to_terminal, [], 'nothing sent to terminal on dupe session' );
+
+ t.notEqual(
+ tester.detected,
+ first_detected,
+ '… but a new detection happened in its place',
+ );
+
+ t.is( first_detected.is_valid(), false, 'old detection is invalid' );
+ t.is( tester.detected.is_valid(), true, 'new detection is valid' );
+
+ //----------------------------------------------------------------------
+
+ var session = tester.detected.confirm();
+
+ t.is( (session instanceof Zmodem.Session), true, 'confirm() on the detection' );
+ t.is( session.type, "receive", 'session is of the right type' );
+
+ tester.reset();
+
+ //Verify that the Detection configures the Session correctly.
+ session.start();
+ t.is( !!tester.to_server.length, true, 'sent output after start()' );
+
+ t.end();
+} );
+
+tape('parse passthrough', (t) => {
+ var tester = _generate_tester();
+
+ var strings = new Map( [
+ [ "plain", "heyhey", ],
+ [ "one_asterisk", "hey*hey", ],
+ [ "two_asterisks", "hey**hey", ],
+ [ "wrong_header", "hey**\x18B09010203040506\x0d\x0a", ],
+ [ "ZRQINIT but not at end", "hey**\x18B00000000000000\x0d\x0ahahahaha", ],
+ [ "ZRINIT but not at end", "hey**\x18B01010203040506\x0d\x0ahahahaha", ],
+
+ //Use \x2a here to avoid tripping up ZMODEM-detection in
+ //text editors when working on this code.
+ [ "no_ZDLE", "hey\x2a*B00000000000000\x0d\x0a", ],
+ ] );
+
+ for (let [name, string] of strings) {
+ tester.reset();
+
+ var octets = helper.string_to_octets(string);
+
+ var before = octets.slice(0);
+
+ tester.sentry.consume(octets);
+
+ t.deepEquals(
+ tester.to_terminal,
+ before,
+ `regular text goes through: ${name}`
+ );
+
+ t.is( tester.detected, undefined, '... and there is no session' );
+ t.deepEquals( octets, before, '... and the array is unchanged' );
+ }
+
+ t.end();
+} );
+
+tape('parse', (t) => {
+ var hdrs = new Map( [
+ [ "receive", Zmodem.Header.build("ZRQINIT"), ],
+ [ "send", Zmodem.Header.build("ZRINIT", ["CANFDX", "CANOVIO", "ESCCTL"]), ],
+ ] );
+
+ for ( let [sesstype, hdr] of hdrs ) {
+ var full_input = helper.string_to_octets("before").concat(
+ hdr.to_hex()
+ );
+
+ for (var start=1; start<full_input.length - 1; start++) {
+ let octets1 = full_input.slice(0, start);
+ let octets2 = full_input.slice(start);
+
+ var tester = _generate_tester();
+ tester.sentry.consume(octets1);
+
+ t.deepEquals(
+ tester.to_terminal,
+ octets1,
+ `${sesstype}: Parse first ${start} byte(s) of text (${full_input.length} total)`
+ );
+ t.is( tester.detected, undefined, '... and there is no session' );
+
+ tester.reset();
+
+ tester.sentry.consume(octets2);
+ t.deepEquals(
+ tester.to_terminal,
+ octets2,
+ `Rest of text goes through`
+ );
+ t.is( typeof tester.detected, "object", '... and now there is a session' );
+ t.is( tester.detected.get_session_role(), sesstype, '... of the right type' );
+
+ }
+ };
+
+ t.end();
+} );
diff --git a/tests/zsession.js b/tests/zsession.js
new file mode 100755
index 0000000..e4b638f
--- /dev/null
+++ b/tests/zsession.js
@@ -0,0 +1,312 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const test = require('tape');
+
+const helper = require('./lib/testhelp');
+global.Zmodem = require('./lib/zmodem');
+
+var ZSession = Zmodem.Session;
+
+var receiver, sender, sender_promise, received_file;
+
+var offer;
+
+function wait(seconds) {
+ return new Promise( resolve => setTimeout(_ => resolve("theValue"), 1000 * seconds) );
+}
+
+function _init(async) {
+ sender = null;
+ receiver = new Zmodem.Session.Receive();
+
+ /*
+ receiver.on("receive", function(hdr) {
+ console.log("Receiver input", hdr);
+ } );
+ receiver.on("offer", function(my_offer) {
+ //console.log("RECEIVED OFFER (window.offer)", my_offer);
+ offer = my_offer;
+ });
+ */
+
+ var resolver;
+ sender_promise = new Promise( (res, rej) => { resolver = res; } );
+
+ function receiver_sender(bytes_arr) {
+ //console.log("receiver sending", String.fromCharCode.apply(String, bytes_arr), bytes_arr);
+
+ if (sender) {
+ var consumer = () => {
+ sender.consume(bytes_arr);
+ };
+
+ if (async) {
+ wait(0.5).then(consumer);
+ }
+ else consumer();
+ }
+ else {
+ var hdr = Zmodem.Header.parse(bytes_arr)[0];
+ sender = new Zmodem.Session.Send(hdr);
+ resolver(sender);
+
+ sender.set_sender( function(bytes_arr) {
+ var consumer = () => {
+ receiver.consume(bytes_arr);
+ };
+
+ if (async) {
+ wait(0.5).then(consumer);
+ }
+ else consumer();
+ } );
+
+ /*
+ sender.on("receive", function(hdr) {
+ console.log("Sender input", hdr);
+ } );
+ */
+ }
+ }
+
+ receiver.set_sender(receiver_sender);
+}
+
+test('Sender receives extra ZRPOS', (t) => {
+ _init();
+
+ var zrinit = Zmodem.Header.build("ZRINIT", ["CANFDX", "CANOVIO", "ESCCTL"]);
+ var mysender = new Zmodem.Session.Send(zrinit);
+
+ var zrpos = Zmodem.Header.build("ZRPOS", 12345);
+
+ var err;
+
+ try {
+ mysender.consume(zrpos.to_hex());
+ }
+ catch(e) {
+ err = e;
+ }
+
+ t.match(err.toString(), /header/, "error as expected");
+ t.match(err.toString(), /ZRPOS/, "error as expected");
+
+ return Promise.resolve();
+} );
+
+test('Offer events', (t) => {
+ _init();
+
+ var inputs = [];
+ var completed = false;
+
+ var r_pms = receiver.start().then( (offer) => {
+ t.deepEquals(
+ offer.get_details(),
+ {
+ name: "my file",
+ size: 32,
+ mode: null,
+ mtime: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ 'get_details() returns expected values'
+ );
+
+ offer.on("input", (payload) => {
+ inputs.push(
+ {
+ offset: offer.get_offset(),
+ payload: payload,
+ }
+ );
+ } );
+
+ offer.on("complete", () => { completed = true });
+
+ return offer.accept();
+ } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file", size: 32 }
+ ).then( (sender_xfer) => {
+ sender_xfer.send( [1, 2, 3] );
+ sender_xfer.send( [4, 5, 6, 7] );
+ sender_xfer.end( [8, 9] ).then( () => {
+ return sender.close();
+ } );
+ } );
+
+ return Promise.all( [ r_pms, s_pms ] ).then( () => {
+ t.deepEquals(
+ inputs,
+ [
+ {
+ payload: [1, 2, 3],
+ offset: 3,
+ },
+ {
+ payload: [4, 5, 6, 7],
+ offset: 7,
+ },
+ {
+ payload: [8, 9],
+ offset: 9,
+ },
+ ],
+ 'Offer “input” events',
+ );
+
+ t.ok( completed, 'Offer “complete” event' );
+ } );
+} );
+
+test('receive one, promises', (t) => {
+ _init();
+
+ var r_pms = receiver.start().then( (offer) => {
+ t.deepEquals(
+ offer.get_details(),
+ {
+ name: "my file",
+ size: 32,
+ mode: null,
+ mtime: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ 'get_details() returns expected values'
+ );
+
+ return offer.accept();
+ } );
+
+ //r_pms.then( () => { console.log("RECEIVER DONE") } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file", size: 32 }
+ ).then( (sender_xfer) => {
+ sender_xfer.end( [12, 23, 34] ).then( () => {
+ return sender.close();
+ } );
+ } );
+
+ return Promise.all( [ r_pms, s_pms ] );
+} );
+
+test('receive one, events', (t) => {
+ _init();
+
+ var content = [ 1,2,3,4,5,6,7,8,9,2,3,5,1,5,33,2,23,7 ];
+
+ var now_epoch = Math.floor(Date.now() / 1000);
+
+ receiver.on("offer", (offer) => {
+ t.deepEquals(
+ offer.get_details(),
+ {
+ name: "my file",
+ size: content.length,
+ mode: parseInt("100644", 8),
+ mtime: new Date( now_epoch * 1000 ),
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ 'get_details() returns expected values'
+ );
+
+ offer.accept();
+ } );
+ receiver.start();
+
+ return sender.send_offer( {
+ name: "my file",
+ size: content.length,
+ mtime: now_epoch,
+ mode: parseInt("0644", 8),
+ } ).then(
+ (sender_xfer) => {
+ sender_xfer.end(content).then( sender.close.bind(sender) );
+ }
+ );
+} );
+
+test('skip one, receive the next', (t) => {
+ _init();
+
+ var r_pms = receiver.start().then( (offer) => {
+ //console.log("first offer", offer);
+
+ t.equals( offer.get_details().name, "my file", "first file’s name" );
+ var next_pms = offer.skip();
+ //console.log("next", next_pms);
+ return next_pms;
+ } ).then( (offer) => {
+ t.equals( offer.get_details().name, "file 2", "second file’s name" );
+ return offer.skip();
+ } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file" }
+ ).then(
+ (sender_xfer) => {
+ t.ok( !sender_xfer, "skip() -> sender sees no transfer object" );
+ return sender.send_offer( { name: "file 2" } );
+ }
+ ).then(
+ (xfer) => {
+ t.ok( !xfer, "2nd skip() -> sender sees no transfer object" );
+ return sender.close();
+ }
+ );
+
+ return Promise.all( [ r_pms, s_pms ] );
+} );
+
+test('abort mid-download', (t) => {
+ _init();
+
+ var transferred_bytes = [];
+
+ var aborted;
+
+ var r_pms = receiver.start().then( (offer) => {
+ offer.on("input", (payload) => {
+ [].push.apply(transferred_bytes, payload);
+
+ if (aborted) throw "already aborted!";
+ aborted = true;
+
+ receiver.abort();
+ });
+ return offer.accept();
+ } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file" }
+ ).then(
+ (xfer) => {
+ xfer.send( [1, 2, 3] );
+ xfer.end( [99, 99, 99] ); //should never get here
+ }
+ );
+
+ return Promise.all( [r_pms, s_pms] ).catch(
+ (err) => {
+ t.ok( err.message.match('abort'), 'error message is about abort' );
+ }
+ ).then( () => {
+ t.deepEquals(
+ transferred_bytes,
+ [1, 2, 3],
+ 'abort() stopped us from sending more',
+ );
+ } );
+} );
diff --git a/tests/zsession_receive.js b/tests/zsession_receive.js
new file mode 100755
index 0000000..4b126a9
--- /dev/null
+++ b/tests/zsession_receive.js
@@ -0,0 +1,295 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const tape = require('blue-tape');
+
+const SZ_PATH = require('which').sync('sz', {nothrow: true});
+
+if (!SZ_PATH) {
+ tape.only('SKIP: no “sz” in PATH!', (t) => {
+ t.end();
+ });
+}
+
+const spawn = require('child_process').spawn;
+
+var helper = require('./lib/testhelp');
+
+Object.assign(
+ global,
+ {
+ Zmodem: require('./lib/zmodem'),
+ }
+);
+
+var FILE1 = helper.make_temp_file(10 * 1024 * 1024); //10 MiB
+
+function _test_steps(t, sz_args, steps) {
+ return helper.exec_lrzsz_steps( t, SZ_PATH, sz_args, steps );
+}
+
+tape('abort() after ZRQINIT', (t) => {
+ return _test_steps( t, [FILE1], [
+ (zsession, child) => {
+ zsession.abort();
+ return true;
+ },
+ ] ).then( (inputs) => {
+ //console.log("inputs", inputs);
+
+ var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
+ t.ok(
+ str.match(/\x18\x18\x18\x18\x18/),
+ 'abort() right after receipt of ZRQINIT',
+ );
+ } );
+});
+
+tape('abort() after ZFILE', (t) => {
+ return _test_steps( t, [FILE1], [
+ (zsession) => {
+ zsession.start();
+ return true;
+ },
+ (zsession) => {
+ zsession.abort();
+ return true;
+ },
+ ] ).then( (inputs) => {
+ //console.log("inputs", inputs);
+
+ var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
+ t.ok(
+ str.match(/\x18\x18\x18\x18\x18/),
+ 'abort() right after receipt of ZFILE',
+ );
+ } );
+});
+
+//NB: This test is not unlikely to flap since it depends
+//on sz reading the abort sequence prior to finishing its read
+//of the file.
+tape('abort() during download', { timeout: 30000 }, (t) => {
+ var child_pms = _test_steps( t, [FILE1], [
+ (zsession) => {
+ zsession.on("offer", (offer) => offer.accept() );
+ zsession.start();
+ return true;
+ },
+ (zsession) => {
+ zsession.abort();
+ return true;
+ },
+ ] );
+
+ return child_pms.then( (inputs) => {
+ t.notEquals( inputs, undefined, 'abort() during download ends the transmission' );
+
+ t.ok(
+ inputs.every( function(bytes) {
+ var str = String.fromCharCode.apply( String, bytes );
+ return !/THE_END/.test(str);
+ } ),
+ "the end of the file was not sent",
+ );
+ } );
+});
+
+//This only works because we use CRC32 to receive. CRC16 in lsz has a
+//buffer overflow bug, fixed here:
+//
+// https://github.com/gooselinux/lrzsz/blob/master/lrzsz-0.12.20.patch
+//
+tape('skip() during download', { timeout: 30000 }, (t) => {
+ var filenames = [FILE1, helper.make_temp_file(12345678)];
+ //filenames = ["-vvvvvvvvvvvvv", FILE1, _make_temp_file()];
+
+ var started, second_offer;
+
+ return _test_steps( t, filenames, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ offer.accept();
+ offer.skip();
+ zsession.off("offer", offer_taker);
+ zsession.on("offer", (offer2) => {
+ second_offer = offer2;
+ offer2.skip();
+ });
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+ //return true;
+ },
+ ] ).then( (inputs) => {
+ var never_end = inputs.every( function(bytes) {
+ var str = String.fromCharCode.apply( String, bytes );
+ return !/THE_END/.test(str);
+ } );
+
+ // This is race-prone.
+ //t.ok( never_end, "the end of a file is never sent" );
+
+ t.ok( !!second_offer, "we got a 2nd offer after the first" );
+ } );
+});
+
+tape('skip() - immediately - at end of download', { timeout: 30000 }, (t) => {
+ var filenames = [helper.make_temp_file(123)];
+
+ var started;
+
+ return _test_steps( t, filenames, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ offer.accept();
+ offer.skip();
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+
+ started = true;
+ }
+ },
+ ] );
+});
+
+// Verify a skip() that happens after a transfer is complete.
+// There are no assertions here.
+tape('skip() - after a parse - at end of download', { timeout: 30000 }, (t) => {
+ var filenames = [helper.make_temp_file(123)];
+
+ var the_offer, started, skipped, completed;
+
+ return _test_steps( t, filenames, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ the_offer = offer;
+ var promise = the_offer.accept();
+ promise.then( () => {
+ completed = 1;
+ } );
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+
+ return the_offer;
+ },
+ () => {
+ if (!skipped && !completed) {
+ the_offer.skip();
+ skipped = true;
+ }
+ },
+ ] );
+});
+
+var happy_filenames = [
+ helper.make_temp_file(5),
+ helper.make_temp_file(3),
+ helper.make_temp_file(1),
+ helper.make_empty_temp_file(),
+];
+
+tape('happy-path: single batch', { timeout: 30000 }, (t) => {
+ var started, the_offer;
+
+ var args = happy_filenames;
+
+ var buffers = [];
+
+ var child_pms = _test_steps( t, args, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ the_offer = offer;
+ the_offer.accept( { on_input: "spool_array" } ).then( (byte_lists) => {
+ var flat = [].concat.apply([], byte_lists);
+ var str = String.fromCharCode.apply( String, flat );
+ buffers.push(str);
+ } );
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+
+ return false;
+ },
+ ] );
+
+ return child_pms.then( (inputs) => {
+ t.equals( buffers[0], "xxxxx=THE_END", '5-byte transfer plus end' );
+ t.equals( buffers[1], "xxx=THE_END", '3-byte transfer plus end' );
+ t.equals( buffers[2], "x=THE_END", '1-byte transfer plus end' );
+ t.equals( buffers[3], "", 'empty transfer plus end' );
+ } );
+});
+
+tape('happy-path: individual transfers', { timeout: 30000 }, (t) => {
+ var promises = happy_filenames.map( (fn) => {
+ var str;
+
+ var started;
+
+ var child_pms = _test_steps( t, [fn], [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ offer.accept( { on_input: "spool_array" } ).then( (byte_lists) => {
+ var flat = [].concat.apply([], byte_lists);
+ str = String.fromCharCode.apply( String, flat );
+ } );
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+
+ return false;
+ },
+ ] );
+
+ return child_pms.then( () => str );
+ } );
+
+ return Promise.all(promises).then( (strs) => {
+ t.equals( strs[0], "xxxxx=THE_END", '5-byte transfer plus end' );
+ t.equals( strs[1], "xxx=THE_END", '3-byte transfer plus end' );
+ t.equals( strs[2], "x=THE_END", '1-byte transfer plus end' );
+ t.equals( strs[3], "", 'empty transfer plus end' );
+ } );
+});
+
+//This doesn’t work because we automatically send ZFIN once we receive it,
+//which prompts the child to finish up.
+tape.skip("abort() after ZEOF", (t) => {
+ var received;
+
+ return _test_steps( t, [FILE1], [
+ (zsession) => {
+ zsession.on("offer", (offer) => {
+ offer.accept().then( () => { received = true } );
+ } );
+ zsession.start();
+ return true;
+ },
+ (zsession) => {
+ if (received) {
+ zsession.abort();
+ return true;
+ }
+ },
+ ] ).then( (inputs) => {
+ var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
+ t.is( str, "OO", "successful close despite abort" );
+ } );
+});
diff --git a/tests/zsession_send.js b/tests/zsession_send.js
new file mode 100755
index 0000000..da7cbf0
--- /dev/null
+++ b/tests/zsession_send.js
@@ -0,0 +1,248 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const fs = require('fs');
+const tape = require('blue-tape');
+
+const RZ_PATH = require('which').sync('rz', {nothrow: true});
+
+if (!RZ_PATH) {
+ tape.only('SKIP: no “rz” in PATH!', (t) => {
+ t.end();
+ });
+}
+
+Object.assign(
+ global,
+ {
+ Zmodem: require('./lib/zmodem'),
+ }
+);
+
+var helper = require('./lib/testhelp');
+
+var dir_before = process.cwd();
+tape.onFinish( () => process.chdir( dir_before ) );
+
+let TEST_STRINGS = [
+ "",
+ "0",
+ "123",
+ "\x00",
+ "\x18",
+ "\x18\x18\x18\x18\x18", //invalid as UTF-8
+ "\x8a\x9a\xff\xfe", //invalid as UTF-8
+ "épée",
+ "Hi diddle-ee, dee! A sailor’s life for me!",
+];
+
+var text_encoder = require('text-encoding').TextEncoder;
+text_encoder = new text_encoder();
+
+function _send_batch(t, batch, on_offer) {
+ batch = batch.slice(0);
+
+ return helper.exec_lrzsz_steps( t, RZ_PATH, [], [
+ (zsession, child) => {
+ function offer_sender() {
+ if (!batch.length) {
+ zsession.close();
+ return; //batch finished
+ }
+
+ return zsession.send_offer(
+ batch[0][0]
+ ).then( (xfer) => {
+ if (on_offer) {
+ on_offer(xfer, batch[0]);
+ }
+
+ let file_contents = batch.shift()[1];
+
+ var octets;
+ if ("string" === typeof file_contents) {
+ octets = text_encoder.encode(file_contents);
+ }
+ else {
+ octets = file_contents; // Buffer
+ }
+
+ return xfer && xfer.end( Array.from(octets) );
+ } ).then( offer_sender );
+ }
+
+ return offer_sender();
+ },
+ (zsession, child) => {
+ return zsession.has_ended();
+ },
+ ] );
+}
+
+function _do_in_temp_dir( todo ) {
+ var ret;
+
+ process.chdir( helper.make_temp_dir() );
+
+ try {
+ ret = todo();
+ }
+ catch(e) {
+ throw e;
+ }
+ finally {
+ if (!ret) {
+ process.chdir( dir_before );
+ }
+ }
+
+ if (ret) {
+ ret = ret.then( () => process.chdir( dir_before ) );
+ }
+
+ return ret;
+}
+
+tape("rz accepts one, then skips next", (t) => {
+ return _do_in_temp_dir( () => {
+ let filename = "no-clobberage";
+
+ var batch = [
+ [
+ { name: filename },
+ "the first",
+ ],
+ [
+ { name: filename },
+ "the second",
+ ],
+ ];
+
+ var offers = [];
+ function offer_cb(xfer, batch_item) {
+ offers.push( xfer );
+ }
+
+ return _send_batch(t, batch, offer_cb).then( () => {
+ var got_contents = fs.readFileSync(filename, "utf-8");
+ t.equals( got_contents, "the first", 'second offer was rejected' );
+
+ t.notEquals( offers[0], undefined, 'got an offer at first' );
+ t.equals( offers[1], undefined, '… but no offer second' );
+ } );
+ } );
+});
+
+tape("send batch", (t) => {
+ return _do_in_temp_dir( () => {
+ var string_num = 0;
+
+ var base = "batch_";
+ var mtime_1990 = new Date("1990-01-01T00:00:00Z");
+
+ var batch = TEST_STRINGS.map( (str, i) => {
+ return [
+ {
+ name: base + i,
+ mtime: mtime_1990,
+ },
+ str,
+ ];
+ } );
+
+ return _send_batch(t, batch).then( () => {
+ for (var sn=0; sn < TEST_STRINGS.length; sn++) {
+ var got_contents = fs.readFileSync(base + sn, "utf-8");
+ t.equals( got_contents, TEST_STRINGS[sn], `rz wrote out the file: ` + JSON.stringify(TEST_STRINGS[sn]) );
+ t.equals( 0 + fs.statSync(base + sn).mtime, 0 + mtime_1990, `... and observed the sent mtime` );
+ }
+ } );
+ } );
+});
+
+tape("send one at a time", (t) => {
+ return _do_in_temp_dir( () => {
+ var xfer;
+
+ let test_strings = TEST_STRINGS.slice(0);
+
+ function doer() {
+ var file_contents = test_strings.shift();
+ if (typeof(file_contents) !== "string") return; //we’re done
+
+ return helper.exec_lrzsz_steps( t, RZ_PATH, ["--overwrite"], [
+ (zsession, child) => {
+ zsession.send_offer( { name: "single" } ).then( (xf) => {
+ t.ok( !!xf, 'rz accepted offer' );
+ xfer = xf;
+ } ).then(
+ () => xfer.end( Array.from( text_encoder.encode(file_contents) ) )
+ ).then(
+ () => zsession.close()
+ );
+
+ return true;
+ },
+ (zsession, child) => {
+ return zsession.has_ended();
+ },
+ ] ).then( () => {
+ var got_contents = fs.readFileSync("single", "utf-8");
+ t.equals( got_contents, file_contents, `rz wrote out the file: ` + JSON.stringify(file_contents) );
+ } ).then( doer );
+ }
+
+ return doer();
+ } );
+});
+
+tape("send single large file", (t) => {
+ return _do_in_temp_dir( () => {
+ var string_num = 0;
+
+ var mtime_1990 = new Date("1990-01-01T00:00:00Z");
+ var big_string = Array(30 * 1024 * 1024).fill('x').join("");
+
+ var batch = [
+ [
+ {
+ name: "big_kahuna",
+ },
+ big_string,
+ ],
+ ];
+
+ return _send_batch(t, batch).then( () => {
+ var got_contents = fs.readFileSync("big_kahuna", "utf-8");
+ t.equals( got_contents, big_string, 'rz wrote out the file');
+ } );
+ } );
+});
+
+tape("send single random file", (t) => {
+ return _do_in_temp_dir( () => {
+ var string_num = 0;
+
+ var mtime_1990 = new Date("1990-01-01T00:00:00Z");
+
+ var big_buffer = new Buffer(1024 * 1024);
+ for (var i=0; i<big_buffer.length; i++) {
+ big_buffer[i] = Math.floor( Math.random(256) );
+ }
+
+ var batch = [
+ [
+ {
+ name: "big_kahuna",
+ },
+ big_buffer,
+ ],
+ ];
+
+ return _send_batch(t, batch).then( () => {
+ var got_contents = fs.readFileSync("big_kahuna");
+ t.equals( got_contents.join(), big_buffer.join(), 'rz wrote out the file');
+ } );
+ } );
+});
diff --git a/tests/zsubpacket.js b/tests/zsubpacket.js
new file mode 100755
index 0000000..eaf1624
--- /dev/null
+++ b/tests/zsubpacket.js
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const tape = require('blue-tape');
+
+const testhelp = require('./lib/testhelp');
+
+global.Zmodem = require('./lib/zmodem');
+
+var zdle = new Zmodem.ZDLE( { escape_ctrl_chars: true } );
+
+tape('build, encode, parse', function(t) {
+ let content = [1, 2, 3, 4];
+
+ ["end_ack", "no_end_ack", "end_no_ack", "no_end_no_ack"].forEach( end => {
+ var header = Zmodem.Subpacket.build( content, end );
+
+ t.deepEquals(
+ header.get_payload(),
+ content,
+ `${end}: get_payload()`
+ );
+
+ t.is(
+ header.frame_end(),
+ !/no_end/.test(end),
+ `${end}: frame_end()`
+ );
+
+ t.is(
+ header.ack_expected(),
+ !/no_ack/.test(end),
+ `${end}: ack_expected()`
+ );
+
+ [16, 32].forEach( crclen => {
+ var encoded = header["encode" + crclen](zdle);
+ var parsed = Zmodem.Subpacket["parse" + crclen](encoded);
+
+ t.deepEquals(
+ parsed.get_payload(),
+ content,
+ `${end}, CRC${crclen} rount-trip: get_payload()`
+ );
+
+ t.is(
+ parsed.frame_end(),
+ header.frame_end(),
+ `${end}, CRC${crclen} rount-trip: frame_end()`
+ );
+
+ t.is(
+ parsed.ack_expected(),
+ header.ack_expected(),
+ `${end}, CRC${crclen} rount-trip: ack_expected()`
+ );
+ } );
+ } );
+
+ t.end();
+} );
diff --git a/tests/zvalidation.js b/tests/zvalidation.js
new file mode 100644
index 0000000..64231b0
--- /dev/null
+++ b/tests/zvalidation.js
@@ -0,0 +1,227 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+
+const zcrc = Zmodem.CRC;
+
+var now = new Date();
+var now_epoch = Math.floor( now.getTime() / 1000 );
+
+var failures = [
+ [
+ 'empty name',
+ { name: "" },
+ function(t, e) {
+ t.ok( /name/.test(e.message), 'has “name”' );
+ },
+ ],
+ [
+ 'non-string name',
+ { name: 123 },
+ function(t, e) {
+ t.ok( /name/.test(e.message), 'has “name”' );
+ t.ok( /string/.test(e.message), 'has “string”' );
+ },
+ ],
+ [
+ 'non-empty serial',
+ { name: "123", serial: 0 },
+ function(t, e) {
+ t.ok( /serial/.test(e.message), 'has “serial”' );
+ },
+ ],
+ [
+ 'files_remaining === 0',
+ { name: "123", files_remaining: 0 },
+ function(t, e) {
+ t.ok( /files_remaining/.test(e.message), 'has “files_remaining”' );
+ },
+ ],
+ [
+ 'pre-epoch mtime',
+ { name: "123", mtime: new Date("1969-12-30T01:02:03Z") },
+ function(t, e) {
+ t.ok( /mtime/.test(e.message), 'has “mtime”' );
+ t.ok( /1969/.test(e.message), 'has “1969”' );
+ t.ok( /1970/.test(e.message), 'has “1970”' );
+ },
+ ],
+];
+
+["size", "mode", "mtime", "files_remaining", "bytes_remaining"].forEach( (k) => {
+ var input = { name: "the name" };
+ input[k] = "123123";
+
+ var key_regexp = new RegExp(k);
+ var value_regexp = new RegExp(input[k]);
+
+ failures.push( [
+ `string “${k}”`,
+ input,
+ function(t, e) {
+ t.ok( key_regexp.test(e.message), `has “${k}”` );
+ t.ok( value_regexp.test(e.message), 'has value' );
+ t.ok( /number/.test(e.message), 'has “number”' );
+ },
+ ] );
+
+ input = Object.assign( {}, input );
+ input[k] = -input[k];
+
+ var negative_regexp = new RegExp(input[k]);
+
+ failures.push( [
+ `negative “${k}”`,
+ input,
+ function(t, e) {
+ t.ok( key_regexp.test(e.message), `has “${k}”` );
+ t.ok( negative_regexp.test(e.message), 'has value' );
+ },
+ ] );
+
+ input = Object.assign( {}, input );
+ input[k] = -input[k] - 0.1;
+
+ var fraction_regexp = new RegExp( ("" + input[k]).replace(/\./, "\\.") );
+
+ failures.push( [
+ `fraction “${k}”`,
+ input,
+ function(t, e) {
+ t.ok( key_regexp.test(e.message), `has “${k}”` );
+ t.ok( fraction_regexp.test(e.message), 'has value' );
+ },
+ ] );
+} );
+
+
+var transformations = [
+ [
+ 'name only',
+ { name: "My name", },
+ {
+ name: "My name",
+ size: null,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name is all numerals',
+ { name: "0", },
+ {
+ name: "0",
+ size: null,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name only (undefined rather than null)',
+ {
+ name: "My name",
+ size: undefined,
+ mtime: undefined,
+ mode: undefined,
+ serial: undefined,
+ files_remaining: undefined,
+ bytes_remaining: undefined,
+ },
+ {
+ name: "My name",
+ size: null,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name and all numbers',
+ {
+ name: "My name",
+ size: 0,
+ mtime: 0,
+ mode: parseInt("0644", 8),
+ serial: null,
+ files_remaining: 1,
+ bytes_remaining: 0,
+ },
+ {
+ name: "My name",
+ size: 0,
+ mtime: 0,
+ mode: parseInt("100644", 8),
+ serial: null,
+ files_remaining: 1,
+ bytes_remaining: 0,
+ },
+ ],
+ [
+ 'name, zero size',
+ { name: "My name", mtime: now },
+ {
+ name: "My name",
+ size: null,
+ mtime: now_epoch,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name, mtime as Date',
+ { name: "My name", size: 0 },
+ {
+ name: "My name",
+ size: 0,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+];
+
+tape('offer_parameters - failures', function(t) {
+
+ for (const [label, input, todo] of failures) {
+ let err;
+ try {
+ Zmodem.Validation.offer_parameters(input);
+ }
+ catch(e) { err = e }
+
+ t.ok( err instanceof Zmodem.Error, `throws ok: ${label}` );
+
+ todo(t, err);
+ }
+
+ t.end();
+});
+
+tape('offer_parameters - happy path', function(t) {
+
+ for (const [label, input, output] of transformations) {
+ t.deepEquals(
+ Zmodem.Validation.offer_parameters(input),
+ output,
+ label,
+ );
+ }
+
+ t.end();
+});