summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2021-11-20 06:01:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2021-11-20 06:19:39 +0000
commit56eec1de7018759c0ec251dba4455c18f73c3bbd (patch)
tree3aeb2d10356530bc2cc3f24e74f41048a13885b4
parentInitial commit. (diff)
downloadzmodemjs-upstream.tar.xz
zmodemjs-upstream.zip
Adding upstream version 0.1.10+dfsg.upstream/0.1.10+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md34
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml9
-rw-r--r--CHANGELOG.md44
-rw-r--r--LICENSE201
-rw-r--r--README.md432
-rwxr-xr-xbin/zmodemjs-sz.js140
-rw-r--r--index.js6
-rw-r--r--jsdoc.json10
-rw-r--r--package.json49
-rw-r--r--src/encode.js124
-rw-r--r--src/text.js33
-rw-r--r--src/zcrc.js143
-rw-r--r--src/zdle.js240
-rw-r--r--src/zerror.js47
-rw-r--r--src/zheader.js763
-rw-r--r--src/zmlib.js102
-rw-r--r--src/zmodem.js4
-rw-r--r--src/zmodem_browser.js182
-rw-r--r--src/zsentry.js394
-rw-r--r--src/zsession.js1677
-rw-r--r--src/zsubpacket.js241
-rw-r--r--src/zvalidation.js130
-rwxr-xr-xtests/encode.js119
-rw-r--r--tests/lib/testhelp.js121
-rw-r--r--tests/lib/zmodem.js1
-rwxr-xr-xtests/text.js45
-rwxr-xr-xtests/zcrc.js113
-rwxr-xr-xtests/zdle.js41
-rw-r--r--tests/zerror.js82
-rwxr-xr-xtests/zheader.js309
-rwxr-xr-xtests/zmlib.js81
-rwxr-xr-xtests/zsentry.js226
-rwxr-xr-xtests/zsession.js312
-rwxr-xr-xtests/zsession_receive.js295
-rwxr-xr-xtests/zsession_send.js248
-rwxr-xr-xtests/zsubpacket.js62
-rw-r--r--tests/zvalidation.js227
-rw-r--r--tools/all_bytesbin0 -> 256 bytes
-rwxr-xr-xtools/talk_to_sz.pl227
-rw-r--r--webpack.config.js28
-rw-r--r--yarn.lock3010
42 files changed, 10556 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..472795d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..780cf9b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.*.sw?
+/dist
+/documentation
+/node_modules
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..3018f3e
--- /dev/null
+++ b/.travis.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1cc5bbf
--- /dev/null
+++ b/CHANGELOG.md
@@ -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.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4722445
--- /dev/null
+++ b/README.md
@@ -0,0 +1,432 @@
+# zmodem.js - ZMODEM for JavaScript
+
+[![build status](https://api.travis-ci.org/FGasper/zmodemjs.svg?branch=master)](http://travis-ci.org/FGasper/zmodemjs)
+
+# SYNOPSIS
+
+ let zsentry = new Zmodem.Sentry( {
+ to_terminal(octets) { .. }, //i.e. send to the terminal
+
+ sender(octets) { .. }, //i.e. send to the ZMODEM peer
+
+ on_detect(detection) { .. }, //for when Sentry detects a new ZMODEM
+
+ on_retract() { .. }, //for when Sentry retracts a Detection
+ } );
+
+ //We have to configure whatever gives us new input to send that
+ //input to zsentry.
+ //
+ //EXAMPLE: From web browsers that use WebSocket …
+ //
+ ws.addEventListener("message", function(evt) {
+ zsentry.consume(evt.data);
+ } );
+
+The `on_detect(detection)` function call is probably the most complex
+piece of the above; one potential implementation might look like:
+
+ on_detect(detection) {
+
+ //Do this if we determine that what looked like a ZMODEM session
+ //is actually not meant to be ZMODEM.
+ if (no_good) {
+ detection.deny();
+ return;
+ }
+
+ zsession = detection.confirm();
+
+ if (zsession.type === "send") {
+
+ //Send a group of files, e.g., from an <input>’s “.files”.
+ //There are events you can listen for here as well,
+ //e.g., to update a progress meter.
+ Zmodem.Browser.send_files( zsession, files_obj );
+ }
+ else {
+ zsession.on("offer", (xfer) => {
+
+ //Do this if you 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.
diff --git a/bin/zmodemjs-sz.js b/bin/zmodemjs-sz.js
new file mode 100755
index 0000000..d2331ba
--- /dev/null
+++ b/bin/zmodemjs-sz.js
@@ -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 …");
+ }
+ }
+});
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..dfd2ac2
--- /dev/null
+++ b/index.js
@@ -0,0 +1,6 @@
+"use strict";
+
+Object.assign(
+ module.exports,
+ require("./src/zsentry")
+);
diff --git a/jsdoc.json b/jsdoc.json
new file mode 100644
index 0000000..06f8f71
--- /dev/null
+++ b/jsdoc.json
@@ -0,0 +1,10 @@
+{
+ "plugins": ["plugins/markdown"],
+ "source": {
+ "include": ["README.md", "src"],
+ "includePattern": "\\.js$"
+ },
+ "opts": {
+ "destination": "documentation"
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ec1f731
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/src/encode.js b/src/encode.js
new file mode 100644
index 0000000..5cb6344
--- /dev/null
+++ b/src/encode.js
@@ -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;
+ },
+};
diff --git a/src/text.js b/src/text.js
new file mode 100644
index 0000000..d267817
--- /dev/null
+++ b/src/text.js
@@ -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,
+};
diff --git a/src/zcrc.js b/src/zcrc.js
new file mode 100644
index 0000000..831f835
--- /dev/null
+++ b/src/zcrc.js
@@ -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;
+ }
+ },
+};
diff --git a/src/zdle.js b/src/zdle.js
new file mode 100644
index 0000000..989222e
--- /dev/null
+++ b/src/zdle.js
@@ -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;
+ }
+}
diff --git a/src/zerror.js b/src/zerror.js
new file mode 100644
index 0000000..1a779de
--- /dev/null
+++ b/src/zerror.js
@@ -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;
+ }
+ }
+};
diff --git a/src/zheader.js b/src/zheader.js
new file mode 100644
index 0000000..56c22fc
--- /dev/null
+++ b/src/zheader.js
@@ -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;
diff --git a/src/zmlib.js b/src/zmlib.js
new file mode 100644
index 0000000..f86070d
--- /dev/null
+++ b/src/zmlib.js
@@ -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;
+ },
+};
diff --git a/src/zmodem.js b/src/zmodem.js
new file mode 100644
index 0000000..c16bcfe
--- /dev/null
+++ b/src/zmodem.js
@@ -0,0 +1,4 @@
+Object.assign(
+ module.exports,
+ require("./zsentry"),
+);
diff --git a/src/zmodem_browser.js b/src/zmodem_browser.js
new file mode 100644
index 0000000..7405f54
--- /dev/null
+++ b/src/zmodem_browser.js
@@ -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);
+ },
+};
diff --git a/src/zsentry.js b/src/zsentry.js
new file mode 100644
index 0000000..470769a
--- /dev/null
+++ b/src/zsentry.js
@@ -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;
+ }
+}
diff --git a/src/zsession.js b/src/zsession.js
new file mode 100644
index 0000000..5f0b8f9
--- /dev/null
+++ b/src/zsession.js
@@ -0,0 +1,1677 @@
+"use strict";
+
+var Zmodem = module.exports;
+
+/**
+ * This is where the protocol-level logic lives: the interaction of ZMODEM
+ * headers and subpackets. The logic here is not unlikely to need tweaking
+ * as little edge cases crop up.
+ */
+
+Zmodem.DEBUG = false;
+
+Object.assign(
+ Zmodem,
+ require("./encode"),
+ require("./text"),
+ require("./zdle"),
+ require("./zmlib"),
+ require("./zheader"),
+ require("./zsubpacket"),
+ require("./zvalidation"),
+ require("./zerror")
+);
+
+const
+ //pertinent to this module
+ KEEPALIVE_INTERVAL = 5000,
+
+ //We ourselves don’t need ESCCTL, so we don’t send it;
+ //however, we always expect to receive it in ZRINIT.
+ //See _ensure_receiver_escapes_ctrl_chars() for more details.
+ ZRINIT_FLAGS = [
+ "CANFDX", //full duplex
+ "CANOVIO", //overlap I/O
+
+ //lsz has a buffer overflow bug that shows itself when:
+ //
+ // - 16-bit CRC is used, and
+ // - lsz receives the abort sequence while sending a file
+ //
+ //To avoid this, we just tell lsz to use 32-bit CRC
+ //even though there is otherwise no reason. This ensures that
+ //unfixed lsz versions will avoid the buffer overflow.
+ "CANFC32",
+ ],
+
+ //We do this because some WebSocket shell servers
+ //(e.g., xterm.js’s demo server) enable the IEXTEN termios flag,
+ //which bars 0x0f and 0x16 from reaching the shell process,
+ //which results in transmission errors.
+ FORCE_ESCAPE_CTRL_CHARS = true,
+
+ DEFAULT_RECEIVE_INPUT_MODE = "spool_uint8array",
+
+ //pertinent to ZMODEM
+ MAX_CHUNK_LENGTH = 8192, //1 KiB officially, but lrzsz allows 8192
+ BS = 0x8,
+ OVER_AND_OUT = [ 79, 79 ],
+ ABORT_SEQUENCE = Zmodem.ZMLIB.ABORT_SEQUENCE
+;
+
+/**
+ * A base class for objects that have events.
+ *
+ * @private
+ */
+class _Eventer {
+
+ /**
+ * Not called directly.
+ */
+ constructor() {
+ this._on_evt = {};
+ this._evt_once_index = {};
+ }
+
+ _Add_event(evt_name) {
+ this._on_evt[evt_name] = [];
+ this._evt_once_index[evt_name] = [];
+ }
+
+ _get_evt_queue(evt_name) {
+ if (!this._on_evt[evt_name]) {
+ throw( "Bad event: " + evt_name );
+ }
+
+ return this._on_evt[evt_name];
+ }
+
+ /**
+ * Register a callback for a given event.
+ *
+ * @param {string} evt_name - The name of the event.
+ *
+ * @param {Function} todo - The function to execute when the event happens.
+ */
+ on(evt_name, todo) {
+ var queue = this._get_evt_queue(evt_name);
+
+ queue.push(todo);
+
+ return this;
+ }
+
+ /**
+ * Unregister a callback for a given event.
+ *
+ * @param {string} evt_name - The name of the event.
+ *
+ * @param {Function} [todo] - The function to execute when the event
+ * happens. If not given, the last event registered for the event
+ * is unregistered.
+ */
+ off(evt_name, todo) {
+ var queue = this._get_evt_queue(evt_name);
+
+ if (todo) {
+ var at = queue.indexOf(todo);
+ if (at === -1) {
+ throw("“" + todo + "” is not in the “" + evt_name + "” queue.");
+ }
+ queue.splice(at, 1);
+ }
+ else {
+ queue.pop();
+ }
+
+ return this;
+ }
+
+ _Happen(evt_name /*, arg0, arg1, .. */) {
+ var queue = this._get_evt_queue(evt_name); //might as well validate
+
+ //console.info("EVENT", this, arguments);
+
+ var args = Array.apply(null, arguments);
+ args.shift();
+
+ var sess = this;
+
+ queue.forEach( function(cb) { cb.apply(sess, args) } );
+
+ return queue.length;
+ }
+}
+
+/**
+ * The Session classes handle the protocol-level logic.
+ * These shield the user from dealing with headers and subpackets.
+ * This is a base class with functionality common to both Receive
+ * and Send subclasses.
+ *
+ * @extends _Eventer
+*/
+Zmodem.Session = class ZmodemSession extends _Eventer {
+
+ /**
+ * Parse out a hex header from the given array.
+ * If there’s a ZRQINIT or ZRINIT at the beginning,
+ * we’ll return it. If the input isn’t a header,
+ * for whatever reason, we return undefined.
+ *
+ * @param {number[]} octets - The bytes to parse.
+ *
+ * @return {Session|undefined} A Session object if the beginning
+ * of a session was parsable in “octets”; otherwise undefined.
+ */
+ static parse( octets ) {
+
+ //Will need to trap errors.
+ var hdr;
+ try {
+ hdr = Zmodem.Header.parse_hex(octets);
+ }
+ catch(e) { //Don’t report since we aren’t in session
+
+ //debug
+ //console.warn("No hex header: ", e);
+
+ return;
+ }
+
+ if (!hdr) return;
+
+ switch (hdr.NAME) {
+ case "ZRQINIT":
+ //throw if ZCOMMAND
+ return new Zmodem.Session.Receive();
+ case "ZRINIT":
+ return new Zmodem.Session.Send(hdr);
+ }
+
+ //console.warn("Invalid first Zmodem header", hdr);
+ }
+
+ /**
+ * Sets the sender function that a Session object will use.
+ *
+ * @param {Function} sender_func - The function to call.
+ * It will receive an Array with the relevant octets.
+ *
+ * @return {Session} The session object (for chaining).
+ */
+ set_sender(sender_func) {
+ this._sender = sender_func;
+ return this;
+ }
+
+ /**
+ * Whether the current Session has ended.
+ *
+ * @returns {boolean} The ended state.
+ */
+ has_ended() { return this._has_ended() }
+
+ /**
+ * Consumes an array of octets as ZMODEM session input.
+ *
+ * @param {number[]} octets - The input octets.
+ */
+ consume(octets) {
+ this._before_consume(octets);
+
+ if (this._aborted) throw new Zmodem.Error('already_aborted');
+
+ if (!octets.length) return;
+
+ this._strip_and_enqueue_input(octets);
+
+ if (!this._check_for_abort_sequence(octets)) {
+ this._consume_first();
+ }
+
+ return;
+ }
+
+ /**
+ * Whether the current Session has been `abort()`ed.
+ *
+ * @returns {boolean} The aborted state.
+ */
+ aborted() { return !!this._aborted }
+
+ /**
+ * Not called directly.
+ */
+ constructor() {
+ super();
+ //if (!sender_func) throw "Need sender!";
+
+ //this._first_header = first_header;
+ //this._sender = sender_func;
+ this._config = {};
+
+ //this._input = new ZInput();
+
+ this._input_buffer = [];
+
+ //This is mostly for debugging.
+ this._Add_event("receive");
+ this._Add_event("garbage");
+ this._Add_event("session_end");
+ }
+
+ /**
+ * Returns the Session object’s role.
+ *
+ * @returns {string} One of:
+ * - `receive`
+ * - `send`
+ */
+ get_role() { return this.type }
+
+ _trim_leading_garbage_until_header() {
+ var garbage = Zmodem.Header.trim_leading_garbage(this._input_buffer);
+
+ if (garbage.length) {
+ if (this._Happen("garbage", garbage) === 0) {
+ console.debug(
+ "Garbage: ",
+ String.fromCharCode.apply(String, garbage),
+ garbage
+ );
+ }
+ }
+ }
+
+ _parse_and_consume_header() {
+ this._trim_leading_garbage_until_header();
+
+ var new_header_and_crc = Zmodem.Header.parse(this._input_buffer);
+ if (!new_header_and_crc) return;
+
+ if (Zmodem.DEBUG) {
+ this._log_header( "RECEIVED HEADER", new_header_and_crc[0] );
+ }
+
+ this._consume_header(new_header_and_crc[0]);
+
+ this._last_header_name = new_header_and_crc[0].NAME;
+ this._last_header_crc = new_header_and_crc[1];
+
+ return new_header_and_crc[0];
+ }
+
+ _log_header(label, header) {
+ console.debug(this.type, label, header.NAME, header._bytes4.join());
+ }
+
+ _consume_header(new_header) {
+ this._on_receive(new_header);
+
+ var handler = this._next_header_handler && this._next_header_handler[ new_header.NAME ];
+ if (!handler) {
+ console.error("Unhandled header!", new_header, this._next_header_handler);
+ throw new Zmodem.Error( "Unhandled header: " + new_header.NAME );
+ }
+
+ this._next_header_handler = null;
+
+ handler.call(this, new_header);
+ }
+
+ //TODO: strip out the abort sequence
+ _check_for_abort_sequence() {
+ var abort_at = Zmodem.ZMLIB.find_subarray( this._input_buffer, ABORT_SEQUENCE );
+
+ if (abort_at !== -1) {
+
+ //TODO: expose this to caller
+ this._input_buffer.splice( 0, abort_at + ABORT_SEQUENCE.length );
+
+ this._aborted = true;
+
+ //TODO compare response here to lrzsz.
+ this._on_session_end();
+
+ //We shouldn’t ever expect to receive an abort. Even if we
+ //have sent an abort ourselves, the Sentry should have stopped
+ //directing input to this Session object.
+ //if (this._expect_abort) {
+ // return true;
+ //}
+
+ throw new Zmodem.Error("peer_aborted");
+ }
+ }
+
+ _send_header(name /*, args */) {
+ if (!this._sender) throw "Need sender!";
+
+ var args = Array.apply( null, arguments );
+
+ var bytes_hdr = this._create_header_bytes(args);
+
+ if (Zmodem.DEBUG) {
+ this._log_header( "SENDING HEADER", bytes_hdr[1] );
+ }
+
+ this._sender(bytes_hdr[0]);
+
+ this._last_sent_header = bytes_hdr[1];
+ }
+
+ _create_header_bytes(name_and_args) {
+
+ var hdr = Zmodem.Header.build.apply( Zmodem.Header, name_and_args );
+
+ var formatter = this._get_header_formatter(name_and_args[0]);
+
+ return [
+ hdr[formatter](this._zencoder),
+ hdr
+ ];
+ }
+
+ _strip_and_enqueue_input(input) {
+ Zmodem.ZMLIB.strip_ignored_bytes(input);
+
+ //It’s possible that “input” is empty at this point.
+ //It doesn’t seem to hurt anything to keep processing, though.
+
+ this._input_buffer.push.apply( this._input_buffer, input );
+ }
+
+ /**
+ * **STOP!** You probably want to `skip()` an Offer rather than
+ * `abort()`. See below.
+ *
+ * Abort the current session by sending the ZMODEM abort sequence.
+ * This function will cause the Session object to refuse to send
+ * any further data.
+ *
+ * Zmodem.Sentry is configured to send all output to the terminal
+ * after a session’s `abort()`. That could result in lots of
+ * ZMODEM garble being sent to the JavaScript terminal, which you
+ * probably don’t want.
+ *
+ * `skip()` on an Offer is better because Session will continue to
+ * discard data until we reach either another file or the
+ * sender-initiated end of the ZMODEM session. So no ZMODEM garble,
+ * and the session will end successfully.
+ *
+ * The behavior of `abort()` is subject to change since it’s not
+ * very useful as currently implemented.
+ */
+ abort() {
+
+ //this._expect_abort = true;
+
+ //From Forsberg:
+ //
+ //The Cancel sequence consists of eight CAN characters
+ //and ten backspace characters. ZMODEM only requires five
+ //Cancel characters; the other three are "insurance".
+ //The trailing backspace characters attempt to erase
+ //the effects of the CAN characters if they are
+ //received by a command interpreter.
+ //
+ //FG: Since we assume our connection is reliable, there’s
+ //no reason to send more than 5 CANs.
+ this._sender(
+ ABORT_SEQUENCE.concat([ BS, BS, BS, BS, BS ])
+ );
+
+ this._aborted = true;
+ this._sender = function() {
+ throw new Zmodem.Error('already_aborted');
+ };
+
+ this._on_session_end();
+
+ return;
+ }
+
+ //----------------------------------------------------------------------
+ _on_session_end() {
+ this._Happen("session_end");
+ }
+
+ _on_receive(hdr_or_pkt) {
+ this._Happen("receive", hdr_or_pkt);
+ }
+
+ _before_consume() {}
+}
+
+function _trim_OO(array) {
+ if (0 === Zmodem.ZMLIB.find_subarray(array, OVER_AND_OUT)) {
+ array.splice(0, OVER_AND_OUT.length);
+ }
+
+ //TODO: This assumes OVER_AND_OUT is 2 bytes long. No biggie, but.
+ else if ( array[0] === OVER_AND_OUT[ OVER_AND_OUT.length - 1 ] ) {
+ array.splice(0, 1);
+ }
+
+ return array;
+}
+
+/** A class for ZMODEM receive sessions.
+ *
+ * @extends Session
+ */
+Zmodem.Session.Receive = class ZmodemReceiveSession extends Zmodem.Session {
+ //We only get 1 file at a time, so on each consume() either
+ //continue state for the current file or start a new one.
+
+ /**
+ * Not called directly.
+ */
+ constructor() {
+ super();
+
+ this._Add_event("offer");
+ this._Add_event("data_in");
+ this._Add_event("file_end");
+ }
+
+ /**
+ * Consume input bytes from the sender.
+ *
+ * @private
+ * @param {number[]} octets - The bytes to consume.
+ */
+ _before_consume(octets) {
+ if (this._bytes_after_OO) {
+ throw "PROTOCOL: Session is completed!";
+ }
+
+ //Put this here so that our logic later on has access to the
+ //input string and can populate _bytes_after_OO when the
+ //session ends.
+ this._bytes_being_consumed = octets;
+ }
+
+ /**
+ * Return any bytes that have been `consume()`d but
+ * came after the end of the ZMODEM session.
+ *
+ * @returns {number[]} The trailing bytes.
+ */
+ get_trailing_bytes() {
+ if (this._aborted) return [];
+
+ if (!this._bytes_after_OO) {
+ throw "PROTOCOL: Session is not completed!";
+ }
+
+ return this._bytes_after_OO.slice(0);
+ }
+
+ _has_ended() { return this.aborted() || !!this._bytes_after_OO }
+
+ //Receiver always sends hex headers.
+ _get_header_formatter() { return "to_hex" }
+
+ _parse_and_consume_subpacket() {
+ var parse_func;
+ if (this._last_header_crc === 16) {
+ parse_func = "parse16";
+ }
+ else {
+ parse_func = "parse32";
+ }
+
+ var subpacket = Zmodem.Subpacket[parse_func](this._input_buffer);
+
+ if (subpacket) {
+ if (Zmodem.DEBUG) {
+ console.debug(this.type, "RECEIVED SUBPACKET", subpacket);
+ }
+
+ this._consume_data(subpacket);
+
+ //What state are we in if the subpacket indicates frame end
+ //but we haven’t gotten ZEOF yet? Can anything other than ZEOF
+ //follow after a ZDATA?
+ if (subpacket.frame_end()) {
+ this._next_subpacket_handler = null;
+ }
+ }
+
+ return subpacket;
+ }
+
+ _consume_first() {
+ if (this._got_ZFIN) {
+ if (this._input_buffer.length < 2) return;
+
+ //if it’s OO, then set this._bytes_after_OO
+ if (Zmodem.ZMLIB.find_subarray(this._input_buffer, OVER_AND_OUT) === 0) {
+
+ //This doubles as an indication that the session has ended.
+ //We need to set this right away so that handlers like
+ //"session_end" will have access to it.
+ this._bytes_after_OO = _trim_OO(this._bytes_being_consumed.slice(0));
+ this._on_session_end();
+
+ return;
+ }
+ else {
+ throw( "PROTOCOL: Only thing after ZFIN should be “OO” (79,79), not: " + this._input_buffer.join() );
+ }
+ }
+
+ var parsed;
+ do {
+ if (this._next_subpacket_handler) {
+ parsed = this._parse_and_consume_subpacket();
+ }
+ else {
+ parsed = this._parse_and_consume_header();
+ }
+ } while (parsed && this._input_buffer.length);
+ }
+
+ _consume_data(subpacket) {
+ this._on_receive(subpacket);
+
+ if (!this._next_subpacket_handler) {
+ throw( "PROTOCOL: Received unexpected data packet after " + this._last_header_name + " header: " + subpacket.get_payload().join() );
+ }
+
+ this._next_subpacket_handler.call(this, subpacket);
+ }
+
+ _octets_to_string(octets) {
+ if (!this._textdecoder) {
+ this._textdecoder = new Zmodem.Text.Decoder();
+ }
+
+ return this._textdecoder.decode( new Uint8Array(octets) );
+ }
+
+ _consume_ZFILE_data(hdr, subpacket) {
+ if (this._file_info) {
+ throw "PROTOCOL: second ZFILE data subpacket received";
+ }
+
+ var packet_payload = subpacket.get_payload();
+ var nul_at = packet_payload.indexOf(0);
+
+ //
+ var fname = this._octets_to_string( packet_payload.slice(0, nul_at) );
+ var the_rest = this._octets_to_string( packet_payload.slice( 1 + nul_at ) ).split(" ");
+
+ var mtime = the_rest[1] && parseInt( the_rest[1], 8 ) || undefined;
+ if (mtime) {
+ mtime = new Date(mtime * 1000);
+ }
+
+ this._file_info = {
+ name: fname,
+ size: the_rest[0] ? parseInt( the_rest[0], 10 ) : null,
+ mtime: mtime || null,
+ mode: the_rest[2] && parseInt( the_rest[2], 8 ) || null,
+ serial: the_rest[3] && parseInt( the_rest[3], 10 ) || null,
+
+ files_remaining: the_rest[4] ? parseInt( the_rest[4], 10 ) : null,
+ bytes_remaining: the_rest[5] ? parseInt( the_rest[5], 10 ) : null,
+ };
+
+ //console.log("ZFILE", hdr);
+
+ var xfer = new Offer(
+ hdr.get_options(),
+ this._file_info,
+ this._accept.bind(this),
+ this._skip.bind(this)
+ );
+ this._current_transfer = xfer;
+
+ //this._Happen("offer", xfer);
+ }
+
+ _consume_ZDATA_data(subpacket) {
+ if (!this._accepted_offer) {
+ throw "PROTOCOL: Received data without accepting!";
+ }
+
+ //TODO: Probably should include some sort of preventive against
+ //infinite loop here: if the peer hasn’t sent us what we want after,
+ //say, 10 ZRPOS headers then we should send ZABORT and just end.
+ if (!this._offset_ok) {
+ console.warn("offset not ok!");
+ _send_ZRPOS();
+ return;
+ }
+
+ this._file_offset += subpacket.get_payload().length;
+ this._on_data_in(subpacket);
+
+ /*
+ console.warn("received error from data_in callback; retrying", e);
+ throw "unimplemented";
+ */
+
+ if (subpacket.ack_expected() && !subpacket.frame_end()) {
+ this._send_header( "ZACK", Zmodem.ENCODELIB.pack_u32_le(this._file_offset) );
+ }
+ }
+
+ _make_promise_for_between_files() {
+ var sess = this;
+
+ return new Promise( function(res) {
+ var between_files_handler = {
+ ZFILE: function(hdr) {
+ this._next_subpacket_handler = function(subpacket) {
+ this._next_subpacket_handler = null;
+ this._consume_ZFILE_data(hdr, subpacket);
+ this._Happen("offer", this._current_transfer);
+ res(this._current_transfer);
+ };
+ },
+
+ //We use this as a keep-alive. Maybe other
+ //implementations do, too?
+ ZSINIT: function(hdr) {
+ //The content of this header doesn’t affect us
+ //since all it does is tell us details of how
+ //the sender will ZDLE-encode binary data. Our
+ //ZDLE parser doesn’t need to know in advance.
+
+ sess._next_subpacket_handler = function(spkt) {
+ sess._next_subpacket_handler = null;
+ sess._consume_ZSINIT_data(spkt);
+ sess._send_header('ZACK');
+ sess._next_header_handler = between_files_handler;
+ };
+ },
+
+ ZFIN: function() {
+ this._consume_ZFIN();
+ res();
+ },
+ };
+
+ sess._next_header_handler = between_files_handler;
+ } );
+ }
+
+ _consume_ZSINIT_data(spkt) {
+
+ //TODO: Should this be used when we signal a cancellation?
+ this._attn = spkt.get_payload();
+ }
+
+ /**
+ * Start the ZMODEM session by signaling to the sender that
+ * we are ready for the first file offer.
+ *
+ * @returns {Promise} A promise that resolves with an Offer object
+ * or, if the sender closes the session immediately without offering
+ * anything, nothing.
+ */
+ start() {
+ if (this._started) throw "Already started!";
+ this._started = true;
+
+ var ret = this._make_promise_for_between_files();
+
+ this._send_ZRINIT();
+
+ return ret;
+ }
+
+ //Returns a promise that’s fulfilled when the file
+ //transfer is done.
+ //
+ // That ZEOF promise return is another promise that’s
+ // fulfilled when we get either ZFIN or another ZFILE.
+ _accept(offset) {
+ this._accepted_offer = true;
+ this._file_offset = offset || 0;
+
+ var sess = this;
+
+ var ret = new Promise( function(resolve_accept) {
+ var last_ZDATA;
+
+ sess._next_header_handler = {
+ ZDATA: function on_ZDATA(hdr) {
+ this._consume_ZDATA(hdr);
+
+ this._next_subpacket_handler = this._consume_ZDATA_data;
+
+ this._next_header_handler = {
+ ZEOF: function on_ZEOF(hdr) {
+
+ // Do this first to verify the ZEOF.
+ // This also fires the “file_end” event.
+ this._consume_ZEOF(hdr);
+
+ this._next_subpacket_handler = null;
+
+ // We don’t care about this promise.
+ // Prior to v0.1.8 we did because we called
+ // resolve_accept() at the resolution of this
+ // promise, but that was a bad idea and was
+ // never documented, so 0.1.8 changed it.
+ this._make_promise_for_between_files();
+
+ resolve_accept();
+
+ this._send_ZRINIT();
+ },
+ };
+ },
+ };
+ } );
+
+ this._send_ZRPOS();
+
+ return ret;
+ }
+
+ _skip() {
+ var ret = this._make_promise_for_between_files();
+
+ if (this._accepted_offer) {
+ // There’s a race condition where we might attempt to
+ // skip() an in-progress transfer near its end but actually
+ // the skip() will fire after the transfer is complete.
+ // While there might be ways to prevent this, they likely
+ // would require extra work on the part of implementations.
+ //
+ // It seems far simpler just to make this function a no-op
+ // in these cases.
+ if (!this._current_transfer) return;
+
+ //For cancel of an in-progress transfer from lsz,
+ //it’s necessary to avoid this buffer overflow bug:
+ //
+ // https://github.com/gooselinux/lrzsz/blob/master/lrzsz-0.12.20.patch
+ //
+ //… which we do by asking for CRC32 from lsz.
+
+ //We might or might not have consumed ZDATA.
+ //The sender also might or might not send a ZEOF before it
+ //parses the ZSKIP. Thus, we want to ignore the following:
+ //
+ // - ZDATA
+ // - ZDATA then ZEOF
+ // - ZEOF
+ //
+ //… and just look for the next between-file header.
+
+ var bound_make_promise_for_between_files = function() {
+
+ //Once this happens we fail on any received data packet.
+ //So it needs not to happen until we’ve received a header.
+ this._accepted_offer = false;
+ this._next_subpacket_handler = null;
+
+ this._make_promise_for_between_files();
+ }.bind(this);
+
+ Object.assign(
+ this._next_header_handler,
+ {
+ ZEOF: bound_make_promise_for_between_files,
+ ZDATA: function() {
+ bound_make_promise_for_between_files();
+ this._next_header_handler.ZEOF = bound_make_promise_for_between_files;
+ }.bind(this),
+ }
+ );
+ }
+
+ //this._accepted_offer = false;
+
+ this._file_info = null;
+
+ this._send_header( "ZSKIP" );
+
+ return ret;
+ }
+
+ _send_ZRINIT() {
+ this._send_header( "ZRINIT", ZRINIT_FLAGS );
+ }
+
+ _consume_ZFIN() {
+ this._got_ZFIN = true;
+ this._send_header( "ZFIN" );
+ }
+
+ _consume_ZEOF(header) {
+ if (this._file_offset !== header.get_offset()) {
+ throw( "ZEOF offset mismatch; unimplemented (local: " + this._file_offset + "; ZEOF: " + header.get_offset() + ")" );
+ }
+
+ this._on_file_end();
+
+ //Preserve these two so that file_end callbacks
+ //will have the right information.
+ this._file_info = null;
+ this._current_transfer = null;
+ }
+
+ _consume_ZDATA(header) {
+ if ( this._file_offset === header.get_offset() ) {
+ this._offset_ok = true;
+ }
+ else {
+ throw "Error correction is unimplemented.";
+ }
+ }
+
+ _send_ZRPOS() {
+ this._send_header( "ZRPOS", this._file_offset );
+ }
+
+ //----------------------------------------------------------------------
+ //events
+
+ _on_file_end() {
+ this._Happen("file_end");
+
+ if (this._current_transfer) {
+ this._current_transfer._Happen("complete");
+ this._current_transfer = null;
+ }
+ }
+
+ _on_data_in(subpacket) {
+ this._Happen("data_in", subpacket);
+
+ if (this._current_transfer) {
+ this._current_transfer._Happen("input", subpacket.get_payload());
+ }
+ }
+}
+
+Object.assign(
+ Zmodem.Session.Receive.prototype,
+ {
+ type: "receive",
+ }
+);
+
+//----------------------------------------------------------------------
+
+/**
+ * @typedef {Object} FileDetails
+ *
+ * @property {string} name - The name of the file.
+ *
+ * @property {number} [size] - The file size, in bytes.
+ *
+ * @property {number} [mode] - The file mode (e.g., 0100644).
+ *
+ * @property {Date|number} [mtime] - The file’s modification time.
+ * When expressed as a number, the unit is epoch seconds.
+ *
+ * @property {number} [files_remaining] - Inclusive of the current file,
+ * so this value is never less than 1.
+ *
+ * @property {number} [bytes_remaining] - Inclusive of the current file.
+ */
+
+/**
+ * Common methods for Transfer and Offer objects.
+ *
+ * @mixin
+ */
+var Transfer_Offer_Mixin = {
+ /**
+ * Returns the file details object.
+ * @returns {FileDetails} `mtime` is a Date.
+ */
+ get_details: function get_details() {
+ return Object.assign( {}, this._file_info );
+ },
+
+ /**
+ * Returns a parse of the ZFILE header’s payload.
+ *
+ * @returns {Object} Members are:
+ *
+ * - `conversion` (string | undefined)
+ * - `management` (string | undefined)
+ * - `transfer` (string | undefined)
+ * - `sparse` (boolean)
+ */
+ get_options: function get_options() {
+ return Object.assign( {}, this._zfile_opts );
+ },
+
+ /**
+ * Returns the offset based on the last transferred chunk.
+ * @returns {number} The file offset (i.e., number of bytes after
+ * the start of the file).
+ */
+ get_offset: function get_offset() {
+ return this._file_offset;
+ },
+};
+
+/**
+ * A class to represent a sender’s interaction with a single file
+ * transfer within a batch. When a receiver accepts an offer, the
+ * Session instantiates this class and passes the instance as the
+ * promise resolution from send_offer().
+ *
+ * @mixes Transfer_Offer_Mixin
+ */
+class Transfer {
+
+ /**
+ * Not called directly.
+ */
+ constructor(file_info, offset, send_func, end_func) {
+ this._file_info = file_info;
+ this._file_offset = offset || 0;
+
+ this._send = send_func;
+ this._end = end_func;
+ }
+
+ /**
+ * Send a (non-terminal) piece of the file.
+ *
+ * @param { number[] | Uint8Array } array_like - The bytes to send.
+ */
+ send(array_like) {
+ this._send(array_like);
+ this._file_offset += array_like.length;
+ }
+
+ /**
+ * Complete the file transfer.
+ *
+ * @param { number[] | Uint8Array } [array_like] - The last bytes to send.
+ *
+ * @return { Promise } Resolves when the receiver has indicated
+ * acceptance of the end of the file transfer.
+ */
+ end(array_like) {
+ var ret = this._end(array_like || []);
+ if (array_like) this._file_offset += array_like.length;
+ return ret;
+ }
+}
+Object.assign( Transfer.prototype, Transfer_Offer_Mixin );
+
+/**
+ * A class to represent a receiver’s interaction with a single file
+ * transfer offer within a batch. There is functionality here to
+ * skip or accept offered files and either to spool the packet
+ * payloads or to handle them yourself.
+ *
+ * @mixes Transfer_Offer_Mixin
+ */
+class Offer extends _Eventer {
+
+ /**
+ * Not called directly.
+ */
+ constructor(zfile_opts, file_info, accept_func, skip_func) {
+ super();
+
+ this._zfile_opts = zfile_opts;
+ this._file_info = file_info;
+
+ this._accept_func = accept_func;
+ this._skip_func = skip_func;
+
+ this._Add_event("input");
+ this._Add_event("complete");
+
+ //Register this first so that application handlers receive
+ //the updated offset.
+ this.on("input", this._input_handler);
+ }
+
+ _verify_not_skipped() {
+ if (this._skipped) {
+ throw new Zmodem.Error("Already skipped!");
+ }
+ }
+
+ /**
+ * Tell the sender that you don’t want the offered file.
+ *
+ * You can send this in lieu of `accept()` or after it, e.g.,
+ * if you find that the transfer is taking too long. Note that,
+ * if you `skip()` after you `accept()`, you’ll likely have to
+ * wait for buffers to clear out.
+ *
+ */
+ skip() {
+ this._verify_not_skipped();
+ this._skipped = true;
+
+ return this._skip_func.apply(this, arguments);
+ }
+
+ /**
+ * Tell the sender to send the offered file.
+ *
+ * @param {Object} [opts] - Can be:
+ * @param {string} [opts.oninput=spool_uint8array] - Can be:
+ *
+ * - `spool_uint8array`: Stores the ZMODEM
+ * packet payloads as Uint8Array instances.
+ * This makes for an easy transition to a Blob,
+ * which JavaScript can use to save the file to disk.
+ *
+ * - `spool_array`: Stores the ZMODEM packet payloads
+ * as Array instances. Each value is an octet value.
+ *
+ * - (function): A handler that receives each payload
+ * as it arrives. The Offer object does not store
+ * the payloads internally when thus configured.
+ *
+ * @return { Promise } Resolves when the file is fully received.
+ * If the Offer has been spooling
+ * the packet payloads, the promise resolves with an Array
+ * that contains those payloads.
+ */
+ accept(opts) {
+ this._verify_not_skipped();
+
+ if (this._accepted) {
+ throw new Zmodem.Error("Already accepted!");
+ }
+ this._accepted = true;
+
+ if (!opts) opts = {};
+
+ this._file_offset = opts.offset || 0;
+
+ switch (opts.on_input) {
+ case null:
+ case undefined:
+ case "spool_array":
+ case DEFAULT_RECEIVE_INPUT_MODE: //default
+ this._spool = [];
+ break;
+ default:
+ if (typeof opts.on_input !== "function") {
+ throw "Invalid “on_input”: " + opts.on_input;
+ }
+ }
+
+ this._input_handler_mode = opts.on_input || DEFAULT_RECEIVE_INPUT_MODE;
+
+ return this._accept_func(this._file_offset).then( this._get_spool.bind(this) );
+ }
+
+ _input_handler(payload) {
+ this._file_offset += payload.length;
+
+ if (typeof this._input_handler_mode === "function") {
+ this._input_handler_mode(payload);
+ }
+ else {
+ if (this._input_handler_mode === DEFAULT_RECEIVE_INPUT_MODE) {
+ payload = new Uint8Array(payload);
+ }
+
+ //sanity
+ else if (this._input_handler_mode !== "spool_array") {
+ throw new Zmodem.Error("WTF?? _input_handler_mode = " + this._input_handler_mode);
+ }
+
+ this._spool.push(payload);
+ }
+ }
+
+ _get_spool() {
+ return this._spool;
+ }
+}
+Object.assign( Offer.prototype, Transfer_Offer_Mixin );
+
+//Curious that ZSINIT isn’t here … but, lsz sends it as hex.
+const SENDER_BINARY_HEADER = {
+ ZFILE: true,
+ ZDATA: true,
+};
+
+/**
+ * A class that encapsulates behavior for a ZMODEM sender.
+ *
+ * @extends Session
+ */
+Zmodem.Session.Send = class ZmodemSendSession extends Zmodem.Session {
+
+ /**
+ * Not called directly.
+ */
+ constructor(zrinit_hdr) {
+ super();
+
+ if (!zrinit_hdr) {
+ throw "Need first header!";
+ }
+ else if (zrinit_hdr.NAME !== "ZRINIT") {
+ throw("First header should be ZRINIT, not " + zrinit_hdr.NAME);
+ }
+
+ this._last_header_name = 'ZRINIT';
+
+ //We don’t need to send crc32. Even if the other side can grok it,
+ //there’s no point to sending it since, for now, we assume we’re
+ //on a reliable connection, e.g., TCP. Ideally we’d just forgo
+ //CRC checks completely, but ZMODEM doesn’t allow that.
+ //
+ //If we *were* to start using crc32, we’d update this every time
+ //we send a header.
+ this._subpacket_encode_func = 'encode16';
+
+ this._zencoder = new Zmodem.ZDLE();
+
+ this._consume_ZRINIT(zrinit_hdr);
+
+ this._file_offset = 0;
+
+ var zrqinit_count = 0;
+
+ this._start_keepalive_on_set_sender = true;
+
+ //lrzsz will send ZRINIT until it gets an offer. (keep-alive?)
+ //It sends 4 additional ones after the initial ZRINIT and, if
+ //no response is received, starts sending “C” (0x43, 67) as if to
+ //try to downgrade to XMODEM or YMODEM.
+ //var sess = this;
+ //this._prepare_to_receive_ZRINIT( function keep_alive() {
+ // sess._prepare_to_receive_ZRINIT(keep_alive);
+ //} );
+
+ //queue up the ZSINIT flag to send -- but seems useless??
+
+ /*
+ Object.assign(
+ this._on_evt,
+ {
+ file_received: [],
+ },
+ };
+ */
+ }
+
+ /**
+ * Sets the sender function. The first time this is called,
+ * it will also initiate a keepalive using ZSINIT until the
+ * first file is sent.
+ *
+ * @param {Function} func - The function to call.
+ * It will receive an Array with the relevant octets.
+ *
+ * @return {Session} The session object (for chaining).
+ */
+ set_sender(func) {
+ super.set_sender(func);
+
+ if (this._start_keepalive_on_set_sender) {
+ this._start_keepalive_on_set_sender = false;
+ this._start_keepalive();
+ }
+
+ return this;
+ }
+
+ //7.3.3 .. The sender also uses hex headers when they are
+ //not followed by binary data subpackets.
+ //
+ //FG: … or when the header is ZSINIT? That’s what lrzsz does, anyway.
+ //Then it sends a single NUL byte as the payload to an end_ack subpacket.
+ _get_header_formatter(name) {
+ return SENDER_BINARY_HEADER[name] ? "to_binary16" : "to_hex";
+ }
+
+ //In order to keep lrzsz from timing out, we send ZSINIT every 5 seconds.
+ //Maybe make this configurable?
+ _start_keepalive() {
+ //if (this._keepalive_promise) throw "Keep-alive already started!";
+ if (!this._keepalive_promise) {
+ var sess = this;
+
+ this._keepalive_promise = new Promise(function(resolve) {
+ //console.log("SETTING KEEPALIVE TIMEOUT");
+ sess._keepalive_timeout = setTimeout(resolve, KEEPALIVE_INTERVAL);
+ }).then( function() {
+ sess._next_header_handler = {
+ ZACK: function() {
+
+ //We’re going to need to ensure that the
+ //receiver is ready for all control characters
+ //to be escaped. If we’ve already sent a ZSINIT
+ //and gotten a response, then we know that that
+ //work is already done later on when we actually
+ //send an offer.
+ sess._got_ZSINIT_ZACK = true;
+ },
+ };
+ sess._send_ZSINIT();
+
+ sess._keepalive_promise = null;
+ sess._start_keepalive();
+ });
+ }
+ }
+
+ _stop_keepalive() {
+ if (this._keepalive_promise) {
+ //console.log("STOPPING KEEPALIVE");
+ clearTimeout(this._keepalive_timeout);
+ this._keep_alive_promise = null;
+ }
+ }
+
+ _send_ZSINIT() {
+ //See note at _ensure_receiver_escapes_ctrl_chars()
+ //for why we have to pass ESCCTL.
+
+ var zsinit_flags = [];
+ if (this._zencoder.escapes_ctrl_chars()) {
+ zsinit_flags.push("ESCCTL");
+ }
+
+ this._send_header_and_data(
+ ["ZSINIT", zsinit_flags],
+ [0],
+ "end_ack"
+ );
+ }
+
+ _consume_ZRINIT(hdr) {
+ this._last_ZRINIT = hdr;
+
+ if (hdr.get_buffer_size()) {
+ throw( "Buffer size (" + hdr.get_buffer_size() + ") is unsupported!" );
+ }
+
+ if (!hdr.can_full_duplex()) {
+ throw( "Half-duplex I/O is unsupported!" );
+ }
+
+ if (!hdr.can_overlap_io()) {
+ throw( "Non-overlap I/O is unsupported!" );
+ }
+
+ if (hdr.escape_8th_bit()) {
+ throw( "8-bit escaping is unsupported!" );
+ }
+
+ if (FORCE_ESCAPE_CTRL_CHARS) {
+ this._zencoder.set_escape_ctrl_chars(true);
+ if (!hdr.escape_ctrl_chars()) {
+ console.debug("Peer didn’t request escape of all control characters. Will send ZSINIT to force recognition of escaped control characters.");
+ }
+ }
+ else {
+ this._zencoder.set_escape_ctrl_chars(hdr.escape_ctrl_chars());
+ }
+ }
+
+ //https://stackoverflow.com/questions/23155939/missing-0xf-and-0x16-when-binary-data-through-virtual-serial-port-pair-created-b
+ //^^ Because of that, we always escape control characters.
+ //The alternative would be that lrz would never receive those
+ //two bytes from zmodem.js.
+ _ensure_receiver_escapes_ctrl_chars() {
+ var promise;
+
+ var needs_ZSINIT = !this._last_ZRINIT.escape_ctrl_chars() && !this._got_ZSINIT_ZACK;
+
+ if (needs_ZSINIT) {
+ var sess = this;
+ promise = new Promise( function(res) {
+ sess._next_header_handler = {
+ ZACK: (hdr) => {
+ res();
+ },
+ };
+ sess._send_ZSINIT();
+ } );
+ }
+ else {
+ promise = Promise.resolve();
+ }
+
+ return promise;
+ }
+
+ _convert_params_to_offer_payload_array(params) {
+ params = Zmodem.Validation.offer_parameters(params);
+
+ var subpacket_payload = params.name + "\x00";
+
+ var subpacket_space_pieces = [
+ (params.size || 0).toString(10),
+ params.mtime ? params.mtime.toString(8) : "0",
+ params.mode ? (0x8000 | params.mode).toString(8) : "0",
+ "0", //serial
+ ];
+
+ if (params.files_remaining) {
+ subpacket_space_pieces.push( params.files_remaining );
+
+ if (params.bytes_remaining) {
+ subpacket_space_pieces.push( params.bytes_remaining );
+ }
+ }
+
+ subpacket_payload += subpacket_space_pieces.join(" ");
+ return this._string_to_octets(subpacket_payload);
+ }
+
+ /**
+ * Send an offer to the receiver.
+ *
+ * @param {FileDetails} params - All about the file you want to transfer.
+ *
+ * @returns {Promise} If the receiver accepts the offer, then the
+ * resolution is a Transfer object; otherwise the resolution is
+ * undefined.
+ */
+ send_offer(params) {
+ if (Zmodem.DEBUG) {
+ console.debug("SENDING OFFER", params);
+ }
+
+ if (!params) throw "need file params!";
+
+ if (this._sending_file) throw "Already sending file!";
+
+ var payload_array = this._convert_params_to_offer_payload_array(params);
+
+ this._stop_keepalive();
+
+ var sess = this;
+
+ function zrpos_handler_setter_func() {
+ sess._next_header_handler = {
+
+ // The receiver may send ZRPOS in at least two cases:
+ //
+ // 1) A malformed subpacket arrived, so we need to
+ // “rewind” a bit and continue from the receiver’s
+ // last-successful location in the file.
+ //
+ // 2) The receiver hasn’t gotten any data for a bit,
+ // so it sends ZRPOS as a “ping”.
+ //
+ // Case #1 shouldn’t happen since zmodem.js requires a
+ // reliable transport. Case #2, though, can happen due
+ // to either normal network congestion or errors in
+ // implementation. In either case, there’s nothing for
+ // us to do but to ignore the ZRPOS, with an optional
+ // warning.
+ //
+ ZRPOS: function(hdr) {
+ if (Zmodem.DEBUG) {
+ console.warn("Mid-transfer ZRPOS … implementation error?");
+ }
+
+ zrpos_handler_setter_func();
+ },
+ };
+ };
+
+ var doer_func = function() {
+
+ //return Promise object that is fulfilled when the ZRPOS or ZSKIP arrives.
+ //The promise value is the byte offset, or undefined for ZSKIP.
+ //If ZRPOS arrives, then send ZDATA(0) and set this._sending_file.
+ var handler_setter_promise = new Promise( function(res) {
+ sess._next_header_handler = {
+ ZSKIP: function() {
+ sess._start_keepalive();
+ res();
+ },
+ ZRPOS: function(hdr) {
+ sess._sending_file = true;
+
+ zrpos_handler_setter_func();
+
+ res(
+ new Transfer(
+ params,
+ hdr.get_offset(),
+ sess._send_interim_file_piece.bind(sess),
+ sess._end_file.bind(sess)
+ )
+ );
+ },
+ };
+ } );
+
+ sess._send_header_and_data( ["ZFILE"], payload_array, "end_ack" );
+
+ delete sess._sent_ZDATA;
+
+ return handler_setter_promise;
+ };
+
+ if (FORCE_ESCAPE_CTRL_CHARS) {
+ return this._ensure_receiver_escapes_ctrl_chars().then(doer_func);
+ }
+
+ return doer_func();
+ }
+
+ _send_header_and_data( hdr_name_and_args, data_arr, frameend ) {
+ var bytes_hdr = this._create_header_bytes(hdr_name_and_args);
+
+ var data_bytes = this._build_subpacket_bytes(data_arr, frameend);
+
+ bytes_hdr[0].push.apply( bytes_hdr[0], data_bytes );
+
+ if (Zmodem.DEBUG) {
+ this._log_header( "SENDING HEADER", bytes_hdr[1] );
+ console.debug( this.type, "-- HEADER PAYLOAD:", frameend, data_bytes.length );
+ }
+
+ this._sender( bytes_hdr[0] );
+
+ this._last_sent_header = bytes_hdr[1];
+ }
+
+ _build_subpacket_bytes( bytes_arr, frameend ) {
+ var subpacket = Zmodem.Subpacket.build(bytes_arr, frameend);
+
+ return subpacket[this._subpacket_encode_func]( this._zencoder );
+ }
+
+ _build_and_send_subpacket( bytes_arr, frameend ) {
+ this._sender( this._build_subpacket_bytes(bytes_arr, frameend) );
+ }
+
+ _string_to_octets(string) {
+ if (!this._textencoder) {
+ this._textencoder = new Zmodem.Text.Encoder();
+ }
+
+ var uint8arr = this._textencoder.encode(string);
+ return Array.prototype.slice.call(uint8arr);
+ }
+
+ /*
+ Potential future support for responding to ZRPOS:
+ send_file_offset(offset) {
+ }
+ */
+
+ /*
+ Sending logic works thus:
+ - ASSUME the receiver can overlap I/O (CANOVIO)
+ (so fail if !CANFDX || !CANOVIO)
+ - Sender opens the firehose … all ZCRCG (!end/!ack)
+ until the end, when we send a ZCRCE (end/!ack)
+ NB: try 8k/32k/64k chunk sizes? Looks like there’s
+ no need to change the packet otherwise.
+ */
+ //TODO: Put this on a Transfer object similar to what Receive uses?
+ _send_interim_file_piece(bytes_obj) {
+
+ //We don’t ask the receiver to confirm because there’s no need.
+ this._send_file_part(bytes_obj, "no_end_no_ack");
+
+ //This pattern will allow
+ //error-correction without buffering the entire stream in JS.
+ //For now the promise is always resolved, but in the future we
+ //can make it only resolve once we’ve gotten acknowledgement.
+ return Promise.resolve();
+ }
+
+ _ensure_we_are_sending() {
+ if (!this._sending_file) throw "Not sending a file currently!";
+ }
+
+ //This resolves once we receive ZEOF.
+ _end_file(bytes_obj) {
+ this._ensure_we_are_sending();
+
+ //Is the frame-end-ness of this last packet redundant
+ //with the ZEOF packet?? - No. It signals the receiver that
+ //the next thing to expect is a header, not a packet.
+
+ //no-ack, following lrzsz’s example
+ this._send_file_part(bytes_obj, "end_no_ack");
+
+ var sess = this;
+
+ //Register this before we send ZEOF in case of local round-trip.
+ //(Basically just for synchronous testing, but.)
+ var ret = new Promise( function(res) {
+ //console.log("UNSETTING SENDING FLAG");
+ sess._sending_file = false;
+ sess._prepare_to_receive_ZRINIT(res);
+ } );
+
+ this._send_header( "ZEOF", this._file_offset );
+
+ this._file_offset = 0;
+
+ return ret;
+ }
+
+ //Called at the beginning of our session
+ //and also when we’re done sending a file.
+ _prepare_to_receive_ZRINIT(after_consume) {
+ this._next_header_handler = {
+ ZRINIT: function(hdr) {
+ this._consume_ZRINIT(hdr);
+ if (after_consume) after_consume();
+ },
+ };
+ }
+
+ /**
+ * Signal to the receiver that the ZMODEM session is wrapping up.
+ *
+ * @returns {Promise} Resolves when the receiver has responded to
+ * our signal that the session is over.
+ */
+ close() {
+ var ok_to_close = (this._last_header_name === "ZRINIT")
+ if (!ok_to_close) {
+ ok_to_close = (this._last_header_name === "ZSKIP");
+ }
+ if (!ok_to_close) {
+ ok_to_close = (this._last_sent_header.name === "ZSINIT") && (this._last_header_name === "ZACK");
+ }
+
+ if (!ok_to_close) {
+ throw( "Can’t close; last received header was “" + this._last_header_name + "”" );
+ }
+
+ var sess = this;
+
+ var ret = new Promise( function(res, rej) {
+ sess._next_header_handler = {
+ ZFIN: function() {
+ sess._sender( OVER_AND_OUT );
+ sess._sent_OO = true;
+ sess._on_session_end();
+ res();
+ },
+ };
+ } );
+
+ this._send_header("ZFIN");
+
+ return ret;
+ }
+
+ _has_ended() {
+ return this.aborted() || !!this._sent_OO;
+ }
+
+ _send_file_part(bytes_obj, final_packetend) {
+ if (!this._sent_ZDATA) {
+ this._send_header( "ZDATA", this._file_offset );
+ this._sent_ZDATA = true;
+ }
+
+ var obj_offset = 0;
+
+ var bytes_count = bytes_obj.length;
+
+ //We have to go through at least once in event of an
+ //empty buffer, e.g., an empty end_file.
+ while (true) {
+ var chunk_size = Math.min(obj_offset + MAX_CHUNK_LENGTH, bytes_count) - obj_offset;
+
+ var at_end = (chunk_size + obj_offset) >= bytes_count;
+
+ var chunk = bytes_obj.slice( obj_offset, obj_offset + chunk_size );
+ if (!(chunk instanceof Array)) {
+ chunk = Array.prototype.slice.call(chunk);
+ }
+
+ this._build_and_send_subpacket(
+ chunk,
+ at_end ? final_packetend : "no_end_no_ack"
+ );
+
+ this._file_offset += chunk_size;
+ obj_offset += chunk_size;
+
+ if (obj_offset >= bytes_count) break;
+ }
+ }
+
+ _consume_first() {
+ if (!this._parse_and_consume_header()) {
+
+ //When the ZMODEM receive program starts, it immediately sends
+ //a ZRINIT header to initiate ZMODEM file transfers, or a
+ //ZCHALLENGE header to verify the sending program. The receive
+ //program resends its header at response time (default 10 second)
+ //intervals for a suitable period of time (40 seconds total)
+ //before falling back to YMODEM protocol.
+ if (this._input_buffer.join() === "67") {
+ throw "Receiver has fallen back to YMODEM.";
+ }
+ }
+ }
+
+ _on_session_end() {
+ this._stop_keepalive();
+ super._on_session_end();
+ }
+}
+
+Object.assign(
+ Zmodem.Session.Send.prototype,
+ {
+ type: "send",
+ }
+);
diff --git a/src/zsubpacket.js b/src/zsubpacket.js
new file mode 100644
index 0000000..f77a527
--- /dev/null
+++ b/src/zsubpacket.js
@@ -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,
+};
diff --git a/src/zvalidation.js b/src/zvalidation.js
new file mode 100644
index 0000000..e8618e9
--- /dev/null
+++ b/src/zvalidation.js
@@ -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;
+ },
+};
diff --git a/tests/encode.js b/tests/encode.js
new file mode 100755
index 0000000..615635f
--- /dev/null
+++ b/tests/encode.js
@@ -0,0 +1,119 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+
+var enclib = Zmodem.ENCODELIB;
+
+tape('round-trip: 32-bit little-endian', function(t) {
+ var times = 1000;
+
+ t.doesNotThrow(
+ () => {
+ for (var a=0; a<times; a++) {
+ var orig = Math.floor( 0xffffffff * Math.random() );
+
+ var enc = enclib.pack_u32_le(orig);
+ var roundtrip = enclib.unpack_u32_le(enc);
+
+ if (roundtrip !== orig) {
+ throw( `Orig: ${orig}, Packed: ` + JSON.stringify(enc) + `, Parsed: ${roundtrip}` );
+ }
+ }
+ },
+ `round-trip 32-bit little-endian: ${times} times`
+ );
+
+ t.end();
+} );
+
+tape('unpack_u32_le', function(t) {
+ t.equals(
+ enclib.unpack_u32_le([222,233,202,254]),
+ 4274711006,
+ 'unpack 4-byte number'
+ );
+
+ var highest = 0xffffffff;
+ t.equals(
+ enclib.unpack_u32_le([255,255,255,255]),
+ highest,
+ `highest number possible (${highest})`
+ );
+
+ t.equals(
+ enclib.unpack_u32_le([1, 0, 0, 0]),
+ 1,
+ '1'
+ );
+
+ t.end();
+});
+
+tape('unpack_u16_be', function(t) {
+ t.equals(
+ enclib.unpack_u16_be([202,254]),
+ 51966,
+ 'unpack 2-byte number'
+ );
+
+ var highest = 0xffff;
+ t.equals(
+ enclib.unpack_u16_be([255,255]),
+ highest,
+ `highest number possible (${highest})`
+ );
+
+ t.equals(
+ enclib.unpack_u16_be([0, 1]),
+ 1,
+ '1'
+ );
+
+ t.end();
+});
+
+tape('octets_to_hex', function(t) {
+ t.deepEquals(
+ enclib.octets_to_hex( [ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x0a ] ),
+ '123456789abcdef00a'.split("").map( (c) => c.charCodeAt(0) ),
+ 'hex encoding'
+ );
+
+ t.end();
+} );
+
+tape('parse_hex_octets', function(t) {
+ t.deepEquals(
+ enclib.parse_hex_octets( [ 48, 49, 102, 101 ] ),
+ [ 0x01, 0xfe ],
+ 'parse hex excoding',
+ );
+
+ t.end();
+} );
+
+tape('round-trip: 16-bit big-endian', function(t) {
+ var times = 10000;
+
+ t.doesNotThrow(
+ () => {
+ for (var a=0; a<times; a++) {
+ var orig = Math.floor( 0x10000 * Math.random() );
+
+ var enc = enclib.pack_u16_be(orig);
+ var roundtrip = enclib.unpack_u16_be(enc);
+
+ if (roundtrip !== orig) {
+ throw( `Orig: ${orig}, Packed: ` + JSON.stringify(enc) + `, Parsed: ${roundtrip}` );
+ }
+ }
+ },
+ `round-trip 16-bit big-endian: ${times} times`
+ );
+
+ t.end();
+} );
diff --git a/tests/lib/testhelp.js b/tests/lib/testhelp.js
new file mode 100644
index 0000000..ae1ab69
--- /dev/null
+++ b/tests/lib/testhelp.js
@@ -0,0 +1,121 @@
+var Zmodem = require('./zmodem');
+
+module.exports = {
+ /**
+ * Return an array with the given number of random octet values.
+ *
+ * @param {Array} count - The number of octet values to return.
+ *
+ * @returns {Array} The octet values.
+ */
+ get_random_octets(count) {
+ if (!(count > 0)) throw( "Must be positive, not " + count );
+
+ var octets = [];
+
+ //This assigns backwards both for convenience and so that
+ //the initial assignment allocates the needed size.
+ while (count) {
+ octets[count - 1] = Math.floor( Math.random() * 256 );
+ count--;
+ }
+
+ return octets;
+ },
+
+ //This is meant NOT to do UTF-8 stuff since it handles \xXX.
+ string_to_octets(string) {
+ return string.split("").map( (c) => c.charCodeAt(0) );
+ },
+
+ make_temp_dir() {
+ return require('tmp').dirSync().name;
+ },
+
+ make_temp_file(size) {
+ const fs = require('fs');
+ const tmp = require('tmp');
+
+ var tmpobj = tmp.fileSync();
+ var content = Array(size).fill("x").join("");
+ fs.writeSync( tmpobj.fd, content );
+ fs.writeSync( tmpobj.fd, "=THE_END" );
+ fs.closeSync( tmpobj.fd );
+
+ return tmpobj.name;
+ },
+
+ make_empty_temp_file() {
+ const fs = require('fs');
+ const tmp = require('tmp');
+
+ var tmpobj = tmp.fileSync();
+ fs.closeSync( tmpobj.fd );
+
+ return tmpobj.name;
+ },
+
+ exec_lrzsz_steps(t, binpath, z_args, steps) {
+ const spawn = require('child_process').spawn;
+
+ var child;
+
+ var zsession;
+ var zsentry = new Zmodem.Sentry( {
+ to_terminal: Object,
+ on_detect: (d) => { zsession = d.confirm() },
+ on_retract: console.error.bind(console),
+ sender: (d) => {
+ child.stdin.write( new Buffer(d) );
+ },
+ } );
+
+ var step = 0;
+ var inputs = [];
+
+ child = spawn(binpath, z_args);
+ console.log("child PID:", child.pid);
+
+ child.on("error", console.error.bind(console));
+
+ child.stdin.on("close", () => console.log(`# PID ${child.pid} STDIN closed`));
+ child.stdout.on("close", () => console.log(`# PID ${child.pid} STDOUT closed`));
+ child.stderr.on("close", () => console.log(`# PID ${child.pid} STDERR closed`));
+
+ //We can’t just pipe this on through because there can be lone CR
+ //bytes which screw up TAP::Harness.
+ child.stderr.on("data", (d) => {
+ d = d.toString().replace(/\r\n?/g, "\n");
+ if (d.substr(-1) !== "\n") d += "\n";
+ process.stderr.write(`STDERR: ${d}`);
+ });
+
+ child.stdout.on("data", (d) => {
+ //console.log(`STDOUT from PID ${child.pid}`, d);
+ inputs.push( Array.from(d) );
+
+ zsentry.consume( Array.from(d) );
+
+ if (zsession) {
+ if ( steps[step] ) {
+ if ( steps[step](zsession, child) ) {
+ step++;
+ }
+ }
+ else {
+ console.log(`End of task list; closing PID ${child.pid}’s STDIN`);
+ child.stdin.end();
+ }
+ }
+ });
+
+ var exit_promise = new Promise( (res, rej) => {
+ child.on("exit", (code, signal) => {
+ console.log(`# "${binpath}" exit: code ${code}, signal ${signal}`);
+ res([code, signal]);
+ } );
+ } );
+
+ return exit_promise.then( () => { return inputs } );
+ },
+};
diff --git a/tests/lib/zmodem.js b/tests/lib/zmodem.js
new file mode 100644
index 0000000..8b485b4
--- /dev/null
+++ b/tests/lib/zmodem.js
@@ -0,0 +1 @@
+module.exports = require('../../src/zmodem.js');
diff --git a/tests/text.js b/tests/text.js
new file mode 100755
index 0000000..cdb5200
--- /dev/null
+++ b/tests/text.js
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var Zmodem = require('../src/zmodem');
+
+var ZText = Zmodem.Text;
+
+const TEXTS = [
+ [ "-./", [45, 46, 47] ],
+ [ "épée", [195, 169, 112, 195, 169, 101] ],
+ [ "“words”", [226, 128, 156, 119, 111, 114, 100, 115, 226, 128, 157] ],
+ [ "🍊", [240, 159, 141, 138] ],
+ [ "🍊🍊", [240, 159, 141, 138, 240, 159, 141, 138] ],
+];
+
+tape('decoder', function(t) {
+ var decoder = new ZText.Decoder();
+
+ TEXTS.forEach( (tt) => {
+ t.is(
+ decoder.decode( new Uint8Array(tt[1]) ),
+ tt[0],
+ `decode: ${tt[1]} -> ${tt[0]}`
+ );
+ } );
+
+ t.end();
+} );
+
+tape('encoder', function(t) {
+ var encoder = new ZText.Encoder();
+
+ TEXTS.forEach( (tt) => {
+ t.deepEquals(
+ encoder.encode(tt[0]),
+ new Uint8Array( tt[1] ),
+ `encode: ${tt[0]} -> ${tt[1]}`
+ );
+ } );
+
+ t.end();
+} );
diff --git a/tests/zcrc.js b/tests/zcrc.js
new file mode 100755
index 0000000..0698fa4
--- /dev/null
+++ b/tests/zcrc.js
@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var Zmodem = Object.assign(
+ {},
+ require('../src/zcrc')
+);
+
+var zcrc = Zmodem.CRC;
+
+tape('crc16', function(t) {
+ t.deepEqual(
+ zcrc.crc16( [ 0x0d, 0x0a ] ),
+ [ 0xd7, 0x16 ],
+ 'crc16 - first test'
+ );
+
+ t.deepEqual(
+ zcrc.crc16( [ 0x11, 0x17, 0, 0, 0 ] ),
+ [ 0xe4, 0x81 ],
+ 'crc16 - second test'
+ );
+
+ t.end();
+} );
+
+tape('verify16', function(t) {
+ t.doesNotThrow(
+ () => zcrc.verify16( [ 0x0d, 0x0a ], [ 0xd7, 0x16 ] ),
+ 'verify16 - no throw on good'
+ );
+
+ var err;
+ try { zcrc.verify16( [ 0x0d, 0x0a ], [ 0xd7, 16 ] ) }
+ catch(e) { err = e };
+
+ t.ok(
+ /215,16.*215,22/.test(err.message),
+ 'verify16 - throw on bad (message)'
+ );
+
+ t.ok(
+ err instanceof Zmodem.Error,
+ 'verify16 - typed error'
+ );
+
+ t.ok(
+ err.type,
+ 'verify16 - error type'
+ );
+
+ t.end();
+} );
+
+//----------------------------------------------------------------------
+// The crc32 logic is unused for now, but some misbehaving ZMODEM
+// implementation might send CRC32 regardless of that we don’t
+// advertise it.
+//----------------------------------------------------------------------
+
+tape('crc32', function(t) {
+ const tests = [
+ [ [ 4, 0, 0, 0, 0 ], [ 0xdd, 0x51, 0xa2, 0x33 ] ],
+ [ [ 11, 17, 0, 0, 0 ], [ 0xf6, 0xf6, 0x57, 0x59 ] ],
+ [ [ 3, 0, 0, 0, 0 ], [ 205, 141, 130, 129 ] ],
+ ];
+// } [ 3, 0, 0, 0, 0 ] [ 205, 141, 131, -127 ]
+//2172816845
+//crc32 [ 3, 0, 0, 0, 0 ] -2122150451
+
+ tests.forEach( (cur_t) => {
+ let [ input, output ] = cur_t;
+
+ t.deepEqual(
+ zcrc.crc32(input),
+ output,
+ "crc32: " + input.join(", ")
+ );
+ } );
+
+ t.end();
+} );
+
+tape('verify32', function(t) {
+ t.doesNotThrow(
+ () => zcrc.verify32( [ 4, 0, 0, 0, 0 ], [ 0xdd, 0x51, 0xa2, 0x33 ] ),
+ 'verify32 - no throw on good'
+ );
+
+ var err;
+ try { zcrc.verify32( [ 4, 0, 0, 0, 0 ], [ 1,2,3,4 ] ) }
+ catch(e) { err = e };
+
+ t.ok(
+ /1,2,3,4.*221,81,162,51/.test(err.message),
+ 'verify32 - throw on bad (message)'
+ );
+
+ t.ok(
+ err instanceof Zmodem.Error,
+ 'verify32 - typed error'
+ );
+
+ t.ok(
+ err.type,
+ 'verify32 - error type'
+ );
+
+ t.end();
+} );
diff --git a/tests/zdle.js b/tests/zdle.js
new file mode 100755
index 0000000..9fc49b7
--- /dev/null
+++ b/tests/zdle.js
@@ -0,0 +1,41 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+const helper = require('./lib/testhelp');
+
+var zmlib = Zmodem.ZMLIB;
+var ZDLE = Zmodem.ZDLE;
+
+tape('round-trip', function(t) {
+ var zdle = new ZDLE( { escape_ctrl_chars: true } );
+
+ var times = 1000;
+
+ t.doesNotThrow(
+ () => {
+ for (let a of Array(times)) {
+ var orig = helper.get_random_octets(38);
+ var enc = zdle.encode( orig.slice(0) );
+ var dec = ZDLE.decode( enc.slice(0) );
+
+ var orig_j = orig.join();
+ var dec_j = dec.join();
+
+ if (orig_j !== dec_j) {
+ console.error("Original", orig.join());
+ console.error("Encoded", enc.join());
+ console.error("Decoded", dec.join());
+
+ throw 'mismatch';
+ }
+ }
+ },
+ `round-trip`
+ );
+
+ t.end();
+} );
diff --git a/tests/zerror.js b/tests/zerror.js
new file mode 100644
index 0000000..f7961e6
--- /dev/null
+++ b/tests/zerror.js
@@ -0,0 +1,82 @@
+#!/usr/bin/env node
+
+"use strict";
+
+global.Zmodem = require('./lib/zmodem');
+
+const tape = require('blue-tape'),
+ TYPE_CHECKS = {
+ aborted: [ [] ],
+ peer_aborted: [],
+ already_aborted: [],
+ crc: [
+ [ [ 1, 2 ], [ 3, 4 ] ],
+ (t, err) => {
+ t.ok(
+ /1,2/.test(err.message),
+ '"got" values are in the message'
+ );
+ t.ok(
+ /3,4/.test(err.message),
+ '"expected" values are in the message'
+ );
+ t.ok(
+ /CRC/i.test(err.message),
+ '"CRC" is in the message'
+ );
+ },
+ ],
+ validation: [
+ [ "some string" ],
+ (t, err) => {
+ t.is(
+ err.message,
+ "some string",
+ 'message is given value'
+ );
+ },
+ ],
+ }
+;
+
+tape("typed", (t) => {
+ let Ctr = Zmodem.Error;
+
+ for (let type in TYPE_CHECKS) {
+ let args = [type].concat( TYPE_CHECKS[type][0] );
+
+ //https://stackoverflow.com/questions/33193310/constr-applythis-args-in-es6-classes
+ var err = new (Ctr.bind.apply(Ctr, [null].concat(args)));
+
+ t.ok(
+ (err instanceof Zmodem.Error),
+ `${type} type isa ZmodemError`
+ );
+ t.ok(
+ !!err.message.length,
+ `${type}: message has length`
+ );
+
+ if ( TYPE_CHECKS[type][1] ) {
+ TYPE_CHECKS[type][1](t, err);
+ }
+ }
+
+ t.end();
+});
+
+tape("generic", (t) => {
+ let err = new Zmodem.Error("Van Gogh was a guy.");
+
+ t.ok(
+ (err instanceof Zmodem.Error),
+ `generic isa ZmodemError`
+ );
+ t.is(
+ err.message,
+ "Van Gogh was a guy.",
+ "passthrough of string"
+ );
+
+ t.end();
+});
diff --git a/tests/zheader.js b/tests/zheader.js
new file mode 100755
index 0000000..b462da6
--- /dev/null
+++ b/tests/zheader.js
@@ -0,0 +1,309 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var testhelp = require('./lib/testhelp');
+
+global.Zmodem = require('./lib/zmodem');
+
+var zdle = new Zmodem.ZDLE( { escape_ctrl_chars: true } );
+
+tape('trim_leading_garbage', function(t) {
+ var header = Zmodem.Header.build('ZACK');
+
+ var header_octets = new Map( [
+ [ "hex", header.to_hex(), ],
+ [ "b16", header.to_binary16(zdle), ],
+ [ "b32", header.to_binary32(zdle), ],
+ ] );
+
+ var leading_garbage = [
+ "",
+ " ",
+ "\n\n",
+ "\r\n\r\n",
+ "*",
+ "**",
+ "*\x18",
+ "*\x18D",
+ "**\x18",
+ ];
+
+ leading_garbage.forEach( (garbage) => {
+ let garbage_json = JSON.stringify(garbage);
+ let garbage_octets = testhelp.string_to_octets( garbage );
+
+ for ( let [label, hdr_octets] of header_octets ) {
+ var input = garbage_octets.slice(0).concat( hdr_octets );
+ var trimmed = Zmodem.Header.trim_leading_garbage(input);
+
+ t.deepEquals(trimmed, garbage_octets, `${garbage_json} + ${label}: garbage trimmed`);
+ t.deepEquals(input, hdr_octets, `… leaving the header`);
+ }
+ } );
+
+ //----------------------------------------------------------------------
+
+ //input, number of bytes trimmed
+ var partial_trims = [
+ [ "*", 0 ],
+ [ "**", 0 ],
+ [ "***", 1 ],
+ [ "*\x18**", 2 ],
+ [ "*\x18*\x18", 2 ],
+ [ "*\x18*\x18**", 4 ],
+ [ "*\x18*\x18*\x18", 4 ],
+ ];
+
+ partial_trims.forEach( (cur) => {
+ let [ input, trimmed_count ] = cur;
+
+ let input_json = JSON.stringify(input);
+
+ let input_octets = testhelp.string_to_octets(input);
+
+ let garbage = Zmodem.Header.trim_leading_garbage(input_octets.slice(0));
+
+ t.deepEquals(
+ garbage,
+ input_octets.slice(0, trimmed_count),
+ `${input_json}: trim first ${trimmed_count} byte(s)`
+ );
+ } );
+
+ t.end();
+});
+
+//Test that we parse a trailing 0x8a, since we ourselves follow the
+//documentation and put a plain LF (0x0a).
+tape('parse_hex', function(t) {
+ var octets = testhelp.string_to_octets( "**\x18B0901020304a57f\x0d\x8a" );
+
+ var parsed = Zmodem.Header.parse( octets );
+
+ t.is( parsed[1], 16, 'CRC size' );
+
+ t.is(
+ parsed[0].NAME,
+ 'ZRPOS',
+ 'parsed NAME'
+ );
+
+ t.is(
+ parsed[0].TYPENUM,
+ 9,
+ 'parsed TYPENUM'
+ );
+
+ t.is(
+ parsed[0].get_offset(),
+ 0x04030201, //it’s little-endian
+ 'parsed offset'
+ );
+
+ t.end();
+} );
+
+tape('round-trip, empty headers', function(t) {
+ ["ZRQINIT", "ZSKIP", "ZABORT", "ZFIN", "ZFERR"].forEach( (n) => {
+ var orig = Zmodem.Header.build(n);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ for ( const [ enc, h ] of rounds ) {
+ let [ parsed, crclen ] = Zmodem.Header.parse(h);
+
+ t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );
+
+ //Here’s where we test the CRC length in the response.
+ t.is(
+ crclen,
+ /32/.test(enc) ? 32 : 16,
+ `${n}, ${enc}: CRC length`,
+ );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('round-trip, offset headers', function(t) {
+ ["ZRPOS", "ZDATA", "ZEOF"].forEach( (n) => {
+ var orig = Zmodem.Header.build(n, 12345);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ for ( const [ enc, h ] of rounds ) {
+ //Here’s where we test that parse() leaves in trailing bytes.
+ let extra = [99, 99, 99];
+ let bytes_with_extra = h.slice().concat(extra);
+
+ let parsed = Zmodem.Header.parse(bytes_with_extra)[0];
+
+ t.is( parsed.NAME, orig.NAME, `${n}, ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `${n}, ${enc}: TYPENUM` );
+ t.is( parsed.get_offset(), orig.get_offset(), `${n}, ${enc}: get_offset()` );
+
+ let expected = extra.slice(0);
+ if (enc === "to_hex") {
+ expected.splice( 0, 0, Zmodem.ZMLIB.XON );
+ }
+
+ t.deepEquals(
+ bytes_with_extra,
+ expected,
+ `${enc}: parse() leaves in trailing bytes`,
+ );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('round-trip, ZSINIT', function(t) {
+ var opts = [
+ [],
+ ["ESCCTL"],
+ ];
+
+ opts.forEach( (args) => {
+ var orig = Zmodem.Header.build("ZSINIT", args);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ var args_str = JSON.stringify(args);
+
+ for ( const [ enc, h ] of rounds ) {
+ let parsed = Zmodem.Header.parse(h)[0];
+
+ t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );
+
+ t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
+ t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('round-trip, ZRINIT', function(t) {
+ var opts = [];
+
+ [ [], ["CANFDX"] ].forEach( (canfdx) => {
+ [ [], ["CANOVIO"] ].forEach( (canovio) => {
+ [ [], ["CANBRK"] ].forEach( (canbrk) => {
+ [ [], ["CANFC32"] ].forEach( (canfc32) => {
+ [ [], ["ESCCTL"] ].forEach( (escctl) => {
+ opts.push( [
+ ...canfdx,
+ ...canovio,
+ ...canbrk,
+ ...canfc32,
+ ...escctl,
+ ] );
+ } );
+ } );
+ } );
+ } );
+ } );
+
+ opts.forEach( (args) => {
+ var orig = Zmodem.Header.build("ZRINIT", args);
+
+ var hex = orig.to_hex();
+ var b16 = orig.to_binary16(zdle);
+ var b32 = orig.to_binary32(zdle);
+
+ var rounds = new Map( [
+ [ "to_hex", hex ],
+ [ "to_binary16", b16 ],
+ [ "to_binary32", b32 ],
+ ] );
+
+ var args_str = JSON.stringify(args);
+
+ for ( const [ enc, h ] of rounds ) {
+ let parsed = Zmodem.Header.parse(h)[0];
+
+ t.is( parsed.NAME, orig.NAME, `opts ${args_str}: ${enc}: NAME` );
+ t.is( parsed.TYPENUM, orig.TYPENUM, `opts ${args_str}: ${enc}: TYPENUM` );
+
+ t.is( parsed.can_full_duplex(), orig.can_full_duplex(), `opts ${args_str}: ${enc}: can_full_duplex()` );
+ t.is( parsed.can_overlap_io(), orig.can_overlap_io(), `opts ${args_str}: ${enc}: can_overlap_io()` );
+ t.is( parsed.can_break(), orig.can_break(), `opts ${args_str}: ${enc}: can_break()` );
+ t.is( parsed.can_fcs_32(), orig.can_fcs_32(), `opts ${args_str}: ${enc}: can_fcs_32()` );
+ t.is( parsed.escape_ctrl_chars(), orig.escape_ctrl_chars(), `opts ${args_str}: ${enc}: escape_ctrl_chars()` );
+ t.is( parsed.escape_8th_bit(), orig.escape_8th_bit(), `opts ${args_str}: ${enc}: escape_8th_bit()` );
+ }
+ } );
+
+ t.end();
+} );
+
+tape('hex_final_XON', function(t) {
+ var hex_ZFIN = Zmodem.Header.build("ZFIN").to_hex();
+
+ t.notEquals(
+ hex_ZFIN.slice(-1)[0],
+ Zmodem.ZMLIB.XON,
+ 'ZFIN hex does NOT end with XON',
+ );
+
+ var hex_ZACK = Zmodem.Header.build("ZACK").to_hex();
+
+ t.notEquals(
+ hex_ZACK.slice(-1)[0],
+ Zmodem.ZMLIB.XON,
+ 'ZACK hex does NOT end with XON',
+ );
+
+ var headers = [
+ "ZRQINIT",
+ Zmodem.Header.build("ZRINIT", []),
+ Zmodem.Header.build("ZSINIT", []),
+ "ZRPOS",
+ "ZABORT",
+ "ZFERR",
+ ];
+
+ //These are the only headers we expect to send as hex … right?
+ headers.forEach( hdr => {
+ if (typeof hdr === "string") hdr = Zmodem.Header.build(hdr);
+
+ t.is(
+ hdr.to_hex().slice(-1)[0],
+ Zmodem.ZMLIB.XON,
+ `${hdr.NAME} hex ends with XON`
+ );
+ } );
+
+ t.end();
+} );
diff --git a/tests/zmlib.js b/tests/zmlib.js
new file mode 100755
index 0000000..e9dfc11
--- /dev/null
+++ b/tests/zmlib.js
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+
+var zmlib = Zmodem.ZMLIB;
+
+tape('constants', function(t) {
+ t.equal(typeof zmlib.ZDLE, "number", 'ZDLE');
+ t.equal(typeof zmlib.XON, "number", 'XON');
+ t.equal(typeof zmlib.XOFF, "number", 'XOFF');
+ t.end();
+} );
+
+tape('strip_ignored_bytes', function(t) {
+ var input = [ zmlib.XOFF, 12, 45, 76, zmlib.XON, 22, zmlib.XOFF, 32, zmlib.XON | 0x80, 0, zmlib.XOFF | 0x80, 255, zmlib.XON ];
+ var should_be = [ 12, 45, 76, 22, 32, 0, 255 ];
+
+ var input_copy = input.slice(0);
+
+ var out = zmlib.strip_ignored_bytes(input_copy);
+
+ t.deepEqual( out, should_be, 'intended bytes are stripped' );
+ t.equal( out, input_copy, 'output is the mutated input' );
+
+ t.end();
+} );
+
+/*
+tape('get_random_octets', function(t) {
+ t.equal(
+ zmlib.get_random_octets(42).length,
+ 42,
+ 'length is correct'
+ );
+
+ t.equal(
+ typeof zmlib.get_random_octets(42)[0],
+ "number",
+ 'type is correct'
+ );
+
+ t.ok(
+ zmlib.get_random_octets(999999).every( (i) => i>=0 && i<=255 ),
+ 'values are all octet values'
+ );
+
+ t.end();
+} );
+*/
+
+tape('find_subarray', function(t) {
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [43, 77]),
+ 2,
+ 'finds at end'
+ );
+
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [12, 56]),
+ 0,
+ 'finds at begin'
+ );
+
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [56, 43]),
+ 1,
+ 'finds in the middle'
+ );
+
+ t.equal(
+ zmlib.find_subarray([12, 56, 43, 77], [56, 43, 43]),
+ -1,
+ 'non-find'
+ );
+
+ t.end();
+} );
diff --git a/tests/zsentry.js b/tests/zsentry.js
new file mode 100755
index 0000000..d97f486
--- /dev/null
+++ b/tests/zsentry.js
@@ -0,0 +1,226 @@
+#!/usr/bin/env node
+
+"use strict";
+
+var tape = require('blue-tape');
+
+var helper = require('./lib/testhelp');
+
+global.Zmodem = require('./lib/zmodem');
+
+var ZSentry = Zmodem.Sentry;
+
+function _generate_tester() {
+ var tester = {
+ reset() {
+ this.to_terminal = [];
+ this.to_server = [];
+ this.retracted = 0;
+ }
+ };
+
+ tester.sentry = new ZSentry( {
+ to_terminal(octets) { tester.to_terminal.push.apply( tester.to_terminal, octets ) },
+ on_detect(z) { tester.detected = z; },
+ on_retract(z) { tester.retracted++; },
+ sender(octets) { tester.to_server.push.apply( tester.to_server, octets ) },
+ } );
+
+ tester.reset();
+
+ return tester;
+}
+
+tape('user says deny() to detection', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("hey**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.is( typeof tester.detected, "object", 'There is a session after ZRQINIT' );
+
+ var sent_before = tester.to_server.length;
+
+ tester.detected.deny();
+
+ t.deepEqual(
+ tester.to_server.slice(-Zmodem.ZMLIB.ABORT_SEQUENCE.length),
+ Zmodem.ZMLIB.ABORT_SEQUENCE,
+ 'deny() sends abort sequence to server',
+ );
+
+ t.end();
+} );
+
+tape('retraction because of non-ZMODEM', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("hey**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.is( typeof tester.detected, "object", 'There is a session after ZRQINIT' );
+
+ tester.sentry.consume([ 0x20, 0x21, 0x22 ]);
+
+ t.is( tester.retracted, 1, 'retraction since we got non-ZMODEM input' );
+
+ t.end();
+} );
+
+tape('retraction because of YMODEM downgrade', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.deepEquals( tester.to_server, [], 'nothing sent to server before' );
+
+ tester.sentry.consume( helper.string_to_octets("C") );
+
+ t.deepEquals( tester.to_server, Zmodem.ZMLIB.ABORT_SEQUENCE, 'abort sent to server' );
+
+ t.end();
+} );
+
+tape('replacement ZMODEM is not of same type', (t) => {
+ var tester = _generate_tester();
+
+ var zrqinit = helper.string_to_octets("**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(zrqinit);
+
+ var before = tester.to_terminal.length;
+
+ var zrinit = helper.string_to_octets("**\x18B0100000000aa51\x0d\x0a\x11");
+ tester.sentry.consume(zrinit);
+
+ t.notEqual(
+ tester.to_terminal.length,
+ before,
+ 'output to terminal when replacement session is of different type'
+ );
+
+ t.end();
+} );
+
+tape('retraction because of duplicate ZMODEM, and confirm()', (t) => {
+ var tester = _generate_tester();
+
+ var makes_offer = helper.string_to_octets("**\x18B00000000000000\x0d\x0a\x11");
+ tester.sentry.consume(makes_offer);
+
+ t.is( typeof tester.detected, "object", 'There is a detection after ZRQINIT' );
+
+ var first_detected = tester.detected;
+ t.is( first_detected.is_valid(), true, 'detection is valid' );
+
+ tester.reset();
+
+ tester.sentry.consume(makes_offer);
+
+ t.is( tester.retracted, 1, 'retraction since we got non-ZMODEM input' );
+ t.deepEquals( tester.to_terminal, [], 'nothing sent to terminal on dupe session' );
+
+ t.notEqual(
+ tester.detected,
+ first_detected,
+ '… but a new detection happened in its place',
+ );
+
+ t.is( first_detected.is_valid(), false, 'old detection is invalid' );
+ t.is( tester.detected.is_valid(), true, 'new detection is valid' );
+
+ //----------------------------------------------------------------------
+
+ var session = tester.detected.confirm();
+
+ t.is( (session instanceof Zmodem.Session), true, 'confirm() on the detection' );
+ t.is( session.type, "receive", 'session is of the right type' );
+
+ tester.reset();
+
+ //Verify that the Detection configures the Session correctly.
+ session.start();
+ t.is( !!tester.to_server.length, true, 'sent output after start()' );
+
+ t.end();
+} );
+
+tape('parse passthrough', (t) => {
+ var tester = _generate_tester();
+
+ var strings = new Map( [
+ [ "plain", "heyhey", ],
+ [ "one_asterisk", "hey*hey", ],
+ [ "two_asterisks", "hey**hey", ],
+ [ "wrong_header", "hey**\x18B09010203040506\x0d\x0a", ],
+ [ "ZRQINIT but not at end", "hey**\x18B00000000000000\x0d\x0ahahahaha", ],
+ [ "ZRINIT but not at end", "hey**\x18B01010203040506\x0d\x0ahahahaha", ],
+
+ //Use \x2a here to avoid tripping up ZMODEM-detection in
+ //text editors when working on this code.
+ [ "no_ZDLE", "hey\x2a*B00000000000000\x0d\x0a", ],
+ ] );
+
+ for (let [name, string] of strings) {
+ tester.reset();
+
+ var octets = helper.string_to_octets(string);
+
+ var before = octets.slice(0);
+
+ tester.sentry.consume(octets);
+
+ t.deepEquals(
+ tester.to_terminal,
+ before,
+ `regular text goes through: ${name}`
+ );
+
+ t.is( tester.detected, undefined, '... and there is no session' );
+ t.deepEquals( octets, before, '... and the array is unchanged' );
+ }
+
+ t.end();
+} );
+
+tape('parse', (t) => {
+ var hdrs = new Map( [
+ [ "receive", Zmodem.Header.build("ZRQINIT"), ],
+ [ "send", Zmodem.Header.build("ZRINIT", ["CANFDX", "CANOVIO", "ESCCTL"]), ],
+ ] );
+
+ for ( let [sesstype, hdr] of hdrs ) {
+ var full_input = helper.string_to_octets("before").concat(
+ hdr.to_hex()
+ );
+
+ for (var start=1; start<full_input.length - 1; start++) {
+ let octets1 = full_input.slice(0, start);
+ let octets2 = full_input.slice(start);
+
+ var tester = _generate_tester();
+ tester.sentry.consume(octets1);
+
+ t.deepEquals(
+ tester.to_terminal,
+ octets1,
+ `${sesstype}: Parse first ${start} byte(s) of text (${full_input.length} total)`
+ );
+ t.is( tester.detected, undefined, '... and there is no session' );
+
+ tester.reset();
+
+ tester.sentry.consume(octets2);
+ t.deepEquals(
+ tester.to_terminal,
+ octets2,
+ `Rest of text goes through`
+ );
+ t.is( typeof tester.detected, "object", '... and now there is a session' );
+ t.is( tester.detected.get_session_role(), sesstype, '... of the right type' );
+
+ }
+ };
+
+ t.end();
+} );
diff --git a/tests/zsession.js b/tests/zsession.js
new file mode 100755
index 0000000..e4b638f
--- /dev/null
+++ b/tests/zsession.js
@@ -0,0 +1,312 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const test = require('tape');
+
+const helper = require('./lib/testhelp');
+global.Zmodem = require('./lib/zmodem');
+
+var ZSession = Zmodem.Session;
+
+var receiver, sender, sender_promise, received_file;
+
+var offer;
+
+function wait(seconds) {
+ return new Promise( resolve => setTimeout(_ => resolve("theValue"), 1000 * seconds) );
+}
+
+function _init(async) {
+ sender = null;
+ receiver = new Zmodem.Session.Receive();
+
+ /*
+ receiver.on("receive", function(hdr) {
+ console.log("Receiver input", hdr);
+ } );
+ receiver.on("offer", function(my_offer) {
+ //console.log("RECEIVED OFFER (window.offer)", my_offer);
+ offer = my_offer;
+ });
+ */
+
+ var resolver;
+ sender_promise = new Promise( (res, rej) => { resolver = res; } );
+
+ function receiver_sender(bytes_arr) {
+ //console.log("receiver sending", String.fromCharCode.apply(String, bytes_arr), bytes_arr);
+
+ if (sender) {
+ var consumer = () => {
+ sender.consume(bytes_arr);
+ };
+
+ if (async) {
+ wait(0.5).then(consumer);
+ }
+ else consumer();
+ }
+ else {
+ var hdr = Zmodem.Header.parse(bytes_arr)[0];
+ sender = new Zmodem.Session.Send(hdr);
+ resolver(sender);
+
+ sender.set_sender( function(bytes_arr) {
+ var consumer = () => {
+ receiver.consume(bytes_arr);
+ };
+
+ if (async) {
+ wait(0.5).then(consumer);
+ }
+ else consumer();
+ } );
+
+ /*
+ sender.on("receive", function(hdr) {
+ console.log("Sender input", hdr);
+ } );
+ */
+ }
+ }
+
+ receiver.set_sender(receiver_sender);
+}
+
+test('Sender receives extra ZRPOS', (t) => {
+ _init();
+
+ var zrinit = Zmodem.Header.build("ZRINIT", ["CANFDX", "CANOVIO", "ESCCTL"]);
+ var mysender = new Zmodem.Session.Send(zrinit);
+
+ var zrpos = Zmodem.Header.build("ZRPOS", 12345);
+
+ var err;
+
+ try {
+ mysender.consume(zrpos.to_hex());
+ }
+ catch(e) {
+ err = e;
+ }
+
+ t.match(err.toString(), /header/, "error as expected");
+ t.match(err.toString(), /ZRPOS/, "error as expected");
+
+ return Promise.resolve();
+} );
+
+test('Offer events', (t) => {
+ _init();
+
+ var inputs = [];
+ var completed = false;
+
+ var r_pms = receiver.start().then( (offer) => {
+ t.deepEquals(
+ offer.get_details(),
+ {
+ name: "my file",
+ size: 32,
+ mode: null,
+ mtime: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ 'get_details() returns expected values'
+ );
+
+ offer.on("input", (payload) => {
+ inputs.push(
+ {
+ offset: offer.get_offset(),
+ payload: payload,
+ }
+ );
+ } );
+
+ offer.on("complete", () => { completed = true });
+
+ return offer.accept();
+ } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file", size: 32 }
+ ).then( (sender_xfer) => {
+ sender_xfer.send( [1, 2, 3] );
+ sender_xfer.send( [4, 5, 6, 7] );
+ sender_xfer.end( [8, 9] ).then( () => {
+ return sender.close();
+ } );
+ } );
+
+ return Promise.all( [ r_pms, s_pms ] ).then( () => {
+ t.deepEquals(
+ inputs,
+ [
+ {
+ payload: [1, 2, 3],
+ offset: 3,
+ },
+ {
+ payload: [4, 5, 6, 7],
+ offset: 7,
+ },
+ {
+ payload: [8, 9],
+ offset: 9,
+ },
+ ],
+ 'Offer “input” events',
+ );
+
+ t.ok( completed, 'Offer “complete” event' );
+ } );
+} );
+
+test('receive one, promises', (t) => {
+ _init();
+
+ var r_pms = receiver.start().then( (offer) => {
+ t.deepEquals(
+ offer.get_details(),
+ {
+ name: "my file",
+ size: 32,
+ mode: null,
+ mtime: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ 'get_details() returns expected values'
+ );
+
+ return offer.accept();
+ } );
+
+ //r_pms.then( () => { console.log("RECEIVER DONE") } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file", size: 32 }
+ ).then( (sender_xfer) => {
+ sender_xfer.end( [12, 23, 34] ).then( () => {
+ return sender.close();
+ } );
+ } );
+
+ return Promise.all( [ r_pms, s_pms ] );
+} );
+
+test('receive one, events', (t) => {
+ _init();
+
+ var content = [ 1,2,3,4,5,6,7,8,9,2,3,5,1,5,33,2,23,7 ];
+
+ var now_epoch = Math.floor(Date.now() / 1000);
+
+ receiver.on("offer", (offer) => {
+ t.deepEquals(
+ offer.get_details(),
+ {
+ name: "my file",
+ size: content.length,
+ mode: parseInt("100644", 8),
+ mtime: new Date( now_epoch * 1000 ),
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ 'get_details() returns expected values'
+ );
+
+ offer.accept();
+ } );
+ receiver.start();
+
+ return sender.send_offer( {
+ name: "my file",
+ size: content.length,
+ mtime: now_epoch,
+ mode: parseInt("0644", 8),
+ } ).then(
+ (sender_xfer) => {
+ sender_xfer.end(content).then( sender.close.bind(sender) );
+ }
+ );
+} );
+
+test('skip one, receive the next', (t) => {
+ _init();
+
+ var r_pms = receiver.start().then( (offer) => {
+ //console.log("first offer", offer);
+
+ t.equals( offer.get_details().name, "my file", "first file’s name" );
+ var next_pms = offer.skip();
+ //console.log("next", next_pms);
+ return next_pms;
+ } ).then( (offer) => {
+ t.equals( offer.get_details().name, "file 2", "second file’s name" );
+ return offer.skip();
+ } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file" }
+ ).then(
+ (sender_xfer) => {
+ t.ok( !sender_xfer, "skip() -> sender sees no transfer object" );
+ return sender.send_offer( { name: "file 2" } );
+ }
+ ).then(
+ (xfer) => {
+ t.ok( !xfer, "2nd skip() -> sender sees no transfer object" );
+ return sender.close();
+ }
+ );
+
+ return Promise.all( [ r_pms, s_pms ] );
+} );
+
+test('abort mid-download', (t) => {
+ _init();
+
+ var transferred_bytes = [];
+
+ var aborted;
+
+ var r_pms = receiver.start().then( (offer) => {
+ offer.on("input", (payload) => {
+ [].push.apply(transferred_bytes, payload);
+
+ if (aborted) throw "already aborted!";
+ aborted = true;
+
+ receiver.abort();
+ });
+ return offer.accept();
+ } );
+
+ var s_pms = sender.send_offer(
+ { name: "my file" }
+ ).then(
+ (xfer) => {
+ xfer.send( [1, 2, 3] );
+ xfer.end( [99, 99, 99] ); //should never get here
+ }
+ );
+
+ return Promise.all( [r_pms, s_pms] ).catch(
+ (err) => {
+ t.ok( err.message.match('abort'), 'error message is about abort' );
+ }
+ ).then( () => {
+ t.deepEquals(
+ transferred_bytes,
+ [1, 2, 3],
+ 'abort() stopped us from sending more',
+ );
+ } );
+} );
diff --git a/tests/zsession_receive.js b/tests/zsession_receive.js
new file mode 100755
index 0000000..4b126a9
--- /dev/null
+++ b/tests/zsession_receive.js
@@ -0,0 +1,295 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const tape = require('blue-tape');
+
+const SZ_PATH = require('which').sync('sz', {nothrow: true});
+
+if (!SZ_PATH) {
+ tape.only('SKIP: no “sz” in PATH!', (t) => {
+ t.end();
+ });
+}
+
+const spawn = require('child_process').spawn;
+
+var helper = require('./lib/testhelp');
+
+Object.assign(
+ global,
+ {
+ Zmodem: require('./lib/zmodem'),
+ }
+);
+
+var FILE1 = helper.make_temp_file(10 * 1024 * 1024); //10 MiB
+
+function _test_steps(t, sz_args, steps) {
+ return helper.exec_lrzsz_steps( t, SZ_PATH, sz_args, steps );
+}
+
+tape('abort() after ZRQINIT', (t) => {
+ return _test_steps( t, [FILE1], [
+ (zsession, child) => {
+ zsession.abort();
+ return true;
+ },
+ ] ).then( (inputs) => {
+ //console.log("inputs", inputs);
+
+ var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
+ t.ok(
+ str.match(/\x18\x18\x18\x18\x18/),
+ 'abort() right after receipt of ZRQINIT',
+ );
+ } );
+});
+
+tape('abort() after ZFILE', (t) => {
+ return _test_steps( t, [FILE1], [
+ (zsession) => {
+ zsession.start();
+ return true;
+ },
+ (zsession) => {
+ zsession.abort();
+ return true;
+ },
+ ] ).then( (inputs) => {
+ //console.log("inputs", inputs);
+
+ var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
+ t.ok(
+ str.match(/\x18\x18\x18\x18\x18/),
+ 'abort() right after receipt of ZFILE',
+ );
+ } );
+});
+
+//NB: This test is not unlikely to flap since it depends
+//on sz reading the abort sequence prior to finishing its read
+//of the file.
+tape('abort() during download', { timeout: 30000 }, (t) => {
+ var child_pms = _test_steps( t, [FILE1], [
+ (zsession) => {
+ zsession.on("offer", (offer) => offer.accept() );
+ zsession.start();
+ return true;
+ },
+ (zsession) => {
+ zsession.abort();
+ return true;
+ },
+ ] );
+
+ return child_pms.then( (inputs) => {
+ t.notEquals( inputs, undefined, 'abort() during download ends the transmission' );
+
+ t.ok(
+ inputs.every( function(bytes) {
+ var str = String.fromCharCode.apply( String, bytes );
+ return !/THE_END/.test(str);
+ } ),
+ "the end of the file was not sent",
+ );
+ } );
+});
+
+//This only works because we use CRC32 to receive. CRC16 in lsz has a
+//buffer overflow bug, fixed here:
+//
+// https://github.com/gooselinux/lrzsz/blob/master/lrzsz-0.12.20.patch
+//
+tape('skip() during download', { timeout: 30000 }, (t) => {
+ var filenames = [FILE1, helper.make_temp_file(12345678)];
+ //filenames = ["-vvvvvvvvvvvvv", FILE1, _make_temp_file()];
+
+ var started, second_offer;
+
+ return _test_steps( t, filenames, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ offer.accept();
+ offer.skip();
+ zsession.off("offer", offer_taker);
+ zsession.on("offer", (offer2) => {
+ second_offer = offer2;
+ offer2.skip();
+ });
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+ //return true;
+ },
+ ] ).then( (inputs) => {
+ var never_end = inputs.every( function(bytes) {
+ var str = String.fromCharCode.apply( String, bytes );
+ return !/THE_END/.test(str);
+ } );
+
+ // This is race-prone.
+ //t.ok( never_end, "the end of a file is never sent" );
+
+ t.ok( !!second_offer, "we got a 2nd offer after the first" );
+ } );
+});
+
+tape('skip() - immediately - at end of download', { timeout: 30000 }, (t) => {
+ var filenames = [helper.make_temp_file(123)];
+
+ var started;
+
+ return _test_steps( t, filenames, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ offer.accept();
+ offer.skip();
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+
+ started = true;
+ }
+ },
+ ] );
+});
+
+// Verify a skip() that happens after a transfer is complete.
+// There are no assertions here.
+tape('skip() - after a parse - at end of download', { timeout: 30000 }, (t) => {
+ var filenames = [helper.make_temp_file(123)];
+
+ var the_offer, started, skipped, completed;
+
+ return _test_steps( t, filenames, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ the_offer = offer;
+ var promise = the_offer.accept();
+ promise.then( () => {
+ completed = 1;
+ } );
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+
+ return the_offer;
+ },
+ () => {
+ if (!skipped && !completed) {
+ the_offer.skip();
+ skipped = true;
+ }
+ },
+ ] );
+});
+
+var happy_filenames = [
+ helper.make_temp_file(5),
+ helper.make_temp_file(3),
+ helper.make_temp_file(1),
+ helper.make_empty_temp_file(),
+];
+
+tape('happy-path: single batch', { timeout: 30000 }, (t) => {
+ var started, the_offer;
+
+ var args = happy_filenames;
+
+ var buffers = [];
+
+ var child_pms = _test_steps( t, args, [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ the_offer = offer;
+ the_offer.accept( { on_input: "spool_array" } ).then( (byte_lists) => {
+ var flat = [].concat.apply([], byte_lists);
+ var str = String.fromCharCode.apply( String, flat );
+ buffers.push(str);
+ } );
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+
+ return false;
+ },
+ ] );
+
+ return child_pms.then( (inputs) => {
+ t.equals( buffers[0], "xxxxx=THE_END", '5-byte transfer plus end' );
+ t.equals( buffers[1], "xxx=THE_END", '3-byte transfer plus end' );
+ t.equals( buffers[2], "x=THE_END", '1-byte transfer plus end' );
+ t.equals( buffers[3], "", 'empty transfer plus end' );
+ } );
+});
+
+tape('happy-path: individual transfers', { timeout: 30000 }, (t) => {
+ var promises = happy_filenames.map( (fn) => {
+ var str;
+
+ var started;
+
+ var child_pms = _test_steps( t, [fn], [
+ (zsession) => {
+ if (!started) {
+ function offer_taker(offer) {
+ offer.accept( { on_input: "spool_array" } ).then( (byte_lists) => {
+ var flat = [].concat.apply([], byte_lists);
+ str = String.fromCharCode.apply( String, flat );
+ } );
+ }
+ zsession.on("offer", offer_taker);
+ zsession.start();
+ started = true;
+ }
+
+ return false;
+ },
+ ] );
+
+ return child_pms.then( () => str );
+ } );
+
+ return Promise.all(promises).then( (strs) => {
+ t.equals( strs[0], "xxxxx=THE_END", '5-byte transfer plus end' );
+ t.equals( strs[1], "xxx=THE_END", '3-byte transfer plus end' );
+ t.equals( strs[2], "x=THE_END", '1-byte transfer plus end' );
+ t.equals( strs[3], "", 'empty transfer plus end' );
+ } );
+});
+
+//This doesn’t work because we automatically send ZFIN once we receive it,
+//which prompts the child to finish up.
+tape.skip("abort() after ZEOF", (t) => {
+ var received;
+
+ return _test_steps( t, [FILE1], [
+ (zsession) => {
+ zsession.on("offer", (offer) => {
+ offer.accept().then( () => { received = true } );
+ } );
+ zsession.start();
+ return true;
+ },
+ (zsession) => {
+ if (received) {
+ zsession.abort();
+ return true;
+ }
+ },
+ ] ).then( (inputs) => {
+ var str = String.fromCharCode.apply( String, inputs[ inputs.length - 1 ]);
+ t.is( str, "OO", "successful close despite abort" );
+ } );
+});
diff --git a/tests/zsession_send.js b/tests/zsession_send.js
new file mode 100755
index 0000000..da7cbf0
--- /dev/null
+++ b/tests/zsession_send.js
@@ -0,0 +1,248 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const fs = require('fs');
+const tape = require('blue-tape');
+
+const RZ_PATH = require('which').sync('rz', {nothrow: true});
+
+if (!RZ_PATH) {
+ tape.only('SKIP: no “rz” in PATH!', (t) => {
+ t.end();
+ });
+}
+
+Object.assign(
+ global,
+ {
+ Zmodem: require('./lib/zmodem'),
+ }
+);
+
+var helper = require('./lib/testhelp');
+
+var dir_before = process.cwd();
+tape.onFinish( () => process.chdir( dir_before ) );
+
+let TEST_STRINGS = [
+ "",
+ "0",
+ "123",
+ "\x00",
+ "\x18",
+ "\x18\x18\x18\x18\x18", //invalid as UTF-8
+ "\x8a\x9a\xff\xfe", //invalid as UTF-8
+ "épée",
+ "Hi diddle-ee, dee! A sailor’s life for me!",
+];
+
+var text_encoder = require('text-encoding').TextEncoder;
+text_encoder = new text_encoder();
+
+function _send_batch(t, batch, on_offer) {
+ batch = batch.slice(0);
+
+ return helper.exec_lrzsz_steps( t, RZ_PATH, [], [
+ (zsession, child) => {
+ function offer_sender() {
+ if (!batch.length) {
+ zsession.close();
+ return; //batch finished
+ }
+
+ return zsession.send_offer(
+ batch[0][0]
+ ).then( (xfer) => {
+ if (on_offer) {
+ on_offer(xfer, batch[0]);
+ }
+
+ let file_contents = batch.shift()[1];
+
+ var octets;
+ if ("string" === typeof file_contents) {
+ octets = text_encoder.encode(file_contents);
+ }
+ else {
+ octets = file_contents; // Buffer
+ }
+
+ return xfer && xfer.end( Array.from(octets) );
+ } ).then( offer_sender );
+ }
+
+ return offer_sender();
+ },
+ (zsession, child) => {
+ return zsession.has_ended();
+ },
+ ] );
+}
+
+function _do_in_temp_dir( todo ) {
+ var ret;
+
+ process.chdir( helper.make_temp_dir() );
+
+ try {
+ ret = todo();
+ }
+ catch(e) {
+ throw e;
+ }
+ finally {
+ if (!ret) {
+ process.chdir( dir_before );
+ }
+ }
+
+ if (ret) {
+ ret = ret.then( () => process.chdir( dir_before ) );
+ }
+
+ return ret;
+}
+
+tape("rz accepts one, then skips next", (t) => {
+ return _do_in_temp_dir( () => {
+ let filename = "no-clobberage";
+
+ var batch = [
+ [
+ { name: filename },
+ "the first",
+ ],
+ [
+ { name: filename },
+ "the second",
+ ],
+ ];
+
+ var offers = [];
+ function offer_cb(xfer, batch_item) {
+ offers.push( xfer );
+ }
+
+ return _send_batch(t, batch, offer_cb).then( () => {
+ var got_contents = fs.readFileSync(filename, "utf-8");
+ t.equals( got_contents, "the first", 'second offer was rejected' );
+
+ t.notEquals( offers[0], undefined, 'got an offer at first' );
+ t.equals( offers[1], undefined, '… but no offer second' );
+ } );
+ } );
+});
+
+tape("send batch", (t) => {
+ return _do_in_temp_dir( () => {
+ var string_num = 0;
+
+ var base = "batch_";
+ var mtime_1990 = new Date("1990-01-01T00:00:00Z");
+
+ var batch = TEST_STRINGS.map( (str, i) => {
+ return [
+ {
+ name: base + i,
+ mtime: mtime_1990,
+ },
+ str,
+ ];
+ } );
+
+ return _send_batch(t, batch).then( () => {
+ for (var sn=0; sn < TEST_STRINGS.length; sn++) {
+ var got_contents = fs.readFileSync(base + sn, "utf-8");
+ t.equals( got_contents, TEST_STRINGS[sn], `rz wrote out the file: ` + JSON.stringify(TEST_STRINGS[sn]) );
+ t.equals( 0 + fs.statSync(base + sn).mtime, 0 + mtime_1990, `... and observed the sent mtime` );
+ }
+ } );
+ } );
+});
+
+tape("send one at a time", (t) => {
+ return _do_in_temp_dir( () => {
+ var xfer;
+
+ let test_strings = TEST_STRINGS.slice(0);
+
+ function doer() {
+ var file_contents = test_strings.shift();
+ if (typeof(file_contents) !== "string") return; //we’re done
+
+ return helper.exec_lrzsz_steps( t, RZ_PATH, ["--overwrite"], [
+ (zsession, child) => {
+ zsession.send_offer( { name: "single" } ).then( (xf) => {
+ t.ok( !!xf, 'rz accepted offer' );
+ xfer = xf;
+ } ).then(
+ () => xfer.end( Array.from( text_encoder.encode(file_contents) ) )
+ ).then(
+ () => zsession.close()
+ );
+
+ return true;
+ },
+ (zsession, child) => {
+ return zsession.has_ended();
+ },
+ ] ).then( () => {
+ var got_contents = fs.readFileSync("single", "utf-8");
+ t.equals( got_contents, file_contents, `rz wrote out the file: ` + JSON.stringify(file_contents) );
+ } ).then( doer );
+ }
+
+ return doer();
+ } );
+});
+
+tape("send single large file", (t) => {
+ return _do_in_temp_dir( () => {
+ var string_num = 0;
+
+ var mtime_1990 = new Date("1990-01-01T00:00:00Z");
+ var big_string = Array(30 * 1024 * 1024).fill('x').join("");
+
+ var batch = [
+ [
+ {
+ name: "big_kahuna",
+ },
+ big_string,
+ ],
+ ];
+
+ return _send_batch(t, batch).then( () => {
+ var got_contents = fs.readFileSync("big_kahuna", "utf-8");
+ t.equals( got_contents, big_string, 'rz wrote out the file');
+ } );
+ } );
+});
+
+tape("send single random file", (t) => {
+ return _do_in_temp_dir( () => {
+ var string_num = 0;
+
+ var mtime_1990 = new Date("1990-01-01T00:00:00Z");
+
+ var big_buffer = new Buffer(1024 * 1024);
+ for (var i=0; i<big_buffer.length; i++) {
+ big_buffer[i] = Math.floor( Math.random(256) );
+ }
+
+ var batch = [
+ [
+ {
+ name: "big_kahuna",
+ },
+ big_buffer,
+ ],
+ ];
+
+ return _send_batch(t, batch).then( () => {
+ var got_contents = fs.readFileSync("big_kahuna");
+ t.equals( got_contents.join(), big_buffer.join(), 'rz wrote out the file');
+ } );
+ } );
+});
diff --git a/tests/zsubpacket.js b/tests/zsubpacket.js
new file mode 100755
index 0000000..eaf1624
--- /dev/null
+++ b/tests/zsubpacket.js
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const tape = require('blue-tape');
+
+const testhelp = require('./lib/testhelp');
+
+global.Zmodem = require('./lib/zmodem');
+
+var zdle = new Zmodem.ZDLE( { escape_ctrl_chars: true } );
+
+tape('build, encode, parse', function(t) {
+ let content = [1, 2, 3, 4];
+
+ ["end_ack", "no_end_ack", "end_no_ack", "no_end_no_ack"].forEach( end => {
+ var header = Zmodem.Subpacket.build( content, end );
+
+ t.deepEquals(
+ header.get_payload(),
+ content,
+ `${end}: get_payload()`
+ );
+
+ t.is(
+ header.frame_end(),
+ !/no_end/.test(end),
+ `${end}: frame_end()`
+ );
+
+ t.is(
+ header.ack_expected(),
+ !/no_ack/.test(end),
+ `${end}: ack_expected()`
+ );
+
+ [16, 32].forEach( crclen => {
+ var encoded = header["encode" + crclen](zdle);
+ var parsed = Zmodem.Subpacket["parse" + crclen](encoded);
+
+ t.deepEquals(
+ parsed.get_payload(),
+ content,
+ `${end}, CRC${crclen} rount-trip: get_payload()`
+ );
+
+ t.is(
+ parsed.frame_end(),
+ header.frame_end(),
+ `${end}, CRC${crclen} rount-trip: frame_end()`
+ );
+
+ t.is(
+ parsed.ack_expected(),
+ header.ack_expected(),
+ `${end}, CRC${crclen} rount-trip: ack_expected()`
+ );
+ } );
+ } );
+
+ t.end();
+} );
diff --git a/tests/zvalidation.js b/tests/zvalidation.js
new file mode 100644
index 0000000..64231b0
--- /dev/null
+++ b/tests/zvalidation.js
@@ -0,0 +1,227 @@
+#!/usr/bin/env node
+
+"use strict";
+
+const tape = require('blue-tape');
+
+global.Zmodem = require('./lib/zmodem');
+
+const zcrc = Zmodem.CRC;
+
+var now = new Date();
+var now_epoch = Math.floor( now.getTime() / 1000 );
+
+var failures = [
+ [
+ 'empty name',
+ { name: "" },
+ function(t, e) {
+ t.ok( /name/.test(e.message), 'has “name”' );
+ },
+ ],
+ [
+ 'non-string name',
+ { name: 123 },
+ function(t, e) {
+ t.ok( /name/.test(e.message), 'has “name”' );
+ t.ok( /string/.test(e.message), 'has “string”' );
+ },
+ ],
+ [
+ 'non-empty serial',
+ { name: "123", serial: 0 },
+ function(t, e) {
+ t.ok( /serial/.test(e.message), 'has “serial”' );
+ },
+ ],
+ [
+ 'files_remaining === 0',
+ { name: "123", files_remaining: 0 },
+ function(t, e) {
+ t.ok( /files_remaining/.test(e.message), 'has “files_remaining”' );
+ },
+ ],
+ [
+ 'pre-epoch mtime',
+ { name: "123", mtime: new Date("1969-12-30T01:02:03Z") },
+ function(t, e) {
+ t.ok( /mtime/.test(e.message), 'has “mtime”' );
+ t.ok( /1969/.test(e.message), 'has “1969”' );
+ t.ok( /1970/.test(e.message), 'has “1970”' );
+ },
+ ],
+];
+
+["size", "mode", "mtime", "files_remaining", "bytes_remaining"].forEach( (k) => {
+ var input = { name: "the name" };
+ input[k] = "123123";
+
+ var key_regexp = new RegExp(k);
+ var value_regexp = new RegExp(input[k]);
+
+ failures.push( [
+ `string “${k}”`,
+ input,
+ function(t, e) {
+ t.ok( key_regexp.test(e.message), `has “${k}”` );
+ t.ok( value_regexp.test(e.message), 'has value' );
+ t.ok( /number/.test(e.message), 'has “number”' );
+ },
+ ] );
+
+ input = Object.assign( {}, input );
+ input[k] = -input[k];
+
+ var negative_regexp = new RegExp(input[k]);
+
+ failures.push( [
+ `negative “${k}”`,
+ input,
+ function(t, e) {
+ t.ok( key_regexp.test(e.message), `has “${k}”` );
+ t.ok( negative_regexp.test(e.message), 'has value' );
+ },
+ ] );
+
+ input = Object.assign( {}, input );
+ input[k] = -input[k] - 0.1;
+
+ var fraction_regexp = new RegExp( ("" + input[k]).replace(/\./, "\\.") );
+
+ failures.push( [
+ `fraction “${k}”`,
+ input,
+ function(t, e) {
+ t.ok( key_regexp.test(e.message), `has “${k}”` );
+ t.ok( fraction_regexp.test(e.message), 'has value' );
+ },
+ ] );
+} );
+
+
+var transformations = [
+ [
+ 'name only',
+ { name: "My name", },
+ {
+ name: "My name",
+ size: null,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name is all numerals',
+ { name: "0", },
+ {
+ name: "0",
+ size: null,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name only (undefined rather than null)',
+ {
+ name: "My name",
+ size: undefined,
+ mtime: undefined,
+ mode: undefined,
+ serial: undefined,
+ files_remaining: undefined,
+ bytes_remaining: undefined,
+ },
+ {
+ name: "My name",
+ size: null,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name and all numbers',
+ {
+ name: "My name",
+ size: 0,
+ mtime: 0,
+ mode: parseInt("0644", 8),
+ serial: null,
+ files_remaining: 1,
+ bytes_remaining: 0,
+ },
+ {
+ name: "My name",
+ size: 0,
+ mtime: 0,
+ mode: parseInt("100644", 8),
+ serial: null,
+ files_remaining: 1,
+ bytes_remaining: 0,
+ },
+ ],
+ [
+ 'name, zero size',
+ { name: "My name", mtime: now },
+ {
+ name: "My name",
+ size: null,
+ mtime: now_epoch,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+ [
+ 'name, mtime as Date',
+ { name: "My name", size: 0 },
+ {
+ name: "My name",
+ size: 0,
+ mtime: null,
+ mode: null,
+ serial: null,
+ files_remaining: null,
+ bytes_remaining: null,
+ },
+ ],
+];
+
+tape('offer_parameters - failures', function(t) {
+
+ for (const [label, input, todo] of failures) {
+ let err;
+ try {
+ Zmodem.Validation.offer_parameters(input);
+ }
+ catch(e) { err = e }
+
+ t.ok( err instanceof Zmodem.Error, `throws ok: ${label}` );
+
+ todo(t, err);
+ }
+
+ t.end();
+});
+
+tape('offer_parameters - happy path', function(t) {
+
+ for (const [label, input, output] of transformations) {
+ t.deepEquals(
+ Zmodem.Validation.offer_parameters(input),
+ output,
+ label,
+ );
+ }
+
+ t.end();
+});
diff --git a/tools/all_bytes b/tools/all_bytes
new file mode 100644
index 0000000..c866266
--- /dev/null
+++ b/tools/all_bytes
Binary files differ
diff --git a/tools/talk_to_sz.pl b/tools/talk_to_sz.pl
new file mode 100755
index 0000000..8357a01
--- /dev/null
+++ b/tools/talk_to_sz.pl
@@ -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();
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..be591cf
--- /dev/null
+++ b/webpack.config.js
@@ -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'
+ })
+ ]
+}
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..6018de5
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,3010 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+
+acorn-dynamic-import@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
+ dependencies:
+ acorn "^4.0.3"
+
+acorn@^4.0.3:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+acorn@^5.0.0:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7"
+
+ajv-keywords@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
+
+ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.1.5:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ json-schema-traverse "^0.3.0"
+ json-stable-stringify "^1.0.1"
+
+align-text@^0.1.1, align-text@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+ dependencies:
+ kind-of "^3.0.2"
+ longest "^1.0.1"
+ repeat-string "^1.5.2"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+anymatch@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
+ dependencies:
+ micromatch "^2.1.5"
+ normalize-path "^2.0.0"
+
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+ integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+asn1.js@^4.0.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ dependencies:
+ util "0.10.3"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async@^2.1.2:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+ dependencies:
+ lodash "^4.14.0"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
+ integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
+ dependencies:
+ array-filter "^1.0.0"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws4@^1.2.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
+babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-core@^6.24.1, babel-core@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.0"
+ debug "^2.6.8"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.7"
+ slash "^1.0.0"
+ source-map "^0.5.6"
+
+babel-generator@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.6"
+ trim-right "^1.0.1"
+
+babel-helper-evaluate-path@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.2.0.tgz#0bb2eb01996c0cef53c5e8405e999fe4a0244c08"
+
+babel-helper-flip-expressions@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-flip-expressions/-/babel-helper-flip-expressions-0.2.0.tgz#160d2090a3d9f9c64a750905321a0bc218f884ec"
+
+babel-helper-is-nodes-equiv@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-is-nodes-equiv/-/babel-helper-is-nodes-equiv-0.0.1.tgz#34e9b300b1479ddd98ec77ea0bbe9342dfe39684"
+
+babel-helper-is-void-0@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-is-void-0/-/babel-helper-is-void-0-0.2.0.tgz#6ed0ada8a9b1c5b6e88af6b47c1b3b5c080860eb"
+
+babel-helper-mark-eval-scopes@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-mark-eval-scopes/-/babel-helper-mark-eval-scopes-0.2.0.tgz#7648aaf2ec92aae9b09a20ad91e8df5e1fcc94b2"
+
+babel-helper-remove-or-void@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-remove-or-void/-/babel-helper-remove-or-void-0.2.0.tgz#8e46ad5b30560d57d7510b3fd93f332ee7c67386"
+
+babel-helper-to-multiple-sequence-expressions@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.2.0.tgz#d1a419634c6cb301f27858c659167cfee0a9d318"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-minify-webpack-plugin@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-minify-webpack-plugin/-/babel-minify-webpack-plugin-0.2.0.tgz#ef9694d11a1b8ab8f3204d89f5c9278dd28fc2a9"
+ dependencies:
+ babel-core "^6.24.1"
+ babel-preset-minify "^0.2.0"
+ webpack-sources "^1.0.1"
+
+babel-plugin-minify-builtins@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.2.0.tgz#317f824b0907210b6348671bb040ca072e2e0c82"
+ dependencies:
+ babel-helper-evaluate-path "^0.2.0"
+
+babel-plugin-minify-constant-folding@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-constant-folding/-/babel-plugin-minify-constant-folding-0.2.0.tgz#8c70b528b2eb7c13e94d95c8789077d4cdbc3970"
+ dependencies:
+ babel-helper-evaluate-path "^0.2.0"
+
+babel-plugin-minify-dead-code-elimination@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.2.0.tgz#e8025ee10a1e5e4f202633a6928ce892c33747e3"
+ dependencies:
+ babel-helper-evaluate-path "^0.2.0"
+ babel-helper-mark-eval-scopes "^0.2.0"
+ babel-helper-remove-or-void "^0.2.0"
+ lodash.some "^4.6.0"
+
+babel-plugin-minify-flip-comparisons@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-flip-comparisons/-/babel-plugin-minify-flip-comparisons-0.2.0.tgz#0c9c8e93155c8f09dedad8118b634c259f709ef5"
+ dependencies:
+ babel-helper-is-void-0 "^0.2.0"
+
+babel-plugin-minify-guarded-expressions@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-guarded-expressions/-/babel-plugin-minify-guarded-expressions-0.2.0.tgz#8a8c950040fce3e258a12e6eb21eab94ad7235ab"
+ dependencies:
+ babel-helper-flip-expressions "^0.2.0"
+
+babel-plugin-minify-infinity@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-infinity/-/babel-plugin-minify-infinity-0.2.0.tgz#30960c615ddbc657c045bb00a1d8eb4af257cf03"
+
+babel-plugin-minify-mangle-names@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-mangle-names/-/babel-plugin-minify-mangle-names-0.2.0.tgz#719892297ff0106a6ec1a4b0fc062f1f8b6a8529"
+ dependencies:
+ babel-helper-mark-eval-scopes "^0.2.0"
+
+babel-plugin-minify-numeric-literals@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-numeric-literals/-/babel-plugin-minify-numeric-literals-0.2.0.tgz#5746e851700167a380c05e93f289a7070459a0d1"
+
+babel-plugin-minify-replace@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-replace/-/babel-plugin-minify-replace-0.2.0.tgz#3c1f06bc4e6d3e301eacb763edc1be611efc39b0"
+
+babel-plugin-minify-simplify@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-simplify/-/babel-plugin-minify-simplify-0.2.0.tgz#21ceec4857100c5476d7cef121f351156e5c9bc0"
+ dependencies:
+ babel-helper-flip-expressions "^0.2.0"
+ babel-helper-is-nodes-equiv "^0.0.1"
+ babel-helper-to-multiple-sequence-expressions "^0.2.0"
+
+babel-plugin-minify-type-constructors@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-minify-type-constructors/-/babel-plugin-minify-type-constructors-0.2.0.tgz#7f3b6458be0863cfd59e9985bed6d134aa7a2e17"
+ dependencies:
+ babel-helper-is-void-0 "^0.2.0"
+
+babel-plugin-transform-inline-consecutive-adds@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.2.0.tgz#15dae78921057f4004f8eafd79e15ddc5f12f426"
+
+babel-plugin-transform-member-expression-literals@^6.8.5:
+ version "6.8.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.8.5.tgz#e06ae305cf48d819822e93a70d79269f04d89eec"
+
+babel-plugin-transform-merge-sibling-variables@^6.8.6:
+ version "6.8.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.8.6.tgz#6d21efa5ee4981f71657fae716f9594bb2622aef"
+
+babel-plugin-transform-minify-booleans@^6.8.3:
+ version "6.8.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-minify-booleans/-/babel-plugin-transform-minify-booleans-6.8.3.tgz#5906ed776d3718250519abf1bace44b0b613ddf9"
+
+babel-plugin-transform-property-literals@^6.8.5:
+ version "6.8.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.8.5.tgz#67ed5930b34805443452c8b9690c7ebe1e206c40"
+ dependencies:
+ esutils "^2.0.2"
+
+babel-plugin-transform-regexp-constructors@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.2.0.tgz#6aa5dd0acc515db4be929bbcec4ed4c946c534a3"
+
+babel-plugin-transform-remove-console@^6.8.5:
+ version "6.8.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.8.5.tgz#fde9d2d3d725530b0fadd8d31078402410386810"
+
+babel-plugin-transform-remove-debugger@^6.8.5:
+ version "6.8.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.8.5.tgz#809584d412bf918f071fdf41e1fdb15ea89cdcd5"
+
+babel-plugin-transform-remove-undefined@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-undefined/-/babel-plugin-transform-remove-undefined-0.2.0.tgz#94f052062054c707e8d094acefe79416b63452b1"
+ dependencies:
+ babel-helper-evaluate-path "^0.2.0"
+
+babel-plugin-transform-simplify-comparison-operators@^6.8.5:
+ version "6.8.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.8.5.tgz#a838786baf40cc33a93b95ae09e05591227e43bf"
+
+babel-plugin-transform-undefined-to-void@^6.8.3:
+ version "6.8.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.8.3.tgz#fc52707f6ee1ddc71bb91b0d314fbefdeef9beb4"
+
+babel-preset-minify@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-minify/-/babel-preset-minify-0.2.0.tgz#006566552d9b83834472273f306c0131062a0acc"
+ dependencies:
+ babel-plugin-minify-builtins "^0.2.0"
+ babel-plugin-minify-constant-folding "^0.2.0"
+ babel-plugin-minify-dead-code-elimination "^0.2.0"
+ babel-plugin-minify-flip-comparisons "^0.2.0"
+ babel-plugin-minify-guarded-expressions "^0.2.0"
+ babel-plugin-minify-infinity "^0.2.0"
+ babel-plugin-minify-mangle-names "^0.2.0"
+ babel-plugin-minify-numeric-literals "^0.2.0"
+ babel-plugin-minify-replace "^0.2.0"
+ babel-plugin-minify-simplify "^0.2.0"
+ babel-plugin-minify-type-constructors "^0.2.0"
+ babel-plugin-transform-inline-consecutive-adds "^0.2.0"
+ babel-plugin-transform-member-expression-literals "^6.8.5"
+ babel-plugin-transform-merge-sibling-variables "^6.8.6"
+ babel-plugin-transform-minify-booleans "^6.8.3"
+ babel-plugin-transform-property-literals "^6.8.5"
+ babel-plugin-transform-regexp-constructors "^0.2.0"
+ babel-plugin-transform-remove-console "^6.8.5"
+ babel-plugin-transform-remove-debugger "^6.8.5"
+ babel-plugin-transform-remove-undefined "^0.2.0"
+ babel-plugin-transform-simplify-comparison-operators "^6.8.5"
+ babel-plugin-transform-undefined-to-void "^6.8.3"
+ lodash.isplainobject "^4.0.6"
+
+babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
+babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+babel-template@^6.24.1, babel-template@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+ dependencies:
+ babel-runtime "^6.26.0"
+ esutils "^2.0.2"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babylon@7.0.0-beta.19:
+ version "7.0.0-beta.19"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.19.tgz#e928c7e807e970e0536b078ab3e0c48f9e052503"
+
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64-js@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+big.js@^3.1.3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+
+binary-extensions@^1.0.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ dependencies:
+ inherits "~2.0.0"
+
+blue-tape@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/blue-tape/-/blue-tape-1.0.0.tgz#7581d04c07395c95c426b2ed6d1edb454a76b92b"
+ dependencies:
+ tape ">=2.0.0 <5.0.0"
+
+bluebird@~3.5.0:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ dependencies:
+ hoek "2.x.x"
+
+brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.8.tgz#c8fa3b1b7585bb7ba77c5560b60996ddec6d5309"
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
+ dependencies:
+ pako "~0.2.0"
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+
+camelcase@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+catharsis@~0.8.9:
+ version "0.8.9"
+ resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.9.tgz#98cc890ca652dd2ef0e70b37925310ff9e90fc8b"
+ dependencies:
+ underscore-contrib "~0.3.0"
+
+center-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+ dependencies:
+ align-text "^0.1.3"
+ lazy-cache "^1.0.3"
+
+chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chokidar@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
+ dependencies:
+ anymatch "^1.3.0"
+ async-each "^1.0.0"
+ glob-parent "^2.0.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^2.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+cliui@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+ dependencies:
+ center-align "^0.1.1"
+ right-align "^0.1.1"
+ wordwrap "0.0.2"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+
+convert-source-map@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+
+core-js@^2.4.0, core-js@^2.5.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+crc-32@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.1.1.tgz#5d739d5e4c6e352ad8304d73223d483fe55adb8d"
+ dependencies:
+ exit-on-epipe "~1.0.1"
+ printj "~1.1.0"
+
+create-ecdh@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+cross-spawn@^5.0.1:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ dependencies:
+ boom "2.x.x"
+
+crypto-browserify@^3.11.0:
+ version "3.11.1"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ dependencies:
+ es5-ext "^0.10.9"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+
+debug@^2.2.0, debug@^2.6.8:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+decamelize@^1.0.0, decamelize@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+deep-equal@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
+ integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
+ dependencies:
+ es-abstract "^1.17.5"
+ es-get-iterator "^1.1.0"
+ is-arguments "^1.0.4"
+ is-date-object "^1.0.2"
+ is-regex "^1.0.5"
+ isarray "^2.0.5"
+ object-is "^1.1.2"
+ object-keys "^1.1.1"
+ object.assign "^4.1.0"
+ regexp.prototype.flags "^1.3.0"
+ side-channel "^1.0.2"
+ which-boxed-primitive "^1.0.1"
+ which-collection "^1.0.1"
+ which-typed-array "^1.1.2"
+
+deep-equal@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+defined@^1.0.0, defined@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+diffie-hellman@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+domain-browser@^1.1.1:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+
+dotignore@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905"
+ integrity sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==
+ dependencies:
+ minimatch "^3.0.4"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.1.0"
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+
+enhanced-resolve@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ object-assign "^4.0.1"
+ tapable "^0.2.7"
+
+errno@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
+ dependencies:
+ prr "~0.0.0"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5:
+ version "1.17.6"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
+ integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
+ dependencies:
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.1"
+ is-callable "^1.2.0"
+ is-regex "^1.1.0"
+ object-inspect "^1.7.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.0"
+ string.prototype.trimend "^1.0.1"
+ string.prototype.trimstart "^1.0.1"
+
+es-abstract@^1.5.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-get-iterator@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
+ integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
+ dependencies:
+ es-abstract "^1.17.4"
+ has-symbols "^1.0.1"
+ is-arguments "^1.0.4"
+ is-map "^2.0.1"
+ is-set "^2.0.1"
+ is-string "^1.0.5"
+ isarray "^2.0.5"
+
+es-to-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
+ dependencies:
+ is-callable "^1.1.1"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.1"
+
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
+ version "0.10.30"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.30.tgz#7141a16836697dbabfaaaeee41495ce29f52c939"
+ dependencies:
+ es6-iterator "2"
+ es6-symbol "~3.1"
+
+es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-symbol "^3.1"
+
+es6-map@^0.1.3:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-set "~0.1.5"
+ es6-symbol "~3.1.1"
+ event-emitter "~0.3.5"
+
+es6-set@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-symbol "3.1.1"
+ event-emitter "~0.3.5"
+
+es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
+
+escape-string-regexp@^1.0.2, escape-string-regexp@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escope@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+ dependencies:
+ es6-map "^0.1.3"
+ es6-weak-map "^2.0.1"
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.1.0, estraverse@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+event-emitter@~0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exit-on-epipe@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+extend@~3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extsprintf@1.3.0, extsprintf@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fill-range@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^1.1.3"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+find-up@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+for-each@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+ integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+ dependencies:
+ is-callable "^1.1.3"
+
+for-each@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4"
+ dependencies:
+ is-function "~1.0.0"
+
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+fs-extra@^0.30.0:
+ version "0.30.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+ path-is-absolute "^1.0.0"
+ rimraf "^2.2.8"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.36"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+function-bind@^1.0.2, function-bind@^1.1.1, function-bind@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob@^7.0.5, glob@~7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.1.6:
+ version "7.1.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+ integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+ integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+has@^1.0.1, has@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hash-base@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+ dependencies:
+ inherits "^2.0.1"
+
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+hawk@3.1.3, hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+hosted-git-info@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+
+ieee754@^1.1.4:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+inherits@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ini@~1.3.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+
+interpret@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
+
+invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+is-arguments@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
+ integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-bigint@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
+ integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-boolean-object@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
+ integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
+
+is-buffer@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.1.1, is-callable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+
+is-callable@^1.1.4, is-callable@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
+ integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
+is-date-object@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
+ integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-function@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-map@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
+ integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
+
+is-number-object@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
+ integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
+is-regex@^1.0.5, is-regex@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
+ integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
+ dependencies:
+ has-symbols "^1.0.1"
+
+is-set@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
+ integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
+
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-string@^1.0.4, is-string@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
+ integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+
+is-symbol@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
+
+is-symbol@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
+ integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+ dependencies:
+ has-symbols "^1.0.1"
+
+is-typed-array@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
+ integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
+ dependencies:
+ available-typed-arrays "^1.0.0"
+ es-abstract "^1.17.4"
+ foreach "^2.0.5"
+ has-symbols "^1.0.1"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-weakmap@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
+ integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
+
+is-weakset@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
+ integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isarray@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+js-tokens@^3.0.0, js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js2xmlparser@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-3.0.0.tgz#3fb60eaa089c5440f9319f51760ccd07e2499733"
+ dependencies:
+ xmlcreate "^1.0.1"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jsdoc-webpack-plugin@^0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/jsdoc-webpack-plugin/-/jsdoc-webpack-plugin-0.0.2.tgz#9f5186bd2d3c2450d450a1982e93fd55333689a0"
+ dependencies:
+ fs-extra "^0.30.0"
+ jsdoc "^3.4.0"
+ lodash "^4.11.2"
+
+jsdoc@^3.4.0:
+ version "3.5.5"
+ resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.5.5.tgz#484521b126e81904d632ff83ec9aaa096708fa4d"
+ dependencies:
+ babylon "7.0.0-beta.19"
+ bluebird "~3.5.0"
+ catharsis "~0.8.9"
+ escape-string-regexp "~1.0.5"
+ js2xmlparser "~3.0.0"
+ klaw "~2.0.0"
+ marked "~0.3.6"
+ mkdirp "~0.5.1"
+ requizzle "~0.2.1"
+ strip-json-comments "~2.0.1"
+ taffydb "2.6.2"
+ underscore "~1.8.3"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+json-loader@^0.5.4:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json5@^0.5.0, json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+kind-of@^3.0.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
+klaw@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-2.0.0.tgz#59c128e0dc5ce410201151194eeb9cbf858650f6"
+ dependencies:
+ graceful-fs "^4.1.9"
+
+lazy-cache@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+loader-runner@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+
+loader-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+
+lodash.some@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
+
+lodash@^4.11.2, lodash@^4.14.0, lodash@^4.17.4:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+longest@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+marked@~0.3.6:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
+
+md5.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+memory-fs@^0.4.0, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+micromatch@^2.1.5:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+mime-db@~1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+
+mime-types@^2.1.12, mime-types@~2.1.7:
+ version "2.1.17"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
+ dependencies:
+ mime-db "~1.30.0"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+
+minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.2.0, minimist@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minimist@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+ integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
+"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+nan@^2.3.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
+
+node-libs-browser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.1.4"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "0.0.1"
+ os-browserify "^0.2.0"
+ path-browserify "0.0.0"
+ process "^0.11.0"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.0.5"
+ stream-browserify "^2.0.1"
+ stream-http "^2.3.1"
+ string_decoder "^0.10.25"
+ timers-browserify "^2.0.2"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-pre-gyp@^0.6.36:
+ version "0.6.38"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d"
+ dependencies:
+ hawk "3.1.3"
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.0, normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+oauth-sign@~0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-inspect@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
+ integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
+
+object-inspect@~1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d"
+
+object-is@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
+ integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+once@^1.3.0, once@^1.3.3:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+os-browserify@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+pako@~0.2.0:
+ version "0.2.9"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+
+parse-asn1@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+ integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.14"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+printj@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.0.tgz#85487b5e8f96763b0b4a253613bef9dd9b387e3c"
+
+private@^0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+process@^0.11.0:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+
+prr@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+public-encrypt@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+
+randomatic@^1.1.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+randombytes@^2.0.0, randombytes@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79"
+ dependencies:
+ safe-buffer "^5.1.0"
+
+rc@^1.1.7:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.6:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+regenerator-runtime@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
+
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+regexp.prototype.flags@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
+ integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+request@2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+requizzle@~0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.1.tgz#6943c3530c4d9a7e46f1cddd51c158fc670cdbde"
+ dependencies:
+ underscore "~1.6.0"
+
+resolve@^1.17.0:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
+ integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
+ dependencies:
+ path-parse "^1.0.6"
+
+resolve@~1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+ dependencies:
+ path-parse "^1.0.5"
+
+resumer@^0.0.0, resumer@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759"
+ dependencies:
+ through "~2.3.4"
+
+right-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+ dependencies:
+ align-text "^0.1.1"
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ dependencies:
+ glob "^7.0.5"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+ dependencies:
+ hash-base "^2.0.0"
+ inherits "^2.0.1"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.9"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+side-channel@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
+ integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==
+ dependencies:
+ es-abstract "^1.17.0-next.1"
+ object-inspect "^1.7.0"
+
+signal-exit@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ dependencies:
+ hoek "2.x.x"
+
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+
+source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-http@^2.3.1:
+ version "2.7.2"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.2.6"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string.prototype.trim@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782"
+ integrity sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.0-next.1"
+ function-bind "^1.1.1"
+
+string.prototype.trim@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.0"
+ function-bind "^1.0.2"
+
+string.prototype.trimend@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
+ integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
+string.prototype.trimstart@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
+ integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.17.5"
+
+string_decoder@^0.10.25:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringstream@~0.0.4:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^4.2.1:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
+ dependencies:
+ has-flag "^2.0.0"
+
+taffydb@2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268"
+
+tapable@^0.2.7:
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+
+"tape@>=2.0.0 <5.0.0":
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e"
+ dependencies:
+ deep-equal "~1.0.1"
+ defined "~1.0.0"
+ for-each "~0.3.2"
+ function-bind "~1.1.0"
+ glob "~7.1.2"
+ has "~1.0.1"
+ inherits "~2.0.3"
+ minimist "~1.2.0"
+ object-inspect "~1.3.0"
+ resolve "~1.4.0"
+ resumer "~0.0.0"
+ string.prototype.trim "~1.1.2"
+ through "~2.3.8"
+
+tape@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/tape/-/tape-5.0.1.tgz#0d70ce90a586387c4efda4393e72872672a416a3"
+ integrity sha512-wVsOl2shKPcjdJdc8a+PwacvrOdJZJ57cLUXlxW4TQ2R6aihXwG0m0bKm4mA4wjtQNTaLMCrYNEb4f9fjHKUYQ==
+ dependencies:
+ deep-equal "^2.0.3"
+ defined "^1.0.0"
+ dotignore "^0.1.2"
+ for-each "^0.3.3"
+ function-bind "^1.1.1"
+ glob "^7.1.6"
+ has "^1.0.3"
+ inherits "^2.0.4"
+ is-regex "^1.0.5"
+ minimist "^1.2.5"
+ object-inspect "^1.7.0"
+ object-is "^1.1.2"
+ object.assign "^4.1.0"
+ resolve "^1.17.0"
+ resumer "^0.0.0"
+ string.prototype.trim "^1.2.1"
+ through "^2.3.8"
+
+tar-pack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+text-encoding@^0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
+
+through@^2.3.8, through@~2.3.4, through@~2.3.8:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+timers-browserify@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
+ dependencies:
+ setimmediate "^1.0.4"
+
+tmp@0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+tough-cookie@~2.3.0:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
+ dependencies:
+ punycode "^1.4.1"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+uglify-js@^2.8.29:
+ version "2.8.29"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+ dependencies:
+ source-map "~0.5.1"
+ yargs "~3.10.0"
+ optionalDependencies:
+ uglify-to-browserify "~1.0.0"
+
+uglify-to-browserify@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+uglifyjs-webpack-plugin@^0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
+ dependencies:
+ source-map "^0.5.6"
+ uglify-js "^2.8.29"
+ webpack-sources "^1.0.1"
+
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
+underscore-contrib@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7"
+ dependencies:
+ underscore "1.6.0"
+
+underscore@1.6.0, underscore@~1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
+
+underscore@~1.8.3:
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util@0.10.3, util@^0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ dependencies:
+ inherits "2.0.1"
+
+uuid@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ dependencies:
+ indexof "0.0.1"
+
+watchpack@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
+ dependencies:
+ async "^2.1.2"
+ chokidar "^1.7.0"
+ graceful-fs "^4.1.2"
+
+webpack-sources@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.5.3"
+
+webpack@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc"
+ dependencies:
+ acorn "^5.0.0"
+ acorn-dynamic-import "^2.0.0"
+ ajv "^5.1.5"
+ ajv-keywords "^2.0.0"
+ async "^2.1.2"
+ enhanced-resolve "^3.4.0"
+ escope "^3.6.0"
+ interpret "^1.0.0"
+ json-loader "^0.5.4"
+ json5 "^0.5.1"
+ loader-runner "^2.3.0"
+ loader-utils "^1.1.0"
+ memory-fs "~0.4.1"
+ mkdirp "~0.5.0"
+ node-libs-browser "^2.0.0"
+ source-map "^0.5.3"
+ supports-color "^4.2.1"
+ tapable "^0.2.7"
+ uglifyjs-webpack-plugin "^0.4.6"
+ watchpack "^1.4.0"
+ webpack-sources "^1.0.1"
+ yargs "^8.0.2"
+
+which-boxed-primitive@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
+ integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
+ dependencies:
+ is-bigint "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-number-object "^1.0.3"
+ is-string "^1.0.4"
+ is-symbol "^1.0.2"
+
+which-collection@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
+ integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
+ dependencies:
+ is-map "^2.0.1"
+ is-set "^2.0.1"
+ is-weakmap "^2.0.1"
+ is-weakset "^2.0.1"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which-typed-array@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
+ integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
+ dependencies:
+ available-typed-arrays "^1.0.2"
+ es-abstract "^1.17.5"
+ foreach "^2.0.5"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.1"
+ is-typed-array "^1.1.3"
+
+which@^1.2.9, which@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+window-size@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+xmlcreate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-1.0.2.tgz#fa6bf762a60a413fb3dd8f4b03c5b269238d308f"
+
+xtend@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yargs-parser@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
+yargs@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+ dependencies:
+ camelcase "^1.0.2"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ window-size "0.1.0"