Commit 174c82d8 authored by Jack Wakefield's avatar Jack Wakefield Committed by Paul Yang

Add well-known timestamps to JSON for PHP (#3564)

parent 2ad5c0a8
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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 &&
......
......@@ -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);
}
}
......@@ -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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment