1<?php
2
3/**
4 * Identity function, returns its argument unmodified.
5 *
6 * This is useful almost exclusively as a workaround to an oddity in the PHP
7 * grammar -- this is a syntax error:
8 *
9 *    COUNTEREXAMPLE
10 *    new Thing()->doStuff();
11 *
12 * ...but this works fine:
13 *
14 *    id(new Thing())->doStuff();
15 *
16 * @param   wild Anything.
17 * @return  wild Unmodified argument.
18 */
19function id($x) {
20  return $x;
21}
22
23
24/**
25 * Access an array index, retrieving the value stored there if it exists or
26 * a default if it does not. This function allows you to concisely access an
27 * index which may or may not exist without raising a warning.
28 *
29 * @param   array   Array to access.
30 * @param   scalar  Index to access in the array.
31 * @param   wild    Default value to return if the key is not present in the
32 *                  array.
33 * @return  wild    If `$array[$key]` exists, that value is returned. If not,
34 *                  $default is returned without raising a warning.
35 */
36function idx(array $array, $key, $default = null) {
37  // isset() is a micro-optimization - it is fast but fails for null values.
38  if (isset($array[$key])) {
39    return $array[$key];
40  }
41
42  // Comparing $default is also a micro-optimization.
43  if ($default === null || array_key_exists($key, $array)) {
44    return null;
45  }
46
47  return $default;
48}
49
50
51/**
52 * Access a sequence of array indexes, retrieving a deeply nested value if
53 * it exists or a default if it does not.
54 *
55 * For example, `idxv($dict, array('a', 'b', 'c'))` accesses the key at
56 * `$dict['a']['b']['c']`, if it exists. If it does not, or any intermediate
57 * value is not itself an array, it returns the defualt value.
58 *
59 * @param array Array to access.
60 * @param list<string> List of keys to access, in sequence.
61 * @param wild Default value to return.
62 * @return wild Accessed value, or default if the value is not accessible.
63 */
64function idxv(array $map, array $path, $default = null) {
65  if (!$path) {
66    return $default;
67  }
68
69  $last = last($path);
70  $path = array_slice($path, 0, -1);
71
72  $cursor = $map;
73  foreach ($path as $key) {
74    $cursor = idx($cursor, $key);
75    if (!is_array($cursor)) {
76      return $default;
77    }
78  }
79
80  return idx($cursor, $last, $default);
81}
82
83
84/**
85 * Call a method on a list of objects. Short for "method pull", this function
86 * works just like @{function:ipull}, except that it operates on a list of
87 * objects instead of a list of arrays. This function simplifies a common type
88 * of mapping operation:
89 *
90 *    COUNTEREXAMPLE
91 *    $names = array();
92 *    foreach ($objects as $key => $object) {
93 *      $names[$key] = $object->getName();
94 *    }
95 *
96 * You can express this more concisely with mpull():
97 *
98 *    $names = mpull($objects, 'getName');
99 *
100 * mpull() takes a third argument, which allows you to do the same but for
101 * the array's keys:
102 *
103 *    COUNTEREXAMPLE
104 *    $names = array();
105 *    foreach ($objects as $object) {
106 *      $names[$object->getID()] = $object->getName();
107 *    }
108 *
109 * This is the mpull version():
110 *
111 *    $names = mpull($objects, 'getName', 'getID');
112 *
113 * If you pass ##null## as the second argument, the objects will be preserved:
114 *
115 *    COUNTEREXAMPLE
116 *    $id_map = array();
117 *    foreach ($objects as $object) {
118 *      $id_map[$object->getID()] = $object;
119 *    }
120 *
121 * With mpull():
122 *
123 *    $id_map = mpull($objects, null, 'getID');
124 *
125 * See also @{function:ipull}, which works similarly but accesses array indexes
126 * instead of calling methods.
127 *
128 * @param   list          Some list of objects.
129 * @param   string|null   Determines which **values** will appear in the result
130 *                        array. Use a string like 'getName' to store the
131 *                        value of calling the named method in each value, or
132 *                        ##null## to preserve the original objects.
133 * @param   string|null   Determines how **keys** will be assigned in the result
134 *                        array. Use a string like 'getID' to use the result
135 *                        of calling the named method as each object's key, or
136 *                        `null` to preserve the original keys.
137 * @return  dict          A dictionary with keys and values derived according
138 *                        to whatever you passed as `$method` and `$key_method`.
139 */
140function mpull(array $list, $method, $key_method = null) {
141  $result = array();
142  foreach ($list as $key => $object) {
143    if ($key_method !== null) {
144      $key = $object->$key_method();
145    }
146    if ($method !== null) {
147      $value = $object->$method();
148    } else {
149      $value = $object;
150    }
151    $result[$key] = $value;
152  }
153  return $result;
154}
155
156
157/**
158 * Access a property on a list of objects. Short for "property pull", this
159 * function works just like @{function:mpull}, except that it accesses object
160 * properties instead of methods. This function simplifies a common type of
161 * mapping operation:
162 *
163 *    COUNTEREXAMPLE
164 *    $names = array();
165 *    foreach ($objects as $key => $object) {
166 *      $names[$key] = $object->name;
167 *    }
168 *
169 * You can express this more concisely with ppull():
170 *
171 *    $names = ppull($objects, 'name');
172 *
173 * ppull() takes a third argument, which allows you to do the same but for
174 * the array's keys:
175 *
176 *    COUNTEREXAMPLE
177 *    $names = array();
178 *    foreach ($objects as $object) {
179 *      $names[$object->id] = $object->name;
180 *    }
181 *
182 * This is the ppull version():
183 *
184 *    $names = ppull($objects, 'name', 'id');
185 *
186 * If you pass ##null## as the second argument, the objects will be preserved:
187 *
188 *    COUNTEREXAMPLE
189 *    $id_map = array();
190 *    foreach ($objects as $object) {
191 *      $id_map[$object->id] = $object;
192 *    }
193 *
194 * With ppull():
195 *
196 *    $id_map = ppull($objects, null, 'id');
197 *
198 * See also @{function:mpull}, which works similarly but calls object methods
199 * instead of accessing object properties.
200 *
201 * @param   list          Some list of objects.
202 * @param   string|null   Determines which **values** will appear in the result
203 *                        array. Use a string like 'name' to store the value of
204 *                        accessing the named property in each value, or
205 *                        `null` to preserve the original objects.
206 * @param   string|null   Determines how **keys** will be assigned in the result
207 *                        array. Use a string like 'id' to use the result of
208 *                        accessing the named property as each object's key, or
209 *                        `null` to preserve the original keys.
210 * @return  dict          A dictionary with keys and values derived according
211 *                        to whatever you passed as `$property` and
212 *                        `$key_property`.
213 */
214function ppull(array $list, $property, $key_property = null) {
215  $result = array();
216  foreach ($list as $key => $object) {
217    if ($key_property !== null) {
218      $key = $object->$key_property;
219    }
220    if ($property !== null) {
221      $value = $object->$property;
222    } else {
223      $value = $object;
224    }
225    $result[$key] = $value;
226  }
227  return $result;
228}
229
230
231/**
232 * Choose an index from a list of arrays. Short for "index pull", this function
233 * works just like @{function:mpull}, except that it operates on a list of
234 * arrays and selects an index from them instead of operating on a list of
235 * objects and calling a method on them.
236 *
237 * This function simplifies a common type of mapping operation:
238 *
239 *    COUNTEREXAMPLE
240 *    $names = array();
241 *    foreach ($list as $key => $dict) {
242 *      $names[$key] = $dict['name'];
243 *    }
244 *
245 * With ipull():
246 *
247 *    $names = ipull($list, 'name');
248 *
249 * See @{function:mpull} for more usage examples.
250 *
251 * @param   list          Some list of arrays.
252 * @param   scalar|null   Determines which **values** will appear in the result
253 *                        array. Use a scalar to select that index from each
254 *                        array, or null to preserve the arrays unmodified as
255 *                        values.
256 * @param   scalar|null   Determines which **keys** will appear in the result
257 *                        array. Use a scalar to select that index from each
258 *                        array, or null to preserve the array keys.
259 * @return  dict          A dictionary with keys and values derived according
260 *                        to whatever you passed for `$index` and `$key_index`.
261 */
262function ipull(array $list, $index, $key_index = null) {
263  $result = array();
264  foreach ($list as $key => $array) {
265    if ($key_index !== null) {
266      $key = $array[$key_index];
267    }
268    if ($index !== null) {
269      $value = $array[$index];
270    } else {
271      $value = $array;
272    }
273    $result[$key] = $value;
274  }
275  return $result;
276}
277
278
279/**
280 * Group a list of objects by the result of some method, similar to how
281 * GROUP BY works in an SQL query. This function simplifies grouping objects
282 * by some property:
283 *
284 *    COUNTEREXAMPLE
285 *    $animals_by_species = array();
286 *    foreach ($animals as $animal) {
287 *      $animals_by_species[$animal->getSpecies()][] = $animal;
288 *    }
289 *
290 * This can be expressed more tersely with mgroup():
291 *
292 *    $animals_by_species = mgroup($animals, 'getSpecies');
293 *
294 * In either case, the result is a dictionary which maps species (e.g., like
295 * "dog") to lists of animals with that property, so all the dogs are grouped
296 * together and all the cats are grouped together, or whatever super
297 * businessesey thing is actually happening in your problem domain.
298 *
299 * See also @{function:igroup}, which works the same way but operates on
300 * array indexes.
301 *
302 * @param   list    List of objects to group by some property.
303 * @param   string  Name of a method, like 'getType', to call on each object
304 *                  in order to determine which group it should be placed into.
305 * @param   ...     Zero or more additional method names, to subgroup the
306 *                  groups.
307 * @return  dict    Dictionary mapping distinct method returns to lists of
308 *                  all objects which returned that value.
309 */
310function mgroup(array $list, $by /* , ... */) {
311  $map = mpull($list, $by);
312
313  $groups = array();
314  foreach ($map as $group) {
315    // Can't array_fill_keys() here because 'false' gets encoded wrong.
316    $groups[$group] = array();
317  }
318
319  foreach ($map as $key => $group) {
320    $groups[$group][$key] = $list[$key];
321  }
322
323  $args = func_get_args();
324  $args = array_slice($args, 2);
325  if ($args) {
326    array_unshift($args, null);
327    foreach ($groups as $group_key => $grouped) {
328      $args[0] = $grouped;
329      $groups[$group_key] = call_user_func_array('mgroup', $args);
330    }
331  }
332
333  return $groups;
334}
335
336
337/**
338 * Group a list of arrays by the value of some index. This function is the same
339 * as @{function:mgroup}, except it operates on the values of array indexes
340 * rather than the return values of method calls.
341 *
342 * @param   list    List of arrays to group by some index value.
343 * @param   string  Name of an index to select from each array in order to
344 *                  determine which group it should be placed into.
345 * @param   ...     Zero or more additional indexes names, to subgroup the
346 *                  groups.
347 * @return  dict    Dictionary mapping distinct index values to lists of
348 *                  all objects which had that value at the index.
349 */
350function igroup(array $list, $by /* , ... */) {
351  $map = ipull($list, $by);
352
353  $groups = array();
354  foreach ($map as $group) {
355    $groups[$group] = array();
356  }
357
358  foreach ($map as $key => $group) {
359    $groups[$group][$key] = $list[$key];
360  }
361
362  $args = func_get_args();
363  $args = array_slice($args, 2);
364  if ($args) {
365    array_unshift($args, null);
366    foreach ($groups as $group_key => $grouped) {
367      $args[0] = $grouped;
368      $groups[$group_key] = call_user_func_array('igroup', $args);
369    }
370  }
371
372  return $groups;
373}
374
375
376/**
377 * Sort a list of objects by the return value of some method. In PHP, this is
378 * often vastly more efficient than `usort()` and similar.
379 *
380 *    // Sort a list of Duck objects by name.
381 *    $sorted = msort($ducks, 'getName');
382 *
383 * It is usually significantly more efficient to define an ordering method
384 * on objects and call `msort()` than to write a comparator. It is often more
385 * convenient, as well.
386 *
387 * NOTE: This method does not take the list by reference; it returns a new list.
388 *
389 * @param   list    List of objects to sort by some property.
390 * @param   string  Name of a method to call on each object; the return values
391 *                  will be used to sort the list.
392 * @return  list    Objects ordered by the return values of the method calls.
393 */
394function msort(array $list, $method) {
395  $surrogate = mpull($list, $method);
396
397  // See T13303. A "PhutilSortVector" is technically a sortable object, so
398  // a method which returns a "PhutilSortVector" is suitable for use with
399  // "msort()". However, it's almost certain that the caller intended to use
400  // "msortv()", not "msort()", and forgot to add a "v". Treat this as an error.
401
402  if ($surrogate) {
403    $item = head($surrogate);
404    if ($item instanceof PhutilSortVector) {
405      throw new Exception(
406        pht(
407          'msort() was passed a method ("%s") which returns '.
408          '"PhutilSortVector" objects. Use "msortv()", not "msort()", to '.
409          'sort a list which produces vectors.',
410          $method));
411    }
412  }
413
414  asort($surrogate);
415
416  $result = array();
417  foreach ($surrogate as $key => $value) {
418    $result[$key] = $list[$key];
419  }
420
421  return $result;
422}
423
424
425/**
426 * Sort a list of objects by a sort vector.
427 *
428 * This sort is stable, well-behaved, and more efficient than `usort()`.
429 *
430 * @param list List of objects to sort.
431 * @param string Name of a method to call on each object. The method must
432 *   return a @{class:PhutilSortVector}.
433 * @return list Objects ordered by the vectors.
434 */
435function msortv(array $list, $method) {
436  return msortv_internal($list, $method, SORT_STRING);
437}
438
439function msortv_natural(array $list, $method) {
440  return msortv_internal($list, $method, SORT_NATURAL | SORT_FLAG_CASE);
441}
442
443function msortv_internal(array $list, $method, $flags) {
444  $surrogate = mpull($list, $method);
445
446  $index = 0;
447  foreach ($surrogate as $key => $value) {
448    if (!($value instanceof PhutilSortVector)) {
449      throw new Exception(
450        pht(
451          'Objects passed to "%s" must return sort vectors (objects of '.
452          'class "%s") from the specified method ("%s"). One object (with '.
453          'key "%s") did not.',
454          'msortv()',
455          'PhutilSortVector',
456          $method,
457          $key));
458    }
459
460    // Add the original index to keep the sort stable.
461    $value->addInt($index++);
462
463    $surrogate[$key] = (string)$value;
464  }
465
466  asort($surrogate, $flags);
467
468  $result = array();
469  foreach ($surrogate as $key => $value) {
470    $result[$key] = $list[$key];
471  }
472
473  return $result;
474}
475
476
477/**
478 * Sort a list of arrays by the value of some index. This method is identical to
479 * @{function:msort}, but operates on a list of arrays instead of a list of
480 * objects.
481 *
482 * @param   list    List of arrays to sort by some index value.
483 * @param   string  Index to access on each object; the return values
484 *                  will be used to sort the list.
485 * @return  list    Arrays ordered by the index values.
486 */
487function isort(array $list, $index) {
488  $surrogate = ipull($list, $index);
489
490  asort($surrogate);
491
492  $result = array();
493  foreach ($surrogate as $key => $value) {
494    $result[$key] = $list[$key];
495  }
496
497  return $result;
498}
499
500
501/**
502 * Filter a list of objects by executing a method across all the objects and
503 * filter out the ones with empty() results. this function works just like
504 * @{function:ifilter}, except that it operates on a list of objects instead
505 * of a list of arrays.
506 *
507 * For example, to remove all objects with no children from a list, where
508 * 'hasChildren' is a method name, do this:
509 *
510 *   mfilter($list, 'hasChildren');
511 *
512 * The optional third parameter allows you to negate the operation and filter
513 * out nonempty objects. To remove all objects that DO have children, do this:
514 *
515 *   mfilter($list, 'hasChildren', true);
516 *
517 * @param  array        List of objects to filter.
518 * @param  string       A method name.
519 * @param  bool         Optionally, pass true to drop objects which pass the
520 *                      filter instead of keeping them.
521 * @return array        List of objects which pass the filter.
522 */
523function mfilter(array $list, $method, $negate = false) {
524  if (!is_string($method)) {
525    throw new InvalidArgumentException(pht('Argument method is not a string.'));
526  }
527
528  $result = array();
529  foreach ($list as $key => $object) {
530    $value = $object->$method();
531
532    if (!$negate) {
533      if (!empty($value)) {
534        $result[$key] = $object;
535      }
536    } else {
537      if (empty($value)) {
538        $result[$key] = $object;
539      }
540    }
541  }
542
543  return $result;
544}
545
546
547/**
548 * Filter a list of arrays by removing the ones with an empty() value for some
549 * index. This function works just like @{function:mfilter}, except that it
550 * operates on a list of arrays instead of a list of objects.
551 *
552 * For example, to remove all arrays without value for key 'username', do this:
553 *
554 *   ifilter($list, 'username');
555 *
556 * The optional third parameter allows you to negate the operation and filter
557 * out nonempty arrays. To remove all arrays that DO have value for key
558 * 'username', do this:
559 *
560 *   ifilter($list, 'username', true);
561 *
562 * @param  array        List of arrays to filter.
563 * @param  scalar       The index.
564 * @param  bool         Optionally, pass true to drop arrays which pass the
565 *                      filter instead of keeping them.
566 * @return array        List of arrays which pass the filter.
567 */
568function ifilter(array $list, $index, $negate = false) {
569  if (!is_scalar($index)) {
570    throw new InvalidArgumentException(pht('Argument index is not a scalar.'));
571  }
572
573  $result = array();
574  if (!$negate) {
575    foreach ($list as $key => $array) {
576      if (!empty($array[$index])) {
577        $result[$key] = $array;
578      }
579    }
580  } else {
581    foreach ($list as $key => $array) {
582      if (empty($array[$index])) {
583        $result[$key] = $array;
584      }
585    }
586  }
587
588  return $result;
589}
590
591
592/**
593 * Selects a list of keys from an array, returning a new array with only the
594 * key-value pairs identified by the selected keys, in the specified order.
595 *
596 * Note that since this function orders keys in the result according to the
597 * order they appear in the list of keys, there are effectively two common
598 * uses: either reducing a large dictionary to a smaller one, or changing the
599 * key order on an existing dictionary.
600 *
601 * @param  dict    Dictionary of key-value pairs to select from.
602 * @param  list    List of keys to select.
603 * @return dict    Dictionary of only those key-value pairs where the key was
604 *                 present in the list of keys to select. Ordering is
605 *                 determined by the list order.
606 */
607function array_select_keys(array $dict, array $keys) {
608  $result = array();
609  foreach ($keys as $key) {
610    if (array_key_exists($key, $dict)) {
611      $result[$key] = $dict[$key];
612    }
613  }
614  return $result;
615}
616
617
618/**
619 * Checks if all values of array are instances of the passed class. Throws
620 * `InvalidArgumentException` if it isn't true for any value.
621 *
622 * @param  array
623 * @param  string  Name of the class or 'array' to check arrays.
624 * @return array   Returns passed array.
625 */
626function assert_instances_of(array $arr, $class) {
627  $is_array = !strcasecmp($class, 'array');
628
629  foreach ($arr as $key => $object) {
630    if ($is_array) {
631      if (!is_array($object)) {
632        $given = gettype($object);
633        throw new InvalidArgumentException(
634          pht(
635            "Array item with key '%s' must be of type array, %s given.",
636            $key,
637            $given));
638      }
639
640    } else if (!($object instanceof $class)) {
641      $given = gettype($object);
642      if (is_object($object)) {
643        $given = pht('instance of %s', get_class($object));
644      }
645      throw new InvalidArgumentException(
646        pht(
647          "Array item with key '%s' must be an instance of %s, %s given.",
648          $key,
649          $class,
650          $given));
651    }
652  }
653
654  return $arr;
655}
656
657/**
658 * Assert that two arrays have the exact same keys, in any order.
659 *
660 * @param map Array with expected keys.
661 * @param map Array with actual keys.
662 * @return void
663 */
664function assert_same_keys(array $expect, array $actual) {
665  foreach ($expect as $key => $value) {
666    if (isset($actual[$key]) || array_key_exists($key, $actual)) {
667      continue;
668    }
669
670    throw new InvalidArgumentException(
671      pht(
672        'Expected to find key "%s", but it is not present.',
673        $key));
674
675  }
676
677  foreach ($actual as $key => $value) {
678    if (isset($expect[$key]) || array_key_exists($key, $expect)) {
679      continue;
680    }
681
682    throw new InvalidArgumentException(
683      pht(
684        'Found unexpected surplus key "%s" where no such key was expected.',
685        $key));
686  }
687}
688
689/**
690 * Assert that passed data can be converted to string.
691 *
692 * @param  string    Assert that this data is valid.
693 * @return void
694 *
695 * @task   assert
696 */
697function assert_stringlike($parameter) {
698  switch (gettype($parameter)) {
699    case 'string':
700    case 'NULL':
701    case 'boolean':
702    case 'double':
703    case 'integer':
704      return;
705    case 'object':
706      if (method_exists($parameter, '__toString')) {
707        return;
708      }
709      break;
710    case 'array':
711    case 'resource':
712    case 'unknown type':
713    default:
714      break;
715  }
716
717  throw new InvalidArgumentException(
718    pht(
719      'Argument must be scalar or object which implements %s!',
720      '__toString()'));
721}
722
723/**
724 * Returns the first argument which is not strictly null, or `null` if there
725 * are no such arguments. Identical to the MySQL function of the same name.
726 *
727 * @param  ...         Zero or more arguments of any type.
728 * @return mixed       First non-`null` arg, or null if no such arg exists.
729 */
730function coalesce(/* ... */) {
731  $args = func_get_args();
732  foreach ($args as $arg) {
733    if ($arg !== null) {
734      return $arg;
735    }
736  }
737  return null;
738}
739
740
741/**
742 * Similar to @{function:coalesce}, but less strict: returns the first
743 * non-`empty()` argument, instead of the first argument that is strictly
744 * non-`null`. If no argument is nonempty, it returns the last argument. This
745 * is useful idiomatically for setting defaults:
746 *
747 *   $display_name = nonempty($user_name, $full_name, "Anonymous");
748 *
749 * @param  ...         Zero or more arguments of any type.
750 * @return mixed       First non-`empty()` arg, or last arg if no such arg
751 *                     exists, or null if you passed in zero args.
752 */
753function nonempty(/* ... */) {
754  $args = func_get_args();
755  $result = null;
756  foreach ($args as $arg) {
757    $result = $arg;
758    if ($arg) {
759      break;
760    }
761  }
762  return $result;
763}
764
765
766/**
767 * Invokes the "new" operator with a vector of arguments. There is no way to
768 * `call_user_func_array()` on a class constructor, so you can instead use this
769 * function:
770 *
771 *   $obj = newv($class_name, $argv);
772 *
773 * That is, these two statements are equivalent:
774 *
775 *   $pancake = new Pancake('Blueberry', 'Maple Syrup', true);
776 *   $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true));
777 *
778 * DO NOT solve this problem in other, more creative ways! Three popular
779 * alternatives are:
780 *
781 *   - Build a fake serialized object and unserialize it.
782 *   - Invoke the constructor twice.
783 *   - just use `eval()` lol
784 *
785 * These are really bad solutions to the problem because they can have side
786 * effects (e.g., __wakeup()) and give you an object in an otherwise impossible
787 * state. Please endeavor to keep your objects in possible states.
788 *
789 * If you own the classes you're doing this for, you should consider whether
790 * or not restructuring your code (for instance, by creating static
791 * construction methods) might make it cleaner before using `newv()`. Static
792 * constructors can be invoked with `call_user_func_array()`, and may give your
793 * class a cleaner and more descriptive API.
794 *
795 * @param  string  The name of a class.
796 * @param  list    Array of arguments to pass to its constructor.
797 * @return obj     A new object of the specified class, constructed by passing
798 *                 the argument vector to its constructor.
799 */
800function newv($class_name, array $argv) {
801  $reflector = new ReflectionClass($class_name);
802  if ($argv) {
803    return $reflector->newInstanceArgs($argv);
804  } else {
805    return $reflector->newInstance();
806  }
807}
808
809
810/**
811 * Returns the first element of an array. Exactly like reset(), but doesn't
812 * choke if you pass it some non-referenceable value like the return value of
813 * a function.
814 *
815 * @param    array Array to retrieve the first element from.
816 * @return   wild  The first value of the array.
817 */
818function head(array $arr) {
819  return reset($arr);
820}
821
822/**
823 * Returns the last element of an array. This is exactly like `end()` except
824 * that it won't warn you if you pass some non-referencable array to
825 * it -- e.g., the result of some other array operation.
826 *
827 * @param    array Array to retrieve the last element from.
828 * @return   wild  The last value of the array.
829 */
830function last(array $arr) {
831  return end($arr);
832}
833
834/**
835 * Returns the first key of an array.
836 *
837 * @param    array       Array to retrieve the first key from.
838 * @return   int|string  The first key of the array.
839 */
840function head_key(array $arr) {
841  reset($arr);
842  return key($arr);
843}
844
845/**
846 * Returns the last key of an array.
847 *
848 * @param    array       Array to retrieve the last key from.
849 * @return   int|string  The last key of the array.
850 */
851function last_key(array $arr) {
852  end($arr);
853  return key($arr);
854}
855
856/**
857 * Merge a vector of arrays performantly. This has the same semantics as
858 * array_merge(), so these calls are equivalent:
859 *
860 *   array_merge($a, $b, $c);
861 *   array_mergev(array($a, $b, $c));
862 *
863 * However, when you have a vector of arrays, it is vastly more performant to
864 * merge them with this function than by calling array_merge() in a loop,
865 * because using a loop generates an intermediary array on each iteration.
866 *
867 * @param list Vector of arrays to merge.
868 * @return list Arrays, merged with array_merge() semantics.
869 */
870function array_mergev(array $arrayv) {
871  if (!$arrayv) {
872    return array();
873  }
874
875  foreach ($arrayv as $key => $item) {
876    if (!is_array($item)) {
877      throw new InvalidArgumentException(
878        pht(
879          'Expected all items passed to `%s` to be arrays, but '.
880          'argument with key "%s" has type "%s".',
881          __FUNCTION__.'()',
882          $key,
883          gettype($item)));
884    }
885  }
886
887  return call_user_func_array('array_merge', $arrayv);
888}
889
890
891/**
892 * Split a corpus of text into lines. This function splits on "\n", "\r\n", or
893 * a mixture of any of them.
894 *
895 * NOTE: This function does not treat "\r" on its own as a newline because none
896 * of SVN, Git or Mercurial do on any OS.
897 *
898 * @param string Block of text to be split into lines.
899 * @param bool If true, retain line endings in result strings.
900 * @return list List of lines.
901 *
902 * @phutil-external-symbol class PhutilSafeHTML
903 * @phutil-external-symbol function phutil_safe_html
904 */
905function phutil_split_lines($corpus, $retain_endings = true) {
906  if (!strlen($corpus)) {
907    return array('');
908  }
909
910  // Split on "\r\n" or "\n".
911  if ($retain_endings) {
912    $lines = preg_split('/(?<=\n)/', $corpus);
913  } else {
914    $lines = preg_split('/\r?\n/', $corpus);
915  }
916
917  // If the text ends with "\n" or similar, we'll end up with an empty string
918  // at the end; discard it.
919  if (end($lines) == '') {
920    array_pop($lines);
921  }
922
923  if ($corpus instanceof PhutilSafeHTML) {
924    foreach ($lines as $key => $line) {
925      $lines[$key] = phutil_safe_html($line);
926    }
927    return $lines;
928  }
929
930  return $lines;
931}
932
933
934/**
935 * Simplifies a common use of `array_combine()`. Specifically, this:
936 *
937 *   COUNTEREXAMPLE:
938 *   if ($list) {
939 *     $result = array_combine($list, $list);
940 *   } else {
941 *     // Prior to PHP 5.4, array_combine() failed if given empty arrays.
942 *     $result = array();
943 *   }
944 *
945 * ...is equivalent to this:
946 *
947 *   $result = array_fuse($list);
948 *
949 * @param   list  List of scalars.
950 * @return  dict  Dictionary with inputs mapped to themselves.
951 */
952function array_fuse(array $list) {
953  if ($list) {
954    return array_combine($list, $list);
955  }
956  return array();
957}
958
959
960/**
961 * Add an element between every two elements of some array. That is, given a
962 * list `A, B, C, D`, and some element to interleave, `x`, this function returns
963 * `A, x, B, x, C, x, D`. This works like `implode()`, but does not concatenate
964 * the list into a string. In particular:
965 *
966 *   implode('', array_interleave($x, $list));
967 *
968 * ...is equivalent to:
969 *
970 *   implode($x, $list);
971 *
972 * This function does not preserve keys.
973 *
974 * @param wild  Element to interleave.
975 * @param list  List of elements to be interleaved.
976 * @return list Original list with the new element interleaved.
977 */
978function array_interleave($interleave, array $array) {
979  $result = array();
980  foreach ($array as $item) {
981    $result[] = $item;
982    $result[] = $interleave;
983  }
984  array_pop($result);
985  return $result;
986}
987
988function phutil_is_windows() {
989  // We can also use PHP_OS, but that's kind of sketchy because it returns
990  // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for
991  // DIRECTORY_SEPARATOR is more straightforward.
992  return (DIRECTORY_SEPARATOR != '/');
993}
994
995function phutil_is_hiphop_runtime() {
996  return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1);
997}
998
999/**
1000 * Converts a string to a loggable one, with unprintables and newlines escaped.
1001 *
1002 * @param string  Any string.
1003 * @return string String with control and newline characters escaped, suitable
1004 *                for printing on a single log line.
1005 */
1006function phutil_loggable_string($string) {
1007  if (preg_match('/^[\x20-\x7E]+$/', $string)) {
1008    return $string;
1009  }
1010
1011  $result = '';
1012
1013  static $c_map = array(
1014    '\\' => '\\\\',
1015    "\n" => '\\n',
1016    "\r" => '\\r',
1017    "\t" => '\\t',
1018  );
1019
1020  $len = strlen($string);
1021  for ($ii = 0; $ii < $len; $ii++) {
1022    $c = $string[$ii];
1023    if (isset($c_map[$c])) {
1024      $result .= $c_map[$c];
1025    } else {
1026      $o = ord($c);
1027      if ($o < 0x20 || $o >= 0x7F) {
1028        $result .= '\\x'.sprintf('%02X', $o);
1029      } else {
1030        $result .= $c;
1031      }
1032    }
1033  }
1034
1035  return $result;
1036}
1037
1038
1039/**
1040 * Perform an `fwrite()` which distinguishes between EAGAIN and EPIPE.
1041 *
1042 * PHP's `fwrite()` is broken, and never returns `false` for writes to broken
1043 * nonblocking pipes: it always returns 0, and provides no straightforward
1044 * mechanism for distinguishing between EAGAIN (buffer is full, can't write any
1045 * more right now) and EPIPE or similar (no write will ever succeed).
1046 *
1047 * See: https://bugs.php.net/bug.php?id=39598
1048 *
1049 * If you call this method instead of `fwrite()`, it will attempt to detect
1050 * when a zero-length write is caused by EAGAIN and return `0` only if the
1051 * write really should be retried.
1052 *
1053 * @param resource  Socket or pipe stream.
1054 * @param string    Bytes to write.
1055 * @return bool|int Number of bytes written, or `false` on any error (including
1056 *                  errors which `fwrite()` can not detect, like a broken pipe).
1057 */
1058function phutil_fwrite_nonblocking_stream($stream, $bytes) {
1059  if (!strlen($bytes)) {
1060    return 0;
1061  }
1062
1063  $result = @fwrite($stream, $bytes);
1064  if ($result !== 0) {
1065    // In cases where some bytes are witten (`$result > 0`) or
1066    // an error occurs (`$result === false`), the behavior of fwrite() is
1067    // correct. We can return the value as-is.
1068    return $result;
1069  }
1070
1071  // If we make it here, we performed a 0-length write. Try to distinguish
1072  // between EAGAIN and EPIPE. To do this, we're going to `stream_select()`
1073  // the stream, write to it again if PHP claims that it's writable, and
1074  // consider the pipe broken if the write fails.
1075
1076  // (Signals received during the "fwrite()" do not appear to affect anything,
1077  // see D20083.)
1078
1079  $read = array();
1080  $write = array($stream);
1081  $except = array();
1082
1083  $result = @stream_select($read, $write, $except, 0);
1084  if ($result === false) {
1085    // See T13243. If the select is interrupted by a signal, it may return
1086    // "false" indicating an underlying EINTR condition. In this case, the
1087    // results (notably, "$write") are not usable because "stream_select()"
1088    // didn't update them.
1089
1090    // In this case, treat this stream as blocked and tell the caller to
1091    // retry, since EINTR is the only condition we're currently aware of that
1092    // can cause "fwrite()" to return "0" and "stream_select()" to return
1093    // "false" on the same stream.
1094    return 0;
1095  }
1096
1097  if (!$write) {
1098    // The stream isn't writable, so we conclude that it probably really is
1099    // blocked and the underlying error was EAGAIN. Return 0 to indicate that
1100    // no data could be written yet.
1101    return 0;
1102  }
1103
1104  // If we make it here, PHP **just** claimed that this stream is writable, so
1105  // perform a write. If the write also fails, conclude that these failures are
1106  // EPIPE or some other permanent failure.
1107  $result = @fwrite($stream, $bytes);
1108  if ($result !== 0) {
1109    // The write worked or failed explicitly. This value is fine to return.
1110    return $result;
1111  }
1112
1113  // We performed a 0-length write, were told that the stream was writable, and
1114  // then immediately performed another 0-length write. Conclude that the pipe
1115  // is broken and return `false`.
1116  return false;
1117}
1118
1119
1120/**
1121 * Convert a human-readable unit description into a numeric one. This function
1122 * allows you to replace this:
1123 *
1124 *   COUNTEREXAMPLE
1125 *   $ttl = (60 * 60 * 24 * 30); // 30 days
1126 *
1127 * ...with this:
1128 *
1129 *   $ttl = phutil_units('30 days in seconds');
1130 *
1131 * ...which is self-documenting and difficult to make a mistake with.
1132 *
1133 * @param   string  Human readable description of a unit quantity.
1134 * @return  int     Quantity of specified unit.
1135 */
1136function phutil_units($description) {
1137  $matches = null;
1138  if (!preg_match('/^(\d+) (\w+) in (\w+)$/', $description, $matches)) {
1139    throw new InvalidArgumentException(
1140      pht(
1141        'Unable to parse unit specification (expected a specification in the '.
1142        'form "%s"): %s',
1143        '5 days in seconds',
1144        $description));
1145  }
1146
1147  $quantity = (int)$matches[1];
1148  $src_unit = $matches[2];
1149  $dst_unit = $matches[3];
1150
1151  $is_divisor = false;
1152
1153  switch ($dst_unit) {
1154    case 'seconds':
1155      switch ($src_unit) {
1156        case 'second':
1157        case 'seconds':
1158          $factor = 1;
1159          break;
1160        case 'minute':
1161        case 'minutes':
1162          $factor = 60;
1163          break;
1164        case 'hour':
1165        case 'hours':
1166          $factor = 60 * 60;
1167          break;
1168        case 'day':
1169        case 'days':
1170          $factor = 60 * 60 * 24;
1171          break;
1172        default:
1173          throw new InvalidArgumentException(
1174            pht(
1175              'This function can not convert from the unit "%s".',
1176              $src_unit));
1177      }
1178      break;
1179
1180    case 'bytes':
1181      switch ($src_unit) {
1182        case 'byte':
1183        case 'bytes':
1184          $factor = 1;
1185          break;
1186        case 'bit':
1187        case 'bits':
1188          $factor = 8;
1189          $is_divisor = true;
1190          break;
1191        default:
1192          throw new InvalidArgumentException(
1193            pht(
1194              'This function can not convert from the unit "%s".',
1195              $src_unit));
1196      }
1197      break;
1198
1199    case 'milliseconds':
1200      switch ($src_unit) {
1201        case 'second':
1202        case 'seconds':
1203          $factor = 1000;
1204          break;
1205        case 'minute':
1206        case 'minutes':
1207          $factor = 1000 * 60;
1208          break;
1209        case 'hour':
1210        case 'hours':
1211          $factor = 1000 * 60 * 60;
1212          break;
1213        case 'day':
1214        case 'days':
1215          $factor = 1000 * 60 * 60 * 24;
1216          break;
1217        default:
1218          throw new InvalidArgumentException(
1219            pht(
1220              'This function can not convert from the unit "%s".',
1221              $src_unit));
1222      }
1223      break;
1224
1225    case 'microseconds':
1226      switch ($src_unit) {
1227        case 'second':
1228        case 'seconds':
1229          $factor = 1000000;
1230          break;
1231        case 'minute':
1232        case 'minutes':
1233          $factor = 1000000 * 60;
1234          break;
1235        case 'hour':
1236        case 'hours':
1237          $factor = 1000000 * 60 * 60;
1238          break;
1239        case 'day':
1240        case 'days':
1241          $factor = 1000000 * 60 * 60 * 24;
1242          break;
1243        default:
1244          throw new InvalidArgumentException(
1245            pht(
1246              'This function can not convert from the unit "%s".',
1247              $src_unit));
1248      }
1249      break;
1250
1251    default:
1252      throw new InvalidArgumentException(
1253        pht(
1254          'This function can not convert into the unit "%s".',
1255          $dst_unit));
1256  }
1257
1258  if ($is_divisor) {
1259    if ($quantity % $factor) {
1260      throw new InvalidArgumentException(
1261        pht(
1262          '"%s" is not an exact quantity.',
1263          $description));
1264    }
1265    return (int)($quantity / $factor);
1266  } else {
1267    return $quantity * $factor;
1268  }
1269}
1270
1271
1272/**
1273 * Compute the number of microseconds that have elapsed since an earlier
1274 * timestamp (from `microtime(true)`).
1275 *
1276 * @param double Microsecond-precision timestamp, from `microtime(true)`.
1277 * @return int Elapsed microseconds.
1278 */
1279function phutil_microseconds_since($timestamp) {
1280  if (!is_float($timestamp)) {
1281    throw new Exception(
1282      pht(
1283        'Argument to "phutil_microseconds_since(...)" should be a value '.
1284        'returned from "microtime(true)".'));
1285  }
1286
1287  $delta = (microtime(true) - $timestamp);
1288  $delta = 1000000 * $delta;
1289  $delta = (int)$delta;
1290
1291  return $delta;
1292}
1293
1294
1295/**
1296 * Decode a JSON dictionary.
1297 *
1298 * @param   string    A string which ostensibly contains a JSON-encoded list or
1299 *                    dictionary.
1300 * @return  mixed     Decoded list/dictionary.
1301 */
1302function phutil_json_decode($string) {
1303  $result = @json_decode($string, true);
1304
1305  if (!is_array($result)) {
1306    // Failed to decode the JSON. Try to use @{class:PhutilJSONParser} instead.
1307    // This will probably fail, but will throw a useful exception.
1308    $parser = new PhutilJSONParser();
1309    $result = $parser->parse($string);
1310  }
1311
1312  return $result;
1313}
1314
1315
1316/**
1317 * Encode a value in JSON, raising an exception if it can not be encoded.
1318 *
1319 * @param wild A value to encode.
1320 * @return string JSON representation of the value.
1321 */
1322function phutil_json_encode($value) {
1323  $result = @json_encode($value);
1324  if ($result === false) {
1325    $reason = phutil_validate_json($value);
1326    if (function_exists('json_last_error')) {
1327      $err = json_last_error();
1328      if (function_exists('json_last_error_msg')) {
1329        $msg = json_last_error_msg();
1330        $extra = pht('#%d: %s', $err, $msg);
1331      } else {
1332        $extra = pht('#%d', $err);
1333      }
1334    } else {
1335      $extra = null;
1336    }
1337
1338    if ($extra) {
1339      $message = pht(
1340        'Failed to JSON encode value (%s): %s.',
1341        $extra,
1342        $reason);
1343    } else {
1344      $message = pht(
1345        'Failed to JSON encode value: %s.',
1346        $reason);
1347    }
1348
1349    throw new Exception($message);
1350  }
1351
1352  return $result;
1353}
1354
1355
1356/**
1357 * Produce a human-readable explanation why a value can not be JSON-encoded.
1358 *
1359 * @param wild Value to validate.
1360 * @param string Path within the object to provide context.
1361 * @return string|null Explanation of why it can't be encoded, or null.
1362 */
1363function phutil_validate_json($value, $path = '') {
1364  if ($value === null) {
1365    return;
1366  }
1367
1368  if ($value === true) {
1369    return;
1370  }
1371
1372  if ($value === false) {
1373    return;
1374  }
1375
1376  if (is_int($value)) {
1377    return;
1378  }
1379
1380  if (is_float($value)) {
1381    return;
1382  }
1383
1384  if (is_array($value)) {
1385    foreach ($value as $key => $subvalue) {
1386      if (strlen($path)) {
1387        $full_key = $path.' > ';
1388      } else {
1389        $full_key = '';
1390      }
1391
1392      if (!phutil_is_utf8($key)) {
1393        $full_key = $full_key.phutil_utf8ize($key);
1394        return pht(
1395          'Dictionary key "%s" is not valid UTF8, and cannot be JSON encoded.',
1396          $full_key);
1397      }
1398
1399      $full_key .= $key;
1400      $result = phutil_validate_json($subvalue, $full_key);
1401      if ($result !== null) {
1402        return $result;
1403      }
1404    }
1405  }
1406
1407  if (is_string($value)) {
1408    if (!phutil_is_utf8($value)) {
1409      $display = substr($value, 0, 256);
1410      $display = phutil_utf8ize($display);
1411      if (!strlen($path)) {
1412        return pht(
1413          'String value is not valid UTF8, and can not be JSON encoded: %s',
1414          $display);
1415      } else {
1416        return pht(
1417          'Dictionary value at key "%s" is not valid UTF8, and cannot be '.
1418          'JSON encoded: %s',
1419          $path,
1420          $display);
1421      }
1422    }
1423  }
1424
1425  return;
1426}
1427
1428
1429/**
1430 * Decode an INI string.
1431 *
1432 * @param  string
1433 * @return mixed
1434 */
1435function phutil_ini_decode($string) {
1436  $results = null;
1437  $trap = new PhutilErrorTrap();
1438
1439  try {
1440    $have_call = false;
1441    if (function_exists('parse_ini_string')) {
1442      if (defined('INI_SCANNER_RAW')) {
1443        $results = @parse_ini_string($string, true, INI_SCANNER_RAW);
1444        $have_call = true;
1445      }
1446    }
1447
1448    if (!$have_call) {
1449      throw new PhutilMethodNotImplementedException(
1450        pht(
1451          '%s is not compatible with your version of PHP (%s). This function '.
1452          'is only supported on PHP versions newer than 5.3.0.',
1453          __FUNCTION__,
1454          phpversion()));
1455    }
1456
1457    if ($results === false) {
1458      throw new PhutilINIParserException(trim($trap->getErrorsAsString()));
1459    }
1460
1461    foreach ($results as $section => $result) {
1462      if (!is_array($result)) {
1463        // We JSON decode the value in ordering to perform the following
1464        // conversions:
1465        //
1466        //   - `'true'` => `true`
1467        //   - `'false'` => `false`
1468        //   - `'123'` => `123`
1469        //   - `'1.234'` => `1.234`
1470        //
1471        $result = json_decode($result, true);
1472
1473        if ($result !== null && !is_array($result)) {
1474          $results[$section] = $result;
1475        }
1476
1477        continue;
1478      }
1479
1480      foreach ($result as $key => $value) {
1481        $value = json_decode($value, true);
1482
1483        if ($value !== null && !is_array($value)) {
1484          $results[$section][$key] = $value;
1485        }
1486      }
1487    }
1488  } catch (Exception $ex) {
1489    $trap->destroy();
1490    throw $ex;
1491  }
1492
1493  $trap->destroy();
1494  return $results;
1495}
1496
1497
1498/**
1499 * Attempt to censor any plaintext credentials from a string.
1500 *
1501 * The major use case here is to censor usernames and passwords from command
1502 * output. For example, when `git fetch` fails, the output includes credentials
1503 * for authenticated HTTP remotes.
1504 *
1505 * @param   string  Some block of text.
1506 * @return  string  A similar block of text, but with credentials that could
1507 *                  be identified censored.
1508 */
1509function phutil_censor_credentials($string) {
1510  return preg_replace(',(?<=://)([^/@\s]+)(?=@|$),', '********', $string);
1511}
1512
1513
1514/**
1515 * Returns a parsable string representation of a variable.
1516 *
1517 * This function is intended to behave similarly to PHP's `var_export` function,
1518 * but the output is intended to follow our style conventions.
1519 *
1520 * @param  wild    The variable you want to export.
1521 * @return string
1522 */
1523function phutil_var_export($var) {
1524  // `var_export(null, true)` returns `"NULL"` (in uppercase).
1525  if ($var === null) {
1526    return 'null';
1527  }
1528
1529  // PHP's `var_export` doesn't format arrays very nicely. In particular:
1530  //
1531  //   - An empty array is split over two lines (`"array (\n)"`).
1532  //   - A space separates "array" and the first opening brace.
1533  //   - Non-associative arrays are returned as associative arrays with an
1534  //     integer key.
1535  //
1536  if (is_array($var)) {
1537    if (count($var) === 0) {
1538      return 'array()';
1539    }
1540
1541    // Don't show keys for non-associative arrays.
1542    $show_keys = !phutil_is_natural_list($var);
1543
1544    $output = array();
1545    $output[] = 'array(';
1546
1547    foreach ($var as $key => $value) {
1548      // Adjust the indentation of the value.
1549      $value = str_replace("\n", "\n  ", phutil_var_export($value));
1550      $output[] = '  '.
1551        ($show_keys ? var_export($key, true).' => ' : '').
1552        $value.',';
1553    }
1554
1555    $output[] = ')';
1556    return implode("\n", $output);
1557  }
1558
1559  // Let PHP handle everything else.
1560  return var_export($var, true);
1561}
1562
1563
1564/**
1565 * An improved version of `fnmatch`.
1566 *
1567 * @param  string  A glob pattern.
1568 * @param  string  A path.
1569 * @return bool
1570 */
1571function phutil_fnmatch($glob, $path) {
1572  // Modify the glob to allow `**/` to match files in the root directory.
1573  $glob = preg_replace('@(?:(?<!\\\\)\\*){2}/@', '{,*/,**/}', $glob);
1574
1575  $escaping = false;
1576  $in_curlies = 0;
1577  $regex = '';
1578
1579  for ($i = 0; $i < strlen($glob); $i++) {
1580    $char = $glob[$i];
1581    $next_char = ($i < strlen($glob) - 1) ? $glob[$i + 1] : null;
1582
1583    $escape = array('$', '(', ')', '+', '.', '^', '|');
1584    $mapping = array();
1585
1586    if ($escaping) {
1587      $escape[] = '*';
1588      $escape[] = '?';
1589      $escape[] = '{';
1590    } else {
1591      $mapping['*'] = $next_char === '*' ? '.*' : '[^/]*';
1592      $mapping['?'] = '[^/]';
1593      $mapping['{'] = '(';
1594
1595      if ($in_curlies) {
1596        $mapping[','] = '|';
1597        $mapping['}'] = ')';
1598      }
1599    }
1600
1601    if (in_array($char, $escape)) {
1602      $regex .= "\\{$char}";
1603    } else if ($replacement = idx($mapping, $char)) {
1604      $regex .= $replacement;
1605    } else if ($char === '\\') {
1606      if ($escaping) {
1607        $regex .= '\\\\';
1608      }
1609      $escaping = !$escaping;
1610      continue;
1611    } else {
1612      $regex .= $char;
1613    }
1614
1615    if ($char === '{' && !$escaping) {
1616      $in_curlies++;
1617    } else if ($char === '}' && $in_curlies && !$escaping) {
1618      $in_curlies--;
1619    }
1620
1621    $escaping = false;
1622  }
1623
1624  if ($in_curlies || $escaping) {
1625    throw new InvalidArgumentException(pht('Invalid glob pattern.'));
1626  }
1627
1628  $regex = '(\A'.$regex.'\z)';
1629  return (bool)preg_match($regex, $path);
1630}
1631
1632
1633/**
1634 * Compare two hashes for equality.
1635 *
1636 * This function defuses two attacks: timing attacks and type juggling attacks.
1637 *
1638 * In a timing attack, the attacker observes that strings which match the
1639 * secret take slightly longer to fail to match because more characters are
1640 * compared. By testing a large number of strings, they can learn the secret
1641 * character by character. This defuses timing attacks by always doing the
1642 * same amount of work.
1643 *
1644 * In a type juggling attack, an attacker takes advantage of PHP's type rules
1645 * where `"0" == "0e12345"` for any exponent. A portion of of hexadecimal
1646 * hashes match this pattern and are vulnerable. This defuses this attack by
1647 * performing bytewise character-by-character comparison.
1648 *
1649 * It is questionable how practical these attacks are, but they are possible
1650 * in theory and defusing them is straightforward.
1651 *
1652 * @param string First hash.
1653 * @param string Second hash.
1654 * @return bool True if hashes are identical.
1655 */
1656function phutil_hashes_are_identical($u, $v) {
1657  if (!is_string($u)) {
1658    throw new Exception(pht('First hash argument must be a string.'));
1659  }
1660
1661  if (!is_string($v)) {
1662    throw new Exception(pht('Second hash argument must be a string.'));
1663  }
1664
1665  if (strlen($u) !== strlen($v)) {
1666    return false;
1667  }
1668
1669  $len = strlen($v);
1670
1671  $bits = 0;
1672  for ($ii = 0; $ii < $len; $ii++) {
1673    $bits |= (ord($u[$ii]) ^ ord($v[$ii]));
1674  }
1675
1676  return ($bits === 0);
1677}
1678
1679
1680/**
1681 * Build a query string from a dictionary.
1682 *
1683 * @param map<string, string> Dictionary of parameters.
1684 * @return string HTTP query string.
1685 */
1686function phutil_build_http_querystring(array $parameters) {
1687  $pairs = array();
1688  foreach ($parameters as $key => $value) {
1689    $pairs[] = array($key, $value);
1690  }
1691
1692  return phutil_build_http_querystring_from_pairs($pairs);
1693}
1694
1695/**
1696 * Build a query string from a list of parameter pairs.
1697 *
1698 * @param list<pair<string, string>> List of pairs.
1699 * @return string HTTP query string.
1700 */
1701function phutil_build_http_querystring_from_pairs(array $pairs) {
1702  // We want to encode in RFC3986 mode, but "http_build_query()" did not get
1703  // a flag for that mode until PHP 5.4.0. This is equivalent to calling
1704  // "http_build_query()" with the "PHP_QUERY_RFC3986" flag.
1705
1706  $query = array();
1707  foreach ($pairs as $pair_key => $pair) {
1708    if (!is_array($pair) || (count($pair) !== 2)) {
1709      throw new Exception(
1710        pht(
1711          'HTTP parameter pair (with key "%s") is not valid: each pair must '.
1712          'be an array with exactly two elements.',
1713          $pair_key));
1714    }
1715
1716    list($key, $value) = $pair;
1717    list($key, $value) = phutil_http_parameter_pair($key, $value);
1718    $query[] = rawurlencode($key).'='.rawurlencode($value);
1719  }
1720  $query = implode('&', $query);
1721
1722  return $query;
1723}
1724
1725/**
1726 * Typecheck and cast an HTTP key-value parameter pair.
1727 *
1728 * Scalar values are converted to strings. Nonscalar values raise exceptions.
1729 *
1730 * @param scalar HTTP parameter key.
1731 * @param scalar HTTP parameter value.
1732 * @return pair<string, string> Key and value as strings.
1733 */
1734function phutil_http_parameter_pair($key, $value) {
1735  try {
1736    assert_stringlike($key);
1737  } catch (InvalidArgumentException $ex) {
1738    throw new PhutilProxyException(
1739      pht('HTTP query parameter key must be a scalar.'),
1740      $ex);
1741  }
1742
1743  $key = phutil_string_cast($key);
1744
1745  try {
1746    assert_stringlike($value);
1747  } catch (InvalidArgumentException $ex) {
1748    throw new PhutilProxyException(
1749      pht(
1750        'HTTP query parameter value (for key "%s") must be a scalar.',
1751        $key),
1752      $ex);
1753  }
1754
1755  $value = phutil_string_cast($value);
1756
1757  return array($key, $value);
1758}
1759
1760function phutil_decode_mime_header($header) {
1761  if (function_exists('iconv_mime_decode')) {
1762    return iconv_mime_decode($header, 0, 'UTF-8');
1763  }
1764
1765  if (function_exists('mb_decode_mimeheader')) {
1766    return mb_decode_mimeheader($header);
1767  }
1768
1769  throw new Exception(
1770    pht(
1771      'Unable to decode MIME header: install "iconv" or "mbstring" '.
1772      'extension.'));
1773}
1774
1775/**
1776 * Perform a "(string)" cast without disabling standard exception behavior.
1777 *
1778 * When PHP invokes "__toString()" automatically, it fatals if the method
1779 * raises an exception. In older versions of PHP (until PHP 7.1), this fatal is
1780 * fairly opaque and does not give you any information about the exception
1781 * itself, although newer versions of PHP at least include the exception
1782 * message.
1783 *
1784 * This is documented on the "__toString()" manual page:
1785 *
1786 *   Warning
1787 *   You cannot throw an exception from within a __toString() method. Doing
1788 *   so will result in a fatal error.
1789 *
1790 * However, this only applies to implicit invocation by the language runtime.
1791 * Application code can safely call `__toString()` directly without any effect
1792 * on exception handling behavior. Very cool.
1793 *
1794 * We also reject arrays. PHP casts them to the string "Array". This behavior
1795 * is, charitably, evil.
1796 *
1797 * @param wild Any value which aspires to be represented as a string.
1798 * @return string String representation of the provided value.
1799 */
1800function phutil_string_cast($value) {
1801  if (is_array($value)) {
1802    throw new Exception(
1803      pht(
1804        'Value passed to "phutil_string_cast()" is an array; arrays can '.
1805        'not be sensibly cast to strings.'));
1806  }
1807
1808  if (is_object($value)) {
1809    $string = $value->__toString();
1810
1811    if (!is_string($string)) {
1812      throw new Exception(
1813        pht(
1814          'Object (of class "%s") did not return a string from "__toString()".',
1815          get_class($value)));
1816    }
1817
1818    return $string;
1819  }
1820
1821  return (string)$value;
1822}
1823
1824
1825/**
1826 * Return a short, human-readable description of an object's type.
1827 *
1828 * This is mostly useful for raising errors like "expected x() to return a Y,
1829 * but it returned a Z".
1830 *
1831 * This is similar to "get_type()", but describes objects and arrays in more
1832 * detail.
1833 *
1834 * @param wild Anything.
1835 * @return string Human-readable description of the value's type.
1836 */
1837function phutil_describe_type($value) {
1838  return PhutilTypeSpec::getTypeOf($value);
1839}
1840
1841
1842/**
1843 * Test if a list has the natural numbers (1, 2, 3, and so on) as keys, in
1844 * order.
1845 *
1846 * @return bool True if the list is a natural list.
1847 */
1848function phutil_is_natural_list(array $list) {
1849  $expect = 0;
1850
1851  foreach ($list as $key => $item) {
1852    if ($key !== $expect) {
1853      return false;
1854    }
1855    $expect++;
1856  }
1857
1858  return true;
1859}
1860
1861
1862/**
1863 * Escape text for inclusion in a URI or a query parameter. Note that this
1864 * method does NOT escape '/', because "%2F" is invalid in paths and Apache
1865 * will automatically 404 the page if it's present. This will produce correct
1866 * (the URIs will work) and desirable (the URIs will be readable) behavior in
1867 * these cases:
1868 *
1869 *    '/path/?param='.phutil_escape_uri($string);         # OK: Query Parameter
1870 *    '/path/to/'.phutil_escape_uri($string);             # OK: URI Suffix
1871 *
1872 * It will potentially produce the WRONG behavior in this special case:
1873 *
1874 *    COUNTEREXAMPLE
1875 *    '/path/to/'.phutil_escape_uri($string).'/thing/';   # BAD: URI Infix
1876 *
1877 * In this case, any '/' characters in the string will not be escaped, so you
1878 * will not be able to distinguish between the string and the suffix (unless
1879 * you have more information, like you know the format of the suffix). For infix
1880 * URI components, use @{function:phutil_escape_uri_path_component} instead.
1881 *
1882 * @param   string  Some string.
1883 * @return  string  URI encoded string, except for '/'.
1884 */
1885function phutil_escape_uri($string) {
1886  return str_replace('%2F', '/', rawurlencode($string));
1887}
1888
1889
1890/**
1891 * Escape text for inclusion as an infix URI substring. See discussion at
1892 * @{function:phutil_escape_uri}. This function covers an unusual special case;
1893 * @{function:phutil_escape_uri} is usually the correct function to use.
1894 *
1895 * This function will escape a string into a format which is safe to put into
1896 * a URI path and which does not contain '/' so it can be correctly parsed when
1897 * embedded as a URI infix component.
1898 *
1899 * However, you MUST decode the string with
1900 * @{function:phutil_unescape_uri_path_component} before it can be used in the
1901 * application.
1902 *
1903 * @param   string  Some string.
1904 * @return  string  URI encoded string that is safe for infix composition.
1905 */
1906function phutil_escape_uri_path_component($string) {
1907  return rawurlencode(rawurlencode($string));
1908}
1909
1910
1911/**
1912 * Unescape text that was escaped by
1913 * @{function:phutil_escape_uri_path_component}. See
1914 * @{function:phutil_escape_uri} for discussion.
1915 *
1916 * Note that this function is NOT the inverse of
1917 * @{function:phutil_escape_uri_path_component}! It undoes additional escaping
1918 * which is added to survive the implied unescaping performed by the webserver
1919 * when interpreting the request.
1920 *
1921 * @param string  Some string emitted
1922 *                from @{function:phutil_escape_uri_path_component} and
1923 *                then accessed via a web server.
1924 * @return string Original string.
1925 */
1926function phutil_unescape_uri_path_component($string) {
1927  return rawurldecode($string);
1928}
1929
1930function phutil_is_noninteractive() {
1931  if (function_exists('posix_isatty') && !posix_isatty(STDIN)) {
1932    return true;
1933  }
1934
1935  return false;
1936}
1937
1938function phutil_is_interactive() {
1939  if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
1940    return true;
1941  }
1942
1943  return false;
1944}
1945
1946function phutil_encode_log($message) {
1947  return addcslashes($message, "\0..\37\\\177..\377");
1948}
1949
1950/**
1951 * Insert a value in between each pair of elements in a list.
1952 *
1953 * Keys in the input list are preserved.
1954 */
1955function phutil_glue(array $list, $glue) {
1956  if (!$list) {
1957    return $list;
1958  }
1959
1960  $last_key = last_key($list);
1961
1962  $keys = array();
1963  $values = array();
1964
1965  $tmp = $list;
1966
1967  foreach ($list as $key => $ignored) {
1968    $keys[] = $key;
1969    if ($key !== $last_key) {
1970      $tmp[] = $glue;
1971      $keys[] = last_key($tmp);
1972    }
1973  }
1974
1975  return array_select_keys($tmp, $keys);
1976}
1977
1978function phutil_partition(array $map) {
1979  $partitions = array();
1980
1981  $partition = array();
1982  $is_first = true;
1983  $partition_value = null;
1984
1985  foreach ($map as $key => $value) {
1986    if (!$is_first) {
1987      if ($partition_value === $value) {
1988        $partition[$key] = $value;
1989        continue;
1990      }
1991
1992      $partitions[] = $partition;
1993    }
1994
1995    $is_first = false;
1996    $partition = array($key => $value);
1997    $partition_value = $value;
1998  }
1999
2000  if ($partition) {
2001    $partitions[] = $partition;
2002  }
2003
2004  return $partitions;
2005}
2006