1<?php
2
3// Protocol Buffers - Google's data interchange format
4// Copyright 2008 Google Inc.  All rights reserved.
5// https://developers.google.com/protocol-buffers/
6//
7// Redistribution and use in source and binary forms, with or without
8// modification, are permitted provided that the following conditions are
9// met:
10//
11//     * Redistributions of source code must retain the above copyright
12// notice, this list of conditions and the following disclaimer.
13//     * Redistributions in binary form must reproduce the above
14// copyright notice, this list of conditions and the following disclaimer
15// in the documentation and/or other materials provided with the
16// distribution.
17//     * Neither the name of Google Inc. nor the names of its
18// contributors may be used to endorse or promote products derived from
19// this software without specific prior written permission.
20//
21// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
33namespace Google\Protobuf\Internal;
34
35use Google\Protobuf\Duration;
36use Google\Protobuf\FieldMask;
37use Google\Protobuf\Internal\GPBType;
38use Google\Protobuf\Internal\RepeatedField;
39use Google\Protobuf\Internal\MapField;
40
41function camel2underscore($input) {
42    preg_match_all(
43        '!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!',
44        $input,
45        $matches);
46    $ret = $matches[0];
47    foreach ($ret as &$match) {
48        $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
49    }
50    return implode('_', $ret);
51}
52
53class GPBUtil
54{
55    const NANOS_PER_MILLISECOND = 1000000;
56    const NANOS_PER_MICROSECOND = 1000;
57    const TYPE_URL_PREFIX = 'type.googleapis.com/';
58
59    public static function divideInt64ToInt32($value, &$high, &$low, $trim = false)
60    {
61        $isNeg = (bccomp($value, 0) < 0);
62        if ($isNeg) {
63            $value = bcsub(0, $value);
64        }
65
66        $high = bcdiv($value, 4294967296);
67        $low = bcmod($value, 4294967296);
68        if (bccomp($high, 2147483647) > 0) {
69            $high = (int) bcsub($high, 4294967296);
70        } else {
71            $high = (int) $high;
72        }
73        if (bccomp($low, 2147483647) > 0) {
74            $low = (int) bcsub($low, 4294967296);
75        } else {
76            $low = (int) $low;
77        }
78
79        if ($isNeg) {
80            $high = ~$high;
81            $low = ~$low;
82            $low++;
83            if (!$low) {
84                $high = (int)($high + 1);
85            }
86        }
87
88        if ($trim) {
89            $high = 0;
90        }
91    }
92
93    public static function checkString(&$var, $check_utf8)
94    {
95        if (is_array($var) || is_object($var)) {
96            throw new \InvalidArgumentException("Expect string.");
97        }
98        if (!is_string($var)) {
99            $var = strval($var);
100        }
101        if ($check_utf8 && !preg_match('//u', $var)) {
102            throw new \Exception("Expect utf-8 encoding.");
103        }
104    }
105
106    public static function checkEnum(&$var)
107    {
108      static::checkInt32($var);
109    }
110
111    public static function checkInt32(&$var)
112    {
113        if (is_numeric($var)) {
114            $var = intval($var);
115        } else {
116            throw new \Exception("Expect integer.");
117        }
118    }
119
120    public static function checkUint32(&$var)
121    {
122        if (is_numeric($var)) {
123            if (PHP_INT_SIZE === 8) {
124                $var = intval($var);
125                $var |= ((-(($var >> 31) & 0x1)) & ~0xFFFFFFFF);
126            } else {
127                if (bccomp($var, 0x7FFFFFFF) > 0) {
128                    $var = bcsub($var, "4294967296");
129                }
130                $var = (int) $var;
131            }
132        } else {
133            throw new \Exception("Expect integer.");
134        }
135    }
136
137    public static function checkInt64(&$var)
138    {
139        if (is_numeric($var)) {
140            if (PHP_INT_SIZE == 8) {
141                $var = intval($var);
142            } else {
143                if (is_float($var) ||
144                    is_integer($var) ||
145                    (is_string($var) &&
146                         bccomp($var, "9223372036854774784") < 0)) {
147                    $var = number_format($var, 0, ".", "");
148                }
149            }
150        } else {
151            throw new \Exception("Expect integer.");
152        }
153    }
154
155    public static function checkUint64(&$var)
156    {
157        if (is_numeric($var)) {
158            if (PHP_INT_SIZE == 8) {
159                $var = intval($var);
160            } else {
161                $var = number_format($var, 0, ".", "");
162            }
163        } else {
164            throw new \Exception("Expect integer.");
165        }
166    }
167
168    public static function checkFloat(&$var)
169    {
170        if (is_float($var) || is_numeric($var)) {
171            $var = floatval($var);
172        } else {
173            throw new \Exception("Expect float.");
174        }
175    }
176
177    public static function checkDouble(&$var)
178    {
179        if (is_float($var) || is_numeric($var)) {
180            $var = floatval($var);
181        } else {
182            throw new \Exception("Expect float.");
183        }
184    }
185
186    public static function checkBool(&$var)
187    {
188        if (is_array($var) || is_object($var)) {
189            throw new \Exception("Expect boolean.");
190        }
191        $var = boolval($var);
192    }
193
194    public static function checkMessage(&$var, $klass, $newClass = null)
195    {
196        if (!$var instanceof $klass && !is_null($var)) {
197            throw new \Exception("Expect $klass.");
198        }
199    }
200
201    public static function checkRepeatedField(&$var, $type, $klass = null)
202    {
203        if (!$var instanceof RepeatedField && !is_array($var)) {
204            throw new \Exception("Expect array.");
205        }
206        if (is_array($var)) {
207            $tmp = new RepeatedField($type, $klass);
208            foreach ($var as $value) {
209                $tmp[] = $value;
210            }
211            return $tmp;
212        } else {
213            if ($var->getType() != $type) {
214                throw new \Exception(
215                    "Expect repeated field of different type.");
216            }
217            if ($var->getType() === GPBType::MESSAGE &&
218                $var->getClass() !== $klass &&
219                $var->getLegacyClass() !== $klass) {
220                throw new \Exception(
221                    "Expect repeated field of " . $klass . ".");
222            }
223            return $var;
224        }
225    }
226
227    public static function checkMapField(&$var, $key_type, $value_type, $klass = null)
228    {
229        if (!$var instanceof MapField && !is_array($var)) {
230            throw new \Exception("Expect dict.");
231        }
232        if (is_array($var)) {
233            $tmp = new MapField($key_type, $value_type, $klass);
234            foreach ($var as $key => $value) {
235                $tmp[$key] = $value;
236            }
237            return $tmp;
238        } else {
239            if ($var->getKeyType() != $key_type) {
240                throw new \Exception("Expect map field of key type.");
241            }
242            if ($var->getValueType() != $value_type) {
243                throw new \Exception("Expect map field of value type.");
244            }
245            if ($var->getValueType() === GPBType::MESSAGE &&
246                $var->getValueClass() !== $klass &&
247                $var->getLegacyValueClass() !== $klass) {
248                throw new \Exception(
249                    "Expect map field of " . $klass . ".");
250            }
251            return $var;
252        }
253    }
254
255    public static function Int64($value)
256    {
257        return new Int64($value);
258    }
259
260    public static function Uint64($value)
261    {
262        return new Uint64($value);
263    }
264
265    public static function getClassNamePrefix(
266        $classname,
267        $file_proto)
268    {
269        $option = $file_proto->getOptions();
270        $prefix = is_null($option) ? "" : $option->getPhpClassPrefix();
271        if ($prefix !== "") {
272            return $prefix;
273        }
274
275        $reserved_words = array(
276            "abstract"=>0, "and"=>0, "array"=>0, "as"=>0, "break"=>0,
277            "callable"=>0, "case"=>0, "catch"=>0, "class"=>0, "clone"=>0,
278            "const"=>0, "continue"=>0, "declare"=>0, "default"=>0, "die"=>0,
279            "do"=>0, "echo"=>0, "else"=>0, "elseif"=>0, "empty"=>0,
280            "enddeclare"=>0, "endfor"=>0, "endforeach"=>0, "endif"=>0,
281            "endswitch"=>0, "endwhile"=>0, "eval"=>0, "exit"=>0, "extends"=>0,
282            "final"=>0, "for"=>0, "foreach"=>0, "function"=>0, "global"=>0,
283            "goto"=>0, "if"=>0, "implements"=>0, "include"=>0,
284            "include_once"=>0, "instanceof"=>0, "insteadof"=>0, "interface"=>0,
285            "isset"=>0, "list"=>0, "namespace"=>0, "new"=>0, "or"=>0,
286            "print"=>0, "private"=>0, "protected"=>0, "public"=>0, "require"=>0,
287            "require_once"=>0, "return"=>0, "static"=>0, "switch"=>0,
288            "throw"=>0, "trait"=>0, "try"=>0, "unset"=>0, "use"=>0, "var"=>0,
289            "while"=>0, "xor"=>0, "int"=>0, "float"=>0, "bool"=>0, "string"=>0,
290            "true"=>0, "false"=>0, "null"=>0, "void"=>0, "iterable"=>0
291        );
292
293        if (array_key_exists(strtolower($classname), $reserved_words)) {
294            if ($file_proto->getPackage() === "google.protobuf") {
295                return "GPB";
296            } else {
297                return "PB";
298            }
299        }
300
301        return "";
302    }
303
304    public static function getLegacyClassNameWithoutPackage(
305        $name,
306        $file_proto)
307    {
308        $classname = implode('_', explode('.', $name));
309        return static::getClassNamePrefix($classname, $file_proto) . $classname;
310    }
311
312    public static function getClassNameWithoutPackage(
313        $name,
314        $file_proto)
315    {
316        $parts = explode('.', $name);
317        foreach ($parts as $i => $part) {
318            $parts[$i] = static::getClassNamePrefix($parts[$i], $file_proto) . $parts[$i];
319        }
320        return implode('\\', $parts);
321    }
322
323    public static function getFullClassName(
324        $proto,
325        $containing,
326        $file_proto,
327        &$message_name_without_package,
328        &$classname,
329        &$legacy_classname,
330        &$fullname)
331    {
332        // Full name needs to start with '.'.
333        $message_name_without_package = $proto->getName();
334        if ($containing !== "") {
335            $message_name_without_package =
336                $containing . "." . $message_name_without_package;
337        }
338
339        $package = $file_proto->getPackage();
340        if ($package === "") {
341            $fullname = "." . $message_name_without_package;
342        } else {
343            $fullname = "." . $package . "." . $message_name_without_package;
344        }
345
346        $class_name_without_package =
347            static::getClassNameWithoutPackage($message_name_without_package, $file_proto);
348        $legacy_class_name_without_package =
349            static::getLegacyClassNameWithoutPackage(
350                $message_name_without_package, $file_proto);
351
352        $option = $file_proto->getOptions();
353        if (!is_null($option) && $option->hasPhpNamespace()) {
354            $namespace = $option->getPhpNamespace();
355            if ($namespace !== "") {
356                $classname = $namespace . "\\" . $class_name_without_package;
357                $legacy_classname =
358                    $namespace . "\\" . $legacy_class_name_without_package;
359                return;
360            } else {
361                $classname = $class_name_without_package;
362                $legacy_classname = $legacy_class_name_without_package;
363                return;
364            }
365        }
366
367        if ($package === "") {
368            $classname = $class_name_without_package;
369            $legacy_classname = $legacy_class_name_without_package;
370        } else {
371            $parts = array_map('ucwords', explode('.', $package));
372            foreach ($parts as $i => $part) {
373                $parts[$i] = self::getClassNamePrefix($part, $file_proto).$part;
374            }
375            $classname =
376                implode('\\', $parts) .
377                "\\".self::getClassNamePrefix($class_name_without_package,$file_proto).
378                $class_name_without_package;
379            $legacy_classname =
380                implode('\\', array_map('ucwords', explode('.', $package))).
381                "\\".$legacy_class_name_without_package;
382        }
383    }
384
385    public static function combineInt32ToInt64($high, $low)
386    {
387        $isNeg = $high < 0;
388        if ($isNeg) {
389            $high = ~$high;
390            $low = ~$low;
391            $low++;
392            if (!$low) {
393                $high = (int) ($high + 1);
394            }
395        }
396        $result = bcadd(bcmul($high, 4294967296), $low);
397        if ($low < 0) {
398            $result = bcadd($result, 4294967296);
399        }
400        if ($isNeg) {
401          $result = bcsub(0, $result);
402        }
403        return $result;
404    }
405
406    public static function parseTimestamp($timestamp)
407    {
408        // prevent parsing timestamps containing with the non-existant year "0000"
409        // DateTime::createFromFormat parses without failing but as a nonsensical date
410        if (substr($timestamp, 0, 4) === "0000") {
411            throw new \Exception("Year cannot be zero.");
412        }
413        // prevent parsing timestamps ending with a lowercase z
414        if (substr($timestamp, -1, 1) === "z") {
415            throw new \Exception("Timezone cannot be a lowercase z.");
416        }
417
418        $nanoseconds = 0;
419        $periodIndex = strpos($timestamp, ".");
420        if ($periodIndex !== false) {
421            $nanosecondsLength = 0;
422            // find the next non-numeric character in the timestamp to calculate
423            // the length of the nanoseconds text
424            for ($i = $periodIndex + 1, $length = strlen($timestamp); $i < $length; $i++) {
425                if (!is_numeric($timestamp[$i])) {
426                    $nanosecondsLength = $i - ($periodIndex + 1);
427                    break;
428                }
429            }
430            if ($nanosecondsLength % 3 !== 0) {
431                throw new \Exception("Nanoseconds must be disible by 3.");
432            }
433            if ($nanosecondsLength > 9) {
434                throw new \Exception("Nanoseconds must be in the range of 0 to 999,999,999 nanoseconds.");
435            }
436            if ($nanosecondsLength > 0) {
437                $nanoseconds = substr($timestamp, $periodIndex + 1, $nanosecondsLength);
438                $nanoseconds = intval($nanoseconds);
439
440                // remove the nanoseconds and preceding period from the timestamp
441                $date = substr($timestamp, 0, $periodIndex);
442                $timezone = substr($timestamp, $periodIndex + $nanosecondsLength + 1);
443                $timestamp = $date.$timezone;
444            }
445        }
446
447        $date = \DateTime::createFromFormat(\DateTime::RFC3339, $timestamp, new \DateTimeZone("UTC"));
448        if ($date === false) {
449            throw new \Exception("Invalid RFC 3339 timestamp.");
450        }
451
452        $value = new \Google\Protobuf\Timestamp();
453        $seconds = $date->format("U");
454        $value->setSeconds($seconds);
455        $value->setNanos($nanoseconds);
456        return $value;
457    }
458
459    public static function formatTimestamp($value)
460    {
461        if (bccomp($value->getSeconds(), "253402300800") != -1) {
462          throw new GPBDecodeException("Duration number too large.");
463        }
464        if (bccomp($value->getSeconds(), "-62135596801") != 1) {
465          throw new GPBDecodeException("Duration number too small.");
466        }
467        $nanoseconds = static::getNanosecondsForTimestamp($value->getNanos());
468        if (!empty($nanoseconds)) {
469            $nanoseconds = ".".$nanoseconds;
470        }
471        $date = new \DateTime('@'.$value->getSeconds(), new \DateTimeZone("UTC"));
472        return $date->format("Y-m-d\TH:i:s".$nanoseconds."\Z");
473    }
474
475    public static function parseDuration($value)
476    {
477        if (strlen($value) < 2 || substr($value, -1) !== "s") {
478          throw new GPBDecodeException("Missing s after duration string");
479        }
480        $number = substr($value, 0, -1);
481        if (bccomp($number, "315576000001") != -1) {
482          throw new GPBDecodeException("Duration number too large.");
483        }
484        if (bccomp($number, "-315576000001") != 1) {
485          throw new GPBDecodeException("Duration number too small.");
486        }
487        $pos = strrpos($number, ".");
488        if ($pos !== false) {
489            $seconds = substr($number, 0, $pos);
490            if (bccomp($seconds, 0) < 0) {
491                $nanos = bcmul("0" . substr($number, $pos), -1000000000);
492            } else {
493                $nanos = bcmul("0" . substr($number, $pos), 1000000000);
494            }
495        } else {
496            $seconds = $number;
497            $nanos = 0;
498        }
499        $duration = new Duration();
500        $duration->setSeconds($seconds);
501        $duration->setNanos($nanos);
502        return $duration;
503    }
504
505    public static function formatDuration($value)
506    {
507        if (bccomp($value->getSeconds(), '315576000001') != -1) {
508            throw new GPBDecodeException('Duration number too large.');
509        }
510        if (bccomp($value->getSeconds(), '-315576000001') != 1) {
511            throw new GPBDecodeException('Duration number too small.');
512        }
513
514        $nanos = $value->getNanos();
515        if ($nanos === 0) {
516            return (string) $value->getSeconds();
517        }
518
519        if ($nanos % 1000000 === 0) {
520            $digits = 3;
521        } elseif ($nanos % 1000 === 0) {
522            $digits = 6;
523        } else {
524            $digits = 9;
525        }
526
527        $nanos = bcdiv($nanos, '1000000000', $digits);
528        return bcadd($value->getSeconds(), $nanos, $digits);
529    }
530
531    public static function parseFieldMask($paths_string)
532    {
533        $field_mask = new FieldMask();
534        if (strlen($paths_string) === 0) {
535            return $field_mask;
536        }
537        $path_strings = explode(",", $paths_string);
538        $paths = $field_mask->getPaths();
539        foreach($path_strings as &$path_string) {
540            $field_strings = explode(".", $path_string);
541            foreach($field_strings as &$field_string) {
542                $field_string = camel2underscore($field_string);
543            }
544            $path_string = implode(".", $field_strings);
545            $paths[] = $path_string;
546        }
547        return $field_mask;
548    }
549
550    public static function formatFieldMask($field_mask)
551    {
552        $converted_paths = [];
553        foreach($field_mask->getPaths() as $path) {
554            $fields = explode('.', $path);
555            $converted_path = [];
556            foreach ($fields as $field) {
557                $segments = explode('_', $field);
558                $start = true;
559                $converted_segments = "";
560                foreach($segments as $segment) {
561                  if (!$start) {
562                    $converted = ucfirst($segment);
563                  } else {
564                    $converted = $segment;
565                    $start = false;
566                  }
567                  $converted_segments .= $converted;
568                }
569                $converted_path []= $converted_segments;
570            }
571            $converted_path = implode(".", $converted_path);
572            $converted_paths []= $converted_path;
573        }
574        return implode(",", $converted_paths);
575    }
576
577    public static function getNanosecondsForTimestamp($nanoseconds)
578    {
579        if ($nanoseconds == 0) {
580            return '';
581        }
582        if ($nanoseconds % static::NANOS_PER_MILLISECOND == 0) {
583            return sprintf('%03d', $nanoseconds / static::NANOS_PER_MILLISECOND);
584        }
585        if ($nanoseconds % static::NANOS_PER_MICROSECOND == 0) {
586            return sprintf('%06d', $nanoseconds / static::NANOS_PER_MICROSECOND);
587        }
588        return sprintf('%09d', $nanoseconds);
589    }
590
591    public static function hasSpecialJsonMapping($msg)
592    {
593        return is_a($msg, 'Google\Protobuf\Any')         ||
594               is_a($msg, "Google\Protobuf\ListValue")   ||
595               is_a($msg, "Google\Protobuf\Struct")      ||
596               is_a($msg, "Google\Protobuf\Value")       ||
597               is_a($msg, "Google\Protobuf\Duration")    ||
598               is_a($msg, "Google\Protobuf\Timestamp")   ||
599               is_a($msg, "Google\Protobuf\FieldMask")   ||
600               static::hasJsonValue($msg);
601    }
602
603    public static function hasJsonValue($msg)
604    {
605        return is_a($msg, "Google\Protobuf\DoubleValue") ||
606               is_a($msg, "Google\Protobuf\FloatValue")  ||
607               is_a($msg, "Google\Protobuf\Int64Value")  ||
608               is_a($msg, "Google\Protobuf\UInt64Value") ||
609               is_a($msg, "Google\Protobuf\Int32Value")  ||
610               is_a($msg, "Google\Protobuf\UInt32Value") ||
611               is_a($msg, "Google\Protobuf\BoolValue")   ||
612               is_a($msg, "Google\Protobuf\StringValue") ||
613               is_a($msg, "Google\Protobuf\BytesValue");
614    }
615}
616