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. * @param $var. */ 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; } }