Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/vendor/
|
||||||
|
/.phpunit.*
|
||||||
309
LICENSE
Normal file
309
LICENSE
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||||
|
document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share
|
||||||
|
and change it. By contrast, the GNU General Public License is intended to
|
||||||
|
guarantee your freedom to share and change free software--to make sure the
|
||||||
|
software is free for all its users. This General Public License applies to most
|
||||||
|
of the Free Software Foundation's software and to any other program whose
|
||||||
|
authors commit to using it. (Some other Free Software Foundation software is
|
||||||
|
covered by the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our
|
||||||
|
General Public Licenses are designed to make sure that you have the freedom to
|
||||||
|
distribute copies of free software (and charge for this service if you wish),
|
||||||
|
that you receive source code or can get it if you want it, that you can change
|
||||||
|
the software or use pieces of it in new free programs; and that you know you can
|
||||||
|
do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny
|
||||||
|
you these rights or to ask you to surrender the rights. These restrictions
|
||||||
|
translate to certain responsibilities for you if you distribute copies of the
|
||||||
|
software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a
|
||||||
|
fee, you must give the recipients all the rights that you have. You must make
|
||||||
|
sure that they, too, receive or can get the source code. And you must show them
|
||||||
|
these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2) offer
|
||||||
|
you this license which gives you legal permission to copy, distribute and/or
|
||||||
|
modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that
|
||||||
|
everyone understands that there is no warranty for this free software. If the
|
||||||
|
software is modified by someone else and passed on, we want its recipients to
|
||||||
|
know that what they have is not the original, so that any problems introduced by
|
||||||
|
others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish
|
||||||
|
to avoid the danger that redistributors of a free program will individually
|
||||||
|
obtain patent licenses, in effect making the program proprietary. To prevent
|
||||||
|
this, we have made it clear that any patent must be licensed for everyone's free
|
||||||
|
use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification
|
||||||
|
follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a notice
|
||||||
|
placed by the copyright holder saying it may be distributed under the terms of
|
||||||
|
this General Public License. The "Program", below, refers to any such program or
|
||||||
|
work, and a "work based on the Program" means either the Program or any
|
||||||
|
derivative work under copyright law: that is to say, a work containing the
|
||||||
|
Program or a portion of it, either verbatim or with modifications and/or
|
||||||
|
translated into another language. (Hereinafter, translation is included without
|
||||||
|
limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not covered by
|
||||||
|
this License; they are outside its scope. The act of running the Program is not
|
||||||
|
restricted, and the output from the Program is covered only if its contents
|
||||||
|
constitute a work based on the Program (independent of having been made by
|
||||||
|
running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source code as
|
||||||
|
you receive it, in any medium, provided that you conspicuously and appropriately
|
||||||
|
publish on each copy an appropriate copyright notice and disclaimer of warranty;
|
||||||
|
keep intact all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may at
|
||||||
|
your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it, thus
|
||||||
|
forming a work based on the Program, and copy and distribute such modifications
|
||||||
|
or work under the terms of Section 1 above, provided that you also meet all of
|
||||||
|
these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating
|
||||||
|
that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole or
|
||||||
|
in part contains or is derived from the Program or any part thereof, to be
|
||||||
|
licensed as a whole at no charge to all third parties under the terms of this
|
||||||
|
License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when run,
|
||||||
|
you must cause it, when started running for such interactive use in the most
|
||||||
|
ordinary way, to print or display an announcement including an appropriate
|
||||||
|
copyright notice and a notice that there is no warranty (or else, saying that
|
||||||
|
you provide a warranty) and that users may redistribute the program under these
|
||||||
|
conditions, and telling the user how to view a copy of this License. (Exception:
|
||||||
|
if the Program itself is interactive but does not normally print such an
|
||||||
|
announcement, your work based on the Program is not required to print an
|
||||||
|
announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable
|
||||||
|
sections of that work are not derived from the Program, and can be reasonably
|
||||||
|
considered independent and separate works in themselves, then this License, and
|
||||||
|
its terms, do not apply to those sections when you distribute them as separate
|
||||||
|
works. But when you distribute the same sections as part of a whole which is a
|
||||||
|
work based on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the entire whole,
|
||||||
|
and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your
|
||||||
|
rights to work written entirely by you; rather, the intent is to exercise the
|
||||||
|
right to control the distribution of derivative or collective works based on the
|
||||||
|
Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the
|
||||||
|
Program (or with a work based on the Program) on a volume of a storage or
|
||||||
|
distribution medium does not bring the other work under the scope of this
|
||||||
|
License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under Section
|
||||||
|
2) in object code or executable form under the terms of Sections 1 and 2 above
|
||||||
|
provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source
|
||||||
|
code, which must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to
|
||||||
|
give any third party, for a charge no more than your cost of physically
|
||||||
|
performing source distribution, a complete machine-readable copy of the
|
||||||
|
corresponding source code, to be distributed under the terms of Sections 1 and 2
|
||||||
|
above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to
|
||||||
|
distribute corresponding source code. (This alternative is allowed only for
|
||||||
|
noncommercial distribution and only if you received the program in object code
|
||||||
|
or executable form with such an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making
|
||||||
|
modifications to it. For an executable work, complete source code means all the
|
||||||
|
source code for all modules it contains, plus any associated interface
|
||||||
|
definition files, plus the scripts used to control compilation and installation
|
||||||
|
of the executable. However, as a special exception, the source code distributed
|
||||||
|
need not include anything that is normally distributed (in either source or
|
||||||
|
binary form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component itself
|
||||||
|
accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the source code
|
||||||
|
from the same place counts as distribution of the source code, even though third
|
||||||
|
parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except as
|
||||||
|
expressly provided under this License. Any attempt otherwise to copy, modify,
|
||||||
|
sublicense or distribute the Program is void, and will automatically terminate
|
||||||
|
your rights under this License. However, parties who have received copies, or
|
||||||
|
rights, from you under this License will not have their licenses terminated so
|
||||||
|
long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed it.
|
||||||
|
However, nothing else grants you permission to modify or distribute the Program
|
||||||
|
or its derivative works. These actions are prohibited by law if you do not
|
||||||
|
accept this License. Therefore, by modifying or distributing the Program (or any
|
||||||
|
work based on the Program), you indicate your acceptance of this License to do
|
||||||
|
so, and all its terms and conditions for copying, distributing or modifying the
|
||||||
|
Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the Program),
|
||||||
|
the recipient automatically receives a license from the original licensor to
|
||||||
|
copy, distribute or modify the Program subject to these terms and conditions.
|
||||||
|
You may not impose any further restrictions on the recipients' exercise of the
|
||||||
|
rights granted herein. You are not responsible for enforcing compliance by third
|
||||||
|
parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent infringement
|
||||||
|
or for any other reason (not limited to patent issues), conditions are imposed
|
||||||
|
on you (whether by court order, agreement or otherwise) that contradict the
|
||||||
|
conditions of this License, they do not excuse you from the conditions of this
|
||||||
|
License. If you cannot distribute so as to satisfy simultaneously your
|
||||||
|
obligations under this License and any other pertinent obligations, then as a
|
||||||
|
consequence you may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by all those
|
||||||
|
who receive copies directly or indirectly through you, then the only way you
|
||||||
|
could satisfy both it and this License would be to refrain entirely from
|
||||||
|
distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply and the
|
||||||
|
section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or
|
||||||
|
other property right claims or to contest validity of any such claims; this
|
||||||
|
section has the sole purpose of protecting the integrity of the free software
|
||||||
|
distribution system, which is implemented by public license practices. Many
|
||||||
|
people have made generous contributions to the wide range of software
|
||||||
|
distributed through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing to
|
||||||
|
distribute software through any other system and a licensee cannot impose that
|
||||||
|
choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a
|
||||||
|
consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in certain
|
||||||
|
countries either by patents or by copyrighted interfaces, the original copyright
|
||||||
|
holder who places the Program under this License may add an explicit
|
||||||
|
geographical distribution limitation excluding those countries, so that
|
||||||
|
distribution is permitted only in or among countries not thus excluded. In such
|
||||||
|
case, this License incorporates the limitation as if written in the body of this
|
||||||
|
License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of the
|
||||||
|
General Public License from time to time. Such new versions will be similar in
|
||||||
|
spirit to the present version, but may differ in detail to address new problems
|
||||||
|
or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies
|
||||||
|
a version number of this License which applies to it and "any later version",
|
||||||
|
you have the option of following the terms and conditions either of that version
|
||||||
|
or of any later version published by the Free Software Foundation. If the
|
||||||
|
Program does not specify a version number of this License, you may choose any
|
||||||
|
version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free programs
|
||||||
|
whose distribution conditions are different, write to the author to ask for
|
||||||
|
permission. For software which is copyrighted by the Free Software Foundation,
|
||||||
|
write to the Free Software Foundation; we sometimes make exceptions for this.
|
||||||
|
Our decision will be guided by the two goals of preserving the free status of
|
||||||
|
all derivatives of our free software and of promoting the sharing and reuse of
|
||||||
|
software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE
|
||||||
|
PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED
|
||||||
|
IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS
|
||||||
|
IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT
|
||||||
|
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
|
||||||
|
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE
|
||||||
|
PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
|
||||||
|
SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
|
||||||
|
TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
|
||||||
|
THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
||||||
|
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use
|
||||||
|
to the public, the best way to achieve this is to make it free software which
|
||||||
|
everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach
|
||||||
|
them to the start of each source file to most effectively convey the exclusion
|
||||||
|
of warranty; and each file should have at least the "copyright" line and a
|
||||||
|
pointer to where the full notice is found.
|
||||||
|
|
||||||
|
one line to give the program's name and an idea of what it does. Copyright
|
||||||
|
(C) yyyy name of author
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it
|
||||||
|
under the terms of the GNU General Public License as published by the Free
|
||||||
|
Software Foundation; either version 2 of the License, or (at your option) any
|
||||||
|
later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||||
|
Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information
|
||||||
|
on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this when it
|
||||||
|
starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
|
||||||
|
with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software,
|
||||||
|
and you are welcome to redistribute it under certain conditions; type `show c'
|
||||||
|
for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may be
|
||||||
|
called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary. Here is
|
||||||
|
a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||||
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Protocol Base for ReactPHP
|
||||||
|
|
||||||
|
Use this library to build protocols. It doesn't do any single protocol to any greater
|
||||||
|
extent, but it provides callbacks and scaffolding for you to build and stream most
|
||||||
|
protocol data.
|
||||||
|
|
||||||
|
## About the protocols
|
||||||
|
|
||||||
|
This library is intended to be generic, so only protocols that have no external
|
||||||
|
dependencies will be included.
|
||||||
|
|
||||||
|
## Using
|
||||||
|
|
||||||
|
### Protocols
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Basic line-oriented protocol, think shell or SMTP
|
||||||
|
$proto = new LineProtocol($stream, [
|
||||||
|
'lineSeparator' => "\n",
|
||||||
|
'fieldSeparator' => " ",
|
||||||
|
'fieldQuote' => '"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// encode your frames and send it however you desire
|
||||||
|
$data = $proto->packFrame([ "mv", "foo.txt", "bar.txt" ]);
|
||||||
|
// → mv foo.txt bar.txt\n
|
||||||
|
|
||||||
|
// Or use JSON if you so desire
|
||||||
|
$proto = new JsonProtocol($stream, [
|
||||||
|
'frameSeparator' => "\0",
|
||||||
|
]);
|
||||||
|
$data = $proto->packFrame([ 'foo' => 'bar' ]);
|
||||||
|
// → {"foo":"bar"}\0
|
||||||
|
```
|
||||||
|
|
||||||
|
Unpacking works as expected:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$proto = new LineProtocol($stream, [
|
||||||
|
'lineSeparator' => "\n",
|
||||||
|
'fieldSeparator' => " ",
|
||||||
|
'fieldQuote' => '"',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// encode your frames and send it however you desire
|
||||||
|
$cmdline = $proto->unpackFrame("mv foo.txt bar.txt\n" ]);
|
||||||
|
// → [ "mv", "foo.txt", "bar.txt" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protocols with ProtocolStream
|
||||||
|
|
||||||
|
ProtocolStream combines a ProtocolInterface and a DuplexStreamInterface into
|
||||||
|
one package. It will emit an `message` event when a new frame has been
|
||||||
|
decoded, an `error` event if something goes wrong, and an `overflow` event
|
||||||
|
if the buffer overflows a configured max size.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$stream = new ProtocolStream($proto, $someduplexstream);
|
||||||
|
$stream->on("message", function (array $message) {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
$stream->send([ 'data' => 'here' ]);
|
||||||
|
```
|
||||||
24
composer.json
Normal file
24
composer.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "noccylabs/react-protocol",
|
||||||
|
"description": "Generic wire protocols for ReactPHP applications",
|
||||||
|
"type": "library",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"NoccyLabs\\React\\Protocol\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Christopher Vagnetoft",
|
||||||
|
"email": "labs@noccy.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"react/stream": "^1.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^13.0",
|
||||||
|
"phpstan/phpstan": "^2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
2049
composer.lock
generated
Normal file
2049
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
doc/json-protos.md
Normal file
12
doc/json-protos.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# JSON Protocols
|
||||||
|
|
||||||
|
The JSON protocol simply serializes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Available sub-formats
|
||||||
|
|
||||||
|
### JsonRpcProtocol
|
||||||
|
|
||||||
|
### NativeMessagingProtocol
|
||||||
34
doc/pack-ref.md
Normal file
34
doc/pack-ref.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# PHP pack/unpack reference
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| ---- | ------------------------------------------------------------ |
|
||||||
|
| a | NUL-padded string |
|
||||||
|
| A | SPACE-padded string |
|
||||||
|
| h | Hex string, low nibble first |
|
||||||
|
| H | Hex string, high nibble first |
|
||||||
|
| c | signed char |
|
||||||
|
| C | unsigned char |
|
||||||
|
| s | signed short (always 16 bit, machine byte order) |
|
||||||
|
| S | unsigned short (always 16 bit, machine byte order) |
|
||||||
|
| n | unsigned short (always 16 bit, big endian byte order) |
|
||||||
|
| v | unsigned short (always 16 bit, little endian byte order) |
|
||||||
|
| i | signed integer (machine dependent size and byte order) |
|
||||||
|
| I | unsigned integer (machine dependent size and byte order) |
|
||||||
|
| l | signed long (always 32 bit, machine byte order) |
|
||||||
|
| L | unsigned long (always 32 bit, machine byte order) |
|
||||||
|
| N | unsigned long (always 32 bit, big endian byte order) |
|
||||||
|
| V | unsigned long (always 32 bit, little endian byte order) |
|
||||||
|
| q | signed long long (always 64 bit, machine byte order) |
|
||||||
|
| Q | unsigned long long (always 64 bit, machine byte order) |
|
||||||
|
| J | unsigned long long (always 64 bit, big endian byte order) |
|
||||||
|
| P | unsigned long long (always 64 bit, little endian byte order) |
|
||||||
|
| f | float (machine dependent size and representation) |
|
||||||
|
| g | float (machine dependent size, little endian byte order) |
|
||||||
|
| G | float (machine dependent size, big endian byte order) |
|
||||||
|
| d | double (machine dependent size and representation) |
|
||||||
|
| e | double (machine dependent size, little endian byte order) |
|
||||||
|
| E | double (machine dependent size, big endian byte order) |
|
||||||
|
| x | NUL byte |
|
||||||
|
| X | Back up one byte |
|
||||||
|
| Z | NUL-terminated (ASCIIZ) string, will be NUL padded |
|
||||||
|
| @ | NUL-fill to absolute position |
|
||||||
48
doc/recipes.md
Normal file
48
doc/recipes.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Recipes
|
||||||
|
|
||||||
|
These are some suggestions and ideas for when building your own protocols using the
|
||||||
|
various protocol classes.
|
||||||
|
|
||||||
|
## Marshalling objects
|
||||||
|
|
||||||
|
You can use the beforePackCb and afterUnpackCb to serialize and unserialize parameters
|
||||||
|
as desired. Consider the security implications of this before you do anything stupid
|
||||||
|
though, as a spoofed frame could have unexpected consequences when unserialized.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
beforePackCb: function (array $frame): array {
|
||||||
|
$frame['obj'] = serialize($frame['obj']);
|
||||||
|
return $frame;
|
||||||
|
},
|
||||||
|
afterUnpackCb: function (array $frame): array {
|
||||||
|
$frame['obj'] = unserialize($frame['obj]);
|
||||||
|
return $frame;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building line-based messages
|
||||||
|
|
||||||
|
By using the `beforePackCb` callback, you can take messages in a structured format
|
||||||
|
and return them as a list for line-based protocols. You can do the same in reverse
|
||||||
|
with the `afterUnpackCb` callback.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$proto = new LineProtocol(
|
||||||
|
beforePackCb: function (array $frame): array {
|
||||||
|
switch ($frame['cmd']) {
|
||||||
|
case 'search':
|
||||||
|
return [ "search", $frame['query'] ];
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
},
|
||||||
|
afterUnpackCb: function (array $frame): array {
|
||||||
|
switch ($frame[0]) {
|
||||||
|
case 'search':
|
||||||
|
return [ 'cmd' => $frame[0], 'query' => $frame[1] ];
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
35
doc/text-protos.md
Normal file
35
doc/text-protos.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Text/Line Protocols
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Available sub-protocols
|
||||||
|
|
||||||
|
### HttpLikeProtocol
|
||||||
|
|
||||||
|
This is as the name says a HTTP-like parser. It can be used for HTTP, STOMP
|
||||||
|
and more. All messages are expected to follow the following style:
|
||||||
|
|
||||||
|
```text
|
||||||
|
query-line <$lineSeparator>
|
||||||
|
header-line* <$lineSeparator>
|
||||||
|
<$lineSeparator>
|
||||||
|
optional-body
|
||||||
|
```
|
||||||
|
|
||||||
|
Line separators default to `\n`, but can be set to any sequence or the value `true`
|
||||||
|
to attempt to automatically detect the endings used.
|
||||||
|
|
||||||
|
The presence of a body is determined by consulting the headers. The `$contentLengthHeader`
|
||||||
|
constructor parameter defaults to 'content-length', and can be set to null to disable parsing
|
||||||
|
of bodies entirely.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$proto = new HttpLikeProtocol(
|
||||||
|
queryFormat: '{command} {path}',
|
||||||
|
lineSeparator: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
13
examples/jsonproto.php
Normal file
13
examples/jsonproto.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__."/../vendor/autoload.php";
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||||
|
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
frameSeparator: "\n",
|
||||||
|
prependSizeBytes: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
echo $proto->packFrame([ 'hello'=>'world', 'answer'=>42 ]);
|
||||||
23
examples/jsonprotostream.php
Normal file
23
examples/jsonprotostream.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__."/../vendor/autoload.php";
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolStream;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ThroughStream;
|
||||||
|
use React\Stream\WritableResourceStream;
|
||||||
|
|
||||||
|
$rs = new ThroughStream();
|
||||||
|
$ws = new WritableResourceStream(STDOUT);
|
||||||
|
$stream = new CompositeStream($rs,$ws);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
frameSeparator: "\n",
|
||||||
|
prependSizeBytes: 0,
|
||||||
|
);
|
||||||
|
$stream = new ProtocolStream($stream, $proto);
|
||||||
|
|
||||||
|
$stream->send([ 'hello' => 'world' ]);
|
||||||
|
$stream->send([ 'foo' => 'bar', 'answer' => 42 ]);
|
||||||
|
|
||||||
9
examples/lineproto.php
Normal file
9
examples/lineproto.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__."/../vendor/autoload.php";
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\Line\LineProtocol;
|
||||||
|
|
||||||
|
$proto = new LineProtocol();
|
||||||
|
|
||||||
|
echo $proto->packFrame([ 'hello', 'world' ]);
|
||||||
55
examples/upgrading.php
Normal file
55
examples/upgrading.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This example demonstrates upgrading a ProtocolStream to use a different protocol
|
||||||
|
* than initially configured with.
|
||||||
|
*
|
||||||
|
* Run the example and try entering some things such as:
|
||||||
|
* hello world
|
||||||
|
* foo "bar baz"
|
||||||
|
* and finally:
|
||||||
|
* upgrade
|
||||||
|
* After the upgrade command, it will only respond to JSON data:
|
||||||
|
* {"foo":"bar"}
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||||
|
use NoccyLabs\React\Protocol\Line\LineProtocol;
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolStream;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ReadableResourceStream;
|
||||||
|
use React\Stream\WritableResourceStream;
|
||||||
|
|
||||||
|
require_once __DIR__."/../vendor/autoload.php";
|
||||||
|
|
||||||
|
$stdin = new ReadableResourceStream(STDIN);
|
||||||
|
$stdout = new WritableResourceStream(STDOUT);
|
||||||
|
$stdio = new CompositeStream($stdin, $stdout);
|
||||||
|
|
||||||
|
$textProto = new LineProtocol(
|
||||||
|
beforePackCb: function (array $msg): array {
|
||||||
|
$out = [];
|
||||||
|
$cmd = array_shift($msg);
|
||||||
|
$out = [ $cmd, ...array_map("json_encode", $msg) ];
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$jsonProto = new JsonProtocol(
|
||||||
|
frameSeparator: "\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
$stream = new ProtocolStream($stdio, $textProto);
|
||||||
|
$stream->on("message", function (array $msg) use ($stream, $jsonProto) {
|
||||||
|
if (array_is_list($msg)) {
|
||||||
|
if (reset($msg) == 'upgrade') {
|
||||||
|
$stream->send([ 'OK', 'Upgrading to JSON protocol']);
|
||||||
|
$stream->upgrade($jsonProto);
|
||||||
|
$stream->send([ 'info' => 'Now using JSON proto' ]);
|
||||||
|
} else {
|
||||||
|
$stream->send([ 'echo', json_encode($msg)]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$stream->send([ 'echo' => $msg ]);
|
||||||
|
}
|
||||||
|
});
|
||||||
5
phpstan.neon
Normal file
5
phpstan.neon
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
level: 5
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
|
||||||
25
phpunit.xml
Normal file
25
phpunit.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
executionOrder="depends,defects"
|
||||||
|
requireCoverageMetadata="true"
|
||||||
|
beStrictAboutCoverageMetadata="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
displayDetailsOnPhpunitDeprecations="true"
|
||||||
|
failOnPhpunitDeprecation="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="default">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
|
||||||
|
<include>
|
||||||
|
<directory>src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
142
src/Binary/BinaryProtocol.php
Normal file
142
src/Binary/BinaryProtocol.php
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Binary;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolException;
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolInterface;
|
||||||
|
|
||||||
|
class BinaryProtocol implements ProtocolInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $frameSeparator = "",
|
||||||
|
public readonly int $prependSizeBytes = 0,
|
||||||
|
public readonly string $prependSizeEndian = 'l',
|
||||||
|
public readonly ?string $compression = null,
|
||||||
|
private readonly ?Closure $beforePackCb = null,
|
||||||
|
private readonly ?Closure $afterPackCb = null,
|
||||||
|
private readonly ?Closure $beforeUnpackCb = null,
|
||||||
|
private readonly ?Closure $afterUnpackCb = null,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function packFrame(array $frame): string
|
||||||
|
{
|
||||||
|
if (is_callable($this->beforePackCb))
|
||||||
|
$frame = call_user_func($this->beforePackCb, $frame);
|
||||||
|
|
||||||
|
$data = $frame['payload'];
|
||||||
|
|
||||||
|
// append separator
|
||||||
|
$data = $data . $this->frameSeparator;
|
||||||
|
|
||||||
|
// prepend size
|
||||||
|
if ($this->prependSizeBytes > 0) {
|
||||||
|
$data = $this->packSizeBytes(strlen($data)) . $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($this->afterPackCb))
|
||||||
|
$data = call_user_func($this->afterPackCb, $data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unpackFrame(string $data): array
|
||||||
|
{
|
||||||
|
if (is_callable($this->beforeUnpackCb))
|
||||||
|
$data = call_user_func($this->beforeUnpackCb, $data);
|
||||||
|
|
||||||
|
if ($this->prependSizeBytes > 0) {
|
||||||
|
if ($this->prependSizeBytes > strlen($data)) {
|
||||||
|
// not enough data to parse size...
|
||||||
|
throw new ProtocolException("BinaryProtocol: Not enough data to parse size");
|
||||||
|
}
|
||||||
|
$len = $this->unpackSizeBytes($data);
|
||||||
|
if ($len <= 0) {
|
||||||
|
// unparsable?
|
||||||
|
throw new ProtocolException("BinaryProtocol: Invalid size decoded from frame");
|
||||||
|
}
|
||||||
|
if ($len > strlen($data) - $this->prependSizeBytes) {
|
||||||
|
// insufficient data
|
||||||
|
throw new ProtocolException("BinaryProtocol: Insufficient data for unpacking (want {$len} but got ".(strlen($data)).")");
|
||||||
|
}
|
||||||
|
$data = substr($data, $this->prependSizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->frameSeparator) {
|
||||||
|
$data = substr($data, 0, -strlen($this->frameSeparator));
|
||||||
|
}
|
||||||
|
|
||||||
|
$frame = [ 'payload' => $data ];
|
||||||
|
// echo "[{$data}]"; var_dump($frame);
|
||||||
|
if (!$frame) {
|
||||||
|
// invalid json
|
||||||
|
throw new ProtocolException("Unparsable frame received: {$data}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($this->afterUnpackCb))
|
||||||
|
$frame = call_user_func($this->afterUnpackCb, $frame);
|
||||||
|
|
||||||
|
return $frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consumeFrame(string &$data): ?array
|
||||||
|
{
|
||||||
|
// check for $this->prependSizeBytes
|
||||||
|
if ($this->prependSizeBytes > 0) {
|
||||||
|
$len = $this->unpackSizeBytes($data);
|
||||||
|
// if size is greater than data (i.e. incomplete)
|
||||||
|
if ($len > strlen($data) - $this->prependSizeBytes) return null;
|
||||||
|
$p = $len;
|
||||||
|
// $data = substr($data, $this->prependSizeBytes);
|
||||||
|
} elseif ($this->frameSeparator) {
|
||||||
|
// check for $this->frameSeparator
|
||||||
|
$p = strpos($data, $this->frameSeparator);
|
||||||
|
if ($p === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$frame = substr($data, 0, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||||
|
$data = substr($data, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||||
|
|
||||||
|
return $this->unpackFrame($frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function packSizeBytes(int $size): string
|
||||||
|
{
|
||||||
|
$endian = function($b,$l,$s) {
|
||||||
|
return match ($this->prependSizeEndian) {
|
||||||
|
'b' => $b,
|
||||||
|
'l' => $l,
|
||||||
|
default => $s
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return match ($this->prependSizeBytes) {
|
||||||
|
1 => pack($endian("C","C","C"), $size),
|
||||||
|
2 => pack($endian("n","v","S"), $size),
|
||||||
|
4 => pack($endian("N","V","L"), $size),
|
||||||
|
default => throw new ProtocolException("BinaryProtocol: Invalid message size length")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unpackSizeBytes(string $data): int
|
||||||
|
{
|
||||||
|
$bytes = substr($data, 0, $this->prependSizeBytes);
|
||||||
|
$endian = function($b,$l,$s) {
|
||||||
|
return match ($this->prependSizeEndian) {
|
||||||
|
'b' => $b,
|
||||||
|
'l' => $l,
|
||||||
|
default => $s
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return match ($this->prependSizeBytes) {
|
||||||
|
1 => unpack($endian("C","C","C")."len", $bytes)['len'],
|
||||||
|
2 => unpack($endian("n","v","S")."len", $bytes)['len'],
|
||||||
|
4 => unpack($endian("N","V","L")."len", $bytes)['len'],
|
||||||
|
default => throw new ProtocolException("BinaryProtocol: Invalid message size length")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
148
src/Json/JsonProtocol.php
Normal file
148
src/Json/JsonProtocol.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Json;
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolInterface;
|
||||||
|
use Closure;
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolException;
|
||||||
|
use React\Stream\DuplexStreamInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class JsonProtocol implements ProtocolInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $frameSeparator = "\0",
|
||||||
|
public readonly int $prependSizeBytes = 0,
|
||||||
|
public readonly string $prependSizeEndian = 'l',
|
||||||
|
public readonly bool $unescapedSlashes = true,
|
||||||
|
private readonly ?Closure $beforePackCb = null,
|
||||||
|
private readonly ?Closure $afterPackCb = null,
|
||||||
|
private readonly ?Closure $beforeUnpackCb = null,
|
||||||
|
private readonly ?Closure $afterUnpackCb = null,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function packFrame(array $frame): string
|
||||||
|
{
|
||||||
|
if (is_callable($this->beforePackCb))
|
||||||
|
$frame = call_user_func($this->beforePackCb, $frame);
|
||||||
|
|
||||||
|
$jsonOpts = ($this->unescapedSlashes?\JSON_UNESCAPED_SLASHES:0);
|
||||||
|
$data = @json_encode($frame, $jsonOpts);
|
||||||
|
if (!$data) {
|
||||||
|
throw new ProtocolException("JsonProtocol: Empty data after serializing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// append separator
|
||||||
|
$data = $data . $this->frameSeparator;
|
||||||
|
|
||||||
|
// prepend size
|
||||||
|
if ($this->prependSizeBytes > 0) {
|
||||||
|
$data = $this->packSizeBytes(strlen($data)) . $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($this->afterPackCb))
|
||||||
|
$data = call_user_func($this->afterPackCb, $data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unpackFrame(string $data): array
|
||||||
|
{
|
||||||
|
if (is_callable($this->beforeUnpackCb))
|
||||||
|
$data = call_user_func($this->beforeUnpackCb, $data);
|
||||||
|
|
||||||
|
if ($this->prependSizeBytes > 0) {
|
||||||
|
if ($this->prependSizeBytes > strlen($data)) {
|
||||||
|
// not enough data to parse size...
|
||||||
|
throw new ProtocolException("JsonProtocol: Not enough data to parse size");
|
||||||
|
}
|
||||||
|
$len = $this->unpackSizeBytes($data);
|
||||||
|
if ($len <= 0) {
|
||||||
|
// unparsable?
|
||||||
|
throw new ProtocolException("JsonProtocol: Invalid size decoded from frame");
|
||||||
|
}
|
||||||
|
if ($len > strlen($data) - $this->prependSizeBytes) {
|
||||||
|
// insufficient data
|
||||||
|
throw new ProtocolException("JsonProtocol: Insufficient data for unpacking");
|
||||||
|
}
|
||||||
|
$data = substr($data, $this->prependSizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->frameSeparator) {
|
||||||
|
$data = substr($data, 0, -strlen($this->frameSeparator));
|
||||||
|
}
|
||||||
|
|
||||||
|
$frame = @json_decode($data, true);
|
||||||
|
// echo "[{$data}]"; var_dump($frame);
|
||||||
|
if (!$frame) {
|
||||||
|
// invalid json
|
||||||
|
throw new ProtocolException("Unparsable frame received: {$data}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($this->afterUnpackCb))
|
||||||
|
$frame = call_user_func($this->afterUnpackCb, $frame);
|
||||||
|
|
||||||
|
return $frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consumeFrame(string &$data): ?array
|
||||||
|
{
|
||||||
|
// check for $this->prependSizeBytes
|
||||||
|
if ($this->prependSizeBytes > 0) {
|
||||||
|
$len = $this->unpackSizeBytes($data);
|
||||||
|
// if size is greater than data (i.e. incomplete)
|
||||||
|
if ($len > strlen($data) - $this->prependSizeBytes) return null;
|
||||||
|
$p = $len;
|
||||||
|
// $data = substr($data, $this->prependSizeBytes);
|
||||||
|
} elseif ($this->frameSeparator) {
|
||||||
|
// check for $this->frameSeparator
|
||||||
|
$p = strpos($data, $this->frameSeparator);
|
||||||
|
if ($p === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$frame = substr($data, 0, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||||
|
$data = substr($data, $p + strlen($this->frameSeparator) + $this->prependSizeBytes);
|
||||||
|
|
||||||
|
return $this->unpackFrame($frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function packSizeBytes(int $size): string
|
||||||
|
{
|
||||||
|
$endian = function($b,$l,$s) {
|
||||||
|
return match ($this->prependSizeEndian) {
|
||||||
|
'b' => $b,
|
||||||
|
'l' => $l,
|
||||||
|
default => $s
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return match ($this->prependSizeBytes) {
|
||||||
|
1 => pack($endian("C","C","C"), $size),
|
||||||
|
2 => pack($endian("n","v","S"), $size),
|
||||||
|
4 => pack($endian("N","V","L"), $size),
|
||||||
|
default => throw new ProtocolException("JsonProtocol: Invalid message size length")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unpackSizeBytes(string $data): int
|
||||||
|
{
|
||||||
|
$bytes = substr($data, 0, $this->prependSizeBytes);
|
||||||
|
$endian = function($b,$l,$s) {
|
||||||
|
return match ($this->prependSizeEndian) {
|
||||||
|
'b' => $b,
|
||||||
|
'l' => $l,
|
||||||
|
default => $s
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return match ($this->prependSizeBytes) {
|
||||||
|
1 => unpack($endian("C","C","C")."len", $bytes)['len'],
|
||||||
|
2 => unpack($endian("n","v","S")."len", $bytes)['len'],
|
||||||
|
4 => unpack($endian("N","V","L")."len", $bytes)['len'],
|
||||||
|
default => throw new ProtocolException("JsonProtocol: Invalid message size length")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
43
src/Json/JsonRpcProtocol.php
Normal file
43
src/Json/JsonRpcProtocol.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Json;
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolException;
|
||||||
|
use React\Stream\DuplexStreamInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the JSON-RPC base protocol.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class JsonRpcProtocol extends JsonProtocol
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
string $frameSeparator = "\n",
|
||||||
|
)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
frameSeparator: $frameSeparator,
|
||||||
|
prependSizeBytes: 0,
|
||||||
|
unescapedSlashes: true,
|
||||||
|
beforePackCb: $this->beforePack(...),
|
||||||
|
afterPackCb: null,
|
||||||
|
beforeUnpackCb: null,
|
||||||
|
afterUnpackCb: $this->afterUnpack(...)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function beforePack(array $frame): array
|
||||||
|
{
|
||||||
|
$frame['jsonrpc'] ??= "2.0";
|
||||||
|
if (!(isset($frame['method']) || isset($frame['result']))) {
|
||||||
|
throw new ProtocolException("JsonRpcProtocol: Either method or result key must be present");
|
||||||
|
}
|
||||||
|
return $frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function afterUnpack(array $frame): array
|
||||||
|
{
|
||||||
|
return $frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Json/NativeMessagingProtocol.php
Normal file
28
src/Json/NativeMessagingProtocol.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Json;
|
||||||
|
|
||||||
|
use React\Stream\DuplexStreamInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the common browser Native Messaging Protocol, used to
|
||||||
|
* communicate with a browser script from local processes.
|
||||||
|
*
|
||||||
|
* For more see:
|
||||||
|
* - https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging
|
||||||
|
* - https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging
|
||||||
|
*/
|
||||||
|
class NativeMessagingProtocol extends JsonProtocol
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
frameSeparator: '',
|
||||||
|
prependSizeBytes: 4,
|
||||||
|
prependSizeEndian: 's',
|
||||||
|
unescapedSlashes: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
87
src/Line/HttpLikeProtocol.php
Normal file
87
src/Line/HttpLikeProtocol.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Line;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolException;
|
||||||
|
|
||||||
|
class HttpLikeProtocol extends LineProtocol
|
||||||
|
{
|
||||||
|
private ?int $wantedPayloadSize = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $lineSeparator = "\n",
|
||||||
|
private readonly bool $capturePayloads = false,
|
||||||
|
private readonly ?string $payloadLengthHeader = null,
|
||||||
|
private readonly ?Closure $beforePackCb = null,
|
||||||
|
private readonly ?Closure $afterPackCb = null,
|
||||||
|
private readonly ?Closure $beforeUnpackCb = null,
|
||||||
|
private readonly ?Closure $afterUnpackCb = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return parent::__construct(
|
||||||
|
lineBreak: str_repeat($lineSeparator, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function packFrame(array $frame): string
|
||||||
|
{
|
||||||
|
if (is_callable($this->beforePackCb))
|
||||||
|
$frame = call_user_func($this->beforePackCb, $frame);
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
foreach ($frame['headers']??[] as $header => $values) {
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$headers[] = sprintf("%s: %s", $header, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($this->afterPackCb))
|
||||||
|
$frame = call_user_func($this->afterPackCb, $frame);
|
||||||
|
|
||||||
|
return join($this->lineSeparator, [
|
||||||
|
$frame['query'],
|
||||||
|
...$headers,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unpackFrame(string $data): array
|
||||||
|
{
|
||||||
|
if (is_callable($this->beforeUnpackCb))
|
||||||
|
$data = call_user_func($this->beforeUnpackCb, $data);
|
||||||
|
|
||||||
|
$lines = explode($this->lineSeparator, $data);
|
||||||
|
$query = array_shift($lines);
|
||||||
|
$headers = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (!trim($line) && $line == end($lines)) continue;
|
||||||
|
$line = trim($line);
|
||||||
|
if (!str_contains($line, ":")) {
|
||||||
|
throw new ProtocolException("Invalid header line: {$line}");
|
||||||
|
}
|
||||||
|
[$header, $value] = array_map(trim(...), explode(":", $line, 2));
|
||||||
|
if (!array_key_exists($header, $headers)) $headers[$header] = [];
|
||||||
|
$headers[$header][] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$frame = [
|
||||||
|
'query' => $query,
|
||||||
|
'headers' => $headers,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (is_callable($this->afterUnpackCb))
|
||||||
|
$frame = call_user_func($this->afterUnpackCb, $frame);
|
||||||
|
|
||||||
|
return $frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consumeFrame(string &$data): ?array
|
||||||
|
{
|
||||||
|
if ($this->wantedPayloadSize > 0) {
|
||||||
|
// consume and emit a "payload" event
|
||||||
|
}
|
||||||
|
return parent::consumeFrame($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/Line/LineProtocol.php
Normal file
64
src/Line/LineProtocol.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Line;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolInterface;
|
||||||
|
|
||||||
|
class LineProtocol implements ProtocolInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $lineBreak = "\n",
|
||||||
|
private readonly bool $quoteStrings = false,
|
||||||
|
private readonly bool $escapeSpecial = false,
|
||||||
|
private readonly ?Closure $beforePackCb = null,
|
||||||
|
private readonly ?Closure $afterPackCb = null,
|
||||||
|
private readonly ?Closure $beforeUnpackCb = null,
|
||||||
|
private readonly ?Closure $afterUnpackCb = null,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function packFrame(array $frame): string
|
||||||
|
{
|
||||||
|
if (is_callable($this->beforePackCb))
|
||||||
|
$frame = call_user_func($this->beforePackCb, $frame);
|
||||||
|
|
||||||
|
if ($this->escapeSpecial) {
|
||||||
|
$frame = array_map(quotemeta(...), $frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->quoteStrings) {
|
||||||
|
$frame = array_map(fn($v) => is_string($v) ? ('"'.str_replace('"',"\\\"",$v).'"') : $v, $frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = join(" ", $frame);
|
||||||
|
$data .= $this->lineBreak;
|
||||||
|
|
||||||
|
if (is_callable($this->afterPackCb))
|
||||||
|
$data = call_user_func($this->afterPackCb, $data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unpackFrame(string $data): array
|
||||||
|
{
|
||||||
|
return str_getcsv($data, ' ', '"', "\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consumeFrame(string &$data): ?array
|
||||||
|
{
|
||||||
|
// check for $this->lineBreak
|
||||||
|
$p = strpos($data, $this->lineBreak);
|
||||||
|
if ($p === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// break on separator
|
||||||
|
$frame = substr($data, 0, $p);
|
||||||
|
$data = substr($data, $p + strlen($this->lineBreak));
|
||||||
|
|
||||||
|
return $this->unpackFrame($frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/ProtocolException.php
Normal file
10
src/ProtocolException.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ProtocolException extends Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
50
src/ProtocolInterface.php
Normal file
50
src/ProtocolInterface.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol;
|
||||||
|
|
||||||
|
interface ProtocolInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Pack a frame, turning an array of data into the frame format.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function packFrame(array $data): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpack a frame, turning a string or binary blob into an array of data.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param array
|
||||||
|
*/
|
||||||
|
public function unpackFrame(string $data): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpacks a frame from the buffer if possible. If a frame is unpacked, the
|
||||||
|
* consumed bytes will be removed from the head of $data.
|
||||||
|
*
|
||||||
|
* @param string $data (byref)
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function consumeFrame(string &$data): ?array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peek at the buffer and determine if we can consume a frame.
|
||||||
|
*
|
||||||
|
* If peekFrame returns true, calling consumeFrame() should return the frame
|
||||||
|
* detected by peekFrame(). The function may therefore prepare a frame to be
|
||||||
|
* returned immediately on the next call to consumeFrame(). This also means
|
||||||
|
* that you should NEVER call peekFrame() without intending to call
|
||||||
|
* consumeFrame() immediately on a true result.
|
||||||
|
*
|
||||||
|
* The reasoning for this is that peekFrame() may need to parse a header to
|
||||||
|
* determine a body length, and this parsing may be as involved as the
|
||||||
|
* actual consumption/parsing.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
// public function peekFrame(string $data): bool;
|
||||||
|
|
||||||
|
}
|
||||||
72
src/ProtocolStream.php
Normal file
72
src/ProtocolStream.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol;
|
||||||
|
|
||||||
|
use Evenement\EventEmitterInterface;
|
||||||
|
use Evenement\EventEmitterTrait;
|
||||||
|
use React\Stream\DuplexStreamInterface;
|
||||||
|
|
||||||
|
class ProtocolStream implements EventEmitterInterface
|
||||||
|
{
|
||||||
|
use EventEmitterTrait;
|
||||||
|
|
||||||
|
private string $readBuffer = '';
|
||||||
|
|
||||||
|
private ProtocolInterface $protocol;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly DuplexStreamInterface $stream,
|
||||||
|
ProtocolInterface $protocol,
|
||||||
|
private readonly int $maxBuffer = 8192,
|
||||||
|
private readonly bool $closeOnOverflow = false,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
$this->protocol = $protocol;
|
||||||
|
$stream->on("data", $this->receive(...));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade the protocol, switching from f.ex. a text based to a JSON
|
||||||
|
* based protocol without having to re-create the ProtocolStream.
|
||||||
|
*
|
||||||
|
* @param ProtocolInterface $protocol
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function upgrade(ProtocolInterface $protocol): void
|
||||||
|
{
|
||||||
|
$this->protocol = $protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undocumented function
|
||||||
|
*
|
||||||
|
* @param array $frame
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function send(array $frame): void
|
||||||
|
{
|
||||||
|
$data = $this->protocol->packFrame($frame);
|
||||||
|
$this->stream->write($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function receive(string $data): void
|
||||||
|
{
|
||||||
|
$this->readBuffer .= $data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while ($frame = $this->protocol->consumeFrame($this->readBuffer)) {
|
||||||
|
$this->emit("message", [ $frame ]);
|
||||||
|
}
|
||||||
|
} catch (ProtocolException $e) {
|
||||||
|
$this->emit("error", [ $e ]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($this->readBuffer) > $this->maxBuffer) {
|
||||||
|
$this->emit("overflow");
|
||||||
|
if ($this->closeOnOverflow) {
|
||||||
|
$this->stream->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
tests/Binary/BinaryProtocolTest.php
Normal file
92
tests/Binary/BinaryProtocolTest.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Binary;
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\ProtocolException;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
|
||||||
|
#[CoversClass(BinaryProtocol::class)]
|
||||||
|
class BinaryProtocolTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testPackingFrames()
|
||||||
|
{
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "\0"
|
||||||
|
);
|
||||||
|
$res1 = $proto->packFrame([ 'payload' => 'foobar' ]);
|
||||||
|
$this->assertEquals("foobar\0", $res1);
|
||||||
|
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "",
|
||||||
|
prependSizeBytes: 2,
|
||||||
|
);
|
||||||
|
$res1 = $proto->packFrame([ 'payload' => 'foobar' ]);
|
||||||
|
$this->assertEquals("\x06\x00foobar", $res1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnpackingFrames()
|
||||||
|
{
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "\0"
|
||||||
|
);
|
||||||
|
$res1 = $proto->unpackFrame("foobar\0");
|
||||||
|
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||||
|
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "",
|
||||||
|
prependSizeBytes: 2,
|
||||||
|
);
|
||||||
|
$res1 = $proto->unpackFrame("\x06\x00foobar");
|
||||||
|
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnpackingIncompleteFrames()
|
||||||
|
{
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "\0"
|
||||||
|
);
|
||||||
|
$res1 = $proto->unpackFrame("foobar\0");
|
||||||
|
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||||
|
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "",
|
||||||
|
prependSizeBytes: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectException(ProtocolException::class);
|
||||||
|
$res0 = $proto->unpackFrame("\x06");
|
||||||
|
$this->expectException(ProtocolException::class);
|
||||||
|
$res1 = $proto->unpackFrame("\x00foo");
|
||||||
|
$res2 = $proto->unpackFrame("bar");
|
||||||
|
|
||||||
|
$this->assertNull($res1);
|
||||||
|
$this->assertEquals([ 'payload' => 'foobar' ], $res2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConsumingFrames()
|
||||||
|
{
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "\0"
|
||||||
|
);
|
||||||
|
$res1 = $proto->unpackFrame("foobar\0");
|
||||||
|
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||||
|
|
||||||
|
$proto = new BinaryProtocol(
|
||||||
|
frameSeparator: "",
|
||||||
|
prependSizeBytes: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
$buffer = "\x06\x00foobar\x06\x00foobaz";
|
||||||
|
|
||||||
|
$res1 = $proto->consumeFrame($buffer);
|
||||||
|
$res2 = $proto->consumeFrame($buffer);
|
||||||
|
|
||||||
|
$this->assertEquals([ 'payload' => 'foobar' ], $res1);
|
||||||
|
$this->assertEquals([ 'payload' => 'foobaz' ], $res2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
109
tests/Json/JsonProtocolTest.php
Normal file
109
tests/Json/JsonProtocolTest.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Json;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ThroughStream;
|
||||||
|
|
||||||
|
#[CoversClass(JsonProtocol::class)]
|
||||||
|
class JsonProtocolTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testFrameSeparators()
|
||||||
|
{
|
||||||
|
$proto = new JsonProtocol("\0");
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("{\"foo\":\"bar\"}\0", $packed);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol("\n");
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("{\"foo\":\"bar\"}\n", $packed);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFrameSizePrefixes()
|
||||||
|
{
|
||||||
|
$proto = new JsonProtocol(prependSizeBytes: 1);
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("\x0E{\"foo\":\"bar\"}\0", $packed);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(prependSizeBytes: 2, prependSizeEndian: 'l');
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("\x0E\x00{\"foo\":\"bar\"}\0", $packed);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(prependSizeBytes: 2, prependSizeEndian: 'b');
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("\x00\x0E{\"foo\":\"bar\"}\0", $packed);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(prependSizeBytes: 4, prependSizeEndian: 'l');
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("\x0E\x00\x00\x00{\"foo\":\"bar\"}\0", $packed);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(prependSizeBytes: 4, prependSizeEndian: 'b');
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("\x00\x00\x00\x0E{\"foo\":\"bar\"}\0", $packed);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPackingCallbacks()
|
||||||
|
{
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
prependSizeBytes: 0,
|
||||||
|
beforePackCb: function (array $data) {
|
||||||
|
$data['a'] = true;
|
||||||
|
return $data;
|
||||||
|
},
|
||||||
|
afterPackCb: function (string $data) {
|
||||||
|
return $data."a";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$packed = $proto->packFrame(['foo' => 'bar']);
|
||||||
|
$this->assertEquals("{\"foo\":\"bar\",\"a\":true}\0a", $packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnpackingCallbacks()
|
||||||
|
{
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
prependSizeBytes: 0,
|
||||||
|
beforeUnpackCb: function (string $data) {
|
||||||
|
return strtolower($data);
|
||||||
|
},
|
||||||
|
afterUnpackCb: function (array $data) {
|
||||||
|
unset($data['a']);
|
||||||
|
return $data;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$unpacked = $proto->unpackFrame("{\"FOO\":\"bar\",\"a\":true}\0");
|
||||||
|
$expect = ['foo' => 'bar'];
|
||||||
|
$this->assertEquals($expect, $unpacked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConsumingFrames()
|
||||||
|
{
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
frameSeparator: "\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
$buffer = '{"foo":true}'."\n".'{"bar":false}'."\n";
|
||||||
|
$msg1 = $proto->consumeFrame($buffer);
|
||||||
|
$msg2 = $proto->consumeFrame($buffer);
|
||||||
|
|
||||||
|
$this->assertEquals(['foo'=>true], $msg1);
|
||||||
|
$this->assertEquals(['bar'=>false], $msg2);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
frameSeparator: "\n",
|
||||||
|
prependSizeBytes: 4,
|
||||||
|
prependSizeEndian: 'l',
|
||||||
|
);
|
||||||
|
|
||||||
|
$buffer = "\x0c\x00\x00\x00".'{"foo":true}'."\n"."\x0d\x00\x00\x00".'{"bar":false}'."\n";
|
||||||
|
$msg1 = $proto->consumeFrame($buffer);
|
||||||
|
$msg2 = $proto->consumeFrame($buffer);
|
||||||
|
|
||||||
|
$this->assertEquals(['foo'=>true], $msg1);
|
||||||
|
$this->assertEquals(['bar'=>false], $msg2);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
25
tests/Json/JsonRpcProtocolTest.php
Normal file
25
tests/Json/JsonRpcProtocolTest.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Json;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ThroughStream;
|
||||||
|
|
||||||
|
#[CoversClass(JsonProtocol::class)]
|
||||||
|
#[CoversClass(JsonRpcProtocol::class)]
|
||||||
|
class JsonRpcProtocolTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testProcessingCallbacks()
|
||||||
|
{
|
||||||
|
$proto = new JsonRpcProtocol(
|
||||||
|
frameSeparator: "\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
$packed = $proto->packFrame([ 'method' => 'foo', 'params' => [ 'bar' => true ] ]);
|
||||||
|
|
||||||
|
$this->assertEquals('{"method":"foo","params":{"bar":true},"jsonrpc":"2.0"}'."\n", $packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
23
tests/Json/NativeMessagingProtocolTest.php
Normal file
23
tests/Json/NativeMessagingProtocolTest.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Json;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ThroughStream;
|
||||||
|
|
||||||
|
#[CoversClass(JsonProtocol::class)]
|
||||||
|
#[CoversClass(NativeMessagingProtocol::class)]
|
||||||
|
class NativeMessagingProtocolTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testFramePacking()
|
||||||
|
{
|
||||||
|
$proto = new NativeMessagingProtocol();
|
||||||
|
|
||||||
|
$packed = $proto->packFrame([ 'method' => 'foo', 'params' => [ 'bar' => true ] ]);
|
||||||
|
|
||||||
|
$this->assertEquals("\x26\x00\x00\x00".'{"method":"foo","params":{"bar":true}}', $packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
45
tests/Line/HttpLikeProtocolTest.php
Normal file
45
tests/Line/HttpLikeProtocolTest.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Line;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ThroughStream;
|
||||||
|
|
||||||
|
#[CoversClass(HttpLikeProtocol::class)]
|
||||||
|
class HttpLikeProtocolTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testPackingFrames()
|
||||||
|
{
|
||||||
|
$proto = new HttpLikeProtocol(
|
||||||
|
lineSeparator: "\n"
|
||||||
|
);
|
||||||
|
$packed = $proto->packFrame(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]]);
|
||||||
|
$this->assertEquals("GET / HTTP/1.1\nfoo: bar\n\n", $packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnpackingFrames()
|
||||||
|
{
|
||||||
|
$proto = new HttpLikeProtocol(
|
||||||
|
lineSeparator: "\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
$msg1 = $proto->unpackFrame("GET / HTTP/1.1\nfoo: bar\n\n");
|
||||||
|
$this->assertEquals(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]], $msg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConsumingFrames()
|
||||||
|
{
|
||||||
|
$proto = new HttpLikeProtocol(
|
||||||
|
lineSeparator: "\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
$buffer = "GET / HTTP/1.1\nfoo: bar\n\n";
|
||||||
|
$msg1 = $proto->consumeFrame($buffer);
|
||||||
|
// $msg2 = $proto->consumeFrame($buffer);
|
||||||
|
|
||||||
|
$this->assertEquals(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]], $msg1);
|
||||||
|
// $this->assertEquals(['bar','false'], $msg2);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
tests/Line/LineProtocolTest.php
Normal file
33
tests/Line/LineProtocolTest.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol\Line;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ThroughStream;
|
||||||
|
|
||||||
|
#[CoversClass(LineProtocol::class)]
|
||||||
|
class LineProtocolTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testFrameSeparators()
|
||||||
|
{
|
||||||
|
$proto = new LineProtocol("\n");
|
||||||
|
$packed = $proto->packFrame(['foo', 'bar']);
|
||||||
|
$this->assertEquals("foo bar\n", $packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConsumingFrames()
|
||||||
|
{
|
||||||
|
$proto = new LineProtocol(
|
||||||
|
lineBreak: "\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
$buffer = "foo true\nbar false\n";
|
||||||
|
$msg1 = $proto->consumeFrame($buffer);
|
||||||
|
$msg2 = $proto->consumeFrame($buffer);
|
||||||
|
|
||||||
|
$this->assertEquals(['foo','true'], $msg1);
|
||||||
|
$this->assertEquals(['bar','false'], $msg2);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
tests/ProtocolStreamTest.php
Normal file
82
tests/ProtocolStreamTest.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\React\Protocol;
|
||||||
|
|
||||||
|
use NoccyLabs\React\Protocol\Json\JsonProtocol;
|
||||||
|
use NoccyLabs\React\Protocol\Line\LineProtocol;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use React\EventLoop\Loop;
|
||||||
|
use React\Stream\CompositeStream;
|
||||||
|
use React\Stream\ThroughStream;
|
||||||
|
|
||||||
|
#[CoversClass(ProtocolStream::class)]
|
||||||
|
#[CoversClass(LineProtocol::class)]
|
||||||
|
#[CoversClass(JsonProtocol::class)]
|
||||||
|
class ProtocolStreamTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testSendingFrames()
|
||||||
|
{
|
||||||
|
$is = new ThroughStream();
|
||||||
|
$os = new ThroughStream();
|
||||||
|
$cs = new CompositeStream($is, $os);
|
||||||
|
|
||||||
|
$proto = new LineProtocol();
|
||||||
|
$stream = new ProtocolStream($cs, $proto);
|
||||||
|
|
||||||
|
$written = null;
|
||||||
|
$os->on("data", function ($v) use (&$written) { $written = $v; });
|
||||||
|
|
||||||
|
$stream->send(["helloworld"]);
|
||||||
|
|
||||||
|
$this->assertEquals("helloworld\n", $written);
|
||||||
|
|
||||||
|
$cs->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReceivingFrames()
|
||||||
|
{
|
||||||
|
$is = new ThroughStream();
|
||||||
|
$os = new ThroughStream();
|
||||||
|
$cs = new CompositeStream($is, $os);
|
||||||
|
$ws = new CompositeStream($os, $is);
|
||||||
|
|
||||||
|
$proto = new LineProtocol();
|
||||||
|
$stream = new ProtocolStream($cs, $proto);
|
||||||
|
|
||||||
|
$read = null;
|
||||||
|
$stream->on("message", function ($v) use (&$read) { $read = $v; });
|
||||||
|
|
||||||
|
$ws->write("helloworld\n");
|
||||||
|
|
||||||
|
$this->assertEquals([ "helloworld" ], $read);
|
||||||
|
|
||||||
|
$cs->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPartialFrames()
|
||||||
|
{
|
||||||
|
$is = new ThroughStream();
|
||||||
|
$os = new ThroughStream();
|
||||||
|
$cs = new CompositeStream($is, $os);
|
||||||
|
$ws = new CompositeStream($os, $is);
|
||||||
|
|
||||||
|
$proto = new JsonProtocol(
|
||||||
|
frameSeparator: "\n"
|
||||||
|
);
|
||||||
|
$stream = new ProtocolStream($cs, $proto);
|
||||||
|
|
||||||
|
$read = null;
|
||||||
|
$stream->on("message", function ($v) use (&$read) { $read = $v; });
|
||||||
|
|
||||||
|
$ws->write('{"hello":');
|
||||||
|
$ws->write('"world"}');
|
||||||
|
$ws->write("\n");
|
||||||
|
|
||||||
|
$this->assertEquals([ "hello" => "world" ], $read);
|
||||||
|
|
||||||
|
$cs->close();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user