Initial commit

This commit is contained in:
Christopher Vagnetoft
2026-04-02 22:29:35 +02:00
commit 62a1167055
31 changed files with 3759 additions and 0 deletions

148
src/Json/JsonProtocol.php Normal file
View 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")
};
}
}

View 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;
}
}

View 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,
);
}
}