Skip to content

Latest commit

 

History

History
617 lines (437 loc) · 14.2 KB

read-buffer.md

File metadata and controls

617 lines (437 loc) · 14.2 KB

ReadBuffer

The ReadBuffer class provides binary deserialization for the RabbitMQ Stream Protocol. It reads big-endian binary data from a buffer and converts it to PHP values.

Overview

ReadBuffer is a cursor-based buffer reader that supports:

  • All integer types (signed and unsigned, 8-64 bit)
  • UTF-8 strings with length prefix
  • Binary byte arrays with length prefix
  • Arrays of deserializable objects
  • Position tracking and navigation
  • Peek operations (non-consuming reads)

Constructor

public function __construct(private readonly string $buffer)

Parameters:

  • $buffer - Binary data to read from

Example:

$buffer = new ReadBuffer($frameData);

Throws: UnsupportedPlatformException on a 32-bit PHP build — getUint32(), getUint64() and getInt64() would return floats above PHP_INT_MAX there, silently breaking their int return types and the offsets they carry. See Requirements.

Integer Read Methods

All integer methods read from the current position and advance the cursor.

getUint8()

Reads an unsigned 8-bit integer.

public function getUint8(): int

Returns: Integer in range 0 to 255

Throws:

  • DeserializationException - If buffer underflow (less than 1 byte available)

Example:

$value = $buffer->getUint8();  // e.g., 255

getUint16()

Reads an unsigned 16-bit integer (big-endian).

public function getUint16(): int

Returns: Integer in range 0 to 65535

Throws:

  • DeserializationException - If buffer underflow (less than 2 bytes available)

Example:

$value = $buffer->getUint16();  // e.g., 65535

getUint32()

Reads an unsigned 32-bit integer (big-endian).

public function getUint32(): int

Returns: Integer in range 0 to 4294967295

Throws:

  • DeserializationException - If buffer underflow (less than 4 bytes available)

Example:

$value = $buffer->getUint32();  // e.g., 4294967295

getUint64()

Reads an unsigned 64-bit integer (big-endian).

public function getUint64(): int

Returns: Integer in range 0 to PHP_INT_MAX

Throws:

  • DeserializationException - If buffer underflow (less than 8 bytes available)
  • DeserializationException - If the value exceeds PHP_INT_MAX. A uint64 above the signed limit cannot be represented as a native int; the message includes the raw bytes (uint64 value 0x... at position N exceeds PHP_INT_MAX). This rejects a corrupt or hostile value instead of returning a wrapped negative offset.

Example:

$value = $buffer->getUint64();  // e.g., 9223372036854775807

getUint64Array()

Reads $count consecutive unsigned 64-bit integers (big-endian) with a single bounds check and a single unpack() call with an explicit repeat count (unpack('J' . $count, ...)). Use it instead of a loop over getUint64() on frames that carry an array of ids — a broker confirms thousands of publishing ids in one PublishConfirm frame.

public function getUint64Array(int $count): array

Parameters:

  • $count - Number of values to read. Must be 0 or greater; 0 returns an empty array and consumes nothing.

Returns: Zero-indexed array<int, int> with values in range 0 to PHP_INT_MAX. Note that unpack('J' . $count, ...) is 1-indexed; this method re-indexes from 0, like every other array reader in this class.

Throws:

  • DeserializationException - If $count is negative
  • DeserializationException - If buffer underflow (less than $count * 8 bytes available)
  • DeserializationException - If any value exceeds PHP_INT_MAX, with the same message getUint64() uses. The whole array is checked, so a corrupt or hostile value anywhere in it is rejected at the parse site instead of flowing on as a wrapped negative id or offset.

Example:

$count = $buffer->getUint32();       // e.g., PublishingIdsCount
$ids = $buffer->getUint64Array($count);  // e.g., [0, 1, 2]

getInt16()

