Build Reactive Signals for Bluesky's AT Protocol Firehose in Laravel
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add binary reader and varint decoding utilities

+283
+140
src/Binary/Reader.php
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\Signal\Binary; 6 + 7 + use RuntimeException; 8 + 9 + /** 10 + * Binary data reader with position tracking. 11 + * 12 + * Provides stream-like interface for reading from binary strings. 13 + */ 14 + class Reader 15 + { 16 + private int $position = 0; 17 + 18 + public function __construct( 19 + private readonly string $data, 20 + ) {} 21 + 22 + /** 23 + * Get current position in the data. 24 + */ 25 + public function getPosition(): int 26 + { 27 + return $this->position; 28 + } 29 + 30 + /** 31 + * Get total length of data. 32 + */ 33 + public function getLength(): int 34 + { 35 + return strlen($this->data); 36 + } 37 + 38 + /** 39 + * Check if there's more data to read. 40 + */ 41 + public function hasMore(): bool 42 + { 43 + return $this->position < strlen($this->data); 44 + } 45 + 46 + /** 47 + * Get remaining bytes count. 48 + */ 49 + public function remaining(): int 50 + { 51 + return strlen($this->data) - $this->position; 52 + } 53 + 54 + /** 55 + * Peek at the next byte without advancing position. 56 + * 57 + * @throws RuntimeException If no more data available 58 + */ 59 + public function peek(): int 60 + { 61 + if (!$this->hasMore()) { 62 + throw new RuntimeException('Unexpected end of data'); 63 + } 64 + 65 + return ord($this->data[$this->position]); 66 + } 67 + 68 + /** 69 + * Read a single byte and advance position. 70 + * 71 + * @throws RuntimeException If no more data available 72 + */ 73 + public function readByte(): int 74 + { 75 + $byte = $this->peek(); 76 + $this->position++; 77 + 78 + return $byte; 79 + } 80 + 81 + /** 82 + * Read exactly N bytes and advance position. 83 + * 84 + * @throws RuntimeException If not enough data available 85 + */ 86 + public function readBytes(int $length): string 87 + { 88 + if ($this->remaining() < $length) { 89 + throw new RuntimeException("Cannot read {$length} bytes, only {$this->remaining()} remaining"); 90 + } 91 + 92 + $bytes = substr($this->data, $this->position, $length); 93 + $this->position += $length; 94 + 95 + return $bytes; 96 + } 97 + 98 + /** 99 + * Read a varint (variable-length integer). 100 + * 101 + * @throws RuntimeException If varint is malformed 102 + */ 103 + public function readVarint(): int 104 + { 105 + return Varint::decode($this->data, $this->position); 106 + } 107 + 108 + /** 109 + * Get all remaining data without advancing position. 110 + */ 111 + public function peekRemaining(): string 112 + { 113 + return substr($this->data, $this->position); 114 + } 115 + 116 + /** 117 + * Read all remaining data and advance position to end. 118 + */ 119 + public function readRemaining(): string 120 + { 121 + $remaining = $this->peekRemaining(); 122 + $this->position = strlen($this->data); 123 + 124 + return $remaining; 125 + } 126 + 127 + /** 128 + * Skip N bytes forward. 129 + * 130 + * @throws RuntimeException If trying to skip past end 131 + */ 132 + public function skip(int $bytes): void 133 + { 134 + if ($this->remaining() < $bytes) { 135 + throw new RuntimeException("Cannot skip {$bytes} bytes, only {$this->remaining()} remaining"); 136 + } 137 + 138 + $this->position += $bytes; 139 + } 140 + }
+68
src/Binary/Varint.php
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\Signal\Binary; 6 + 7 + use RuntimeException; 8 + 9 + /** 10 + * Varint (Variable-Length Integer) decoder for unsigned integers. 11 + * 12 + * Used in CAR format to encode block lengths. Implements the same 13 + * varint encoding used in Protocol Buffers and other binary formats. 14 + */ 15 + class Varint 16 + { 17 + /** 18 + * Decode a varint from the beginning of the data. 19 + * 20 + * @param string $data Binary data containing varint 21 + * @param int $offset Starting position (will be updated to position after varint) 22 + * @return int Decoded unsigned integer 23 + * @throws RuntimeException If varint is malformed or data ends unexpectedly 24 + */ 25 + public static function decode(string $data, int &$offset = 0): int 26 + { 27 + $result = 0; 28 + $shift = 0; 29 + $length = strlen($data); 30 + 31 + while ($offset < $length) { 32 + $byte = ord($data[$offset]); 33 + $offset++; 34 + 35 + // Take lower 7 bits and shift into result 36 + $result |= ($byte & 0x7F) << $shift; 37 + 38 + // If MSB is 0, we're done 39 + if (($byte & 0x80) === 0) { 40 + return $result; 41 + } 42 + 43 + $shift += 7; 44 + 45 + // Prevent overflow (64-bit max) 46 + if ($shift > 63) { 47 + throw new RuntimeException('Varint too long (max 64 bits)'); 48 + } 49 + } 50 + 51 + throw new RuntimeException('Unexpected end of varint data'); 52 + } 53 + 54 + /** 55 + * Read a varint and return both the value and remaining data. 56 + * 57 + * @param string $data Binary data starting with varint 58 + * @return array{0: int, 1: string} [decoded value, remaining data] 59 + */ 60 + public static function decodeFirst(string $data): array 61 + { 62 + $offset = 0; 63 + $value = self::decode($data, $offset); 64 + $remainder = substr($data, $offset); 65 + 66 + return [$value, $remainder]; 67 + } 68 + }
+75
tests/Unit/VarintTest.php
··· 1 + <?php 2 + 3 + declare(strict_types=1); 4 + 5 + namespace SocialDept\Signal\Tests\Unit; 6 + 7 + use PHPUnit\Framework\TestCase; 8 + use RuntimeException; 9 + use SocialDept\Signal\Binary\Varint; 10 + 11 + class VarintTest extends TestCase 12 + { 13 + public function test_decode_single_byte_values(): void 14 + { 15 + $this->assertSame(0, Varint::decode("\x00")); 16 + $this->assertSame(1, Varint::decode("\x01")); 17 + $this->assertSame(127, Varint::decode("\x7F")); 18 + } 19 + 20 + public function test_decode_multi_byte_values(): void 21 + { 22 + // 128 = 0x80 0x01 23 + $this->assertSame(128, Varint::decode("\x80\x01")); 24 + 25 + // 300 = 0xAC 0x02 26 + $this->assertSame(300, Varint::decode("\xAC\x02")); 27 + 28 + // 16384 = 0x80 0x80 0x01 29 + $this->assertSame(16384, Varint::decode("\x80\x80\x01")); 30 + } 31 + 32 + public function test_decode_with_offset(): void 33 + { 34 + $data = "\x00\x01\x7F\x80\x01"; 35 + $offset = 0; 36 + 37 + $this->assertSame(0, Varint::decode($data, $offset)); 38 + $this->assertSame(1, $offset); 39 + 40 + $this->assertSame(1, Varint::decode($data, $offset)); 41 + $this->assertSame(2, $offset); 42 + 43 + $this->assertSame(127, Varint::decode($data, $offset)); 44 + $this->assertSame(3, $offset); 45 + 46 + $this->assertSame(128, Varint::decode($data, $offset)); 47 + $this->assertSame(5, $offset); 48 + } 49 + 50 + public function test_decode_first_returns_value_and_remainder(): void 51 + { 52 + [$value, $remainder] = Varint::decodeFirst("\x7F\x01\x02\x03"); 53 + 54 + $this->assertSame(127, $value); 55 + $this->assertSame("\x01\x02\x03", $remainder); 56 + } 57 + 58 + public function test_decode_throws_on_unexpected_end(): void 59 + { 60 + $this->expectException(RuntimeException::class); 61 + $this->expectExceptionMessage('Unexpected end of varint data'); 62 + 63 + Varint::decode("\x80"); 64 + } 65 + 66 + public function test_decode_throws_on_too_long_varint(): void 67 + { 68 + $this->expectException(RuntimeException::class); 69 + $this->expectExceptionMessage('Varint too long (max 64 bits)'); 70 + 71 + // Create a varint that would be longer than 64 bits (10 bytes with continuation bits) 72 + $tooLong = str_repeat("\xFF", 10); 73 + Varint::decode($tooLong); 74 + } 75 + }