From ecca6ea95d56a6f70ff7b223ec3f904758acc8b1 Mon Sep 17 00:00:00 2001 From: Paul Yang Date: Fri, 30 Jun 2017 12:14:09 -0700 Subject: Add json encode/decode for php. (#3226) * Add json encode/decode for php. * Fix php conformance test on 32-bit machines. * Fix conformance test for c extension. * Fix comments --- .../Google/Protobuf/Internal/CodedInputStream.php | 373 +++++++++++++ .../Google/Protobuf/Internal/CodedOutputStream.php | 159 ++++++ php/src/Google/Protobuf/Internal/Descriptor.php | 32 +- .../Google/Protobuf/Internal/EnumDescriptor.php | 16 + .../Protobuf/Internal/EnumValueDescriptor.php | 22 + .../Google/Protobuf/Internal/FieldDescriptor.php | 85 +-- php/src/Google/Protobuf/Internal/GPBJsonWire.php | 285 ++++++++++ php/src/Google/Protobuf/Internal/GPBUtil.php | 70 +-- php/src/Google/Protobuf/Internal/GPBWire.php | 49 +- php/src/Google/Protobuf/Internal/GPBWireType.php | 43 ++ php/src/Google/Protobuf/Internal/InputStream.php | 370 ------------- php/src/Google/Protobuf/Internal/MapField.php | 2 +- php/src/Google/Protobuf/Internal/MapFieldIter.php | 14 +- php/src/Google/Protobuf/Internal/Message.php | 586 ++++++++++++++++++++- php/src/Google/Protobuf/Internal/OutputStream.php | 159 ------ .../Google/Protobuf/Internal/RawInputStream.php | 50 ++ 16 files changed, 1662 insertions(+), 653 deletions(-) create mode 100644 php/src/Google/Protobuf/Internal/CodedInputStream.php create mode 100644 php/src/Google/Protobuf/Internal/CodedOutputStream.php create mode 100644 php/src/Google/Protobuf/Internal/GPBJsonWire.php create mode 100644 php/src/Google/Protobuf/Internal/GPBWireType.php delete mode 100644 php/src/Google/Protobuf/Internal/InputStream.php delete mode 100644 php/src/Google/Protobuf/Internal/OutputStream.php create mode 100644 php/src/Google/Protobuf/Internal/RawInputStream.php (limited to 'php/src/Google/Protobuf') diff --git a/php/src/Google/Protobuf/Internal/CodedInputStream.php b/php/src/Google/Protobuf/Internal/CodedInputStream.php new file mode 100644 index 00000000..6131d5d1 --- /dev/null +++ b/php/src/Google/Protobuf/Internal/CodedInputStream.php @@ -0,0 +1,373 @@ +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; + } + + private function current() + { + return $this->total_bytes_read - + ($this->buffer_end - $this->current + + $this->buffer_size_after_limit); + } + + 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 unsiged 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 unsiged 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 larget 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; + } + + $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; + } +} diff --git a/php/src/Google/Protobuf/Internal/CodedOutputStream.php b/php/src/Google/Protobuf/Internal/CodedOutputStream.php new file mode 100644 index 00000000..4525d8dd --- /dev/null +++ b/php/src/Google/Protobuf/Internal/CodedOutputStream.php @@ -0,0 +1,159 @@ +current = 0; + $this->buffer_size = $size; + $this->buffer = str_repeat(chr(0), $this->buffer_size); + } + + public function getData() + { + return $this->buffer; + } + + public function writeVarint32($value, $trim) + { + $bytes = str_repeat(chr(0), self::MAX_VARINT64_BYTES); + $size = self::writeVarintToArray($value, $bytes, $trim); + return $this->writeRaw($bytes, $size); + } + + public function writeVarint64($value) + { + $bytes = str_repeat(chr(0), self::MAX_VARINT64_BYTES); + $size = self::writeVarintToArray($value, $bytes); + return $this->writeRaw($bytes, $size); + } + + public function writeLittleEndian32($value) + { + $bytes = str_repeat(chr(0), 4); + $size = self::writeLittleEndian32ToArray($value, $bytes); + return $this->writeRaw($bytes, $size); + } + + public function writeLittleEndian64($value) + { + $bytes = str_repeat(chr(0), 8); + $size = self::writeLittleEndian64ToArray($value, $bytes); + return $this->writeRaw($bytes, $size); + } + + public function writeTag($tag) + { + return $this->writeVarint32($tag, true); + } + + public function writeRaw($data, $size) + { + if ($this->buffer_size < $size) { + trigger_error("Output stream doesn't have enough buffer."); + return false; + } + + for ($i = 0; $i < $size; $i++) { + $this->buffer[$this->current] = $data[$i]; + $this->current++; + $this->buffer_size--; + } + return true; + } + + private static function writeVarintToArray($value, &$buffer, $trim = false) + { + $current = 0; + + $high = 0; + $low = 0; + if (PHP_INT_SIZE == 4) { + GPBUtil::divideInt64ToInt32($value, $high, $low, $trim); + } else { + $low = $value; + } + + while (($low >= 0x80 || $low < 0) || $high != 0) { + $buffer[$current] = chr($low | 0x80); + $value = ($value >> 7) & ~(0x7F << ((PHP_INT_SIZE << 3) - 7)); + $carry = ($high & 0x7F) << ((PHP_INT_SIZE << 3) - 7); + $high = ($high >> 7) & ~(0x7F << ((PHP_INT_SIZE << 3) - 7)); + $low = (($low >> 7) & ~(0x7F << ((PHP_INT_SIZE << 3) - 7)) | $carry); + $current++; + } + $buffer[$current] = chr($low); + return $current + 1; + } + + private static function writeLittleEndian32ToArray($value, &$buffer) + { + $buffer[0] = chr($value & 0x000000FF); + $buffer[1] = chr(($value >> 8) & 0x000000FF); + $buffer[2] = chr(($value >> 16) & 0x000000FF); + $buffer[3] = chr(($value >> 24) & 0x000000FF); + return 4; + } + + private static function writeLittleEndian64ToArray($value, &$buffer) + { + $high = 0; + $low = 0; + if (PHP_INT_SIZE == 4) { + GPBUtil::divideInt64ToInt32($value, $high, $low); + } else { + $low = $value & 0xFFFFFFFF; + $high = ($value >> 32) & 0xFFFFFFFF; + } + + $buffer[0] = chr($low & 0x000000FF); + $buffer[1] = chr(($low >> 8) & 0x000000FF); + $buffer[2] = chr(($low >> 16) & 0x000000FF); + $buffer[3] = chr(($low >> 24) & 0x000000FF); + $buffer[4] = chr($high & 0x000000FF); + $buffer[5] = chr(($high >> 8) & 0x000000FF); + $buffer[6] = chr(($high >> 16) & 0x000000FF); + $buffer[7] = chr(($high >> 24) & 0x000000FF); + return 8; + } + +} diff --git a/php/src/Google/Protobuf/Internal/Descriptor.php b/php/src/Google/Protobuf/Internal/Descriptor.php index f8d24e45..44225ad2 100644 --- a/php/src/Google/Protobuf/Internal/Descriptor.php +++ b/php/src/Google/Protobuf/Internal/Descriptor.php @@ -37,6 +37,8 @@ class Descriptor private $full_name; private $field = []; + private $json_to_field = []; + private $name_to_field = []; private $nested_type = []; private $enum_type = []; private $klass; @@ -66,6 +68,8 @@ class Descriptor public function addField($field) { $this->field[$field->getNumber()] = $field; + $this->json_to_field[$field->getJsonName()] = $field; + $this->name_to_field[$field->getName()] = $field; } public function getField() @@ -95,11 +99,29 @@ class Descriptor public function getFieldByNumber($number) { - if (!isset($this->field[$number])) { - return NULL; - } else { - return $this->field[$number]; - } + if (!isset($this->field[$number])) { + return NULL; + } else { + return $this->field[$number]; + } + } + + public function getFieldByJsonName($json_name) + { + if (!isset($this->json_to_field[$json_name])) { + return NULL; + } else { + return $this->json_to_field[$json_name]; + } + } + + public function getFieldByName($name) + { + if (!isset($this->name_to_field[$name])) { + return NULL; + } else { + return $this->name_to_field[$name]; + } } public function setClass($klass) diff --git a/php/src/Google/Protobuf/Internal/EnumDescriptor.php b/php/src/Google/Protobuf/Internal/EnumDescriptor.php index 7360a477..33a55a4a 100644 --- a/php/src/Google/Protobuf/Internal/EnumDescriptor.php +++ b/php/src/Google/Protobuf/Internal/EnumDescriptor.php @@ -8,6 +8,7 @@ class EnumDescriptor private $klass; private $full_name; private $value; + private $name_to_value; public function setFullName($full_name) { @@ -22,6 +23,17 @@ class EnumDescriptor public function addValue($number, $value) { $this->value[$number] = $value; + $this->name_to_value[$value->getName()] = $value; + } + + public function getValueByNumber($number) + { + return $this->value[$number]; + } + + public function getValueByName($name) + { + return $this->name_to_value[$name]; } public function setClass($klass) @@ -50,6 +62,10 @@ class EnumDescriptor $fullname); $desc->setFullName($fullname); $desc->setClass($classname); + $values = $proto->getValue(); + foreach ($values as $value) { + $desc->addValue($value->getNumber(), $value); + } return $desc; } diff --git a/php/src/Google/Protobuf/Internal/EnumValueDescriptor.php b/php/src/Google/Protobuf/Internal/EnumValueDescriptor.php index e65a4e8d..549766e3 100644 --- a/php/src/Google/Protobuf/Internal/EnumValueDescriptor.php +++ b/php/src/Google/Protobuf/Internal/EnumValueDescriptor.php @@ -34,4 +34,26 @@ namespace Google\Protobuf\Internal; class EnumValueDescriptor { + private $name; + private $number; + + public function setName($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function setNumber($number) + { + $this->number = $number; + } + + public function getNumber() + { + return $this->number; + } } diff --git a/php/src/Google/Protobuf/Internal/FieldDescriptor.php b/php/src/Google/Protobuf/Internal/FieldDescriptor.php index 6c91950f..f18bf810 100644 --- a/php/src/Google/Protobuf/Internal/FieldDescriptor.php +++ b/php/src/Google/Protobuf/Internal/FieldDescriptor.php @@ -36,6 +36,7 @@ class FieldDescriptor { private $name; + private $json_name; private $setter; private $getter; private $number; @@ -67,6 +68,16 @@ class FieldDescriptor return $this->name; } + public function setJsonName($json_name) + { + $this->json_name = $json_name; + } + + public function getJsonName() + { + return $this->json_name; + } + public function setSetter($setter) { $this->setter = $setter; @@ -172,23 +183,49 @@ class FieldDescriptor $field_type !== GPBType::BYTES); } - public static function getFieldDescriptor( - $name, - $label, - $type, - $number, - $oneof_index, - $packed, - $type_name = null) + public static function getFieldDescriptor($proto) { + $type_name = null; + $type = $proto->getType(); + switch ($type) { + case GPBType::MESSAGE: + case GPBType::GROUP: + case GPBType::ENUM: + $type_name = $proto->getTypeName(); + break; + default: + break; + } + + $oneof_index = $proto->hasOneofIndex() ? $proto->getOneofIndex() : -1; + $packed = false; + $options = $proto->getOptions(); + if ($options !== null) { + $packed = $options->getPacked(); + } + $field = new FieldDescriptor(); - $field->setName($name); - $camel_name = implode('', array_map('ucwords', explode('_', $name))); + $field->setName($proto->getName()); + + $json_name = $proto->hasJsonName() ? $proto->getJsonName() : + lcfirst(implode('', array_map('ucwords', explode('_', $proto->getName())))); + if ($proto->hasJsonName()) { + $json_name = $proto->getJsonName(); + } else { + $proto_name = $proto->getName(); + $json_name = implode('', array_map('ucwords', explode('_', $proto_name))); + if ($proto_name[0] !== "_" && !ctype_upper($proto_name[0])) { + $json_name = lcfirst($json_name); + } + } + $field->setJsonName($json_name); + + $camel_name = implode('', array_map('ucwords', explode('_', $proto->getName()))); $field->setGetter('get' . $camel_name); $field->setSetter('set' . $camel_name); - $field->setType($type); - $field->setNumber($number); - $field->setLabel($label); + $field->setType($proto->getType()); + $field->setNumber($proto->getNumber()); + $field->setLabel($proto->getLabel()); $field->setPacked($packed); $field->setOneofIndex($oneof_index); @@ -211,26 +248,6 @@ class FieldDescriptor public static function buildFromProto($proto) { - $type_name = null; - switch ($proto->getType()) { - case GPBType::MESSAGE: - case GPBType::GROUP: - case GPBType::ENUM: - $type_name = $proto->getTypeName(); - break; - default: - break; - } - - $oneof_index = $proto->hasOneofIndex() ? $proto->getOneofIndex() : -1; - $packed = false; - $options = $proto->getOptions(); - if ($options !== null) { - $packed = $options->getPacked(); - } - - return FieldDescriptor::getFieldDescriptor( - $proto->getName(), $proto->getLabel(), $proto->getType(), - $proto->getNumber(), $oneof_index, $packed, $type_name); + return FieldDescriptor::getFieldDescriptor($proto); } } diff --git a/php/src/Google/Protobuf/Internal/GPBJsonWire.php b/php/src/Google/Protobuf/Internal/GPBJsonWire.php new file mode 100644 index 00000000..97789356 --- /dev/null +++ b/php/src/Google/Protobuf/Internal/GPBJsonWire.php @@ -0,0 +1,285 @@ +writeRaw("\"", 1); + $field_name = GPBJsonWire::formatFieldName($field); + $output->writeRaw($field_name, strlen($field_name)); + $output->writeRaw("\":", 2); + return static::serializeFieldValueToStream($value, $field, $output); + } + + private static function serializeFieldValueToStream( + $values, + $field, + &$output) + { + if ($field->isMap()) { + $output->writeRaw("{", 1); + $first = true; + $map_entry = $field->getMessageType(); + $key_field = $map_entry->getFieldByNumber(1); + $value_field = $map_entry->getFieldByNumber(2); + + switch ($key_field->getType()) { + case GPBType::STRING: + case GPBType::SFIXED64: + case GPBType::INT64: + case GPBType::SINT64: + case GPBType::FIXED64: + case GPBType::UINT64: + $additional_quote = false; + break; + default: + $additional_quote = true; + } + + foreach ($values as $key => $value) { + if ($first) { + $first = false; + } else { + $output->writeRaw(",", 1); + } + if ($additional_quote) { + $output->writeRaw("\"", 1); + } + if (!static::serializeSingularFieldValueToStream( + $key, + $key_field, + $output)) { + return false; + } + if ($additional_quote) { + $output->writeRaw("\"", 1); + } + $output->writeRaw(":", 1); + if (!static::serializeSingularFieldValueToStream( + $value, + $value_field, + $output)) { + return false; + } + } + $output->writeRaw("}", 1); + return true; + } elseif ($field->isRepeated()) { + $output->writeRaw("[", 1); + $first = true; + foreach ($values as $value) { + if ($first) { + $first = false; + } else { + $output->writeRaw(",", 1); + } + if (!static::serializeSingularFieldValueToStream( + $value, + $field, + $output)) { + return false; + } + } + $output->writeRaw("]", 1); + return true; + } else { + return static::serializeSingularFieldValueToStream( + $values, + $field, + $output); + } + } + + private static function serializeSingularFieldValueToStream( + $value, + $field, + &$output) + { + switch ($field->getType()) { + case GPBType::SFIXED32: + case GPBType::SINT32: + case GPBType::INT32: + $str_value = strval($value); + $output->writeRaw($str_value, strlen($str_value)); + break; + case GPBType::FIXED32: + case GPBType::UINT32: + if ($value < 0) { + $value = bcadd($value, "4294967296"); + } + $str_value = strval($value); + $output->writeRaw($str_value, strlen($str_value)); + break; + case GPBType::FIXED64: + case GPBType::UINT64: + if ($value < 0) { + $value = bcadd($value, "18446744073709551616"); + } + // Intentional fall through. + case GPBType::SFIXED64: + case GPBType::INT64: + case GPBType::SINT64: + $output->writeRaw("\"", 1); + $str_value = strval($value); + $output->writeRaw($str_value, strlen($str_value)); + $output->writeRaw("\"", 1); + break; + case GPBType::FLOAT: + if (is_nan($value)) { + $str_value = "\"NaN\""; + } elseif ($value === INF) { + $str_value = "\"Infinity\""; + } elseif ($value === -INF) { + $str_value = "\"-Infinity\""; + } else { + $str_value = sprintf("%.8g", $value); + } + $output->writeRaw($str_value, strlen($str_value)); + break; + case GPBType::DOUBLE: + if (is_nan($value)) { + $str_value = "\"NaN\""; + } elseif ($value === INF) { + $str_value = "\"Infinity\""; + } elseif ($value === -INF) { + $str_value = "\"-Infinity\""; + } else { + $str_value = sprintf("%.17g", $value); + } + $output->writeRaw($str_value, strlen($str_value)); + break; + case GPBType::ENUM: + $enum_desc = $field->getEnumType(); + $enum_value_desc = $enum_desc->getValueByNumber($value); + if (!is_null($enum_value_desc)) { + $str_value = $enum_value_desc->getName(); + $output->writeRaw("\"", 1); + $output->writeRaw($str_value, strlen($str_value)); + $output->writeRaw("\"", 1); + } else { + $str_value = strval($value); + $output->writeRaw($str_value, strlen($str_value)); + } + break; + case GPBType::BOOL: + if ($value) { + $output->writeRaw("true", 4); + } else { + $output->writeRaw("false", 5); + } + break; + case GPBType::BYTES: + $value = base64_encode($value); + case GPBType::STRING: + $value = json_encode($value); + $output->writeRaw($value, strlen($value)); + break; + // case GPBType::GROUP: + // echo "GROUP\xA"; + // trigger_error("Not implemented.", E_ERROR); + // break; + case GPBType::MESSAGE: + $value->serializeToJsonStream($output); + break; + default: + user_error("Unsupported type."); + return false; + } + return true; + } + + private static function formatFieldName($field) + { + return $field->getJsonName(); + } + + // Used for escaping control chars in strings. + private static $k_control_char_limit = 0x20; + + private static function jsonNiceEscape($c) + { + switch ($c) { + case '"': return "\\\""; + case '\\': return "\\\\"; + case '/': return "\\/"; + case '\b': return "\\b"; + case '\f': return "\\f"; + case '\n': return "\\n"; + case '\r': return "\\r"; + case '\t': return "\\t"; + default: return NULL; + } + } + + private static function isJsonEscaped($c) + { + // See RFC 4627. + return $c < chr($k_control_char_limit) || $c === "\"" || $c === "\\"; + } + + public static function escapedJson($value) + { + $escaped_value = ""; + $unescaped_run = ""; + for ($i = 0; $i < strlen($value); $i++) { + $c = $value[$i]; + // Handle escaping. + if (static::isJsonEscaped($c)) { + // Use a "nice" escape, like \n, if one exists for this + // character. + $escape = static::jsonNiceEscape($c); + if (is_null($escape)) { + $escape = "\\u00" . bin2hex($c); + } + if ($unescaped_run !== "") { + $escaped_value .= $unescaped_run; + $unescaped_run = ""; + } + $escaped_value .= $escape; + } else { + if ($unescaped_run === "") { + $unescaped_run .= $c; + } + } + } + $escaped_value .= $unescaped_run; + return $escaped_value; + } + +} diff --git a/php/src/Google/Protobuf/Internal/GPBUtil.php b/php/src/Google/Protobuf/Internal/GPBUtil.php index 8c97e9fa..22ad27f9 100644 --- a/php/src/Google/Protobuf/Internal/GPBUtil.php +++ b/php/src/Google/Protobuf/Internal/GPBUtil.php @@ -45,8 +45,13 @@ class GPBUtil $value = bcsub(0, $value); } - $high = (int) bcdiv(bcadd($value, 1), 4294967296); + $high = bcdiv($value, 4294967296); $low = bcmod($value, 4294967296); + if (bccomp($high, 2147483647) > 0) { + $high = (int) bcsub($high, 4294967296); + } else { + $high = (int) $high; + } if (bccomp($low, 2147483647) > 0) { $low = (int) bcsub($low, 4294967296); } else { @@ -58,7 +63,7 @@ class GPBUtil $low = ~$low; $low++; if (!$low) { - $high++; + $high = (int)($high + 1); } } @@ -70,15 +75,13 @@ class GPBUtil public static function checkString(&$var, $check_utf8) { if (is_array($var) || is_object($var)) { - trigger_error("Expect string.", E_USER_ERROR); - return; + throw new \InvalidArgumentException("Expect string."); } if (!is_string($var)) { $var = strval($var); } if ($check_utf8 && !preg_match('//u', $var)) { - trigger_error("Expect utf-8 encoding.", E_USER_ERROR); - return; + throw new \Exception("Expect utf-8 encoding."); } } @@ -92,7 +95,7 @@ class GPBUtil if (is_numeric($var)) { $var = intval($var); } else { - trigger_error("Expect integer.", E_USER_ERROR); + throw new \Exception("Expect integer."); } } @@ -109,7 +112,7 @@ class GPBUtil $var = (int) $var; } } else { - trigger_error("Expect integer.", E_USER_ERROR); + throw new \Exception("Expect integer."); } } @@ -119,10 +122,15 @@ class GPBUtil if (PHP_INT_SIZE == 8) { $var = intval($var); } else { - $var = bcdiv($var, 1, 0); + if (is_float($var) || + is_integer($var) || + (is_string($var) && + bccomp($var, "9223372036854774784") < 0)) { + $var = number_format($var, 0, ".", ""); + } } } else { - trigger_error("Expect integer.", E_USER_ERROR); + throw new \Exception("Expect integer."); } } @@ -132,10 +140,10 @@ class GPBUtil if (PHP_INT_SIZE == 8) { $var = intval($var); } else { - $var = bcdiv($var, 1, 0); + $var = number_format($var, 0, ".", ""); } } else { - trigger_error("Expect integer.", E_USER_ERROR); + throw new \Exception("Expect integer."); } } @@ -144,7 +152,7 @@ class GPBUtil if (is_float($var) || is_numeric($var)) { $var = floatval($var); } else { - trigger_error("Expect float.", E_USER_ERROR); + throw new \Exception("Expect float."); } } @@ -153,15 +161,14 @@ class GPBUtil if (is_float($var) || is_numeric($var)) { $var = floatval($var); } else { - trigger_error("Expect float.", E_USER_ERROR); + throw new \Exception("Expect float."); } } public static function checkBool(&$var) { if (is_array($var) || is_object($var)) { - trigger_error("Expect boolean.", E_USER_ERROR); - return; + throw new \Exception("Expect boolean."); } $var = boolval($var); } @@ -169,14 +176,14 @@ class GPBUtil public static function checkMessage(&$var, $klass) { if (!$var instanceof $klass && !is_null($var)) { - trigger_error("Expect message.", E_USER_ERROR); + throw new \Exception("Expect message."); } } public static function checkRepeatedField(&$var, $type, $klass = null) { if (!$var instanceof RepeatedField && !is_array($var)) { - trigger_error("Expect array.", E_USER_ERROR); + throw new \Exception("Expect array."); } if (is_array($var)) { $tmp = new RepeatedField($type, $klass); @@ -186,15 +193,13 @@ class GPBUtil return $tmp; } else { if ($var->getType() != $type) { - trigger_error( - "Expect repeated field of different type.", - E_USER_ERROR); + throw new \Exception( + "Expect repeated field of different type."); } if ($var->getType() === GPBType::MESSAGE && $var->getClass() !== $klass) { - trigger_error( - "Expect repeated field of different message.", - E_USER_ERROR); + throw new \Exception( + "Expect repeated field of different message."); } return $var; } @@ -203,7 +208,7 @@ class GPBUtil public static function checkMapField(&$var, $key_type, $value_type, $klass = null) { if (!$var instanceof MapField && !is_array($var)) { - trigger_error("Expect dict.", E_USER_ERROR); + throw new \Exception("Expect dict."); } if (is_array($var)) { $tmp = new MapField($key_type, $value_type, $klass); @@ -213,20 +218,15 @@ class GPBUtil return $tmp; } else { if ($var->getKeyType() != $key_type) { - trigger_error( - "Expect map field of key type.", - E_USER_ERROR); + throw new \Exception("Expect map field of key type."); } if ($var->getValueType() != $value_type) { - trigger_error( - "Expect map field of value type.", - E_USER_ERROR); + throw new \Exception("Expect map field of value type."); } if ($var->getValueType() === GPBType::MESSAGE && $var->getValueClass() !== $klass) { - trigger_error( - "Expect map field of different value message.", - E_USER_ERROR); + throw new \Exception( + "Expect map field of different value message."); } return $var; } @@ -328,7 +328,7 @@ class GPBUtil $low = ~$low; $low++; if (!$low) { - $high++; + $high = (int) ($high + 1); } } $result = bcadd(bcmul($high, 4294967296), $low); diff --git a/php/src/Google/Protobuf/Internal/GPBWire.php b/php/src/Google/Protobuf/Internal/GPBWire.php index 67eb1bee..e7eec552 100644 --- a/php/src/Google/Protobuf/Internal/GPBWire.php +++ b/php/src/Google/Protobuf/Internal/GPBWire.php @@ -117,19 +117,12 @@ class GPBWire // << decode << public static function zigZagEncode32($int32) { - // Fill high 32 bits. - if (PHP_INT_SIZE === 8) { - $int32 |= ((($int32 << 32) >> 31) & (0xFFFFFFFF << 32)); + if (PHP_INT_SIZE == 8) { + $trim_int32 = $int32 & 0xFFFFFFFF; + return (($trim_int32 << 1) ^ ($int32 << 32 >> 63)) & 0xFFFFFFFF; + } else { + return ($int32 << 1) ^ ($int32 >> 31); } - - $uint32 = ($int32 << 1) ^ ($int32 >> 31); - - // Fill high 32 bits. - if (PHP_INT_SIZE === 8) { - $uint32 |= ((($uint32 << 32) >> 31) & (0xFFFFFFFF << 32)); - } - - return $uint32; } public static function zigZagDecode32($uint32) @@ -177,7 +170,11 @@ class GPBWire public static function readInt64(&$input, &$value) { - return $input->readVarint64($value); + $success = $input->readVarint64($value); + if (PHP_INT_SIZE == 4 && bccomp($value, "9223372036854775807") > 0) { + $value = bcsub($value, "18446744073709551616"); + } + return $success; } public static function readUint32(&$input, &$value) @@ -231,7 +228,11 @@ class GPBWire public static function readSfixed64(&$input, &$value) { - return $input->readLittleEndian64($value); + $success = $input->readLittleEndian64($value); + if (PHP_INT_SIZE == 4 && bccomp($value, "9223372036854775807") > 0) { + $value = bcsub($value, "18446744073709551616"); + } + return $success; } public static function readFloat(&$input, &$value) @@ -298,7 +299,7 @@ class GPBWire public static function writeInt32(&$output, $value) { - return $output->writeVarint32($value); + return $output->writeVarint32($value, false); } public static function writeInt64(&$output, $value) @@ -308,7 +309,7 @@ class GPBWire public static function writeUint32(&$output, $value) { - return $output->writeVarint32($value); + return $output->writeVarint32($value, true); } public static function writeUint64(&$output, $value) @@ -319,7 +320,7 @@ class GPBWire public static function writeSint32(&$output, $value) { $value = GPBWire::zigZagEncode32($value); - return $output->writeVarint64($value); + return $output->writeVarint32($value, true); } public static function writeSint64(&$output, $value) @@ -351,9 +352,9 @@ class GPBWire public static function writeBool(&$output, $value) { if ($value) { - return $output->writeVarint32(1); + return $output->writeVarint32(1, true); } else { - return $output->writeVarint32(0); + return $output->writeVarint32(0, true); } } @@ -377,7 +378,7 @@ class GPBWire public static function writeBytes(&$output, $value) { $size = strlen($value); - if (!$output->writeVarint32($size)) { + if (!$output->writeVarint32($size, true)) { return false; } return $output->writeRaw($value, $size); @@ -386,7 +387,7 @@ class GPBWire public static function writeMessage(&$output, $value) { $size = $value->byteSize(); - if (!$output->writeVarint32($size)) { + if (!$output->writeVarint32($size, true)) { return false; } return $value->serializeToStream($output); @@ -442,7 +443,8 @@ class GPBWire public static function varint64Size($value) { if (PHP_INT_SIZE == 4) { - if (bccomp($value, 0) < 0) { + if (bccomp($value, 0) < 0 || + bccomp($value, "9223372036854775807") > 0) { return 10; } if (bccomp($value, 1 << 7) < 0) { @@ -578,6 +580,9 @@ class GPBWire } break; case GPBType::UINT32: + if (PHP_INT_SIZE === 8 && $value < 0) { + $value += 4294967296; + } if (!GPBWire::writeUint32($output, $value)) { return false; } diff --git a/php/src/Google/Protobuf/Internal/GPBWireType.php b/php/src/Google/Protobuf/Internal/GPBWireType.php new file mode 100644 index 00000000..c1ad370e --- /dev/null +++ b/php/src/Google/Protobuf/Internal/GPBWireType.php @@ -0,0 +1,43 @@ +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; - } - - private function bufferSize() - { - return $this->buffer_end - $this->current; - } - - private function current() - { - return $this->total_bytes_read - - ($this->buffer_end - $this->current + - $this->buffer_size_after_limit); - } - - 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); - } 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 unsiged 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 unsiged 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 larget 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; - } - - $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; - } -} diff --git a/php/src/Google/Protobuf/Internal/MapField.php b/php/src/Google/Protobuf/Internal/MapField.php index 12f09d61..38736dad 100644 --- a/php/src/Google/Protobuf/Internal/MapField.php +++ b/php/src/Google/Protobuf/Internal/MapField.php @@ -205,7 +205,7 @@ class MapField implements \ArrayAccess, \IteratorAggregate, \Countable */ public function getIterator() { - return new MapFieldIter($this->container); + return new MapFieldIter($this->container, $this->key_type); } /** diff --git a/php/src/Google/Protobuf/Internal/MapFieldIter.php b/php/src/Google/Protobuf/Internal/MapFieldIter.php index a0388d92..cb707955 100644 --- a/php/src/Google/Protobuf/Internal/MapFieldIter.php +++ b/php/src/Google/Protobuf/Internal/MapFieldIter.php @@ -54,11 +54,13 @@ class MapFieldIter implements \Iterator * * @param MapField The MapField instance for which this iterator is * created. + * @param GPBType Map key type. * @ignore */ - public function __construct($container) + public function __construct($container, $key_type) { $this->container = $container; + $this->key_type = $key_type; } /** @@ -88,7 +90,13 @@ class MapFieldIter implements \Iterator */ public function key() { - return key($this->container); + $key = key($this->container); + // PHP associative array stores bool as integer for key. + if ($this->key_type === GPBType::BOOL) { + return boolval($key); + } else { + return $key; + } } /** @@ -110,4 +118,4 @@ class MapFieldIter implements \Iterator { return key($this->container) !== null; } -} \ No newline at end of file +} diff --git a/php/src/Google/Protobuf/Internal/Message.php b/php/src/Google/Protobuf/Internal/Message.php index 9ba249a0..1ecd4fa2 100644 --- a/php/src/Google/Protobuf/Internal/Message.php +++ b/php/src/Google/Protobuf/Internal/Message.php @@ -36,8 +36,8 @@ namespace Google\Protobuf\Internal; -use Google\Protobuf\Internal\InputStream; -use Google\Protobuf\Internal\OutputStream; +use Google\Protobuf\Internal\CodedInputStream; +use Google\Protobuf\Internal\CodedOutputStream; use Google\Protobuf\Internal\DescriptorPool; use Google\Protobuf\Internal\GPBLabel; use Google\Protobuf\Internal\GPBType; @@ -68,6 +68,10 @@ class Message // specific descriptor from the descriptor pool. if (get_class($this) === 'Google\Protobuf\Internal\MapEntry') { $this->desc = $desc; + foreach ($desc->getField() as $field) { + $setter = $field->getSetter(); + $this->$setter($this->defaultValue($field)); + } return; } $pool = DescriptorPool::getGeneratedPool(); @@ -216,6 +220,58 @@ class Message } } + /** + * @ignore + */ + private static function skipField($input, $tag) + { + $number = GPBWire::getTagFieldNumber($tag); + if ($number === 0) { + throw new GPBDecodeException("Illegal field number zero."); + } + + switch (GPBWire::getTagWireType($tag)) { + case GPBWireType::VARINT: + $uint64 = 0; + if (!$input->readVarint64($uint64)) { + throw new GPBDecodeException( + "Unexpected EOF inside varint."); + } + return; + case GPBWireType::FIXED64: + $uint64 = 0; + if (!$input->readLittleEndian64($uint64)) { + throw new GPBDecodeException( + "Unexpected EOF inside fixed64."); + } + return; + case GPBWireType::FIXED32: + $uint32 = 0; + if (!$input->readLittleEndian32($uint32)) { + throw new GPBDecodeException( + "Unexpected EOF inside fixed32."); + } + return; + case GPBWireType::LENGTH_DELIMITED: + $length = 0; + if (!$input->readVarint32($length)) { + throw new GPBDecodeException( + "Unexpected EOF inside length."); + } + $data = NULL; + if (!$input->readRaw($length, $data)) { + throw new GPBDecodeException( + "Unexpected EOF inside length delimited data."); + } + return; + case GPBWireType::START_GROUP: + case GPBWireType::END_GROUP: + throw new GPBDecodeException("Unexpected wire type."); + default: + throw new GPBDecodeException("Unexpected wire type."); + } + } + /** * @ignore */ @@ -278,7 +334,6 @@ class Message } break; case GPBType::GROUP: - echo "GROUP\xA"; trigger_error("Not implemented.", E_ERROR); break; case GPBType::MESSAGE: @@ -349,19 +404,25 @@ class Message private function parseFieldFromStream($tag, $input, $field) { $value = null; - $field_type = $field->getType(); - $value_format = GPBWire::UNKNOWN; - if (GPBWire::getTagWireType($tag) === - GPBWire::getWireType($field_type)) { + if (is_null($field)) { + $value_format = GPBWire::UNKNOWN; + } elseif (GPBWire::getTagWireType($tag) === + GPBWire::getWireType($field->getType())) { $value_format = GPBWire::NORMAL_FORMAT; } elseif ($field->isPackable() && GPBWire::getTagWireType($tag) === GPBWire::WIRETYPE_LENGTH_DELIMITED) { $value_format = GPBWire::PACKED_FORMAT; + } else { + // the wire type doesn't match. Put it in our unknown field set. + $value_format = GPBWire::UNKNOWN; } - if ($value_format === GPBWire::NORMAL_FORMAT) { + if ($value_format === GPBWire::UNKNOWN) { + self::skipField($input, $tag); + return; + } elseif ($value_format === GPBWire::NORMAL_FORMAT) { self::parseFieldFromStreamNoTag($input, $field, $value); } elseif ($value_format === GPBWire::PACKED_FORMAT) { $length = 0; @@ -378,7 +439,7 @@ class Message $input->popLimit($limit); return; } else { - return false; + return; } if ($field->isMap()) { @@ -583,10 +644,28 @@ class Message */ public function mergeFromString($data) { - $input = new InputStream($data); + $input = new CodedInputStream($data); $this->parseFromStream($input); } + /** + * Parses a json string to protobuf message. + * + * This function takes a string in the json wire format, matching the + * encoding output by serializeToJsonString(). + * See mergeFrom() for merging behavior, if the field is already set in the + * specified message. + * + * @param string $data Json protobuf data. + * @return null. + * @throws Exception Invalid data. + */ + public function mergeFromJsonString($data) + { + $input = new RawInputStream($data); + $this->parseFromJsonStream($input); + } + /** * @ignore */ @@ -602,12 +681,233 @@ class Message $number = GPBWire::getTagFieldNumber($tag); $field = $this->desc->getFieldByNumber($number); - // Check whether we retrieved a known field - if ($field === NULL) { - continue; + $this->parseFieldFromStream($tag, $input, $field); + } + } + + private function convertJsonValueToProtoValue( + $value, + $field, + $is_map_key = false) + { + if (is_null($value)) { + return $this->defaultValue($field); + } + switch ($field->getType()) { + case GPBType::MESSAGE: + $klass = $field->getMessageType()->getClass(); + if (!is_object($value) && !is_array($value)) { + throw new \Exception("Expect message."); + } + $submsg = new $klass; + if (!is_null($value) && + $klass !== "Google\Protobuf\Any") { + $submsg->mergeFromJsonArray($value); + } + return $submsg; + case GPBType::ENUM: + if (is_integer($value)) { + return $value; + } else { + $enum_value = + $field->getEnumType()->getValueByName($value); + } + if (!is_null($enum_value)) { + return $enum_value->getNumber(); + } + case GPBType::STRING: + if (!is_string($value)) { + throw new GPBDecodeException("Expect string"); + } + return $value; + case GPBType::BYTES: + if (!is_string($value)) { + throw new GPBDecodeException("Expect string"); + } + $proto_value = base64_decode($value, true); + if ($proto_value === false) { + throw new GPBDecodeException( + "Invalid base64 characters"); + } + return $proto_value; + case GPBType::BOOL: + if ($is_map_key) { + if ($value === "true") { + return true; + } + if ($value === "false") { + return false; + } + throw new GPBDecodeException( + "Bool field only accept bool value"); + } + if (!is_bool($value)) { + throw new GPBDecodeException( + "Bool field only accept bool value"); + } + return $value; + case GPBType::FLOAT: + if ($value === "Infinity") { + return INF; + } + if ($value === "-Infinity") { + return -INF; + } + if ($value === "NaN") { + return NAN; + } + return $value; + case GPBType::DOUBLE: + if ($value === "Infinity") { + return INF; + } + if ($value === "-Infinity") { + return -INF; + } + if ($value === "NaN") { + return NAN; + } + return $value; + case GPBType::INT32: + if (!is_numeric($value)) { + throw new GPBDecodeException( + "Invalid data type for int32 field"); + } + if (bccomp($value, "2147483647") > 0) { + throw new GPBDecodeException( + "Int32 too large"); + } + if (bccomp($value, "-2147483648") < 0) { + throw new GPBDecodeException( + "Int32 too small"); + } + return $value; + case GPBType::UINT32: + if (!is_numeric($value)) { + throw new GPBDecodeException( + "Invalid data type for uint32 field"); + } + if (bccomp($value, 4294967295) > 0) { + throw new GPBDecodeException( + "Uint32 too large"); + } + return $value; + case GPBType::INT64: + if (!is_numeric($value)) { + throw new GPBDecodeException( + "Invalid data type for int64 field"); + } + if (bccomp($value, "9223372036854775807") > 0) { + throw new GPBDecodeException( + "Int64 too large"); + } + if (bccomp($value, "-9223372036854775808") < 0) { + throw new GPBDecodeException( + "Int64 too small"); + } + return $value; + case GPBType::UINT64: + if (!is_numeric($value)) { + throw new GPBDecodeException( + "Invalid data type for int64 field"); + } + if (bccomp($value, "18446744073709551615") > 0) { + throw new GPBDecodeException( + "Uint64 too large"); + } + if (bccomp($value, "9223372036854775807") > 0) { + $value = bcsub($value, "18446744073709551616"); + } + return $value; + case GPBType::FIXED64: + return $value; + default: + return $value; + } + } + + private function mergeFromJsonArray($array) + { + foreach ($array as $key => $value) { + $field = $this->desc->getFieldByJsonName($key); + if (is_null($field)) { + $field = $this->desc->getFieldByName($key); + if (is_null($field)) { + continue; + } + } + $setter = $field->getSetter(); + if ($field->isMap()) { + if (is_null($value)) { + continue; + } + $getter = $field->getGetter(); + $key_field = $field->getMessageType()->getFieldByNumber(1); + $value_field = $field->getMessageType()->getFieldByNumber(2); + foreach ($value as $tmp_key => $tmp_value) { + if (is_null($tmp_value)) { + throw new \Exception( + "Map value field element cannot be null."); + } + $proto_key = + $this->convertJsonValueToProtoValue( + $tmp_key, + $key_field, + true); + $proto_value = + $this->convertJsonValueToProtoValue( + $tmp_value, + $value_field); + $this->$getter()[$proto_key] = $proto_value; + } + } else if ($field->isRepeated()) { + if (is_null($value)) { + continue; + } + $getter = $field->getGetter(); + foreach ($value as $tmp) { + if (is_null($tmp)) { + throw new \Exception( + "Repeated field elements cannot be null."); + } + $proto_value = + $this->convertJsonValueToProtoValue($tmp, $field); + $this->$getter()[] = $proto_value; + } + } else { + $setter = $field->getSetter(); + $proto_value = + $this->convertJsonValueToProtoValue($value, $field); + if ($field->getType() === GPBType::MESSAGE) { + if (is_null($proto_value)) { + continue; + } + $getter = $field->getGetter(); + $submsg = $this->$getter(); + if (!is_null($submsg)) { + $submsg->mergeFrom($proto_value); + continue; + } + } + $this->$setter($proto_value); } + } + } - $this->parseFieldFromStream($tag, $input, $field); + /** + * @ignore + */ + public function parseFromJsonStream($input) + { + $array = json_decode($input->getData(), JSON_BIGINT_AS_STRING); + if (is_null($array)) { + throw new GPBDecodeException( + "Cannot decode json string."); + } + try { + $this->mergeFromJsonArray($array); + } catch (Exception $e) { + throw new GPBDecodeException($e->getMessage()); } } @@ -650,7 +950,7 @@ class Message foreach ($values as $value) { $size += $this->fieldDataOnlyByteSize($field, $value); } - if (!$output->writeVarint32($size)) { + if (!$output->writeVarint32($size, true)) { return false; } } @@ -708,6 +1008,16 @@ class Message } } + /** + * @ignore + */ + private function serializeFieldToJsonStream(&$output, $field) + { + $getter = $field->getGetter(); + $values = $this->$getter(); + return GPBJsonWire::serializeFieldToStream($values, $field, $output); + } + /** * @ignore */ @@ -722,17 +1032,52 @@ class Message return true; } + /** + * @ignore + */ + public function serializeToJsonStream(&$output) + { + $output->writeRaw("{", 1); + $fields = $this->desc->getField(); + $first = true; + foreach ($fields as $field) { + if ($this->existField($field)) { + if ($first) { + $first = false; + } else { + $output->writeRaw(",", 1); + } + if (!$this->serializeFieldToJsonStream($output, $field)) { + return false; + } + } + } + $output->writeRaw("}", 1); + return true; + } + /** * Serialize the message to string. * @return string Serialized binary protobuf data. */ public function serializeToString() { - $output = new OutputStream($this->byteSize()); + $output = new CodedOutputStream($this->byteSize()); $this->serializeToStream($output); return $output->getData(); } + /** + * Serialize the message to json string. + * @return string Serialized json protobuf data. + */ + public function serializeToJsonString() + { + $output = new CodedOutputStream($this->jsonByteSize()); + $this->serializeToJsonStream($output); + return $output->getData(); + } + /** * @ignore */ @@ -746,8 +1091,14 @@ class Message } $getter = $field->getGetter(); - $value = $this->$getter(); - return $value !== $this->defaultValue($field); + $values = $this->$getter(); + if ($field->isMap()) { + return count($values) !== 0; + } elseif ($field->isRepeated()) { + return count($values) !== 0; + } else { + return $values !== $this->defaultValue($field); + } } /** @@ -827,6 +1178,101 @@ class Message return $size; } + /** + * @ignore + */ + private function fieldDataOnlyJsonByteSize($field, $value) + { + $size = 0; + + switch ($field->getType()) { + case GPBType::SFIXED32: + case GPBType::SINT32: + case GPBType::INT32: + $size += strlen(strval($value)); + break; + case GPBType::FIXED32: + case GPBType::UINT32: + if ($value < 0) { + $value = bcadd($value, "4294967296"); + } + $size += strlen(strval($value)); + break; + case GPBType::FIXED64: + case GPBType::UINT64: + if ($value < 0) { + $value = bcadd($value, "18446744073709551616"); + } + // Intentional fall through. + case GPBType::SFIXED64: + case GPBType::INT64: + case GPBType::SINT64: + $size += 2; // size for "" + $size += strlen(strval($value)); + break; + case GPBType::FLOAT: + if (is_nan($value)) { + $size += strlen("NaN") + 2; + } elseif ($value === INF) { + $size += strlen("Infinity") + 2; + } elseif ($value === -INF) { + $size += strlen("-Infinity") + 2; + } else { + $size += strlen(sprintf("%.8g", $value)); + } + break; + case GPBType::DOUBLE: + if (is_nan($value)) { + $size += strlen("NaN") + 2; + } elseif ($value === INF) { + $size += strlen("Infinity") + 2; + } elseif ($value === -INF) { + $size += strlen("-Infinity") + 2; + } else { + $size += strlen(sprintf("%.17g", $value)); + } + break; + case GPBType::ENUM: + $enum_desc = $field->getEnumType(); + $enum_value_desc = $enum_desc->getValueByNumber($value); + if (!is_null($enum_value_desc)) { + $size += 2; // size for "" + $size += strlen($enum_value_desc->getName()); + } else { + $str_value = strval($value); + $size += strlen($str_value); + } + break; + case GPBType::BOOL: + if ($value) { + $size += 4; + } else { + $size += 5; + } + break; + case GPBType::STRING: + $value = json_encode($value); + $size += strlen($value); + break; + case GPBType::BYTES: + $size += strlen(base64_encode($value)); + $size += 2; // size for \"\" + break; + case GPBType::MESSAGE: + $size += $value->jsonByteSize(); + break; +# case GPBType::GROUP: +# // TODO(teboring): Add support. +# user_error("Unsupported type."); +# break; + default: + user_error("Unsupported type " . $field->getType()); + return 0; + } + + return $size; + } + /** * @ignore */ @@ -844,12 +1290,18 @@ class Message $value_field = $message_type->getFieldByNumber(2); foreach ($values as $key => $value) { $data_size = 0; - $data_size += $this->fieldDataOnlyByteSize($key_field, $key); - $data_size += $this->fieldDataOnlyByteSize( - $value_field, - $value); - $data_size += GPBWire::tagSize($key_field); - $data_size += GPBWire::tagSize($value_field); + if ($key != $this->defaultValue($key_field)) { + $data_size += $this->fieldDataOnlyByteSize( + $key_field, + $key); + $data_size += GPBWire::tagSize($key_field); + } + if ($value != $this->defaultValue($value_field)) { + $data_size += $this->fieldDataOnlyByteSize( + $value_field, + $value); + $data_size += GPBWire::tagSize($value_field); + } $size += GPBWire::varint32Size($data_size) + $data_size; } } @@ -882,6 +1334,68 @@ class Message return $size; } + /** + * @ignore + */ + private function fieldJsonByteSize($field) + { + $size = 0; + if ($field->isMap()) { + $getter = $field->getGetter(); + $values = $this->$getter(); + $count = count($values); + if ($count !== 0) { + $size += 5; // size for "\"\":{}". + $size += strlen($field->getJsonName()); // size for field name + $size += $count - 1; // size for commas + $getter = $field->getGetter(); + $map_entry = $field->getMessageType(); + $key_field = $map_entry->getFieldByNumber(1); + $value_field = $map_entry->getFieldByNumber(2); + switch ($key_field->getType()) { + case GPBType::STRING: + case GPBType::SFIXED64: + case GPBType::INT64: + case GPBType::SINT64: + case GPBType::FIXED64: + case GPBType::UINT64: + $additional_quote = false; + break; + default: + $additional_quote = true; + } + foreach ($values as $key => $value) { + if ($additional_quote) { + $size += 2; // size for "" + } + $size += $this->fieldDataOnlyJsonByteSize($key_field, $key); + $size += $this->fieldDataOnlyJsonByteSize($value_field, $value); + $size += 1; // size for : + } + } + } elseif ($field->isRepeated()) { + $getter = $field->getGetter(); + $values = $this->$getter(); + $count = count($values); + if ($count !== 0) { + $size += 5; // size for "\"\":[]". + $size += strlen($field->getJsonName()); // size for field name + $size += $count - 1; // size for commas + $getter = $field->getGetter(); + foreach ($values as $value) { + $size += $this->fieldDataOnlyJsonByteSize($field, $value); + } + } + } elseif ($this->existField($field)) { + $size += 3; // size for "\"\":". + $size += strlen($field->getJsonName()); // size for field name + $getter = $field->getGetter(); + $value = $this->$getter(); + $size += $this->fieldDataOnlyJsonByteSize($field, $value); + } + return $size; + } + /** * @ignore */ @@ -921,4 +1435,28 @@ class Message $this->$setter($field_arr_value); } } + + /** + * @ignore + */ + public function jsonByteSize() + { + $size = 0; + + // Size for "{}". + $size += 2; + + $fields = $this->desc->getField(); + $count = 0; + foreach ($fields as $field) { + $field_size = $this->fieldJsonByteSize($field); + $size += $field_size; + if ($field_size != 0) { + $count++; + } + } + // size for comma + $size += $count > 0 ? ($count - 1) : 0; + return $size; + } } diff --git a/php/src/Google/Protobuf/Internal/OutputStream.php b/php/src/Google/Protobuf/Internal/OutputStream.php deleted file mode 100644 index 8c6d9b68..00000000 --- a/php/src/Google/Protobuf/Internal/OutputStream.php +++ /dev/null @@ -1,159 +0,0 @@ -current = 0; - $this->buffer_size = $size; - $this->buffer = str_repeat(chr(0), $this->buffer_size); - } - - public function getData() - { - return $this->buffer; - } - - public function writeVarint32($value) - { - $bytes = str_repeat(chr(0), self::MAX_VARINT64_BYTES); - $size = self::writeVarintToArray($value, $bytes); - return $this->writeRaw($bytes, $size); - } - - public function writeVarint64($value) - { - $bytes = str_repeat(chr(0), self::MAX_VARINT64_BYTES); - $size = self::writeVarintToArray($value, $bytes); - return $this->writeRaw($bytes, $size); - } - - public function writeLittleEndian32($value) - { - $bytes = str_repeat(chr(0), 4); - $size = self::writeLittleEndian32ToArray($value, $bytes); - return $this->writeRaw($bytes, $size); - } - - public function writeLittleEndian64($value) - { - $bytes = str_repeat(chr(0), 8); - $size = self::writeLittleEndian64ToArray($value, $bytes); - return $this->writeRaw($bytes, $size); - } - - public function writeTag($tag) - { - return $this->writeVarint32($tag); - } - - public function writeRaw($data, $size) - { - if ($this->buffer_size < $size) { - trigger_error("Output stream doesn't have enough buffer."); - return false; - } - - for ($i = 0; $i < $size; $i++) { - $this->buffer[$this->current] = $data[$i]; - $this->current++; - $this->buffer_size--; - } - return true; - } - - private static function writeVarintToArray($value, &$buffer) - { - $current = 0; - - $high = 0; - $low = 0; - if (PHP_INT_SIZE == 4) { - GPBUtil::divideInt64ToInt32($value, $high, $low); - } else { - $low = $value; - } - - while ($low >= 0x80 || $low < 0) { - $buffer[$current] = chr($low | 0x80); - $value = ($value >> 7) & ~(0x7F << ((PHP_INT_SIZE << 3) - 7)); - $carry = ($high & 0x7F) << ((PHP_INT_SIZE << 3) - 7); - $high = ($high >> 7) & ~(0x7F << ((PHP_INT_SIZE << 3) - 7)); - $low = (($low >> 7) & ~(0x7F << ((PHP_INT_SIZE << 3) - 7)) | $carry); - $current++; - } - $buffer[$current] = chr($low); - return $current + 1; - } - - private static function writeLittleEndian32ToArray($value, &$buffer) - { - $buffer[0] = chr($value & 0x000000FF); - $buffer[1] = chr(($value >> 8) & 0x000000FF); - $buffer[2] = chr(($value >> 16) & 0x000000FF); - $buffer[3] = chr(($value >> 24) & 0x000000FF); - return 4; - } - - private static function writeLittleEndian64ToArray($value, &$buffer) - { - $high = 0; - $low = 0; - if (PHP_INT_SIZE == 4) { - GPBUtil::divideInt64ToInt32($value, $high, $low); - } else { - $low = $value & 0xFFFFFFFF; - $high = ($value >> 32) & 0xFFFFFFFF; - } - - $buffer[0] = chr($low & 0x000000FF); - $buffer[1] = chr(($low >> 8) & 0x000000FF); - $buffer[2] = chr(($low >> 16) & 0x000000FF); - $buffer[3] = chr(($low >> 24) & 0x000000FF); - $buffer[4] = chr($high & 0x000000FF); - $buffer[5] = chr(($high >> 8) & 0x000000FF); - $buffer[6] = chr(($high >> 16) & 0x000000FF); - $buffer[7] = chr(($high >> 24) & 0x000000FF); - return 8; - } - -} diff --git a/php/src/Google/Protobuf/Internal/RawInputStream.php b/php/src/Google/Protobuf/Internal/RawInputStream.php new file mode 100644 index 00000000..4e7ed5cb --- /dev/null +++ b/php/src/Google/Protobuf/Internal/RawInputStream.php @@ -0,0 +1,50 @@ +buffer = $buffer; + } + + public function getData() + { + return $this->buffer; + } + +} -- cgit v1.2.3