diff --git a/conformance/conformance_php.php b/conformance/conformance_php.php index b6e12c01e1..2e3f783150 100755 --- a/conformance/conformance_php.php +++ b/conformance/conformance_php.php @@ -38,6 +38,10 @@ require_once("GPBMetadata/Google/Protobuf/TestMessagesProto3.php"); use \Conformance\WireFormat; +if (!ini_get("date.timezone")) { + ini_set("date.timezone", "UTC"); +} + $test_count = 0; function doTest($request) diff --git a/conformance/failure_list_php.txt b/conformance/failure_list_php.txt index 2f91a3f589..c1713cb1ce 100644 --- a/conformance/failure_list_php.txt +++ b/conformance/failure_list_php.txt @@ -7,11 +7,6 @@ Recommended.Proto3.JsonInput.DurationHas3FractionalDigits.Validator Recommended.Proto3.JsonInput.DurationHas6FractionalDigits.Validator Recommended.Proto3.JsonInput.DurationHas9FractionalDigits.Validator Recommended.Proto3.JsonInput.DurationHasZeroFractionalDigit.Validator -Recommended.Proto3.JsonInput.TimestampHas3FractionalDigits.Validator -Recommended.Proto3.JsonInput.TimestampHas6FractionalDigits.Validator -Recommended.Proto3.JsonInput.TimestampHas9FractionalDigits.Validator -Recommended.Proto3.JsonInput.TimestampHasZeroFractionalDigit.Validator -Recommended.Proto3.JsonInput.TimestampZeroNormalized.Validator Required.DurationProtoInputTooLarge.JsonOutput Required.DurationProtoInputTooSmall.JsonOutput Required.Proto3.JsonInput.Any.JsonOutput @@ -82,16 +77,6 @@ Required.Proto3.JsonInput.RepeatedUint64Wrapper.JsonOutput Required.Proto3.JsonInput.RepeatedUint64Wrapper.ProtobufOutput Required.Proto3.JsonInput.Struct.JsonOutput Required.Proto3.JsonInput.Struct.ProtobufOutput -Required.Proto3.JsonInput.TimestampMaxValue.JsonOutput -Required.Proto3.JsonInput.TimestampMaxValue.ProtobufOutput -Required.Proto3.JsonInput.TimestampMinValue.JsonOutput -Required.Proto3.JsonInput.TimestampMinValue.ProtobufOutput -Required.Proto3.JsonInput.TimestampRepeatedValue.JsonOutput -Required.Proto3.JsonInput.TimestampRepeatedValue.ProtobufOutput -Required.Proto3.JsonInput.TimestampWithNegativeOffset.JsonOutput -Required.Proto3.JsonInput.TimestampWithNegativeOffset.ProtobufOutput -Required.Proto3.JsonInput.TimestampWithPositiveOffset.JsonOutput -Required.Proto3.JsonInput.TimestampWithPositiveOffset.ProtobufOutput Required.Proto3.JsonInput.ValueAcceptBool.JsonOutput Required.Proto3.JsonInput.ValueAcceptBool.ProtobufOutput Required.Proto3.JsonInput.ValueAcceptFloat.JsonOutput diff --git a/conformance/failure_list_php_c.txt b/conformance/failure_list_php_c.txt index 2e37884298..088708e9b5 100644 --- a/conformance/failure_list_php_c.txt +++ b/conformance/failure_list_php_c.txt @@ -19,11 +19,6 @@ Recommended.Proto3.JsonInput.StringEndsWithEscapeChar Recommended.Proto3.JsonInput.StringFieldSurrogateInWrongOrder Recommended.Proto3.JsonInput.StringFieldUnpairedHighSurrogate Recommended.Proto3.JsonInput.StringFieldUnpairedLowSurrogate -Recommended.Proto3.JsonInput.TimestampHas3FractionalDigits.Validator -Recommended.Proto3.JsonInput.TimestampHas6FractionalDigits.Validator -Recommended.Proto3.JsonInput.TimestampHas9FractionalDigits.Validator -Recommended.Proto3.JsonInput.TimestampHasZeroFractionalDigit.Validator -Recommended.Proto3.JsonInput.TimestampZeroNormalized.Validator Recommended.Proto3.JsonInput.Uint64FieldBeString.Validator Recommended.Proto3.ProtobufInput.OneofZeroBytes.JsonOutput Recommended.Proto3.ProtobufInput.OneofZeroBytes.ProtobufOutput @@ -160,16 +155,6 @@ Required.Proto3.JsonInput.StringFieldUnicodeEscapeWithLowercaseHexLetters.JsonOu Required.Proto3.JsonInput.StringFieldUnicodeEscapeWithLowercaseHexLetters.ProtobufOutput Required.Proto3.JsonInput.Struct.JsonOutput Required.Proto3.JsonInput.Struct.ProtobufOutput -Required.Proto3.JsonInput.TimestampMaxValue.JsonOutput -Required.Proto3.JsonInput.TimestampMaxValue.ProtobufOutput -Required.Proto3.JsonInput.TimestampMinValue.JsonOutput -Required.Proto3.JsonInput.TimestampMinValue.ProtobufOutput -Required.Proto3.JsonInput.TimestampRepeatedValue.JsonOutput -Required.Proto3.JsonInput.TimestampRepeatedValue.ProtobufOutput -Required.Proto3.JsonInput.TimestampWithNegativeOffset.JsonOutput -Required.Proto3.JsonInput.TimestampWithNegativeOffset.ProtobufOutput -Required.Proto3.JsonInput.TimestampWithPositiveOffset.JsonOutput -Required.Proto3.JsonInput.TimestampWithPositiveOffset.ProtobufOutput Required.Proto3.JsonInput.Uint32FieldMaxFloatValue.JsonOutput Required.Proto3.JsonInput.Uint32FieldMaxFloatValue.ProtobufOutput Required.Proto3.JsonInput.Uint64FieldMaxValue.JsonOutput diff --git a/php/src/Google/Protobuf/Internal/FieldDescriptor.php b/php/src/Google/Protobuf/Internal/FieldDescriptor.php index 1443c6fd0b..6644a2e053 100644 --- a/php/src/Google/Protobuf/Internal/FieldDescriptor.php +++ b/php/src/Google/Protobuf/Internal/FieldDescriptor.php @@ -181,6 +181,12 @@ class FieldDescriptor $this->getMessageType()->getOptions()->getMapEntry(); } + public function isTimestamp() + { + return $this->getType() == GPBType::MESSAGE && + $this->getMessageType()->getClass() === "Google\Protobuf\Timestamp"; + } + private static function isTypePackable($field_type) { return ($field_type !== GPBType::STRING && diff --git a/php/src/Google/Protobuf/Internal/GPBUtil.php b/php/src/Google/Protobuf/Internal/GPBUtil.php index 84e8ecf0cf..a27220a057 100644 --- a/php/src/Google/Protobuf/Internal/GPBUtil.php +++ b/php/src/Google/Protobuf/Internal/GPBUtil.php @@ -38,6 +38,9 @@ use Google\Protobuf\Internal\MapField; class GPBUtil { + const NANOS_PER_MILLISECOND = 1000000; + const NANOS_PER_MICROSECOND = 1000; + public static function divideInt64ToInt32($value, &$high, &$low, $trim = false) { $isNeg = (bccomp($value, 0) < 0); @@ -340,4 +343,81 @@ class GPBUtil } return $result; } + + public static function parseTimestamp($timestamp) + { + // prevent parsing timestamps containing with the non-existant year "0000" + // DateTime::createFromFormat parses without failing but as a nonsensical date + if (substr($timestamp, 0, 4) === "0000") { + throw new \Exception("Year cannot be zero."); + } + // prevent parsing timestamps ending with a lowercase z + if (substr($timestamp, -1, 1) === "z") { + throw new \Exception("Timezone cannot be a lowercase z."); + } + + $nanoseconds = 0; + $periodIndex = strpos($timestamp, "."); + if ($periodIndex !== false) { + $nanosecondsLength = 0; + // find the next non-numeric character in the timestamp to calculate + // the length of the nanoseconds text + for ($i = $periodIndex + 1, $length = strlen($timestamp); $i < $length; $i++) { + if (!is_numeric($timestamp[$i])) { + $nanosecondsLength = $i - ($periodIndex + 1); + break; + } + } + if ($nanosecondsLength % 3 !== 0) { + throw new \Exception("Nanoseconds must be disible by 3."); + } + if ($nanosecondsLength > 9) { + throw new \Exception("Nanoseconds must be in the range of 0 to 999,999,999 nanoseconds."); + } + if ($nanosecondsLength > 0) { + $nanoseconds = substr($timestamp, $periodIndex + 1, $nanosecondsLength); + $nanoseconds = intval($nanoseconds); + + // remove the nanoseconds and preceding period from the timestamp + $date = substr($timestamp, 0, $periodIndex - 1); + $timezone = substr($timestamp, $periodIndex + $nanosecondsLength); + $timestamp = $date.$timezone; + } + } + + $date = \DateTime::createFromFormat(\DateTime::RFC3339, $timestamp, new \DateTimeZone("UTC")); + if ($date === false) { + throw new \Exception("Invalid RFC 3339 timestamp."); + } + + $value = new \Google\Protobuf\Timestamp(); + $seconds = $date->format("U"); + $value->setSeconds($seconds); + $value->setNanos($nanoseconds); + return $value; + } + + public static function formatTimestamp($value) + { + $nanoseconds = static::getNanosecondsForTimestamp($value->getNanos()); + if (!empty($nanoseconds)) { + $nanoseconds = ".".$nanoseconds; + } + $date = new \DateTime('@'.$value->getSeconds(), new \DateTimeZone("UTC")); + return $date->format("Y-m-d\TH:i:s".$nanoseconds."\Z"); + } + + public static function getNanosecondsForTimestamp($nanoseconds) + { + if ($nanoseconds == 0) { + return ''; + } + if ($nanoseconds % static::NANOS_PER_MILLISECOND == 0) { + return sprintf('%03d', $nanoseconds / static::NANOS_PER_MILLISECOND); + } + if ($nanoseconds % static::NANOS_PER_MICROSECOND == 0) { + return sprintf('%06d', $nanoseconds / static::NANOS_PER_MICROSECOND); + } + return sprintf('%09d', $nanoseconds); + } } diff --git a/php/src/Google/Protobuf/Internal/Message.php b/php/src/Google/Protobuf/Internal/Message.php index 8886e61a3e..c0a3218bab 100644 --- a/php/src/Google/Protobuf/Internal/Message.php +++ b/php/src/Google/Protobuf/Internal/Message.php @@ -699,12 +699,25 @@ class Message 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") { + + if ($field->isTimestamp()) { + if (!is_string($value)) { + throw new GPBDecodeException("Expect string."); + } + try { + $timestamp = GPBUtil::parseTimestamp($value); + } catch (\Exception $e) { + throw new GPBDecodeException("Invalid RFC 3339 timestamp: ".$e->getMessage()); + } + + $submsg->setSeconds($timestamp->getSeconds()); + $submsg->setNanos($timestamp->getNanos()); + } else if ($klass !== "Google\Protobuf\Any") { + if (!is_object($value) && !is_array($value)) { + throw new GPBDecodeException("Expect message."); + } + $submsg->mergeFromJsonArray($value); } return $submsg; @@ -1038,22 +1051,28 @@ class Message */ 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; + if (get_class($this) === 'Google\Protobuf\Timestamp') { + $timestamp = GPBUtil::formatTimestamp($this); + $timestamp = json_encode($timestamp); + $output->writeRaw($timestamp, strlen($timestamp)); + } else { + $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); } - $output->writeRaw("}", 1); return true; } @@ -1341,6 +1360,7 @@ class Message private function fieldJsonByteSize($field) { $size = 0; + if ($field->isMap()) { $getter = $field->getGetter(); $values = $this->$getter(); @@ -1443,21 +1463,26 @@ class Message 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++; + if (get_class($this) === 'Google\Protobuf\Timestamp') { + $timestamp = GPBUtil::formatTimestamp($this); + $timestamp = json_encode($timestamp); + $size += strlen($timestamp); + } else { + // 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; } - // size for comma - $size += $count > 0 ? ($count - 1) : 0; return $size; } }