diff options
Diffstat (limited to '')
-rwxr-xr-x | tests/encode.js | 119 | ||||
-rw-r--r-- | tests/lib/testhelp.js | 121 | ||||
-rw-r--r-- | tests/lib/zmodem.js | 1 | ||||
-rwxr-xr-x | tests/text.js | 45 | ||||
-rwxr-xr-x | tests/zcrc.js | 113 | ||||
-rwxr-xr-x | tests/zdle.js | 41 | ||||
-rw-r--r-- | tests/zerror.js | 82 | ||||
-rwxr-xr-x | tests/zheader.js | 309 | ||||
-rwxr-xr-x | tests/zmlib.js | 81 | ||||
-rwxr-xr-x | tests/zsentry.js | 226 | ||||
-rwxr-xr-x | tests/zsession.js | 312 | ||||
-rwxr-xr-x | tests/zsession_receive.js | 295 | ||||
-rwxr-xr-x | tests/zsession_send.js | 248 | ||||
-rwxr-xr-x | tests/zsubpacket.js | 62 | ||||
-rw-r--r-- | tests/zvalidation.js | 227 |
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(); +}); |