From 235c5fc762b54682e1ad7579e0ad2859caff937c Mon Sep 17 00:00:00 2001 From: jorgecc Date: Mon, 7 Nov 2022 09:21:06 -0300 Subject: [PATCH] 1.1 --- .gitattributes | 9 + .gitignore | 5 + README.md | 94 +++ composer.json | 33 ++ examples/example_decode.php | 15 + examples/example_decode_unquoted.php | 17 + examples/example_encode.php | 20 + src/Services_JSON.php | 838 +++++++++++++++++++++++++++ 8 files changed, 1031 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 examples/example_decode.php create mode 100644 examples/example_decode_unquoted.php create mode 100644 examples/example_encode.php create mode 100644 src/Services_JSON.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a427067 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/examples export-ignore +/vendor export-ignore +/docs export-ignore +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/*.md export-ignore +/phpunit.xml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c9d5ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea +composer.lock +vendor/ +tests/resources/compiled/ +*.cache diff --git a/README.md b/README.md index 67043b6..fad914b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,96 @@ # Services_JSON PHP implementaion of json_encode/decode + +[![Packagist](https://img.shields.io/packagist/v/eftec/services_json.svg)](https://packagist.org/packages/eftec/bladeone) +[![Total Downloads](https://poser.pugx.org/eftec/services_json/downloads)](https://packagist.org/packages/eftec/bladeone) +[![Maintenance](https://img.shields.io/maintenance/yes/2022.svg)]() +[![composer](https://img.shields.io/badge/composer-%3E1.6-blue.svg)]() +[![php](https://img.shields.io/badge/php-7.x-green.svg)]() +[![php](https://img.shields.io/badge/php-8.x-green.svg)]() +[![CocoaPods](https://img.shields.io/badge/docs-70%25-yellow.svg)]() + +JSON (JavaScript Object Notation, http://json.org) is a lightweight data-interchange format. +It is easy for humans to read and write. It is easy for machines to parse and generate. +It is based on a subset of the JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999. +This feature can also be found in Python. JSON is a text format that is completely language independent +but uses conventions that are familiar to programmers of the C-family of languages, including +C, C++, C#, Java, JavaScript, Perl, TCL, and many others. These properties make JSON an ideal +data-interchange language. + +This package provides a simple encoder and decoder for JSON notation. It is intended for use +with client-side Javascript applications that make use of HTTPRequest to perform server +communication functions - data can be encoded into JSON notation for use in a client-side +javascript, or decoded from incoming Javascript requests. JSON format is native to Javascript, +and can be directly eval() with no further parsing overhead. + +## So, what is the goal with this version? + +While this version doesn't have a better performance than json_encode() and json_decode() available as extension, +it has the next features: + +[x] it doesn't require an extension. If you can't install an extension, then you can use this version. +[x] **it works with json with unquoted keys** (for example javascript notation) +[x] It is a simple file with no dependency. + +## Usage + +### Getting started + +* Install the library using composer (or you could download Services_JSON.php manually) +```shell +composer require eftec/services_json +``` +* Include the dependency +```php +use eftec\ServicesJson\Services_JSON; +include '../vendor/autoload.php'; +$s=new Services_JSON(); +``` + +* And creates a service class +```php +$s=new Services_JSON(); +``` +See the folder examples for further examples + +### Decode + +Decode transform (decodified) a json string into a stdclass or an associative array + +```php +$s=new Services_JSON(); +$json='{"hello":{"a":2,"b":3},"world":[1,2,3,"aaa"]}'; +var_dump($s->decode($json)); // as stdclass +var_dump($s->decode($json,true)); // as array +``` +It also works with unquoted keys + +```php +$s=new Services_JSON(); +$json='{hello:{a:2,b:3},world:[1,2,3,"aaa","bbbb"]}'; // the keys are unquoted. +var_dump($s->decode($json)); // as stdclass +var_dump($s->decode($json,true)); // as array +``` + +### Encode + +Encode transform a value (array, object, primitive value, etc.) into a json expression (a string) + +```php +$array=["hello"=>['a'=>2,'b'=>3],'world'=>[1,2,3,"aaa","bbb"]]; +$obj=(object)$array; +var_dump($s->encode($array)); // encode an associative array +var_dump($s->encode($obj)); // encode an object +``` + + +## Changelog + +* 1.1 + * It works with PHP 7.2 and higher. + * It doesn't require PECL to work. + * The code was cleaned + * web header is removed. + * Method decode() could return an associative array. +* 1.0.3-1.0.0 PECL version. It only works with PHP 4.x and PHP 5.x + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..078468d --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "eftec/services_json", + "description": "PHP implementaion of json_encode/decode", + "type": "library", + "license": "BSD", + "autoload": { + "psr-4": { + "eftec\\ServicesJson\\": "src/" + } + }, + "authors": [ + { + "name": "Michal Migurski", + "email": "mike-json@teczno.com" + }, + { + "name": "Matt Knapp" + }, + { + "name": "Brett Stimmerman" + }, + { + "name": "jorgecc", + "email": "jcastro@eftec.cl" + } + ], + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "ext-mbstring": "*" + } +} diff --git a/examples/example_decode.php b/examples/example_decode.php new file mode 100644 index 0000000..5d1b884 --- /dev/null +++ b/examples/example_decode.php @@ -0,0 +1,15 @@ +"; +echo "
";
+var_dump($s->decode($json));
+echo "
"; +echo "Decode as array:
"; +echo "
";
+var_dump($s->decode($json,true));
+echo "
"; diff --git a/examples/example_decode_unquoted.php b/examples/example_decode_unquoted.php new file mode 100644 index 0000000..b48a62b --- /dev/null +++ b/examples/example_decode_unquoted.php @@ -0,0 +1,17 @@ +"; +echo "
";
+var_dump($s->decode($json));
+echo "
"; +echo "Decode as array:
"; +echo "
";
+var_dump($s->decode($json,true));
+echo "
"; diff --git a/examples/example_encode.php b/examples/example_encode.php new file mode 100644 index 0000000..73c04bb --- /dev/null +++ b/examples/example_encode.php @@ -0,0 +1,20 @@ +['a'=>2,'b'=>3],'world'=>[1,2,3,"aaa","bbb"]]; + +$obj=(object)$array; + +echo "Encode array to json
"; +echo "
";
+var_dump($s->encode($array));
+echo "
"; +echo "Encode object to json
"; +echo "
";
+var_dump($s->encode($obj));
+echo "
"; diff --git a/src/Services_JSON.php b/src/Services_JSON.php new file mode 100644 index 0000000..1311d6e --- /dev/null +++ b/src/Services_JSON.php @@ -0,0 +1,838 @@ + + * @author Matt Knapp + * @author Brett Stimmerman + * @autor Jorge Castro + * @copyright 2005 Michal Migurski + * @version 1.1 2022-11-07 + * @license http://www.opensource.org/licenses/bsd-license.php + * @link https://https://github.com/EFTEC/Services_JSON + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 + */ +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_SLICE', 1); +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_STR', 2); +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_ARR', 3); +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_OBJ', 4); +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_CMT', 5); +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_LOOSE_TYPE', 16); +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_USE_TO_JSON', 64); + +/** + * Converts to and from JSON format. + * + * Brief example of use: + * + * + * // create a new instance of Services_JSON + * $json = new Services_JSON(); + * + * // convert a complexe value to JSON notation, and send it to the browser + * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); + * $output = $json->encode($value); + * + * print($output); + * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] + * + * // accept incoming POST data, assumed to be in JSON notation + * $input = file_get_contents('php://input', 1000000); + * $value = $json->decode($input); + * + */ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + * - SERVICES_JSON_USE_TO_JSON: call toJSON when serializing objects + * It serializes the return value from the toJSON call rather + * than the object it'self, toJSON can return associative arrays, + * strings or numbers, if you return an object, make sure it does + * not have a toJSON method, otherwise an error will occur. + */ + public function __construct($use = 0) + { + $this->use = $use; + $this->_mb_strlen = function_exists('mb_strlen'); + $this->_mb_convert_encoding = function_exists('mb_convert_encoding'); + $this->_mb_substr = function_exists('mb_substr'); + } + + // private - cache the mbstring lookup results.. + public $_mb_strlen = false; + public $_mb_substr = false; + public $_mb_convert_encoding = false; + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + public function utf162utf8($utf16): string + { + if ($this->_mb_convert_encoding) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + $bytes = (ord($utf16[0]) << 8) | ord($utf16[1]); + switch (true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + public function utf82utf16($utf8): string + { + // oh please oh please oh please oh please oh please + if ($this->_mb_convert_encoding) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + switch ($this->strlen8($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8[0]) >> 2)) + . chr((0xC0 & (ord($utf8[0]) << 6)) + | (0x3F & ord($utf8[1]))); + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8[0]) << 4)) + | (0x0F & (ord($utf8[1]) >> 2))) + . chr((0xC0 & (ord($utf8[1]) << 6)) + | (0x7F & ord($utf8[2]))); + } + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format (and sends JSON Header) + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + public function encode($var) + { + //header('Content-type: application/json'); + return $this->encodeUnsafe($var); + } + + /** + * encodes an arbitrary variable into JSON format without JSON Header - warning - may allow XSS!!!!) + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + public function encodeUnsafe($var) + { + // see bug #16908 - regarding numeric locale printing + $lc = setlocale(LC_NUMERIC, 0); + setlocale(LC_NUMERIC, 'C'); + $ret = $this->_encode($var); + setlocale(LC_NUMERIC, $lc); + return $ret; + } + + /** + * PRIVATE CODE that does the work of encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + public function _encode($var) + { + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + case 'NULL': + return 'null'; + case 'integer': + return (int)$var; + case 'double': + case 'float': + return (float)$var; + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = $this->strlen8($var); + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + $ord_var_c = ord($var[$c]); + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\' . $var[$c]; + break; + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var[$c]; + break; + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + if ($c + 1 >= $strlen_var) { + $c += 1; + $ascii .= '?'; + break; + } + $char = pack('C*', $ord_var_c, ord($var[$c + 1])); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + case (($ord_var_c & 0xF0) == 0xE0): + if ($c + 2 >= $strlen_var) { + $c += 2; + $ascii .= '?'; + break; + } + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + @ord($var[$c + 1]), + @ord($var[$c + 2])); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + case (($ord_var_c & 0xF8) == 0xF0): + if ($c + 3 >= $strlen_var) { + $c += 3; + $ascii .= '?'; + break; + } + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var[$c + 1]), + ord($var[$c + 2]), + ord($var[$c + 3])); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + if ($c + 4 >= $strlen_var) { + $c += 4; + $ascii .= '?'; + break; + } + $char = pack('C*', $ord_var_c, + ord($var[$c + 1]), + ord($var[$c + 2]), + ord($var[$c + 3]), + ord($var[$c + 4])); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + case (($ord_var_c & 0xFE) == 0xFC): + if ($c + 5 >= $strlen_var) { + $c += 5; + $ascii .= '?'; + break; + } + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var[$c + 1]), + ord($var[$c + 2]), + ord($var[$c + 3]), + ord($var[$c + 4]), + ord($var[$c + 5])); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + return '"' . $ascii . '"'; + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, count($var) - 1))) { + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + foreach ($properties as $property) { + if ($this->isError($property)) { + return $property; + } + } + return '{' . implode(',', $properties) . '}'; + } + // treat it like a regular array + $elements = array_map(array($this, '_encode'), $var); + foreach ($elements as $element) { + if ($this->isError($element)) { + return $element; + } + } + return '[' . implode(',', $elements) . ']'; + case 'object': + // support toJSON methods. + if (($this->use & SERVICES_JSON_USE_TO_JSON) && method_exists($var, 'toJSON')) { + // this may end up allowing unlimited recursion + // so we check the return value to make sure it's not got the same method. + $recode = $var->toJSON(); + if (method_exists($recode, 'toJSON')) { + if (($this->use & SERVICES_JSON_SUPPRESS_ERRORS)) { + return 'null'; + } + $this->Services_JSON_Error(get_class($var) . + " toJSON returned an object with a toJSON method."); + } + return $this->_encode($recode); + } + $vars = get_object_vars($var); + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + foreach ($properties as $property) { + if ($this->isError($property)) { + return $property; + } + } + return '{' . implode(',', $properties) . '}'; + default: + if (($this->use & SERVICES_JSON_SUPPRESS_ERRORS)) { + return 'null'; + } + $this->Services_JSON_Error(gettype($var) . " can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + public function name_value($name, $value) + { + $encoded_value = $this->_encode($value); + if ($this->isError($encoded_value)) { + return $encoded_value; + } + return $this->_encode(strval($name)) . ':' . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + public function reduce_string($str): string + { + $str = preg_replace(array( + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + ), '', $str); + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * @param bool $asAssocArray if true, then it returns as an associative array.
+ * if false (default), then it will return as an stdClass + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + public function decode($str, $asAssocArray = false) + { + $str = $this->reduce_string($str); + switch (strtolower($str)) { + case 'true': + return true; + case 'false': + return false; + case 'null': + return null; + default: + $m = array(); + if (is_numeric($str)) { + // Lookie-loo, it's a number + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + } + if (preg_match('/^(["\']).*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = $this->substr8($str, 0, 1); + $chrs = $this->substr8($str, 1, -1); + $utf8 = ''; + $strlen_chrs = $this->strlen8($chrs); + for ($c = 0; $c < $strlen_chrs; ++$c) { + $substr_chrs_c_2 = $this->substr8($chrs, $c, 2); + $ord_chrs_c = ord($chrs[$c]); + switch (true) { + case $substr_chrs_c_2 === '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 === '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 === '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 === '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 === '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + case $substr_chrs_c_2 === '\\"': + case $substr_chrs_c_2 === '\\\'': + case $substr_chrs_c_2 === '\\\\': + case $substr_chrs_c_2 === '\\/': + if (($delim === '"' && $substr_chrs_c_2 !== '\\\'') || + ($delim === "'" && $substr_chrs_c_2 !== '\\"')) { + $utf8 .= $chrs[++$c]; + } + break; + case preg_match('/\\\u[0-9A-F]{4}/i', $this->substr8($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec($this->substr8($chrs, ($c + 2), 2))) + . chr(hexdec($this->substr8($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs[$c]; + break; + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= $this->substr8($chrs, $c, 2); + ++$c; + break; + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= $this->substr8($chrs, $c, 3); + $c += 2; + break; + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= $this->substr8($chrs, $c, 4); + $c += 3; + break; + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= $this->substr8($chrs, $c, 5); + $c += 4; + break; + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= $this->substr8($chrs, $c, 6); + $c += 5; + break; + } + } + return $utf8; + } + if (preg_match('/^\[.*]$/s', $str) || preg_match('/^\{.*}$/s', $str)) { + // array, or object notation + if ($str[0] === '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + $stk[] = array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false); + $chrs = $this->substr8($str, 1, -1); + $chrs = $this->reduce_string($chrs); + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + } + return $obj; + } + //print("\nparsing {$chrs}\n"); + $strlen_chrs = $this->strlen8($chrs); + for ($c = 0; $c <= $strlen_chrs; ++$c) { + $top = end($stk); + $substr_chrs_c_2 = $this->substr8($chrs, $c, 2); + if (($c == $strlen_chrs) || (($chrs[$c] === ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = $this->substr8($chrs, $top['where'], ($c - $top['where'])); + $stk[] = array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false); + //print("Found split at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + $arr[] = $this->decode($slice); + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B")); + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode(trim(substr($slice, strlen($parts[0])), ", \t\n\r\0\x0B")); + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + } + } elseif ((($chrs[$c] === '"') || ($chrs[$c] === "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + $stk[] = array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs[$c]); + //print("Found start of string at {$c}\n"); + } elseif (($chrs[$c] == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + (($this->strlen8($this->substr8($chrs, 0, $c)) - $this->strlen8(rtrim($this->substr8($chrs, 0, $c), '\\'))) % 2 != 1)) { + // found a quote, we're in a string, and it's not escaped + // we know that it's not escaped becase there is _not_ an + // odd number of backslashes at the end of the string so far + array_pop($stk); + //print("Found end of string at {$c}: ".$this->substr8($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + } elseif (($chrs[$c] === '[') && + in_array($top['what'], [SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ])) { + // found a left-bracket, and we are in an array, object, or slice + $stk[] = array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false); + //print("Found start of array at {$c}\n"); + } elseif (($chrs[$c] === ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + } elseif (($chrs[$c] === '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + $stk[] = array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false); + //print("Found start of object at {$c}\n"); + } elseif (($chrs[$c] === '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + } elseif (($substr_chrs_c_2 === '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + $stk[] = array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false); + $c++; + //print("Found start of comment at {$c}\n"); + } elseif (($substr_chrs_c_2 === '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + //print("Found end of comment at {$c}: ".$this->substr8($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + } + } + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + } + if (reset($stk) == SERVICES_JSON_IN_OBJ) { + if ($asAssocArray) { + return $this->arrayCastRecursive($obj); + } + return $obj; + } + } + } + return null; + } + + public function arrayCastRecursive($array) + { + if (is_array($array)) { + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = $this->arrayCastRecursive($value); + } + if ($value instanceof stdClass) { + $array[$key] = $this->arrayCastRecursive((array)$value); + } + } + } + if ($array instanceof stdClass) { + return $this->arrayCastRecursive((array)$array); + } + return $array; + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + public function isError($data, $code = null): bool + { + if (class_exists('pear')) { + /** @noinspection PhpUndefinedClassInspection */ + return PEAR::isError($data, $code); + } + if (is_object($data) && (get_class($data) === 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + return false; + } + + /** + * Calculates length of string in bytes + * @param string $str + * @return integer length + */ + public function strlen8($str): int + { + if ($this->_mb_strlen) { + return mb_strlen($str, "8bit"); + } + return strlen($str); + } + + /** + * Returns part of a string, interpreting $start and $length as number of bytes. + * @param string $string + * @param integer $start start + * @param integer $length length + * @return string length + */ + public function substr8($string, $start, $length = false): string + { + if ($length === false) { + $length = $this->strlen8($string) - $start; + } + if ($this->_mb_substr) { + return mb_substr($string, $start, $length, "8bit"); + } + return substr($string, $start, $length); + } + + public function Services_JSON_Error($msg): void + { + throw new RuntimeException($msg); + } + +}