kodekeep/bytebuffer

View on GitHub
src/ByteBuffer.php

Summary

Maintainability
A
3 hrs
Test Coverage
A
99%
<?php

declare(strict_types=1);

/**
 * Copyright (c) Konceiver Oy <legal@konceiver.dev>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Konceiver\ByteBuffer;

use InvalidArgumentException;

/**
 * This is the byte buffer class.
 *
 * @author Brian Faust <brian@konceiver.dev>
 */
class ByteBuffer
{
    use Concerns\Initialisable,
        Concerns\Offsetable,
        Concerns\Positionable,
        Concerns\Readable,
        Concerns\Sizeable,
        Concerns\Transformable,
        Concerns\Writeable;

    /**
     * Backing ArrayBuffer.
     *
     * @var array
     */
    private $buffer = [];

    /**
     * Absolute read/write offset.
     *
     * @var int
     */
    private $offset = 0;

    /**
     * Absolute length of the contained data.
     *
     * @var int
     */
    private $length;

    /**
     * Whether to use big endian, little endian or machine byte order.
     *
     * @var int
     */
    private $order = 1;

    /**
     * Constructs a new ByteBuffer.
     *
     * @param array|string|int $value
     */
    private function __construct($value)
    {
        switch (gettype($value)) {
            case 'array':
                $this->initializeBuffer(count($value), $value);
                break;

            case 'integer':
                $this->initializeBuffer($value, pack("x{$value}"));
                break;

            case 'string':
                $this->initializeBuffer(strlen($value), $value);
                break;

            default:
                throw new InvalidArgumentException('Constructor argument must be a binary string or integer.');
        }
    }

    /**
     * Dynamically retrieve a value from the buffer.
     *
     * @param int $offset
     *
     * @return mixed
     */
    public function __get($offset)
    {
        return $this->offsetGet($offset);
    }

    /**
     * Dynamically set a value in the buffer.
     *
     * @param int $offset
     * @param mixed $value
     */
    public function __set($offset, $value)
    {
        $this->offsetSet($offset, $value);
    }

    /**
     * Dynamically check if a value in the buffer is set.
     *
     * @param int $offset
     *
     * @return bool
     */
    public function __isset($offset)
    {
        return $this->offsetExists($offset);
    }

    /**
     * Dynamically unset a value in the buffer.
     *
     * @param int $offset
     */
    public function __unset($offset)
    {
        $this->offsetUnset($offset);
    }

    /**
     * Allocates a new ByteBuffer backed by a buffer with the specified data.
     *
     * @param array|string|int $value
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public static function new($value): self
    {
        return new static($value);
    }

    /**
     * Allocates a new ByteBuffer backed by a buffer of the specified capacity.
     *
     * @param int $capacity
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public static function allocate(int $capacity): self
    {
        if ($capacity < 0) {
            throw new InvalidArgumentException('Negative integers not supported by ByteBuffer.');
        }

        return new static($capacity);
    }

    /**
     * Initialise a new buffer from the given content.
     *
     * @param int              $length
     * @param string|int|array $content
     */
    public function initializeBuffer(int $length, $content): void
    {
        for ($i = 0; $i < $length; $i++) {
            $this->buffer[$i] = $content[$i];
        }

        $this->length = $length;
    }

    /**
     * Pack data into a binary string.
     *
     * @param string     $format
     * @param string|int $value
     * @param int        $offset
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function pack(string $format, $value, int $offset): self
    {
        $this->skip($offset);

        $bytes = pack($format, $value);

        for ($i = 0; $i < strlen($bytes); $i++) {
            $this->buffer[$this->offset++] = $bytes[$i];
        }

        return $this;
    }

    /**
     * Unpack data from a binary string.
     *
     * @param string $format
     * @param int    $offset
     *
     * @return string|int
     */
    public function unpack(string $format, int $offset = 0)
    {
        $this->skip($offset);

        $value = unpack($format, $this->toBinary(), $this->offset)[1];

        $this->skip(LengthMap::get($format));

        return $value;
    }

    /**
     * Get a value from the buffer.
     *
     * @param int $offset
     *
     * @return mixed
     */
    public function get(int $offset)
    {
        return $this->offsetGet($offset);
    }

