| <?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; |
| } |
| } |