aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/Stream
diff options
context:
space:
mode:
Diffstat (limited to 'src/Stream')
-rw-r--r--src/Stream/FileStream.php123
-rw-r--r--src/Stream/IoException.php11
-rw-r--r--src/Stream/StreamInterface.php77
-rw-r--r--src/Stream/UnexpectedEofException.php11
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
+{
+}