blob: cfb3c4da71f619d103a3e2c3d812858ad84d07e1 [file] [log] [blame]
<?php
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
namespace Google\Protobuf\Internal;
use Google\Protobuf\Internal\Uint64;
class CodedInputStream
{
private $buffer;
private $buffer_size_after_limit;
private $buffer_end;
private $current;
private $current_limit;
private $legitimate_message_end;
private $recursion_budget;
private $recursion_limit;
private $total_bytes_limit;
private $total_bytes_read;
const MAX_VARINT_BYTES = 10;
const DEFAULT_RECURSION_LIMIT = 100;
const DEFAULT_TOTAL_BYTES_LIMIT = 33554432; // 32 << 20, 32MB
public function __construct($buffer)
{
$start = 0;
$end = strlen($buffer);
$this->buffer = $buffer;
$this->buffer_size_after_limit = 0;
$this->buffer_end = $end;
$this->current = $start;
$this->current_limit = $end;
$this->legitimate_message_end = false;
$this->recursion_budget = self::DEFAULT_RECURSION_LIMIT;
$this->recursion_limit = self::DEFAULT_RECURSION_LIMIT;
$this->total_bytes_limit = self::DEFAULT_TOTAL_BYTES_LIMIT;
$this->total_bytes_read = $end - $start;
}
private function advance($amount)
{
$this->current += $amount;
}
public function bufferSize()
{
return $this->buffer_end - $this->current;
}
public function current()
{
return $this->total_bytes_read -
($this->buffer_end - $this->current +
$this->buffer_size_after_limit);
}
public function substr($start, $end)
{
return substr($this->buffer, $start, $end - $start);
}
private function recomputeBufferLimits()
{
$this->buffer_end += $this->buffer_size_after_limit;
$closest_limit = min($this->current_limit, $this->total_bytes_limit);
if ($closest_limit < $this->total_bytes_read) {
// The limit position is in the current buffer. We must adjust the
// buffer size accordingly.
$this->buffer_size_after_limit = $this->total_bytes_read -
$closest_limit;
$this->buffer_end -= $this->buffer_size_after_limit;
} else {
$this->buffer_size_after_limit = 0;
}
}
private function consumedEntireMessage()
{
return $this->legitimate_message_end;
}
/**
* Read uint32 into $var. Advance buffer with consumed bytes. If the
* contained varint is larger than 32 bits, discard the high order bits.
* @param $var
*/
public function readVarint32(&$var)
{
if (!$this->readVarint64($var)) {
return false;
}
if (PHP_INT_SIZE == 4) {
$var = bcmod($var, 4294967296);
} else {
$var &= 0xFFFFFFFF;
}
// Convert large uint32 to int32.
if ($var > 0x7FFFFFFF) {
if (PHP_INT_SIZE === 8) {
$var = $var | (0xFFFFFFFF << 32);
} else {
$var = bcsub($var, 4294967296);
}
}
$var = intval($var);
return true;
}
/**
* Read Uint64 into $var. Advance buffer with consumed bytes.
* @param $var
*/
public function readVarint64(&$var)
{
$count = 0;
if (PHP_INT_SIZE == 4) {
$high = 0;
$low = 0;
$b = 0;
do {
if ($this->current === $this->buffer_end) {
return false;
}
if ($count === self::MAX_VARINT_BYTES) {
return false;
}
$b = ord($this->buffer[$this->current]);
$bits = 7 * $count;
if ($bits >= 32) {
$high |= (($b & 0x7F) << ($bits - 32));
} else if ($bits > 25){
// $bits is 28 in this case.
$low |= (($b & 0x7F) << 28);
$high = ($b & 0x7F) >> 4;
} else {
$low |= (($b & 0x7F) << $bits);
}
$this->advance(1);
$count += 1;
} while ($b & 0x80);
$var = GPBUtil::combineInt32ToInt64($high, $low);
if (bccomp($var, 0) < 0) {
$var = bcadd($var, "18446744073709551616");
}
} else {
$result = 0;
$shift = 0;
do {
if ($this->current === $this->buffer_end) {
return false;
}
if ($count === self::MAX_VARINT_BYTES) {
return false;
}
$byte = ord($this->buffer[$this->current]);
$result |= ($byte & 0x7f) << $shift;
$shift += 7;
$this->advance(1);
$count += 1;
} while ($byte > 0x7f);
$var = $result;
}
return true;
}
/**
* Read int into $var. If the result is larger than the largest integer, $var
* will be -1. Advance buffer with consumed bytes.
* @param $var
*/
public function readVarintSizeAsInt(&$var)
{
if (!$this->readVarint64($var)) {
return false;
}
$var = (int)$var;
return true;
}
/**
* Read 32-bit unsigned integer to $var. If the buffer has less than 4 bytes,
* return false. Advance buffer with consumed bytes.
* @param $var
*/
public function readLittleEndian32(&$var)
{
$data = null;
if (!$this->readRaw(4, $data)) {
return false;
}
$var = unpack('V', $data);
$var = $var[1];
return true;
}
/**
* Read 64-bit unsigned integer to $var. If the buffer has less than 8 bytes,
* return false. Advance buffer with consumed bytes.
* @param $var
*/
public function readLittleEndian64(&$var)
{
$data = null;
if (!$this->readRaw(4, $data)) {
return false;
}
$low = unpack('V', $data)[1];
if (!$this->readRaw(4, $data)) {
return false;
}
$high = unpack('V', $data)[1];
if (PHP_INT_SIZE == 4) {
$var = GPBUtil::combineInt32ToInt64($high, $low);
} else {
$var = ($high << 32) | $low;
}
return true;
}
/**
* Read tag into $var. Advance buffer with consumed bytes.
*/
public function readTag()
{
if ($this->current === $this->buffer_end) {
// Make sure that it failed due to EOF, not because we hit
// total_bytes_limit, which, unlike normal limits, is not a valid
// place to end a message.
$current_position = $this->total_bytes_read -
$this->buffer_size_after_limit;
if ($current_position >= $this->total_bytes_limit) {
// Hit total_bytes_limit_. But if we also hit the normal limit,
// we're still OK.
$this->legitimate_message_end =
($this->current_limit === $this->total_bytes_limit);
} else {
$this->legitimate_message_end = true;
}
return 0;
}
$result = 0;
// The largest tag is 2^29 - 1, which can be represented by int32.
$success = $this->readVarint32($result);
if ($success) {
return $result;
} else {
return 0;
}
}
public function readRaw($size, &$buffer)
{
$current_buffer_size = 0;
if ($this->bufferSize() < $size) {
return false;
}
if ($size === 0) {
$buffer = "";
} else {
$buffer = substr($this->buffer, $this->current, $size);
$this->advance($size);
}
return true;
}
/* Places a limit on the number of bytes that the stream may read, starting
* from the current position. Once the stream hits this limit, it will act
* like the end of the input has been reached until popLimit() is called.
*
* As the names imply, the stream conceptually has a stack of limits. The
* shortest limit on the stack is always enforced, even if it is not the top
* limit.
*
* The value returned by pushLimit() is opaque to the caller, and must be
* passed unchanged to the corresponding call to popLimit().
*
* @param integer $byte_limit
* @throws \Exception Fail to push limit.
*/
public function pushLimit($byte_limit)
{
// Current position relative to the beginning of the stream.
$current_position = $this->current();
$old_limit = $this->current_limit;
// security: byte_limit is possibly evil, so check for negative values
// and overflow.
if ($byte_limit >= 0 &&
$byte_limit <= PHP_INT_MAX - $current_position &&
$byte_limit <= $this->current_limit - $current_position) {
$this->current_limit = $current_position + $byte_limit;
$this->recomputeBufferLimits();
} else {
throw new GPBDecodeException("Fail to push limit.");
}
return $old_limit;
}
/* The limit passed in is actually the *old* limit, which we returned from
* PushLimit().
*
* @param integer $byte_limit
*/
public function popLimit($byte_limit)
{
$this->current_limit = $byte_limit;
$this->recomputeBufferLimits();
// We may no longer be at a legitimate message end. ReadTag() needs to
// be called again to find out.
$this->legitimate_message_end = false;
}
public function incrementRecursionDepthAndPushLimit(
$byte_limit, &$old_limit, &$recursion_budget)
{
$old_limit = $this->pushLimit($byte_limit);
$recursion_limit = --$this->recursion_limit;
}
public function decrementRecursionDepthAndPopLimit($byte_limit)
{
$result = $this->consumedEntireMessage();
$this->popLimit($byte_limit);
++$this->recursion_budget;
return $result;
}
public function bytesUntilLimit()
{
if ($this->current_limit === PHP_INT_MAX) {
return -1;
}
return $this->current_limit - $this->current;
}
}