├── .github └── workflows │ └── ci.yml ├── .gitignore ├── AUTHORS.txt ├── LICENSE ├── README.md ├── make_strings.php ├── package-lock.json ├── package.json ├── phpUnserialize.d.ts ├── phpUnserialize.js ├── phpUnserialize.spec.js └── phpUnserialize.test-d.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x, 16.x, 18.x] 17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'npm' 26 | - run: npm ci 27 | - run: npm run build --if-present 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.coverage/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Authors ordered by first contribution. 2 | 3 | Bryan Davis 4 | Chris Galli 5 | Vovan-VE 6 | Tarwin Stroh-Spijer 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Bryan Davis and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://img.shields.io/npm/v/phpunserialize.svg?style=flat)](https://www.npmjs.com/package/phpunserialize) 2 | 3 | phpUnserialize 4 | ============== 5 | 6 | Convert serialized PHP data to a javascript object graph. 7 | 8 | 9 | Why? 10 | ---- 11 | > OMG why would anyone do something this perverse? PHP has a `json_encode()` 12 | > method so you don't have to try and cobble together ugly hacks like this. 13 | 14 | It all started so innocently. The guy at the desk next to mine asked "hey is 15 | there a javascript library that can turn this php serialize mess into 16 | something that I can read?" I gaped. He explained that he was trying to slap 17 | together a js testing harness for a set of REST services that returned 18 | serialized PHP as their transport representation. 19 | 20 | A [google search][] turned up [something][] so I went back to listening to the 21 | latest [OMM][] album. Fifteen minutes later the stream of curses coming from 22 | Gallilama started harshing my groove. It turns out that the venerable phpjs 23 | function only handles a particular subset of PHP's `serialize` output. 24 | Specifically it doesn't handle references and objects at all. Google found 25 | a [java implementation][] that looked more complete. I did a quick port of it 26 | to javascript and moved on to my [$wingin' Utter$][] playlist. 27 | 28 | The next day I checked in and found out that strange things were afoot with my 29 | port. It turns out that private and protected members `serialize` in an 30 | "interesting" way. PHP prepends the member name with either the class name 31 | (private) or an asterisk (protected) surrounded by null bytes (\u0000). The 32 | hack parser was going into an infinite loop when it tried to extract these 33 | values. 34 | 35 | By this point I was fully committed. Nothing less than a TDD validated library 36 | that could handle just about any craziness I threw at it would do. I'm sure 37 | there are still gaps, but this "quick hack" is working for our twisted needs. 38 | 39 | 40 | Implementation Details 41 | ---------------------- 42 | PHP's serialization format is not well documented, but this function takes 43 | a best guess approach to parsing and interpreting it. Serialized integers, 44 | floats, booleans, strings, arrays, objects and references are currently 45 | supported. 46 | 47 | PHP's array type is a hybrid of javascript's array and object types. 48 | phpUnserialize translates PHP arrays having only 0-based consecutive numeric 49 | keys into javascript arrays. All other arrays are translated into javascript 50 | objects. 51 | 52 | Serialized members of a PHP object carry scope information via name mangling. 53 | `phpUnserialize` strips this scope signifier prefix from private and protected 54 | members. 55 | 56 | Check out the [tests][] for more details or read the source. 57 | 58 | 59 | Usage 60 | ----- 61 | The `phpUnserialize.js` file implements the [Universal Module Definition][] 62 | pattern which attempts to be compatible with multiple script loaders including 63 | [AMD][], [CommonJS][] and direct usage in an HTML file. 64 | 65 | Plain HTML: 66 | ```html 67 | 68 | 71 | ``` 72 | 73 | With an [AMD][] loader: 74 | ```javascript 75 | define(["phpunserialize"], function (phpUnserialize) { 76 | return { 77 | foo: phpUnserialize('s:3:"foo";') 78 | }; 79 | }); 80 | ``` 81 | 82 | With a [CommonJS][] loader: 83 | ```javascript 84 | var phpUnserialize = require('phpunserialize'); 85 | var foo = phpUnserialize('s:3:"foo";'); 86 | ``` 87 | 88 | Running the Unit Tests 89 | ---------------------- 90 | ```sh 91 | npm install 92 | npm test 93 | ``` 94 | 95 | --- 96 | [google search]: https://www.google.com/search?q=php+unserialize+javascript 97 | [something]: http://phpjs.org/functions/unserialize/ 98 | [OMM]: http://www.oldmanmarkley.com/ 99 | [java implementation]: https://code.google.com/p/serialized-php-parser 100 | [$wingin' Utter$]: http://swinginutters.com/ 101 | [tests]: php-unserialize.spec.js 102 | [Universal Module Definition]: https://github.com/umdjs/umd 103 | [AMD]: https://github.com/amdjs/amdjs-api/blob/master/AMD.md 104 | [CommonJS]: http://wiki.commonjs.org/wiki/CommonJS 105 | -------------------------------------------------------------------------------- /make_strings.php: -------------------------------------------------------------------------------- 1 | self = $this; 16 | } 17 | } 18 | $f = new Foo(); 19 | dump($f); 20 | 21 | class Bar { 22 | } 23 | $f = new Bar(); 24 | $f->self = $f; 25 | dump($f); 26 | 27 | class Child extends Foo { 28 | public $lorem = 42; 29 | protected $ipsum = 37; 30 | private $dolor = 13; 31 | } 32 | $f = new Child(); 33 | dump($f); 34 | 35 | $f = new stdClass; 36 | $f->obj1->obj2->obj3->arr = array(); 37 | $f->obj1->obj2->obj3->arr[] = 1; 38 | $f->obj1->obj2->obj3->arr[] = 2; 39 | $f->obj1->obj2->obj3->arr[] = 3; 40 | $f->obj1->obj2->obj3->arr['ref1'] = $f->obj1->obj2; 41 | $f->obj1->obj2->obj3->arr['ref2'] = &$f->obj1->obj2->obj3->arr; 42 | dump($f); 43 | 44 | $f = array( 45 | 'int' => 42, 46 | 'str' => "lorem", 47 | 'nul' => null, 48 | 'obj' => new stdClass(), 49 | ); 50 | $f['obj']->lorem = 10; 51 | $f['obj']->ipsum = new stdClass(); 52 | $f['obj']->ipsumLink = $f['obj']->ipsum; 53 | $f['obj']->ipsumRef = &$f['obj']->ipsum; 54 | 55 | $f['intRef'] = &$f['int']; 56 | $f['strRef'] = &$f['str']; 57 | $f['nulRef'] = &$f['nul']; 58 | $f['objLink'] = $f['obj']; 59 | $f['objRef'] = &$f['obj']; 60 | dump($f); 61 | 62 | $f = new SplDoublyLinkedList(); 63 | dump($f); 64 | 65 | $o = new StdClass(); 66 | $a = new StdClass(); 67 | $o->a = $a; 68 | $f = new SplObjectStorage(); 69 | $f[$o] = 1; 70 | $f[$a] = 2; 71 | dump($f); 72 | 73 | $f = 'blåbærsyltetøy'; 74 | dump($f); 75 | 76 | $f = '$¢€𠜎'; 77 | dump($f); 78 | 79 | $array = array( 80 | "0b5f7j" => "value1", 81 | "anotherKey" => "value2" 82 | ); 83 | dump($array); 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpunserialize", 3 | "description": "Convert serialized PHP data to a javascript object graph", 4 | "version": "1.3.0", 5 | "keywords": [ 6 | "php", 7 | "serialization", 8 | "unserialize" 9 | ], 10 | "homepage": "https://github.com/bd808/php-unserialize-js", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/bd808/php-unserialize-js.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/bd808/php-unserialize-js/issues" 17 | }, 18 | "author": "Bryan Davis (https://bd808.com)", 19 | "license": "MIT", 20 | "scripts": { 21 | "test": "jest && tsd" 22 | }, 23 | "main": "phpUnserialize.js", 24 | "devDependencies": { 25 | "jest": "^29.3.1", 26 | "tsd": "^0.25.0", 27 | "typescript": "^4.8.4" 28 | }, 29 | "jest": { 30 | "clearMocks": true, 31 | "collectCoverage": true, 32 | "coverageDirectory": ".coverage" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpUnserialize.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for phpUnserialize 2 | type phpscalar = string | number | boolean | null; 3 | type phparray = Array | Record; 4 | export type unserialized = phpscalar | phparray; 5 | export function phpUnserialize(phpstr: string): unserialized; 6 | -------------------------------------------------------------------------------- /phpUnserialize.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * php-unserialize-js JavaScript Library 3 | * https://github.com/bd808/php-unserialize-js 4 | * 5 | * Copyright 2013 Bryan Davis and contributors 6 | * Released under the MIT license 7 | * http://www.opensource.org/licenses/MIT 8 | */ 9 | 10 | (function (root, factory) { 11 | /*global define, exports, module */ 12 | "use strict"; 13 | 14 | /* istanbul ignore next: no coverage reporting for UMD wrapper */ 15 | if (typeof define === 'function' && define.amd) { 16 | // AMD. Register as an anonymous module. 17 | define([], factory); 18 | } else if (typeof exports === 'object') { 19 | // Node. Does not work with strict CommonJS, but 20 | // only CommonJS-like environments that support module.exports, 21 | // like Node. 22 | module.exports = factory(); 23 | } else { 24 | // Browser globals (root is window) 25 | root.phpUnserialize = factory(); 26 | } 27 | }(this, function () { 28 | "use strict"; 29 | 30 | /** 31 | * Parse php serialized data into js objects. 32 | * 33 | * @param {String} phpstr Php serialized string to parse 34 | * @return {mixed} Parsed result 35 | */ 36 | return function (phpstr) { 37 | var idx = 0 38 | , refStack = [] 39 | , ridx = 0 40 | , parseNext // forward declaraton for "use strict" 41 | 42 | , readLength = function () { 43 | var del = phpstr.indexOf(':', idx) 44 | , val = phpstr.substring(idx, del); 45 | idx = del + 2; 46 | return parseInt(val, 10); 47 | } //end readLength 48 | 49 | , readInt = function () { 50 | var del = phpstr.indexOf(';', idx) 51 | , val = phpstr.substring(idx, del); 52 | idx = del + 1; 53 | return parseInt(val, 10); 54 | } //end readInt 55 | 56 | , parseAsInt = function () { 57 | var val = readInt(); 58 | refStack[ridx++] = val; 59 | return val; 60 | } //end parseAsInt 61 | 62 | , parseAsFloat = function () { 63 | var del = phpstr.indexOf(';', idx) 64 | , val = phpstr.substring(idx, del); 65 | idx = del + 1; 66 | val = parseFloat(val); 67 | refStack[ridx++] = val; 68 | return val; 69 | } //end parseAsFloat 70 | 71 | , parseAsBoolean = function () { 72 | var del = phpstr.indexOf(';', idx) 73 | , val = phpstr.substring(idx, del); 74 | idx = del + 1; 75 | val = ("1" === val)? true: false; 76 | refStack[ridx++] = val; 77 | return val; 78 | } //end parseAsBoolean 79 | 80 | , readString = function (expect) { 81 | expect = typeof expect !== "undefined" ? expect : '"'; 82 | var len = readLength() 83 | , utfLen = 0 84 | , bytes = 0 85 | , ch 86 | , val; 87 | while (bytes < len) { 88 | ch = phpstr.charCodeAt(idx + utfLen++); 89 | if (ch <= 0x007F) { 90 | bytes++; 91 | } else if (ch > 0x07FF) { 92 | bytes += 3; 93 | } else { 94 | bytes += 2; 95 | } 96 | } 97 | // catch non-compliant utf8 encodings 98 | if (phpstr.charAt(idx + utfLen) !== expect) { 99 | utfLen += phpstr.indexOf('"', idx + utfLen) - idx - utfLen; 100 | } 101 | val = phpstr.substring(idx, idx + utfLen); 102 | idx += utfLen + 2; 103 | return val; 104 | } //end readString 105 | 106 | , parseAsString = function () { 107 | var val = readString(); 108 | refStack[ridx++] = val; 109 | return val; 110 | } //end parseAsString 111 | 112 | , readType = function () { 113 | var type = phpstr.charAt(idx); 114 | idx += 2; 115 | return type; 116 | } //end readType 117 | 118 | , readKey = function () { 119 | var type = readType(); 120 | switch (type) { 121 | case 'i': return readInt(); 122 | case 's': return readString(); 123 | default: 124 | var msg = "Unknown key type '" + type + "' at position " + 125 | (idx - 2); 126 | throw new Error(msg); 127 | } //end switch 128 | } 129 | 130 | , parseAsArray = function () { 131 | var len = readLength() 132 | , resultArray = [] 133 | , resultHash = {} 134 | , keep = resultArray 135 | , lref = ridx++ 136 | , key 137 | , val 138 | , i 139 | , j 140 | , alen; 141 | 142 | refStack[lref] = keep; 143 | try { 144 | for (i = 0; i < len; i++) { 145 | key = readKey(); 146 | val = parseNext(); 147 | if (keep === resultArray && key + '' === i + '') { 148 | // store in array version 149 | resultArray.push(val); 150 | 151 | } else { 152 | if (keep !== resultHash) { 153 | // found first non-sequential numeric key 154 | // convert existing data to hash 155 | for (j = 0, alen = resultArray.length; j < alen; j++) { 156 | resultHash[j] = resultArray[j]; 157 | } 158 | keep = resultHash; 159 | refStack[lref] = keep; 160 | } 161 | resultHash[key] = val; 162 | } //end if 163 | } //end for 164 | } catch (e) { 165 | // decorate exception with current state 166 | e.state = keep; 167 | throw e; 168 | } 169 | 170 | idx++; 171 | return keep; 172 | } //end parseAsArray 173 | 174 | , fixPropertyName = function (parsedName, baseClassName) { 175 | var class_name 176 | , prop_name 177 | , pos; 178 | if ( 179 | typeof parsedName === 'string' && 180 | "\u0000" === parsedName.charAt(0) 181 | ) { 182 | // "*property" 183 | // "classproperty" 184 | pos = parsedName.indexOf("\u0000", 1); 185 | if (pos > 0) { 186 | class_name = parsedName.substring(1, pos); 187 | prop_name = parsedName.substr(pos + 1); 188 | 189 | if ("*" === class_name) { 190 | // protected 191 | return prop_name; 192 | } else if (baseClassName === class_name) { 193 | // own private 194 | return prop_name; 195 | } else { 196 | // private of a descendant 197 | return class_name + "::" + prop_name; 198 | 199 | // On the one hand, we need to prefix property name with 200 | // class name, because parent and child classes both may 201 | // have private property with same name. We don't want 202 | // just to overwrite it and lose something. 203 | // 204 | // On the other hand, property name can be "foo::bar" 205 | // 206 | // $obj = new stdClass(); 207 | // $obj->{"foo::bar"} = 42; 208 | // // any user-defined class can do this by default 209 | // 210 | // and such property also can overwrite something. 211 | // 212 | // So, we can to lose something in any way. 213 | } 214 | } else { 215 | var msg = 'Expected two characters in non-public ' + 216 | "property name '" + parsedName + "' at position " + 217 | (idx - parsedName.length - 2); 218 | throw new Error(msg); 219 | } 220 | } else { 221 | // public "property" 222 | return parsedName; 223 | } 224 | } 225 | 226 | , parseAsObject = function () { 227 | var len 228 | , obj = {} 229 | , lref = ridx++ 230 | // HACK last char after closing quote is ':', 231 | // but not ';' as for normal string 232 | , clazzname = readString() 233 | , key 234 | , val 235 | , i; 236 | 237 | refStack[lref] = obj; 238 | len = readLength(); 239 | try { 240 | for (i = 0; i < len; i++) { 241 | key = fixPropertyName(readKey(), clazzname); 242 | val = parseNext(); 243 | obj[key] = val; 244 | } 245 | } catch (e) { 246 | // decorate exception with current state 247 | e.state = obj; 248 | throw e; 249 | } 250 | idx++; 251 | return obj; 252 | } //end parseAsObject 253 | 254 | , parseAsCustom = function () { 255 | var clazzname = readString() 256 | , content = readString('}'); 257 | // There is no char after the closing quote 258 | idx--; 259 | return { 260 | "__PHP_Incomplete_Class_Name": clazzname, 261 | "serialized": content 262 | }; 263 | } //end parseAsCustom 264 | 265 | , parseAsRefValue = function () { 266 | var ref = readInt() 267 | // php's ref counter is 1-based; our stack is 0-based. 268 | , val = refStack[ref - 1]; 269 | refStack[ridx++] = val; 270 | return val; 271 | } //end parseAsRefValue 272 | 273 | , parseAsRef = function () { 274 | var ref = readInt(); 275 | // php's ref counter is 1-based; our stack is 0-based. 276 | return refStack[ref - 1]; 277 | } //end parseAsRef 278 | 279 | , parseAsNull = function () { 280 | var val = null; 281 | refStack[ridx++] = val; 282 | return val; 283 | }; //end parseAsNull 284 | 285 | parseNext = function () { 286 | var type = readType(); 287 | switch (type) { 288 | case 'i': return parseAsInt(); 289 | case 'd': return parseAsFloat(); 290 | case 'b': return parseAsBoolean(); 291 | case 's': return parseAsString(); 292 | case 'a': return parseAsArray(); 293 | case 'O': return parseAsObject(); 294 | case 'C': return parseAsCustom(); 295 | case 'E': return parseAsString(); 296 | 297 | // link to object, which is a value - affects refStack 298 | case 'r': return parseAsRefValue(); 299 | 300 | // PHP's reference - DOES NOT affect refStack 301 | case 'R': return parseAsRef(); 302 | 303 | case 'N': return parseAsNull(); 304 | 305 | default: 306 | var msg = "Unknown type '" + type + "' at position " + (idx - 2); 307 | throw new Error(msg); 308 | } //end switch 309 | }; //end parseNext 310 | 311 | return parseNext(); 312 | }; 313 | })); 314 | -------------------------------------------------------------------------------- /phpUnserialize.spec.js: -------------------------------------------------------------------------------- 1 | var phpUnserialize = require('./phpUnserialize'); 2 | 3 | describe('Php-serialize Suite', () => { 4 | describe('Primative values', () => { 5 | it('can parse a string', () => { 6 | expect(phpUnserialize('s:3:"foo";')).toBe('foo'); 7 | expect(phpUnserialize('s:17:"bl\u00e5b\u00e6rsyltet\u00f8y";')). 8 | toBe('blåbærsyltetøy'); 9 | expect(phpUnserialize('s:17:"blåbærsyltetøy";')). 10 | toBe('blåbærsyltetøy'); 11 | expect(phpUnserialize('s:10:"$\u00a2\u20ac\ud841\udf0e";')) 12 | .toBe('$¢€𠜎'); 13 | expect(phpUnserialize('s:10:"$¢€𠜎";')) 14 | .toBe('$¢€𠜎'); 15 | }); 16 | 17 | it('can parse a non-UTF-8 string', () => { 18 | expect(phpUnserialize('s:28:"�f���V�����b��r�[�t�n���~�q";')) 19 | .toBe('�f���V�����b��r�[�t�n���~�q'); 20 | expect(phpUnserialize('s:30:"ベジタリアンですか?";')) 21 | .toBe('ベジタリアンですか?'); 22 | }); 23 | 24 | it('can parse an integer', () => { 25 | expect(phpUnserialize('i:1337;')).toBe(1337); 26 | }); 27 | 28 | it('can parse a float', () => { 29 | expect(phpUnserialize('d:13.37;')).toBe(13.37); 30 | }); 31 | 32 | it('can parse a boolean', () => { 33 | expect(phpUnserialize('b:1;')).toBeTruthy(); 34 | expect(phpUnserialize('b:0;')).toBeFalsy(); 35 | }); 36 | 37 | it('can parse a null', () => { 38 | expect(phpUnserialize('N;')).toBeNull(); 39 | }); 40 | 41 | it('can parse an array', () => { 42 | expect(phpUnserialize('a:2:{i:0;s:5:"hello";i:1;s:5:"world";}')). 43 | toEqual(['hello', 'world']); 44 | }); 45 | 46 | it('can parse a dictionary', () => { 47 | expect(phpUnserialize('a:2:{s:5:"hello";i:0;s:5:"world";i:1;}')). 48 | toEqual({'hello':0, 'world':1}); 49 | expect(phpUnserialize('a:2:{s:6:"0b5f7j";s:6:"value1";s:10:"anotherKey";s:6:"value2";}')). 50 | toEqual({'0b5f7j':'value1', 'anotherKey':'value2'}); 51 | }); 52 | 53 | it('can parse a reference', () => { 54 | expect(phpUnserialize('a:2:{s:5:"hello";i:42;s:5:"world";R:2;}')). 55 | toEqual({'hello':42, 'world':42}); 56 | }); 57 | 58 | it('can parse an enum', () => { 59 | expect(phpUnserialize('E:11:"Suit:Hearts";')).toEqual('Suit:Hearts'); 60 | }); 61 | }); 62 | 63 | describe('Object values', () => { 64 | it('can parse an empty object', () => { 65 | expect(phpUnserialize('O:5:"blank":0:{}')).toEqual({}); 66 | }); 67 | 68 | it('can parse public members', () => { 69 | expect(phpUnserialize( 70 | 'O:3:"Foo":2:{s:5:"hello";i:0;s:5:"world";i:1;};' 71 | )).toEqual({'hello':0, 'world':1}); 72 | }); 73 | 74 | it('can parse protected members', () => { 75 | expect(phpUnserialize( 76 | 'O:3:"Foo":2:{s:8:"\u0000*\u0000hello";i:0;s:8:"\u0000*\u0000world";i:1;};' 77 | )).toEqual({'hello':0, 'world':1}); 78 | }); 79 | 80 | it('can parse private members', () => { 81 | expect(phpUnserialize( 82 | 'O:3:"Foo":2:{s:10:"\u0000Foo\u0000hello";i:0;s:10:"\u0000Foo\u0000world";i:1;};' 83 | )).toEqual({'hello':0, 'world':1}); 84 | }); 85 | 86 | it('can parse a circular reference', () => { 87 | expected = {}; 88 | expected.self = expected; 89 | 90 | expect(phpUnserialize( 91 | "O:3:\"Bar\":1:{s:4:\"self\";r:1;}" 92 | )).toEqual(expected); 93 | }); 94 | 95 | it('can parse a numeric key', () => { 96 | expect(phpUnserialize( 97 | 'O:3:"key":2:{i:0;N;i:1;s:4:"main";}' 98 | )).toEqual({ 0: null, 1: "main" }); 99 | }); 100 | 101 | it('can parse a complex object', () => { 102 | expected = { 103 | bar : 1, 104 | baz : 2, 105 | xyzzy : [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] 106 | }; 107 | expected.self = expected; 108 | 109 | expect(phpUnserialize( 110 | "O:3:\"Foo\":4:{s:3:\"bar\";i:1;s:6:\"\u0000*\u0000baz\";i:2;s:10:\"\u0000Foo\u0000xyzzy\";a:9:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;}s:7:\"\u0000*\u0000self\";r:1;}" 111 | )).toEqual(expected); 112 | }); 113 | 114 | it('can parse inherited private members', () => { 115 | expected = { 116 | bar : 1, 117 | baz : 2, 118 | 'Foo::xyzzy' : [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ], 119 | lorem : 42, 120 | ipsum : 37, 121 | dolor : 13 122 | }; 123 | expected.self = expected; 124 | 125 | expect(phpUnserialize( 126 | "O:5:\"Child\":7:{s:5:\"lorem\";i:42;s:8:\"\u0000*\u0000ipsum\";i:37;s:12:\"\u0000Child\u0000dolor\";i:13;s:3:\"bar\";i:1;s:6:\"\u0000*\u0000baz\";i:2;s:10:\"\u0000Foo\u0000xyzzy\";a:9:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;i:9;}s:7:\"\u0000*\u0000self\";r:1;}" 127 | )).toEqual(expected); 128 | }); 129 | 130 | it('can parse an ugly mess', () => { 131 | expected = { 132 | obj1 : { 133 | obj2 : { 134 | obj3 : { 135 | arr : { 0 : 1, 1 : 2, 2 : 3 } 136 | } 137 | } 138 | } 139 | }; 140 | expected.obj1.obj2.obj3.arr['ref1'] = expected.obj1.obj2; 141 | expected.obj1.obj2.obj3.arr['ref2'] = expected.obj1.obj2.obj3.arr; 142 | 143 | expect(phpUnserialize( 144 | "O:8:\"stdClass\":1:{s:4:\"obj1\";O:8:\"stdClass\":1:{s:4:\"obj2\";O:8:\"stdClass\":1:{s:4:\"obj3\";O:8:\"stdClass\":1:{s:3:\"arr\";a:5:{i:0;i:1;i:1;i:2;i:2;i:3;s:4:\"ref1\";r:3;s:4:\"ref2\";R:5;}}}}}" 145 | )).toEqual(expected); 146 | }); 147 | 148 | it('can parse more ugly references', () => { 149 | expected = { 150 | int : 42, 151 | str : 'lorem', 152 | nul : null, 153 | obj : { 154 | lorem : 10, 155 | ipsum : {} 156 | } 157 | }; 158 | expected.obj.ipsumLink = expected.obj.ipsum; 159 | expected.obj.ipsumRef = expected.obj.ipsum; 160 | expected.intRef = expected.int; 161 | expected.strRef = expected.str; 162 | expected.nulRef = expected.nul; 163 | expected.objLink = expected.obj; 164 | expected.objRef = expected.obj; 165 | 166 | expect(phpUnserialize( 167 | "a:9:{s:3:\"int\";i:42;s:3:\"str\";s:5:\"lorem\";s:3:\"nul\";N;s:3:\"obj\";O:8:\"stdClass\":4:{s:5:\"lorem\";i:10;s:5:\"ipsum\";O:8:\"stdClass\":0:{}s:9:\"ipsumLink\";r:7;s:8:\"ipsumRef\";R:7;}s:6:\"intRef\";R:2;s:6:\"strRef\";R:3;s:6:\"nulRef\";R:4;s:7:\"objLink\";r:5;s:6:\"objRef\";R:5;}" 168 | )).toEqual(expected); 169 | }); 170 | }); 171 | 172 | describe('Custom serializers', () => { 173 | it('can parse a SplDoublyLinkedList', () => { 174 | expected = { 175 | '__PHP_Incomplete_Class_Name': 'SplDoublyLinkedList', 176 | 'serialized': 'i:0;' 177 | }; 178 | expect( 179 | phpUnserialize("C:19:\"SplDoublyLinkedList\":4:{i:0;}") 180 | ).toEqual(expected); 181 | }); 182 | 183 | it('can parse a SplObjectStorage', () => { 184 | expected = { 185 | '__PHP_Incomplete_Class_Name': 'SplObjectStorage', 186 | 'serialized': 'x:i:2;O:8:"stdClass":1:{s:1:"a";O:8:"stdClass":0:{}},i:1;;r:4;,i:2;;m:a:0:{}' 187 | }; 188 | expect( 189 | phpUnserialize("C:16:\"SplObjectStorage\":76:{x:i:2;O:8:\"stdClass\":1:{s:1:\"a\";O:8:\"stdClass\":0:{}},i:1;;r:4;,i:2;;m:a:0:{}}") 190 | ).toEqual(expected); 191 | }); 192 | 193 | it('can parse a SplObjectStorage with subsequent value', () => { 194 | expected = [ 195 | { 196 | __PHP_Incomplete_Class_Name: 'SplObjectStorage', 197 | serialized: 'a:1:{s:2:"id";s:10:"caO8WPx0GQ";}' 198 | }, 199 | true 200 | ]; 201 | expect( 202 | phpUnserialize('a:2:{i:0;C:16:"SplObjectStorage":33:{a:1:{s:2:"id";s:10:"caO8WPx0GQ";}}i:1;b:1;}') 203 | ).toEqual(expected); 204 | }); 205 | }); 206 | 207 | describe('Invalid input', () => { 208 | it('throws exception on unknown type', () => { 209 | expect.assertions(2); 210 | try { 211 | phpUnserialize(''); 212 | } catch (e) { 213 | expect(e).toBeInstanceOf(Error); 214 | expect(e).toHaveProperty('message', "Unknown type '' at position 0"); 215 | } 216 | }); 217 | 218 | it('throws exception on unknown array key type', () => { 219 | expect.assertions(3); 220 | try { 221 | phpUnserialize('a:1:{d:1.0;s:5:"hello";}'); 222 | } catch (e) { 223 | expect(e).toBeInstanceOf(Error); 224 | expect(e).toHaveProperty( 225 | 'message', "Unknown key type 'd' at position 5" 226 | ); 227 | expect(e).toHaveProperty('state', []); 228 | } 229 | }); 230 | 231 | it('throws exception on unknown object key type', () => { 232 | expect.assertions(3); 233 | try { 234 | phpUnserialize('O:4:"junk":1:{d:1.0;s:5:"hello";}') 235 | } catch (e) { 236 | expect(e).toBeInstanceOf(Error); 237 | expect(e).toHaveProperty( 238 | 'message', "Unknown key type 'd' at position 14" 239 | ); 240 | expect(e).toHaveProperty('state', {}); 241 | } 242 | }); 243 | 244 | it('throws exception on malformed property name', () => { 245 | expect.assertions(3); 246 | try { 247 | phpUnserialize('O:1:"A":1:{s:6:"\u0000hello";i:0;};') 248 | } catch (e) { 249 | expect(e).toBeInstanceOf(Error); 250 | expect(e).toHaveProperty( 251 | 'message', 252 | 'Expected two characters in non-public property name ' + 253 | "'\u0000hello' at position 16" 254 | ); 255 | expect(e).toHaveProperty('state', {}); 256 | } 257 | }); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /phpUnserialize.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType, expectError} from 'tsd'; 2 | import {phpUnserialize, unserialized} from './phpUnserialize'; 3 | 4 | expectType(phpUnserialize('s:3:"foo";')); 5 | expectType(phpUnserialize('i:1337;')); 6 | expectType(phpUnserialize('d:13.37;')); 7 | expectType(phpUnserialize('b:1;')); 8 | expectType(phpUnserialize('N;')); 9 | expectType(phpUnserialize('a:2:{i:0;s:5:"hello";i:1;s:5:"world";}')); 10 | expectType(phpUnserialize('a:2:{s:5:"hello";i:0;s:5:"world";i:1;}')); 11 | expectType(phpUnserialize('O:5:"blank":0:{}')); 12 | --------------------------------------------------------------------------------