--- a/lib/Protobuf-PHP/library/DrSlump/Protobuf/Codec/Binary.php +++ b/lib/Protobuf-PHP/library/DrSlump/Protobuf/Codec/Binary.php @@ -1,1 +1,389 @@ - +encodeMessage($message); + } + + /** + * @param String|Message $message + * @param String $data + * @return \DrSlump\Protobuf\Message + */ + public function decode(Protobuf\Message $message, $data) + { + return $this->decodeMessage($message, $data); + } + + + protected function encodeMessage(Protobuf\Message $message) + { + $writer = new Binary\Writer(); + + // Get message descriptor + $descriptor = Protobuf::getRegistry()->getDescriptor($message); + + foreach ($descriptor->getFields() as $tag=>$field) { + + $empty = !$message->_has($tag); + if ($field->isRequired() && $empty) { + throw new \UnexpectedValueException( + 'Message ' . get_class($message) . '\'s field tag ' . $tag . '(' . $field->getName() . ') is required but has no value' + ); + } + + // Skip empty fields + if ($empty) { + continue; + } + + $type = $field->getType(); + $wire = $field->isPacked() ? self::WIRE_LENGTH : $this->getWireType($type, null); + + // Compute key with tag number and wire type + $key = $tag << 3 | $wire; + + $value = $message->_get($tag); + + if ($field->isRepeated()) { + + // Packed fields are encoded as a length-delimited stream containing + // the concatenated encoding of each value. + if ($field->isPacked() && !empty($value)) { + $subwriter = new Binary\Writer(); + foreach($value as $val) { + $this->encodeSimpleType($subwriter, $type, $val); + } + $data = $subwriter->getBytes(); + $writer->varint($key); + $writer->varint(strlen($data)); + $writer->write($data); + } else { + foreach($value as $val) { + if ($type !== Protobuf::TYPE_MESSAGE) { + $writer->varint($key); + $this->encodeSimpleType($writer, $type, $val); + } else { + $writer->varint($key); + $data = $this->encodeMessage($val); + $writer->varint(strlen($data)); + $writer->write($data); + } + } + } + } else { + if ($type !== Protobuf::TYPE_MESSAGE) { + $writer->varint($key); + $this->encodeSimpleType($writer, $type, $value); + } else { + $writer->varint($key); + $data = $this->encodeMessage($value); + $writer->varint(strlen($data)); + $writer->write($data); + } + } + } + + return $writer->getBytes(); + } + + protected function encodeSimpleType($writer, $type, $value) + { + switch ($type) { + case Protobuf::TYPE_INT32: + case Protobuf::TYPE_INT64: + case Protobuf::TYPE_UINT64: + case Protobuf::TYPE_UINT32: + $writer->varint($value); + break; + + case Protobuf::TYPE_SINT32: // ZigZag + $writer->zigzag($value, 32); + break; + + case Protobuf::TYPE_SINT64 : // ZigZag + $writer->zigzag($value, 64); + break; + + case Protobuf::TYPE_DOUBLE: + $writer->double($value); + break; + case Protobuf::TYPE_FIXED64: + $writer->fixed64($value); + break; + case Protobuf::TYPE_SFIXED64: + $writer->sFixed64($value); + break; + + case Protobuf::TYPE_FLOAT: + $writer->float($value); + break; + case Protobuf::TYPE_FIXED32: + $writer->fixed32($value); + break; + case Protobuf::TYPE_SFIXED32: + $writer->sFixed32($value); + break; + + case Protobuf::TYPE_BOOL: + $writer->varint($value ? 1 : 0); + break; + + case Protobuf::TYPE_STRING: + case Protobuf::TYPE_BYTES: + $writer->varint(strlen($value)); + $writer->write($value); + break; + + case Protobuf::TYPE_MESSAGE: + // Messages are not supported in this method + return null; + + case Protobuf::TYPE_ENUM: + $writer->varint($value); + break; + + default: + throw new \Exception('Unknown field type ' . $type); + } + } + + + protected function decodeMessage(\DrSlump\Protobuf\Message $message, $data) + { + /** @var $message \DrSlump\Protobuf\Message */ + /** @var $descriptor \DrSlump\Protobuf\Descriptor */ + + // Create a binary reader for the data + $reader = new Protobuf\Codec\Binary\Reader($data); + + // Get message descriptor + $descriptor = Protobuf::getRegistry()->getDescriptor($message); + + while (!$reader->eof()) { + + // Get initial varint with tag number and wire type + $key = $reader->varint(); + if ($reader->eof()) break; + + $wire = $key & 0x7; + $tag = $key >> 3; + + // Find the matching field for the tag number + $field = $descriptor->getField($tag); + if (!$field) { + $data = $this->decodeUnknown($reader, $wire); + $unknown = new Binary\Unknown($tag, $wire, $data); + $message->addUnknown($unknown); + continue; + } + + $type = $field->getType(); + + // Check if we are dealing with a packed stream, we cannot rely on the packed + // flag of the message since we cannot be certain if the creator of the message + // was using it. + if ($wire === self::WIRE_LENGTH && $field->isRepeated() && $this->isPackable($type)) { + $length = $reader->varint(); + $until = $reader->pos() + $length; + while ($reader->pos() < $until) { + $item = $this->decodeSimpleType($reader, $type, self::WIRE_VARINT); + $message->_add($tag, $item); + } + + } else { + + // Assert wire and type match + $this->assertWireType($wire, $type); + + // Check if it's a sub-message + if ($type === Protobuf::TYPE_MESSAGE) { + $submessage = $field->getReference(); + $submessage = new $submessage; + + $length = $this->decodeSimpleType($reader, Protobuf::TYPE_INT64, self::WIRE_VARINT); + $data = $reader->read($length); + + $value = $this->decodeMessage($submessage, $data); + } else { + $value = $this->decodeSimpleType($reader, $type, $wire); + } + + // Support non-packed repeated fields + if ($field->isRepeated()) { + $message->_add($tag, $value); + } else { + $message->_set($tag, $value); + } + } + } + + return $message; + } + + protected function isPackable($type) + { + static $packable = array( + Protobuf::TYPE_INT64, + Protobuf::TYPE_UINT64, + Protobuf::TYPE_INT32, + Protobuf::TYPE_UINT32, + Protobuf::TYPE_SINT32, + Protobuf::TYPE_SINT64, + Protobuf::TYPE_DOUBLE, + Protobuf::TYPE_FIXED64, + Protobuf::TYPE_SFIXED64, + Protobuf::TYPE_FLOAT, + Protobuf::TYPE_FIXED32, + Protobuf::TYPE_SFIXED32, + Protobuf::TYPE_BOOL, + Protobuf::TYPE_ENUM + ); + + return in_array($type, $packable); + } + + protected function decodeUnknown($reader, $wire) + { + switch ($wire) { + case self::WIRE_VARINT: + return $reader->varint(); + case self::WIRE_LENGTH: + $length = $reader->varint(); + return $reader->read($length); + case self::WIRE_FIXED32: + return $reader->fixed32(); + case self::WIRE_FIXED64: + return $reader->fixed64(); + case self::WIRE_GROUP_START: + case self::WIRE_GROUP_END: + throw new \RuntimeException('Groups are deprecated in Protocol Buffers and unsupported by this library'); + default: + throw new \RuntimeException('Unsupported wire type (' . $wire . ') while consuming unknown field'); + } + } + + protected function assertWireType($wire, $type) + { + $expected = $this->getWireType($type, $wire); + if ($wire !== $expected) { + throw new \RuntimeException("Expected wire type $expected but got $wire for type $type"); + } + } + + protected function getWireType($type, $default) + { + switch ($type) { + case Protobuf::TYPE_INT32: + case Protobuf::TYPE_INT64: + case Protobuf::TYPE_UINT32: + case Protobuf::TYPE_UINT64: + case Protobuf::TYPE_SINT32: + case Protobuf::TYPE_SINT64: + case Protobuf::TYPE_BOOL: + case Protobuf::TYPE_ENUM: + return self::WIRE_VARINT; + case Protobuf::TYPE_FIXED64: + case Protobuf::TYPE_SFIXED64: + case Protobuf::TYPE_DOUBLE: + return self::WIRE_FIXED64; + case Protobuf::TYPE_STRING: + case Protobuf::TYPE_BYTES: + case Protobuf::TYPE_MESSAGE: + return self::WIRE_LENGTH; + case Protobuf::TYPE_FIXED32: + case Protobuf::TYPE_SFIXED32: + case Protobuf::TYPE_FLOAT: + return self::WIRE_FIXED32; + default: + // Unknown fields just return the reported wire type + return $default; + } + } + + protected function decodeSimpleType($reader, $type, $wireType) + { + switch ($type) { + case Protobuf::TYPE_INT64: + case Protobuf::TYPE_UINT64: + case Protobuf::TYPE_INT32: + case Protobuf::TYPE_UINT32: + return $reader->varint(); + + case Protobuf::TYPE_SINT32: // ZigZag + return $reader->zigzag(); + case Protobuf::TYPE_SINT64: // ZigZag + return $reader->zigzag(); + case Protobuf::TYPE_DOUBLE: + return $reader->double(); + case Protobuf::TYPE_FIXED64: + return $reader->fixed64(); + case Protobuf::TYPE_SFIXED64: + return $reader->sFixed64(); + + case Protobuf::TYPE_FLOAT: + return $reader->float(); + case Protobuf::TYPE_FIXED32: + return $reader->fixed32(); + case Protobuf::TYPE_SFIXED32: + return $reader->sFixed32(); + + case Protobuf::TYPE_BOOL: + return (bool)$reader->varint(); + + case Protobuf::TYPE_STRING: + $length = $reader->varint(); + return $reader->read($length); + + case Protobuf::TYPE_MESSAGE: + // Messages are not supported in this method + return null; + + case Protobuf::TYPE_BYTES: + $length = $reader->varint(); + return $reader->read($length); + + case Protobuf::TYPE_ENUM: + return $reader->varint(); + + default: + // Unknown type, follow wire type rules + switch ($wireType) { + case self::WIRE_VARINT: + return $reader->varint(); + case self::WIRE_FIXED32: + return $reader->fixed32(); + case self::WIRE_FIXED64: + return $reader->fixed64(); + case self::WIRE_LENGTH: + $length = $reader->varint(); + return $reader->read($length); + case self::WIRE_GROUP_START: + case self::WIRE_GROUP_END: + throw new \RuntimeException('Group is deprecated and not supported'); + default: + throw new \RuntimeException('Unsupported wire type number ' . $wireType); + } + } + + } + +}