diff options
Diffstat (limited to 'src/Stream')
| -rw-r--r-- | src/Stream/FileStream.php | 123 | ||||
| -rw-r--r-- | src/Stream/IoException.php | 11 | ||||
| -rw-r--r-- | src/Stream/StreamInterface.php | 77 | ||||
| -rw-r--r-- | src/Stream/UnexpectedEofException.php | 11 |
4 files changed, 222 insertions, 0 deletions
diff --git a/src/Stream/FileStream.php b/src/Stream/FileStream.php new file mode 100644 index 0000000..cb5b519 --- /dev/null +++ b/src/Stream/FileStream.php @@ -0,0 +1,123 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +use function assert; +use function chr; +use function fclose; +use function fopen; +use function fread; +use function ord; +use function strlen; + +final class FileStream implements StreamInterface +{ + /** + * @var resource + */ + private readonly mixed $fp; + + /** + * @var ?int<0, 255> + */ + private ?int $peekedByte = null; + + public function __construct( + private readonly string $path, + ) { + $fp = fopen($path, 'rb'); + if ($fp === false) { + throw new IoException("Failed to open file: $path"); + } + $this->fp = $fp; + } + + public function close(): void + { + fclose($this->fp); + } + + public function read(int $bytes): string + { + if ($this->peekedByte !== null) { + $first = chr($this->peekedByte); + $this->peekedByte = null; + if ($bytes === 1) { + return $first; + } else { + return $first . $this->doRead($bytes - 1); + } + } else { + return $this->doRead($bytes); + } + } + + public function readByte(): int + { + if ($this->peekedByte !== null) { + $ret = $this->peekedByte; + $this->peekedByte = null; + return $ret; + } + return ord($this->doRead(1)); + } + + public function peekByte(): int + { + if ($this->peekedByte === null) { + $this->peekedByte = ord($this->doRead(1)); + } + return $this->peekedByte; + } + + public function seek(int $bytes): void + { + $this->read($bytes); + } + + public function tell(): int + { + $ret = ftell($this->fp); + if ($ret === false) { + throw new IoException("Failed to get current position in file: $this->path"); + } + assert(0 <= $ret); + return $ret; + } + + public function eof(): bool + { + // feof() does not work because it returns true only after an + // unsuccessful fread(). + if ($this->peekedByte !== null) { + return false; + } + $result = fread($this->fp, 1); + if ($result === false || $result === '') { + return true; + } + $this->peekedByte = ord($result); + return false; + } + + /** + * @param positive-int $bytes + * + * @return non-empty-string + * + * @phpstan-impure + */ + private function doRead(int $bytes): string + { + $result = fread($this->fp, $bytes); + if ($result === false) { + throw new IoException("Failed to read from file: $this->path"); + } + if (strlen($result) < $bytes) { + throw new UnexpectedEofException(sprintf("Unexpected EOF while reading from file: %s (%d bytes expected, %d bytes read)", $this->path, $bytes, strlen($result))); + } + return $result; + } +} diff --git a/src/Stream/IoException.php b/src/Stream/IoException.php new file mode 100644 index 0000000..bdd0723 --- /dev/null +++ b/src/Stream/IoException.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +use RuntimeException; + +final class IoException extends RuntimeException +{ +} diff --git a/src/Stream/StreamInterface.php b/src/Stream/StreamInterface.php new file mode 100644 index 0000000..0655d22 --- /dev/null +++ b/src/Stream/StreamInterface.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +interface StreamInterface +{ + /** + * Reads $bytes bytes from the stream. + * + * @param positive-int $bytes + * + * @return non-empty-string + * A binary string of $bytes bytes. + * + * @throws UnexpectedEofException + * Thrown if the stream does not have enough bytes to read. + * + * @phpstan-impure + */ + public function read(int $bytes): string; + + /** + * Reads a single byte from the stream. + * + * @return int<0, 255> + * An 8-bit unsigned integer read from the stream. + * + * @throws UnexpectedEofException + * Thrown if the stream have reached the end. + * + * @phpstan-impure + */ + public function readByte(): int; + + /** + * Reads a single byte from the stream without advancing the position. + * + * @return int<0, 255> + * An 8-bit unsigned integer read from the stream. + * + * @throws UnexpectedEofException + * Thrown if the stream have reached the end. + * + * @phpstan-impure + */ + public function peekByte(): int; + + /** + * Seeks $bytes bytes from the current position. + * + * @param positive-int $bytes + * + * @throws UnexpectedEofException + * Thrown if the stream does not have enough bytes to seek. + * + * @phpstan-impure + */ + public function seek(int $bytes): void; + + /** + * Returns the current position in the stream. + * + * @return 0|positive-int + * + * @phpstan-impure + */ + public function tell(): int; + + /** + * Returns whether the stream has reached the end. + * + * @phpstan-impure + */ + public function eof(): bool; +} diff --git a/src/Stream/UnexpectedEofException.php b/src/Stream/UnexpectedEofException.php new file mode 100644 index 0000000..c4780ab --- /dev/null +++ b/src/Stream/UnexpectedEofException.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace Nsfisis\Waddiwasi\Stream; + +use RuntimeException; + +final class UnexpectedEofException extends RuntimeException +{ +} |
