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