mirror of
https://github.com/shufflingpixels/php-io.git
synced 2026-10-18 22:16:47 +02:00
Initial commit
This commit is contained in:
commit
c677cfccd4
23 changed files with 5553 additions and 0 deletions
123
src/BinaryReader.php
Normal file
123
src/BinaryReader.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class BinaryReader
|
||||
{
|
||||
public function __construct(protected StreamInterface $stream)
|
||||
{
|
||||
}
|
||||
|
||||
public static function stream(StreamInterface $stream)
|
||||
{
|
||||
return new self($stream);
|
||||
}
|
||||
|
||||
public static function string(string $data)
|
||||
{
|
||||
return self::stream(new Buffer($data));
|
||||
}
|
||||
|
||||
public function length() : int
|
||||
{
|
||||
return $this->stream->length();
|
||||
}
|
||||
|
||||
public function tell() : int
|
||||
{
|
||||
return $this->stream->tell();
|
||||
}
|
||||
|
||||
public function eof() : bool
|
||||
{
|
||||
return $this->stream->eof();
|
||||
}
|
||||
|
||||
public function seek(int $position, SeekMode $mode = SeekMode::SET)
|
||||
{
|
||||
$this->stream->seek($position, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function read(int $length): string
|
||||
{
|
||||
if ($length < 0) {
|
||||
throw new InvalidArgumentException('Length must be >= 0');
|
||||
}
|
||||
|
||||
$data = $this->stream->read($length);
|
||||
if (\strlen($data) !== $length) {
|
||||
throw new RuntimeException(
|
||||
"Not enough bytes to read {$length} byte(s), " . \strlen($data) . ' read'
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function readUInt8(): int
|
||||
{
|
||||
return \ord($this->read(1));
|
||||
}
|
||||
|
||||
public function readInt8(): int
|
||||
{
|
||||
$value = $this->readUInt8();
|
||||
|
||||
return $value >= 0x80 ? $value - 0x100 : $value;
|
||||
}
|
||||
|
||||
public function readUInt16LE(): int
|
||||
{
|
||||
return \unpack('v', $this->read(2))[1];
|
||||
}
|
||||
|
||||
public function readUInt16BE(): int
|
||||
{
|
||||
return \unpack('n', $this->read(2))[1];
|
||||
}
|
||||
|
||||
public function readInt16LE(): int
|
||||
{
|
||||
$value = $this->readUInt16LE();
|
||||
|
||||
return $value >= 0x8000 ? $value - 0x10000 : $value;
|
||||
}
|
||||
|
||||
public function readInt16BE(): int
|
||||
{
|
||||
$value = $this->readUInt16BE();
|
||||
|
||||
return $value >= 0x8000 ? $value - 0x10000 : $value;
|
||||
}
|
||||
|
||||
public function readUInt32LE(): int
|
||||
{
|
||||
return \unpack('V', $this->read(4))[1];
|
||||
}
|
||||
|
||||
public function readUInt32BE(): int
|
||||
{
|
||||
return \unpack('N', $this->read(4))[1];
|
||||
}
|
||||
|
||||
public function readInt32LE(): int
|
||||
{
|
||||
$value = $this->readUInt32LE();
|
||||
|
||||
return $value >= 0x80000000 ? $value - 0x100000000 : $value;
|
||||
}
|
||||
|
||||
public function readInt32BE(): int
|
||||
{
|
||||
$value = $this->readUInt32BE();
|
||||
|
||||
return $value >= 0x80000000 ? $value - 0x100000000 : $value;
|
||||
}
|
||||
}
|
||||
109
src/Buffer.php
Normal file
109
src/Buffer.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OutOfBoundsException;
|
||||
use Shufflingpixels\IO\Exception\EndOfStreamException;
|
||||
|
||||
class Buffer implements StreamInterface
|
||||
{
|
||||
private int $position = 0;
|
||||
|
||||
public function __construct(protected string $data)
|
||||
{
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function length(): int
|
||||
{
|
||||
return \strlen($this->data);
|
||||
}
|
||||
|
||||
public function remaining() : int
|
||||
{
|
||||
return $this->length() - $this->position;
|
||||
}
|
||||
|
||||
public function eof(): bool
|
||||
{
|
||||
return $this->remaining() === 0;
|
||||
}
|
||||
|
||||
public function tell() : int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function isSeekable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function seek(int $offset, SeekMode $mode = SeekMode::SET): void
|
||||
{
|
||||
$position = match($mode) {
|
||||
SeekMode::SET => $offset,
|
||||
SeekMode::CUR => $this->position + $offset,
|
||||
SeekMode::END => $this->length() + $offset,
|
||||
default => throw new InvalidArgumentException("Invalid seek mode")
|
||||
};
|
||||
|
||||
if ($position > $this->length()) {
|
||||
throw new OutOfBoundsException("Seek position out of bounds: {$position}");
|
||||
}
|
||||
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function read(int $length): string
|
||||
{
|
||||
if ($length < 0) {
|
||||
throw new InvalidArgumentException("Length must be >= 0");
|
||||
}
|
||||
|
||||
if ($this->remaining() < $length) {
|
||||
throw new EndOfStreamException(
|
||||
"Not enough bytes to read {$length} byte(s), {$this->remaining()} remaining"
|
||||
);
|
||||
}
|
||||
|
||||
$result = substr($this->data, $this->position, $length);
|
||||
$this->position += $length;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isWriteable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function write(string $data): int
|
||||
{
|
||||
$length = \strlen($data);
|
||||
|
||||
if ($length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$prefix = \substr($this->data, 0, $this->position);
|
||||
$suffixStart = $this->position + $length;
|
||||
$suffix = $suffixStart < $this->length()
|
||||
? \substr($this->data, $suffixStart)
|
||||
: '';
|
||||
|
||||
$this->data = $prefix . $data . $suffix;
|
||||
$this->position += $length;
|
||||
|
||||
return $length;
|
||||
}
|
||||
}
|
||||
7
src/Exception/EndOfStreamException.php
Normal file
7
src/Exception/EndOfStreamException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO\Exception;
|
||||
|
||||
class EndOfStreamException extends IOException
|
||||
{
|
||||
}
|
||||
7
src/Exception/IOException.php
Normal file
7
src/Exception/IOException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO\Exception;
|
||||
|
||||
class IOException extends \Exception
|
||||
{
|
||||
}
|
||||
28
src/File.php
Normal file
28
src/File.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
use Shufflingpixels\IO\Exception\IOException;
|
||||
|
||||
class File extends Resource
|
||||
{
|
||||
/**
|
||||
* @throws IOException
|
||||
*/
|
||||
public static function open(string $filename, FileMode $mode = FileMode::RW) : self
|
||||
{
|
||||
set_error_handler(static fn () => true);
|
||||
|
||||
try {
|
||||
$fd = fopen($filename, $mode->value);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
if ($fd === false) {
|
||||
throw new IOException("Unable to open file");
|
||||
}
|
||||
|
||||
return new self($fd, $mode->seekable(), $mode->readable(), $mode->writeable());
|
||||
}
|
||||
}
|
||||
25
src/FileMode.php
Normal file
25
src/FileMode.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
enum FileMode : string
|
||||
{
|
||||
case READ = 'r';
|
||||
case WRITE = 'w';
|
||||
case RW = 'r+';
|
||||
|
||||
public function seekable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function readable()
|
||||
{
|
||||
return $this === self::READ || $this === self::RW;
|
||||
}
|
||||
|
||||
public function writeable()
|
||||
{
|
||||
return $this === self::WRITE || $this === self::RW;
|
||||
}
|
||||
}
|
||||
99
src/Resource.php
Normal file
99
src/Resource.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
use Shufflingpixels\IO\Exception\IOException;
|
||||
|
||||
abstract class Resource implements StreamInterface
|
||||
{
|
||||
protected ?int $size = null;
|
||||
|
||||
/**
|
||||
* @param resource $resource
|
||||
*/
|
||||
protected function __construct(
|
||||
protected mixed $resource,
|
||||
protected bool $seekable,
|
||||
protected bool $readable,
|
||||
protected bool $writeable)
|
||||
{
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
fclose($this->resource);
|
||||
}
|
||||
|
||||
public function length() : int
|
||||
{
|
||||
if (!$this->isSeekable()) {
|
||||
throw new IOException("Unable to get length on a non-seekable stream");
|
||||
}
|
||||
|
||||
if ($this->size === null) {
|
||||
$pos = $this->tell();
|
||||
fseek($this->resource, 0, SEEK_END);
|
||||
|
||||
$this->size = $this->tell();
|
||||
fseek($this->resource, $pos, SEEK_SET);
|
||||
}
|
||||
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function eof(): bool
|
||||
{
|
||||
return feof($this->resource);
|
||||
}
|
||||
|
||||
public function isSeekable(): bool
|
||||
{
|
||||
return $this->seekable;
|
||||
}
|
||||
|
||||
public function tell() : int
|
||||
{
|
||||
return ftell($this->resource);
|
||||
}
|
||||
|
||||
public function seek(int $position, SeekMode $mode = SeekMode::SET): void
|
||||
{
|
||||
if (!$this->isSeekable()) {
|
||||
throw new IOException("Unable to seek on a non-seekable stream");
|
||||
}
|
||||
|
||||
if (fseek($this->resource, $position, $mode->value) < 0) {
|
||||
throw new IOException("Unable to seek to the given position");
|
||||
}
|
||||
}
|
||||
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return $this->readable;
|
||||
}
|
||||
|
||||
public function read(int $length): string
|
||||
{
|
||||
return fread($this->resource, $length);
|
||||
}
|
||||
|
||||
public function isWriteable(): bool
|
||||
{
|
||||
return $this->writeable;
|
||||
}
|
||||
|
||||
public function write(string $data): int
|
||||
{
|
||||
if (!$this->writeable) {
|
||||
throw new IOException("Stream is not writeable");
|
||||
}
|
||||
|
||||
$result = fwrite($this->resource, $data);
|
||||
if ($result === false) {
|
||||
throw new IOException("Failed to write to stream");
|
||||
}
|
||||
|
||||
$this->size = null;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
18
src/SeekMode.php
Normal file
18
src/SeekMode.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
/**
|
||||
* Basic wrapper around SEEK_* constants for type-safety.
|
||||
*/
|
||||
enum SeekMode : int
|
||||
{
|
||||
// Set to start of stream.
|
||||
case SET = SEEK_SET;
|
||||
|
||||
// Set to current position in stream.
|
||||
case CUR = SEEK_CUR;
|
||||
|
||||
// Set to end of file.
|
||||
case END = SEEK_END;
|
||||
}
|
||||
69
src/StreamInterface.php
Normal file
69
src/StreamInterface.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
interface StreamInterface
|
||||
{
|
||||
/**
|
||||
* Close the underlying resource.
|
||||
*/
|
||||
public function close() : void;
|
||||
|
||||
/**
|
||||
* returns true if stream is at End-of-file
|
||||
*/
|
||||
public function eof() : bool;
|
||||
|
||||
/**
|
||||
* Get the length of the stream,
|
||||
* if length can't be returned RuntimeException is thrown.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function length() : int;
|
||||
|
||||
/**
|
||||
* Returns true if the stream supports seeking.
|
||||
*/
|
||||
public function isSeekable() : bool;
|
||||
|
||||
/**
|
||||
* Seek to a position in the stream.
|
||||
* may throw RuntimeException if seeking is not supported.
|
||||
*
|
||||
* @throws \OutOfBoundsException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function seek(int $position, SeekMode $mode = SeekMode::SET): void;
|
||||
|
||||
/**
|
||||
* Get the current position in the stream.
|
||||
*/
|
||||
public function tell() : int;
|
||||
|
||||
/**
|
||||
* Returns true if stream can be read from.
|
||||
*/
|
||||
public function isReadable() : bool;
|
||||
|
||||
/**
|
||||
* Read from stream, may throw RuntimeException if stream is not readable.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function read(int $length): string;
|
||||
|
||||
/**
|
||||
* Returns true if stream can be written to.
|
||||
*/
|
||||
public function isWriteable() : bool;
|
||||
|
||||
/**
|
||||
* Write to stream.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function write(string $data) : int;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue