diff options
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. @@ -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 Binary files differnew file mode 100644 index 0000000..c866266 --- /dev/null +++ b/tools/all_bytes 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" |