Reads a signed 16-bit integer (big-endian, two's complement).

public function getInt16(): int

Returns: Integer in range -32768 to 32767

Implementation Note: Reads as unsigned and converts negative values using two's complement. For example, 0xFFFF becomes -1.

Throws:

  • DeserializationException - If buffer underflow (less than 2 bytes available)

Example:

$value = $buffer->getInt16();  // e.g., -1 (from 0xFFFF)

getInt32()

Reads a signed 32-bit integer (big-endian, two's complement).

public function getInt32(): int

Returns: Integer in range -2147483648 to 2147483647

Implementation Note: Reads as unsigned and converts negative values using two's complement. For example, 0xFFFFFFFF becomes -1.

Throws:

  • DeserializationException - If buffer underflow (less than 4 bytes available)

Example:

$value = $buffer->getInt32();  // e.g., -2147483648

getInt64()

Reads a signed 64-bit integer (big-endian, two's complement).

public function getInt64(): int

Returns: Integer in range PHP_INT_MIN to PHP_INT_MAX

Implementation Note: Reads as unsigned and converts negative values using two's complement. For example, 0xFFFFFFFFFFFFFFFF becomes -1.

Throws:

  • DeserializationException - If buffer underflow (less than 8 bytes available)

Example:

$value = $buffer->getInt64();  // e.g., PHP_INT_MIN

String/Bytes Read Methods

getString()

Reads a UTF-8 string with 16-bit length prefix.

public function getString(): ?string

Returns: String value, or null if length is -1 (0xFFFF)

Encoding:

  • Length is read as signed 16-bit integer
  • Length -1 (0xFFFF) represents null
  • Maximum length: 32767 bytes
  • Content is read as raw bytes (assumed UTF-8)

Throws:

  • DeserializationException - If buffer underflow

Example:

$string = $buffer->getString();  // e.g., "Hello, World!"
$nullString = $buffer->getString();  // null (if length was -1)

getBytes()

Reads binary data with 32-bit length prefix.

public function getBytes(): ?string

Returns: Binary data, or null if length is -1 (0xFFFFFFFF)

Encoding:

  • Length is read as signed 32-bit integer
  • Length -1 (0xFFFFFFFF) represents null
  • Maximum length: 2147483647 bytes

Throws:

  • DeserializationException - If buffer underflow

Example:

$data = $buffer->getBytes();  // e.g., "\x00\x01\x02\x03"
$nullData = $buffer->getBytes();  // null (if length was -1)

Array Methods

getObjectArray()

Reads an array of objects from the buffer.

/**
 * @template T of FromStreamBufferInterface
 * @param class-string<T> $class
 * @return array<int, T>
 */
public function getObjectArray(string $class): array

Parameters:

  • $class - Fully-qualified class name implementing FromStreamBufferInterface

Returns: Array of deserialized objects

Encoding:

  • Array length as uint32
  • Each item deserialized via fromStreamBuffer()

Throws:

  • DeserializationException - If buffer underflow or deserialization fails

Example:

use CrazyGoat\RabbitStream\VO\KeyValue;

// Each element of the array is deserialized with KeyValue::fromStreamBuffer().
$properties = $buffer->getObjectArray(KeyValue::class);

getStringArray()

Reads an array of strings from the buffer.

/**
 * @return array<int, string|null>
 */
public function getStringArray(): array

Returns: Array of strings (may contain null values)

Encoding:

  • Array length as uint32
  • Each string decoded with getString()

Throws:

  • DeserializationException - If buffer underflow

Example:

$streams = $buffer->getStringArray();  // e.g., ['stream1', 'stream2']

Position and Navigation Methods

getPosition()

Returns the current cursor position.

public function getPosition(): int

Returns: Current position in bytes (0 = start of buffer)

Example:

$pos = $buffer->getPosition();  // e.g., 42

skip()

Advances the cursor by the specified number of bytes.

public function skip(int $bytes): void

Parameters:

  • $bytes - Number of bytes to skip

Throws:

  • DeserializationException - If skipping would exceed buffer bounds

Example:

$buffer->skip(10);  // Skip 10 bytes

rewind()

Resets the cursor to the beginning of the buffer.

public function rewind(): void

Example:

$buffer->rewind();  // Position is now 0

readBytes()

Reads a fixed number of raw bytes.

public function readBytes(int $length): string

Parameters:

  • $length - Number of bytes to read

Returns: Raw binary data

Throws:

  • DeserializationException - If buffer underflow

Example:

$raw = $buffer->readBytes(16);  // Read 16 raw bytes

getRemainingBytes()

Reads all remaining bytes from the current position to the end.

public function getRemainingBytes(): string

Returns: All remaining buffer content

Throws:

  • DeserializationException - If position is past buffer end

Example:

$remaining = $buffer->getRemainingBytes();
// Position is now at end of buffer

Peek Methods

peekUint16()

Reads an unsigned 16-bit integer without advancing the cursor.

public function peekUint16(): int

Returns: Integer in range 0 to 65535

Throws:

  • DeserializationException - If buffer underflow (less than 2 bytes available)

Example:

$key = $buffer->peekUint16();  // Peek at command key
// Position unchanged

Error Handling

DeserializationException

Thrown for the following conditions:

  • Buffer underflow: Attempting to read more bytes than available
  • Unpack failure: PHP's unpack() failed (rare, indicates corrupted data)
  • Position past end: Cursor position exceeds buffer length
  • Object deserialization failure: fromStreamBuffer() returned null
  • uint64 out of range: A getUint64() or getUint64Array() value exceeds PHP_INT_MAX and cannot be represented as a native signed int

Error Message Format:

Buffer underflow: need {needed} bytes at position {position}, but only {available} available
Buffer underflow: position {position} is past buffer end {end}
Failed to unpack {type} at position {position}
Failed to deserialize object of class {class}
uint64 value 0x{raw bytes} at position {position} exceeds PHP_INT_MAX

Examples

Reading a Protocol Frame

use CrazyGoat\RabbitStream\Buffer\ReadBuffer;
use CrazyGoat\RabbitStream\Enum\KeyEnum;

$buffer = new ReadBuffer($frameData);

$key = $buffer->getUint16();       // Command key (e.g., 0x8015 for OpenResponse)
$version = $buffer->getUint16();   // Protocol version
$correlationId = $buffer->getUint32();  // Correlation ID
$responseCode = $buffer->getUint16();   // Response code (1 = OK)

// Map key to enum
$command = KeyEnum::fromStreamCode($key);
echo "Received: {$command->name}\n";

Sequential Reading

$buffer = new ReadBuffer($data);

// Read header
$magic = $buffer->getUint16();
$flags = $buffer->getUint8();

// Read string
$clientName = $buffer->getString();

// Read array of properties
$propertyCount = $buffer->getUint32();
for ($i = 0; $i < $propertyCount; $i++) {
    $key = $buffer->getString();
    $value = $buffer->getString();
    $properties[$key] = $value;
}

Position Tracking

$buffer = new ReadBuffer($data);

$startPos = $buffer->getPosition();  // 0

$key = $buffer->getUint16();         // Reads 2 bytes
$version = $buffer->getUint16();     // Reads 2 more bytes

currentPos = $buffer->getPosition();  // 4

// Skip correlation ID for now
$buffer->skip(4);

// Read response code
$responseCode = $buffer->getUint16();

// Go back and read correlation ID
$buffer->rewind();
$buffer->skip(4);  // Skip key + version
$correlationId = $buffer->getUint32();

Handling Null Values

$buffer = new ReadBuffer($data);

// String that might be null
$streamName = $buffer->getString();
if ($streamName === null) {
    echo "Stream name is null\n";
} else {
    echo "Stream: $streamName\n";
}

// Binary data that might be null
$metadata = $buffer->getBytes();
if ($metadata === null) {
    echo "No metadata\n";
}

Peek Operations

use CrazyGoat\RabbitStream\Enum\KeyEnum;

$buffer = new ReadBuffer($frameData);

// Peek at command key without consuming
$key = $buffer->peekUint16();

// Check if it's a server-push frame
$isServerPush = in_array($key, [
    KeyEnum::PUBLISH_CONFIRM->value,
    KeyEnum::PUBLISH_ERROR->value,
    KeyEnum::DELIVER->value,
], true);

if ($isServerPush) {
    // Handle server-push frame
} else {
    // Now actually read the key and continue
    $key = $buffer->getUint16();
    // ... process response
}

Error Handling

use CrazyGoat\RabbitStream\Exception\DeserializationException;

try {
    $buffer = new ReadBuffer($incompleteData);
    $value = $buffer->getUint32();  // May throw if < 4 bytes
} catch (DeserializationException $e) {
    echo "Parse error: " . $e->getMessage() . "\n";
    // "Buffer underflow: need 4 bytes at position 0, but only 2 available"
}

Reading Complex Structures

use CrazyGoat\RabbitStream\Buffer\ReadBuffer;
use CrazyGoat\RabbitStream\Buffer\FromStreamBufferInterface;

class KeyValue implements FromStreamBufferInterface
{
    private string $key;
    private ?string $value;

    public static function fromStreamBuffer(ReadBuffer $buffer): ?object
    {
        $obj = new self();
        $obj->key = $buffer->getString();
        $obj->value = $buffer->getString();
        return $obj;
    }

    public function getKey(): string { return $this->key; }
    public function getValue(): ?string { return $this->value; }
}

// Read array of KeyValue objects
$properties = $buffer->getObjectArray(KeyValue::class);
foreach ($properties as $prop) {
    echo "{$prop->getKey()}: {$prop->getValue()}\n";
}

Working with Remaining Data

use CrazyGoat\RabbitStream\ResponseBuilder;

$buffer = new ReadBuffer($frameData);

// Read fixed header
$key = $buffer->getUint16();
$version = $buffer->getUint16();
$correlationId = $buffer->getUint32();

// Everything else is the payload
$payload = $buffer->getRemainingBytes();

// Or let the library's dispatcher decode the whole frame for you:
$response = ResponseBuilder::fromResponseBuffer(new ReadBuffer($frameData));