From 2e3505bea41552ba28978399cddead52d64d7e85 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 11 Jul 2024 00:27:55 +0900 Subject: feat: support streaming decoding --- src/Stream/FileStream.php | 123 ++++++++++++++++++++++++++++++++++ src/Stream/IoException.php | 11 +++ src/Stream/StreamInterface.php | 77 +++++++++++++++++++++ src/Stream/UnexpectedEofException.php | 11 +++ 4 files changed, 222 insertions(+) create mode 100644 src/Stream/FileStream.php create mode 100644 src/Stream/IoException.php create mode 100644 src/Stream/StreamInterface.php create mode 100644 src/Stream/UnexpectedEofException.php (limited to 'src/Stream') 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 @@ + + */ + 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 @@ + + * 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 @@ +