1
0
Fork 0

Adding upstream version 0.1.10+dfsg.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-04-22 16:48:36 +02:00
parent 157f539082
commit 4d3e0bf859
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
42 changed files with 10556 additions and 0 deletions

119
tests/encode.js Executable file
View file

@ -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();
} );

121
tests/lib/testhelp.js Normal file
View file

@ -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 cant 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 } );
},
};

1
tests/lib/zmodem.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('../../src/zmodem.js');

45
tests/text.js Executable file
View file

@ -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();
} );

113
tests/zcrc.js Executable file
View file

@ -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 dont
// 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();
} );

41
tests/zdle.js Executable file
View file

@ -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();
} );

82
tests/zerror.js Normal file
View file

@ -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();
});

309
tests/zheader.js Executable file
View file

@ -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, //its 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` );
//Heres 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 ) {
//Heres 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();
} );

81
tests/zmlib.js Executable file
View file

@ -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();
} );

226
tests/zsentry.js Executable file
View file

@ -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();
} );

312
tests/zsession.js Executable file
View file

@ -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 files 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 files 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',
);
} );
} );

295
tests/zsession_receive.js Executable file
View file

@ -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 doesnt 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" );
} );
});

248
tests/zsession_send.js Executable file
View file

@ -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 sailors 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; //were 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');
} );
} );
});

62
tests/zsubpacket.js Executable file
View file

@ -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();
} );

227
tests/zvalidation.js Normal file
View file

@ -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();
});