diff --git a/composer.json b/composer.json index 65c7e22..aedb659 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,7 @@ } ], "require": { - "php": ">=8.0", - "psr/http-message": "^2.0" + "php": ">=8.0" }, "require-dev": { "pestphp/pest": "^3.0" diff --git a/composer.lock b/composer.lock index ef0f2fa..625fd85 100644 --- a/composer.lock +++ b/composer.lock @@ -4,62 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c3d163cb00c11d9951f136537f938f25", - "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" - } - ], + "content-hash": "01336b98a52a118feaa53d4ed39377bd", + "packages": [], "packages-dev": [ { "name": "brianium/paratest", diff --git a/src/BinaryReader.php b/src/BinaryReader.php index e72d6bc..65de1d3 100644 --- a/src/BinaryReader.php +++ b/src/BinaryReader.php @@ -3,7 +3,6 @@ namespace Shufflingpixels\IO; use InvalidArgumentException; -use Psr\Http\Message\StreamInterface; use RuntimeException; class BinaryReader @@ -24,12 +23,7 @@ class BinaryReader public function length() : int { - $size = $this->stream->getSize(); - if ($size === null) { - throw new RuntimeException('Stream size is not known'); - } - - return $size; + return $this->stream->length(); } public function tell() : int @@ -42,9 +36,9 @@ class BinaryReader return $this->stream->eof(); } - public function seek(int $position, int $whence = SEEK_SET): void + public function seek(int $position, SeekMode $mode = SeekMode::SET) { - $this->stream->seek($position, $whence); + $this->stream->seek($position, $mode); } /** diff --git a/src/Buffer.php b/src/Buffer.php index b3f89ab..c333776 100644 --- a/src/Buffer.php +++ b/src/Buffer.php @@ -4,13 +4,11 @@ namespace Shufflingpixels\IO; use InvalidArgumentException; use OutOfBoundsException; -use Psr\Http\Message\StreamInterface; -use RuntimeException; +use Shufflingpixels\IO\Exception\EndOfStreamException; class Buffer implements StreamInterface { private int $position = 0; - private bool $detached = false; public function __construct(protected string $data) { @@ -18,25 +16,6 @@ class Buffer implements StreamInterface 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 @@ -46,10 +25,6 @@ class Buffer implements StreamInterface public function remaining() : int { - if ($this->detached) { - return 0; - } - return $this->length() - $this->position; } @@ -60,8 +35,6 @@ class Buffer implements StreamInterface public function tell() : int { - $this->ensureAttached(); - return $this->position; } @@ -70,29 +43,22 @@ class Buffer implements StreamInterface return true; } - public function seek(int $offset, int $whence = SEEK_SET): void + public function seek(int $offset, SeekMode $mode = SeekMode::SET): void { - $this->ensureAttached(); - - $position = match($whence) { - SEEK_SET => $offset, - SEEK_CUR => $this->position + $offset, - SEEK_END => $this->length() + $offset, + $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 < 0 || $position > $this->length()) { + if ($position > $this->length()) { throw new OutOfBoundsException("Seek position out of bounds: {$position}"); } $this->position = $position; } - public function rewind(): void - { - $this->seek(0); - } - public function isReadable(): bool { return true; @@ -100,37 +66,30 @@ class Buffer implements StreamInterface public function read(int $length): string { - $this->ensureAttached(); - if ($length < 0) { throw new InvalidArgumentException("Length must be >= 0"); } - if ($length === 0 || $this->eof()) { - return ''; + if ($this->remaining() < $length) { + throw new EndOfStreamException( + "Not enough bytes to read {$length} byte(s), {$this->remaining()} remaining" + ); } - $result = substr($this->data, $this->position, min($length, $this->remaining())); - $this->position += strlen($result); + $result = substr($this->data, $this->position, $length); + $this->position += $length; return $result; } - public function isWritable(): bool + public function isWriteable(): bool { return true; } - public function isWriteable(): bool + public function write(string $data): int { - return $this->isWritable(); - } - - public function write(string $string): int - { - $this->ensureAttached(); - - $length = \strlen($string); + $length = \strlen($data); if ($length === 0) { return 0; @@ -142,51 +101,9 @@ class Buffer implements StreamInterface ? \substr($this->data, $suffixStart) : ''; - $this->data = $prefix . $string . $suffix; + $this->data = $prefix . $data . $suffix; $this->position += $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'); - } - } } diff --git a/src/Resource.php b/src/Resource.php index 5379f40..dcfe840 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -2,7 +2,6 @@ namespace Shufflingpixels\IO; -use Psr\Http\Message\StreamInterface; use Shufflingpixels\IO\Exception\IOException; abstract class Resource implements StreamInterface @@ -22,24 +21,13 @@ abstract class Resource implements StreamInterface public function close(): void { - if ($this->resource !== null) { - fclose($this->resource); - $this->resource = null; - } + fclose($this->resource); } - public function detach(): mixed - { - $resource = $this->resource; - $this->resource = null; - - return $resource; - } - - public function getSize(): ?int + public function length() : int { if (!$this->isSeekable()) { - return null; + throw new IOException("Unable to get length on a non-seekable stream"); } if ($this->size === null) { @@ -68,13 +56,13 @@ abstract class Resource implements StreamInterface return ftell($this->resource); } - public function seek(int $position, int $whence = SEEK_SET): void + 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, $whence) < 0) { + if (fseek($this->resource, $position, $mode->value) < 0) { throw new IOException("Unable to seek to the given position"); } } @@ -89,23 +77,18 @@ abstract class Resource implements StreamInterface return fread($this->resource, $length); } - public function isWritable(): bool + public function isWriteable(): bool { return $this->writeable; } - public function isWriteable(): bool - { - return $this->isWritable(); - } - - public function write(string $string): int + public function write(string $data): int { if (!$this->writeable) { throw new IOException("Stream is not writeable"); } - $result = fwrite($this->resource, $string); + $result = fwrite($this->resource, $data); if ($result === false) { throw new IOException("Failed to write to stream"); } @@ -113,26 +96,4 @@ abstract class Resource implements StreamInterface $this->size = null; 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(); - } } diff --git a/src/SeekMode.php b/src/SeekMode.php new file mode 100644 index 0000000..74bf878 --- /dev/null +++ b/src/SeekMode.php @@ -0,0 +1,18 @@ +tell())->toBe(2) ->and($reader->read(1))->toBe('c'); - $reader->seek(-1, SEEK_END); + $reader->seek(-1, SeekMode::END); expect($reader->read(1))->toBe('d') ->and($reader->eof())->toBeTrue(); }); @@ -34,22 +36,17 @@ it('throws for negative read length', function () { }); it('throws when stream returns fewer bytes than requested', function () { - $stream = new class implements \Psr\Http\Message\StreamInterface { - public function __toString(): string { return ''; } + $stream = new class implements StreamInterface { public function close(): void {} - public function detach(): mixed { return null; } - public function getSize(): ?int { return 0; } public function eof(): bool { return false; } + public function length(): int { return 0; } public function isSeekable(): bool { return false; } - public function seek(int $position, int $whence = SEEK_SET): void {} - public function rewind(): void {} + public function seek(int $position, Shufflingpixels\IO\SeekMode $mode = Shufflingpixels\IO\SeekMode::SET): void {} 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 read(int $length): string { return 'x'; } - public function getContents(): string { return ''; } - public function getMetadata(?string $key = null): mixed { return null; } + public function isWriteable(): bool { return false; } + public function write($data): int { return 0; } }; $reader = BinaryReader::stream($stream); diff --git a/tests/Unit/BufferTest.php b/tests/Unit/BufferTest.php index 5796a31..e43ffca 100644 --- a/tests/Unit/BufferTest.php +++ b/tests/Unit/BufferTest.php @@ -1,11 +1,13 @@ getSize())->toBe(6) + expect($buffer->length())->toBe(6) ->and($buffer->tell())->toBe(0) ->and($buffer->remaining())->toBe(6) ->and($buffer->eof())->toBeFalse(); @@ -14,7 +16,7 @@ it('reads, seeks and tracks position', function () { ->and($buffer->tell())->toBe(2) ->and($buffer->remaining())->toBe(4); - $buffer->seek(-1, SEEK_END); + $buffer->seek(-1, SeekMode::END); expect($buffer->tell())->toBe(5) ->and($buffer->read(1))->toBe('f') @@ -25,7 +27,7 @@ it('supports relative seek modes', function () { $buffer = new Buffer('abcdef'); $buffer->seek(3); - $buffer->seek(-2, SEEK_CUR); + $buffer->seek(-2, SeekMode::CUR); expect($buffer->tell())->toBe(1) ->and($buffer->read(2))->toBe('bc'); @@ -43,11 +45,10 @@ it('throws for negative read length', function () { expect(fn () => $buffer->read(-1))->toThrow(InvalidArgumentException::class); }); -it('returns available bytes when reading beyond end of stream', function () { +it('throws when reading beyond end of stream', function () { $buffer = new Buffer('abc'); - expect($buffer->read(4))->toBe('abc') - ->and($buffer->eof())->toBeTrue(); + expect(fn () => $buffer->read(4))->toThrow(EndOfStreamException::class); }); it('writes at current position and updates contents', function () { @@ -67,14 +68,5 @@ it('writes nothing for empty payload', function () { expect($buffer->write(''))->toBe(0) ->and($buffer->tell())->toBe(0) - ->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(''); + ->and($buffer->length())->toBe(3); }); diff --git a/tests/Unit/FileTest.php b/tests/Unit/FileTest.php index abc38be..df9adf7 100644 --- a/tests/Unit/FileTest.php +++ b/tests/Unit/FileTest.php @@ -11,8 +11,8 @@ it('opens readable files and reads contents', function () { $file = File::open($path, FileMode::READ); expect($file->isReadable())->toBeTrue() - ->and($file->isWritable())->toBeFalse() - ->and($file->getSize())->toBe(5) + ->and($file->isWriteable())->toBeFalse() + ->and($file->length())->toBe(5) ->and($file->read(5))->toBe('hello'); $file->close(); @@ -27,7 +27,7 @@ it('opens read-write files and persists writes', function () { $file->seek(0); expect($file->isReadable())->toBeTrue() - ->and($file->isWritable())->toBeTrue() + ->and($file->isWriteable())->toBeTrue() ->and($file->write('X'))->toBe(1); $file->seek(0); @@ -44,8 +44,8 @@ it('opens write mode files and truncates existing contents', function () { $file = File::open($path, FileMode::WRITE); expect($file->isReadable())->toBeFalse() - ->and($file->isWritable())->toBeTrue() - ->and($file->getSize())->toBe(0) + ->and($file->isWriteable())->toBeTrue() + ->and($file->length())->toBe(0) ->and($file->write('xy'))->toBe(2); $file->close(); diff --git a/tests/Unit/ResourceTest.php b/tests/Unit/ResourceTest.php index 32d56bb..4800430 100644 --- a/tests/Unit/ResourceTest.php +++ b/tests/Unit/ResourceTest.php @@ -15,11 +15,11 @@ it('reads, writes, seeks and reports stream state', function () { } }; - expect($stream->getSize())->toBe(5) + expect($stream->length())->toBe(5) ->and($stream->tell())->toBe(0) ->and($stream->isSeekable())->toBeTrue() ->and($stream->isReadable())->toBeTrue() - ->and($stream->isWritable())->toBeTrue() + ->and($stream->isWriteable())->toBeTrue() ->and($stream->read(2))->toBe('he'); $stream->seek(0); @@ -27,7 +27,7 @@ it('reads, writes, seeks and reports stream state', function () { $stream->seek(0); expect($stream->read(5))->toBe('Hello') - ->and($stream->getSize())->toBe(5) + ->and($stream->length())->toBe(5) ->and($stream->eof())->toBeFalse(); $stream->read(1); @@ -46,7 +46,7 @@ it('throws when seeking or getting length on non-seekable stream', function () { } }; - expect($stream->getSize())->toBeNull() + expect(fn () => $stream->length())->toThrow(IOException::class) ->and(fn () => $stream->seek(0))->toThrow(IOException::class); $stream->close(); diff --git a/tests/Unit/SeekModeTest.php b/tests/Unit/SeekModeTest.php new file mode 100644 index 0000000..f8fa81e --- /dev/null +++ b/tests/Unit/SeekModeTest.php @@ -0,0 +1,9 @@ +value)->toBe(SEEK_SET) + ->and(SeekMode::CUR->value)->toBe(SEEK_CUR) + ->and(SeekMode::END->value)->toBe(SEEK_END); +});