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

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
Give clear and concise description of the bug.
**What happens when you follow README.mds TROUBLESHOOTING steps?**
_DO NOT_ omit this, or your issue may be closed without comment!
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
Describe, clearly and concisely, what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment(s):**
- OS: [e.g. iOS]
- JS engine [e.g., Chrome, Firefox, node.js, …]
**Additional context**
Add any other context about the problem here.

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.*.sw?
/dist
/documentation
/node_modules

9
.travis.yml Normal file
View file

@ -0,0 +1,9 @@
language: node_js
os:
- linux
- osx
node_js:
- "node"
before_install:
- if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get install lrzsz; fi
- if [ $TRAVIS_OS_NAME = osx ]; then brew update; brew install lrzsz; fi

44
CHANGELOG.md Normal file
View file

@ -0,0 +1,44 @@
# 0.1.10
Make unrecognized-header detection more resilient.
Ignore extra ZRPOS if received while sending a file. (See comments
for the rationale.)
Expose Zmodem.DEBUG for runtime adjustment.
Add a proof-of-concept CLI “sz” implementation to the distribution.
Change quality designation from ALPHA to BETA.
Documentation updates, including addition of a TROUBLESHOOTING section.
---
# 0.1.9
No production changes; this just disables a flapping test.
---
# 0.1.8
This version introduces some minor, mostly-under-the-hood changes:
1. `accept()` callbacks now fire after receipt of the ZEOF.
Previously they didnt fire until the sender indicated either the next
file (ZFILE) or the end of the batch (ZFIN). This actually brings the
behavior more in line with the documentation.
2. In the same vein, the `file_end` event now fires before ZRINIT is sent.
3. `skip()` is now a no-op if called outside of a transfer. Previously
it always sent a ZSKIP, which confused `sz` into sending an extra ZFIN if
it happened outside of a file transfer, which tripped up protocol errors
in zmodem.js.
4. A misnamed variable is now fixed.
Additionally, a bug in the tests that caused the test runner to skip
some test files is fixed. Every test now runs, and new tests are added that
verify the “happy-path” in receive sessions.

201
LICENSE Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

432
README.md Normal file
View file

