aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/BinaryFormat/Decoder.php145
-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
5 files changed, 276 insertions, 91 deletions
diff --git a/src/BinaryFormat/Decoder.php b/src/BinaryFormat/Decoder.php
index 3c23fc8..14079e1 100644
--- a/src/BinaryFormat/Decoder.php
+++ b/src/BinaryFormat/Decoder.php
@@ -7,6 +7,7 @@ namespace Nsfisis\Waddiwasi\BinaryFormat;
use Nsfisis\Waddiwasi\BinaryFormat\Internal\Code;
use Nsfisis\Waddiwasi\BinaryFormat\Internal\Locals;
use Nsfisis\Waddiwasi\BinaryFormat\Internal\SectionId;
+use Nsfisis\Waddiwasi\Stream\StreamInterface;
use Nsfisis\Waddiwasi\Structure\Instructions\Instr;
use Nsfisis\Waddiwasi\Structure\Instructions\Instrs;
use Nsfisis\Waddiwasi\Structure\Instructions\Instrs\Control\BlockType;
@@ -44,19 +45,12 @@ use function in_array;
use function is_float;
use function is_int;
use function ord;
-use function strlen;
final class Decoder
{
- private string $input;
- private int $inputSize;
- private int $pos;
-
- public function __construct(string $wasmBinary)
- {
- $this->input = $wasmBinary;
- $this->inputSize = strlen($wasmBinary);
- $this->pos = 0;
+ public function __construct(
+ private readonly StreamInterface $stream,
+ ) {
}
public function decode(): Module
@@ -77,7 +71,7 @@ final class Decoder
$codes = $this->decodeSection(SectionId::Code, $this->decodeCodeSecRest(...)) ?? [];
$datas = $this->decodeSection(SectionId::Data, $this->decodeDataSecRest(...)) ?? [];
- if (!$this->eof()) {
+ if (!$this->stream->eof()) {
throw new InvalidBinaryFormatException("eof");
}
if ($dataCount === null) {
@@ -125,30 +119,26 @@ final class Decoder
private function checkMagic(): void
{
- assert($this->pos === 0);
- $this->ensureNBytesRemains(4);
- $b1 = ord($this->input[0]);
- $b2 = ord($this->input[1]);
- $b3 = ord($this->input[2]);
- $b4 = ord($this->input[3]);
+ $bs = $this->stream->read(4);
+ $b1 = ord($bs[0]);
+ $b2 = ord($bs[1]);
+ $b3 = ord($bs[2]);
+ $b4 = ord($bs[3]);
if ([$b1, $b2, $b3, $b4] !== [0x00, 0x61, 0x73, 0x6D]) {
throw new InvalidBinaryFormatException("magic");
}
- $this->pos += 4;
}
private function checkVersion(): void
{
- assert($this->pos === 4);
- $this->ensureNBytesRemains(4);
- $b1 = ord($this->input[4]);
- $b2 = ord($this->input[5]);
- $b3 = ord($this->input[6]);
- $b4 = ord($this->input[7]);
+ $bs = $this->stream->read(4);
+ $b1 = ord($bs[0]);
+ $b2 = ord($bs[1]);
+ $b3 = ord($bs[2]);
+ $b4 = ord($bs[3]);
if ([$b1, $b2, $b3, $b4] !== [0x01, 0x00, 0x00, 0x00]) {
throw new InvalidBinaryFormatException(sprintf("version: [%x, %x, %x, %x]", $b1, $b2, $b3, $b4));
}
- $this->pos += 4;
}
/**
@@ -159,11 +149,11 @@ final class Decoder
private function decodeSection(SectionId $sectionId, callable $decoder): mixed
{
$this->skipCustomSections();
- if ($this->eof()) {
+ if ($this->stream->eof()) {
return null;
}
- $idValue = $this->peekByte();
+ $idValue = $this->stream->peekByte();
$id = SectionId::tryFrom($idValue);
if ($id === null) {
throw new InvalidBinaryFormatException("section id");
@@ -171,12 +161,12 @@ final class Decoder
if ($id !== $sectionId) {
return null;
}
- $this->skipNBytes(1);
+ $this->stream->seek(1);
$size = $this->decodeU32();
- $prevPos = $this->pos;
+ $prevPos = $this->stream->tell();
$result = $decoder();
- if ($this->pos - $prevPos !== $size) {
+ if ($this->stream->tell() - $prevPos !== $size) {
throw new InvalidBinaryFormatException("type section size");
}
return $result;
@@ -184,17 +174,23 @@ final class Decoder
private function skipCustomSections(): void
{
- while (!$this->eof()) {
- $b = $this->peekByte();
+ while (!$this->stream->eof()) {
+ $b = $this->stream->peekByte();
if ($b !== SectionId::Custom->value) {
break;
}
- $this->skipNBytes(1);
+ $this->stream->seek(1);
$size = $this->decodeU32();
- $prevPos = $this->pos;
+ $prevPos = $this->stream->tell();
$this->decodeName();
- $encodedSizeOfName = $this->pos - $prevPos;
- $this->skipNBytes($size - $encodedSizeOfName);
+ $encodedSizeOfName = $this->stream->tell() - $prevPos;
+ $offset = $size - $encodedSizeOfName;
+ if ($offset < 0) {
+ throw new InvalidBinaryFormatException("custom section size");
+ }
+ if ($offset !== 0) {
+ $this->stream->seek($offset);
+ }
}
}
@@ -312,21 +308,21 @@ final class Decoder
private function decodeValType(): ValType
{
- $b = $this->peekByte();
+ $b = $this->stream->peekByte();
if ($b === 0x7F) {
- $this->skipNBytes(1);
+ $this->stream->seek(1);
return ValType::NumType(NumType::I32);
} elseif ($b === 0x7E) {
- $this->skipNBytes(1);
+ $this->stream->seek(1);
return ValType::NumType(NumType::I64);
} elseif ($b === 0x7D) {
- $this->skipNBytes(1);
+ $this->stream->seek(1);
return ValType::NumType(NumType::F32);
} elseif ($b === 0x7C) {
- $this->skipNBytes(1);
+ $this->stream->seek(1);
return ValType::NumType(NumType::F64);
} elseif ($b === 0x7B) {
- $this->skipNBytes(1);
+ $this->stream->seek(1);
return ValType::VecType(VecType::V128);
} else {
return ValType::RefType($this->decodeRefType());
@@ -524,10 +520,10 @@ final class Decoder
private function decodeCode(): Code
{
$size = $this->decodeU32();
- $prevPos = $this->pos;
+ $prevPos = $this->stream->tell();
$compressedLocals = $this->decodeVec($this->decodeLocals(...));
$body = $this->decodeExpr();
- if ($this->pos - $prevPos !== $size) {
+ if ($this->stream->tell() - $prevPos !== $size) {
throw new InvalidBinaryFormatException("code size");
}
return new Code(
@@ -672,7 +668,7 @@ final class Decoder
private function decodeInstr(): Instr
{
- switch ($this->decodeByte()) {
+ switch ($op = $this->decodeByte()) {
case 0x00: return Instr::Unreachable();
case 0x01: return Instr::Nop();
case 0x02:
@@ -739,14 +735,15 @@ final class Decoder
case 0x3D: return Instr::I64Store16(...$this->decodeMemArg());
case 0x3E: return Instr::I64Store32(...$this->decodeMemArg());
case 0x3F:
- if ($this->decodeByte() !== 0) {
- $this->seekBy(-1);
- throw new InvalidBinaryFormatException("Unexpected value while decoding an instruction `memory.size`, expected 0, but got " . $this->decodeByte());
+ $c = $this->decodeByte();
+ if ($c !== 0) {
+ throw new InvalidBinaryFormatException("Unexpected value while decoding an instruction `memory.size`, expected 0, but got $c");
}
return Instr::MemorySize();
case 0x40:
- if ($this->decodeByte() !== 0) {
- throw new InvalidBinaryFormatException("memory grow");
+ $c = $this->decodeByte();
+ if ($c !== 0) {
+ throw new InvalidBinaryFormatException("Unexpected value while decoding an instruction `memory.grow`, expected 0, but got $c");
}
return Instr::MemoryGrow();
case 0x41: return Instr::I32Const($this->decodeS32());
@@ -931,8 +928,6 @@ final class Decoder
}
// no break
default:
- $this->seekBy(-1);
- $op = $this->decodeByte();
throw new InvalidBinaryFormatException("Unexpected opcode $op while decoding an instruction");
}
}
@@ -949,9 +944,9 @@ final class Decoder
private function decodeBlockType(): BlockType
{
- $b = $this->peekByte();
+ $b = $this->stream->peekByte();
if ($b === 0x40) {
- $this->skipNBytes(1);
+ $this->stream->seek(1);
return BlockType::ValType(null);
} elseif (in_array($b, [0x7F, 0x7E, 0x7D, 0x7C, 0x7B, 0x70, 0x6F], true)) {
return BlockType::ValType($this->decodeValType());
@@ -995,42 +990,12 @@ final class Decoder
return $result;
}
- private function eof(): bool
- {
- return strlen($this->input) <= $this->pos;
- }
-
- private function ensureNBytesRemains(int $n): void
- {
- if ($this->inputSize < $this->pos + $n) {
- throw new InvalidBinaryFormatException("ensureNBytesRemains: $this->inputSize < $this->pos + $n");
- }
- }
-
- private function skipNBytes(int $n): void
- {
- $this->ensureNBytesRemains($n);
- $this->pos += $n;
- }
-
- private function peekByte(): int
- {
- $this->ensureNBytesRemains(1);
- return ord($this->input[$this->pos]);
- }
-
/**
* @phpstan-impure
*/
private function decodeByte(): int
{
- $this->ensureNBytesRemains(1);
- return ord($this->input[$this->pos++]);
- }
-
- private function seekBy(int $offset): void
- {
- $this->pos += $offset;
+ return $this->stream->readByte();
}
private function decodeU32(): int
@@ -1115,9 +1080,8 @@ final class Decoder
*/
private function decodeF32(): float
{
- $this->ensureNBytesRemains(4);
- $result = unpack('g', $this->input, $this->pos);
- $this->pos += 4;
+ $buf = $this->stream->read(4);
+ $result = unpack('g', $buf);
if ($result === false) {
throw new InvalidBinaryFormatException("f32");
}
@@ -1130,9 +1094,8 @@ final class Decoder
*/
private function decodeF64(): float
{
- $this->ensureNBytesRemains(8);
- $result = unpack('e', $this->input, $this->pos);
- $this->pos += 8;
+ $buf = $this->stream->read(8);
+ $result = unpack('e', $buf);
if ($result === false) {
throw new InvalidBinaryFormatException("f64");
}
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
+{
+}