-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Basic backend y.js message decoder
Signed-off-by: Julius Härtl <[email protected]>
- Loading branch information
1 parent
d5fa392
commit 9b778a8
Showing
7 changed files
with
165 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
<?php | ||
|
||
namespace OCA\Text; | ||
|
||
use InvalidArgumentException; | ||
|
||
/** | ||
* Steps are base64 encoded messages of the yjs protocols | ||
* https://github.com/yjs/y-protocols | ||
* | ||
* This class is a simple representation of a message containing some methods | ||
* to decode parts of it for what we need on the backend | ||
* | ||
* Relevant resources: | ||
* https://github.com/yjs/y-protocols/blob/master/PROTOCOL.md | ||
* https://github.com/yjs/y-websocket/blob/master/src/y-websocket.js#L19-L22 | ||
* https://github.com/yjs/y-protocols/blob/master/sync.js#L38-L40 | ||
* https://github.com/dmonad/lib0/blob/master/decoding.js | ||
*/ | ||
class YjsMessage { | ||
|
||
public const YJS_MESSAGE_SYNC = 0; | ||
public const YJS_MESSAGE_AWARENESS = 1; | ||
public const YJS_MESSAGE_AWARENESS_QUERY = 3; | ||
|
||
public const YJS_MESSAGE_SYNC_STEP1 = 0; | ||
public const YJS_MESSAGE_SYNC_STEP2 = 1; | ||
public const YJS_MESSAGE_SYNC_UPDATE = 2; | ||
|
||
private int $pos = 0; | ||
|
||
public function __construct( | ||
private string $data = '' | ||
) { | ||
} | ||
|
||
public static function fromBase64(string $data = ''): self { | ||
return new self(base64_decode($data)); | ||
} | ||
|
||
/** | ||
* https://github.com/dmonad/lib0/blob/bd69ab4dc701d77e808f2bab08d96d63acd297da/decoding.js#L242 | ||
*/ | ||
public function readVarUint(): int { | ||
$bytes = array_values(unpack('C*', $this->data)); | ||
$num = 0; | ||
$mult = 1; | ||
$len = count($bytes); | ||
while ($this->pos < $len) { | ||
$r = $bytes[$this->pos++]; | ||
// num = num | ((r & binary.BITS7) << len) | ||
$num = $num + ($r & 0b1111111) * $mult; | ||
$mult *= 128; | ||
if ($r <= 0b1111111) { | ||
return $num; | ||
} | ||
// Number.MAX_SAFE_INTEGER in JS | ||
if ($num > 9007199254740990) { | ||
throw new \OutOfBoundsException(); | ||
} | ||
} | ||
throw new InvalidArgumentException(); | ||
} | ||
|
||
public function getYjsMessageType(): int { | ||
$oldPos = $this->pos; | ||
$this->pos = 0; | ||
$messageType = $this->readVarUint(); | ||
$this->pos = $oldPos; | ||
return $messageType; | ||
} | ||
|
||
public function getYjsSyncType(): int { | ||
$oldPos = $this->pos; | ||
$this->pos = 0; | ||
$messageType = $this->readVarUint(); | ||
if ($messageType !== self::YJS_MESSAGE_SYNC) { | ||
throw new \ValueError('Message is not a sync message'); | ||
} | ||
$syncType = $this->readVarUint(); | ||
$this->pos = $oldPos; | ||
return $syncType; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?php | ||
|
||
namespace OCA\Text; | ||
|
||
use Test\TestCase; | ||
|
||
class YjsMessageTest extends TestCase { | ||
protected function setUp(): void { | ||
parent::setUp(); | ||
} | ||
|
||
// https://github.com/yjs/y-dat/blob/745d25f9690fceae5901d1225575fe8b6bcafdd7/src/y-dat.js#LL207C59-L210C1 | ||
public function dataMessageTypes() { | ||
return [ | ||
// | ||
['AAABAA==', 0, 0], | ||
// messageSync messageYjsSyncStep1 | ||
['AAAyCIqghaQNBvrS2LoMB8L10I8KrQPD+t3RB2DcrYL4A40Ema2O4AMHz9bk8AIOtbm0PAE=', 0, 0], | ||
// messageAwareness | ||
['AQoBkZWK7gMAAnt9', 1, null], | ||
['AUIBwvXQjwqWAzl7InVzZXIiOnsibmFtZSI6ImFkbWluIiwiY29sb3IiOiIjZDA5ZTZkIn0sImN1cnNvciI6bnVsbH0=', 1, null], | ||
['AegBAcP63dEHtwHeAXsidXNlciI6eyJuYW1lIjoiR3Vlc3QiLCJjb2xvciI6IiM5M2IyN2IifSwiY3Vyc29yIjp7ImFuY2hvciI6eyJ0eXBlIjp7ImNsaWVudCI6MjcxNzEzNzYwMiwiY2xvY2siOjl9LCJ0bmFtZSI6bnVsbCwiaXRlbSI6bnVsbCwiYXNzb2MiOjB9LCJoZWFkIjp7InR5cGUiOnsiY2xpZW50IjoyNzE3MTM3NjAyLCJjbG9jayI6OX0sInRuYW1lIjpudWxsLCJpdGVtIjpudWxsLCJhc3NvYyI6MH19fQ==', 1, null], | ||
['AbsBAZGViu4DArIBeyJ1c2VyIjp7Im5hbWUiOiJHdWVzdCIsImNvbG9yIjoiI2I4YmU2OCJ9LCJjdXJzb3IiOnsiYW5jaG9yIjp7InR5cGUiOm51bGwsInRuYW1lIjoiZGVmYXVsdCIsIml0ZW0iOm51bGwsImFzc29jIjowfSwiaGVhZCI6eyJ0eXBlIjpudWxsLCJ0bmFtZSI6ImRlZmF1bHQiLCJpdGVtIjpudWxsLCJhc3NvYyI6MH19fQ==', 1, null], | ||
// messageSync messageYjsUpdate | ||
['AAISAQHD+t3RB2CEwvXQjwpHAWEA', 0, 2], | ||
['AAI0AQOKoIWkDQAHAQdkZWZhdWx0AwlwYXJhZ3JhcGgHAIqghaQNAAYEAIqghaQNAQR0ZXN0AA==', 0, 2], | ||
['AAIdAQGRlYruAx2okZWK7gMbAXcCaC0BkZWK7gMBGwE=', 0, 2], | ||
['AAIKAAGRlYruAwEVBA==', 0, 2], | ||
// query, response | ||
['AAABAA==', YjsMessage::YJS_MESSAGE_SYNC, YjsMessage::YJS_MESSAGE_SYNC_STEP1], | ||
['AAECAAA=', YjsMessage::YJS_MESSAGE_SYNC, YjsMessage::YJS_MESSAGE_SYNC_STEP2], | ||
]; | ||
} | ||
|
||
/** @dataProvider dataMessageTypes */ | ||
public function testMessageTypes($data, $type, $subtype) { | ||
$buffer = YjsMessage::fromBase64($data); | ||
$unpack1 = $buffer->getYjsMessageType(); | ||
self::assertEquals($type, $unpack1, 'type'); | ||
if ($subtype !== null) { | ||
$unpack2 = $buffer->getYjsSyncType(); | ||
self::assertEquals($subtype, $unpack2); | ||
} | ||
} | ||
|
||
public function testV() { | ||
self::assertEquals(0, YjsMessage::fromBase64('AA==')->readVarUint()); | ||
self::assertEquals(127, YjsMessage::fromBase64('fw==')->readVarUint()); | ||
self::assertEquals(128, YjsMessage::fromBase64('gAE=')->readVarUint()); | ||
self::assertEquals(129, YjsMessage::fromBase64('gQE=')->readVarUint()); | ||
self::assertEquals(259, YjsMessage::fromBase64('gwI=')->readVarUint()); | ||
self::assertEquals(0, YjsMessage::fromBase64('AA==')->readVarUint()); | ||
self::assertEquals(13372342, YjsMessage::fromBase64('tpewBg==')->readVarUint()); | ||
self::assertEquals(1357913579, YjsMessage::fromBase64('67vAhwU=')->readVarUint()); | ||
|
||
$buffer = YjsMessage::fromBase64('tpewBg=='); | ||
self::assertEquals(13372342, $buffer->readVarUint()); | ||
} | ||
} |