@ -0,0 +1,432 @@
# zmodem.js - ZMODEM for JavaScript
[![build status](https://api.travis-ci.org/FGasper/zmodemjs.svg?branch=master)](http://travis-ci.org/FGasper/zmodemjs)
# SYNOPSIS
let zsentry = new Zmodem.Sentry( {
to_terminal(octets) { .. }, //i.e. send to the terminal
sender(octets) { .. }, //i.e. send to the ZMODEM peer
on_detect(detection) { .. }, //for when Sentry detects a new ZMODEM
on_retract() { .. }, //for when Sentry retracts a Detection
} );
//We have to configure whatever gives us new input to send that
//input to zsentry.
//
//EXAMPLE: From web browsers that use WebSocket …
//
ws.addEventListener("message", function(evt) {
zsentry.consume(evt.data);
} );
The `on_detect(detection)` function call is probably the most complex
piece of the above; one potential implementation might look like:
on_detect(detection) {
//Do this if we determine that what looked like a ZMODEM session
//is actually not meant to be ZMODEM.
if (no_good) {
detection.deny();
return;
}
zsession = detection.confirm();
if (zsession.type === "send") {
//Send a group of files, e.g., from an <input>s “.files”.
//There are events you can listen for here as well,
//e.g., to update a progress meter.
Zmodem.Browser.send_files( zsession, files_obj );
}
else {
zsession.on("offer", (xfer) => {
//Do this if you dont want the offered file.
if (no_good) {
xfer.skip();
return;
}
xfer.accept().then( () => {
//Now you need some mechanism to save the file.
//An example of how you can do this in a browser:
Zmodem.Browser.save_to_disk(
xfer.get_payloads(),
xfer.get_details().name
);
} );
});
zsession.start();
}
}
# DESCRIPTION
zmodem.js is a JavaScript implementation of the ZMODEM
file transfer protocol, which facilitates file transfers via a terminal.
# STATUS
This library is BETA quality. It should be safe for general use, but
breaking changes may still happen.
# HOW TO USE THIS LIBRARY
The basic workflow is:
1. Create a `Zmodem.Sentry` object. This object must scan all input for
a ZMODEM initialization string. See `zsentry.js`s documentation for more
details.
2. Once that initialization is found, the `on_detect` event is fired
with a `Detection` object as parameter. At this point you can `deny()`
that Detection or `confirm()` it; the latter will return a Session
object.
3. Now you do the actual file transfer(s):
* If the session is a receive session, do something like this:
zsession.on("offer", (offer) => { ... });
let { name, size, mtime, mode, serial, files_remaining, bytes_remaining } = offer.get_details();
offer.skip();
//...or:
offer.on("input", (octets) => { ... });
//accept()s return resolves when the transfer is complete.
offer.accept().then(() => { ... });
});
zsession.on("session_end", () => { ... });
zsession.start();
The `offer` handler receives an Offer object. This object exposes the details
about the transfer offer. The object also exposes controls for skipping or
accepting the offer.
* Otherwise, your session is a send session. Now the user chooses
zero or more files to send. For each of these you should do:
zsession.send_offer( { ... } ).then( (xfer) => {
if (!xfer) ... //skipped
else {
xfer.send( chunk );
xfer.end( chunk ).then(after_end);
}
} );
Note that `xfer.end()`s return is a Promise. The resolution of this
Promise is the point at which either to send another offer or to do:
zsession.close().then( () => { ... } );
The `close()` Promises resolution is the point at which the session
has ended successfully.
That should be all you need. If you want to go deeper, though, each module
in this distribution has JSDoc and unit tests.
# RATIONALE
ZMODEM facilitates terminal-based file transfers.
This was an important capability in the 1980s and early 1990s because
most modem use was for terminal applications, especially
[BBS](https://en.wikipedia.org/wiki/Bulletin_board_system)es.
(This was how, for example,
popular shareware games like [Wolfenstein 3D](http://3d.wolfenstein.com)
were often distributed.) The World Wide Web in the
mid-1990s, however, proved a more convenient way to accomplish most of
what BBSes were useful for, as a result of which the problem that ZMODEM
solved became a much less important one.
ZMODEM stuck around, though, as it remained a convenient solution
for terminal users who didnt want open a separate session to transfer a
file. [Uwe Ohse](https://uwe.ohse.de/)s
[lrzsz](https://ohse.de/uwe/software/lrzsz.html) package
provided a portable C implementation of the protocol (reworked from
the last public domain release of the original code) that is installed on
many systems today.
Where `lrzsz` cant reach, though, is terminals that dont have command-line
access—such as terminals that run in JavaScript. Now that
[WebSocket](https://en.wikipedia.org/wiki/WebSocket) makes real-time
applications like terminals possible in a web browser,
there is a use case for a JavaScript
implementation of ZMODEM to allow file transfers in this context.
# GENERAL FLOW OF A ZMODEM SESSION:
The following is an overview of an error-free ZMODEM session.
0. If you call the `sz` command (or equivalent), that command will send
a special ZRQINIT “pre-header” to signal your terminal to be a ZMODEM
receiver.
1. The receiver, upon recognizing the ZRQINIT header, responds with
a ZRINIT header.
2. The sender sends a ZFILE header along with information about the file.
(This may also include the size and file count for the entire batch of files.)
3. The recipient either accepts the file or skips it.
4. If the recipient did not skip the file, then the sender sends the file
contents. At the end the sender sends a ZEOF header to let the recipient
know this file is done.
5. The recipient sends another ZRINIT header. This lets the sender know that
the recipient confirms receipt of the entire file.
6. Repeat steps 2-5 until the sender has no more files to send.
7. Once the sender has no more files to send, the sender sends a ZEOF header,
which the recipient echoes back. The sender closes the session by sending
`OO` (“over and out”).
# PROTOCOL NOTES AND ASSUMPTIONS
Here are some notes about this particular implementation.
Particular notes:
* We send with a maximum data subpacket size of 8 KiB (8,192 bytes). While
the ZMODEM specification stipulates a maximum of 1 KiB, `lrzsz` accepts
the larger size, and it seems to have become a de facto standard extension
to the protocol.
* Remote command execution (i.e., ZCOMMAND) is unimplemented. It probably
wouldnt work in browsers, which is zmodem.jss principal use case.
* No file translations are done. (Unix/Windows line endings are a
future feature possibility.)
* It is assumed that no error correction will be needed. All connections
are assumed to be **“reliable”**; i.e.,
data is received exactly as sent. We take this for granted today,
but ZMODEMs original application was over raw modem connections that
often didnt have reliable hardware error correction. TCP also wasnt
in play to do software error correction as generally happens
today over remote connections. Because the forseeable use of zmodem.js
is either over TCP or a local socket—both of which are reliable—it seems
safe to assume that zmodem.js will not need to implement error correction.
* zmodem.js sends with CRC-16 by default. Ideally we would just use CRC-16
for everything, but lsz 0.12.20 has a [buffer overflow bug](https://github.com/gooselinux/lrzsz/blob/master/lrzsz-0.12.20.patch) that rears its
head when you try to abort a ZMODEM session in the middle of a CRC-16 file
transfer. To avoid this bug, zmodem.js advertises CRC-32 support when it
receives a file, which makes lsz avoid the buffer overflow bug by using
CRC-32.
The bug is reported, incidentally, and a fix is expected (nearly 20 years
after the last official lrzsz release!).
* There is no XMODEM/YMODEM fallback.
* Occasionally lrzsz will output things to the console that arent
actual ZMODEM—for example, if you skip an offered file, `sz` will write a
message about it to the console. For the most part we can accommodate these
because they happen between ZMODEM headers; however, its possible to
“poison” such messages, e.g., by sending a file whose name includes a
ZMODEM header. So dont do that. :-P
# IMPLEMENTATION NOTES
* I initially had success integrating zmodem.js with
[xterm.js](https://xtermjs.org); however, that librarys plugin interface
changed dramatically, and I havent created a new plugin to replace the
old one. (It should be relatively straightforward if someone else wants to
pick it up.)
* Browsers dont have an easy way to download only part of a file;
as a result, anything the browser saves to disk must be the entire file.
* ZMODEM is a _binary_ protocol. (There was an extension planned
to escape everything down to 7-bit ASCII, but it doesnt seem to have
been implemented?) Hence, **if you use WebSocket, youll need to use
binary messages, not text**.
* lrzsz is the only widely-distributed ZMODEM implementation nowadays,
which makes it a de facto standard in its
own right. Thus far all end-to-end testing has been against it. It is
thus possible that resolutions to disparities between `lrzsz` and the
protocol specification may need to favor the implementation.
* It is a generally-unavoidable byproduct of how ZMODEM works that
the first header in a ZMODEM session will echo to the terminal. This
explains the unsightly `**B0000…` stuff that youll see when you run
either `rz` or `sz`.
That header
will include some form of line break. (From `lrzsz` means bytes 0x0d
and 0x8a—**not** 0x0a). Your terminal might react oddly to that;
if it does, try stripping out one or the other line ending character.
# PROTOCOL CHOICE
Both XMODEM and YMODEM (including the latters many variants) require the
receiver to initiate the session by sending a “magic character” (ASCII SOH);
the problem is that theres nothing in the protocol to prompt the receiver
to do so. ZMODEM is sender-driven, so the terminal can show a notice that
says, “Do you want to receive a file?”
This is a shame because these other two protocols are a good deal simpler
than ZMODEM. The YMODEM-g variant in particular would be better-suited to
our purpose because it doesnt “litter” the transfer with CRCs.
There is also [Kermit](http://www.columbia.edu/kermit/kermit.html), which
seems to be more standardized than ZMODEM but **much** more complex.
# DESIGN NOTES
zmodem.js tries to avoid “useless” states:
either we fail completely, or we succeed. To that end, some callbacks are
required arguments (e.g., the Sentry constructors `to_terminal` argument),
while others are registered separately.
Likewise, for this reason some of the session-level logic is exposed only
through the Transfer and Offer objects. The Session creates these
internally then exposes them via callback
# SOURCES
ZMODEM is not standardized in a nice, clean, official RFC like DNS or HTTP;
rather, it was one guys solution to a particular problem. There is
documentation, but its not as helpful as it might be; for example,
theres only one example workflow given, and its a “happy-path”
transmission of a single file.
As part of writing zmodem.js Ive culled together various resources
about the protocol. As far as I know these are the best sources for
information on ZMODEM.
Two documents that describe ZMODEM are saved in the repository for reference.
The first is the closest there is to an official ZMODEM specification:
a description of the protocol from its author, Chuck Forsberg. The second
seems to be based on the first and comes from
[Jacques Mattheij](https://jacquesmattheij.com).
**HISTORICAL:** The included `rzsz.zip` file (fetched from [ftp://archives.thebbs.org/file_transfer_protocols/](ftp://archives.thebbs.org/file_transfer_protocols/) on 16 October 2017)
is the last public domain release
from Forsberg. [http://freeware.nekochan.net/source/rzsz/](http://freeware.nekochan.net/source/rzsz/) has what is supposedly Forsbergs last shareware release;
I have not looked at it except for the README. Im not sure of the
copyright status of this software: Forsberg is deceased, and his company
appears to be defunct. Regardless, neither it nor its public domain
predecessor is likely in widespread use.
Here are some other available ZMODEM implementations:
* [lrzsz](https://ohse.de/uwe/software/lrzsz.html)
A widely-deployed adaptation of Forsbergs last public domain ZMODEM
code. This is the de facto “reference” implementation, both by virtue
of its wide availability and its derivation from Forsbergs original.
If your server has the `rz` and `sz` commands, theyre probably
from this package.
* [SyncTERM](http://syncterm.bbsdev.net)
Based on Jacques Mattheijs ZMODEM implementation, originally called
zmtx/zmrx. This is a much more readable implementation than lrzsz
but lamentably one that doesnt seem to compile as readily.
* [Qodem](https://github.com/klamonte/qodem)
This terminal emulator package appears to contain its own ZMODEM
implementation.
* [PD Zmodem](http://pcmicro.com/netfoss/pdzmodem.html)
I know nothing of this one.
* [zmodem (Rust)](https://github.com/lexxvir/zmodem)
A pure [Rust](http://rust-lang.org) implementation of ZMODEM.
# REQUIREMENTS
This library only supports modern browsers. There is no support for
Internet Explorer or other older browsers planned.
The tests have run successfully against node.js version 8.
# DOCUMENTATION
Besides this document, each module has inline [jsdoc](http://usejsdoc.org).
You can see it by running `yarn` in the repositorys root directory;
the documentation will build in a newly-created `documentation` directory.
# CONTRIBUTING
Contributions are welcome via
[https://github.com/FGasper/zmodemjs](https://github.com/FGasper/zmodemjs).
# TROUBLESHOOTING
Before you do anything else, set `Zmodem.DEBUG` to true. This will log
useful information about the ZMODEM session to your JavaScript console. That
may give you all you need to fix your problem.
If you have trouble transferring files, try these diagnostics:
1. Transfer an empty file. (Run `touch empty.bin` to create one named `empty.bin`.)
2. Transfer a small file. (`echo hello > small.txt`)
3. Transfer a file that contains all possible octets. (`perl -e 'print chr for 0 .. 255' > all_bytes.bin`)
4. If a specific file fails, does it still fail if you `truncate` a copy of
the file down to, say, half size and transfer that truncated file? Does it
work if you truncate the file down to 1 byte? If so, then use this method
to determine which specific place in the file triggers the transfer error.
**IF YOU HAVE DONE THE ABOVE** and still think the problem is with zmodem.js,
you can file a bug report. Note that, historically, most bug reports have
reflected implementation errors rather than bugs in zmodem.js.
# TODO
* Teach send sessions to “fast-forward” so as to honor requests for
append-style sessions.
* Implement newline conversions.
* Teach Session how to do and to handle pre-CRC checks.
* Possible: command-line `rz`, if theres demand for it, e.g., in
environments where `lrzsz` cant run. (NB: The distribution includes
a bare-bones, proof-of-concept `sz` replacement.)
# KNOWN ISSUERS
* In testing, Microsoft Edge appeared not to care what string was given
to `<a>`s `download` attribute; the saved filename was based on the
browsers internal Blob object URL instead.
# COPYRIGHT
Copyright 2017 Gasper Software Consulting
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Parts of the CRC-16 logic are adapted from crc-js by Johannes Rudolph.

140
bin/zmodemjs-sz.js Executable file
View file

@ -0,0 +1,140 @@
"use strict";
// A proof-of-concept CLI implementation of “sz” using zmodem.js.
// This is not tested extensively and isnt really meant for production use.
const process = require('process');
const fs = require('fs');
const Zmodem = require('../src/zmodem');
var paths = process.argv.slice(1);
// Accommodate “node $script …”
if (paths[0] === __filename) {
paths = paths.slice(1);
}
if (!paths.length) {
console.error("Need at least one path!");
process.exit(1);
}
// Cant be to the same terminal as STDOUT.
// npms “ttyname” can tell us, but its annoying to require
// a module for this.
const DEBUG = false;
if (DEBUG) {
var outtype = fs.fstatSync(1).mode & fs.constants.S_IFMT;
var errtype = fs.fstatSync(1).mode & fs.constants.S_IFMT;
if (outtype === errtype && outtype === fs.constants.S_IFCHR) {
console.error("STDOUT and STDERR cant both be to a terminal when debugging is on.");
process.exit(1);
}
}
function _debug() {
DEBUG && console.warn.apply( console, arguments );
}
_debug("PID:", process.pid);
_debug("Paths to send:", paths);
//----------------------------------------------------------------------
var path_fd = {};
paths.forEach( (path) => path_fd[path] = fs.openSync(path, 'r') );
// TODO: This should maybe be in its own module?
// The notion of starting a session in JS wasnt envisioned when
// this module was written.
const initial_bytes = Zmodem.Header.build("ZRQINIT").to_hex();
process.stdout.write(Buffer.from(initial_bytes));
_debug('Sent ZRQINIT');
// We need a binary stdin.
var stdin = fs.createReadStream( "", { fd: 0 } );
function send_files(zsession, paths) {
function send_next() {
var path = paths.shift();
if (path) {
_debug("Sending offer: ", path);
var fd = path_fd[path];
var fstat = fs.fstatSync(fd);
var filename = path.match(/.+\/(.+)/);
filename = filename ? filename[0] : path;
return zsession.send_offer( {
name: filename,
size: fstat.size,
mtime: Math.round( fstat.mtimeMs / 1000 ),
} ).then( (xfer) => {
if (!xfer) {
_debug("Offer was rejected.");
return send_next();
}
_debug("Offer was accepted.");
var stream = fs.createReadStream( "", {
fd: fd,
} );
stream.on('data', (chunk) => {
_debug("Sending chunk.");
xfer.send(chunk);
} );
return new Promise( (res, rej) => {
stream.on('end', () => {
_debug("Reached EOF; sending end.");
xfer.end().then( () => {;
res( send_next() );
} );
} );
} );
} );
}
else {
_debug("Reached end of files batch.");
}
}
return send_next();
}
var zsession;
stdin.on('data', (chunk) => {
var octets = Array.from(chunk)
if (zsession) {
zsession.consume(octets);
}
else {
_debug("Received on STDIN; checking for session.", octets);
zsession = Zmodem.Session.parse(octets);
if (zsession) {
_debug("Got session.");
// It seems like .parse() should strip out the header bytes,
// but thats not how it works.
// zsession.consume(octets);
zsession.set_sender( (octets) => process.stdout.write( Buffer.from(octets) ) );
send_files(zsession, paths).then( () => zsession.close() );
}
else {
_debug("No session yet …");
}
}
});

6
index.js Normal file
View file

@ -0,0 +1,6 @@
"use strict";
Object.assign(
module.exports,
require("./src/zsentry")
);

10
jsdoc.json Normal file
View file

@ -0,0 +1,10 @@
{
"plugins": ["plugins/markdown"],
"source": {
"include": ["README.md", "src"],
"includePattern": "\\.js$"
},
"opts": {
"destination": "documentation"
}
}

49
package.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "zmodem.js",
"version": "0.1.10",
"description": "ZMODEM file transfers in JavaScript",
"devDependencies": {
"babel-minify-webpack-plugin": "^0.2.0",
"blue-tape": "^1.0.0",
"jsdoc-webpack-plugin": "^0.0.2",
"tape": "^5.0.1",
"text-encoding": "^0.6.4",
"tmp": "0.0.33",
"webpack": "^3.6.0",
"which": "^1.3.0"
},
"files": [
"dist/",
"index.js",
"src/"
],
"directories": {
"test": "tests"
},
"scripts": {
"test": "tape ./tests/*.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/FGasper/zmodemjs.git"
},
"keywords": [
"zmodem",
"shell",
"terminal",
"file",
"transfer",
"websocket",
"xmodem",
"ymodem"
],
"author": "Gasper Software Consulting (http://gaspersoftware.com)",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/FGasper/zmodemjs/issues"
},
"homepage": "https://github.com/FGasper/zmodemjs#readme",
"dependencies": {
"crc-32": "^1.1.1"
}
}

124
src/encode.js Normal file
View file

@ -0,0 +1,124 @@
"use strict";
var Zmodem = module.exports;
const HEX_DIGITS = [ 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102 ];
const HEX_OCTET_VALUE = {};
for (var hd=0; hd<HEX_DIGITS.length; hd++) {
HEX_OCTET_VALUE[ HEX_DIGITS[hd] ] = hd;
}
/**
* General, non-ZMODEM-specific encoding logic.
*
* @exports ENCODELIB
*/
Zmodem.ENCODELIB = {
/**
* Return an array with the given number as 2 big-endian bytes.
*
* @param {number} number - The number to encode.
*
* @returns {number[]} The octet values.
*/
pack_u16_be: function pack_u16_be(number) {
if (number > 0xffff) throw( "Number cannot exceed 16 bits: " + number )
return [ number >> 8, number & 0xff ];
},
/**
* Return an array with the given number as 4 little-endian bytes.
*
* @param {number} number - The number to encode.
*
* @returns {number[]} The octet values.
*/
pack_u32_le: function pack_u32_le(number) {
//Cant bit-shift because that runs into JSs bit-shift problem.
//(See _updcrc32() for an example.)
var high_bytes = number / 65536; //fraction is ok
//a little-endian 4-byte sequence
return [
number & 0xff,
(number & 65535) >> 8,
high_bytes & 0xff,
high_bytes >> 8,
];
},
/**
* The inverse of pack_u16_be() - i.e., take in 2 octet values
* and parse them as an unsigned, 2-byte big-endian number.
*
* @param {number[]} octets - The octet values (2 of them).
*
* @returns {number} The decoded number.
*/
unpack_u16_be: function unpack_u16_be(bytes_arr) {
return (bytes_arr[0] << 8) + bytes_arr[1];
},
/**
* The inverse of pack_u32_le() - i.e., take in a 4-byte sequence
* and parse it as an unsigned, 4-byte little-endian number.
*
* @param {number[]} octets - The octet values (4 of them).
*
* @returns {number} The decoded number.
*/
unpack_u32_le: function unpack_u32_le(octets) {
//<sigh> … (254 << 24 is -33554432, according to JavaScript)
return octets[0] + (octets[1] << 8) + (octets[2] << 16) + (octets[3] * 16777216);
},
/**
* Encode a series of octet values to be the octet values that
* correspond to the ASCII hex characters for each octet. The
* returned array is suitable for use as binary data.
*
* For example:
*
* Original Hex Returned
* 254 fe 102, 101
* 12 0c 48, 99
* 129 81 56, 49
*
* @param {number[]} octets - The original octet values.
*
* @returns {number[]} The octet values that correspond to an ASCII
* representation of the given octets.
*/
octets_to_hex: function octets_to_hex(octets) {
var hex = [];
for (var o=0; o<octets.length; o++) {
hex.push(
HEX_DIGITS[ octets[o] >> 4 ],
HEX_DIGITS[ octets[o] & 0x0f ]
);
}
return hex;
},
/**
* The inverse of octets_to_hex(): takes an array
* of hex octet pairs and returns their octet values.
*
* @param {number[]} hex_octets - The hex octet values.
*
* @returns {number[]} The parsed octet values.
*/
parse_hex_octets: function parse_hex_octets(hex_octets) {
var octets = new Array(hex_octets.length / 2);
for (var i=0; i<octets.length; i++) {
octets[i] = (HEX_OCTET_VALUE[ hex_octets[2 * i] ] << 4) + HEX_OCTET_VALUE[ hex_octets[1 + 2 * i] ];
}
return octets;
},
};

33
src/text.js Normal file
View file

@ -0,0 +1,33 @@
class _my_TextEncoder {
encode(text) {
text = unescape(encodeURIComponent(text));
var bytes = new Array( text.length );
for (var b = 0; b < text.length; b++) {
bytes[b] = text.charCodeAt(b);
}
return new Uint8Array(bytes);
}
}
class _my_TextDecoder {
decode(bytes) {
return decodeURIComponent( escape( String.fromCharCode.apply(String, bytes) ) );
}
}
var Zmodem = module.exports;
/**
* A limited-use compatibility shim for TextEncoder and TextDecoder.
* Useful because both Edge and node.js still lack support for these
* as of October 2017.
*
* @exports Text
*/
Zmodem.Text = {
Encoder: (typeof TextEncoder !== "undefined") ? TextEncoder : _my_TextEncoder,
Decoder: (typeof TextDecoder !== "undefined") ? TextDecoder : _my_TextDecoder,
};

143
src/zcrc.js Normal file
View file

@ -0,0 +1,143 @@
"use strict";
const CRC32_MOD = require('crc-32');
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zerror"),
require("./encode")
);
//----------------------------------------------------------------------
// BEGIN adapted from crc-js by Johannes Rudolph
var _crctab;
const
crc_width = 16,
crc_polynomial = 0x1021,
crc_castmask = 0xffff,
crc_msbmask = 1 << (crc_width - 1)
;
function _compute_crctab() {
_crctab = new Array(256);
var divident_shift = crc_width - 8;
for (var divident = 0; divident < 256; divident++) {
var currByte = (divident << divident_shift) & crc_castmask;
for (var bit = 0; bit < 8; bit++) {
if ((currByte & crc_msbmask) !== 0) {
currByte <<= 1;
currByte ^= crc_polynomial;
}
else {
currByte <<= 1;
}
}
_crctab[divident] = (currByte & crc_castmask);
}
}
// END adapted from crc-js by Johannes Rudolph
//----------------------------------------------------------------------
function _updcrc(cp, crc) {
if (!_crctab) _compute_crctab();
return(
_crctab[((crc >> 8) & 255)]
^ ((255 & crc) << 8)
^ cp
);
}
function __verify(expect, got) {
var err;
if ( expect.join() !== got.join() ) {
throw new Zmodem.Error("crc", got, expect);
}
}
//TODO: use external implementation(s)
Zmodem.CRC = {
//https://www.lammertbies.nl/comm/info/crc-calculation.html
//CRC-CCITT (XModem)
/**
* Deduce a given set of octet values CRC16, as per the CRC16
* variant that ZMODEM uses (CRC-CCITT/XModem).
*
* @param {Array} octets - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @returns {Array} crc - The CRC, expressed as an array of octet values.
*/
crc16: function crc16(octet_nums) {
var crc = octet_nums[0];
for (var b=1; b<octet_nums.length; b++) {
crc = _updcrc( octet_nums[b], crc );
}
crc = _updcrc( 0, _updcrc(0, crc) );
//a big-endian 2-byte sequence
return Zmodem.ENCODELIB.pack_u16_be(crc);
},
/**
* Deduce a given set of octet values CRC32.
*
* @param {Array} octets - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @returns {Array} crc - The CRC, expressed as an array of octet values.
*/
crc32: function crc32(octet_nums) {
return Zmodem.ENCODELIB.pack_u32_le(
CRC32_MOD.buf(octet_nums) >>> 0 //bit-shift to get unsigned
);
},
/**
* Verify a given set of octet values CRC16.
* An exception is thrown on failure.
*
* @param {Array} bytes_arr - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {Array} crc - The CRC to check against, expressed as
* an array of octet values.
*/
verify16: function verify16(bytes_arr, got) {
return __verify( this.crc16(bytes_arr), got );
},
/**
* Verify a given set of octet values CRC32.
* An exception is thrown on failure.
*
* @param {Array} bytes_arr - The array of octet values.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {Array} crc - The CRC to check against, expressed as
* an array of octet values.
*/
verify32: function verify32(bytes_arr, crc) {
try {
__verify( this.crc32(bytes_arr), crc );
}
catch(err) {
err.input = bytes_arr.slice(0);
throw err;
}
},
};

240
src/zdle.js Normal file
View file

@ -0,0 +1,240 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zmlib")
);
//encode() variables - declare them here so we dont
//create them in the function.
var encode_cur, encode_todo;
const ZDLE = Zmodem.ZMLIB.ZDLE;
/**
* Class that handles ZDLE encoding and decoding.
* Encoding is subject to a given configuration--specifically, whether
* we want to escape all control characters. Decoding is static; however
* a given string is encoded we can always decode it.
*/
Zmodem.ZDLE = class ZmodemZDLE {
/**
* Create a ZDLE encoder.
*
* @param {object} [config] - The initial configuration.
* @param {object} config.escape_ctrl_chars - Whether the ZDLE encoder
* should escape control characters.
*/
constructor(config) {
this._config = {};
if (config) {
this.set_escape_ctrl_chars(!!config.escape_ctrl_chars);
}
}
/**
* Enable or disable control-character escaping.
* You should probably enable this for sender sessions.
*
* @param {boolean} value - Whether to enable (true) or disable (false).
*/
set_escape_ctrl_chars(value) {
if (typeof value !== "boolean") throw "need boolean!";
if (value !== this._config.escape_ctrl_chars) {
this._config.escape_ctrl_chars = value;
this._setup_zdle_table();
}
}
/**
* Whether or not control-character escaping is enabled.
*
* @return {boolean} Whether the escaping is on (true) or off (false).
*/
escapes_ctrl_chars() {
return !!this._config.escape_ctrl_chars;
}
//I dont know of any Zmodem implementations that use ZESC8
//(“escape_8th_bit”)??
/*
ZMODEM software escapes ZDLE, 020, 0220, 021, 0221, 023, and 0223. If
preceded by 0100 or 0300 (@), 015 and 0215 are also escaped to protect the
Telenet command escape CR-@-CR.
*/
/**
* Encode an array of octet values and return it.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array, transformed. This is the
* same object that is passed in.
*/
encode(octets) {
//NB: Performance matters here!
if (!this._zdle_table) throw "No ZDLE encode table configured!";
var zdle_table = this._zdle_table;
var last_code = this._lastcode;
var arrbuf = new ArrayBuffer( 2 * octets.length );
var arrbuf_uint8 = new Uint8Array(arrbuf);
var escctl_yn = this._config.escape_ctrl_chars;
var arrbuf_i = 0;
for (encode_cur=0; encode_cur<octets.length; encode_cur++) {
encode_todo = zdle_table[octets[encode_cur]];
if (!encode_todo) {
console.trace();
console.error("bad encode() call:", JSON.stringify(octets));
this._lastcode = last_code;
throw( "Invalid octet: " + octets[encode_cur] );
}
last_code = octets[encode_cur];
if (encode_todo === 1) {
//Do nothing; we append last_code below.
}
//0x40 = '@'; i.e., only escape if the last
//octet was '@'.
else if (escctl_yn || (encode_todo === 2) || ((last_code & 0x7f) === 0x40)) {
arrbuf_uint8[arrbuf_i] = ZDLE;
arrbuf_i++;
last_code ^= 0x40; //0100
}
arrbuf_uint8[arrbuf_i] = last_code;
arrbuf_i++;
}
this._lastcode = last_code;
octets.splice(0);
octets.push.apply(octets, new Uint8Array( arrbuf, 0, arrbuf_i ));
return octets;
}
/**
* Decode an array of octet values and return it.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array.
* This is the same object that is passed in.
*/
static decode(octets) {
for (var o=octets.length-1; o>=0; o--) {
if (octets[o] === ZDLE) {
octets.splice( o, 2, octets[o+1] - 64 );
}
}
return octets;
}
/**
* Remove, ZDLE-decode, and return bytes from the passed-in array.
* If the requested number of ZDLE-encoded bytes isnt available,
* then the passed-in array is unmodified (and the return is undefined).
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @param {number} offset - The number of (undecoded) bytes to skip
* at the beginning of the octets array.
*
* @param {number} count - The number of bytes (octet values) to return.
*
* @returns {number[]|undefined} An array with the requested number of
* decoded octet values, or undefined if that number of decoded
* octets isnt available (given the passed-in offset).
*/
static splice(octets, offset, count) {
var so_far = 0;
if (!offset) offset = 0;
for (var i = offset; i<octets.length && so_far<count; i++) {
so_far++;
if (octets[i] === ZDLE) i++;
}
if (so_far === count) {
//Dont accept trailing ZDLE. This check works
//because of the i++ logic above.
if (octets.length === (i - 1)) return;
octets.splice(0, offset);
return ZmodemZDLE.decode( octets.splice(0, i - offset) );
}
return;
}
_setup_zdle_table() {
var zsendline_tab = new Array(256);
for (var i=0; i<zsendline_tab.length; i++) {
//1 = never escape
//2 = always escape
//3 = escape only if the previous byte was '@'
//Never escape characters from 0x20 (32) to 0x7f (127).
//This is the range of printable characters, plus DEL.
//I guess ZMODEM doesnt consider DEL to be a control character?
if ( i & 0x60 ) {
zsendline_tab[i] = 1;
}
else {
switch(i) {
case ZDLE: //NB: no (ZDLE | 0x80)
case Zmodem.ZMLIB.XOFF:
case Zmodem.ZMLIB.XON:
case (Zmodem.ZMLIB.XOFF | 0x80):
case (Zmodem.ZMLIB.XON | 0x80):
zsendline_tab[i] = 2;
break;
case 0x10: // 020
case 0x90: // 0220
zsendline_tab[i] = this._config.turbo_escape ? 1 : 2;
break;
case 0x0d: // 015
case 0x8d: // 0215
zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : !this._config.turbo_escape ? 3 : 1;
break;
default:
zsendline_tab[i] = this._config.escape_ctrl_chars ? 2 : 1;
}
}
}
this._zdle_table = zsendline_tab;
}
}

47
src/zerror.js Normal file
View file

@ -0,0 +1,47 @@
"use strict";
var Zmodem = module.exports;
function _crc_message(got, expected) {
this.got = got.slice(0);
this.expected = expected.slice(0);
return "CRC check failed! (got: " + got.join() + "; expected: " + expected.join() + ")";
}
function _pass(val) { return val }
const TYPE_MESSAGE = {
aborted: "Session aborted",
peer_aborted: "Peer aborted session",
already_aborted: "Session already aborted",
crc: _crc_message,
validation: _pass,
};
function _generate_message(type) {
const msg = TYPE_MESSAGE[type];
switch (typeof msg) {
case "string":
return msg;
case "function":
var args_after_type = [].slice.call(arguments).slice(1);
return msg.apply(this, args_after_type);
}
return null;
}
Zmodem.Error = class ZmodemError extends Error {
constructor(msg_or_type) {
super();
var generated = _generate_message.apply(this, arguments);
if (generated) {
this.type = msg_or_type;
this.message = generated;
}
else {
this.message = msg_or_type;
}
}
};

763
src/zheader.js Normal file
View file

@ -0,0 +1,763 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./encode"),
require("./zdle"),
require("./zmlib"),
require("./zcrc"),
require("./zerror")
);
const ZPAD = '*'.charCodeAt(0),
ZBIN = 'A'.charCodeAt(0),
ZHEX = 'B'.charCodeAt(0),
ZBIN32 = 'C'.charCodeAt(0)
;
//NB: lrzsz uses \x8a rather than \x0a where the specs
//say to use LF. For simplicity, we avoid that and just use
//the 7-bit LF character.
const HEX_HEADER_CRLF = [ 0x0d, 0x0a ];
const HEX_HEADER_CRLF_XON = HEX_HEADER_CRLF.slice(0).concat( [Zmodem.ZMLIB.XON] );
//These are more or less duplicated by the logic in trim_leading_garbage().
//
//"**" + ZDLE_CHAR + "B"
const HEX_HEADER_PREFIX = [ ZPAD, ZPAD, Zmodem.ZMLIB.ZDLE, ZHEX ];
const BINARY16_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN ];
const BINARY32_HEADER_PREFIX = [ ZPAD, Zmodem.ZMLIB.ZDLE, ZBIN32 ];
/** Class that represents a ZMODEM header. */
Zmodem.Header = class ZmodemHeader {
//lrzszs “sz” command sends a random (?) CR/0x0d byte
//after ZEOF. Lets accommodate 0x0a, 0x0d, 0x8a, and 0x8d.
//
//Also, when you skip a file, sz outputs a message about it.
//
//It appears that were supposed to ignore anything until
//[ ZPAD, ZDLE ] when were looking for a header.
/**
* Weed out the leading bytes that arent valid to start a ZMODEM header.
*
* @param {number[]} ibuffer - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The octet values that were removed from the start
* of ibuffer. Order is preserved.
*/
static trim_leading_garbage(ibuffer) {
//Since theres no escaping of the output its possible
//that the garbage could trip us up, e.g., by having a filename
//be a legit ZMODEM header. But thats pretty unlikely.
//Everything up to the first ZPAD: garbage
//If first ZPAD has asterisk + ZDLE
var garbage = [];
var discard_all, parser, next_ZPAD_at_least = 0;
TRIM_LOOP:
while (ibuffer.length && !parser) {
var first_ZPAD = ibuffer.indexOf(ZPAD);
//No ZPAD? Then we purge the input buffer cuz its all garbage.
if (first_ZPAD === -1) {
discard_all = true;
break TRIM_LOOP;
}
else {
garbage.push.apply( garbage, ibuffer.splice(0, first_ZPAD) );
//buffer has only an asterisk … gotta see about more
if (ibuffer.length < 2) {
break TRIM_LOOP;
}
else if (ibuffer[1] === ZPAD) {
//Two leading ZPADs should be a hex header.
//Were assuming the length of the header is 4 in
//this logic … but ZMODEM isnt likely to change, so.
if (ibuffer.length < HEX_HEADER_PREFIX.length) {
if (ibuffer.join() === HEX_HEADER_PREFIX.slice(0, ibuffer.length).join()) {
//We have an incomplete fragment that matches
//HEX_HEADER_PREFIX. So dont trim any more.
break TRIM_LOOP;
}
//Otherwise, well discard one.
}
else if ((ibuffer[2] === HEX_HEADER_PREFIX[2]) && (ibuffer[3] === HEX_HEADER_PREFIX[3])) {
parser = _parse_hex;
}
}
else if (ibuffer[1] === Zmodem.ZMLIB.ZDLE) {
//ZPAD + ZDLE should be a binary header.
if (ibuffer.length < BINARY16_HEADER_PREFIX.length) {
break TRIM_LOOP;
}
if (ibuffer[2] === BINARY16_HEADER_PREFIX[2]) {
parser = _parse_binary16;
}
else if (ibuffer[2] === BINARY32_HEADER_PREFIX[2]) {
parser = _parse_binary32;
}
}
if (!parser) {
garbage.push( ibuffer.shift() );
}
}
}
if (discard_all) {
garbage.push.apply( garbage, ibuffer.splice(0) );
}
//For now well throw away the parser.
//Its not hard for parse() to discern anyway.
return garbage;
}
/**
* Parse out a Header object from a given array of octet values.
*
* An exception is thrown if the given bytes are definitively invalid
* as header values.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Header|undefined} An instance of the appropriate Header
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid header here or not.
*/
static parse(octets) {
var hdr;
if (octets[1] === ZPAD) {
hdr = _parse_hex(octets);
return hdr && [ hdr, 16 ];
}
else if (octets[2] === ZBIN) {
hdr = _parse_binary16(octets, 3);
return hdr && [ hdr, 16 ];
}
else if (octets[2] === ZBIN32) {
hdr = _parse_binary32(octets);
return hdr && [ hdr, 32 ];
}
if (octets.length < 3) return;
throw( "Unrecognized/unsupported octets: " + octets.join() );
}
/**
* Build a Header subclass given a name and arguments.
*
* @param {string} name - The header type name, e.g., ZRINIT.
*
* @param {...*} args - The arguments to pass to the appropriate
* subclass constructor. These arent documented currently
* but are pretty easy to glean from the code.
*
* @returns {Header} An instance of the appropriate Header subclass.
*/
static build(name /*, args */) {
var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
//TODO: make this better
var Ctr = FRAME_NAME_CREATOR[name];
if (!Ctr) throw("No frame class “" + name + "” is defined!");
args.shift();
//Plegh!
//https://stackoverflow.com/questions/33193310/constr-applythis-args-in-es6-classes
var hdr = new (Ctr.bind.apply(Ctr, [null].concat(args)));
return hdr;
}
/**
* Return the octet values array that represents the object
* in ZMODEM hex encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_hex() {
var to_crc = this._crc_bytes();
return HEX_HEADER_PREFIX.concat(
Zmodem.ENCODELIB.octets_to_hex( to_crc.concat( Zmodem.CRC.crc16(to_crc) ) ),
this._hex_header_ending
);
}
/**
* Return the octet values array that represents the object
* in ZMODEM binary encoding with a 16-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for
* ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_binary16(zencoder) {
return this._to_binary(zencoder, BINARY16_HEADER_PREFIX, Zmodem.CRC.crc16);
}
/**
* Return the octet values array that represents the object
* in ZMODEM binary encoding with a 32-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for
* ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
to_binary32(zencoder) {
return this._to_binary(zencoder, BINARY32_HEADER_PREFIX, Zmodem.CRC.crc32);
}
//This is never called directly, but only as super().
constructor() {
if (!this._bytes4) {
this._bytes4 = [0, 0, 0, 0];
}
}
_to_binary(zencoder, prefix, crc_func) {
var to_crc = this._crc_bytes();
//Both the 4-byte payload and the CRC bytes are ZDLE-encoded.
var octets = prefix.concat(
zencoder.encode( to_crc.concat( crc_func(to_crc) ) )
);
return octets;
}
_crc_bytes() {
return [ this.TYPENUM ].concat(this._bytes4);
}
}
Zmodem.Header.prototype._hex_header_ending = HEX_HEADER_CRLF_XON;
class ZRQINIT_HEADER extends Zmodem.Header {};
//----------------------------------------------------------------------
const ZRINIT_FLAG = {
//----------------------------------------------------------------------
// Bit Masks for ZRINIT flags byte ZF0
//----------------------------------------------------------------------
CANFDX: 0x01, // Rx can send and receive true FDX
CANOVIO: 0x02, // Rx can receive data during disk I/O
CANBRK: 0x04, // Rx can send a break signal
CANCRY: 0x08, // Receiver can decrypt -- nothing does this
CANLZW: 0x10, // Receiver can uncompress -- nothing does this
CANFC32: 0x20, // Receiver can use 32 bit Frame Check
ESCCTL: 0x40, // Receiver expects ctl chars to be escaped
ESC8: 0x80, // Receiver expects 8th bit to be escaped
};
function _get_ZRINIT_flag_num(fl) {
if (!ZRINIT_FLAG[fl]) {
throw new Zmodem.Error("Invalid ZRINIT flag: " + fl);
}
return ZRINIT_FLAG[fl];
}
class ZRINIT_HEADER extends Zmodem.Header {
constructor(flags_arr, bufsize) {
super();
var flags_num = 0;
if (!bufsize) bufsize = 0;
flags_arr.forEach( function(fl) {
flags_num |= _get_ZRINIT_flag_num(fl);
} );
this._bytes4 = [
bufsize & 0xff,
bufsize >> 8,
0,
flags_num,
];
}
//undefined if nonstop I/O is allowed
get_buffer_size() {
return Zmodem.ENCODELIB.unpack_u16_be( this._bytes4.slice(0, 2) ) || undefined;
}
//Unimplemented:
// can_decrypt
// can_decompress
//----------------------------------------------------------------------
//function names taken from Jacques Mattheijs implementation,
//as used in syncterm.
can_full_duplex() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFDX );
}
can_overlap_io() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANOVIO );
}
can_break() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANBRK );
}
can_fcs_32() {
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFC32 );
}
escape_ctrl_chars() {
return !!( this._bytes4[3] & ZRINIT_FLAG.ESCCTL );
}
//Is this used? I dont see it used in lrzsz or syncterm
//Looks like it was a “foreseen” feature that Forsberg
//never implemented. (The need for it went away, maybe?)
escape_8th_bit() {
return !!( this._bytes4[3] & ZRINIT_FLAG.ESC8 );
}
};
//----------------------------------------------------------------------
//Since context makes clear whats going on, we use these
//rather than the T-prefixed constants in the specification.
const ZSINIT_FLAG = {
ESCCTL: 0x40, // Transmitter will escape ctl chars
ESC8: 0x80, // Transmitter will escape 8th bit
};
function _get_ZSINIT_flag_num(fl) {
if (!ZSINIT_FLAG[fl]) {
throw("Invalid ZSINIT flag: " + fl);
}
return ZSINIT_FLAG[fl];
}
class ZSINIT_HEADER extends Zmodem.Header {
constructor( flags_arr, attn_seq_arr ) {
super();
var flags_num = 0;
flags_arr.forEach( function(fl) {
flags_num |= _get_ZSINIT_flag_num(fl);
} );
this._bytes4 = [ 0, 0, 0, flags_num ];
if (attn_seq_arr) {
if (attn_seq_arr.length > 31) {
throw("Attn sequence must be <= 31 bytes");
}
if (attn_seq_arr.some( function(num) { return num > 255 } )) {
throw("Attn sequence (" + attn_seq_arr + ") must be <256");
}
this._data = attn_seq_arr.concat([0]);
}
}
escape_ctrl_chars() {
return !!( this._bytes4[3] & ZSINIT_FLAG.ESCCTL );
}
//Is this used? I dont see it used in lrzsz or syncterm
escape_8th_bit() {
return !!( this._bytes4[3] & ZSINIT_FLAG.ESC8 );
}
}
//Thus far it doesnt seem we really need this header except to respond
//to ZSINIT, which doesnt require a payload.
class ZACK_HEADER extends Zmodem.Header {
constructor(payload4) {
super();
if (payload4) {
this._bytes4 = payload4.slice();
}
}
}
ZACK_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
//----------------------------------------------------------------------
const ZFILE_VALUES = {
//ZF3 (i.e., first byte)
extended: {
sparse: 0x40, //ZXSPARS
},
//ZF2
transport: [
undefined,
"compress", //ZTLZW
"encrypt", //ZTCRYPT
"rle", //ZTRLE
],
//ZF1
management: [
undefined,
"newer_or_longer", //ZF1_ZMNEWL
"crc", //ZF1_ZMCRC
"append", //ZF1_ZMAPND
"clobber", //ZF1_ZMCLOB
"newer", //ZF1_ZMNEW
"mtime_or_length", //ZF1_ZMNEW
"protect", //ZF1_ZMPROT
"rename", //ZF1_ZMPROT
],
//ZF0 (i.e., last byte)
conversion: [
undefined,
"binary", //ZCBIN
"text", //ZCNL
"resume", //ZCRESUM
],
};
const ZFILE_ORDER = ["extended", "transport", "management", "conversion"];
const ZMSKNOLOC = 0x80,
MANAGEMENT_MASK = 0x1f,
ZXSPARS = 0x40
;
class ZFILE_HEADER extends Zmodem.Header {
//TODO: allow options on instantiation
get_options() {
var opts = {
sparse: !!(this._bytes4[0] & ZXSPARS),
};
var bytes_copy = this._bytes4.slice(0);
ZFILE_ORDER.forEach( function(key, i) {
if (ZFILE_VALUES[key] instanceof Array) {
if (key === "management") {
opts.skip_if_absent = !!(bytes_copy[i] & ZMSKNOLOC);
bytes_copy[i] &= MANAGEMENT_MASK;
}
opts[key] = ZFILE_VALUES[key][ bytes_copy[i] ];
}
else {
for (var extkey in ZFILE_VALUES[key]) {
opts[extkey] = !!(bytes_copy[i] & ZFILE_VALUES[key][extkey]);
if (opts[extkey]) {
bytes_copy[i] ^= ZFILE_VALUES[key][extkey]
}
}
}
if (!opts[key] && bytes_copy[i]) {
opts[key] = "unknown:" + bytes_copy[i];
}
} );
return opts;
}
}
//----------------------------------------------------------------------
//Empty headers - in addition to ZRQINIT
class ZSKIP_HEADER extends Zmodem.Header {}
//No need for ZNAK
class ZABORT_HEADER extends Zmodem.Header {}
class ZFIN_HEADER extends Zmodem.Header {}
class ZFERR_HEADER extends Zmodem.Header {}
ZFIN_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
class ZOffsetHeader extends Zmodem.Header {
constructor(offset) {
super();
this._bytes4 = Zmodem.ENCODELIB.pack_u32_le(offset);
}
get_offset() {
return Zmodem.ENCODELIB.unpack_u32_le(this._bytes4);
}
}
class ZRPOS_HEADER extends ZOffsetHeader {};
class ZDATA_HEADER extends ZOffsetHeader {};
class ZEOF_HEADER extends ZOffsetHeader {};
//As request, receiver creates.
/* UNIMPLEMENTED FOR NOW
class ZCRC_HEADER extends ZHeader {
constructor(crc_le_bytes) {
super();
if (crc_le_bytes) { //response, sender creates
this._bytes4 = crc_le_bytes;
}
}
}
*/
//No ZCHALLENGE implementation
//class ZCOMPL_HEADER extends ZHeader {}
//class ZCAN_HEADER extends Zmodem.Header {}
//As described, this header represents an information disclosure.
//It could be interpreted, I suppose, merely as “this is how much space
//I have FOR YOU.”
//TODO: implement if needed/requested
//class ZFREECNT_HEADER extends ZmodemHeader {}
//----------------------------------------------------------------------
const FRAME_CLASS_TYPES = [
[ ZRQINIT_HEADER, "ZRQINIT" ],
[ ZRINIT_HEADER, "ZRINIT" ],
[ ZSINIT_HEADER, "ZSINIT" ],
[ ZACK_HEADER, "ZACK" ],
[ ZFILE_HEADER, "ZFILE" ],
[ ZSKIP_HEADER, "ZSKIP" ],
undefined, // [ ZNAK_HEADER, "ZNAK" ],
[ ZABORT_HEADER, "ZABORT" ],
[ ZFIN_HEADER, "ZFIN" ],
[ ZRPOS_HEADER, "ZRPOS" ],
[ ZDATA_HEADER, "ZDATA" ],
[ ZEOF_HEADER, "ZEOF" ],
[ ZFERR_HEADER, "ZFERR" ], //see note
undefined, //[ ZCRC_HEADER, "ZCRC" ],
undefined, //[ ZCHALLENGE_HEADER, "ZCHALLENGE" ],
undefined, //[ ZCOMPL_HEADER, "ZCOMPL" ],
undefined, //[ ZCAN_HEADER, "ZCAN" ],
undefined, //[ ZFREECNT_HEADER, "ZFREECNT" ],
undefined, //[ ZCOMMAND_HEADER, "ZCOMMAND" ],
undefined, //[ ZSTDERR_HEADER, "ZSTDERR" ],
];
/*
ZFERR is described as error in reading or writing file. Its really
not a good idea from a security angle for the endpoint to expose this
information. We should parse this and handle it as ZABORT but never send it.
Likewise with ZFREECNT: the sender shouldnt ask how much space is left
on the other box; rather, the receiver should decide what to do with the
file size as the sender reports it.
*/
var FRAME_NAME_CREATOR = {};
for (var fc=0; fc<FRAME_CLASS_TYPES.length; fc++) {
if (!FRAME_CLASS_TYPES[fc]) continue;
FRAME_NAME_CREATOR[ FRAME_CLASS_TYPES[fc][1] ] = FRAME_CLASS_TYPES[fc][0];
Object.assign(
FRAME_CLASS_TYPES[fc][0].prototype,
{
TYPENUM: fc,
NAME: FRAME_CLASS_TYPES[fc][1],
}
);
}
//----------------------------------------------------------------------
const CREATORS = [
ZRQINIT_HEADER,
ZRINIT_HEADER,
ZSINIT_HEADER,
ZACK_HEADER,
ZFILE_HEADER,
ZSKIP_HEADER,
'ZNAK',
ZABORT_HEADER,
ZFIN_HEADER,
ZRPOS_HEADER,
ZDATA_HEADER,
ZEOF_HEADER,
ZFERR_HEADER,
'ZCRC', //ZCRC_HEADER, -- leaving unimplemented?
'ZCHALLENGE',
'ZCOMPL',
'ZCAN',
'ZFREECNT', // ZFREECNT_HEADER,
'ZCOMMAND',
'ZSTDERR',
];
function _get_blank_header(typenum) {
var creator = CREATORS[typenum];
if (typeof(creator) === "string") {
throw( "Received unsupported header: " + creator );
}
/*
if (creator === ZCRC_HEADER) {
return new creator([0, 0, 0, 0]);
}
*/
return _get_blank_header_from_constructor(creator);
}
//referenced outside TODO
function _get_blank_header_from_constructor(creator) {
if (creator.prototype instanceof ZOffsetHeader) {
return new creator(0);
}
return new creator([]);
}
function _parse_binary16(bytes_arr) {
//The max length of a ZDLE-encoded binary header w/ 16-bit CRC is:
// 3 initial bytes, NOT ZDLE-encoded
// 2 typenum bytes (1 decoded)
// 8 data bytes (4 decoded)
// 4 CRC bytes (2 decoded)
//A 16-bit payload has 7 ZDLE-encoded octets.
//The ZDLE-encoded octets follow the initial prefix.
var zdle_decoded = Zmodem.ZDLE.splice( bytes_arr, BINARY16_HEADER_PREFIX.length, 7 );
return zdle_decoded && _parse_non_zdle_binary16(zdle_decoded);
}
function _parse_non_zdle_binary16(decoded) {
Zmodem.CRC.verify16(
decoded.slice(0, 5),
decoded.slice(5)
);
var typenum = decoded[0];
var hdr = _get_blank_header(typenum);
hdr._bytes4 = decoded.slice( 1, 5 );
return hdr;
}
function _parse_binary32(bytes_arr) {
//Same deal as with 16-bit CRC except there are two more
//potentially ZDLE-encoded bytes, for a total of 9.
var zdle_decoded = Zmodem.ZDLE.splice(
bytes_arr, //omit the leading "*", ZDLE, and "C"
BINARY32_HEADER_PREFIX.length,
9
);
if (!zdle_decoded) return;
Zmodem.CRC.verify32(
zdle_decoded.slice(0, 5),
zdle_decoded.slice(5)
);
var typenum = zdle_decoded[0];
var hdr = _get_blank_header(typenum);
hdr._bytes4 = zdle_decoded.slice( 1, 5 );
return hdr;
}
function _parse_hex(bytes_arr) {
//A hex header always has:
// 4 bytes for the ** . ZDLE . 'B'
// 2 hex bytes for the header type
// 8 hex bytes for the header content
// 4 hex bytes for the CRC
// 1-2 bytes for (CR/)LF
// (...and at this point the trailing XON is already stripped)
//
//----------------------------------------------------------------------
//A carriage return and line feed are sent with HEX headers. The
//receive routine expects to see at least one of these characters, two
//if the first is CR.
//----------------------------------------------------------------------
//
//^^ I guess it can be either CR/LF or just LF … though those two
//sentences appear to be saying contradictory things.
var lf_pos = bytes_arr.indexOf( 0x8a ); //lrzsz sends this
if (-1 === lf_pos) {
lf_pos = bytes_arr.indexOf( 0x0a );
}
var hdr_err, hex_bytes;
if (-1 === lf_pos) {
if (bytes_arr.length > 11) {
hdr_err = "Invalid hex header - no LF detected within 12 bytes!";
}
//incomplete header
return;
}
else {
hex_bytes = bytes_arr.splice( 0, lf_pos );
//Trim off the LF
bytes_arr.shift();
if ( hex_bytes.length === 19 ) {
//NB: The spec says CR but seems to treat high-bit variants
//of control characters the same as the regulars; should we
//also allow 0x8d?
var preceding = hex_bytes.pop();
if ( preceding !== 0x0d && preceding !== 0x8d ) {
hdr_err = "Invalid hex header: (CR/)LF doesnt have CR!";
}
}
else if ( hex_bytes.length !== 18 ) {
hdr_err = "Invalid hex header: invalid number of bytes before LF!";
}
}
if (hdr_err) {
hdr_err += " (" + hex_bytes.length + " bytes: " + hex_bytes.join() + ")";
throw hdr_err;
}
hex_bytes.splice(0, 4);
//Should be 7 bytes ultimately:
// 1 for typenum
// 4 for header data
// 2 for CRC
var octets = Zmodem.ENCODELIB.parse_hex_octets(hex_bytes);
return _parse_non_zdle_binary16(octets);
}
Zmodem.Header.parse_hex = _parse_hex;

102
src/zmlib.js Normal file
View file

@ -0,0 +1,102 @@
"use strict";
var Zmodem = module.exports;
const
ZDLE = 0x18,
XON = 0x11,
XOFF = 0x13,
XON_HIGH = 0x80 | XON,
XOFF_HIGH = 0x80 | XOFF,
CAN = 0x18 //NB: same character as ZDLE
;
/**
* Tools and constants that are useful for ZMODEM.
*
* @exports ZMLIB
*/
Zmodem.ZMLIB = {
/**
* @property {number} The ZDLE constant, which ZMODEM uses for escaping
*/
ZDLE: ZDLE,
/**
* @property {number} XON - ASCII XON
*/
XON: XON,
/**
* @property {number} XOFF - ASCII XOFF
*/
XOFF: XOFF,
/**
* @property {number[]} ABORT_SEQUENCE - ZMODEMs abort sequence
*/
ABORT_SEQUENCE: [ CAN, CAN, CAN, CAN, CAN ],
/**
* Remove octet values from the given array that ZMODEM always ignores.
* This will mutate the given array.
*
* @param {number[]} octets - The octet values to transform.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {number[]} The passed-in array. This is the same object that is
* passed in.
*/
strip_ignored_bytes: function strip_ignored_bytes(octets) {
for (var o=octets.length-1; o>=0; o--) {
switch (octets[o]) {
case XON:
case XON_HIGH:
case XOFF:
case XOFF_HIGH:
octets.splice(o, 1);
continue;
}
}
return octets;
},
/**
* Like Array.prototype.indexOf, but searches for a subarray
* rather than just a particular value.
*
* @param {Array} haystack - The array to search, i.e., the bigger.
*
* @param {Array} needle - The array whose values to find,
* i.e., the smaller.
*
* @returns {number} The position in haystack where needle
* first appearsor, -1 if needle doesnt appear anywhere
* in haystack.
*/
find_subarray: function find_subarray(haystack, needle) {
var h=0, n;
var start = Date.now();
HAYSTACK:
while (h !== -1) {
h = haystack.indexOf( needle[0], h );
if (h === -1) break HAYSTACK;
for (n=1; n<needle.length; n++) {
if (haystack[h + n] !== needle[n]) {
h++;
continue HAYSTACK;
}
}
return h;
}
return -1;
},
};

4
src/zmodem.js Normal file
View file

@ -0,0 +1,4 @@
Object.assign(
module.exports,
require("./zsentry"),
);

182
src/zmodem_browser.js Normal file
View file

@ -0,0 +1,182 @@
"use strict";
var Zmodem = module.exports;
//TODO: Make this usable without require.js or what not.
window.Zmodem = Zmodem;
Object.assign(
Zmodem,
require("./zmodem")
);
function _check_aborted(session) {
if (session.aborted()) {
throw new Zmodem.Error("aborted");
}
}
/** Browser-specific tools
*
* @exports Browser
*/
Zmodem.Browser = {
/**
* Send a batch of files in sequence. The session is left open
* afterward, which allows for more files to be sent if desired.
*
* @param {Zmodem.Session} session - The send session
*
* @param {FileList|Array} files - A list of File objects
*
* @param {Object} [options]
* @param {Function} [options.on_offer_response] - Called when an
* offer response arrives. Arguments are:
*
* - (File) - The File object that corresponds to the offer.
* - (Transfer|undefined) - If the receiver accepts the offer, then
* this is a Transfer object; otherwise its undefined.
*
* @param {Function} [options.on_progress] - Called immediately
* after a chunk of a file is sent. Arguments are:
*
* - (File) - The File object that corresponds to the file.
* - (Transfer) - The Transfer object for the current transfer.
* - (Uint8Array) - The chunk of data that was just loaded from disk
* and sent to the receiver.
*
* @param {Function} [options.on_file_complete] - Called immediately
* after the last file packet is sent. Arguments are:
*
* - (File) - The File object that corresponds to the file.
* - (Transfer) - The Transfer object for the now-completed transfer.
*
* @return {Promise} A Promise that fulfills when the batch is done.
* Note that skipped files are not considered an error condition.
*/
send_files: function send_files(session, files, options) {
if (!options) options = {};
//Populate the batch in reverse order to simplify sending
//the remaining files/bytes components.
var batch = [];
var total_size = 0;
for (var f=files.length - 1; f>=0; f--) {
var fobj = files[f];
total_size += fobj.size;
batch[f] = {
obj: fobj,
name: fobj.name,
size: fobj.size,
mtime: new Date(fobj.lastModified),
files_remaining: files.length - f,
bytes_remaining: total_size,
};
}
var file_idx = 0;
function promise_callback() {
var cur_b = batch[file_idx];
if (!cur_b) {
return Promise.resolve(); //batch done!
}
file_idx++;
return session.send_offer(cur_b).then( function after_send_offer(xfer) {
if (options.on_offer_response) {
options.on_offer_response(cur_b.obj, xfer);
}
if (xfer === undefined) {
return promise_callback(); //skipped
}
return new Promise( function(res) {
var reader = new FileReader();
//This really shouldnt happen … so lets
//blow up if it does.
reader.onerror = function reader_onerror(e) {
console.error("file read error", e);
throw("File read error: " + e);
};
var piece;
reader.onprogress = function reader_onprogress(e) {
//Some browsers (e.g., Chrome) give partial returns,
//while others (e.g., Firefox) dont.
if (e.target.result) {
piece = new Uint8Array(e.target.result, xfer.get_offset())
_check_aborted(session);
xfer.send(piece);
if (options.on_progress) {
options.on_progress(cur_b.obj, xfer, piece);
}
}
};
reader.onload = function reader_onload(e) {
piece = new Uint8Array(e.target.result, xfer, piece)
_check_aborted(session);
xfer.end(piece).then( function() {
if (options.on_progress && piece.length) {
options.on_progress(cur_b.obj, xfer, piece);
}
if (options.on_file_complete) {
options.on_file_complete(cur_b.obj, xfer);
}
//Resolve the current file-send promise with
//another promise. That promise resolves immediately
//if were done, or with another file-send promise
//if theres more to send.
res( promise_callback() );
} );
};
reader.readAsArrayBuffer(cur_b.obj);
} );
} );
}
return promise_callback();
},
/**
* Prompt a user to save the given packets as a file by injecting an
* `<a>` element (with `display: none` styling) into the page and
* calling the elements `click()`
* method. The element is removed immediately after.
*
* @param {Array} packets - Same as the first argument to [Blobs constructor](https://developer.mozilla.org/en-US/docs/Web/API/Blob).
* @param {string} name - The name to give the file.
*/
save_to_disk: function save_to_disk(packets, name) {
var blob = new Blob(packets);
var url = URL.createObjectURL(blob);
var el = document.createElement("a");
el.style.display = "none";
el.href = url;
el.download = name;
document.body.appendChild(el);
//It seems like a security problem that this actually works;
//Id think there would need to be some confirmation before
//a browser could save arbitrarily many bytes onto the disk.
//But, hey.
el.click();
document.body.removeChild(el);
},
};

394
src/zsentry.js Normal file
View file

@ -0,0 +1,394 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zmlib"),
require("./zsession")
);
const
MIN_ZM_HEX_START_LENGTH = 20,
MAX_ZM_HEX_START_LENGTH = 21,
// **, ZDLE, 'B0'
//ZRQINITs next byte will be '0'; ZRINITs will be '1'.
COMMON_ZM_HEX_START = [ 42, 42, 24, 66, 48 ],
SENTRY_CONSTRUCTOR_REQUIRED_ARGS = [
"to_terminal",
"on_detect",
"on_retract",
"sender",
],
ASTERISK = 42
;
/**
* An instance of this object is passed to the Sentrys on_detect
* callback each time the Sentry object sees what looks like the
* start of a ZMODEM session.
*
* Note that it is possible for a detection to be retracted
* if the Sentry consumes bytes afterward that are not ZMODEM.
* When this happens, the Sentrys `retract` event will fire,
* after which the Detection object is no longer usable.
*/
class Detection {
/**
* Not called directly.
*/
constructor(session_type, accepter, denier, checker) {
//confirm() - user confirms that ZMODEM is desired
this._confirmer = accepter;
//deny() - user declines ZMODEM; send abort sequence
//
//TODO: It might be ideal to forgo the session “peaceably”,
//i.e., such that the peer doesnt end in error. Thats
//possible if were the sender, we accept the session,
//then we just send a close(), but it doesnt seem to be
//possible for a receiver. Thus, lets just leave it so
//its at least consistent (and simpler, too).
this._denier = denier;
this._is_valid = checker;
this._session_type = session_type;
}
/**
* Confirm that the detected ZMODEM sequence indicates the
* start of a ZMODEM session.
*
* @return {Session} The ZMODEM Session object (i.e., either a
* Send or Receive instance).
*/
confirm() {
return this._confirmer.apply(this, arguments);
}
/**
* Tell the Sentry that the detected bytes sequence is
* **NOT** intended to be the start of a ZMODEM session.
*/
deny() {
return this._denier.apply(this, arguments);
}
/**
* Tells whether the Detection is still valid; i.e., whether
* the Sentry has `consume()`d bytes that invalidate the
* Detection.
*
* @returns {boolean} Whether the Detection is valid.
*/
is_valid() {
return this._is_valid.apply(this, arguments);
}
/**
* Gives the sessions role.
*
* @returns {string} One of:
* - `receive`
* - `send`
*/
get_session_role() { return this._session_type }
}
/**
* Class that parses an input stream for the beginning of a
* ZMODEM session. We look for the tell-tale signs
* of a ZMODEM transfer and allow the client to determine whether
* its really ZMODEM or not.
*
* This is the mother class for zmodem.js;
* all other class instances are created, directly or indirectly,
* by an instance of this class.
*
* This logic is not unlikely to need tweaking, and it can never
* be fully bulletproof; if it could be bulletproof it would be
* simpler since there wouldnt need to be the .confirm()/.deny()
* step.
*
* One thing you could do to make things a bit simpler *is* just
* to make that assumption for your users--i.e., to .confirm()
* Detection objects automatically. Thatll be one less step
* for the user, but an unaccustomed user might find that a bit
* confusing. Its also then possible to have a false positive:
* a text stream that contains a ZMODEM initialization string but
* isnt, in fact, meant to start a ZMODEM session.
*
* Workflow:
* - parse all input with .consume(). As long as nothing looks
* like ZMODEM, all the traffic will go to to_terminal().
*
* - when a tell-tale sequence of bytes arrives, we create a
* Detection object and pass it to the on_detect handler.
*
* - Either .confirm() or .deny() with the Detection object.
* This is the users chance to say, yeah, I know those
* bytes look like ZMODEM, but theyre not. So back off!
*
* If you .confirm(), the Session object is returned, and
* further input that goes to the Sentrys .consume() will
* go to the (now-active) Session object.
*
* - Sometimes additional traffic arrives that makes it apparent
* that no ZMODEM session is intended to start; in this case,
* the Sentry marks the Detection as stale and calls the
* `on_retract` handler. Any attempt from here to .confirm()
* on the Detection object will prompt an exception.
*
* (This retraction behavior will only happen prior to
* .confirm() or .deny() being called on the Detection object.
* Beyond that point, either the Session has to deal with the
* garbage, or its back to the terminal anyway.
*
* - Once the Session object is done, the Sentry will again send
* all traffic to to_terminal().
*/
Zmodem.Sentry = class ZmodemSentry {
/**
* Invoked directly. Creates a new Sentry that inspects all
* traffic before it goes to the terminal.
*
* @param {Object} options - The Sentry parameters
*
* @param {Function} options.to_terminal - Handler that sends
* traffic to the terminal object. Receives an iterable object
* (e.g., an Array) that contains octet numbers.
*
* @param {Function} options.on_detect - Handler for new
* detection events. Receives a new Detection object.
*
* @param {Function} options.on_retract - Handler for retraction
* events. Receives no input.
*
* @param {Function} options.sender - Handler that sends traffic to
* the peer. If, for example, your application uses WebSocket to talk
* to the peer, use this to send data to the WebSocket instance.
*/
constructor(options) {
if (!options) throw "Need options!";
var sentry = this;
SENTRY_CONSTRUCTOR_REQUIRED_ARGS.forEach( function(arg) {
if (!options[arg]) {
throw "Need “" + arg + "”!";
}
sentry["_" + arg] = options[arg];
} );
this._cache = [];
}
_after_session_end() {
this._zsession = null;
}
/**
* Consumes a piece of input:
*
* - If there is no active or pending ZMODEM session, the text is
* all output. (This is regardless of whether weve got a new
* Detection.)
*
* - If there is no active ZMODEM session and the input **ends** with
* a ZRINIT or ZRQINIT, then a new Detection object is created,
* and it is passed to the on_detect function.
* If there was another pending Detection object, it is retracted.
*
* - If there is no active ZMODEM session and the input does NOT end
* with a ZRINIT or ZRQINIT, then any pending Detection object is
* retracted.
*
* - If there is an active ZMODEM session, the input is passed to it.
* Any non-ZMODEM data (i.e., garbage) parsed from the input
* is sent to output.
* If the ZMODEM session ends, any post-ZMODEM part of the input
* is sent to output.
*
* @param {number[] | ArrayBuffer} input - Octets to parse as input.
*/
consume(input) {
if (!(input instanceof Array)) {
input = Array.prototype.slice.call( new Uint8Array(input) );
}
if (this._zsession) {
var session_before_consume = this._zsession;
session_before_consume.consume(input);
if (session_before_consume.has_ended()) {
if (session_before_consume.type === "receive") {
input = session_before_consume.get_trailing_bytes();
}
else {
input = [];
}
}
else return;
}
var new_session = this._parse(input);
var to_terminal = input;
if (new_session) {
let replacement_detect = !!this._parsed_session;
if (replacement_detect) {
//no terminal output if the new session is of the
//same type as the old
if (this._parsed_session.type === new_session.type) {
to_terminal = [];
}
this._on_retract();
}
this._parsed_session = new_session;
var sentry = this;
function checker() {
return sentry._parsed_session === new_session;
}
//This runs with the Sentry object as the context.
function accepter() {
if (!this.is_valid()) {
throw "Stale ZMODEM session!";
}
new_session.on("garbage", sentry._to_terminal);
new_session.on(
"session_end",
sentry._after_session_end.bind(sentry)
);
new_session.set_sender(sentry._sender);
delete sentry._parsed_session;
return sentry._zsession = new_session;
};
function denier() {
if (!this.is_valid()) return;
};
this._on_detect( new Detection(
new_session.type,
accepter,
this._send_abort.bind(this),
checker
) );
}
else {
/*
if (this._parsed_session) {
this._session_stale_because = 'Non-ZMODEM output received after ZMODEM initialization.';
}
*/
var expired_session = this._parsed_session;
this._parsed_session = null;
if (expired_session) {
//If we got a single “C” after parsing a session,
//that means our peer is trying to downgrade to YMODEM.
//That wont work, so we just send the ABORT_SEQUENCE
//right away.
if (to_terminal.length === 1 && to_terminal[0] === 67) {
this._send_abort();
}
this._on_retract();
}
}
this._to_terminal(to_terminal);
}
/**
* @return {Session|null} The sentrys current Session object, or
* null if there is none.
*/
get_confirmed_session() {
return this._zsession || null;
}
_send_abort() {
this._sender( Zmodem.ZMLIB.ABORT_SEQUENCE );
}
/**
* Parse an input stream and decide how much of it goes to the
* terminal or to a new Session object.
*
* This will accommodate input strings that are fragmented
* across calls to this function; e.g., if you send the first
* two bytes at the end of one parse() call then send the rest
* at the beginning of the next, parse() will recognize it as
* the beginning of a ZMODEM session.
*
* In order to keep from blocking any actual useful data to the
* terminal in real-time, this will send on the initial
* ZRINIT/ZRQINIT bytes to the terminal. Theyre meant to go to the
* terminal anyway, so that should be fine.
*
* @private
*
* @param {Array|Uint8Array} array_like - The input bytes.
* Each member should be a number between 0 and 255 (inclusive).
*
* @return {Array} A two-member list:
* 0) the bytes that should be printed on the terminal
* 1) the created Session object (if any)
*/
_parse(array_like) {
var cache = this._cache;
cache.push.apply( cache, array_like );
while (true) {
let common_hex_at = Zmodem.ZMLIB.find_subarray( cache, COMMON_ZM_HEX_START );
if (-1 === common_hex_at) break;
let before_common_hex = cache.splice(0, common_hex_at);
let zsession;
try {
zsession = Zmodem.Session.parse(cache);
} catch(err) { //ignore errors
//console.log(err);
}
if (!zsession) break;
//Dont need to parse the trailing XON.
if ((cache.length === 1) && (cache[0] === Zmodem.ZMLIB.XON)) {
cache.shift();
}
//If there are still bytes in the cache,
//then we dont have a ZMODEM session. This logic depends
//on the sender only sending one initial header.
return cache.length ? null : zsession;
}
cache.splice( MAX_ZM_HEX_START_LENGTH );
return null;
}
}

1677
src/zsession.js Normal file

File diff suppressed because it is too large Load diff

241
src/zsubpacket.js Normal file
View file

@ -0,0 +1,241 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zcrc"),
require("./zdle"),
require("./zmlib"),
require("./zerror")
);
const
ZCRCE = 0x68, // 'h', 104, frame ends, header packet follows
ZCRCG = 0x69, // 'i', 105, frame continues nonstop
ZCRCQ = 0x6a, // 'j', 106, frame continues, ZACK expected
ZCRCW = 0x6b // 'k', 107, frame ends, ZACK expected
;
var SUBPACKET_BUILDER;
/** Class that represents a ZMODEM data subpacket. */
Zmodem.Subpacket = class ZmodemSubpacket {
/**
* Build a Subpacket subclass given a payload and frame end string.
*
* @param {Array} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
*
* @param {string} frameend - One of:
* - `no_end_no_ack`
* - `end_no_ack`
* - `no_end_ack` (unused currently)
* - `end_ack`
*
* @returns {Subpacket} An instance of the appropriate Subpacket subclass.
*/
static build(octets, frameend) {
//TODO: make this better
var Ctr = SUBPACKET_BUILDER[frameend];
if (!Ctr) {
throw("No subpacket type “" + frameend + "” is defined! Try one of: " + Object.keys(SUBPACKET_BUILDER).join(", "));
}
return new Ctr(octets);
}
/**
* Return the octet values array that represents the object
* encoded with a 16-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
encode16(zencoder) {
return this._encode( zencoder, Zmodem.CRC.crc16 );
}
/**
* Return the octet values array that represents the object
* encoded with a 32-bit CRC.
*
* @param {ZDLE} zencoder - A ZDLE instance to use for ZDLE encoding.
*
* @returns {number[]} An array of octet values suitable for sending
* as binary data.
*/
encode32(zencoder) {
return this._encode( zencoder, Zmodem.CRC.crc32 );
}
/**
* Return the subpacket payloads octet values.
*
* NOTE: For speed, this returns the actual data in the subpacket;
* if you mutate this return value, you alter the Subpacket object
* internals. This is OK if you wont need the Subpacket anymore, but
* just be careful.
*
* @returns {number[]} The subpackets payload, represented as an
* array of octet values. **DO NOT ALTER THIS ARRAY** unless you
* no longer need the Subpacket.
*/
get_payload() { return this._payload }
/**
* Parse out a Subpacket object from a given array of octet values,
* assuming a 16-bit CRC.
*
* An exception is thrown if the given bytes are definitively invalid
* as subpacket values with 16-bit CRC.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Subpacket|undefined} An instance of the appropriate Subpacket
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid subpacket here or not.
*/
static parse16(octets) {
return ZmodemSubpacket._parse(octets, 2);
}
//parse32 test:
//[102, 105, 108, 101, 110, 97, 109, 101, 119, 105, 116, 104, 115, 112, 97, 99, 101, 115, 0, 49, 55, 49, 51, 49, 52, 50, 52, 51, 50, 49, 55, 50, 49, 48, 48, 54, 52, 52, 48, 49, 49, 55, 0, 43, 8, 63, 115, 23, 17]
/**
* Same as parse16(), but assuming a 32-bit CRC.
*
* @param {number[]} octets - The octet values to parse.
* Each array member should be an 8-bit unsigned integer (0-255).
* This object is mutated in the function.
*
* @returns {Subpacket|undefined} An instance of the appropriate Subpacket
* subclass, or undefined if not enough octet values are given
* to determine whether there is a valid subpacket here or not.
*/
static parse32(octets) {
return ZmodemSubpacket._parse(octets, 4);
}
/**
* Not used directly.
*/
constructor(payload) {
this._payload = payload;
}
_encode(zencoder, crc_func) {
return zencoder.encode( this._payload.slice(0) ).concat(
[ Zmodem.ZMLIB.ZDLE, this._frameend_num ],
zencoder.encode( crc_func( this._payload.concat(this._frameend_num) ) )
);
}
//Because of ZDLE encoding, well never see any of the frame-end octets
//in a stream except as the ends of data payloads.
static _parse(bytes_arr, crc_len) {
var end_at;
var creator;
//These have to be written in decimal since theyre lookup keys.
var _frame_ends_lookup = {
104: ZEndNoAckSubpacket,
105: ZNoEndNoAckSubpacket,
106: ZNoEndAckSubpacket,
107: ZEndAckSubpacket,
};
var zdle_at = 0;
while (zdle_at < bytes_arr.length) {
zdle_at = bytes_arr.indexOf( Zmodem.ZMLIB.ZDLE, zdle_at );
if (zdle_at === -1) return;
var after_zdle = bytes_arr[ zdle_at + 1 ];
creator = _frame_ends_lookup[ after_zdle ];
if (creator) {
end_at = zdle_at + 1;
break;
}
zdle_at++;
}
if (!creator) return;
var frameend_num = bytes_arr[end_at];
//sanity check
if (bytes_arr[end_at - 1] !== Zmodem.ZMLIB.ZDLE) {
throw( "Byte before frame end should be ZDLE, not " + bytes_arr[end_at - 1] );
}
var zdle_encoded_payload = bytes_arr.splice( 0, end_at - 1 );
var got_crc = Zmodem.ZDLE.splice( bytes_arr, 2, crc_len );
if (!got_crc) {
//got payload but no CRC yet .. should be rare!
//We have to put the ZDLE-encoded payload back before returning.
bytes_arr.unshift.apply(bytes_arr, zdle_encoded_payload);
return;
}
var payload = Zmodem.ZDLE.decode(zdle_encoded_payload);
//We really shouldnt need to do this, but just for good measure.
//I suppose its conceivable this may run over UDP or something?
Zmodem.CRC[ (crc_len === 2) ? "verify16" : "verify32" ](
payload.concat( [frameend_num] ),
got_crc
);
return new creator(payload, got_crc);
}
}
class ZEndSubpacketBase extends Zmodem.Subpacket {
frame_end() { return true }
}
class ZNoEndSubpacketBase extends Zmodem.Subpacket {
frame_end() { return false }
}
//Used for end-of-file.
class ZEndNoAckSubpacket extends ZEndSubpacketBase {
ack_expected() { return false }
}
ZEndNoAckSubpacket.prototype._frameend_num = ZCRCE;
//Used for ZFILE and ZSINIT payloads.
class ZEndAckSubpacket extends ZEndSubpacketBase {
ack_expected() { return true }
}
ZEndAckSubpacket.prototype._frameend_num = ZCRCW;
//Used for ZDATA, prior to end-of-file.
class ZNoEndNoAckSubpacket extends ZNoEndSubpacketBase {
ack_expected() { return false }
}
ZNoEndNoAckSubpacket.prototype._frameend_num = ZCRCG;
//only used if receiver can full-duplex
class ZNoEndAckSubpacket extends ZNoEndSubpacketBase {
ack_expected() { return true }
}
ZNoEndAckSubpacket.prototype._frameend_num = ZCRCQ;
SUBPACKET_BUILDER = {
end_no_ack: ZEndNoAckSubpacket,
end_ack: ZEndAckSubpacket,
no_end_no_ack: ZNoEndNoAckSubpacket,
no_end_ack: ZNoEndAckSubpacket,
};

130
src/zvalidation.js Normal file
View file

@ -0,0 +1,130 @@
"use strict";
var Zmodem = module.exports;
Object.assign(
Zmodem,
require("./zerror")
);
const LOOKS_LIKE_ZMODEM_HEADER = /\*\x18[AC]|\*\*\x18B/;
function _validate_number(key, value) {
if (value < 0) {
throw new Zmodem.Error("validation", "“" + key + "” (" + value + ") must be nonnegative.");
}
if (value !== Math.floor(value)) {
throw new Zmodem.Error("validation", "“" + key + "” (" + value + ") must be an integer.");
}
}
/** Validation logic for zmodem.js
*
* @exports Validation
*/
Zmodem.Validation = {
/**
* Validates and normalizes a set of parameters for an offer to send.
* NOTE: This returns mtime as epoch seconds, not a Date. This is
* inconsistent with the get_details() method in Session, but its
* more useful for sending over the wire.
*
* @param {FileDetails} params - The file details. Some fairly trivial
* variances from the specification are allowed.
*
* @return {FileDetails} The parameters that should be sent. `mtime`
* will be a Date rather than a number.
*/
offer_parameters: function offer_parameters(params) {
if (!params.name) {
throw new Zmodem.Error("validation", "Need “name”!");
}
if (typeof params.name !== "string") {
throw new Zmodem.Error("validation", "“name” (" + params.name + ") must be a string!");
}
//So that we can override values as is useful
//without affecting the passed-in object.
params = Object.assign({}, params);
if (LOOKS_LIKE_ZMODEM_HEADER.test(params.name)) {
console.warn("The filename " + JSON.stringify(name) + " contains characters that look like a ZMODEM header. This could corrupt the ZMODEM session; consider renaming it so that the filename doesnt contain control characters.");
}
if (params.serial !== null && params.serial !== undefined) {
throw new Zmodem.Error("validation", "“serial” is meaningless.");
}
params.serial = null;
["size", "mode", "files_remaining", "bytes_remaining"].forEach(
function(k) {
var ok;
switch (typeof params[k]) {
case "object":
ok = (params[k] === null);
break;
case "undefined":
params[k] = null;
ok = true;
break;
case "number":
_validate_number(k, params[k]);
ok = true;
break;
}
if (!ok) {
throw new Zmodem.Error("validation", "“" + k + "” (" + params[k] + ") must be null, undefined, or a number.");
}
}
);
if (typeof params.mode === "number") {
params.mode |= 0x8000;
}
if (params.files_remaining === 0) {
throw new Zmodem.Error("validation", "“files_remaining”, if given, must be positive.");
}
var mtime_ok;
switch (typeof params.mtime) {
case "object":
mtime_ok = true;
if (params.mtime instanceof Date) {
var date_obj = params.mtime;
params.mtime = Math.floor( date_obj.getTime() / 1000 );
if (params.mtime < 0) {
throw new Zmodem.Error("validation", "“mtime” (" + date_obj + ") must not be earlier than 1970.");
}
}
else if (params.mtime !== null) {
mtime_ok = false;
}
break;
case "undefined":
params.mtime = null;
mtime_ok = true;
break;
case "number":
_validate_number("mtime", params.mtime);
mtime_ok = true;
break;
}
if (!mtime_ok) {
throw new Zmodem.Error("validation", "“mtime” (" + params.mtime + ") must be null, undefined, a Date, or a number.");
}
return params;
},
};

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

BIN
tools/all_bytes Normal file

Binary file not shown.

227
tools/talk_to_sz.pl Executable file
View file

@ -0,0 +1,227 @@
#!/usr/bin/env perl
use strict;
use warnings;
use autodie;
use constant CANCEL_BYTES => (
((24) x 5),
((8) x 5),
#0,
);
use constant ZCAN_BYTES => (
42, 42, 24, 66, 49, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 52, 53, 97, 13, 10, 17
);
use constant VERBOSE => '-vvvvvvvvvvvv';
use feature 'say';
use IO::Poll ();
use File::Temp ();
use File::Which ();
use Text::Control ();
my $COMMAND = 'sz';
#my @verbose_flags = ( VERBOSE() );
my @verbose_flags = ();
my $size = 2**24;
my $file_content = ('x' x $size) . '=THE END';
my $cmd_path = File::Which::which($COMMAND) or die "Need “$COMMAND”!";
#$cmd_path = '/Users/felipe/code/lrzsz/src/lsz';
my ($tfh, $tpath) = File::Temp::tempfile( CLEANUP => 1 );
print "temp file path: $tpath\n";
syswrite $tfh, $file_content;
close $tfh;
pipe( my $pr, my $cw );
pipe( my $cr, my $pw );
my $pid = fork or do {
close $_ for ($pr, $pw);
open \*STDIN, '<&=', $cr;
open \*STDOUT, '>>&=', $cw;
exec $cmd_path, @verbose_flags, $tpath or die $!;
};
close $_ for ($cr, $cw);
$pr->blocking(0);
my $poll = IO::Poll->new();
$poll->mask( $pr, IO::Poll::POLLIN() );
sub _poll_in {
return $poll->poll(30) || die 'Timed out on read!';
}
sub _read {
_poll_in();
my $buf = q<>;
sysread( $pr, $buf, 4096, length $buf ); #itll never be that big
return $buf;
}
sub _read_and_report {
my $input = _read();
_report_from_child($input);
}
sub _report_from_child {
my $bytes = $_[0];
my $truncated_yn;
my $orig_len = length $bytes;
if ($orig_len > 70) {
substr($bytes, 25) = q<>;
$truncated_yn = 1;
}
$bytes = Text::Control::to_hex($bytes);
if ($truncated_yn) {
$bytes .= ' … ' . Text::Control::to_hex( substr($_[0], -45) );
$bytes .= " ($orig_len bytes)";
}
say "$COMMAND says: $bytes";
}
sub _write { syswrite $pw, $_[0]; }
sub _write_octets {
my $bytes = join( q<>, map { chr } @_ );
_write( $bytes );
say "to $COMMAND: " . Text::Control::to_hex($bytes);
}
sub _write_and_wait_to_finish {
_write_octets(@_);
_wait_to_finish();
}
sub _wait_to_finish {
close $pw;
$pr->blocking(1);
my $buf = q<>;
while (my $read = sysread $pr, $buf, 65536) {
if ($buf =~ m<=THE END>) {
print STDERR "\x07XXXXX FAILED TO STOP THE ONSLAUGHT!!\n";
sleep 2;
}
print "=========== FINAL ($read) ===========\n";
_report_from_child($buf);
}
close $pr;
waitpid $pid, 0;
my $exit = $? >> 8;
print "$COMMAND exit: $exit\n";
exit;
}
sub _send_cancel {
print "======= SENDING CANCEL\n";
_write_and_wait_to_finish( CANCEL_BYTES() );
}
sub _read_until_packet_end {
my $buf = q<>;
my $next_header;
while (1) {
if ($buf =~ m<\x18h..(.*)>) {
$next_header = $1;
last;
}
_poll_in();
sysread $pr, $buf, 65536, length $buf;
}
print "\nEnd of packet\n";
_report_from_child($next_header) if length $next_header;
return;
}
sub _send_ZCAN {
print "======= SENDING ZCAN\n";
_write_and_wait_to_finish( ZCAN_BYTES() );
}
#----------------------------------------------------------------------
#Shows ZRQINIT
_read_and_report();
#_send_cancel(); #works
#_send_ZCAN(); #doesnt work
use constant ZRINIT_BYTES => (
#CANOVIO, CANFDX
#42, 42, 24, 66, 48, 49, 48, 48, 48, 48, 48, 48, 48, 48, 97, 97, 53, 49, 13, 10, 17,
#CANOVIO, CANFDX, CANFC32
qw( 42 42 24 66 48 49 48 48 48 48 48 48 50 51 98 101 53 48 13 10 17 ),
);
use constant ZSKIP_BYTES => (
42, 42, 24, 66, 48, 53, 48, 48, 48, 48, 48, 48, 48, 48, 50, 51, 53, 55, 13, 10, 17,
);
#ZRINIT
_write_octets( ZRINIT_BYTES() );
#Shows ZFILE and offer subpacket
_read_and_report();
#_send_cancel(); #works
#_send_ZCAN(); #works
#ZRPOS
_write_octets(
42, 42, 24, 66, 48, 57, 48, 48, 48, 48, 48, 48, 48, 48, 97, 56, 55, 99, 13, 10, 17
);
#Shows initial batch of file data
#_read_and_report();
#
#_send_ZCAN(); #works - BUFFER OVERFLOW
_send_cancel(); #works - BUFFER OVERFLOW
_read_and_report();
#_write_octets( ZSKIP_BYTES() );
#_read_until_packet_end();
#_send_cancel(); #works
#ZRINIT
_write_octets( ZRINIT_BYTES() );
#_send_cancel(); #works
_read_and_report();
_send_cancel(); #works - but by this point the transfer is done
#ZFIN
_write_octets(
42, 42, 24, 66, 48, 56, 48, 48, 48, 48, 48, 48, 48, 48, 48, 50, 50, 100, 13, 10
);
_wait_to_finish();

28
webpack.config.js Normal file
View file

@ -0,0 +1,28 @@
"use strict";
const path = require("path");
const MinifyPlugin = require("babel-minify-webpack-plugin");
const JsDocPlugin = require('jsdoc-webpack-plugin');
module.exports = {
entry: {
zmodem: [ "./src/zmodem_browser.js" ],
"zmodem.devel": [ "./src/zmodem_browser.js" ],
},
output: {
path: path.resolve( __dirname, "dist" ),
filename: "[name].js",
},
plugins: [
new MinifyPlugin(
null,
{
test: /zmodem\.js$/,
}
),
new JsDocPlugin({
conf: './jsdoc.json'
})
]
}

3010
yarn.lock Normal file

File diff suppressed because it is too large Load diff