    /**
     * Concatenates multiple ByteBuffers into one.
     *
     * @param array $buffers
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public static function concat(...$buffers): self
    {
        $initial = $buffers[0];

        foreach (array_slice($buffers, 1) as $buffer) {
            $initial->append($buffer);
        }

        return $initial;
    }

    /**
     * Appends some data to this ByteBuffer.
     *
     * @param mixed $value
     * @param int   $offset
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function append($value, int $offset = 0): self
    {
        if ($value instanceof self) {
            $value = $value->toArray($offset);
        }

        if (is_string($value)) {
            $value = str_split($value);
        }

        $buffer = array_merge($this->buffer, $value);

        $bufferCount = count($buffer);
        $this->initializeBuffer($bufferCount, $buffer);
        $this->position($bufferCount); // move current offset to the end of merged buffer after append

        return $this;
    }

    /**
     * Appends this ByteBuffers contents to another ByteBuffer.
     *
     * @param \Konceiver\ByteBuffer\ByteBuffer $buffer
     * @param int                               $offset
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function appendTo(self $buffer, int $offset = 0): self
    {
        return $buffer->append($this);
    }

    /**
     * Prepends some data to this ByteBuffer.
     *
     * @param mixed $value
     * @param int   $offset
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function prepend($value, int $offset = 0): self
    {
        if ($value instanceof self) {
            $value = $value->toArray($offset);
        }

        if (is_string($value)) {
            $value = str_split($value);
        }

        $buffer = $this->buffer;

        foreach (array_reverse($value) as $item) {
            array_unshift($buffer, $item);
        }

        $bufferCount = count($buffer);
        $this->initializeBuffer($bufferCount, $buffer);
        $this->position($bufferCount); // move current offset to the end of merged buffer after prepend

        return $this;
    }

    /**
     * Prepends this ByteBuffers contents to another ByteBuffer.
     *
     * @param \Konceiver\ByteBuffer\ByteBuffer $buffer
     * @param int                               $offset
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function prependTo(self $buffer, int $offset = 0): self
    {
        return $buffer->prepend($this, $offset);
    }

    /**
     * Overwrites this ByteBuffers contents with the specified value.
     *
     * @param int $length
     * @param int $start
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function fill(int $length, int $start = 0): self
    {
        if ($start > 0) {
            $this->position($start);
        }

        for ($i = 0; $i < $length; $i++) {
            $this->buffer[$this->offset++] = pack('x');
        }

        return $this;
    }

    /**
     * Flip byte order of this buffers contents.
     *
     * @param int $start
     * @param int $length
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function flip(int $start = 0, int $length = 0): self
    {
        $reversed = array_reverse($this->slice($start, $length));

        $this->initializeBuffer(count($reversed), $reversed);

        return $this;
    }

    /**
     * Reverses this ByteBuffers contents. This is an alias of flip.
     *
     * @param int $start
     * @param int $length
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function reverse(int $start = 0, int $length = 0): self
    {
        return $this->flip($start, $length);
    }

    /**
     * Sets the byte order.
     *
     * @param int $value
     *
     * @return \Konceiver\ByteBuffer\ByteBuffer
     */
    public function order(int $value): self
    {
        $this->order = $value;

        return $this;
    }

    /**
     * Extract a slice of the ByteBuffer.
     *
     * @param int $offset
     * @param int $length
     *
     * @return array
     */
    public function slice(int $offset, int $length): array
    {
        if ($offset > $this->capacity()) {
            throw new InvalidArgumentException('Start exceeds buffer length');
        }

        if ($length <= 0) {
            return $this->buffer;
        }

        if ($length > $this->capacity()) {
            throw new InvalidArgumentException('Length exceeds buffer length');
        }

        return array_slice($this->buffer, $offset, $length);
    }

    /**
     * Determine if the given value is a ByteBuffer.
     *
     * @param \Konceiver\ByteBuffer\ByteBuffer $buffer
     *
     * @return bool
     */
    public function equals(self $buffer): bool
    {
        return $buffer->capacity() === $this->capacity()
             && $buffer->toBinary() === $this->toBinary();
    }

    /**
     * Determine if the given value is a ByteBuffer.
     *
     * @param mixed $value
     *
     * @return bool
     */
    public static function isByteBuffer($value): bool
    {
        return $value instanceof self;
    }

    /**
     * Determine if the byte order is set to big endian.
     *
     * @return bool
     */
    public function isBigEndian(): bool
    {
        return ByteOrder::BE === $this->order;
    }

    /**
     * Determine if the byte order is set to little endian.
     *
     * @return bool
     */
    public function isLittleEndian(): bool
    {
        return ByteOrder::LE === $this->order;
    }

    /**
     * Determine if the byte order is set to machine byte.
     *
     * @return bool
     */
    public function isMachineByte(): bool
    {
        return ByteOrder::MB === $this->order;
    }
}