Skip to content
129 changes: 129 additions & 0 deletions formwork/src/Images/Binary/ByteReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Formwork\Images\Binary;

use UnexpectedValueException;

/**
* @since 2.4.0
*/
final class ByteReader
{
/**
* Current read position
*/
private int $position = 0;

public function __construct(
private readonly string $data,
int $position = 0,
) {
$this->seek($position);
}

/**
* Get the current read position
*/
public function position(): int
{
return $this->position;
}

/**
* Move the read position to an absolute offset
*
* @throws UnexpectedValueException If the offset is outside the data
*/
public function seek(int $position): void
{
if ($position < 0 || $position > strlen($this->data)) {
throw new UnexpectedValueException('Unexpected end of data');
}

$this->position = $position;
}

/**
* Get the number of unread bytes
*/
public function remaining(): int
{
return strlen($this->data) - $this->position;
}

/**
* Read a given number of bytes
*
* @throws UnexpectedValueException If there are not enough bytes left
*/
public function bytes(int $length): string
{
if ($length < 0 || $length > $this->remaining()) {
throw new UnexpectedValueException('Unexpected end of data');
}

$bytes = substr($this->data, $this->position, $length);
$this->position += $length;

return $bytes;
}

/**
* Get a given number of bytes without moving the read position
*
* @throws UnexpectedValueException If there are not enough bytes left
*/
public function peek(int $length): string
{
$position = $this->position;
$bytes = $this->bytes($length);
$this->position = $position;

return $bytes;
}

/**
* Read all the remaining bytes
*/
public function rest(): string
{
return $this->bytes($this->remaining());
}

/**
* Skip a given number of bytes
*/
public function skip(int $length): void
{
$this->bytes($length);
}

/**
* Read an unsigned integer of the given size (a size of 0 always reads 0)
*
* @throws UnexpectedValueException If there are not enough bytes left
*/
public function uint(int $size, Endianness $endianness = Endianness::Big): int
{
return Bytes::unpack($this->bytes($size), 0, $size, $endianness);
}

/**
* Read a null-terminated string, consuming the terminator
*
* @throws UnexpectedValueException If the string is not terminated
*/
public function cString(): string
{
$end = strpos($this->data, "\x00", $this->position);

if ($end === false) {
throw new UnexpectedValueException('Unexpected end of data');
}

$string = $this->bytes($end - $this->position);
$this->position++;

return $string;
}
}
83 changes: 83 additions & 0 deletions formwork/src/Images/Binary/Bytes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Formwork\Images\Binary;

use UnexpectedValueException;

/**
* @since 2.4.0
*/
final class Bytes
{
/**
* Pack an unsigned integer as a string of the given size
*
* @throws UnexpectedValueException If the value does not fit in the given size
*/
public static function pack(int $value, int $size, Endianness $endianness = Endianness::Big): string
{
if ($value < 0 || $size < 0 || $size > 8 || ($size < 8 && $value >> (8 * $size) !== 0)) {
throw new UnexpectedValueException(sprintf('Value %d does not fit in %d bytes', $value, $size));
}

$bytes = substr(pack('J', $value), 8 - $size);

return $endianness === Endianness::Little ? strrev($bytes) : $bytes;
}

/**
* Unpack an unsigned integer of the given size at the given offset
*
* @throws UnexpectedValueException If the data is too short or the integer does not fit in a PHP integer
*/
public static function unpack(string $data, int $offset, int $size, Endianness $endianness = Endianness::Big): int
{
if ($offset < 0 || $size < 0 || $offset + $size > strlen($data)) {
throw new UnexpectedValueException('Unexpected end of data');
}

$bytes = substr($data, $offset, $size);

if ($endianness === Endianness::Little) {
$bytes = strrev($bytes);
}

$value = 0;

for ($i = 0; $i < $size; $i++) {
if ($value >> 55 !== 0) {
throw new UnexpectedValueException('Integer is too large');
}
$value = ($value << 8) | ord($bytes[$i]);
}

return $value;
}

/**
* Apply a list of replacements to the given data
*
* Every replacement is a triple of offset, length of the replaced range and replacement.
* Offsets always refer to the original data, so that replacements do not shift each other.
*
* @param list<array{int, int, string}> $replacements
*
* @throws UnexpectedValueException If a replacement is outside the data or overlaps another one
*/
public static function replace(string $data, array $replacements): string
{
usort($replacements, static fn(array $a, array $b): int => $b[0] <=> $a[0]);

$limit = strlen($data);

foreach ($replacements as [$offset, $length, $replacement]) {
if ($offset < 0 || $length < 0 || $offset + $length > $limit) {
throw new UnexpectedValueException('Invalid replacement range');
}
$data = substr_replace($data, $replacement, $offset, $length);
$limit = $offset;
}

return $data;
}
}
12 changes: 12 additions & 0 deletions formwork/src/Images/Binary/Endianness.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Formwork\Images\Binary;

/**
* @since 2.4.0
*/
enum Endianness
{
case Big;
case Little;
}
Loading
Loading