Skip to content

Commit

Permalink
Merge pull request #10 from clue-labs/decode-length
Browse files Browse the repository at this point in the history
Limit buffer size to 64 KiB by default
  • Loading branch information
clue authored Apr 16, 2018
2 parents 2010be1 + 39aef52 commit d879d7e
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 5 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ These chunks do not necessarily represent complete JSON elements, as an
element may be broken up into multiple chunks.
This class reassembles these elements by buffering incomplete ones.

The `Decoder` supports the same parameters as the underlying
The `Decoder` supports the same optional parameters as the underlying
[`json_decode()`](http://php.net/json_decode) function.
This means that, by default, JSON objects will be emitted as a `stdClass`.
This behavior can be controlled through the optional constructor parameters:
Expand All @@ -58,6 +58,16 @@ $stream->on('data', function ($data) {
});
```

Additionally, the `Decoder` limits the maximum buffer size (maximum line
length) to avoid buffer overflows due to malformed user input. Usually, there
should be no need to change this value, unless you know you're dealing with some
unreasonably long lines. It accepts an additional argument if you want to change
this from the default of 64 KiB:

```php
$stream = new Decoder($stdin, false, 512, 0, 64 * 1024);
```

If the underlying stream emits an `error` event or the plain stream contains
any data that does not represent a valid NDJson stream,
it will emit an `error` event and then `close` the input stream:
Expand Down
15 changes: 11 additions & 4 deletions src/Decoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ class Decoder extends EventEmitter implements ReadableStreamInterface
private $assoc;
private $depth;
private $options;
private $maxlength;

private $buffer = '';
private $closed = false;

public function __construct(ReadableStreamInterface $input, $assoc = false, $depth = 512, $options = 0)
public function __construct(ReadableStreamInterface $input, $assoc = false, $depth = 512, $options = 0, $maxlength = 65536)
{
// @codeCoverageIgnoreStart
if ($options !== 0 && PHP_VERSION < 5.4) {
Expand All @@ -37,6 +38,7 @@ public function __construct(ReadableStreamInterface $input, $assoc = false, $dep
$this->assoc = $assoc;
$this->depth = $depth;
$this->options = $options;
$this->maxlength = $maxlength;

$this->input->on('data', array($this, 'handleData'));
$this->input->on('end', array($this, 'handleEnd'));
Expand Down Expand Up @@ -87,26 +89,31 @@ public function handleData($data)
$this->buffer .= $data;

// keep parsing while a newline has been found
while (($newline = strpos($this->buffer, "\n")) !== false) {
while (($newline = strpos($this->buffer, "\n")) !== false && $newline <= $this->maxlength) {
// read data up until newline and remove from buffer
$data = (string)substr($this->buffer, 0, $newline);
$this->buffer = (string)substr($this->buffer, $newline + 1);

// decode data with options given in ctor
// @codeCoverageIgnoreStart
if ($this->options === 0) {
$data = json_decode($data, $this->assoc, $this->depth);
} else {
$data = json_decode($data, $this->assoc, $this->depth, $this->options);
}
// @codeCoverageIgnoreEnd

// abort stream if decoding failed
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
$this->emit('error', array(new \RuntimeException('Unable to decode JSON', json_last_error())));
return $this->close();
return $this->handleError(new \RuntimeException('Unable to decode JSON', json_last_error()));
}

$this->emit('data', array($data));
}

if (isset($this->buffer[$this->maxlength])) {
$this->handleError(new \OverflowException('Buffer size exceeded'));
}
}

/** @internal */
Expand Down
29 changes: 29 additions & 0 deletions tests/DecoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,35 @@ public function testEmitDataErrorWillForwardError()
$this->input->emit('data', array("invalid\n"));
}

public function testEmitDataOverflowWillForwardError()
{
$this->decoder->on('data', $this->expectCallableNever());
$this->decoder->on('error', $this->expectCallableOnce());

$this->input->emit('data', array("\"" . str_repeat(".", 40000)));
$this->input->emit('data', array(str_repeat(".", 40000) . "\"\n"));
}

public function testEmitDataWithExactLimitWillForward()
{
$this->decoder = new Decoder($this->input, false, 512, 0, 4);

$this->decoder->on('data', $this->expectCallableOnceWith(null));
$this->decoder->on('error', $this->expectCallableNever());

$this->input->emit('data', array("null\n"));
}

public function testEmitDataOverflowBehindExactLimitWillForwardError()
{
$this->decoder = new Decoder($this->input, false, 512, 0, 3);

$this->decoder->on('data', $this->expectCallableNever());
$this->decoder->on('error', $this->expectCallableOnce());

$this->input->emit('data', array("null"));
}

public function testEmitDataErrorWithoutNewlineWillNotForward()
{
$this->decoder->on('data', $this->expectCallableNever());
Expand Down

0 comments on commit d879d7e

Please sign in to comment.