Initial commit
This commit is contained in:
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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user