Adding upstream version 0.1.10+dfsg.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
157f539082
commit
4d3e0bf859
42 changed files with 10556 additions and 0 deletions
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.md’s 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
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
.*.sw?
|
||||
/dist
|
||||
/documentation
|
||||
/node_modules
|
9
.travis.yml
Normal file
9
.travis.yml
Normal 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
44
CHANGELOG.md
Normal 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 didn’t 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
201
LICENSE
Normal 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
432
README.md
Normal file
|
@ -0,0 +1,432 @@
|
|||
# zmodem.js - ZMODEM for JavaScript
|
||||
|
||||
[](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 don’t 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()` Promise’s 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 didn’t 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` can’t reach, though, is terminals that don’t 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
|
||||
wouldn’t work in browsers, which is zmodem.js’s 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 ZMODEM’s original application was over raw modem connections that
|
||||
often didn’t have reliable hardware error correction. TCP also wasn’t
|
||||
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 aren’t
|
||||
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, it’s possible to
|
||||
“poison” such messages, e.g., by sending a file whose name includes a
|
||||
ZMODEM header. So don’t do that. :-P
|
||||
|
||||
# IMPLEMENTATION NOTES
|
||||
|
||||
* I initially had success integrating zmodem.js with
|
||||
[xterm.js](https://xtermjs.org); however, that library’s plugin interface
|
||||
changed dramatically, and I haven’t created a new plugin to replace the
|
||||
old one. (It should be relatively straightforward if someone else wants to
|
||||
pick it up.)
|
||||
|
||||
* Browsers don’t 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 doesn’t seem to have
|
||||
been implemented?) Hence, **if you use WebSocket, you’ll 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 you’ll 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 latter’s many variants) require the
|
||||
receiver to initiate the session by sending a “magic character” (ASCII SOH);
|
||||
the problem is that there’s 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 doesn’t “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 constructor’s `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 guy’s solution to a particular problem. There is
|
||||
documentation, but it’s not as helpful as it might be; for example,
|
||||
there’s only one example workflow given, and it’s a “happy-path”
|
||||
transmission of a single file.
|
||||
|
||||
As part of writing zmodem.js I’ve 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 Forsberg’s last shareware release;
|
||||
I have not looked at it except for the README. I’m 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 Forsberg’s last public domain ZMODEM
|
||||
code. This is the de facto “reference” implementation, both by virtue
|
||||
of its wide availability and its derivation from Forsberg’s original.
|
||||
If your server has the `rz` and `sz` commands, they’re probably
|
||||
from this package.
|
||||
|
||||
* [SyncTERM](http://syncterm.bbsdev.net)
|
||||
|
||||
Based on Jacques Mattheij’s ZMODEM implementation, originally called
|
||||
zmtx/zmrx. This is a much more readable implementation than lrzsz
|
||||
but lamentably one that doesn’t 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 repository’s 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 there’s demand for it, e.g., in
|
||||
environments where `lrzsz` can’t 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
|
||||
browser’s 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
140
bin/zmodemjs-sz.js
Executable file
|
@ -0,0 +1,140 @@
|
|||
"use strict";
|
||||
|
||||
// A proof-of-concept CLI implementation of “sz” using zmodem.js.
|
||||
// This is not tested extensively and isn’t 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);
|
||||
}
|
||||
|
||||
// Can’t be to the same terminal as STDOUT.
|
||||
// npm’s “ttyname” can tell us, but it’s 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 can’t 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 wasn’t 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 that’s 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
6
index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
Object.assign(
|
||||
module.exports,
|
||||
require("./src/zsentry")
|
||||
);
|
10
jsdoc.json
Normal file
10
jsdoc.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugins": ["plugins/markdown"],
|
||||
"source": {
|
||||
"include": ["README.md", "src"],
|
||||
"includePattern": "\\.js$"
|
||||
},
|
||||
"opts": {
|
||||
"destination": "documentation"
|
||||
}
|
||||
}
|
49
package.json
Normal file
49
package.json
Normal 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
124
src/encode.js
Normal 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) {
|
||||
//Can’t bit-shift because that runs into JS’s 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
33
src/text.js
Normal 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
143
src/zcrc.js
Normal 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
240
src/zdle.js
Normal file
|
@ -0,0 +1,240 @@
|
|||
"use strict";
|
||||
|
||||
var Zmodem = module.exports;
|
||||
|
||||
Object.assign(
|
||||
Zmodem,
|
||||
require("./zmlib")
|
||||
);
|
||||
|
||||
//encode() variables - declare them here so we don’t
|
||||
//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 don’t 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 isn’t 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 isn’t 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) {
|
||||
|
||||
//Don’t 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 doesn’t 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
47
src/zerror.js
Normal 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
763
src/zheader.js
Normal 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 {
|
||||
|
||||
//lrzsz’s “sz” command sends a random (?) CR/0x0d byte
|
||||
//after ZEOF. Let’s accommodate 0x0a, 0x0d, 0x8a, and 0x8d.
|
||||
//
|
||||
//Also, when you skip a file, sz outputs a message about it.
|
||||
//
|
||||
//It appears that we’re supposed to ignore anything until
|
||||
//[ ZPAD, ZDLE ] when we’re looking for a header.
|
||||
|
||||
/**
|
||||
* Weed out the leading bytes that aren’t valid to start a ZMODEM header.
|
||||
*
|
||||
* @param {number[]} ibuffer - The octet values to parse.
|
||||
* Each array member should be an 8-bit unsigned integer (0-255).
|
||||
* This object is mutated in the function.
|
||||
*
|
||||
* @returns {number[]} The octet values that were removed from the start
|
||||
* of “ibuffer”. Order is preserved.
|
||||
*/
|
||||
static trim_leading_garbage(ibuffer) {
|
||||
//Since there’s no escaping of the output it’s possible
|
||||
//that the garbage could trip us up, e.g., by having a filename
|
||||
//be a legit ZMODEM header. But that’s pretty unlikely.
|
||||
|
||||
//Everything up to the first ZPAD: garbage
|
||||
//If first ZPAD has asterisk + ZDLE
|
||||
|
||||
var garbage = [];
|
||||
|
||||
var discard_all, parser, next_ZPAD_at_least = 0;
|
||||
|
||||
TRIM_LOOP:
|
||||
while (ibuffer.length && !parser) {
|
||||
var first_ZPAD = ibuffer.indexOf(ZPAD);
|
||||
|
||||
//No ZPAD? Then we purge the input buffer cuz it’s all garbage.
|
||||
if (first_ZPAD === -1) {
|
||||
discard_all = true;
|
||||
break TRIM_LOOP;
|
||||
}
|
||||
else {
|
||||
garbage.push.apply( garbage, ibuffer.splice(0, first_ZPAD) );
|
||||
|
||||
//buffer has only an asterisk … gotta see about more
|
||||
if (ibuffer.length < 2) {
|
||||
break TRIM_LOOP;
|
||||
}
|
||||
else if (ibuffer[1] === ZPAD) {
|
||||
//Two leading ZPADs should be a hex header.
|
||||
|
||||
//We’re assuming the length of the header is 4 in
|
||||
//this logic … but ZMODEM isn’t likely to change, so.
|
||||
if (ibuffer.length < HEX_HEADER_PREFIX.length) {
|
||||
if (ibuffer.join() === HEX_HEADER_PREFIX.slice(0, ibuffer.length).join()) {
|
||||
//We have an incomplete fragment that matches
|
||||
//HEX_HEADER_PREFIX. So don’t trim any more.
|
||||
break TRIM_LOOP;
|
||||
}
|
||||
|
||||
//Otherwise, we’ll discard one.
|
||||
}
|
||||
else if ((ibuffer[2] === HEX_HEADER_PREFIX[2]) && (ibuffer[3] === HEX_HEADER_PREFIX[3])) {
|
||||
parser = _parse_hex;
|
||||
}
|
||||
}
|
||||
else if (ibuffer[1] === Zmodem.ZMLIB.ZDLE) {
|
||||
//ZPAD + ZDLE should be a binary header.
|
||||
if (ibuffer.length < BINARY16_HEADER_PREFIX.length) {
|
||||
break TRIM_LOOP;
|
||||
}
|
||||
|
||||
if (ibuffer[2] === BINARY16_HEADER_PREFIX[2]) {
|
||||
parser = _parse_binary16;
|
||||
}
|
||||
else if (ibuffer[2] === BINARY32_HEADER_PREFIX[2]) {
|
||||
parser = _parse_binary32;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parser) {
|
||||
garbage.push( ibuffer.shift() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discard_all) {
|
||||
garbage.push.apply( garbage, ibuffer.splice(0) );
|
||||
}
|
||||
|
||||
//For now we’ll throw away the parser.
|
||||
//It’s not hard for parse() to discern anyway.
|
||||
|
||||
return garbage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse out a Header object from a given array of octet values.
|
||||
*
|
||||
* An exception is thrown if the given bytes are definitively invalid
|
||||
* as header values.
|
||||
*
|
||||
* @param {number[]} octets - The octet values to parse.
|
||||
* Each array member should be an 8-bit unsigned integer (0-255).
|
||||
* This object is mutated in the function.
|
||||
*
|
||||
* @returns {Header|undefined} An instance of the appropriate Header
|
||||
* subclass, or undefined if not enough octet values are given
|
||||
* to determine whether there is a valid header here or not.
|
||||
*/
|
||||
static parse(octets) {
|
||||
var hdr;
|
||||
if (octets[1] === ZPAD) {
|
||||
hdr = _parse_hex(octets);
|
||||
return hdr && [ hdr, 16 ];
|
||||
}
|
||||
|
||||
else if (octets[2] === ZBIN) {
|
||||
hdr = _parse_binary16(octets, 3);
|
||||
return hdr && [ hdr, 16 ];
|
||||
}
|
||||
|
||||
else if (octets[2] === ZBIN32) {
|
||||
hdr = _parse_binary32(octets);
|
||||
return hdr && [ hdr, 32 ];
|
||||
}
|
||||
|
||||
if (octets.length < 3) return;
|
||||
|
||||
throw( "Unrecognized/unsupported octets: " + octets.join() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Header subclass given a name and arguments.
|
||||
*
|
||||
* @param {string} name - The header type name, e.g., “ZRINIT”.
|
||||
*
|
||||
* @param {...*} args - The arguments to pass to the appropriate
|
||||
* subclass constructor. These aren’t documented currently
|
||||
* but are pretty easy to glean from the code.
|
||||
*
|
||||
* @returns {Header} An instance of the appropriate Header subclass.
|
||||
*/
|
||||
static build(name /*, args */) {
|
||||
var args = (arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments));
|
||||
|
||||
//TODO: make this better
|
||||
var Ctr = FRAME_NAME_CREATOR[name];
|
||||
if (!Ctr) throw("No frame class “" + name + "” is defined!");
|
||||
|
||||
args.shift();
|
||||
|
||||
//Plegh!
|
||||
//https://stackoverflow.com/questions/33193310/constr-applythis-args-in-es6-classes
|
||||
var hdr = new (Ctr.bind.apply(Ctr, [null].concat(args)));
|
||||
|
||||
return hdr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the octet values array that represents the object
|
||||
* in ZMODEM hex encoding.
|
||||
*
|
||||
* @returns {number[]} An array of octet values suitable for sending
|
||||
* as binary data.
|
||||
*/
|
||||
to_hex() {
|
||||
var to_crc = this._crc_bytes();
|
||||
|
||||
return HEX_HEADER_PREFIX.concat(
|
||||
Zmodem.ENCODELIB.octets_to_hex( to_crc.concat( Zmodem.CRC.crc16(to_crc) ) ),
|
||||
this._hex_header_ending
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the octet values array that represents the object
|
||||
* in ZMODEM binary encoding with a 16-bit CRC.
|
||||
*
|
||||
* @param {ZDLE} zencoder - A ZDLE instance to use for
|
||||
* ZDLE encoding.
|
||||
*
|
||||
* @returns {number[]} An array of octet values suitable for sending
|
||||
* as binary data.
|
||||
*/
|
||||
to_binary16(zencoder) {
|
||||
return this._to_binary(zencoder, BINARY16_HEADER_PREFIX, Zmodem.CRC.crc16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the octet values array that represents the object
|
||||
* in ZMODEM binary encoding with a 32-bit CRC.
|
||||
*
|
||||
* @param {ZDLE} zencoder - A ZDLE instance to use for
|
||||
* ZDLE encoding.
|
||||
*
|
||||
* @returns {number[]} An array of octet values suitable for sending
|
||||
* as binary data.
|
||||
*/
|
||||
to_binary32(zencoder) {
|
||||
return this._to_binary(zencoder, BINARY32_HEADER_PREFIX, Zmodem.CRC.crc32);
|
||||
}
|
||||
|
||||
//This is never called directly, but only as super().
|
||||
constructor() {
|
||||
if (!this._bytes4) {
|
||||
this._bytes4 = [0, 0, 0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
_to_binary(zencoder, prefix, crc_func) {
|
||||
var to_crc = this._crc_bytes();
|
||||
|
||||
//Both the 4-byte payload and the CRC bytes are ZDLE-encoded.
|
||||
var octets = prefix.concat(
|
||||
zencoder.encode( to_crc.concat( crc_func(to_crc) ) )
|
||||
);
|
||||
|
||||
return octets;
|
||||
}
|
||||
|
||||
_crc_bytes() {
|
||||
return [ this.TYPENUM ].concat(this._bytes4);
|
||||
}
|
||||
}
|
||||
Zmodem.Header.prototype._hex_header_ending = HEX_HEADER_CRLF_XON;
|
||||
|
||||
class ZRQINIT_HEADER extends Zmodem.Header {};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
const ZRINIT_FLAG = {
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Bit Masks for ZRINIT flags byte ZF0
|
||||
//----------------------------------------------------------------------
|
||||
CANFDX: 0x01, // Rx can send and receive true FDX
|
||||
CANOVIO: 0x02, // Rx can receive data during disk I/O
|
||||
CANBRK: 0x04, // Rx can send a break signal
|
||||
CANCRY: 0x08, // Receiver can decrypt -- nothing does this
|
||||
CANLZW: 0x10, // Receiver can uncompress -- nothing does this
|
||||
CANFC32: 0x20, // Receiver can use 32 bit Frame Check
|
||||
ESCCTL: 0x40, // Receiver expects ctl chars to be escaped
|
||||
ESC8: 0x80, // Receiver expects 8th bit to be escaped
|
||||
};
|
||||
|
||||
function _get_ZRINIT_flag_num(fl) {
|
||||
if (!ZRINIT_FLAG[fl]) {
|
||||
throw new Zmodem.Error("Invalid ZRINIT flag: " + fl);
|
||||
}
|
||||
return ZRINIT_FLAG[fl];
|
||||
}
|
||||
|
||||
class ZRINIT_HEADER extends Zmodem.Header {
|
||||
constructor(flags_arr, bufsize) {
|
||||
super();
|
||||
var flags_num = 0;
|
||||
if (!bufsize) bufsize = 0;
|
||||
|
||||
flags_arr.forEach( function(fl) {
|
||||
flags_num |= _get_ZRINIT_flag_num(fl);
|
||||
} );
|
||||
|
||||
this._bytes4 = [
|
||||
bufsize & 0xff,
|
||||
bufsize >> 8,
|
||||
0,
|
||||
flags_num,
|
||||
];
|
||||
}
|
||||
|
||||
//undefined if nonstop I/O is allowed
|
||||
get_buffer_size() {
|
||||
return Zmodem.ENCODELIB.unpack_u16_be( this._bytes4.slice(0, 2) ) || undefined;
|
||||
}
|
||||
|
||||
//Unimplemented:
|
||||
// can_decrypt
|
||||
// can_decompress
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
//function names taken from Jacques Mattheij’s implementation,
|
||||
//as used in syncterm.
|
||||
|
||||
can_full_duplex() {
|
||||
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFDX );
|
||||
}
|
||||
|
||||
can_overlap_io() {
|
||||
return !!( this._bytes4[3] & ZRINIT_FLAG.CANOVIO );
|
||||
}
|
||||
|
||||
can_break() {
|
||||
return !!( this._bytes4[3] & ZRINIT_FLAG.CANBRK );
|
||||
}
|
||||
|
||||
can_fcs_32() {
|
||||
return !!( this._bytes4[3] & ZRINIT_FLAG.CANFC32 );
|
||||
}
|
||||
|
||||
escape_ctrl_chars() {
|
||||
return !!( this._bytes4[3] & ZRINIT_FLAG.ESCCTL );
|
||||
}
|
||||
|
||||
//Is this used? I don’t see it used in lrzsz or syncterm
|
||||
//Looks like it was a “foreseen” feature that Forsberg
|
||||
//never implemented. (The need for it went away, maybe?)
|
||||
escape_8th_bit() {
|
||||
return !!( this._bytes4[3] & ZRINIT_FLAG.ESC8 );
|
||||
}
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
//Since context makes clear what’s going on, we use these
|
||||
//rather than the T-prefixed constants in the specification.
|
||||
const ZSINIT_FLAG = {
|
||||
ESCCTL: 0x40, // Transmitter will escape ctl chars
|
||||
ESC8: 0x80, // Transmitter will escape 8th bit
|
||||
};
|
||||
|
||||
function _get_ZSINIT_flag_num(fl) {
|
||||
if (!ZSINIT_FLAG[fl]) {
|
||||
throw("Invalid ZSINIT flag: " + fl);
|
||||
}
|
||||
return ZSINIT_FLAG[fl];
|
||||
}
|
||||
|
||||
class ZSINIT_HEADER extends Zmodem.Header {
|
||||
constructor( flags_arr, attn_seq_arr ) {
|
||||
super();
|
||||
var flags_num = 0;
|
||||
|
||||
flags_arr.forEach( function(fl) {
|
||||
flags_num |= _get_ZSINIT_flag_num(fl);
|
||||
} );
|
||||
|
||||
this._bytes4 = [ 0, 0, 0, flags_num ];
|
||||
|
||||
if (attn_seq_arr) {
|
||||
if (attn_seq_arr.length > 31) {
|
||||
throw("Attn sequence must be <= 31 bytes");
|
||||
}
|
||||
if (attn_seq_arr.some( function(num) { return num > 255 } )) {
|
||||
throw("Attn sequence (" + attn_seq_arr + ") must be <256");
|
||||
}
|
||||
this._data = attn_seq_arr.concat([0]);
|
||||
}
|
||||
}
|
||||
|
||||
escape_ctrl_chars() {
|
||||
return !!( this._bytes4[3] & ZSINIT_FLAG.ESCCTL );
|
||||
}
|
||||
|
||||
//Is this used? I don’t see it used in lrzsz or syncterm
|
||||
escape_8th_bit() {
|
||||
return !!( this._bytes4[3] & ZSINIT_FLAG.ESC8 );
|
||||
}
|
||||
}
|
||||
|
||||
//Thus far it doesn’t seem we really need this header except to respond
|
||||
//to ZSINIT, which doesn’t require a payload.
|
||||
class ZACK_HEADER extends Zmodem.Header {
|
||||
constructor(payload4) {
|
||||
super();
|
||||
|
||||
if (payload4) {
|
||||
this._bytes4 = payload4.slice();
|
||||
}
|
||||
}
|
||||
}
|
||||
ZACK_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
const ZFILE_VALUES = {
|
||||
|
||||
//ZF3 (i.e., first byte)
|
||||
extended: {
|
||||
sparse: 0x40, //ZXSPARS
|
||||
},
|
||||
|
||||
//ZF2
|
||||
transport: [
|
||||
undefined,
|
||||
"compress", //ZTLZW
|
||||
"encrypt", //ZTCRYPT
|
||||
"rle", //ZTRLE
|
||||
],
|
||||
|
||||
//ZF1
|
||||
management: [
|
||||
undefined,
|
||||
"newer_or_longer", //ZF1_ZMNEWL
|
||||
"crc", //ZF1_ZMCRC
|
||||
"append", //ZF1_ZMAPND
|
||||
"clobber", //ZF1_ZMCLOB
|
||||
"newer", //ZF1_ZMNEW
|
||||
"mtime_or_length", //ZF1_ZMNEW
|
||||
"protect", //ZF1_ZMPROT
|
||||
"rename", //ZF1_ZMPROT
|
||||
],
|
||||
|
||||
//ZF0 (i.e., last byte)
|
||||
conversion: [
|
||||
undefined,
|
||||
"binary", //ZCBIN
|
||||
"text", //ZCNL
|
||||
"resume", //ZCRESUM
|
||||
],
|
||||
};
|
||||
|
||||
const ZFILE_ORDER = ["extended", "transport", "management", "conversion"];
|
||||
|
||||
const ZMSKNOLOC = 0x80,
|
||||
MANAGEMENT_MASK = 0x1f,
|
||||
ZXSPARS = 0x40
|
||||
;
|
||||
|
||||
class ZFILE_HEADER extends Zmodem.Header {
|
||||
|
||||
//TODO: allow options on instantiation
|
||||
get_options() {
|
||||
var opts = {
|
||||
sparse: !!(this._bytes4[0] & ZXSPARS),
|
||||
};
|
||||
|
||||
var bytes_copy = this._bytes4.slice(0);
|
||||
|
||||
ZFILE_ORDER.forEach( function(key, i) {
|
||||
if (ZFILE_VALUES[key] instanceof Array) {
|
||||
if (key === "management") {
|
||||
opts.skip_if_absent = !!(bytes_copy[i] & ZMSKNOLOC);
|
||||
bytes_copy[i] &= MANAGEMENT_MASK;
|
||||
}
|
||||
|
||||
opts[key] = ZFILE_VALUES[key][ bytes_copy[i] ];
|
||||
}
|
||||
else {
|
||||
for (var extkey in ZFILE_VALUES[key]) {
|
||||
opts[extkey] = !!(bytes_copy[i] & ZFILE_VALUES[key][extkey]);
|
||||
if (opts[extkey]) {
|
||||
bytes_copy[i] ^= ZFILE_VALUES[key][extkey]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts[key] && bytes_copy[i]) {
|
||||
opts[key] = "unknown:" + bytes_copy[i];
|
||||
}
|
||||
} );
|
||||
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
//Empty headers - in addition to ZRQINIT
|
||||
class ZSKIP_HEADER extends Zmodem.Header {}
|
||||
//No need for ZNAK
|
||||
class ZABORT_HEADER extends Zmodem.Header {}
|
||||
class ZFIN_HEADER extends Zmodem.Header {}
|
||||
class ZFERR_HEADER extends Zmodem.Header {}
|
||||
|
||||
ZFIN_HEADER.prototype._hex_header_ending = HEX_HEADER_CRLF;
|
||||
|
||||
class ZOffsetHeader extends Zmodem.Header {
|
||||
constructor(offset) {
|
||||
super();
|
||||
this._bytes4 = Zmodem.ENCODELIB.pack_u32_le(offset);
|
||||
}
|
||||
|
||||
get_offset() {
|
||||
return Zmodem.ENCODELIB.unpack_u32_le(this._bytes4);
|
||||
}
|
||||
}
|
||||
|
||||
class ZRPOS_HEADER extends ZOffsetHeader {};
|
||||
class ZDATA_HEADER extends ZOffsetHeader {};
|
||||
class ZEOF_HEADER extends ZOffsetHeader {};
|
||||
|
||||
//As request, receiver creates.
|
||||
/* UNIMPLEMENTED FOR NOW
|
||||
class ZCRC_HEADER extends ZHeader {
|
||||
constructor(crc_le_bytes) {
|
||||
super();
|
||||
if (crc_le_bytes) { //response, sender creates
|
||||
this._bytes4 = crc_le_bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//No ZCHALLENGE implementation
|
||||
|
||||
//class ZCOMPL_HEADER extends ZHeader {}
|
||||
//class ZCAN_HEADER extends Zmodem.Header {}
|
||||
|
||||
//As described, this header represents an information disclosure.
|
||||
//It could be interpreted, I suppose, merely as “this is how much space
|
||||
//I have FOR YOU.”
|
||||
//TODO: implement if needed/requested
|
||||
//class ZFREECNT_HEADER extends ZmodemHeader {}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
const FRAME_CLASS_TYPES = [
|
||||
[ ZRQINIT_HEADER, "ZRQINIT" ],
|
||||
[ ZRINIT_HEADER, "ZRINIT" ],
|
||||
[ ZSINIT_HEADER, "ZSINIT" ],
|
||||
[ ZACK_HEADER, "ZACK" ],
|
||||
[ ZFILE_HEADER, "ZFILE" ],
|
||||
[ ZSKIP_HEADER, "ZSKIP" ],
|
||||
undefined, // [ ZNAK_HEADER, "ZNAK" ],
|
||||
[ ZABORT_HEADER, "ZABORT" ],
|
||||
[ ZFIN_HEADER, "ZFIN" ],
|
||||
[ ZRPOS_HEADER, "ZRPOS" ],
|
||||
[ ZDATA_HEADER, "ZDATA" ],
|
||||
[ ZEOF_HEADER, "ZEOF" ],
|
||||
[ ZFERR_HEADER, "ZFERR" ], //see note
|
||||
undefined, //[ ZCRC_HEADER, "ZCRC" ],
|
||||
undefined, //[ ZCHALLENGE_HEADER, "ZCHALLENGE" ],
|
||||
undefined, //[ ZCOMPL_HEADER, "ZCOMPL" ],
|
||||
undefined, //[ ZCAN_HEADER, "ZCAN" ],
|
||||
undefined, //[ ZFREECNT_HEADER, "ZFREECNT" ],
|
||||
undefined, //[ ZCOMMAND_HEADER, "ZCOMMAND" ],
|
||||
undefined, //[ ZSTDERR_HEADER, "ZSTDERR" ],
|
||||
];
|
||||
|
||||
/*
|
||||
ZFERR is described as “error in reading or writing file”. It’s really
|
||||
not a good idea from a security angle for the endpoint to expose this
|
||||
information. We should parse this and handle it as ZABORT but never send it.
|
||||
|
||||
Likewise with ZFREECNT: the sender shouldn’t ask how much space is left
|
||||
on the other box; rather, the receiver should decide what to do with the
|
||||
file size as the sender reports it.
|
||||
*/
|
||||
|
||||
var FRAME_NAME_CREATOR = {};
|
||||
|
||||
for (var fc=0; fc<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 doesn’t have CR!";
|
||||
}
|
||||
}
|
||||
else if ( hex_bytes.length !== 18 ) {
|
||||
hdr_err = "Invalid hex header: invalid number of bytes before LF!";
|
||||
}
|
||||
}
|
||||
|
||||
if (hdr_err) {
|
||||
hdr_err += " (" + hex_bytes.length + " bytes: " + hex_bytes.join() + ")";
|
||||
throw hdr_err;
|
||||
}
|
||||
|
||||
hex_bytes.splice(0, 4);
|
||||
|
||||
//Should be 7 bytes ultimately:
|
||||
// 1 for typenum
|
||||
// 4 for header data
|
||||
// 2 for CRC
|
||||
var octets = Zmodem.ENCODELIB.parse_hex_octets(hex_bytes);
|
||||
|
||||
return _parse_non_zdle_binary16(octets);
|
||||
}
|
||||
|
||||
Zmodem.Header.parse_hex = _parse_hex;
|
102
src/zmlib.js
Normal file
102
src/zmlib.js
Normal 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 - ZMODEM’s 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 appears—or, -1 if “needle” doesn’t 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
4
src/zmodem.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
Object.assign(
|
||||
module.exports,
|
||||
require("./zsentry"),
|
||||
);
|
182
src/zmodem_browser.js
Normal file
182
src/zmodem_browser.js
Normal 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 it’s 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 shouldn’t happen … so let’s
|
||||
//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) don’t.
|
||||
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 we’re done, or with another file-send promise
|
||||
//if there’s 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 element’s `click()`
|
||||
* method. The element is removed immediately after.
|
||||
*
|
||||
* @param {Array} packets - Same as the first argument to [Blob’s 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;
|
||||
//I’d 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
394
src/zsentry.js
Normal 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'
|
||||
//ZRQINIT’s next byte will be '0'; ZRINIT’s 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 Sentry’s 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 Sentry’s `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 doesn’t end in error. That’s
|
||||
//possible if we’re the sender, we accept the session,
|
||||
//then we just send a close(), but it doesn’t seem to be
|
||||
//possible for a receiver. Thus, let’s just leave it so
|
||||
//it’s 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 session’s 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
|
||||
* it’s 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 wouldn’t 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. That’ll be one less step
|
||||
* for the user, but an unaccustomed user might find that a bit
|
||||
* confusing. It’s also then possible to have a “false positive”:
|
||||
* a text stream that contains a ZMODEM initialization string but
|
||||
* isn’t, 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 user’s chance to say, “yeah, I know those
|
||||
* bytes look like ZMODEM, but they’re not. So back off!”
|
||||
*
|
||||
* If you .confirm(), the Session object is returned, and
|
||||
* further input that goes to the Sentry’s .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 it’s 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 we’ve 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 won’t 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 sentry’s 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. They’re 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;
|
||||
|
||||
//Don’t 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 don’t 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
1677
src/zsession.js
Normal file
File diff suppressed because it is too large
Load diff
241
src/zsubpacket.js
Normal file
241
src/zsubpacket.js
Normal 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 payload’s 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 won’t need the Subpacket anymore, but
|
||||
* just be careful.
|
||||
*
|
||||
* @returns {number[]} The subpacket’s 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, we’ll 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 they’re 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 shouldn’t need to do this, but just for good measure.
|
||||
//I suppose it’s 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
130
src/zvalidation.js
Normal 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 it’s
|
||||
* 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 doesn’t 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
119
tests/encode.js
Executable 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
121
tests/lib/testhelp.js
Normal 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 can’t just pipe this on through because there can be lone CR
|
||||
//bytes which screw up TAP::Harness.
|
||||
child.stderr.on("data", (d) => {
|
||||
d = d.toString().replace(/\r\n?/g, "\n");
|
||||
if (d.substr(-1) !== "\n") d += "\n";
|
||||
process.stderr.write(`STDERR: ${d}`);
|
||||
});
|
||||
|
||||
child.stdout.on("data", (d) => {
|
||||
//console.log(`STDOUT from PID ${child.pid}`, d);
|
||||
inputs.push( Array.from(d) );
|
||||
|
||||
zsentry.consume( Array.from(d) );
|
||||
|
||||
if (zsession) {
|
||||
if ( steps[step] ) {
|
||||
if ( steps[step](zsession, child) ) {
|
||||
step++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`End of task list; closing PID ${child.pid}’s STDIN`);
|
||||
child.stdin.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var exit_promise = new Promise( (res, rej) => {
|
||||
child.on("exit", (code, signal) => {
|
||||
console.log(`# "${binpath}" exit: code ${code}, signal ${signal}`);
|
||||
res([code, signal]);
|
||||
} );
|
||||
} );
|
||||
|
||||
return exit_promise.then( () => { return inputs } );
|
||||
},
|
||||
};
|
1
tests/lib/zmodem.js
Normal file
1
tests/lib/zmodem.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('../../src/zmodem.js');
|
45
tests/text.js
Executable file
45
tests/text.js
Executable 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
113
tests/zcrc.js
Executable 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 don’t
|
||||
// advertise it.
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
tape('crc32', function(t) {
|
||||
const tests = [
|
||||
[ [ 4, 0, 0, 0, 0 ], [ 0xdd, 0x51, 0xa2, 0x33 ] ],
|
||||
[ [ 11, 17, 0, 0, 0 ], [ 0xf6, 0xf6, 0x57, 0x59 ] ],
|
||||
[ [ 3, 0, 0, 0, 0 ], [ 205, 141, 130, 129 ] ],
|
||||
];
|
||||
// } [ 3, 0, 0, 0, 0 ] [ 205, 141, 131, -127 ]
|
||||
//2172816845
|
||||
//crc32 [ 3, 0, 0, 0, 0 ] -2122150451
|
||||
|
||||
tests.forEach( (cur_t) => {
|
||||
let [ input, output ] = cur_t;
|
||||
|
||||
t.deepEqual(
|
||||
zcrc.crc32(input),
|
||||
output,
|
||||
"crc32: " + input.join(", ")
|
||||
);
|
||||
} );
|
||||
|
||||
t.end();
|
||||
} );
|
||||
|
||||
tape('verify32', function(t) {
|
||||
t.doesNotThrow(
|
||||
() => zcrc.verify32( [ 4, 0, 0, 0, 0 ], [ 0xdd, 0x51, 0xa2, 0x33 ] ),
|
||||
'verify32 - no throw on good'
|
||||
);
|
||||
|
||||
var err;
|
||||
try { zcrc.verify32( [ 4, 0, 0, 0, 0 ], [ 1,2,3,4 ] ) }
|
||||
catch(e) { err = e };
|
||||
|
||||
t.ok(
|
||||
/1,2,3,4.*221,81,162,51/.test(err.message),
|
||||
'verify32 - throw on bad (message)'
|
||||
);
|
||||
|
||||
t.ok(
|
||||
err instanceof Zmodem.Error,
|
||||
'verify32 - typed error'
|
||||
);
|
||||
|
||||
t.ok(
|
||||
err.type,
|
||||
'verify32 - error type'
|
||||
);
|
||||
|
||||
t.end();
|
||||
} );
|
41
tests/zdle.js
Executable file
41
tests/zdle.js
Executable 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
82
tests/zerror.js
Normal 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
309
tests/zheader.js
Executable 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, //it’s little-endian
|
||||
'parsed offset'
|
||||
);
|
||||
|
||||
t.end();
|
||||
} );
|
||||
|
||||
tape('round-trip, empty headers', function(t) {
|
||||
["ZRQINIT", "ZSKIP", "ZABORT", "ZFIN", "ZFERR"].forEach( (n) => {
|
||||
var orig = Zmodem.Header.build(n);
|
||||
|
||||
var hex = orig.to_hex();
|
||||
var b16 = orig.to_binary16(zdle);
|
||||
var b32 = orig.to_binary32(zdle);
|
||||
|
||||
var rounds = new Map( [
|
||||
[ "to_hex", hex ],
|
||||
[ "to_binary16", b16 ],
|
||||
[ "to_binary32", b32 ],
|
||||
] );
|
||||
|
||||
for ( const [ enc, h ] of rounds ) {
|
||||
let [ parsed, crclen ] = Zmodem.Header.parse(h);
|
||||
|
||||
t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
|
||||
t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );
|
||||
|
||||
//Here’s where we test the CRC length in the response.
|
||||
t.is(
|
||||
crclen,
|
||||
/32/.test(enc) ? 32 : 16,
|
||||
`${n}, ${enc}: CRC length`,
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
t.end();
|
||||
} );
|
||||
|
||||
tape('round-trip, offset headers', function(t) {
|
||||
["ZRPOS", "ZDATA", "ZEOF"].forEach( (n) => {
|
||||
var orig = Zmodem.Header.build(n, 12345);
|
||||
|
||||
var hex = orig.to_hex();
|
||||
var b16 = orig.to_binary16(zdle);
|
||||
var b32 = orig.to_binary32(zdle);
|
||||
|
||||
var rounds = new Map( [
|
||||
[ "to_hex", hex ],
|
||||
[ "to_binary16", b16 ],
|
||||
[ "to_binary32", b32 ],
|
||||
] );
|
||||
|
||||
for ( const [ enc, h ] of rounds ) {
|
||||
//Here’s where we test that parse() leaves in trailing bytes.
|
||||
let extra = [99, 99, 99];
|
||||
let bytes_with_extra = h.slice().concat(extra);
|
||||
|
||||
let parsed = Zmodem.Header.parse(bytes_with_extra)[0];
|
||||
|
||||
t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
|
||||
t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );
|
||||
t.is( parsed.get_offset(), orig.get_offset(), `${n}, ${enc}: get_offset()` );
|
||||
|
||||
let expected = extra.slice(0);
|
||||
if (enc === "to_hex") {
|
||||
expected.splice( 0, 0, Zmodem.ZMLIB.XON );
|
||||
}
|
||||
|
||||
t.deepEquals(
|
||||
bytes_with_extra,
|
||||
expected,
|
||||
`${enc}: parse() leaves in trailing bytes`,
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
t.end();
|
||||
} );
|
||||
|
||||
tape('round-trip, ZSINIT', function(t) {
|
||||
var opts = [
|
||||
[],
|
||||
["ESCCTL"],
|
||||
];
|
||||
|
||||
opts.forEach( (args) => {
|
||||
var orig = Zmodem.Header.build("ZSINIT", args);
|
||||
|
||||
var hex = orig.to_hex();
|
||||
var b16 = orig.to_binary16(zdle);
|
||||
var b32 = orig.to_binary32(zdle);
|
||||
|
||||
var rounds = new Map( [
|
||||
[ "to_hex", hex ],
|
||||
[ "to_binary16", b16 ],
|
||||
[ "to_binary32", b32 ],
|
||||
] );
|
||||
|
||||
var args_str = JSON.stringify(args);
|
||||
|
||||
for ( const [ enc, h ] of rounds ) {
|
||||
let parsed = Zmodem.Header.parse(h)[0];
|
||||
|
||||
t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
|
||||
t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );
|
||||
|
||||
t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
|
||||
t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
|
||||
}
|
||||
} );
|
||||
|
||||
t.end();
|
||||
} );
|
||||
|
||||
tape('round-trip, ZRINIT', function(t) {
|
||||
var opts = [];
|
||||
|
||||
[ [], ["CANFDX"] ].forEach( (canfdx) => {
|
||||
[ [], ["CANOVIO"] ].forEach( (canovio) => {
|
||||
[ [], ["CANBRK"] ].forEach( (canbrk) => {
|
||||
[ [], ["CANFC32"] ].forEach( (canfc32) => {
|
||||
[ [], ["ESCCTL"] ].forEach( (escctl) => {
|
||||
opts.push( [
|
||||
...canfdx,
|
||||
...canovio,
|
||||
...canbrk,
|
||||
...canfc32,
|
||||
...escctl,
|
||||
] );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
opts.forEach( (args) => {
|
||||
var orig = Zmodem.Header.build("ZRINIT", args);
|
||||
|
||||
var hex = orig.to_hex();
|
||||
var b16 = orig.to_binary16(zdle);
|
||||
var b32 = orig.to_binary32(zdle);
|
||||
|
||||
var rounds = new Map( [
|
||||
[ "to_hex", hex ],
|
||||
[ "to_binary16", b16 ],
|
||||
[ "to_binary32", b32 ],
|
||||
] );
|
||||
|
||||
var args_str = JSON.stringify(args);
|
||||
|
||||
for ( const [ enc, h ] of rounds ) {
|
||||
let parsed = Zmodem.Header.parse(h)[0];
|
||||
|
||||
t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
|
||||
t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );
|
||||
|
||||
t.is( parsed.can_full_duplex(), orig.can_full_duplex(), `opts ${args_str}: ${enc}: can_full_duplex()` );
|
||||
t.is( parsed.can_overlap_io(), orig.can_overlap_io(), `opts ${args_str}: ${enc}: can_overlap_io()` );
|
||||
t.is( parsed.can_break(), orig.can_break(), `opts ${args_str}: ${enc}: can_break()` );
|
||||
t.is( parsed.can_fcs_32(), orig.can_fcs_32(), `opts ${args_str}: ${enc}: can_fcs_32()` );
|
||||
t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
|
||||
t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
|
||||
}
|
||||
} );
|
||||
|
||||
t.end();
|
||||
} );
|
||||
|
||||
tape('hex_final_XON', function(t) {
|
||||
var hex_ZFIN = Zmodem.Header.build("ZFIN").to_hex();
|
||||
|
||||
t.notEquals(
|
||||
hex_ZFIN.slice(-1)[0],
|
||||
Zmodem.ZMLIB.XON,
|
||||
'ZFIN hex does NOT end with XON',
|
||||
);
|
||||
|
||||
var hex_ZACK = Zmodem.Header.build("ZACK").to_hex();
|
||||
|
||||
t.notEquals(
|
||||
hex_ZACK.slice(-1)[0],
|
||||
Zmodem.ZMLIB.XON,
|
||||
'ZACK hex does NOT end with XON',
|
||||
);
|
||||
|
||||
var headers = [
|
||||
"ZRQINIT",
|
||||
Zmodem.Header.build("ZRINIT", []),
|
||||
Zmodem.Header.build("ZSINIT", []),
|
||||
"ZRPOS",
|
||||
"ZABORT",
|
||||
"ZFERR",
|
||||
];
|
||||
|
||||
//These are the only headers we expect to send as hex … right?
|
||||
headers.forEach( hdr => {
|
||||
if (typeof hdr === "string") hdr = Zmodem.Header.build(hdr);
|
||||
|
||||
t.is(
|
||||
hdr.to_hex().slice(-1)[0],
|
||||
Zmodem.ZMLIB.XON,
|
||||
`${hdr.NAME} hex ends with XON`
|
||||
);
|
||||
} );
|
||||
|
||||
t.end();
|
||||
} );
|
81
tests/zmlib.js
Executable file
81
tests/zmlib.js
Executable 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
226
tests/zsentry.js
Executable 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
312
tests/zsession.js
Executable 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 file’s name" );
|
||||
var next_pms = offer.skip();
|
||||
//console.log("next", next_pms);
|
||||
return next_pms;
|
||||
} ).then( (offer) => {
|
||||
t.equals( offer.get_details().name, "file 2", "second file’s name" );
|
||||
return offer.skip();
|
||||
} );
|
||||
|
||||
var s_pms = sender.send_offer(
|
||||
{ name: "my file" }
|
||||
).then(
|
||||
(sender_xfer) => {
|
||||
t.ok( !sender_xfer, "skip() -> sender sees no transfer object" );
|
||||
return sender.send_offer( { name: "file 2" } );
|
||||
}
|
||||
).then(
|
||||
(xfer) => {
|
||||
t.ok( !xfer, "2nd skip() -> sender sees no transfer object" );
|
||||
return sender.close();
|
||||
}
|
||||
);
|
||||
|
||||
return Promise.all( [ r_pms, s_pms ] );
|
||||
} );
|
||||
|
||||
test('abort mid-download', (t) => {
|
||||
_init();
|
||||
|
||||
var transferred_bytes = [];
|
||||
|
||||
var aborted;
|
||||
|
||||
var r_pms = receiver.start().then( (offer) => {
|
||||
offer.on("input", (payload) => {
|
||||
[].push.apply(transferred_bytes, payload);
|
||||
|
||||
if (aborted) throw "already aborted!";
|
||||
aborted = true;
|
||||
|
||||
receiver.abort();
|
||||
});
|
||||
return offer.accept();
|
||||
} );
|
||||
|
||||
var s_pms = sender.send_offer(
|
||||
{ name: "my file" }
|
||||
).then(
|
||||
(xfer) => {
|
||||
xfer.send( [1, 2, 3] );
|
||||
xfer.end( [99, 99, 99] ); //should never get here
|
||||
}
|
||||
);
|
||||
|
||||
return Promise.all( [r_pms, s_pms] ).catch(
|
||||
(err) => {
|
||||
t.ok( err.message.match('abort'), 'error message is about abort' );
|
||||
}
|
||||
).then( () => {
|
||||
t.deepEquals(
|
||||
transferred_bytes,
|
||||
[1, 2, 3],
|
||||
'abort() stopped us from sending more',
|
||||
);
|
||||
} );
|
||||
} );
|
295
tests/zsession_receive.js
Executable file
295
tests/zsession_receive.js
Executable 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 doesn’t work because we automatically send ZFIN once we receive it,
|
||||
//which prompts the child to finish up.
|
||||
tape.skip("abort() after ZEOF", (t) => {
|
||||
var received;
|
||||
|
||||
return _test_steps( t, [FILE1], [
|
||||
(zsession) => {
|
||||
zsession.on("offer", (offer) => {
|
||||
offer.accept().then( () => { received = true } );
|
||||
} );
|
||||
zsession.start();
|
||||
return true;
|
||||
},
|
||||
(zsession) => {
|
||||
if (received) {
|
||||
zsession.abort();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
] ).then( (inputs) => {
|
||||
var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
|
||||
t.is( str, "OO", "successful close despite abort" );
|
||||
} );
|
||||
});
|
248
tests/zsession_send.js
Executable file
248
tests/zsession_send.js
Executable 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 sailor’s life for me!",
|
||||
];
|
||||
|
||||
var text_encoder = require('text-encoding').TextEncoder;
|
||||
text_encoder = new text_encoder();
|
||||
|
||||
function _send_batch(t, batch, on_offer) {
|
||||
batch = batch.slice(0);
|
||||
|
||||
return helper.exec_lrzsz_steps( t, RZ_PATH, [], [
|
||||
(zsession, child) => {
|
||||
function offer_sender() {
|
||||
if (!batch.length) {
|
||||
zsession.close();
|
||||
return; //batch finished
|
||||
}
|
||||
|
||||
return zsession.send_offer(
|
||||
batch[0][0]
|
||||
).then( (xfer) => {
|
||||
if (on_offer) {
|
||||
on_offer(xfer, batch[0]);
|
||||
}
|
||||
|
||||
let file_contents = batch.shift()[1];
|
||||
|
||||
var octets;
|
||||
if ("string" === typeof file_contents) {
|
||||
octets = text_encoder.encode(file_contents);
|
||||
}
|
||||
else {
|
||||
octets = file_contents; // Buffer
|
||||
}
|
||||
|
||||
return xfer && xfer.end( Array.from(octets) );
|
||||
} ).then( offer_sender );
|
||||
}
|
||||
|
||||
return offer_sender();
|
||||
},
|
||||
(zsession, child) => {
|
||||
return zsession.has_ended();
|
||||
},
|
||||
] );
|
||||
}
|
||||
|
||||
function _do_in_temp_dir( todo ) {
|
||||
var ret;
|
||||
|
||||
process.chdir( helper.make_temp_dir() );
|
||||
|
||||
try {
|
||||
ret = todo();
|
||||
}
|
||||
catch(e) {
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
if (!ret) {
|
||||
process.chdir( dir_before );
|
||||
}
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
ret = ret.then( () => process.chdir( dir_before ) );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
tape("rz accepts one, then skips next", (t) => {
|
||||
return _do_in_temp_dir( () => {
|
||||
let filename = "no-clobberage";
|
||||
|
||||
var batch = [
|
||||
[
|
||||
{ name: filename },
|
||||
"the first",
|
||||
],
|
||||
[
|
||||
{ name: filename },
|
||||
"the second",
|
||||
],
|
||||
];
|
||||
|
||||
var offers = [];
|
||||
function offer_cb(xfer, batch_item) {
|
||||
offers.push( xfer );
|
||||
}
|
||||
|
||||
return _send_batch(t, batch, offer_cb).then( () => {
|
||||
var got_contents = fs.readFileSync(filename, "utf-8");
|
||||
t.equals( got_contents, "the first", 'second offer was rejected' );
|
||||
|
||||
t.notEquals( offers[0], undefined, 'got an offer at first' );
|
||||
t.equals( offers[1], undefined, '… but no offer second' );
|
||||
} );
|
||||
} );
|
||||
});
|
||||
|
||||
tape("send batch", (t) => {
|
||||
return _do_in_temp_dir( () => {
|
||||
var string_num = 0;
|
||||
|
||||
var base = "batch_";
|
||||
var mtime_1990 = new Date("1990-01-01T00:00:00Z");
|
||||
|
||||
var batch = TEST_STRINGS.map( (str, i) => {
|
||||
return [
|
||||
{
|
||||
name: base + i,
|
||||
mtime: mtime_1990,
|
||||
},
|
||||
str,
|
||||
];
|
||||
} );
|
||||
|
||||
return _send_batch(t, batch).then( () => {
|
||||
for (var sn=0; sn < TEST_STRINGS.length; sn++) {
|
||||
var got_contents = fs.readFileSync(base + sn, "utf-8");
|
||||
t.equals( got_contents, TEST_STRINGS[sn], `rz wrote out the file: ` + JSON.stringify(TEST_STRINGS[sn]) );
|
||||
t.equals( 0 + fs.statSync(base + sn).mtime, 0 + mtime_1990, `... and observed the sent mtime` );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
});
|
||||
|
||||
tape("send one at a time", (t) => {
|
||||
return _do_in_temp_dir( () => {
|
||||
var xfer;
|
||||
|
||||
let test_strings = TEST_STRINGS.slice(0);
|
||||
|
||||
function doer() {
|
||||
var file_contents = test_strings.shift();
|
||||
if (typeof(file_contents) !== "string") return; //we’re done
|
||||
|
||||
return helper.exec_lrzsz_steps( t, RZ_PATH, ["--overwrite"], [
|
||||
(zsession, child) => {
|
||||
zsession.send_offer( { name: "single" } ).then( (xf) => {
|
||||
t.ok( !!xf, 'rz accepted offer' );
|
||||
xfer = xf;
|
||||
} ).then(
|
||||
() => xfer.end( Array.from( text_encoder.encode(file_contents) ) )
|
||||
).then(
|
||||
() => zsession.close()
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
(zsession, child) => {
|
||||
return zsession.has_ended();
|
||||
},
|
||||
] ).then( () => {
|
||||
var got_contents = fs.readFileSync("single", "utf-8");
|
||||
t.equals( got_contents, file_contents, `rz wrote out the file: ` + JSON.stringify(file_contents) );
|
||||
} ).then( doer );
|
||||
}
|
||||
|
||||
return doer();
|
||||
} );
|
||||
});
|
||||
|
||||
tape("send single large file", (t) => {
|
||||
return _do_in_temp_dir( () => {
|
||||
var string_num = 0;
|
||||
|
||||
var mtime_1990 = new Date("1990-01-01T00:00:00Z");
|
||||
var big_string = Array(30 * 1024 * 1024).fill('x').join("");
|
||||
|
||||
var batch = [
|
||||
[
|
||||
{
|
||||
name: "big_kahuna",
|
||||
},
|
||||
big_string,
|
||||
],
|
||||
];
|
||||
|
||||
return _send_batch(t, batch).then( () => {
|
||||
var got_contents = fs.readFileSync("big_kahuna", "utf-8");
|
||||
t.equals( got_contents, big_string, 'rz wrote out the file');
|
||||
} );
|
||||
} );
|
||||
});
|
||||
|
||||
tape("send single random file", (t) => {
|
||||
return _do_in_temp_dir( () => {
|
||||
var string_num = 0;
|
||||
|
||||
var mtime_1990 = new Date("1990-01-01T00:00:00Z");
|
||||
|
||||
var big_buffer = new Buffer(1024 * 1024);
|
||||
for (var i=0; i<big_buffer.length; i++) {
|
||||
big_buffer[i] = Math.floor( Math.random(256) );
|
||||
}
|
||||
|
||||
var batch = [
|
||||
[
|
||||
{
|
||||
name: "big_kahuna",
|
||||
},
|
||||
big_buffer,
|
||||
],
|
||||
];
|
||||
|
||||
return _send_batch(t, batch).then( () => {
|
||||
var got_contents = fs.readFileSync("big_kahuna");
|
||||
t.equals( got_contents.join(), big_buffer.join(), 'rz wrote out the file');
|
||||
} );
|
||||
} );
|
||||
});
|
62
tests/zsubpacket.js
Executable file
62
tests/zsubpacket.js
Executable 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
227
tests/zvalidation.js
Normal 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
BIN
tools/all_bytes
Normal file
Binary file not shown.
227
tools/talk_to_sz.pl
Executable file
227
tools/talk_to_sz.pl
Executable 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 ); #it’ll 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(); #doesn’t 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
28
webpack.config.js
Normal 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'
|
||||
})
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue