mirror of
https://github.com/shufflingpixels/php-io.git
synced 2026-06-16 05:04:59 +02:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 89525294de |
12 changed files with 250 additions and 152 deletions
|
|
@ -15,7 +15,8 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.0"
|
"php": ">=8.0",
|
||||||
|
"psr/http-message": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"pestphp/pest": "^3.0"
|
"pestphp/pest": "^3.0"
|
||||||
|
|
|
||||||
58
composer.lock
generated
58
composer.lock
generated
|
|
@ -4,8 +4,62 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "01336b98a52a118feaa53d4ed39377bd",
|
"content-hash": "c3d163cb00c11d9951f136537f938f25",
|
||||||
"packages": [],
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "psr/http-message",
|
||||||
|
"version": "2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/php-fig/http-message.git",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Psr\\Http\\Message\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "PHP-FIG",
|
||||||
|
"homepage": "https://www.php-fig.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Common interface for HTTP messages",
|
||||||
|
"homepage": "https://github.com/php-fig/http-message",
|
||||||
|
"keywords": [
|
||||||
|
"http",
|
||||||
|
"http-message",
|
||||||
|
"psr",
|
||||||
|
"psr-7",
|
||||||
|
"request",
|
||||||
|
"response"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/php-fig/http-message/tree/2.0"
|
||||||
|
},
|
||||||
|
"time": "2023-04-04T09:54:51+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
{
|
{
|
||||||
"name": "brianium/paratest",
|
"name": "brianium/paratest",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Shufflingpixels\IO;
|
namespace Shufflingpixels\IO;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class BinaryReader
|
class BinaryReader
|
||||||
|
|
@ -23,7 +24,12 @@ class BinaryReader
|
||||||
|
|
||||||
public function length() : int
|
public function length() : int
|
||||||
{
|
{
|
||||||
return $this->stream->length();
|
$size = $this->stream->getSize();
|
||||||
|
if ($size === null) {
|
||||||
|
throw new RuntimeException('Stream size is not known');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $size;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tell() : int
|
public function tell() : int
|
||||||
|
|
@ -36,9 +42,9 @@ class BinaryReader
|
||||||
return $this->stream->eof();
|
return $this->stream->eof();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seek(int $position, SeekMode $mode = SeekMode::SET)
|
public function seek(int $position, int $whence = SEEK_SET): void
|
||||||
{
|
{
|
||||||
$this->stream->seek($position, $mode);
|
$this->stream->seek($position, $whence);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
117
src/Buffer.php
117
src/Buffer.php
|
|
@ -4,11 +4,13 @@ namespace Shufflingpixels\IO;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use OutOfBoundsException;
|
use OutOfBoundsException;
|
||||||
use Shufflingpixels\IO\Exception\EndOfStreamException;
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
class Buffer implements StreamInterface
|
class Buffer implements StreamInterface
|
||||||
{
|
{
|
||||||
private int $position = 0;
|
private int $position = 0;
|
||||||
|
private bool $detached = false;
|
||||||
|
|
||||||
public function __construct(protected string $data)
|
public function __construct(protected string $data)
|
||||||
{
|
{
|
||||||
|
|
@ -16,6 +18,25 @@ class Buffer implements StreamInterface
|
||||||
|
|
||||||
public function close(): void
|
public function close(): void
|
||||||
{
|
{
|
||||||
|
$this->detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detach(): mixed
|
||||||
|
{
|
||||||
|
if ($this->detached) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->detached = true;
|
||||||
|
$this->data = '';
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSize(): ?int
|
||||||
|
{
|
||||||
|
return $this->detached ? null : $this->length();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function length(): int
|
public function length(): int
|
||||||
|
|
@ -25,6 +46,10 @@ class Buffer implements StreamInterface
|
||||||
|
|
||||||
public function remaining() : int
|
public function remaining() : int
|
||||||
{
|
{
|
||||||
|
if ($this->detached) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->length() - $this->position;
|
return $this->length() - $this->position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +60,8 @@ class Buffer implements StreamInterface
|
||||||
|
|
||||||
public function tell() : int
|
public function tell() : int
|
||||||
{
|
{
|
||||||
|
$this->ensureAttached();
|
||||||
|
|
||||||
return $this->position;
|
return $this->position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,22 +70,29 @@ class Buffer implements StreamInterface
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seek(int $offset, SeekMode $mode = SeekMode::SET): void
|
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||||
{
|
{
|
||||||
$position = match($mode) {
|
$this->ensureAttached();
|
||||||
SeekMode::SET => $offset,
|
|
||||||
SeekMode::CUR => $this->position + $offset,
|
$position = match($whence) {
|
||||||
SeekMode::END => $this->length() + $offset,
|
SEEK_SET => $offset,
|
||||||
|
SEEK_CUR => $this->position + $offset,
|
||||||
|
SEEK_END => $this->length() + $offset,
|
||||||
default => throw new InvalidArgumentException("Invalid seek mode")
|
default => throw new InvalidArgumentException("Invalid seek mode")
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($position > $this->length()) {
|
if ($position < 0 || $position > $this->length()) {
|
||||||
throw new OutOfBoundsException("Seek position out of bounds: {$position}");
|
throw new OutOfBoundsException("Seek position out of bounds: {$position}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->position = $position;
|
$this->position = $position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function rewind(): void
|
||||||
|
{
|
||||||
|
$this->seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
public function isReadable(): bool
|
public function isReadable(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -66,30 +100,37 @@ class Buffer implements StreamInterface
|
||||||
|
|
||||||
public function read(int $length): string
|
public function read(int $length): string
|
||||||
{
|
{
|
||||||
|
$this->ensureAttached();
|
||||||
|
|
||||||
if ($length < 0) {
|
if ($length < 0) {
|
||||||
throw new InvalidArgumentException("Length must be >= 0");
|
throw new InvalidArgumentException("Length must be >= 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->remaining() < $length) {
|
if ($length === 0 || $this->eof()) {
|
||||||
throw new EndOfStreamException(
|
return '';
|
||||||
"Not enough bytes to read {$length} byte(s), {$this->remaining()} remaining"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = substr($this->data, $this->position, $length);
|
$result = substr($this->data, $this->position, min($length, $this->remaining()));
|
||||||
$this->position += $length;
|
$this->position += strlen($result);
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isWriteable(): bool
|
public function isWritable(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function write(string $data): int
|
public function isWriteable(): bool
|
||||||
{
|
{
|
||||||
$length = \strlen($data);
|
return $this->isWritable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(string $string): int
|
||||||
|
{
|
||||||
|
$this->ensureAttached();
|
||||||
|
|
||||||
|
$length = \strlen($string);
|
||||||
|
|
||||||
if ($length === 0) {
|
if ($length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -101,9 +142,51 @@ class Buffer implements StreamInterface
|
||||||
? \substr($this->data, $suffixStart)
|
? \substr($this->data, $suffixStart)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
$this->data = $prefix . $data . $suffix;
|
$this->data = $prefix . $string . $suffix;
|
||||||
$this->position += $length;
|
$this->position += $length;
|
||||||
|
|
||||||
return $length;
|
return $length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getContents(): string
|
||||||
|
{
|
||||||
|
$this->ensureAttached();
|
||||||
|
|
||||||
|
if ($this->eof()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = substr($this->data, $this->position);
|
||||||
|
$this->position = $this->length();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadata(?string $key = null): mixed
|
||||||
|
{
|
||||||
|
$metadata = [
|
||||||
|
'seekable' => true,
|
||||||
|
'readable' => true,
|
||||||
|
'writable' => true,
|
||||||
|
'uri' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $key !== null ? $metadata[$key] ?? null : $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
if ($this->detached) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureAttached(): void
|
||||||
|
{
|
||||||
|
if ($this->detached) {
|
||||||
|
throw new RuntimeException('Stream is detached');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Shufflingpixels\IO;
|
namespace Shufflingpixels\IO;
|
||||||
|
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
use Shufflingpixels\IO\Exception\IOException;
|
use Shufflingpixels\IO\Exception\IOException;
|
||||||
|
|
||||||
abstract class Resource implements StreamInterface
|
abstract class Resource implements StreamInterface
|
||||||
|
|
@ -21,13 +22,24 @@ abstract class Resource implements StreamInterface
|
||||||
|
|
||||||
public function close(): void
|
public function close(): void
|
||||||
{
|
{
|
||||||
fclose($this->resource);
|
if ($this->resource !== null) {
|
||||||
|
fclose($this->resource);
|
||||||
|
$this->resource = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function length() : int
|
public function detach(): mixed
|
||||||
|
{
|
||||||
|
$resource = $this->resource;
|
||||||
|
$this->resource = null;
|
||||||
|
|
||||||
|
return $resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSize(): ?int
|
||||||
{
|
{
|
||||||
if (!$this->isSeekable()) {
|
if (!$this->isSeekable()) {
|
||||||
throw new IOException("Unable to get length on a non-seekable stream");
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->size === null) {
|
if ($this->size === null) {
|
||||||
|
|
@ -56,13 +68,13 @@ abstract class Resource implements StreamInterface
|
||||||
return ftell($this->resource);
|
return ftell($this->resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seek(int $position, SeekMode $mode = SeekMode::SET): void
|
public function seek(int $position, int $whence = SEEK_SET): void
|
||||||
{
|
{
|
||||||
if (!$this->isSeekable()) {
|
if (!$this->isSeekable()) {
|
||||||
throw new IOException("Unable to seek on a non-seekable stream");
|
throw new IOException("Unable to seek on a non-seekable stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fseek($this->resource, $position, $mode->value) < 0) {
|
if (fseek($this->resource, $position, $whence) < 0) {
|
||||||
throw new IOException("Unable to seek to the given position");
|
throw new IOException("Unable to seek to the given position");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,18 +89,23 @@ abstract class Resource implements StreamInterface
|
||||||
return fread($this->resource, $length);
|
return fread($this->resource, $length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isWriteable(): bool
|
public function isWritable(): bool
|
||||||
{
|
{
|
||||||
return $this->writeable;
|
return $this->writeable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function write(string $data): int
|
public function isWriteable(): bool
|
||||||
|
{
|
||||||
|
return $this->isWritable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(string $string): int
|
||||||
{
|
{
|
||||||
if (!$this->writeable) {
|
if (!$this->writeable) {
|
||||||
throw new IOException("Stream is not writeable");
|
throw new IOException("Stream is not writeable");
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = fwrite($this->resource, $data);
|
$result = fwrite($this->resource, $string);
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
throw new IOException("Failed to write to stream");
|
throw new IOException("Failed to write to stream");
|
||||||
}
|
}
|
||||||
|
|
@ -96,4 +113,26 @@ abstract class Resource implements StreamInterface
|
||||||
$this->size = null;
|
$this->size = null;
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function rewind(): void
|
||||||
|
{
|
||||||
|
$this->seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContents(): string
|
||||||
|
{
|
||||||
|
return (string) stream_get_contents($this->resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadata(?string $key = null): mixed
|
||||||
|
{
|
||||||
|
$data = stream_get_meta_data($this->resource);
|
||||||
|
|
||||||
|
return $key !== null ? $data[$key] ?? null : $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->getContents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<?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;
|
|
||||||
}
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
use Shufflingpixels\IO\BinaryReader;
|
use Shufflingpixels\IO\BinaryReader;
|
||||||
use Shufflingpixels\IO\Buffer;
|
use Shufflingpixels\IO\Buffer;
|
||||||
use Shufflingpixels\IO\SeekMode;
|
|
||||||
use Shufflingpixels\IO\StreamInterface;
|
|
||||||
|
|
||||||
it('creates reader from stream and string helpers', function () {
|
it('creates reader from stream and string helpers', function () {
|
||||||
$streamReader = BinaryReader::stream(new Buffer('abc'));
|
$streamReader = BinaryReader::stream(new Buffer('abc'));
|
||||||
|
|
@ -24,7 +22,7 @@ it('proxies length tell eof and seek', function () {
|
||||||
expect($reader->tell())->toBe(2)
|
expect($reader->tell())->toBe(2)
|
||||||
->and($reader->read(1))->toBe('c');
|
->and($reader->read(1))->toBe('c');
|
||||||
|
|
||||||
$reader->seek(-1, SeekMode::END);
|
$reader->seek(-1, SEEK_END);
|
||||||
expect($reader->read(1))->toBe('d')
|
expect($reader->read(1))->toBe('d')
|
||||||
->and($reader->eof())->toBeTrue();
|
->and($reader->eof())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
@ -36,17 +34,22 @@ it('throws for negative read length', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when stream returns fewer bytes than requested', function () {
|
it('throws when stream returns fewer bytes than requested', function () {
|
||||||
$stream = new class implements StreamInterface {
|
$stream = new class implements \Psr\Http\Message\StreamInterface {
|
||||||
|
public function __toString(): string { return ''; }
|
||||||
public function close(): void {}
|
public function close(): void {}
|
||||||
|
public function detach(): mixed { return null; }
|
||||||
|
public function getSize(): ?int { return 0; }
|
||||||
public function eof(): bool { return false; }
|
public function eof(): bool { return false; }
|
||||||
public function length(): int { return 0; }
|
|
||||||
public function isSeekable(): bool { return false; }
|
public function isSeekable(): bool { return false; }
|
||||||
public function seek(int $position, Shufflingpixels\IO\SeekMode $mode = Shufflingpixels\IO\SeekMode::SET): void {}
|
public function seek(int $position, int $whence = SEEK_SET): void {}
|
||||||
|
public function rewind(): void {}
|
||||||
public function tell(): int { return 0; }
|
public function tell(): int { return 0; }
|
||||||
|
public function isWritable(): bool { return false; }
|
||||||
|
public function write(string $string): int { return 0; }
|
||||||
public function isReadable(): bool { return true; }
|
public function isReadable(): bool { return true; }
|
||||||
public function read(int $length): string { return 'x'; }
|
public function read(int $length): string { return 'x'; }
|
||||||
public function isWriteable(): bool { return false; }
|
public function getContents(): string { return ''; }
|
||||||
public function write($data): int { return 0; }
|
public function getMetadata(?string $key = null): mixed { return null; }
|
||||||
};
|
};
|
||||||
|
|
||||||
$reader = BinaryReader::stream($stream);
|
$reader = BinaryReader::stream($stream);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Shufflingpixels\IO\Buffer;
|
use Shufflingpixels\IO\Buffer;
|
||||||
use Shufflingpixels\IO\Exception\EndOfStreamException;
|
|
||||||
use Shufflingpixels\IO\SeekMode;
|
|
||||||
|
|
||||||
it('reads, seeks and tracks position', function () {
|
it('reads, seeks and tracks position', function () {
|
||||||
$buffer = new Buffer('abcdef');
|
$buffer = new Buffer('abcdef');
|
||||||
|
|
||||||
expect($buffer->length())->toBe(6)
|
expect($buffer->getSize())->toBe(6)
|
||||||
->and($buffer->tell())->toBe(0)
|
->and($buffer->tell())->toBe(0)
|
||||||
->and($buffer->remaining())->toBe(6)
|
->and($buffer->remaining())->toBe(6)
|
||||||
->and($buffer->eof())->toBeFalse();
|
->and($buffer->eof())->toBeFalse();
|
||||||
|
|
@ -16,7 +14,7 @@ it('reads, seeks and tracks position', function () {
|
||||||
->and($buffer->tell())->toBe(2)
|
->and($buffer->tell())->toBe(2)
|
||||||
->and($buffer->remaining())->toBe(4);
|
->and($buffer->remaining())->toBe(4);
|
||||||
|
|
||||||
$buffer->seek(-1, SeekMode::END);
|
$buffer->seek(-1, SEEK_END);
|
||||||
|
|
||||||
expect($buffer->tell())->toBe(5)
|
expect($buffer->tell())->toBe(5)
|
||||||
->and($buffer->read(1))->toBe('f')
|
->and($buffer->read(1))->toBe('f')
|
||||||
|
|
@ -27,7 +25,7 @@ it('supports relative seek modes', function () {
|
||||||
$buffer = new Buffer('abcdef');
|
$buffer = new Buffer('abcdef');
|
||||||
|
|
||||||
$buffer->seek(3);
|
$buffer->seek(3);
|
||||||
$buffer->seek(-2, SeekMode::CUR);
|
$buffer->seek(-2, SEEK_CUR);
|
||||||
|
|
||||||
expect($buffer->tell())->toBe(1)
|
expect($buffer->tell())->toBe(1)
|
||||||
->and($buffer->read(2))->toBe('bc');
|
->and($buffer->read(2))->toBe('bc');
|
||||||
|
|
@ -45,10 +43,11 @@ it('throws for negative read length', function () {
|
||||||
expect(fn () => $buffer->read(-1))->toThrow(InvalidArgumentException::class);
|
expect(fn () => $buffer->read(-1))->toThrow(InvalidArgumentException::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when reading beyond end of stream', function () {
|
it('returns available bytes when reading beyond end of stream', function () {
|
||||||
$buffer = new Buffer('abc');
|
$buffer = new Buffer('abc');
|
||||||
|
|
||||||
expect(fn () => $buffer->read(4))->toThrow(EndOfStreamException::class);
|
expect($buffer->read(4))->toBe('abc')
|
||||||
|
->and($buffer->eof())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes at current position and updates contents', function () {
|
it('writes at current position and updates contents', function () {
|
||||||
|
|
@ -68,5 +67,14 @@ it('writes nothing for empty payload', function () {
|
||||||
|
|
||||||
expect($buffer->write(''))->toBe(0)
|
expect($buffer->write(''))->toBe(0)
|
||||||
->and($buffer->tell())->toBe(0)
|
->and($buffer->tell())->toBe(0)
|
||||||
->and($buffer->length())->toBe(3);
|
->and($buffer->getSize())->toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns remaining contents and advances cursor', function () {
|
||||||
|
$buffer = new Buffer('abcdef');
|
||||||
|
$buffer->seek(2);
|
||||||
|
|
||||||
|
expect($buffer->getContents())->toBe('cdef')
|
||||||
|
->and($buffer->tell())->toBe(6)
|
||||||
|
->and($buffer->getContents())->toBe('');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ it('opens readable files and reads contents', function () {
|
||||||
$file = File::open($path, FileMode::READ);
|
$file = File::open($path, FileMode::READ);
|
||||||
|
|
||||||
expect($file->isReadable())->toBeTrue()
|
expect($file->isReadable())->toBeTrue()
|
||||||
->and($file->isWriteable())->toBeFalse()
|
->and($file->isWritable())->toBeFalse()
|
||||||
->and($file->length())->toBe(5)
|
->and($file->getSize())->toBe(5)
|
||||||
->and($file->read(5))->toBe('hello');
|
->and($file->read(5))->toBe('hello');
|
||||||
|
|
||||||
$file->close();
|
$file->close();
|
||||||
|
|
@ -27,7 +27,7 @@ it('opens read-write files and persists writes', function () {
|
||||||
$file->seek(0);
|
$file->seek(0);
|
||||||
|
|
||||||
expect($file->isReadable())->toBeTrue()
|
expect($file->isReadable())->toBeTrue()
|
||||||
->and($file->isWriteable())->toBeTrue()
|
->and($file->isWritable())->toBeTrue()
|
||||||
->and($file->write('X'))->toBe(1);
|
->and($file->write('X'))->toBe(1);
|
||||||
|
|
||||||
$file->seek(0);
|
$file->seek(0);
|
||||||
|
|
@ -44,8 +44,8 @@ it('opens write mode files and truncates existing contents', function () {
|
||||||
$file = File::open($path, FileMode::WRITE);
|
$file = File::open($path, FileMode::WRITE);
|
||||||
|
|
||||||
expect($file->isReadable())->toBeFalse()
|
expect($file->isReadable())->toBeFalse()
|
||||||
->and($file->isWriteable())->toBeTrue()
|
->and($file->isWritable())->toBeTrue()
|
||||||
->and($file->length())->toBe(0)
|
->and($file->getSize())->toBe(0)
|
||||||
->and($file->write('xy'))->toBe(2);
|
->and($file->write('xy'))->toBe(2);
|
||||||
|
|
||||||
$file->close();
|
$file->close();
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ it('reads, writes, seeks and reports stream state', function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
expect($stream->length())->toBe(5)
|
expect($stream->getSize())->toBe(5)
|
||||||
->and($stream->tell())->toBe(0)
|
->and($stream->tell())->toBe(0)
|
||||||
->and($stream->isSeekable())->toBeTrue()
|
->and($stream->isSeekable())->toBeTrue()
|
||||||
->and($stream->isReadable())->toBeTrue()
|
->and($stream->isReadable())->toBeTrue()
|
||||||
->and($stream->isWriteable())->toBeTrue()
|
->and($stream->isWritable())->toBeTrue()
|
||||||
->and($stream->read(2))->toBe('he');
|
->and($stream->read(2))->toBe('he');
|
||||||
|
|
||||||
$stream->seek(0);
|
$stream->seek(0);
|
||||||
|
|
@ -27,7 +27,7 @@ it('reads, writes, seeks and reports stream state', function () {
|
||||||
|
|
||||||
$stream->seek(0);
|
$stream->seek(0);
|
||||||
expect($stream->read(5))->toBe('Hello')
|
expect($stream->read(5))->toBe('Hello')
|
||||||
->and($stream->length())->toBe(5)
|
->and($stream->getSize())->toBe(5)
|
||||||
->and($stream->eof())->toBeFalse();
|
->and($stream->eof())->toBeFalse();
|
||||||
|
|
||||||
$stream->read(1);
|
$stream->read(1);
|
||||||
|
|
@ -46,7 +46,7 @@ it('throws when seeking or getting length on non-seekable stream', function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(fn () => $stream->length())->toThrow(IOException::class)
|
expect($stream->getSize())->toBeNull()
|
||||||
->and(fn () => $stream->seek(0))->toThrow(IOException::class);
|
->and(fn () => $stream->seek(0))->toThrow(IOException::class);
|
||||||
|
|
||||||
$stream->close();
|
$stream->close();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Shufflingpixels\IO\SeekMode;
|
|
||||||
|
|
||||||
it('maps to native seek constants', function () {
|
|
||||||
expect(SeekMode::SET->value)->toBe(SEEK_SET)
|
|
||||||
->and(SeekMode::CUR->value)->toBe(SEEK_CUR)
|
|
||||||
->and(SeekMode::END->value)->toBe(SEEK_END);
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue